logo NodeSeekbeta

Ai逆向之xxx App 加解密密钥分析

没有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签名 xhj007

  • 用的是哪个模型

  • Flutter逆向真的格外麻烦

  • @n0de #2 gemini 额度用完了后就用的deepseek

  • @cmd #1 没,我是以前从b站学的手工逆向。学得不精,现在有ai可以直接让ai教你需要做什么,需要那些工具

  • 真强,哪里有AI逆向视频教程嘛

  • 牛比

  • 你这流程图一看就是deepseek的,哈哈哈!

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有72291位seeker

🎉欢迎新用户🎉