HackThisSite - Application Mission 8

Challenge

Application Challenge 8 — Find the 6 digit code. (medium)

找出 6 位数字:程序是个 VB6 写的数字键盘小程序,输入正确的 6 位码后会回显站点密码。

Solution

  • app8win.exeVB6 原生编译程序:导入表只有 MSVBVM60.DLL,且绝大多数条目是 ordinalordinal 632 = rtcMidCharVar,即 VB6 的 Mid$())。
  • 密码/数字都不在字符串里:ASCII 与 UTF-16 搜 password185862 等全部 0 命中;有效信息全在 .text 里的 UTF-16 BSTR 字面量和一串 push imm8 立即数里。VB6 的字符串是 UTF-16LE,必须 strings -el 或直接按 BSTR 结构读。
  • 先做纯静态:解析 PE → 读 BSTR 字面量 → 把校验函数里所有 rtcMidCharVar 调用点的立即数抽出来。静态结果先给出候选,最终结论通过运行程序读取消息框确认

VB6 的 BSTR 字面量结构是 长度 dword + UTF-16 数据,校验值是用 Mid$(源串, n, 1) 逐个字符拼出来的,所以只要拿到源串和那串下标就能还原所有字符串:

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/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. This
script parses the PE, lists MSVBVM60.DLL imports (name + ordinal), locates the
UTF-16 BSTR string literals VB6 stores in .text, and recovers the immediates
handed to the rtcMidCharVar calls (MSVBVM60.DLL ordinal 632, the runtime
implementation of VB6's Mid$()) inside the digit-check routine.

Run from the directory that holds app8/win/app8win.exe.
"""
import re
import struct

PATH = "app8/win/app8win.exe"
IMAGE_BASE = 0x400000
CHECK_FN = (0x406D40, 0x407740) # the button/verification routine
RTC_MIDCHARVAR_IAT = 0x401040 # IAT slot, called as "call ebx"

def load():
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 = 0x407088
chain1 = [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:])))

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
$ cd <hts-workspace>/challenges/hts-app && uv run python app8_static_dump.py
file 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 '-'
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(导入表里同时有 rtcStrReversertcMidCharVar)翻成 '987654321' 再交给 Mid$(),所以 1→98→25→58→26→42→8

也就是说:输入 6 位码后,程序弹出的消息框带出这段魔数。

此处存在一个纯静态无法推出的易错点:如果按下标顺序 = 显示顺序、连字符插在第 6 位之后去推,会得到 264416-473427(12 位数字无误,但连字符位置错误)。__vbaVarCat 的拼接顺序与 Mid$() 调用顺序并不一致,连字符实际落在第 4 位和第 7 位之后。连字符的实际位置只有运行程序并读出消息框才能确认

提交到站点校验端点被接受:

1
2
POST /missions/application/applevelup.php  level=8  password=2644-164-73427
-> Congratulations, you have successfully completed application 8!

Vulnerabilities

校验值是运行时用 Mid$() 从图例字符串拼出来的,读出 push 的立即数即可还原。在本地判定的校验无法保守秘密,等同于把答案交给逆向者。修复方向:校验放服务端,客户端只提交不可逆的校验结果;必须本地校验时,不让答案以可还原的形式出现在代码里。

2644-164-73427