HackThisSite - Application Mission 13
Challenge
Find the numbers. The program takes four numbers on the command line and only prints the final password when all four are correct; any wrong number makes it quit without a message.
找出四个数字。程序从命令行接收四个数字,只有全部正确时才打印最终密码,否则无提示退出。
程序自己的 usage 画面把约束和设计缺陷的提示都写出来了:
1 | $ wine app13win.exe |
Solution
file→PE32 executable for MS Windows 4.00 (console), Intel i386 (stripped to external PDB), 8 sections;objdump -h显示 8 个 section 全都没有名字,import 只剩Kernel32.dll,是典型加壳特征(作者用 Yoda's Crypter)。文件里的代码段是密文,objdump把密文当指令反汇编得到的是jmp/call满天飞的垃圾,strings也只有乱码,手工抠密文不可行。- 运行行为:参数个数不对或超出
0 < n < 1000时打印提示并等按键;四个数都在范围内但不对时一行都不输出、瞬间退出。所以程序唯一泄漏出来的信号是它跑了多久。
Step 1: 前缀短路
Hint 里的 smart card power consumption
是个比喻:验证器把四个数字一个接一个检查,碰到错的立刻
_exit(0),于是前几位都对直接反映成运行时长的台阶。先用
date 夹住一次 wine 调用看个大概:
1 | $ for a in "537 314 137 616" "111 222 333 444" "537 111 333 444" "537 314 111 444"; do |
四个全对的那组明显最慢。把观察做成多点平均的脚本:
1 | #!/usr/bin/env python3 |
1 | $ cd <hts-workspace> && uv run python challenges/hts-app/app13/verify_app13.py |
时间被 wine 的调度量化成几个台阶(0.415 / 0.465 / 0.515…),第 1、3 位能干净地定位到 537、137,第 2、4 位台阶噪声较大。所以计时只能当旁证,真正定案靠静态算法。
Step 2: 脱壳与校验逻辑
剥掉 Yoda's Crypter
后载入反汇编,核心结构如下(按语义重写的干净版本)。timer_complete3
是真正校验的地方,每 5ms 触发一次,共 16 轮:
1 | raise(8); // signal_B -> data2_proc(current_part) |
dword_40F0BC 从 1 起、每轮加 1,切换待校验数字的时机是
case 4/8/12 这三个点,和循环下标本身并不相同;判定也只在
dword_40F0BC 等于 4/8/12/16 时发生。
data1_proc / data2_proc
还带自校验(把内存里运行中的代码和磁盘拷贝逐字节相减,用来发现断点),无调试器时差值为
0,两函数化简为;IsDebuggerPresent 与基于
GetTickCount 的 10 秒超时还会各让
CRC_sum_final 多累加 1,改内存或下断点反而会破坏结果:
1 | // data2_proc(a1) // data1_proc(a1) |
CRC 函数本身是:
1 | DWORD CRC(DWORD crc, DWORD sum) { |
四个硬编码 checkpoint 把四个数字唯一确定了。
Step 3: 脚本验证算法自洽
把上面这条链原样移植成 Python,输入候选四元组,看四个 checkpoint 是否命中:
1 | #!/usr/bin/env python3 |
1 | $ cd <hts-workspace> && uv run python challenges/hts-app/app13/verify_crc.py |
四个 32 位 checkpoint 全中、改动任意一位全崩,这串数字基本就是唯一解。
Step 4: 假校验陷阱
main() 里还有一段表面上有效的校验:循环 255 次
CRC(CRC_sum, v8*pt2 + v8*pt1 - v8*pt3 - v8*pt4) 后比较
CRC_sum == 0x435F2C82。按定义式复现只会得到
0xDF493F04,任意输入都无法满足该比较;真正决定结果的是
timer 回调里的校验链。
Step 5: 动态复现的局限
四个数字都对时,用 wine 跑仍然一行不输出:
1 | $ WINEDEBUG=-all wine app13win.exe 537 314 137 616 |
这套校验挂在 CreateWaitableTimer +
SetWaitableTimer 的完成例程(APC)上,wine 下 timer
完成例程的触发链路不完整,程序执行完 main() 的
SleepEx
循环后直接返回,永远打印不出成功信息。所以本题的定案不是输对数字看到密码:主要证据是
CRC checkpoint 链复现,计时侧信道只作旁证。