猿人学 App 第二十七题「Weex」离机复现:抠出 webpack 里的魔改 SHA1

本文最后更新于:2026年9月4日 上午

[猿人学 App 逆向系列]第二十七题「庭院深深深几许【Weex】」。取数逻辑在 Weex 页面里,而且 Weex bundle 是远程加载的——业务代码根本不在 APK 里。
本文记录:定位远程 bundle → 读出 sign = SHA1("ms:page") → 被”标准 SHA1”骗一次 → 发现是魔改 crypto-js SHA1(只改了输出的第 4 个字) → 从 webpack 里把原版 SHA1 抠出来在 node 里跑 → 补上 token 通关。

⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。

本文目录

  • 业务代码不在 APK:Weex 远程 bundle
  • 读逻辑:sign = SHA1(“ms:page”)
  • 被”标准 SHA1”骗一次
  • 确认魔改:只有第 4 个字(h3)被改
  • 从 webpack 里抠出原版 SHA1
  • 最后一道坑:token

一、业务代码不在 APK:Weex 远程 bundle

第 27 题在 Fragment 列表里没有对应类,它由 WeexEntryActivity 承载:

1
2
3
this.instance.renderByUrl("第27题-庭院深深深几许",
"https://www.python-spider.com/static/app/index.js", // <-- 远程 bundle
null, null, WXRenderStrategy.APPEND_ASYNC);

APK 里的 assets/main.js 只是 Weex 框架(rax/weex core),真正的业务代码是这个远程 index.js直接 curl 下来读即可——比抠 APK 还省事:

1
curl -s https://www.python-spider.com/static/app/index.js -o index.js   # 标准 webpack 打包,326KB

二、读逻辑:sign = SHA1(“ms:page”)

app27,顺出取数逻辑:

1
2
3
4
5
6
7
created: function () {
var e = Date.now(); // 毫秒
var n = (0, i.SHA1)(e + ":" + this.page).toString(); // sign = SHA1("ms:page")
a.fetch({ method: "POST", url: "/api/app27",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: "page=" + this.page + "&ts=" + e + "&sign=" + n });
}

其中 i = n(17),即 webpack 模块 17 导出的 SHA1。注意顺序是 “ms:page”(毫秒在前)。

三、被”标准 SHA1”骗一次

一看 SHA1,想当然按标准 SHA1 算,结果服务器 {"error": "pass"}(403)。而且在 bundle 里搜标准 SHA1 的轮常量:

1
2
3
4
0x5A827999 (K1)  FOUND
0x6ED9EBA1 (K2) FOUND
0x8F1BBCDC (K3) NOT FOUND ← ?!
0xCA62C1D6 (K4) NOT FOUND ← ?!

于是一度以为是”K3/K4 被魔改”(像第 10 题魔改 MD4 那样)。但——这是个误判(下节纠正):K3/K4 其实标准,只是被字符串混淆藏起来了,搜不到明文而已。

四、确认魔改:只有第 4 个字(h3)被改

不猜了,直接把 bundle 的 SHA1 在 node 里跑出来对拍(见下节),和标准 SHA1 逐字节比:

1
2
3
4
输入 "abc":
bundle = a9993e36 4706816a ba3e2571 782c3980 9cd0d89d
标准 = a9993e36 4706816a ba3e2571 850c26c9 9cd0d89d
^^^^^^^^ 只有第 4 个字(h3)不同

对多个输入都一样:h0/h1/h2/h4 与标准完全一致,唯独 h3 被改

这个现象很有意思:如果是轮常量(K1-K4)被改,雪崩效应会让所有 5 个字都变。只有 h3 变、其它不变 → 说明轮函数没动(否则 h0 早就变了),改动在 finalize 里,专门针对 h3。

进一步验证 h3 到底怎么改的:diff / xor 都不恒定,也不等于任何一轮的中间寄存器 d,更不是任何”两个末尾寄存器做运算”——是个自定义的比特级计算。硬扣它得反混淆那段控制流平坦化的 finalize,成本很高。既然能把原版跑起来,就先用原版当预言机。

五、从 webpack 里抠出原版 SHA1

bundle 是标准 webpack:!function(t){...}([模块0, 模块1, ...]),入口 e(e.s=253)不执行入口(避开 Weex 依赖和其它模块的反调试),只把 require 暴露出来,直接 e(17).SHA1:

1
2
3
4
5
// 把 e(e.s=253) 换成 (global.__R=e),不跑入口
global.window = global; global.self = global; global.navigator = {userAgent:'x'};
/* ... 整个 bundle ... */
var C = global.__R(17); // 模块 17 = crypto-js(Base,WordArray,...,SHA1Algo,SHA1)
console.log(C.SHA1("1788414311409:1").toString()); // 原版魔改输出

一个坑:crypto 模块偶发反调试挂死——交互式常驻进程不稳,改成批量模式(一次把 100 页的 content 都算完再返回)就稳定了。

六、最后一道坑:token

sign 用对了还 {"error": "pass"}。抓一条真机请求(第 27 题在 Android 14 上崩,但同型的第 28 题能抓)对比才发现:请求里带了一个 token(登录态)。补上就 200。

{"error": "pass"} 的真相其实是缺 token,不是传输指纹墙——这一点在第 28 题(GoNDK)那边先踩明白的。

于是完整请求:

1
2
3
POST /api/app27
token: <登录 token> # <-- 缺它就 {"error":"pass"}
page=<page>&ts=<ms>&sign=<魔改SHA1("ms:page")>

逐页求和即通关。

小结

第 27 题串起了好几个坑:

  1. 业务代码远程加载——不在 APK 里,renderByUrl 的 URL 直接 curl 下来读;
  2. 魔改哈希别急着下结论——“K3/K4 搜不到”不等于被改,跑出来逐字节对拍才发现真相是”只改了 h3“;
  3. webpack 抠模块——换掉入口、暴露 require,e(模块号) 单独把 crypto 抠出来跑,绕开反调试;偶发挂就上批量模式;
  4. 算法对了还进不去,就去补 token

一句话:远程 bundle + 魔改哈希,靠”抠模块单跑 + 逐字节对拍”最快;别被 magic number 的表象带偏,也别忘了 token 这种”环境位”。


猿人学 App 第二十七题「Weex」离机复现:抠出 webpack 里的魔改 SHA1
https://kingjem.github.io/2026/09/03/猿人学App第二十七题离机复现-抠出webpack里的魔改SHA1/
作者
Ruhai
发布于
2026年9月3日
许可协议