Challenge
An application stores its password on the stack and compares it against user input. 应用程序要求输入密码进行验证,正确密码以常量的形式写在函数内部,运行时复制到栈上,再与用户输入逐字节比较。
附件含两个版本:app5win.zip(Windows 控制台 exe Live_Application_5.exe)和 app5unix.tar.gz(ELF 可执行文件 app5unix)。官方暗示两个平台的密码一致,Linux 版没有 strip,直接从本地 ELF 入手最快。
Solution
Recon:
file app5unix→ELF 32-bit LSB pie executable, Intel i386, not stripped,源码文件名残留在符号表里:app5win.c(Linux 版由同一份 C 源码编译)。strings -a app5unix里看不到完整密码,但有 4 个可疑短串powe、rtri、ppin,以及Please enter the password:、Invalid Password、The password is %s。powe/rtri/ppin恰好是 4 字节对齐的 ASCII 片段:它们是被拆成 4 个 dword、以立即数形式mov进栈的常量,反汇编后按写入顺序拼回来即可。
Step 1: 反汇编 main,定位常量与比较循环
函数符号没去掉,直接反汇编 main:
$ objdump -d -M intel app5unix --section=.text | sed -n '/<main>:/,/^$/p'main 的完整反汇编如下(含 PIE 序言与栈保护样板):
124d: f3 0f 1e fb endbr32 1251: 8d 4c 24 04 lea ecx,[esp+0x4] 1255: 83 e4 f0 and esp,0xfffffff0 1258: ff 71 fc push DWORD PTR [ecx-0x4] 125b: 55 push ebp 125c: 89 e5 mov ebp,esp 125e: 53 push ebx 125f: 51 push ecx 1260: 83 ec 50 sub esp,0x50 1263: e8 e8 fe ff ff call 1150 <__x86.get_pc_thunk.bx> 1268: 81 c3 60 2d 00 00 add ebx,0x2d60 126e: 89 c8 mov eax,ecx 1270: 8b 40 04 mov eax,DWORD PTR [eax+0x4] 1273: 89 45 b4 mov DWORD PTR [ebp-0x4c],eax 1276: 65 a1 14 00 00 00 mov eax,gs:0x14 ; stack canary 127c: 89 45 f4 mov DWORD PTR [ebp-0xc],eax 127f: 31 c0 xor eax,eax 1281: c7 45 d4 67 0a 00 00 mov DWORD PTR [ebp-0x2c],0xa67 ; [ebp-0x2c] = 0xa67 -> 67 0a 00 00 ('g' '\n' 0 0) 1288: c7 45 d8 70 70 69 6e mov DWORD PTR [ebp-0x28],0x6e697070 ; [ebp-0x28] = "ppin" 128f: c7 45 dc 72 74 72 69 mov DWORD PTR [ebp-0x24],0x69727472 ; [ebp-0x24] = "rtri"65 collapsed lines
1296: c7 45 e0 70 6f 77 65 mov DWORD PTR [ebp-0x20],0x65776f70 ; [ebp-0x20] = "powe" 129d: 83 ec 0c sub esp,0xc 12a0: 8d 83 40 e0 ff ff lea eax,[ebx-0x1fc0] 12a6: 50 push eax 12a7: e8 34 fe ff ff call 10e0 <puts@plt> 12ac: 83 c4 10 add esp,0x10 12af: 83 ec 04 sub esp,0x4 12b2: 6a 10 push 0x10 ; memset(input, 0, 16), input 在 [ebp-0x1c] 12b4: 6a 00 push 0x0 12b6: 8d 45 e4 lea eax,[ebp-0x1c] 12b9: 50 push eax 12ba: e8 41 fe ff ff call 1100 <memset@plt> 12bf: 83 c4 10 add esp,0x10 12c2: c7 45 cc 00 00 00 00 mov DWORD PTR [ebp-0x34],0x0 12c9: c7 45 c8 00 00 00 00 mov DWORD PTR [ebp-0x38],0x0 12d0: c7 45 c4 00 00 00 00 mov DWORD PTR [ebp-0x3c],0x0 12d7: e8 e4 fd ff ff call 10c0 <getchar@plt> ; c = getchar() 12dc: 88 45 c3 mov BYTE PTR [ebp-0x3d],al 12df: 8b 45 cc mov eax,DWORD PTR [ebp-0x34] 12e2: 8d 50 01 lea edx,[eax+0x1] 12e5: 89 55 cc mov DWORD PTR [ebp-0x34],edx 12e8: 0f b6 55 c3 movzx edx,BYTE PTR [ebp-0x3d] 12ec: 88 54 05 e4 mov BYTE PTR [ebp+eax*1-0x1c],dl 12f0: 80 7d c3 0a cmp BYTE PTR [ebp-0x3d],0xa 12f4: 74 0c je 1302 <main+0xb5> 12f6: 80 7d c3 00 cmp BYTE PTR [ebp-0x3d],0x0 12fa: 74 06 je 1302 <main+0xb5> 12fc: 83 7d cc 0f cmp DWORD PTR [ebp-0x34],0xf ; len < 16 才继续 1300: 76 d5 jbe 12d7 <main+0x8a> 1302: 8d 45 e4 lea eax,[ebp-0x1c] 1305: 89 45 d0 mov DWORD PTR [ebp-0x30],eax 1308: c7 45 c8 00 00 00 00 mov DWORD PTR [ebp-0x38],0x0 ; i = 0 130f: c7 45 c4 03 00 00 00 mov DWORD PTR [ebp-0x3c],0x3 ; j = 3 1316: eb 40 jmp 1358 <main+0x10b> 1318: 8b 45 c8 mov eax,DWORD PTR [ebp-0x38] ; 取 input_dword[i>>2] 与 const_dword[j] 比较 131b: c1 e8 02 shr eax,0x2 131e: 8d 14 85 00 00 00 00 lea edx,[eax*4+0x0] 1325: 8b 45 d0 mov eax,DWORD PTR [ebp-0x30] 1328: 01 d0 add eax,edx 132a: 8b 10 mov edx,DWORD PTR [eax] 132c: 8b 45 c4 mov eax,DWORD PTR [ebp-0x3c] 132f: 8b 44 85 d4 mov eax,DWORD PTR [ebp+eax*4-0x2c] 1333: 39 c2 cmp edx,eax 1335: 74 19 je 1350 <main+0x103> 1337: 83 ec 0c sub esp,0xc ; 不等 -> printf("Invalid Password") 133a: 8d 83 5b e0 ff ff lea eax,[ebx-0x1fa5] 1340: 50 push eax 1341: e8 6a fd ff ff call 10b0 <printf@plt> 1346: 83 c4 10 add esp,0x10 1349: b8 00 00 00 00 mov eax,0x0 134e: eb 29 jmp 1379 <main+0x12c> 1350: 83 45 c8 04 add DWORD PTR [ebp-0x38],0x4 ; i += 4 1354: 83 6d c4 01 sub DWORD PTR [ebp-0x3c],0x1 ; j -= 1 1358: 83 7d c8 0c cmp DWORD PTR [ebp-0x38],0xc ; while (i <= 12) 135c: 76 ba jbe 1318 <main+0xcb> 135e: 83 ec 08 sub esp,0x8 ; 全等 -> printf("The password is %s", input) 1361: 8d 45 e4 lea eax,[ebp-0x1c] 1364: 50 push eax 1365: 8d 83 6c e0 ff ff lea eax,[ebx-0x1f94] 136b: 50 push eax 136c: e8 3f fd ff ff call 10b0 <printf@plt> 1371: 83 c4 10 add esp,0x10 1374: b8 00 00 00 00 mov eax,0x0 1379: 8b 4d f4 mov ecx,DWORD PTR [ebp-0xc] 137c: 65 33 0d 14 00 00 00 xor ecx,DWORD PTR gs:0x14 1383: 74 05 je 138a <main+0x13d> 1385: e8 96 00 00 00 call 1420 <__stack_chk_fail_local> 138a: 8d 65 f8 lea esp,[ebp-0x8] 138d: 59 pop ecx 138e: 5b pop ebx 138f: 5d pop ebp 1390: 8d 61 fc lea esp,[ecx-0x4] 1393: c3 ret0x12810x1296 把 16 字节的正确密码常量分 4 个 dword 写进 [ebp-0x2c]..[ebp-0x20];0x12d7 起是一个带长度上限的 getchar 循环,把每个字符写进 [ebp-0x1c + i],遇到 \n(0xa)、NUL 或长度超过 15 就停;0x13180x135c 是比较循环,i 从 0 每次 +4、j 从 3 每次 -1。
Step 2: 从校验循环逆推密码
观察 → 推理:
- 常量区在栈上是
[ebp-0x2c] = 0xa67、[ebp-0x28] = "ppin"、[ebp-0x24] = "rtri"、[ebp-0x20] = "powe"。 - 循环让
i从 0 递增、j从 3 递减,比较的是input_dword[i/4] == const_dword[j]。也就是说输入的第 0/1/2/3 个 dword 要分别等于常量里第 3/2/1/0 个 dword。常量在内存里是倒序存的。 - 把 4 个 dword 按内存顺序还原成字节:
67 0a 00 00 | 70 70 69 6e | 72 74 72 69 | 70 6f 77 65→ 反过来按 dword 拼接(powe+rtri+ppin+g)得到powertripping。 - 那个
0x0a正是结尾换行:getchar循环会连同\n一起读进input[13],第 4 个 dword 比较时也把它纳入了匹配,所以输入powertripping\n能对上。
用 gdb 在常量写完、比较开始前断下,直接 dump 栈内存验证推导:
$ gdb -q -batch -ex 'set pagination off' \ -ex 'break *main+0x50' -ex 'run' \ -ex 'x/16bx $ebp-0x2c' ./app5unixBreakpoint 1, 0x5655629d in main ()0xffffbafc: 0x67 0x0a 0x00 0x00 0x70 0x70 0x69 0x6e0xffffbb04: 0x72 0x74 0x72 0x69 0x70 0x6f 0x77 0x650xffffbafc 起 16 字节即 g \n \0 \0 p p i n r t r i p o w e,按 dword 反向读就是 powe rtri ppin g\n。
Step 3: 本地运行验证
把推导出的口令喂给程序,它会自己把密码打印出来:
$ printf 'powertripping\n' | ./app5unixPlease enter the password:The password is powertripping
$ printf 'wrongpass\n' | ./app5unixPlease enter the password:Invalid Passwordpowertripping