猿人学 App 第五题离机复现:OLLVM 包裹的变形 MD4 纯 Python 还原
本文最后更新于:2026年9月1日 下午
猿人学 App 第五题(HTTP / dex,难度”简单”)的纯离机复现记录:不驱动 App,只靠逆向把签名算法搞清楚。
端点靠反射 Retrofit 运行期注解读出;签名是assets/encrypted.dex里一个被 OLLVM 混淆的变形 MD4——本文记录怎么在反编译失败的情况下,用 ASM 插桩 dump 执行轨迹反推出这个变形,并还原成零依赖纯 Python。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 手上的静态素材
- 请求模型(静态反编译)
- 签名:OLLVM 包裹的变形 MD4
- 纯 Python 还原 + JVM 预言机对拍
- 取数与反爬
一、手上的静态素材
从设备 pull 出 base.apk 后就再没碰过手机:
base.apk→classes*.dex(dex2jar 转app.jar)、assets/encrypted.dex- 工具:
androguard(纯 Python 反编译)、dex2jar+ 本机 Temurin JDK(把提取出的类当离机预言机跑)、jadx/CFR(反编译)、ASM(字节码插桩)
二、请求模型(静态反编译)
ChallengeFiveFragment 的翻页逻辑很直白:
1 | |
端点不用解字符串加密——Retrofit 的 @POST/@Field 是 RUNTIME 注解,dex2jar 转 jar 后直接反射接口就能读出来:
1 | |
即 POST /api/app5,表单 content=页号:毫秒、sign=签名;响应 JSON 每页 10 个数字,且按页号确定性返回。
三、签名:OLLVM 包裹的变形 MD4
DexUtils.OooO0O0 把 assets/encrypted.dex(逐字节 XOR 0xA6)内存加载,调用 com.yuanrenxue.challenge.five.Sign.sign()。这个 sign() 有 21938 条指令、控制流扁平化 + 字符串加密,jadx 和 CFR 都无法反编译主体——但辅助函数 f/g/h 和常量 0x5A827999/0x6ED9EBA1 一眼就是 MD4。
麻烦的是 md4(content) ≠ 实测签名。于是换思路:用 ASM 给 ff/gg/hh 插桩,跑一次 dump 出执行轨迹(每步的 a,b,c,d, 消息字, 移位),再把每步的消息字反查回下标,还原出完整调度。结论——这是一个四处被改动的 MD4:
| 改动点 | 标准 MD4 | 本题 |
|---|---|---|
| IV 的 C 字 | 0x98BADCFE |
0xEFCDAB98 |
| 第 1 轮移位 | 3,7,11,19 |
3,9,11,19 |
| 第 2 轮移位 | 3,5,9,13 |
3,5,7,13 |
| 第 3 轮移位 | 3,9,11,15 |
一致 |
| 预处理 | 无 | 正式 48 步前对字 0 多做一次 a=rotl(a+F(b,c,d)+X0,5)(只改 a、不轮换) |
| 输出 | 定长 hex | 4 个结果字各自 Integer.toHexString(逐字丢前导 0)拼接 |
四、纯 Python 还原 + JVM 预言机对拍
按上面反推的调度纯 Python 复现;再用 dex2jar 把 Sign 类跑在本机 JVM 上当预言机,喂随机输入对拍——360 组 0 差异才算真复现。核心实现:
1 | |
五、取数与反爬
有了纯 Python 的 sign(),客户端就能自己拼 content=页号:毫秒 + 签名,直连公网逐页取数。服务器对同 IP 限频(约 60 次/窗口后 403 Access forbidden),对策是请求间隔 ~0.7s + 403 退避(重试时用新时间戳重新签名)。
到这一步就完全离机、零依赖了:不碰手机、不加载 dex,纯 Python 复刻签名即可翻页取数。
小结
这题的关键判断是**”是不是标准算法”——f/g/h + 那两个常量一露头,就该往 MD4 靠。真正花功夫的是确认它被四处魔改:靠 ASM 插桩 dump 执行轨迹、再用本机 JVM 当预言机对拍,才敢说”干净还原成纯 Python”。对可反编译失败但本质是标准算法变体的 dex 层混淆,插桩取轨迹 + 预言机对拍**是很省事的正解。