猿人学 App 第十题「React Native」离机复现:魔改 MD4 藏在 JS Bundle 里
本文最后更新于:2026年9月2日 下午
[猿人学 App 逆向系列]第十题。这题不是 Fragment,而是一个
RNActivity——React Native 应用,取数逻辑在打包好的index.android.bundle里。
好消息是这份 bundle 是明文压缩 JS(不是 Hermes 字节码),签名算法能直接读。坏消息是它把签名做成了一个魔改 MD4,长得像标准 MD4、却对不上——差点被”想当然”骗过去。
本文记录:定位 RN 的签名 → 想当然用标准 MD4 吃 403 → 用 node 直跑 bundle 拿 ground truth → 发现是魔改 MD4 → 逐字转写成纯 Python。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 定位:RNActivity + index.android.bundle
- 读 bundle:sign_hex(page:ms)
- 想当然的坑:标准 MD4 → 403
- node 直跑拿 ground truth:它不是标准 MD4
- 逐字转写魔改 MD4 → 纯 Python
一、定位:RNActivity + index.android.bundle
第十题在 Fragment 列表里根本没有 ChallengeTenFragment——它是个独立 Activity:
1 | |
ReactRootView + rn_module = React Native。RN 的业务代码全在 assets/index.android.bundle。看一眼文件头:
1 | |
var __BUNDLE_START_TIME 说明是 Metro 打的明文 JS bundle(不是 Hermes .hbc 字节码),761KB 压缩 JS——算法可读,省去了反编译 Hermes 的麻烦。
二、读 bundle:sign_hex(page:ms)
直接在 bundle 里搜端点,顺出 loadData:
1 | |
fetchData 是 fetch(url, {method:"post", headers:{"Content-Type":"application/json"}, body:JSON.stringify({page,t,sign})})。
所以请求模型是:POST /api/app10,JSON body {page, t:ms, sign},而 sign = sign_hex("page:ms")。
再找 sign_hex 的定义:
1 | |
初值 1732584193/-271733879/-1732584194/271733878 = 0x67452301/0xefcdab89/0x98badcfe/0x10325476——MD4/MD5 的标准 IV;三轮里出现常量 0x5A827999、0x6ED9EBA1,移位 3,7,11,19——这是 MD4。
三、想当然的坑:标准 MD4 → 403
看到这里很容易下结论:”就是标准 MD4 嘛”。于是直接拿现成的纯 Python MD4 上:
1 | |
403。做个判别:对的 sign、错的 sign、干脆不带 sign,三者返回一模一样的 403——看起来像是 sign 校验之前就被拦(WAF/请求头层面)。抓一条真机请求看头部:头很普通(okhttp/4.9.2 + json),唯一区别是走 HTTP/2。
但真正的破绽在 body 里——把真机的 sign 和我算的一比:
1 | |
根本不是我”想当然”的那个 MD4。 之前的 403,不是 WAF,就是 sign 算错了(错 sign 和无 sign 恰好走同一个拒绝分支)。
四、node 直跑拿 ground truth:它不是标准 MD4
bundle 是明文 JS,那最没有争议的办法就是把 sign_hex 抠出来直接用 node 跑,拿一份权威输出:
1 | |
而标准 MD4(“abc”) = a448017aaf21d8525fc10ae87aa6729d。两者不同 → 这是一个魔改 MD4。
差在哪?细看它的轮函数:
1 | |
外层那串 u=c(u,i,h,v,...)、v=c(v,u,i,h,...)… 的状态字轮换顺序是 u,v,h,i,而 F/G/H 里参与运算的三个字的位置也和标准 MD4 不同。IV、常量、移位全是标准的,唯独 F/G/H 的”喂参顺序”被换了——足以让输出彻底改变,却又骗过”一眼 MD4”的直觉。
五、逐字转写魔改 MD4 → 纯 Python
既然只是参数序/轮换不同,那就照着 JS 一比一转写,不做任何”标准化”想当然,再拿 node 和真机抓包双重对拍:
1 | |
对拍:
1 | |
两条都过,换上正确 sign 后 HTTP/1.1 直接 200——说明当初 403 跟 HTTP/2 无关,纯粹是 sign 错了。之后自己发请求、翻 100 页求和即可(服务器偶发空页,重试补齐)。
小结
第十题的两层皮:
- 壳是 React Native——业务在
index.android.bundle;所幸是明文 JS(不是 Hermes),搜端点顺出sign_hex("page:ms"); - 核是魔改 MD4——IV/常量/移位全标准,唯独
F/G/H参数序与状态轮换被换掉,输出和标准 MD4 完全不同。
最大的教训是那句”一眼 MD4”的想当然:magic number 对上不代表算法一致。明文 JS 的最大优势,就是能直接 node 跑出 ground truth——用它一对拍,李逵李鬼立现,再逐字转写就稳了。