Challenge
Find the password. (easy)
找出密码(easy)。
包内只有一个 app1win.exe(约 1.4 MB)。
Solution
Recon:
file app1win.exe→PE32 executable for MS Windows 4.00 (GUI), Intel i386;导入表里只有MSVBVM60/KERNEL32级别的系统 DLL,用户代码与字符串表不在节区里。- 这是 REALbasic(Xojo 前身) 原生编译的程序:它不把密码作为明文字符串常量保存,而是在运行时逐字符拼出来,用
mov reg, 0x73/push reg/call StringDBCSChr生成单字符,再call RuntimeAddString追加到消息串。所以strings搜不到密码本身,能搜到的只有格式串:
$ grep -a -o -E 'Contratulations[^\x00]{0,50}' app1win.exeContratulations! The password to this level is '$ grep -a -o -E 'incorrect serial[^\x00]{0,30}' app1win.exeincorrect serial number. Please re-enter.(Contratulations 的拼写错误来自原程序。)
- 用户代码和字符链在文件 overlay 里:从 file 偏移
0x153800开始,运行时映射到 VA0x55E000。Ghidra 只把 PE 节区载入到.reloc(0x55d000结束),overlay 不在映射范围,所以analyzeHeadless+DecompileAll.java执行完毕得到 0 个函数。反编译器这条路对本题无效,必须直接扫 overlay 的指令模式。
Step 1: 扫 overlay 指令模式,重建字符链
字符链的形态是固定的 mov r32, imm32 → push r32 → call rel32,每次调用生成一个字符。按地址顺序把 immediate 读出来就是被拼出来的字符串:
#!/usr/bin/env python3"""Rebuild run-time-built password strings from a Xojo/REALbasic native exe.
REALbasic never stores the level password as a plain string constant. Itbuilds it one character at a time at run time:
mov ecx, 73h ; 's' push ecx call StringDBCSChr ; -> "s" ; 每个字符重复 mov/push/call 这三步 call RuntimeAddString ; append to the message
so the password is invisible to `strings`. This script finds the`mov reg,imm32 ; push reg ; call rel32` idiom, groups call sites by calltarget, and prints the immediate characters in address order for everytarget fed printable bytes. The really long runs are the built strings.
usage: ./rebuild_schr.py <exe> [min_run_len]"""import struct76 collapsed lines
import sysfrom collections import defaultdict
def load(path): """Return (image_base, sections, overlay_file_offset, overlay_bytes).""" data = open(path, 'rb').read() e_lfanew = struct.unpack_from('<I', data, 0x3C)[0] assert data[e_lfanew:e_lfanew + 4] == b'PE\0\0' coff = e_lfanew + 4 nsec = struct.unpack_from('<H', data, coff + 2)[0] opt_size = struct.unpack_from('<H', data, coff + 16)[0] opt = coff + 20 magic = struct.unpack_from('<H', data, opt)[0] image_base = (struct.unpack_from('<I', data, opt + 28)[0] if magic == 0x10b else struct.unpack_from('<Q', data, opt + 24)[0]) sec = opt + opt_size sections = [] end = 0 for i in range(nsec): s = sec + i * 40 name = data[s:s + 8].rstrip(b'\0').decode('latin1') vsize, va, rawsize, rawptr = struct.unpack_from('<IIII', data, s + 8) sections.append((name, image_base + va, vsize, rawptr, rawsize)) end = max(end, rawptr + rawsize) # REALbasic appends its own object code + string table as a file overlay return image_base, sections, end, data[end:]
def char_chains(overlay, base_va, gap=0x120): # one chain element is ~0x9c bytes: mov imm32 + push + call SChr + setup + # call RuntimeAddString, so the SChr sites of one string sit ~150 bytes apart """Find `mov r32,imm ; push r32 ; call rel32` sites and chain their chars.""" by_target = defaultdict(list) i = 0 while True: i = overlay.find(b'\xe8', i) # call rel32 if i < 0 or i + 5 > len(overlay): break if i >= 2 and 0x50 <= overlay[i - 1] <= 0x57: # push eax..edi r = overlay[i - 1] - 0x50 if i >= 7 and overlay[i - 6] == 0xB8 + r: # mov reg, imm32 val = struct.unpack_from('<I', overlay, i - 5)[0] if val <= 0xff: rel = struct.unpack_from('<i', overlay, i + 1)[0] by_target[i + 5 + rel].append((i - 6, val)) i += 1
# per-call-site thunks: every SChr call goes through its own stub, so the # chain must be rebuilt from *all* char sites in address order, not grouped # by call target. all_sites = sorted(s for sites in by_target.values() for s in sites) runs = [] cur, prev = [], None for off, val in all_sites: if prev is not None and off - prev > gap: if cur: runs.append((0, cur)) cur = [] cur.append((off, val)) prev = off if cur: runs.append((0, cur)) return runs
def main(): path = sys.argv[1] min_len = int(sys.argv[2]) if len(sys.argv) > 2 else 8 image_base, sections, ov_off, overlay = load(path) base_va = 0x55E000 # where REALbasic maps its overlay print(f'[*] {path}') print(f'[*] overlay at file 0x{ov_off:x}, {len(overlay)} bytes, mapped at 0x{base_va:x}') for target, run in sorted(char_chains(overlay, base_va), key=lambda r: -len(r[1])): if len(run) < min_len: continue s = ''.join(chr(v) for _, v in run) print(f'\n=== run of {len(run)} chars -> {s!r} ===') for off, val in run: print(f' file {ov_off + off:#08x} overlay+{off:#06x} mov reg, {val:#04x} {chr(val)!r}')
if __name__ == '__main__': main()$ cd <hts-workspace> && uv run python challenges/hts-app/rebuild_schr.py challenges/hts-app/app1/win/app1win.exe[*] challenges/hts-app/app1/win/app1win.exe[*] overlay at file 0x153800, 84840 bytes, mapped at 0x55e000
=== run of 13 chars -> 'smashthestate' === file 0x15a7f4 overlay+0x6ff4 mov reg, 0x73 's' file 0x15a88b overlay+0x708b mov reg, 0x6d 'm' file 0x15a923 overlay+0x7123 mov reg, 0x61 'a' file 0x15a9bb overlay+0x71bb mov reg, 0x73 's' file 0x15aa53 overlay+0x7253 mov reg, 0x68 'h' file 0x15aaeb overlay+0x72eb mov reg, 0x74 't' file 0x15ab83 overlay+0x7383 mov reg, 0x68 'h' file 0x15ac1b overlay+0x741b mov reg, 0x65 'e' file 0x15acb3 overlay+0x74b3 mov reg, 0x73 's' file 0x15ad4b overlay+0x754b mov reg, 0x74 't' file 0x15ade3 overlay+0x75e3 mov reg, 0x61 'a' file 0x15ae7b overlay+0x767b mov reg, 0x74 't' file 0x15af13 overlay+0x7713 mov reg, 0x65 'e'Step 2: 合法 serial 与提示串
app1 是”输入 serial”型:程序用 dword 逐个比较输入的 serial,overlay 的串表里放着 6 个合法值(grep -a 直接可见):
$ grep -a -o -E '[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}' app1win.exe | sort -u0130-1414-5624-13410138-4411-5902-24113810-1941-5861-53517692-3349-1914-45678361-9811-5511-81349484-2341-5696-5321smashthestate