REVERSE_ENGINEERING_LOG.md 14.2 KB

WeChat 8.0.72 发消息 API 逆向工程记录

记录时间:2026-05-22 目标:找到 WeChat 8.0.72 在 Android 15 (Pixel 10 Fold) 上发送文本消息的可靠 API 工具:LSPosed 模块 + Frida 17.9.10 + adb


一、已验证的工作流程(接收侧)

自动回复闭环的接收部分已完全跑通:

  1. Termux API 检测微信通知
  2. am start -n com.tencent.mm/.ui.LauncherUI 唤醒微信到前台
  3. copy_db() 复制 EnMicroMsg.db + WAL + SHM 到 Termux home
  4. sqlcipher 解密(key=e03ce81, compat=1, page_size=1024)
  5. query_new_messages() 读取 msgId > last_msg_id
  6. 调用 hermes-agent SSE API 生成回复
  7. 发送侧:目前阻塞中(见下方)

二、LSPosed 模块编译与安装

2.1 Mac 本地编译环境搭建

# Java
brew install openjdk@17
export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

# Android SDK
mkdir -p ~/android-sdk/cmdline-tools
cd ~/android-sdk/cmdline-tools
curl -L "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip" -o tools.zip
unzip -q tools.zip && mv cmdline-tools latest && rm tools.zip
export ANDROID_HOME=~/android-sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
yes | sdkmanager --licenses
sdkmanager "platforms;android-34" "build-tools;34.0.0"

# 编译
cd lsposed-wechat-sendmsg
export ANDROID_HOME=~/android-sdk
./gradlew assembleRelease

# 签名
$ANDROID_HOME/build-tools/34.0.0/apksigner sign \
  --ks hermes-debug.keystore --ks-pass pass:hermes123 --key-pass pass:hermes123 \
  --in app/build/outputs/apk/release/app-release-unsigned.apk \
  --out app/build/outputs/apk/release/app-release.apk

2.2 安装到手机

# 注意:/sdcard/ 是 FUSE 挂载,system_server 无权限读取
# 必须复制到 /data/local/tmp/ 再安装
adb push app/build/outputs/apk/release/app-release.apk /sdcard/Download/
adb shell "cp /sdcard/Download/app-release.apk /data/local/tmp/"
adb shell pm install -r /data/local/tmp/app-release.apk

# LSPosed Manager 中启用模块,作用域选微信,强行停止微信后重新打开

2.3 查看日志

# LSPosed verbose 日志(最可靠)
adb shell su -c "cat /data/adb/lspd/log/verbose_*.log | grep HermesWX | tail -30"

# 或 logcat
adb logcat -s LSPosed-Bridge:I | grep HermesWX

三、ContentProvider 路径(已失败)

3.1 尝试的 URI

// query 路径
Uri uri = Uri.parse("content://com.tencent.mm.plugin.ext.message/sendTextMsg"
    + "?appid=wx00000000000000&source=openapi");
Cursor cursor = resolver.query(uri, null, null, selectionArgs, null);
// → cursor == null

// insert 路径
Uri uri = Uri.parse("content://com.tencent.mm.plugin.ext.message/sendTextMsg");
ContentValues values = new ContentValues();
values.put("talker", toUser);
values.put("content", content);
Uri result = resolver.insert(uri, values);
// → result == null

3.2 已绕过的权限检查

LSPosed 已 hook ExtContentProviderBase:

  • h() → return 1(绕过 UID/签名校验)
  • b() → return true(绕过登录检查)
  • i(Context) → return true(备用校验)

Provider 确实收到了请求(日志显示 Provider query: uri=... args=[filehelper, ...]),但内部返回 null。可能原因:

  • appid 不正确
  • Provider 内部有其他业务逻辑校验
  • URI 路径或参数格式错误

四、u01.r1 → u01.q1 → u01.m1 路径(已验证有效 ✅)

4.1 调用链

u01.r1.a(String toUser)     → u01.q1 Builder
    Builder.e(String content)   → 设置文本
    Builder.h(int type)         → 设置类型 (0=普通)
    Builder.a()                 → u01.m1 Result
        m1.a()                  → 提交到 NetScene 队列

4.2 关键发现(LSPosed 模块验证)

Frida 测试时失败的真正原因:缺少两个关键字段赋值。

Frida 脚本只调用了:

builder.e(content)
builder.h(0)
result = builder.a()
result.a()

但 LSPosed 模块中通过 XposedHelpers.setIntField 补充了:

XposedHelpers.setIntField(builder, "f", 0);  // 关键!
XposedHelpers.setIntField(builder, "i", 4);  // 关键!

加上这两个字段后,消息成功发送并出现在聊天记录中。

4.3 完整有效调用代码

Class<?> factoryClass = XposedHelpers.findClass("u01.r1", classLoader);
Object builder = factoryClass.getMethod("a", String.class).invoke(null, toUser);
Class<?> builderClass = builder.getClass();

builderClass.getMethod("e", String.class).invoke(builder, content);
builderClass.getMethod("h", int.class).invoke(builder, 0);
XposedHelpers.setIntField(builder, "f", 0);
XposedHelpers.setIntField(builder, "i", 4);

Object result = builderClass.getMethod("a").invoke(builder);

// 必须在主线程执行 m1.a()
new Handler(Looper.getMainLooper()).post(() -> {
    result.getClass().getMethod("a").invoke(result);
});

4.4 为什么 Frida 没有检测到 WCDB INSERT

  • u01.r1 创建的消息先进入 NetScene 同步队列(u01.g2.b LinkedList),不是立即写入 DB
  • u01.f2 SyncProcessor 每几秒运行一次,从队列取消息 ID 再写入 storage.h9
  • Frida 的 hook 窗口(单次调用)捕获不到这个延迟写入
  • LSPosed 模块持续运行,所以能观察到完整的 WCDB INSERT 流程

五、手动发送的真实路径(已确认)

5.1 WCDB INSERT 调用栈

dalvik.system.VMStack.getThreadStackTrace(Native Method)
java.lang.Thread.getStackTrace(Thread.java:2842)
com.tencent.wcdb.database.SQLiteDatabase.insert(Unknown Source)
q75.a0$$e.a(Unknown Source:34)
q75.a0.y(Unknown Source:38)
com.tencent.mm.storage.h9.ta(Unknown Source:352)
com.tencent.mm.storage.h9.na(Unknown Source:1)
az0.w9.x(Unknown Source:82)
az0.l.l(Unknown Source:266)
com.tencent.mm.plugin.messenger.foundation.a2.b(Unknown Source:660)
com.tencent.mm.plugin.messenger.foundation.a2.c(Unknown Source:9)
com.tencent.mm.plugin.messenger.foundation.a2.a(Unknown Source:551)
com.tencent.mm.plugin.messenger.foundation.z2.a(Unknown Source:32)
com.tencent.mm.plugin.zero.a1.d(Unknown Source:119)
u01.f2.run(Unknown Source:298)
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:520)
java.util.concurrent.FutureTask.run(FutureTask.java:317)
is5.l.run(Unknown Source:242)
js5.v.run(Unknown Source:8)
java.util.concurrent.ThreadPoolExecutor.runWorker(...)
java.lang.Thread.run(Thread.java:1563)

5.2 路径分析

层级 类/方法 作用
入口 u01.f2.run() Runnable,在线程池中执行
调度 com.tencent.mm.plugin.zero.a1.d() zero 插件层
中介 com.tencent.mm.plugin.messenger.foundation.z2.a() messenger foundation
核心 com.tencent.mm.plugin.messenger.foundation.a2.a/b/c() 消息处理核心
中间层 az0.l.l() / az0.w9.x() 消息构建/序列化
存储 com.tencent.mm.storage.h9.na()/ta() 消息存储写入
DB q75.a0$$e.a() / q75.a0.y() WCDB 封装层
DB WCDB.insert(message, isSend=1) 最终写入

5.3 u01.f2 的本质

  • u01.f2 是长期存在的后台 SyncProcessor,不是为单条消息创建的
  • u01.f2 只有一个字段 d,类型是 u01.g2
  • u01.g2 的字段:
    • a = u01.e2 [u01.t] — 处理器
    • b = LinkedList<String> — 消息 ID 队列(如 128927674_1779461490188)
    • c = HashMap — 同步状态映射(如 {261975436=NetSync[261975436]})
    • d = Object — 锁对象
    • e = js5.b — 线程池标签
    • f = String — SyncProcessorThreadTag@0

u01.f2 每几秒运行一次,从 LinkedList 中取出消息 ID,调用 storage.h9 写入 DB,然后发送到服务器。

5.4 a2 类方法签名

com.tencent.mm.plugin.messenger.foundation.a2:
  a(Object, f35.r40, int, byte[], boolean, f35.pu4, ef3.z4) → void
  b(com.tencent.mm.modelbase.p0, f35.pu4, ef3.z4) → com.tencent.mm.modelbase.q0
  c(f35.i4, Object, int, f35.pu4, ef3.z4) → void

b() 方法的返回值 com.tencent.mm.modelbase.q0 和参数 com.tencent.mm.modelbase.p0 表明 a2 是 NetScene 框架的封装层。

5.5 关键洞察

真正的发送入口不是直接创建 u01.f2,而是:

  1. 将消息添加到 u01.g2.b(LinkedList)队列
  2. 或调用 u01.e2 的某个方法
  3. 或直接调用 a2.a/b/c() 方法

但这些方法的参数涉及大量微信内部类型(f35.og6, f35.i4, f35.pu4, ef3.z4 等),通过反射构造几乎不可能。


六、Frida 脚本汇总

6.1 v1 — 基础搜索(debug_wechat_send.js)

  • 搜索 sendmsg 相关类
  • Hook u01/v01/w01 工厂方法 a(String)
  • Hook WCDB insert
  • Hook ContentProvider
  • 提供 rpc.exports.sendTestMsg()

6.2 v2 — 多调用链测试(debug_wechat_send2.js)

测试 4 种 builder 调用组合,全部无 WCDB INSERT。

6.3 v3/v5 — WCDB 调用栈(debug_wechat_send3.js / debug_wechat_send5.js)

v3 使用 Exception.$new() 获取调用栈(失败,栈为空)。
v5 使用 Thread.currentThread().getStackTrace()(成功,获取到完整 Java 调用栈)。

教训:Frida 中获取 Java 调用栈必须用 Thread.currentThread().getStackTrace(),不能用 Exception.$new()。

6.4 v4 — 全面 WCDB hook(debug_wechat_send4.js)

Hook 了 insert, insertWithOnConflict, replace, execSQL, rawQuery, SQLiteStatement.execute。

6.5 v6 — 深度 inspect u01.g2(debug_wechat_send6.js)

打印 u01.f2 和 u01.g2 的所有字段。

6.6 v7 — u01.f2 构造函数(debug_wechat_send7.js)

  • u01.f2 constructor: (u01.g2,)
  • u01.g2 constructor: (u01.t,)
  • u01.e2 方法:
    • a(f35.og6, u01.f) → void
    • b(f35.og6, u01.u) → void

6.7 v8 — 追踪创建者(debug_wechat_send8.js)

Hook u01.f2 构造函数、u01.g2 构造函数、LinkedList.add,打印调用栈。
问题:LinkedList.add 被频繁调用(各种微信事件),调用栈获取困难。u01.f2 只在启动时创建一次。

6.8 v9 — 全链路对比(debug_wechat_send9.js)

同时 hook u01.r1.a, u01.q1.e, u01.q1.a, u01.m1.a, a2.b, a2.c, WCDB。
导致微信崩溃(hook 太多方法)。

6.9 v10 — 轻量 WCDB + f2(debug_wechat_send10.js)

只 hook WCDB insert 和 u01.f2.run,避免崩溃。

6.10 v11 — 终极验证(debug_wechat_send11.js)

只 hook u01.r1.a(String) + WCDB。
关键结论:手动发送时 u01.r1.a(String) 完全没有被调用。


七、已知问题与限制

  1. Frida hook 导致微信崩溃:hook 过多方法时,微信进程会崩溃重启。建议最多 hook 2-3 个方法。
  2. logcat 缓冲区刷新:LSPosed 日志在 logcat 中会被快速刷新,需及时抓取或使用 adb logcat -d。
  3. FUSE /sdcard 限制:root/system_server 无法读取 /sdcard/ 上的文件,安装 APK 需先 cp 到 /data/local/tmp/。
  4. WAL 复制问题:复制 WAL 到 Termux home 可解决 sqlcipher 解密问题。

八、最终方案:u01.r1 反射路径(已采用 ✅)

方案对比

方案 状态 说明
u01.r1 反射 ✅ 已采用 通过 LSPosed 模块直接调用,无需微信前台
AccessibilityService ✅ 已实现备用 需要微信前台,作为 fallback
ContentProvider ❌ 已放弃 WeChat 8.0.72 所有变体均返回 null
storage.h9 直接插入 ❌ 未尝试 过于底层,风险高

当前架构(2026-05-23 已验证 ✅)

hermes_wechat_daemon.py
    ↓ am broadcast (com.hermes.wechat.SEND_MSG)
LSPosed 模块 (HermesWeChatModule)
    ↓ u01.r1.a(toUser) → builder (toUser 用显示名称,如"文件传输助手")
    ↓ builder.e(content) + setIntField("f", 0) + setIntField("i", 4)
    ↓ builder.a() → m1
    ↓ new Thread(() -> m1.a()).start()   // 后台线程提交,忽略 NPE
WeChat NetScene 队列
    ↓ u01.f2 SyncProcessor 定时消费
WCDB message 表 INSERT
    ↓ 网络发送

关键发现(2026-05-23 最终验证)

  1. m1.a() 必须在后台线程执行:在主线程调用会立即 NPE。
  2. NPE 无害:u01.m1.a() 内部 Objects.requireNonNull 在发送逻辑完成后抛出,消息已成功入队。
  3. toUser 用显示名称:u01.r1.a("文件传输助手") 有效。filehelper(wxid)无法成功。
  4. 无需 Provider init:ExtControlProviderMsg 的初始化对 u01.r1 路径无影响。

待优化

  • [x] 文本消息发送(2026-05-23 已验证 ✅)
  • [ ] 支持发送图片/语音/文件等多媒体消息
  • [ ] 处理发送失败重试
  • [ ] 支持群聊 @ 某人
  • [ ] 微信版本升级后的兼容性(u01.r1 可能改名)

九、文件位置

文件 路径
LSPosed 模块源码 lsposed-wechat-sendmsg/app/src/main/java/.../HermesWeChatModule.java
Daemon 主脚本 local-testkit/hermes_wechat_daemon.py
Frida 脚本 v1-v11 local-testkit/debug_wechat_send*.js
开发计划 local-testkit/plan.md
本记录 local-testkit/REVERSE_ENGINEERING_LOG.md

十、环境信息

  • 设备:Pixel 10 Fold
  • Android:15
  • WeChat:8.0.72
  • Termux + Termux:API
  • LSPosed (Zygisk)
  • Frida 17.9.10 (client + server)
  • sqlcipher 3.x

十一、AccessibilityService 迭代记录

11.1 初始实现(v1)

  • 实现 HermesAccessibilityService 接收 com.hermes.wechat.SEND_MSG 广播
  • Flow: 启动微信 → 检测是否已在聊天界面 → 搜索联系人 → 输入消息 → 点击发送
  • 问题:getRootInActiveWindow() 在微信未前台时返回桌面 launcher
  • 问题:搜索使用 wxid(如 filehelper),但微信搜索框需要显示名称(如 文件助手)

11.2 显示名称支持(v2)

  • 广播 intent 新增 toUserDisplayName 字段
  • HermesAccessibilityService 优先使用 toUserDisplayName 进行搜索和点击结果匹配
  • hermes_wechat_daemon.py 新增 contact_display_names 配置映射表
  • test_send_msg.py 同步支持 toUserDisplayName
  • APK 重新编译并安装成功

11.3 待验证

  • [ ] 使用 toUserDisplayName="文件助手" 搜索并发送消息到 filehelper
  • [ ] 确认消息实际出现在聊天界面(需要手动检查微信)
  • [ ] 处理搜索无结果时的重试逻辑
  • [ ] 测试非 filehelper 联系人的发送流程