Challenge
Application Challenge 8 — Find the 6 digit code. (medium)
找出 6 位数字:程序是个 VB6 写的数字键盘小程序,输入正确的 6 位码后会回显站点密码。
Solution
app8win.exe是 VB6 原生编译程序:导入表只有MSVBVM60.DLL,且绝大多数条目是 ordinal(ordinal 632=rtcMidCharVar,即 VB6 的Mid$())。- 密码/数字都不在字符串里:ASCII 与 UTF-16 搜
password、185862等全部 0 命中;有效信息全在.text里的 UTF-16 BSTR 字面量和一串push imm8立即数里。VB6 的字符串是 UTF-16LE,必须strings -el或直接按 BSTR 结构读。 - 先做纯静态:解析 PE → 读 BSTR 字面量 → 把校验函数里所有
rtcMidCharVar调用点的立即数抽出来。静态结果先给出候选,最终结论通过运行程序读取消息框确认。
VB6 的 BSTR 字面量结构是 长度 dword + UTF-16 数据,校验值是用 Mid$(源串, n, 1) 逐个字符拼出来的,所以只要拿到源串和那串下标就能还原所有字符串:
#!/usr/bin/env python3"""Static recon script for app8win.exe (HTS Application Mission 8).
The sample is a Visual Basic 6 program compiled to native x86 code. Thisscript parses the PE, lists MSVBVM60.DLL imports (name + ordinal), locates theUTF-16 BSTR string literals VB6 stores in .text, and recovers the immediateshanded to the rtcMidCharVar calls (MSVBVM60.DLL ordinal 632, the runtimeimplementation of VB6's Mid$()) inside the digit-check routine.
Run from the directory that holds app8/win/app8win.exe."""import reimport struct
PATH = "app8/win/app8win.exe"IMAGE_BASE = 0x400000CHECK_FN = (0x406D40, 0x407740) # the button/verification routineRTC_MIDCHARVAR_IAT = 0x401040 # IAT slot, called as "call ebx"
def load():114 collapsed lines
data = open(PATH, "rb").read() pe = struct.unpack_from("<I", data, 0x3C)[0] nsec = struct.unpack_from("<H", data, pe + 6)[0] opt_size = struct.unpack_from("<H", data, pe + 20)[0] secs = [] for i in range(nsec): off = pe + 24 + opt_size + i * 40 name = data[off:off + 8].rstrip(b"\0").decode() vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", data, off + 8) secs.append((name, vaddr, vsize, rawptr, rawsize)) return data, pe, secs
def rva_of(secs, raw): for name, vaddr, vsize, rawptr, rawsize in secs: if rawptr <= raw < rawptr + rawsize: return vaddr + (raw - rawptr) raise ValueError(hex(raw))
def raw_of(secs, rva): for name, vaddr, vsize, rawptr, rawsize in secs: if vaddr <= rva < vaddr + max(vsize, rawsize): return rawptr + (rva - vaddr) raise ValueError(hex(rva))
def va_to_raw(secs, va): return raw_of(secs, va - IMAGE_BASE)
def imports(data, pe, opt_size): """Yield (dll, member, iat_va) for every import.""" d = pe + 24 imp_rva, imp_size = struct.unpack_from("<II", data, d + 96 + 8) i = 0 while True: ent = raw_of(SECS, imp_rva) + i * 20 oft, _ts, _fc, name_rva, first = struct.unpack_from("<IIIII", data, ent) if name_rva == 0: break noff = raw_of(SECS, name_rva) dll = data[noff:data.index(b"\0", noff)].decode() thunk_rva = oft or first j = 0 while True: t = struct.unpack_from("<I", data, raw_of(SECS, thunk_rva + j * 4))[0] if t == 0: break if t & 0x80000000: # by ordinal member = "ordinal %d" % (t & 0xFFFF) else: hint_rva = t & 0x7FFFFFFF hoff = raw_of(SECS, hint_rva) member = data[hoff + 2:data.index(b"\0", hoff + 2)].decode() yield dll, member, IMAGE_BASE + first + j * 4 j += 1 i += 1
def bstr(data, secs, va): """Read a VB6 string literal: dword length at va-4, UTF-16 data at va.""" ln = struct.unpack_from("<I", data, va_to_raw(secs, va) - 4)[0] raw = va_to_raw(secs, va) return data[raw:raw + ln].decode("utf-16-le")
def mid_picks(data, secs): """Immediates of the 'push imm8 ; ... ; call ebx' sites where ebx holds the rtcMidCharVar (ordinal 632) import. Returns [(call_va, imm, dest_ebp_offset)] in program order, together with the chain boundary (the VarCat concat block). """ lo = va_to_raw(secs, CHECK_FN[0]) hi = va_to_raw(secs, CHECK_FN[1]) body = data[lo:hi] out = [] # The compiler emits "lea eax,[ebp-..] ; push eax ; push <index>" at the head # of every Mid$() argument block, and the block ends in "call ebx" where ebx # was loaded from the rtcMidCharVar IAT slot. for m in re.finditer(b"\x50\x6a(.)", body): nxt = body.find(b"\xff\xd3", m.end()) if nxt == -1 or nxt - m.end() > 0x60: continue if body.find(b"\x50\x6a", m.end()) not in (-1,) and body.find(b"\x50\x6a", m.end()) < nxt: continue out.append((CHECK_FN[0] + m.start(), m.group(1)[0])) return out
data, pe, SECS = load()print("file %s: %d bytes, %d sections" % (PATH, len(data), len(SECS)))for name, vaddr, vsize, rawptr, rawsize in SECS: print(" %-8s VA %#x vsize %#x raw %#x" % (name, vaddr, vsize, rawptr))print("\nimports:")for dll, member, iat in imports(data, pe, pe and 224): print(" %#x %-14s %s" % (iat, dll, member))
LITERALS = { "keypad legend": 0x4055E0, "digit 1": 0x405608, "digit 2": 0x405610, "digit 3": 0x405618, "digit 4": 0x405620, "digit 5": 0x405628, "digit 6": 0x405630, "digit 7": 0x405638, "digit 8": 0x405640, "digit 9": 0x405648, "ok message": 0x405650, "ok title": 0x40569C, "separator": 0x405694, "label caption": 0x40557C,}print("\nBSTR literals:")for what, va in LITERALS.items(): print(" %#08x %-14s %r" % (va, what, bstr(data, SECS, va)))
print("\nrtcMidCharVar (ordinal 632) call sites in the check routine:")picks = mid_picks(data, SECS)for va, idx in picks: print(" %#08x push %d" % (va, idx))
# The routine builds two concatenations out of Mid$() picks: the first one is# the value the user input is compared against, the second one feeds the# "Correct! The Magic Number is: " message. Split at the VarCat block.CONCAT_START = 0x407088chain1 = [i for va, i in picks if va < CONCAT_START]chain2 = [i for va, i in picks if va > CONCAT_START]print("\nMid$() picks compared against the input : %s" % chain1)print("Mid$() picks used in the success message: %s" % chain2)
for src in ("123456789", "987654321"): def pick(seq): return "".join(src[n - 1] for n in seq) print("\nsource string %r" % src) print(" check value %s" % pick(chain1)) print(" success message %s-%s" % (pick(chain2[:6]), pick(chain2[6:])))$ cd <hts-workspace>/challenges/hts-app && uv run python app8_static_dump.pyfile app8/win/app8win.exe: 40960 bytes, 4 sections .text VA 0x1000 vsize 0x8000 raw 0x400 .rdata VA 0x9000 vsize 0x1000 raw 0x8400 .data VA 0xa000 vsize 0x1000 raw 0x8600
BSTR literals: 0x004055e0 keypad legend '123456789' # 运行时先被 rtcStrReverse 翻成 '987654321' 0x00405608 digit 1 '1' 0x00405610 digit 2 '2' 0x00405618 digit 3 '3' 0x00405620 digit 4 '4' 0x00405628 digit 5 '5' 0x00405630 digit 6 '6' 0x00405638 digit 7 '7' 0x00405640 digit 8 '8' 0x00405648 digit 9 '9' 0x00405650 ok message 'Correct! The Magic Number is: ' 0x0040569c ok title 'Correct!' 0x00405694 separator '-'24 collapsed lines
0x0040577c label caption 'HTS Application Challenge Programmed by Magic.'
rtcMidCharVar (ordinal 632) call sites in the check routine: 0x406f28 push 1 0x406f70 push 8 0x406f9d push 5 0x406fd0 push 8 0x407009 push 6 0x407042 push 2 0x407172 push 8 0x4071a3 push 4 0x4071d7 push 6 0x407214 push 6 0x407251 push 9 0x40728e push 4 0x4072cb push 6 0x407308 push 3 0x407345 push 7 0x407382 push 6 0x4073bf push 8 0x4073fc push 3
Mid$() picks compared against the input : [1, 8, 5, 8, 6, 2]Mid$() picks used in the success message: [8, 4, 6, 6, 9, 4, 6, 3, 7, 6, 8, 3]
source string '123456789' (the raw legend literal as stored) check value 185862 success message 846694-637683
source string '987654321' (the legend after rtcStrReverse -- the one actually used) check value 925248 success message 2644-164-73427 # digits 264416473427, '-' inserted after the 4th and 7th digit校验函数里对 Mid$() 的调用分成两段,中间夹着 __vbaVarCat 拼接块,两段的产物是:
- 6 位码:由下标
[1, 8, 5, 8, 6, 2]从键盘图例串按 1-based 下标取出 - 魔数:由下标
[8, 4, 6, 6, 9, 4, 6, 3, 7, 6, 8, 3]取出,插入字面量'-',整段接在 BSTR 模板'Correct! The Magic Number is: '之后
图例字面量在 .data 里存的是 '123456789',运行时会先经 rtcStrReverse(导入表里同时有 rtcStrReverse 和 rtcMidCharVar)翻成 '987654321' 再交给 Mid$(),所以 1→9、8→2、5→5、8→2、6→4、2→8。
也就是说:输入 6 位码后,程序弹出的消息框带出这段魔数。
此处存在一个纯静态无法推出的易错点:如果按下标顺序 = 显示顺序、连字符插在第 6 位之后去推,会得到 264416-473427(12 位数字无误,但连字符位置错误)。__vbaVarCat 的拼接顺序与 Mid$() 调用顺序并不一致,连字符实际落在第 4 位和第 7 位之后。连字符的实际位置只有运行程序并读出消息框才能确认。
提交到站点校验端点被接受:
POST /missions/application/applevelup.php level=8 password=2644-164-73427 -> Congratulations, you have successfully completed application 8!Vulnerabilities
校验值是运行时用 Mid$() 从图例字符串拼出来的,读出 push 的立即数即可还原。在本地判定的校验无法保守秘密,等同于把答案交给逆向者。修复方向:校验放服务端,客户端只提交不可逆的校验结果;必须本地校验时,不让答案以可还原的形式出现在代码里。
2644-164-73427