# 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 本地编译环境搭建 ```bash # 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 安装到手机 ```bash # 注意:/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 查看日志 ```bash # 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 ```java // 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 脚本只调用了: ```javascript builder.e(content) builder.h(0) result = builder.a() result.a() ``` 但 LSPosed 模块中通过 `XposedHelpers.setIntField` 补充了: ```java XposedHelpers.setIntField(builder, "f", 0); // 关键! XposedHelpers.setIntField(builder, "i", 4); // 关键! ``` **加上这两个字段后,消息成功发送并出现在聊天记录中。** ### 4.3 完整有效调用代码 ```java 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` — 消息 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 联系人的发送流程