HackThisSite - Application Mission 4

Challenge

Press the Button. (easy)

按下按钮(easy)。

包内只有一个 app4win.exe(24 KB)。界面上有两个按钮,鼠标指到哪个,哪个就被置灰、另一个恢复可用,所以两个按钮都点不到。

Solution

Recon:

  • file app4win.exePE32 executable for MS Windows 4.00 (GUI), Intel i386, 3 sections。导入表里只有 MSVBVM60.DLL 一个 DLL,代码在 .text(VA 0x401000,大小 0x2af8),没有 overlay。这是 VB6 编译成 native code 的程序,不是 p-code。
  • 导入表里直接能看到密码是怎么被拼出来的痕迹 —— 调用 rtcVarBstrFromAnsi(把 ANSI 字符转成 BSTR)和 __vbaVarCat(拼接 Variant 字符串):
1
2
3
4
5
6
7
8
9
10
11
$ objdump -x -M intel app4win.exe | sed -n '/The Import Tables/,$p'
The Import Tables (interpreted .text section contents)
00003814 0000383c ffffffff ffffffff 000038d4 00001000

DLL Name: MSVBVM60.DLL
vma: Ordinal Hint Member-Name Bound-To
00001054 608 <none> <none> 660e544f ; rtcVarBstrFromAnsi
0000105c <none> 0000 __vbaVarCat 660ea219
00001064 <none> 0000 __vbaNew2 6601c28c
0000108c <none> 0000 __vbaFreeObj 66024fd4
00001090 <none> 0000 __vbaFreeStr 660246fb
  • 版本信息资源还留着编译时的工程名,确认真的是 VB6:
1
2
3
4
5
$ strings -el app4win.exe | grep -iE 'challenge|project|app'
@*\AC:\Program Files\Microsoft Visual Studio\VB98\Projects\Challenge\c5Project1.vbp
c5Project1
AppChallenge
AppChallenge.exe
  • 密码不在字符串表里grep -a -c daytona app4win.exe0,连 Password 这个词都搜不到。和同批 app1 一样,是运行时逐字符生成的,必须反汇编重建。

Step 1: VB6 事件分发表

VB6 native 程序给每个控件事件编一个 id,进入事件时用一长串 sub dword ptr [esp+4], id + jmp handler 分发。把这套 81 6c 24 04 <id> e9 <rel32> 模式扫出来:

1
$ objdump -d -M intel app4win.exe > out/app4.asm

核心分发在这几行(VA 0x40247c 起):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
40247c: 81 6c 24 04 33 00 00 00   sub    DWORD PTR [esp+0x4],0x33
402484: e9 07 01 00 00 jmp 0x402590 ; 建主窗口 (Form_Load)
402489: 81 6c 24 04 37 00 00 00 sub DWORD PTR [esp+0x4],0x37
402491: e9 ba 02 00 00 jmp 0x402750
402496: 81 6c 24 04 37 00 00 00 sub DWORD PTR [esp+0x4],0x37
40249e: e9 6d 03 00 00 jmp 0x402810 ; 上按钮 MouseMove
4024a3: 81 6c 24 04 3b 00 00 00 sub DWORD PTR [esp+0x4],0x3b
4024ab: e9 60 04 00 00 jmp 0x402910
4024b0: 81 6c 24 04 3b 00 00 00 sub DWORD PTR [esp+0x4],0x3b
4024b8: e9 13 05 00 00 jmp 0x4029d0 ; 下按钮 MouseMove
4024bd: 81 6c 24 04 37 00 00 00 sub DWORD PTR [esp+0x4],0x37
4024c5: e9 06 06 00 00 jmp 0x402ad0 ; 上按钮 Click
4024ca: 81 6c 24 04 3b 00 00 00 sub DWORD PTR [esp+0x4],0x3b
4024d2: e9 99 0c 00 00 jmp 0x403170 ; 下按钮 Click

event id 0x33 是建窗口,0x37 指上按钮、0x3b 指下按钮;每个按钮各有一对「MouseMove / Click」处理函数。

Step 2: 确认「点不到」的原因

上按钮的 MouseMove 处理函数 0x402810 里,第一步就是对这个按钮自己调 EnableWindow(hwnd, FALSE)

1
2
3
4
5
40286d: 8b f8                mov    edi,eax
40286f: 6a 00 push 0x0 ; FALSE
402871: 57 push edi ; hwnd = 上按钮
402872: 8b 0f mov ecx,DWORD PTR [edi]
402874: ff 91 8c 00 00 00 call DWORD PTR [ecx+0x8c] ; EnableWindow 包装

0x4029d0(下按钮 MouseMove)是镜像逻辑:禁用下按钮、启用上按钮。于是鼠标一进入某个按钮它就自我禁用,Click 永远触发不了。真正的密码逻辑在 Click 处理函数 0x402ad0 / 0x403170,不是被禁用的那一边。

Step 3: 从 Click 处理函数重建密码

0x402ad0 逐字符调用 rtcVarBstrFromAnsi 生成单字符 BSTR:

1
2
3
4
5
6
7
8
9
10
11
12
13
402b12: 8b 3d 54 10 40 00    mov    edi,DWORD PTR ds:0x401054   ; rtcVarBstrFromAnsi
402b1d: 6a 50 push 0x50 ; 'P'
402b1f: 50 push eax
; 中间是 42 条 Variant 槽位零初始化 mov DWORD PTR [ebp-0x..],esi,与字符重建无关
402c19: ff d7 call edi
402c1b: 8d 4d c8 lea ecx,[ebp-0x38]
402c1e: 6a 61 push 0x61 ; 'a'
402c20: 51 push ecx
402c21: ff d7 call edi
; 中间是 's' 's' 'w' 'o' 'r' 'd' ' ' 'i' 's' ' ' 这 10 个字符各一组 lea/push/call 三连,模式与上同
402ce9: 6a 27 push 0x27 ; '\''
402ceb: 52 push edx
402cec: ff d7 call edi

每个字符的 immediate 按地址顺序连起来就是消息串。

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
#!/usr/bin/env python3
"""Recover the site password built by HackThisSite app4win.exe (VB6 native).

The exe is compiled to VB6 *native code*, so the password is not a plain
string constant. Each character is turned into a BSTR by the runtime
``rtcVarBstrFromAnsi`` and the pieces are glued together by ``__vbaVarCat``.
The code that does this lives inside the button click handlers:

mov edi, ds:rtcVarBstrFromAnsi ; 8b 3d 54 10 40 00
; (此处插入 Variant 槽位零初始化指令,把 push 0x50 与首次 call 隔开)
push 0x50 ; 'P' (deliberately spaced away from
... ; the first call by the VB
call edi ; variant zero-initialisation)
lea ecx, [ebp-0x38]
push 0x61 ; 'a'
push ecx
call edi
; (其后为 's' 's' 'w' 'o' 'r' 'd' ... 各字符同模式的 lea/push/call 序列)

Both the button-click handler at 0x402AD0 and the one at 0x403170 build the
same message. This script walks every ``rtcVarBstrFromAnsi`` chain and prints
the immediates in address order, which is exactly the string handed to the
message box.

usage: ./extract_app4_password.py <exe>
"""
import struct
import sys

RTC = bytes.fromhex('8b3d54104000') # mov edi, ds:[0x401054] rtcVarBstrFromAnsi
CAT = bytes.fromhex('8b3d5c104000') # mov edi, ds:[0x40105c] __vbaVarCat
CALL_EDI = bytes.fromhex('ffd7') # call edi
DISPATCH = bytes.fromhex('816c2404') # sub dword ptr [esp+4], imm32
JMP_REL = 0xe9


def text(data):
""".text starts at VA 0x401000 / file offset 0x1000 (fixed for this exe)."""
return data, 0x1000, 0x401000


def chains(data, base_file, base_va):
"""Yield (va, [chars]) for every rtcVarBstrFromAnsi character chain."""
pos = 0
while True:
start = data.find(RTC, pos)
if start < 0:
break
end = data.find(CAT, start)
if end < 0:
end = len(data)
chain_start = start
chars = []
for _ in range(64):
call = data.find(CALL_EDI, start, end)
if call < 0:
break
# the character immediate sits just before the two pushes that
# feed the call; scan back for a `push imm8` (6a XX)
off = call
while off > start:
if data[off] == 0x6a:
val = data[off + 1]
if 0x20 <= val <= 0x7e:
chars.append((base_va + (off - base_file), val))
break
off -= 1
start = call + 2
if chars:
yield base_va + (chain_start - base_file), chars
pos = end + 6


def dispatch(data, base_file, base_va):
"""Print the event-id -> handler switch that drives the buttons."""
pos = 0
out = []
while True:
i = data.find(DISPATCH, pos)
if i < 0:
break
ev = struct.unpack_from('<I', data, i + 4)[0]
if data[i + 8] == JMP_REL:
rel = struct.unpack_from('<i', data, i + 9)[0]
target = base_va + (i - base_file) + 13 + rel
out.append((base_va + (i - base_file), ev, target))
pos = i + 13
return out


def main():
path = sys.argv[1]
data = open(path, 'rb').read()
data, base_file, base_va = text(data)

print(f'[*] {path}')
print('[*] event dispatch (event id -> handler)')
for va, ev, target in dispatch(data, base_file, base_va):
print(f' {va:#08x} id {ev:#04x} -> {target:#08x}')

for va, chars in chains(data, base_file, base_va):
s = ''.join(chr(v) for _, v in chars)
print(f'\n[*] rtcVarBstrFromAnsi chain at {va:#08x}: {len(chars)} chars')
for cva, val in chars:
print(f' {cva:#08x} push {val:#04x} {chr(val)!r}')
print(f' => {s!r}')


if __name__ == '__main__':
main()
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
$ cd <hts-workspace> && uv run python challenges/hts-app/app4/extract_app4_password.py challenges/hts-app/app4/win/app4win.exe
[*] challenges/hts-app/app4/win/app4win.exe
[*] event dispatch (event id -> handler)
0x40247c id 0x33 -> 0x402590
0x402489 id 0x37 -> 0x402750
0x402496 id 0x37 -> 0x402810
0x4024a3 id 0x3b -> 0x402910
0x4024b0 id 0x3b -> 0x4029d0
0x4024bd id 0x37 -> 0x402ad0
0x4024ca id 0x3b -> 0x403170

[*] rtcVarBstrFromAnsi chain at 0x402b12: 21 chars
0x402b1d push 0x50 'P'
0x402c1e push 0x61 'a'
0x402c26 push 0x73 's'
0x402c2e push 0x73 's'
0x402c39 push 0x77 'w'
0x402c44 push 0x6f 'o'
0x402c4f push 0x72 'r'
0x402c5a push 0x64 'd'
0x402c65 push 0x20 ' '
0x402c70 push 0x69 'i'
0x402c7b push 0x73 's'
0x402c86 push 0x20 ' '
0x402c91 push 0x27 "'"
0x402c9c push 0x64 'd'
0x402ca7 push 0x61 'a'
0x402cb2 push 0x79 'y'
0x402cbd push 0x74 't'
0x402cc8 push 0x6f 'o'
0x402cd3 push 0x6e 'n'
0x402cde push 0x61 'a'
0x402ce9 push 0x27 "'"
=> "Password is 'daytona'"

下按钮的 Click 处理函数 0x403170 建的是同一个串(push 0x50push 0x27,21 个字符),两条链互相印证。0x402cee 起的 __vbaVarCat 序列把这段 21 字符的串接起来,最后 __vbaNew20x401064)造出弹窗对象显示:

1
2
3
4
5
402d00: ff 15 64 10 40 00    call   DWORD PTR ds:0x401064   ; __vbaNew2
402d50: 8b 3d 5c 10 40 00 mov edi,DWORD PTR ds:0x40105c ; __vbaVarCat
402d8c: ff d7 call edi
402d97: ff d7 call edi
; 其后是一长串同模式的 lea/push/call edi,把 21 个字符 BSTR 逐段拼成完整消息串

Step 4: Patch 让鼠标「按下」上按钮

既然上按钮的 MouseMove(0x402810)会自我禁用,把分发表里指向它的那个 jmp 改成指向真正的 Click 处理函数 0x402ad0 即可 —— 鼠标一进按钮就等于按下了它。要改的指令在文件偏移 0x249e(VA 0x40249e):

1
2
$ xxd -s 0x2496 -l 16 app4win.exe
00002496: 816c 2404 3700 0000 e96d 0300 0081 6c24 .l$.7....m....l$

e9 6d 03 00 00jmp 0x402810rel32 相对指令末尾 0x4024a3 计算,新目标 0x402ad0 对应 rel = 0x402ad0 - 0x4024a3 = 0x62d

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
#!/usr/bin/env python3
"""Patch app4win.exe so the upper button's MouseMove jumps straight to the
click handler that builds the password.

The event dispatcher uses one shared stub per event id; the button events are:

file off 0x247C sub [esp+4], 0x33 jmp 0x402590 Form_Load / create
file off 0x2489 sub [esp+4], 0x37 jmp 0x402750
file off 0x2496 sub [esp+4], 0x37 jmp 0x402810 upper Button MouseMove
file off 0x24A3 sub [esp+4], 0x3B jmp 0x402910
file off 0x24B0 sub [esp+4], 0x3B jmp 0x4029D0 lower Button MouseMove
file off 0x24BD sub [esp+4], 0x37 jmp 0x402AD0 upper Button Click
file off 0x24CA sub [esp+4], 0x3B jmp 0x403170 lower Button Click

Every time the pointer enters the upper button the MouseMove stub at 0x402810
disables that very button (EnableWindow(hwnd, FALSE)), so it can never be
clicked. Redirecting its `jmp` (VA 0x40249E, file offset 0x249E) from
0x402810 to the real click handler 0x402AD0 makes the mouse simply "press" it
and the password message box pops up.

usage: ./patch_app4.py <in.exe> <out.exe>
"""
import shutil
import struct
import sys

JMP_FILE_OFF = 0x249E # `e9 6d 03 00 00` jmp 0x402810
JMP_END_VA = 0x4024A3 # end of the instruction; rel32 is relative to this
NEW_TARGET = 0x402AD0 # loc_402AD0: the upper button's click handler


def main():
src, dst = sys.argv[1], sys.argv[2]
shutil.copyfile(src, dst)
with open(dst, 'r+b') as f:
f.seek(JMP_FILE_OFF)
old = f.read(5)
assert old[0] == 0xE9, f'expected a jmp rel32, got {old.hex()}'
rel = NEW_TARGET - JMP_END_VA
f.seek(JMP_FILE_OFF)
f.write(b'\xE9' + struct.pack('<i', rel))
print(f'{src} -> {dst}')
new = b'\xE9' + struct.pack('<i', rel)
print(f' {JMP_FILE_OFF:#06x}: jmp 0x402810 => jmp {NEW_TARGET:#x} ({old.hex()} -> {new.hex()})')


if __name__ == '__main__':
main()
1
2
3
$ cd <hts-workspace> && uv run python challenges/hts-app/app4/patch_app4.py challenges/hts-app/app4/win/app4win.exe challenges/hts-app/app4/out/app4win_patched.exe
challenges/hts-app/app4/win/app4win.exe -> challenges/hts-app/app4/out/app4win_patched.exe
0x249e: jmp 0x402810 => jmp 0x402ad0 (e96d030000 -> e92d060000)

反汇编验证补丁生效:

1
2
3
4
$ objdump -d -M intel --start-address=0x402496 --stop-address=0x4024a3 app4win_patched.exe
402496: 81 6c 24 04 37 00 00 sub DWORD PTR [esp+0x4],0x37
40249d: 00
40249e: e9 2d 06 00 00 jmp 0x402ad0
daytona