使⽤ AI 实现 最新版本 Flutter HTTPS 明⽂抓包
eBPF 在内核层
AI 自动分析 stripped 的 libflutter.so ,解决了最难的函数地址定位问题 偏移确定后,同一 Flutter 版本可复用,换 App 只需重新跑一遍 AI 分析
AI 工具(Codex)
npm i -g @openai/codexida-pro-mcp
git clone https://github.com/mrexodia/ida-pro-mcp.gitcd ida-pro-mcpuv sync
[mcp_servers."ida-pro-mcp"]command = "/opt/homebrew/bin/uv"args = ["--directory", "你下载的路径", "run", "ida-pro-mcp"]
## 任务在 Flutter Android ARM64 的 libflutter.so(stripped)中定位 BoringSSL 的 SSL_write、SSL_read 函数地址及 ssl_read_inner_offset。
## ⚠️ 关键:IDA 地址 ≠ 文件偏移ecapture 的 `--ssl_write_addr` / `--ssl_read_addr` 需要 **ELF 文件偏移**,不是 IDA 虚拟地址。libflutter.so 的 `.text` PT_LOAD 段通常满足:`VirtAddr = FileOffset + 0x1000`,因此:**`file_offset = IDA_VA - 0x1000`**验证方法(IDA Python):```pythonimport idaapiprint(hex(idaapi.get_fileregion_offset(0xYOUR_VA)))````ssl_read_inner_offset` 是相对偏移(BL_VA - SSL_read_VA),两地址在同一段内相减,偏差自动消除,**无需减 0x1000**。---## 已知条件- 架构:ARM64,IDA base=0x0- Flutter 使用内嵌 BoringSSL(非系统 libssl)- 调用链:SSLFilter::ProcessAllBuffers → SSL_write/SSL_read- SSLFilter 成员布局:- `ssl_` @ offset 0x18- `socket_side_` @ offset 0x20- `buffers_[0..3]` 从 offset 0x30 开始,每 8 字节一个---## 定位策略(按步骤执行)### Step 1:找 __errno PLT用 `mcp0_imports` 或 `mcp0_list_funcs(filter="*errno*")` 找到 `.__errno` 的地址。### Step 2:找 ERR_clear_system_error用 `mcp0_xrefs_to` 找 `.__errno` 的所有调用者。筛选条件:函数体 ≤ 4 条指令,仅调用 `.__errno` 并 `STR WZR, [X0]`。### Step 3:找 ssl_reset_error_state用 `mcp0_xrefs_to` 找 `ERR_clear_system_error` 的调用者。筛选条件:- 函数体 ≤ 10 条指令(~28 字节)- 包含 `STR WZR, [X0, #0xXX]`(清 ssl->s3->rwstate)- 调用 `ERR_clear_error`(另一个辅助函数)- 调用 `ERR_clear_system_error`### Step 4:找 ssl_reset_error_state 的所有调用者用 `mcp0_xrefs_to(ssl_reset_error_state_addr)`,通常 3–5 个。### Step 5:识别 SSL_write
从调用者中找:- 较小的函数(200–350 字节)- 调用 `ssl_reset_error_state` 后,检查 `[ssl, #0xXX]` 是否为 nullptr(CBZ,对应 `do_handshake` 检查)- 内部有 BLR(vtable 间接调用 `write_app_data`)- 有一个内嵌的循环调用另一个中等大小函数(SSL_do_handshake)### Step 6:识别 SSL_read从调用者中找:- 较大的函数(> 1000 字节)- 调用 `ssl_reset_error_state` 后进入复杂 switch/loop(有跳转表 `jpt_xxx`)- 函数体对应 `ssl_read_impl`(内联了 SSL_peek 和 ssl_read_impl)### Step 7:通过调用点验证搜索 `ProcessAllBuffers` 函数(包含 `"Out-of-bounds internal buffer"` 或 `"SecureSocket"`字符串引用的函数):- 找 `BL SSL_write_candidate` 的前几条指令- 验证:`LDR X0, [SSLFilter, #0x18]`(ssl_),`LDR Xn, [SSLFilter, #0x38]`(buffers_[kWritePlaintext])- 同理验证 `SSL_read_candidate` 使用 `[SSLFilter, #0x30]`(buffers_[kReadPlaintext])### Step 8:确认行号反编译 SSL_write 候选函数,找 `ERR_put_error` 调用(`sub_70FXXX`),其 W3 参数(立即数)应约等于 BoringSSL ssl_lib.cc 中 SSL_write 的行号(~940–980)。---### Step 9:找 ssl_read_inner_offset**目标**:找到 SSL_read 内部调用 memcpy 封装桩的 BL 指令偏移量,用于 ecapture inner probe。**封装桩特征**(仅 2 条指令):```asmMOV X2, X; 把 size 放入 X2(ARM64 memcpy 第 3 参数)B .memcpy ; 尾调用真正的 memcpy``` 不同版本 size_reg 不同:旧版(de8c7af5)用 `X24`,新版用 `X22`。**在 SSL_read 中定位该 BL**(在 SSL_read 成功路径末尾):```asm; 识别特征序列(示例来自新版 libflutter.so):LDR X9, [X8, #0x88] ; X9 = ssl3_state->rbuf.len(可读字节数)MOV W10, W; W10 = num(用户请求字节数,来自 SSL_read 第 3 参数)CMP X9, W, UXTW ; 比较 avail vs numCSEL X, X9, X10, CC ; X= min(avail, num) ← 就是 size_regCBZ X,; 若 0 字节则跳过LDR X1, [X8, #0x80] ; X1 = ssl3_state->rbuf.buf(明文源,C++ heap)MOV X0, X; X0 = 目标缓冲区BL sub_memcpy_stub ; ← inner probe 就在这里
**步骤**:1. 用 `mcp0_callees(ssl_read_va)` 列出 SSL_read 的所有被调函数。2. 过滤大小 ≤ 8 字节(2 条指令)的函数,逐一反汇编确认 `MOV X2, Xreg; B .memcpy`。3. 用 `mcp0_xrefs_to(stub_addr)` 找到从 SSL_read 内部调用它的 BL 指令地址(`BL_VA`)。4. 通过反汇编 BL_VA 前后确认上述特征序列(LDR X1 = plaintext src,CSEL = size)。**计算**:```ssl_read_inner_offset = BL_VA - SSL_read_entry_VA```无需减 0x1000(相对偏移,偏差自动消除)。---## 输出格式```SSL_write IDA VA : 0xXXXXXXSSL_read IDA VA : 0xXXXXXXssl_read_inner_offset : 0xXXX (= BL_VA - SSL_read_entry_VA, 无需减 0x1000)# ecapture 实际传参(file offset = IDA VA - 0x1000):--ssl_write_addr 0xXXXXXX (= SSL_write_IDA_VA - 0x1000)--ssl_read_addr 0xXXXXXX (= SSL_read_IDA_VA - 0x1000)--ssl_read_inner_offset 0xXXX (与 IDA 偏移相同)```> 验证 file offset:在 IDA Python 中执行
bash adb push ecapture /data/local/tmp/ecapture adb shell dumpsys package com.yrx.flutternet | grep -E "codePath|resourcePath|dataDir"
# 检查 so 是否存在于文件系统adb shell ls /data/app/~~xxx/com.example-xxx==/lib/arm64/libflutter.so# 若不存在,获取 APK 路径adb shell pm path com.example.app# 输出示例:package:/data/app/~~xxx/com.example-xxx==/base.apk
# 把 APK 拉到本地adb pull /data/app/~~xxx/com.xxx-xxx==/base.apk ./base.apk# 从 APK 解出 libflutter.sounzip -p base.apk lib/arm64-v8a/libflutter.so > libflutter.so
/data/local/tmp/ecapture tls \--libssl /data/app/~~xxx/com.xxx-xxx==/base.apk \--ssl_write_addr 0x73DB50 --ssl_read_addr 0x73D348 \--ssl_version 'boringssl 1.1.1'
--ssl_write_addr / --ssl_read_addr 传的始终是相对于 libflutter.so 文件本身的偏移 ( IDA VA - 0x1000 ),与情况一相同。传入 APK 路径时,ecapture 会自动加上 so 在 APK 内的数据偏移,无需手动计算。
/data/local/tmp/ecapture tls \--libssl /data/app/.../lib/arm64/libflutter.so \--ssl_write_addr 0x73DB50 --ssl_read_addr 0x73D348 \--ssl_version 'boringssl 1.1.1'
ecapture 项目: https://github.com/gojue/ecapture cilium/ebpf 文档: https://pkg.go.dev/github.com/cilium/ebpf BoringSSL 源码: https://boringssl.googlesource.com/boringssl 修改后的实现: https://github.com/Litt1eQ/ecapture