Challenge
Press the Button. (easy)
按下按钮(easy)。
包内只有一个 app4win.exe(24 KB)。界面上有两个按钮,鼠标指到哪个,哪个就被置灰、另一个恢复可用,所以两个按钮都点不到。
Solution
Recon:
file app4win.exe→PE32 executable for MS Windows 4.00 (GUI), Intel i386, 3 sections。导入表里只有MSVBVM60.DLL一个 DLL,代码在.text(VA0x401000,大小0x2af8),没有 overlay。这是 VB6 编译成 native code 的程序,不是 p-code。- 导入表里直接能看到密码是怎么被拼出来的痕迹,即调用
rtcVarBstrFromAnsi(把 ANSI 字符转成 BSTR)和__vbaVarCat(拼接 Variant 字符串):
$ 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:
$ strings -el app4win.exe | grep -iE 'challenge|project|app'@*\AC:\Program Files\Microsoft Visual Studio\VB98\Projects\Challenge\c5Project1.vbpc5Project1AppChallengeAppChallenge.exe- 密码不在字符串表里:
grep -a -c daytona app4win.exe→0,连Password这个词都搜不到。和同批 app1 一样,是运行时逐字符生成的,必须反汇编重建。
Step 1: VB6 事件分发表
VB6 native 程序给每个控件事件编一个 id,进入事件时用一长串 sub dword ptr [esp+4], id + jmp handler 分发。把这套 81 6c 24 04 <id> e9 <rel32> 模式扫出来:
$ objdump -d -M intel app4win.exe > out/app4.asm核心分发在这几行(VA 0x40247c 起):
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 ; 下按钮 Clickevent id 0x33 是建窗口,0x37 指上按钮、0x3b 指下按钮;每个按钮各有一对「MouseMove / Click」处理函数。
Step 2: 确认「点不到」的原因
上按钮的 MouseMove 处理函数 0x402810 里,第一步就是对这个按钮自己调 EnableWindow(hwnd, FALSE):
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:
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 按地址顺序连起来就是消息串。
#!/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 plainstring 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 the82 collapsed lines
same message. This script walks every ``rtcVarBstrFromAnsi`` chain and printsthe immediates in address order, which is exactly the string handed to themessage box.
usage: ./extract_app4_password.py <exe>"""import structimport sys
RTC = bytes.fromhex('8b3d54104000') # mov edi, ds:[0x401054] rtcVarBstrFromAnsiCAT = bytes.fromhex('8b3d5c104000') # mov edi, ds:[0x40105c] __vbaVarCatCALL_EDI = bytes.fromhex('ffd7') # call ediDISPATCH = bytes.fromhex('816c2404') # sub dword ptr [esp+4], imm32JMP_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()$ 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 0x50 … push 0x27,21 个字符),两条链互相印证。0x402cee 起的 __vbaVarCat 序列把这段 21 字符的串接起来,最后 __vbaNew2(0x401064)造出弹窗对象显示:
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):
$ xxd -s 0x2496 -l 16 app4win.exe00002496: 816c 2404 3700 0000 e96d 0300 0081 6c24 .l$.7....m....l$e9 6d 03 00 00 是 jmp 0x402810;rel32 相对指令末尾 0x4024a3 计算,新目标 0x402ad0 对应 。
#!/usr/bin/env python3"""Patch app4win.exe so the upper button's MouseMove jumps straight to theclick 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 0x402810disables that very button (EnableWindow(hwnd, FALSE)), so it can never beclicked. Redirecting its `jmp` (VA 0x40249E, file offset 0x249E) from0x402810 to the real click handler 0x402AD0 makes the mouse simply "press" itand the password message box pops up.
20 collapsed lines
usage: ./patch_app4.py <in.exe> <out.exe>"""import shutilimport structimport sys
JMP_FILE_OFF = 0x249E # `e9 6d 03 00 00` jmp 0x402810JMP_END_VA = 0x4024A3 # end of the instruction; rel32 is relative to thisNEW_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()$ 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.exechallenges/hts-app/app4/win/app4win.exe -> challenges/hts-app/app4/out/app4win_patched.exe 0x249e: jmp 0x402810 => jmp 0x402ad0 (e96d030000 -> e92d060000)反汇编验证补丁生效:
$ 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 0x402ad0daytona