猿人学 App 第七题「flutter」离机复现:Blutter 反编译 Dart AOT,还原 HMAC 双盐签名
本文最后更新于:2026年9月2日 中午
[猿人学 App 逆向系列]第七题「衣带渐宽终不悔【flutter】」。取数逻辑在 Flutter 的 Dart AOT 快照(libapp.so) 里,用 Flutter 自带 BoringSSL 走 HTTPS。
本系列坚持离机约束:不驱动 App、不靠抓包当答案。抓包只是定位算法、拿对拍样本的手段;真正的目标是把签名算法从 Dart AOT 里读出来,用纯 Python 自算签名、自发请求。
本文完整走一遍:抓包定位 → 断定不是裸 SHA256 → 上 Blutter 反编译 Dart AOT → 读出 HMAC-SHA256 + 两个内嵌盐 → 纯 Python 离机复现。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 请求模型:抓包只为定位
- 为什么 Flutter 抓包难(以及怎么看到明文)
- 卡住的地方:盐找不全,裸 SHA256 全错
- 上 Blutter:把 Dart AOT 快照反编译出来
- 读汇编:一行行还原签名构造
- 纯 Python 离机复现 + 对拍
- 踩坑记:Blutter 编译那些事
一、请求模型:抓包只为定位
先说结论式的请求模型(后面会证明每一部分):
1 | |
content = "页码:毫秒时间戳";sign是 64 位小写 hex(一眼 SHA256/HMAC-SHA256 家族);- 没有 cookie、没有 token——这题的鉴权全压在
sign上。
抓包在这里的定位很明确:只用来确认”请求长什么样”、以及拿到一批 (content, sign) 对作为后面离线还原算法的对拍样本。抓包不是答案。
二、为什么 Flutter 抓包难(以及怎么看到明文)
Flutter 的联网走静态链接进 libflutter.so 的 BoringSSL:不吃系统代理、自带根证书、符号全剥离。常规”设代理 + 装 CA”抓不到。
拿明文的办法(仅用于取样)是 reFlutter:它按 SnapshotHash 匹配引擎版本,换上预编译好的、关掉 SSL pinning 的同版本引擎。这里有个实战坑——直接装 reFlutter 重打包的 APK 会崩:
1 | |
这是 apktool 重打包丢了 meta-data,不是 app 自检。破法是只 root 替换 libflutter.so 进原版 app(签名、manifest、其它 so 全不动),再配 adb reverse + mitmdump --mode reverse 把明文导出来。
这套组合拳能看到明文,于是我们拿到了几十条
(content, sign)样本。但这只是起点——离机复现要求能自己算出sign。
三、卡住的地方:盐找不全,裸 SHA256 全错
有了样本,第一反应是穷举常见构造。libapp.so 里静态搜字符串,能扒到一个很像盐的 16 字符串:
1 | |
然后拿它往死里试:
1 | |
没有一个对得上样本,服务器也一律 403。 内存扫描也扑空——哈希输入是个瞬时 Uint8List,用完即焚,扫不到。
问题很清楚:光靠”猜构造 + 静态字符串”不够。nmZO8*USEkF9fIoG 只是拼图的一块,构造本身(是不是 HMAC?key 是谁?)必须从 Dart 代码里读出来。而 Dart AOT 是编译成机器码的快照,得上专门的反编译工具。
四、上 Blutter:把 Dart AOT 快照反编译出来
Blutter 是针对 Dart AOT 快照的反编译器。它的原理决定了它必须用与目标完全一致的 Dart 版本:它会拉对应版本的 Dart SDK 源码、编译出同版本的 libdart,再用它去解析 libapp.so 的对象池与函数。
目标版本从 UA 和快照信息都能确认是 Dart 2.19.1。跑起来:
1 | |
产物结构:
1 | |
一眼就看到 crypto/src/hmac.dart 被编进来了——大概率是 HMAC,不是裸 SHA256。这解释了第三节为什么全错。
五、读汇编:一行行还原签名构造
打开 challenge_seven_page.dart,先扫字符串常量和 crypto 调用:
1 | |
关键是两个 16 字符盐都出现了。跟着数据流读汇编(节选、去掉栈检查噪声):
1 | |
_StringBase::+ 的收发者/参数顺序,可以用抓到的样本反推:样本里 content = "1:1788313273229" 正好是 page + ":" + millis,与汇编里”先 page 再 : 再 millis”的拼接顺序一致,顺序就锁死了。
还原出的算法:
1 | |
之前全错的两个原因一次性解释清楚:(1) 是 HMAC 不是裸 SHA256;(2) 缺了 key 盐 9R^tF2xFmt0A0vm.,而且 nmZO8… 是拼在 message 尾部、不是当 key。
六、纯 Python 离机复现 + 对拍
先拿抓到的样本对拍(离线、不发请求):
1 | |
三条全 True。 算法确定,接下来自己算签名、自己发请求:
1 | |
跑通:page 1 返回 [6745, 5106, 3952, 5372, 6686, 4825, 2995, 1297, 7037, 3118],与抓包里第一页逐个一致;1-100 页 1000 个数求和得到答案。
这才是离机复现:签名是自己按还原的算法算的,请求是自己发的,服务器 200 正常返回,全程不再需要 App、不再需要抓包。
七、踩坑记:Blutter 编译那些事
Blutter 要现编 Dart VM,在一台 15G 内存的 WSL 上连踩三坑,记一下:
- OOM 被 SIGKILL:ninja 默认按核数并行(这里 22 核),Dart VM 有几个重 TU 单个吃 1~2G,峰值直接爆内存、进程
[killed]。把subprocess.run([NINJA_CMD])改成[NINJA_CMD, '-j', '4'],4×2G 稳在 8G 内。 - WSL 出站 TCP 假死:clone 到一半 github TLS 连不上,但宿主机能上网——典型 WSL2 NAT 网络掉线。
wsl --shutdown重启网络栈即恢复(DNS/ping 正常但 443 全挂时基本就是它)。 - 缺 capstone:Dart VM 编完后,blutter 工具本身 cmake 报
None of the required 'capstone' found。apt install libcapstone-dev pkg-config补上即可,重跑会跳过已编好的 Dart VM 直接反编译。
小结
第七题的完整链路:
- 抓包只做定位:reFlutter 关 pinning + 只换 libflutter.so 进原版 app + adb reverse + mitm reverse,拿到请求模型和
(content, sign)样本; - 静态穷举失败说明构造必须从代码读,不能猜;
- Blutter 反编译 Dart AOT,在
challenge_seven_page.dart里读出真身:HMAC_SHA256(key="9R^tF2xFmt0A0vm.", content + "nmZO8*USEkF9fIoG"); - 两个盐都在 libapp.so 里——一个当 HMAC key、一个拼在 message 尾部,少任何一个、或把 HMAC 当成裸 SHA256,都对不上;
- 纯 Python 自算签名、自发请求,样本对拍全 True,离机复现完成。
Flutter 的”难”在于取数逻辑被编成了 Dart AOT 机器码;抓包能看到明文却看不到算法,真正的钥匙是把 AOT 快照反编译回来。这也是本系列一直强调的:能抓包 ≠ 完成逆向,把算法读出来、能自己算,才算真的还原。