没有ai之前,要搞一个Flutter应用我得玩一年,像这次这个app比较复杂的,我都弄不来。。。
现在一个下午 + 几亿token就轻松搞定了。大部分时候还在刷抖音。ai太吊了
xxx App 加解密密钥分析
目标 App: xxx (
com.xxx.www)
分析工具: Frida + Blutter + Python
最终结论:
- AES Key:
8mQ4vT7xK2pL9sWd(16 字节 UTF-8 字符串)- AES IV:
5rN7cH3zV8kM2qXp(16 字节 UTF-8 字符串)- 加密算法: AES-128-CBC + PKCS7 Padding
一、背景与起点
App 所有 API 请求体和响应体均以加密形式传输,标准 HTTP 抓包看到的是:
POST /wwlive/notice/list HTTP/1.1
Content-Type: application/x-www-form-urlencoded
enc=VtaPcY8wfx3mZRaQgRF07%2Fzb2j4eITGm7wyLgvrf...
响应同样加密:
{"enc": "VtaPcY8wfx3mZRaQgRF07/zb2j4eITGm7wyLgvrf..."}
目标:找到加解密所用的 Key 和 IV,实现明文读取与自建请求。
二、架构分析:Flutter/Dart AOT 应用
App 使用 Flutter(Dart AOT 编译) 开发,编译产物为 libapp.so(ARM64 ELF)。Dart AOT 与传统 JVM 字节码不同,所有代码被编译为原生机器码,无法直接反编译为可读 Dart 源码。
工具链选择
| 工具 | 用途 |
|---|---|
| Blutter | 解析 Dart AOT 镜像,提取类/方法/符号表,生成 Frida 胶水脚本 |
| Frida | 运行时动态插桩,Hook Dart 原生函数 |
| Python (PyCryptodome) | 离线 AES 解密验证 |
Blutter 分析 libapp.so 后,产出关键的 pp.txt(Pool Pointer 表,包含所有常量字符串/对象引用)和 asm/ 目录(各类方法的伪代码)。
三、第一步:定位 HTTP 拦截器
通过 Blutter 的 pp.txt 搜索字符串 "enc",找到关键的 Pool Pointer 引用:
[pp+0x1abc8] String: "enc"
[pp+0x1abc0] String: "/wwlive/system/getInitConfig.anon"
[pp+0x1abb8] String: "response.data -------- "
[pp+0x1ab90] String: "invalid parameters passed to AEADBlockCipher"
[pp+0x1ab88] String: "IV must be at least 1 byte"
[pp+0x1ab98] Field <uAc.key>: late (offset: 0x8)
[pp+0x1aba0] AnonymousClosure: (0x11ce4a8), of [JUs] Exc
关键线索 1:
pp+0x1ab90出现了"invalid parameters passed to AEADBlockCipher",说明 App 内部使用了 PointyCastle 库(Dart 的加密库)中的块密码(AES-CBC 或 AES-GCM)。
关键线索 2:
[JUs] Exc类在 Blutter 生成的asm/jus.dart中被找到:class Exc extends uI { [closure] void Ypi(dynamic, NI<dynamic>, sI) { // onResponse: 0x11ce4a8 [closure] Future<void> Xpi(dynamic, JI, rI) { // onRequest: 0x11c7dec }
Exc是 Dio 拦截器类,Ypi是onResponse回调,Xpi是onRequest回调。
四、第二步:反汇编 onResponse 函数
对 0x11ce4e8(onResponse 实际执行体,Frida 挂钩后的真实入口)进行运行时反汇编,提取以下关键片段:
+11ce6d8: ldur w0, [x2, #0xb] ; response.data (Map, 含 {"enc": "..."})
+11ce6e4: ldr x16, [x16, #0xbc8] ; 从 PP 表加载字符串 "enc"
+11ce700: blr x30 ; mapGet(response.data, "enc") → x0 = enc 密文字符串
+11ce704: mov x3, x0 ; x3 = enc 密文字符串
; --- 获取解密器单例 ---
+11ce75c: bl #0x733e802608 ; 调用懒加载 getter → x0 = 解密器单例对象
; --- 从单例读取 Key 和 IV ---
+11ce760: ldur w2, [x0, #0x1b] ; Key 字符串指针,存于单例 offset +0x1b
+11ce770: ldur w3, [x0, #0x1f] ; IV 字符串指针,存于单例 offset +0x1f
; --- 核心解密调用 ---
+11ce780: ldur x1, [x29, #-0x18] ; x1 = enc 密文字符串
+11ce784: bl #0x733de76178 ; ⚡ 调用 0xbf3178 核心解密函数
; 参数: (receiver=x0, enc=x1, key=x2, iv=x3)
; 返回: x0 = 解密后的明文 JSON 字符串
+11ce790: bl #0x733d998440 ; dart:convert::jsonDecode(plaintext) → Map
+11ce7e4: stur w0, [x2, #0xb] ; response.data = 解密后的 Map(替换原密文)
推理表
| 汇编指令 | 推理含义 |
|---|---|
+11ce75c: bl +157f608 |
调用 Dart 懒加载 getter,返回密码器单例(x0) |
+11ce760: ldur w2, [x0, #0x1b] |
单例对象 offset +0x1b = AES Key 字符串指针 |
+11ce770: ldur w3, [x0, #0x1f] |
单例对象 offset +0x1f = AES IV 字符串指针 |
+11ce784: bl +bf3178 |
以 (x0, x1=enc, x2=key, x3=iv) 调用解密函数 |
五、第三步:在 0xbf3178 Hook 实时读取
对核心解密函数入口(libapp.so + 0xbf3178)挂钩,进入时读取寄存器 x2 和 x3:
// frida-js/tools/blutter/out_dir/blutter_frida.js
Interceptor.attach(libapp.add(0xbf3178), {
onEnter: function () {
init(this.context);
var v2 = cleanDartValue(this.context.x2, 5); // Key
var v3 = cleanDartValue(this.context.x3, 5); // IV
console.log(" x2 (param2): " + JSON.stringify(v2));
console.log(" x3 (param3): " + JSON.stringify(v3));
// 同时 dump 解密器单例对象的各字段
var x0Untagged = decompressPointer(this.context.x0).sub(1);
for (var off = 8; off <= 40; off += 4) {
var val = x0Untagged.add(off).readU32();
var parsed = cleanDartValue(ptr(val), 3);
if (parsed) console.log(" x0+0x" + off.toString(16) + ": " + JSON.stringify(parsed));
}
},
onLeave: function (retval) {
var res = cleanDartValue(retval, 5);
console.log("[🎉 解密成功返回]: " + res);
}
});
实际运行输出
[🎯 0xbf3178 核心解密函数触发]
x0 (receiver): 0x72012aeef1
x1 (enc): VtaPcY8wfx3mZRaQgRF07/zb2j4eITGm7wyLgvrfhOs...
x2 (param2): "8mQ4vT7xK2pL9sWd" ← AES Key ✓
x3 (param3): "5rN7cH3zV8kM2qXp" ← AES IV ✓
x0 CID: 1021
x0+0x8: "A419C702DF86BF31"
x0+0x10: "8E3C7A6E38C10046334FD212..."
x0+0x14: "http://127.0.0.1:39739" ← 代理地址(同一单例)
x0+0x1c: "8mQ4vT7xK2pL9sWd" ← Key 在 offset 0x1c(与 x2 一致 ✓)
x0+0x20: "5rN7cH3zV8kM2qXp" ← IV 在 offset 0x20(与 x3 一致 ✓)
[🎉 0xbf3178 解密成功返回]:
{"code":0,"msg":"","sysTime":1791202366377,"data":{...}}
双重验证:寄存器 x2/x3 的值与单例对象字段
x0+0x1c/x0+0x20完全吻合,反汇编推理得到证实。
六、第四步:Python 离线验证
import base64
from Crypto.Cipher import AES
enc_b64 = "VtaPcY8wfx3mZRaQgRF07/zb2j4eITGm7wyLgvrfhOs..."
key = b"8mQ4vT7xK2pL9sWd" # 16 字节,来自 x2
iv = b"5rN7cH3zV8kM2qXp" # 16 字节,来自 x3
raw = base64.b64decode(enc_b64)
# 前 288 字节是 RSA 会话信封,密文体从 288 开始
cipher_part = raw[288:]
cipher = AES.new(key, AES.MODE_CBC, iv)
dec = cipher.decrypt(cipher_part)
pad_len = dec[-1] # PKCS7 去填充
plain = dec[:-pad_len].decode("utf-8")
print(plain)
# 输出: {"code":0,"msg":"","sysTime":...,"data":{...}} ✓
验证通过,与 Frida 实时捕获的明文完全一致。
七、请求体结构
每次 POST 请求中 enc=... 字段解码后结构如下:
┌─────────────────────────────────┬──────────────────────────────────────┐
│ 前 288 字节:RSA 会话信封 │ 288 字节之后:AES-128-CBC 密文体 │
│ (服务端用 RSA 私钥解出 Key/IV)│ (JSON 请求参数,PKCS7 填充加密) │
└─────────────────────────────────┴──────────────────────────────────────┘
- 信封:App 启动时生成,会话内固定;
- 密文体:用已协商的 Key/IV 加密每次请求的 JSON 数据;
- Key/IV 来源:虽然理论上从信封协商,但实际代码中以明文字符串硬编码于
libapp.so的密码器单例对象字段中,不随信封变化。
八、推理路径总览
graph TD
A["抓包发现 enc=... 密文"] --> B["Blutter 分析 libapp.so<br/>pp.txt 找到 AEADBlockCipher 字符串"]
B --> C["定位 Dio 拦截器类<br/>JUs::Exc @ 0x11ce4a8"]
C --> D["反汇编 onResponse (0x11ce4e8)<br/>发现 +11ce75c 加载解密器单例"]
D --> E["单例 offset +0x1b = Key<br/>单例 offset +0x1f = IV"]
E --> F["Hook 核心解密函数 0xbf3178<br/>读取入参寄存器 x2 / x3"]
F --> G["实时捕获:<br/>Key = 8mQ4vT7xK2pL9sWd<br/>IV = 5rN7cH3zV8kM2qXp"]
G --> H["Python 离线 AES-128-CBC 解密<br/>输出明文 JSON ✓"]
H --> I["自建加密请求体<br/>实现任意 API 调用 ✓"]
九、密钥属性总结
| 属性 | 值 |
|---|---|
| Key | 8mQ4vT7xK2pL9sWd |
| IV | 5rN7cH3zV8kM2qXp |
| 算法 | AES-128-CBC + PKCS7 Padding |
| Key/IV 编码 | UTF-8 明文字符串(非 Hex/Base64) |
| 在libapp.so中的位置 | 密码器单例对象 offset +0x1c / +0x20 |
| 生命周期 | 硬编码,随 App 版本更新可能变化 |
| 是否每次请求变化 | 不变;变化的只是请求 JSON 内容 |
| 信封字节数 | 288 字节(每次 App 启动时协商生成) |
真强,哪里有AI逆向视频教程嘛,想入门~ 解决下TK签名
用的是哪个模型
Flutter逆向真的格外麻烦
@n0de #2 gemini 额度用完了后就用的deepseek
@蛋糕FelixCake #3 是啊,还是网页的js简单。
@cmd #1 没,我是以前从b站学的手工逆向。学得不精,现在有ai可以直接让ai教你需要做什么,需要那些工具
真强,哪里有AI逆向视频教程嘛
牛比
你这流程图一看就是deepseek的,哈哈哈!