HackThisSite - Programming Mission 3
Challenge
This level is about reversing an encryption algorithm.
关卡给出 PHP 加密函数
encryptString的源码,以及一段空格分隔的整数密文;要把密文还原成明文 serial 文件,提交最后一个 serial。限时 120 秒。
每次访问实例页,服务端都会换一组随机的 password
和明文,所以取密文、解密、提交必须放在同一次运行里。状态:verified(服务端返回
Good Job, ***, You have successfully completed this mission)。
Solution
加密链
关卡页面的 PHP 源码是这样的两个函数:
1 | function evalCrossTotal($strMD5) { |
逐字节写成公式:
1 | total_0 = evalCrossTotal(md5(password)) # md5(pw) 的 32 个十六进制位之和 |
两个 PHP 语义细节决定了模型是否对得上:
'0x0'.$c($c是 hex 字符)在 PHP 5 里是带0x前缀的数字串,按十六进制解释,所以evalCrossTotal就是把每个字符当 hex 位加起来,取值范围 0..15。md5($int)会把整数按十进制字符串化再取摘要,也就是代码里的md5(str(total_i))。
关键在于 total 的演进方向:第 i+1 步的
total 由 明文前 i+1 个字符的 md5 和
当前 total 的十进制 MD5
拼出来,也就是只依赖已经解出的前缀。这是一个逐字节向前推进的链,第
i 个字节的减法里用到的 total_i
是前面所有字节共同算出来的。同时 md5(pw)[i % 32]
说明密码哈希的 32 个 nibble 每 32
列循环使用一次。这条链意味着逐列独立求解不成立:单个字节的
total_i
由全部已解前缀共同决定,搜索只能按链状态整体推进。
password 未知也能解
password 本身不需要知道,它只通过
md5(password) 进入公式。真正的未知量是 32 个 nibble
h[0..31],以及初始值
total0 = sum(h)。可以从两个层面把搜索压住:
- 逐位置枚举局部候选:第
i个字符满足ord(plain[i]) = enc[i] - h[i % 32] + total_i。h只有 16 种取值;一旦某个r = i % 32被解出来,后面每一轮循环到同一个r时都复用它,不再重新枚举。 - serial 格式提供硬约束:官方示例
serials_example.txt显示明文是固定宽度的 serial 行,形如XXX-XXX-OEM-XXX-1.1,每行以 UNIX 换行结尾(示例文件里那句Don't forget the UNIX-style line breaks.指的就是这个);末尾的\n同样参与加密,模板宽度是 serial 长度加 1(这里 20 列),不是排版噪声。逐列统计示例文件后发现,20 列里有 11 列是恒定字符(两个-、OEM、尾部1.1、换行),这些列的候选集被压到唯一字符;其余列限定在[A-Z0-9]。这样绝大多数位置上 16 个候选里只有 1 个能活下来。 - 全局约束收口:
total0是 32 个 nibble 之和,上界只有32 * 15 = 480,直接枚举0..480;搜索到最后要求 h 已经集齐 32 个 nibble 且sum(h) == total0。错误的total0会在前几十个字符内因为候选集与模板冲突而全灭,只有正确的那个能走到第 100 个字符。实测单次搜索 1.7–5.6 秒。
完整可复现脚本
下面是自包含的解密脚本,只读密文文件和示例 serial 文件,不联网:
1 | #!/usr/bin/env python3 |
脚本里最后那行
assert encrypt_hash(plain, pwd_hash) == enc
是自校验:把恢复出的明文用恢复出的 md5(password)
再加密一遍,必须和原始密文逐字节相等才算通过。搜索模型本身也用一次回环自测验证过(加密一段已知明文后不告诉求解器
total0,让它盲解):
1 | case=serials-15 len=300 enc[0:4]=[-182, -207, -172, -229] -> 1 solution(s), exact=True |
三个长度(整份 15 行、5 行、2 行)都只解出唯一解,且恢复出的哈希与加密时用的密码哈希一致。
实测输出
服务端接受的那次实例的输出(密文与求解器在同一目录):
1 | $ cd /tmp/wu3 # ciphertext.txt + serials_example.txt + decrypt_wu.py |
1 | ciphertext: 100 values |
5
行明文全部对上了模板的固定列(-、OEM、1.1、换行),没有出现非法字符。线上一次完整运行的时间分布:取密文
1.45 s、解密 1.66–5.58 s(随随机实例而变)、提交 2.61 s,合计约 9
s,远在 120 s 限时之内。
服务端响应正文(账号名已脱敏):
1 | Congrats |