HackThisSite - Application Mission 6

Challenge

Find the Password (easy) 附件是一个 Windows 控制台程序,要求输入密码并校验;只有把正确口令输入进去,程序才会把 HTS 要提交的密码回显出来。

附件 app6win.zip 里只有一个 app6win.exe。是用 MSVC 编译的原生 PE,入口处有一段自解密壳:先把 .text 改成可写,XOR 还原 204 字节被加密的代码,再跳进去执行真正的 main。密码校验就在这段被还原出来的代码里。

Solution

Recon:

  • file app6win.exePE32 executable for MS Windows 4.00 (console), Intel i386, 3 sections
  • strings -a 能看到明文常量 Please enter the password:Invalid PasswordThe password is %s,还有一个残留的源文件名 main2.exe
  • 直接 strings 搜不到口令,也搜不到校验逻辑对应的字符串,因为真正干活的代码被 XOR 加壳了:入口只解密 204 字节再执行。
  • 静态定位壳:Ghidra 无头反编译 FUN_00401000 就是解密器,参数是 VA 0x4010d3、长度 0x33 个 dword、密钥 0xbeefcabe

Step 1: Ghidra 无头反编译整程序

DecompileAll.java 把每个函数输出为 C:

1
2
3
4
5
6
7
$ 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.txt
92

-scriptPath 必须给绝对路径,否则 Ghidra 报 Failed to find script in any script directory

Step 2: 读解密壳

入口 entry 最终调用 FUN_00401000,它的反编译结果就是自解密逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.Protect0xffffffdd 相与再或上 4,即把所在内存页加上 PAGE_READWRITE4),然后对 DAT_004010d3 起的 0x33 个 dword 反复 XOR 0xbeefcabe,最后 func_0x004010d3() 直接跳进刚解密的代码。0x33 * 4 = 0xcc 字节,覆盖 VA 0x4010d30x40119e

Step 3: 离线重放 XOR,还原真正的 main

壳的变换是可逆的,直接对文件重放同一个 XOR 即可静态看到明文代码。完整脚本:

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
#!/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, then
XORs 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 = 0x4010d3
XOR_KEY = 0xbeefcabe
N_DWORDS = 0x33


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()
1
2
$ python3 decrypt_app6.py
decrypted 51 dwords at VA 0x4010d3 (file off 0x10d3)

用 objdump 反汇编还原后的代码:

1
$ objdump -d -M intel --start-address=0x4010d3 --stop-address=0x4011ef app6_plain.exe
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
004010d3 <.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,ecx
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 ret

Step 4: 逆推比较逻辑

观察 → 推理:

  • 0x4010d90x4010e0 把两个常量 dword 写进栈:[ebp-0x10] = 0xa6c6163(小端字节 63 61 6c 0a"cal\n")、[ebp-0xc] = 0x6967616d6d 61 67 69"magi")。
  • 输入缓冲在 [ebp-0x28],用 memset 清 16 字节;读取循环把每个字符写进去,遇到 \n、NUL 或长度到 0x10 就停,[ebp-0x14] 记录实际长度 len
  • 校验循环 i = 0, 4cmp [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 里跑即可(无需图形界面):

1
2
3
4
5
6
7
$ 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