Challenge
Find the Password (easy) 附件是一个 Windows 控制台程序,要求输入密码并校验;只有把正确口令输入进去,程序才会把 HTS 要提交的密码回显出来。
附件 app6win.zip 里只有一个 app6win.exe。是用 MSVC 编译的原生 PE,入口处有一段自解密壳:先把 .text 改成可写,XOR 还原 204 字节被加密的代码,再跳进去执行真正的 main。密码校验就在这段被还原出来的代码里。
Solution
Recon:
file app6win.exe→PE32 executable for MS Windows 4.00 (console), Intel i386, 3 sectionsstrings -a能看到明文常量Please enter the password:、Invalid Password、The password is %s,还有一个残留的源文件名main2.exe。- 直接
strings搜不到口令,也搜不到校验逻辑对应的字符串,因为真正干活的代码被 XOR 加壳了:入口只解密 204 字节再执行。 - 静态定位壳:Ghidra 无头反编译
FUN_00401000就是解密器,参数是 VA0x4010d3、长度0x33个 dword、密钥0xbeefcabe。
Step 1: Ghidra 无头反编译整程序
DecompileAll.java 把每个函数输出为 C:
$ cd <hts-workspace>/challenges/hts-app && mkdir -p out$ /opt/ghidra/support/analyzeHeadless /tmp/ghproj app6 \ -import app6/win/app6win.exe \ -scriptPath <hts-workspace>/challenges/hts-app \ -postScript DecompileAll.java -deleteProject > out/app6_decomp.txt 2>&1$ grep -c '============' out/app6_decomp.txt92-scriptPath 必须给绝对路径,否则 Ghidra 报 Failed to find script in any script directory。
Step 2: 读解密壳
入口 entry 最终调用 FUN_00401000,它的反编译结果就是自解密逻辑:
void FUN_00401000(void){ DWORD local_2c; _MEMORY_BASIC_INFORMATION local_28; undefined *local_c; uint local_8;
VirtualQuery(&DAT_004010d3,&local_28,0x1c); VirtualProtect(&DAT_004010d3,0xd0,local_28.Protect & 0xffffffdd | 4,&local_2c); local_c = &DAT_004010d3; for (local_8 = 0; local_8 < 0x33; local_8 = local_8 + 1) { *(uint *)(&DAT_004010d3 + local_8 * 4) = *(uint *)(&DAT_004010d3 + local_8 * 4) ^ 0xbeefcabe; } func_0x004010d3(); return;}VirtualProtect 的保护标志把 local_28.Protect 与 0xffffffdd 相与再或上 4,即把所在内存页加上 PAGE_READWRITE(4),然后对 DAT_004010d3 起的 0x33 个 dword 反复 XOR 0xbeefcabe,最后 func_0x004010d3() 直接跳进刚解密的代码。 字节,覆盖 VA 0x4010d3 到 0x40119e。
Step 3: 离线重放 XOR,还原真正的 main
壳的变换是可逆的,直接对文件重放同一个 XOR 即可静态看到明文代码。完整脚本:
#!/usr/bin/env python3"""Decrypt app6win.exe's self-decrypting .text stub and dump the hidden function.
The PE entry (via FUN_00401000) VirtualProtect()s the .text page to RW, thenXORs 0x33 dwords starting at VA 0x4010d3 with 0xbeefcabe before calling into it.This script replays that XOR so the real main can be disassembled statically."""import struct
PATH = "app6win.exe"IMAGE_BASE = 0x400000# columns: name, RVA, virtual size, raw pointer, raw size (from the PE section table)SECTIONS = [ (".text", 0x1000, 0x4ae6, 0x1000, 0x5000), (".rdata", 0x6000, 0x087e, 0x6000, 0x1000), (".data", 0x7000, 0x1e44, 0x7000, 0x1000),]DECRYPT_VA = 0x4010d3XOR_KEY = 0xbeefcabeN_DWORDS = 0x3315 collapsed lines
def va_to_offset(va): rva = va - IMAGE_BASE for _, rva0, vsize, rawptr, rawsize in SECTIONS: if rva0 <= rva < rva0 + max(vsize, rawsize): return rawptr + (rva - rva0) raise ValueError(f"VA {va:#x} not mapped")
def main(): data = bytearray(open(PATH, "rb").read()) off = va_to_offset(DECRYPT_VA) for i in range(N_DWORDS): p = off + i * 4 val = struct.unpack_from("<I", data, p)[0] struct.pack_into("<I", data, p, val ^ XOR_KEY) open("app6_plain.exe", "wb").write(data) print(f"decrypted {N_DWORDS} dwords at VA {DECRYPT_VA:#x} (file off {off:#x})")
if __name__ == "__main__": main()$ python3 decrypt_app6.pydecrypted 51 dwords at VA 0x4010d3 (file off 0x10d3)用 objdump 反汇编还原后的代码:
$ objdump -d -M intel --start-address=0x4010d3 --stop-address=0x4011ef app6_plain.exe004010d3 <.text+0xd3>: 4010d3: 55 push ebp 4010d4: 8b ec mov ebp,esp 4010d6: 83 ec 2c sub esp,0x2c 4010d9: c7 45 f0 63 61 6c 0a mov DWORD PTR [ebp-0x10],0xa6c6163 ; "cal\n" 4010e0: c7 45 f4 6d 61 67 69 mov DWORD PTR [ebp-0xc],0x6967616d ; "magi" 4010e7: 68 40 70 40 00 push 0x407040 ; "Please enter the password:" 4010ec: e8 47 05 00 00 call 0x401638 4010f1: 83 c4 04 add esp,0x4 4010f4: 6a 10 push 0x10 4010f6: 6a 00 push 0x0 4010f8: 8d 45 d8 lea eax,[ebp-0x28] 4010fb: 50 push eax 4010fc: e8 df 04 00 00 call 0x4015e0 ; memset(input,0,16) 401101: 83 c4 0c add esp,0xc 401104: c7 45 ec 00 00 00 00 mov DWORD PTR [ebp-0x14],0x0 ; len = 0 40110b: c7 45 e8 00 00 00 00 mov DWORD PTR [ebp-0x18],0x0 401112: 8b 0d 8c 70 40 00 mov ecx,DWORD PTR ds:0x40708c 401118: 83 e9 01 sub ecx,0x1 40111b: 89 0d 8c 70 40 00 mov DWORD PTR ds:0x40708c,ecx55 collapsed lines
401121: 83 3d 8c 70 40 00 00 cmp DWORD PTR ds:0x40708c,0x0 401128: 7c 22 jl 0x40114c 40112a: 8b 15 88 70 40 00 mov edx,DWORD PTR ds:0x407088 401130: 0f be 02 movsx eax,BYTE PTR [edx] 401133: 25 ff 00 00 00 and eax,0xff 401138: 89 45 d4 mov DWORD PTR [ebp-0x2c],eax 40113b: 8b 0d 88 70 40 00 mov ecx,DWORD PTR ds:0x407088 401141: 83 c1 01 add ecx,0x1 401144: 89 0d 88 70 40 00 mov DWORD PTR ds:0x407088,ecx 40114a: eb 10 jmp 0x40115c 40114c: 68 88 70 40 00 push 0x407088 401151: e8 e9 02 00 00 call 0x40143f 401156: 83 c4 04 add esp,0x4 401159: 89 45 d4 mov DWORD PTR [ebp-0x2c],eax 40115c: 8a 55 d4 mov dl,BYTE PTR [ebp-0x2c] 40115f: 88 55 fc mov BYTE PTR [ebp-0x4],dl 401162: 8b 45 ec mov eax,DWORD PTR [ebp-0x14] 401165: 8a 4d fc mov cl,BYTE PTR [ebp-0x4] 401168: 88 4c 05 d8 mov BYTE PTR [ebp+eax*1-0x28],cl 40116c: 8b 55 ec mov edx,DWORD PTR [ebp-0x14] 40116f: 83 c2 01 add edx,0x1 401172: 89 55 ec mov DWORD PTR [ebp-0x14],edx ; len++ 401175: 0f be 45 fc movsx eax,BYTE PTR [ebp-0x4] 401179: 83 f8 0a cmp eax,0xa 40117c: 74 0e je 0x40118c 40117e: 0f be 4d fc movsx ecx,BYTE PTR [ebp-0x4] 401182: 85 c9 test ecx,ecx 401184: 74 06 je 0x40118c 401186: 83 7d ec 10 cmp DWORD PTR [ebp-0x14],0x10 40118a: 72 86 jb 0x401112 ; 最多 16 字节 40118c: 8d 55 d8 lea edx,[ebp-0x28] 40118f: 89 55 f8 mov DWORD PTR [ebp-0x8],edx ; ptr = input 401192: c7 45 e8 00 00 00 00 mov DWORD PTR [ebp-0x18],0x0 401199: eb 09 jmp 0x4011a4 40119b: 8b 45 e8 mov eax,DWORD PTR [ebp-0x18] 40119e: 83 c0 04 add eax,0x4 4011a1: 89 45 e8 mov DWORD PTR [ebp-0x18],eax 4011a4: 83 7d e8 08 cmp DWORD PTR [ebp-0x18],0x8 4011a8: 73 2e jae 0x4011d8 4011aa: 8b 4d e8 mov ecx,DWORD PTR [ebp-0x18] 4011ad: c1 e9 02 shr ecx,0x2 4011b0: 8b 55 ec mov edx,DWORD PTR [ebp-0x14] ; edx = len 4011b3: 2b 55 e8 sub edx,DWORD PTR [ebp-0x18] ; edx = len - i 4011b6: c1 ea 02 shr edx,0x2 ; edx = (len-i)/4 4011b9: 8b 45 f8 mov eax,DWORD PTR [ebp-0x8] 4011bc: 8b 0c 88 mov ecx,DWORD PTR [eax+ecx*4] ; input_dword[i/4] 4011bf: 3b 4c 95 ec cmp ecx,DWORD PTR [ebp+edx*4-0x14] ; const_dword[(len-i)/4] 4011c3: 74 11 je 0x4011d6 4011c5: 68 5c 70 40 00 push 0x40705c ; "Invalid Password" 4011ca: e8 3f 02 00 00 call 0x40140e 4011cf: 83 c4 04 add esp,0x4 4011d2: 33 c0 xor eax,eax 4011d4: eb 15 jmp 0x4011eb 4011d6: eb c3 jmp 0x40119b 4011d8: 8d 55 d8 lea edx,[ebp-0x28] 4011db: 52 push edx 4011dc: 68 70 70 40 00 push 0x407070 ; "The password is %s" 4011e1: e8 28 02 00 00 call 0x40140e 4011e6: 83 c4 08 add esp,0x8 4011e9: 33 c0 xor eax,eax 4011eb: 8b e5 mov esp,ebp 4011ed: 5d pop ebp 4011ee: c3 retStep 4: 逆推比较逻辑
观察 → 推理:
0x4010d9和0x4010e0把两个常量 dword 写进栈:[ebp-0x10] = 0xa6c6163(小端字节63 61 6c 0a→"cal\n")、[ebp-0xc] = 0x6967616d(6d 61 67 69→"magi")。- 输入缓冲在
[ebp-0x28],用memset清 16 字节;读取循环把每个字符写进去,遇到\n、NUL 或长度到0x10就停,[ebp-0x14]记录实际长度len。 - 校验循环
i = 0, 4(cmp [ebp-0x18],0x8; jae done说明只在i<8时比较,正好两个 dword):- 取
input_dword[i/4], - 和栈上
[ebp-0x14 + 4*((len-i)/4)]比较。
- 取
[ebp-0x14]往下正是len、"cal\n"、"magi"三个 dword。若输入长度len = 8:i=0:比较input_dword[0]与[ebp-0x14 + 4*(8/4)] = [ebp-0xc] = "magi";i=4:比较input_dword[1]与[ebp-0x14 + 4*((8-4)/4)] = [ebp-0x10] = "cal\n"。
所以输入的第 0 个 dword 要等于 "magi"、第 1 个 dword 要等于 "cal\n",拼起来就是 7 个字母加一个换行。字符串常量在内存里的顺序是反的(cal\n 在前、magi 在后),靠索引 (len-i)/4 倒着取,拼回来才是 magical。
Step 5: 本地运行验证
程序是控制台程序,直接在 Wine 里跑即可(无需图形界面):
$ printf 'magical\n' | wine app6/win/app6win.exe 2>/dev/null | tr -d '\r'Please enter the password:The password is magical
$ printf 'wrong\n' | wine app6/win/app6win.exe 2>/dev/null | tr -d '\r'Please enter the password:Invalid Password程序在成功分支用 printf("The password is %s", input) 把答案回显出来,所以 The password is magical 这一行就是自证的验证结果。
magical