HackThisSite - Application Mission 12
Challenge
Application Challenge 12 (Windows) — Find the Password. (hard) 目标:从这个 Windows 程序里找出 password。
包内只有一个 app12win.exe。文件头显示这是
VB6(Visual Basic 6)编译的原生程序,导入表全部指向
MSVBVM60.DLL,工程名
PwdCheckProject1。密码不在明文里,必须从字符串比较 /
字符变换逻辑里推出来。
Solution
file app12win.exe→PE32 executable for MS Windows 4.00 (GUI), Intel i386, 3 sections;导入的全是 VB6 运行时rtc*/__vba*。strings -el app12win.exe(VB6 字符串是 UTF-16LE)能命中界面文字和一张字符表:
1 | $ strings -el -t x app12/win/app12win.exe | grep -iE 'password|abcde' |
abcdefghijklmnopqrstuvwxyz.!:- 有 30
个字符,可视为一张字符索引表:所有该从程序里拼出来的字符都用
Mid() 按 1-based 下标从这张表里取,而下标本身不写死成
ASCII。这就是这题标 hard
的原因:没有可读的明文比较,只有一个下标表加若干次字符串拼接。
Step 1: VB6 事件表定位校验点
VB6 的入口是一个事件分发表:每条记录把消息号减掉一个常量后
jmp 到对应处理函数。用 objdump 看 0x4062D8
处的表:
1 | $ objdump -d -M intel --start-address=0x4062d8 --stop-address=0x40632a app12/win/app12win.exe |
两个反直觉的点:
Check Password按钮(0x406A70)根本不比较密码。它只做两件事:把输入框内容与占位串"Enter Password Here"比较一次,相等就弹"Please Enter a Password";否则清屏并把状态栏文字设成"Verifying Password"。- 紧接着状态栏会循环
".Verifying Password."、"..Verifying Password.."以及首尾各三个点的更长变体(事件0x3F,函数0x407100),最后才由事件0x47的0x407410做真正的比对。
所以真正的校验逻辑在 FUN_00407410(事件
0x47),Ghidra 无头反编译它即可。
Step 2: 反编译校验逻辑
Ghidra 无头反编译(analyzeHeadless +
一个遍历所有函数的脚本)里,FUN_00407410
的核心路径(精简,只保留校验相关语句):
1 | __vbaStrCopy(); // 复制字符表字面量: "abcdefghijklmnopqrstuvwxyz.!:-" |
用到的那张字符表就是 VA 0x406720
的宽字符串,代码里唯一一处引用在 0x4077A3:
1 | 4077a3: ba 20 67 40 00 mov edx,0x406720 ; -> "abcdefghijklmnopqrstuvwxyz.!:-" |
__vbaStrCopy 拿的正是它。Ordinal_632(VB6
运行时的 Mid 类索引函数)按 1-based
下标取字符,Ordinal_712 是
Replace,Ordinal_528 是
UpperCase。把下标代入这张 30 字符表:
Mid(Upper(charset), 3, 1)→Upper(charset)[2]=CMid(charset, 18, 1)→charset[17]=rMid(charset, 16, 1)→charset[15]=p- 目标串:
"Cr p r"(7 个字符)
也就是说校验分两步:先把用户密码里第 3
个字符的所有出现替换成空格,再拿结果去跟 "Cr p r"
比较。
Step 3: 空格从哪来
需要注意的问题在编辑框的按键事件 FUN_00406D60(事件
0x4B):用户每输入一次它就执行
1 | sVar1 = __vbaStrLike(&DAT_00406670,local_1c); // DAT_00406670 = "* *" |
也就是输入框里一出现空格(匹配
Like "* *"),所有空格就被删掉。所以用户无法输入空格,目标串
"Cr p r" 里的三个空格只能由第 2 步的替换产生,即密码的第
3、4、6 位必须都是同一个字符 X。再看第 1、2、5、7
位被钉死为 C r p r,且 X 不能是
c/r/p(否则 Replace
会把这些钉死位也改成空格),于是合法密码的形状是:
1 | C r X X p X r |
X 取遍这张字符表(去掉 c/r/p)共有 28
个合法串,程序全部接受。而 HTS 站点只认其中那个真正的单词;把 28
个候选输出即可辨识:Creeper。
Step 4: 逆推脚本与输出
脚本直接从 exe 里读字符表(不写死答案),再复现第 2、3 步的算法暴力枚举所有候选:
1 | """Recover the HackThisSite App 12 (app12win.exe) password from its own check logic. |
1 | $ cd <hts-workspace> && uv run python challenges/hts-app/app12/recover_password.py |
脚本从二进制里读出的字符表、下标对应的字符、拼出来的目标串
"Cr p r" 全部与反编译结果一致;Creeper
经同一段 check() 判定为通过,且是 28
个候选里唯一的英文单词。
Evidence:
- 全程 static:未运行程序并点击按钮验证;结论来自 Ghidra/objdump 的反汇编与反编译,加上一个独立复现该算法的脚本。
- partially verified:程序层面这 28 个串它都接受(脚本按逆向出的逻辑复现,自洽);真正密码靠站点只认真实单词推得,无法在本地对 HTS 验证(未提交答案)。
Ordinal_632 / 712 / 528是按 MSVBVM60 的序号导入,本地没有该 DLL 去核对符号名;名称(Mid/Replace/UpperCase)是依据调用形态与行为判定的,行为本身由反编译输出直接证实。
Vulnerabilities
密码校验完全在客户端完成,校验逻辑只是取第 3 个字符替换后与常量串比较,而该常量串由客户端自带的字符表按硬编码下标拼出。本地校验不构成访问控制:读出这段逻辑即可枚举所有被接受的输入。修复方向:校验放在服务端,客户端只提交凭据;确需本地校验时应比对不可逆的哈希值,不在客户端现场拼出明文目标串。
Creeper