猿人学第21题 — 魔改SM3逆向分析-Claude 分析
猿人学第21题 — 魔改SM3逆向分析
1. 目标
- 地址:
https://match.yuanrenxue.cn/match/21 - 目标:逆向页面中
token参数的生成算法,脱离浏览器使用 Python 成功请求第一页数据 - 加密算法:魔改 SM3
2. 抓包分析
页面通过 POST 请求 /api/question/21 获取数据,请求参数:
| 参数 | 说明 |
|---|---|
page |
页码 |
pageSize |
固定为 10 |
kw |
空字符串 |
token |
SM3 哈希值 |
请求头中携带 Accept-Time 字段,值为服务端时间戳。
3. JS分析 — 定位加密入口
3.1 页面脚本逻辑
页面内联脚本(Script ID 18)包含以下关键逻辑:
- XHR 拦截:重写了
XMLHttpRequest.prototype.open/send/setRequestHeader,将 VM 发出的/api/match2023/3请求重定向到真实接口/api/question/21 - 时间戳获取:重写了
Date.now,改为同步请求/api/getTime获取服务端时间 - 调用 VM:最后执行
call(String(1))启动 VM 中的加密流程
3.2 加密JS文件
加密逻辑在 match3.js(525KB),这是一个 VM 混淆的JS文件,核心结构:
1 | |
VM 解释器通过巨大的 if-else 链实现状态机,根据字节码数组执行不同操作。字节码中编码了完整的 SM3 算法实现。
4. Token公式推导
通过浏览器 DevTools 和 preload 脚本注入,hook window.sm3Digest,确定 token 生成公式:
1 | |
timestamp:通过 GET/api/getTime获取的服务端时间戳字符串page:页码字符串- 两者直接字符串拼接后传入
sm3Digest
5. 魔改SM3逆向过程
5.1 分析方法
由于 _compress 函数是 VM 字节码生成的闭包,无法直接阅读源码。采用以下策略:
- Hook辅助函数:通过替换
SM3.prototype上的_rotl、_ff、_gg、_t、_expand方法,捕获每次调用的输入输出 - 对比标准SM3:用标准SM3计算相同输入的期望值,与浏览器实际值逐项对比
- 定位差异:从 IV → T常量 → strToBytes → 压缩函数内部,逐层缩小差异范围
5.2 已验证的标准部分
以下组件经验证与标准SM3完全一致:
_rotl(x, n)— 32位循环左移_ff(j, x, y, z)— j<16 用 XOR,j≥16 用多数函数_gg(j, x, y, z)— j<16 用 XOR,j≥16 用选择函数_expand(buf)— W 扩展(P1置换 + 循环左移)- 消息填充 — 标准 SM3 填充(0x80 + 零填充 + 64位大端长度)
5.3 发现的6处魔改
魔改1:自定义 IV
标准SM3 IV:
1 | |
魔改 IV:
1 | |
发现方法:直接读取 new SM3().reg 数组。
魔改2:自定义 T 常量
标准SM3:T(0-15) = 0x79CC4519,T(16-63) = 0x7A879D8A
魔改:T(0-15) = 0x79DD4519,T(16-63) = 0x7C179D8A
发现方法:Hook _t(j) 函数,捕获返回值。
魔改3:strToBytes 清除LSB
标准SM3 的 strToBytes 是普通的 UTF-8 编码。
魔改版本对每个字节执行 & 0xFE,即清除最低位。
1 | |
发现方法:调用 sm3.strToBytes("abc") 对比输出字节,发现 0x61→0x60, 0x63→0x62。
魔改4:ss1_input 掩码 & 0xFCFFFFFF
在压缩函数的每一轮中,计算 SS1 前对中间值应用掩码:
1 | |
发现方法:Hook _rotl 捕获 rotl(ss1_input, 7) 的输入值,与标准计算值对比。例如第0轮:标准值 0x1B513D0D,实际值 0x18513D0D,差异 0x1B & 0xFC = 0x18。
魔改5:TT1 掩码 & 0xFFFFFFFA
TT1 计算结果清除 bit 0 和 bit 2:
1 | |
发现方法:通过下一轮的 A 值(= 本轮 TT1)反推。第0轮:标准值 0x46B64D4B,实际值 0x46B64D4A(差1);第1轮:标准值 0xB6DFF66F,实际值 0xB6DFF66A(差5)。验证 & 0xFA 对 LSB 字节在所有64轮均成立。
魔改6:TT2 掩码 & 0xFFAFFFFF
TT2 计算结果清除 bit 20 和 bit 22:
1 | |
发现方法:TT2 是 rotl(TT2, 9) 和 rotl(TT2, 17) 的输入,直接从 _rotl hook 中获取。第0轮:标准值 0x9F6B4336,实际值 0x9F2B4336(0x6B & 0xAF = 0x2B)。验证掩码 & 0xFFAFFFFF 在所有64轮均成立。
5.4 掩码汇总
| 位置 | 掩码 | 清除的位 | 字节位置 |
|---|---|---|---|
| ss1_input | 0xFCFFFFFF |
bit 24, 25 | 字节0 的 bit 0,1 |
| TT1 | 0xFFFFFFFA |
bit 0, 2 | 字节3 的 bit 0,2 |
| TT2 | 0xFFAFFFFF |
bit 20, 22 | 字节1 的 bit 4,6 |
6. Python 实现
完整实现见 solve.py,核心压缩函数:
1 | |
7. 验证结果
4组浏览器对照测试全部通过:
| 输入 | 输出 |
|---|---|
"0" |
6cb40d1e1fee0dbbe28c97d57816501d89ecbbc1f8e3a8c1df21892ec8f868c3 |
"abc" |
1fecc55c368568eb24f6d22e2a37246f06c348cca1b735049cb545c2326d36cb |
"test" |
38fbffd4e19cc051d85ff36c016d00accf9a0536f9833c4c677ad8659f4ccffb |
"17741097605571" |
adac3db429079e917b41a79b7dbdf50d6751f89082a8147f963e0f213bccab68 |
8. 请求流程
1 | |
响应示例:
1 | |
9. 工具链
- js-reverse MCP:preload 脚本注入、hook SM3 函数、执行浏览器端JS
- Chrome DevTools MCP:页面导航、脚本分析
- Chrome Remote Debugging:
--remote-debugging-port=9222启动 Chrome - Python requests:最终脱浏览器请求验证
10. 完整代码
1 | |