WeChat 8.0.72 发消息 API 逆向工程记录
记录时间:2026-05-22 目标:找到 WeChat 8.0.72 在 Android 15 (Pixel 10 Fold) 上发送文本消息的可靠 API 工具:LSPosed 模块 + Frida 17.9.10 + adb
一、已验证的工作流程(接收侧)
自动回复闭环的接收部分已完全跑通:
- Termux API 检测微信通知
am start -n com.tencent.mm/.ui.LauncherUI唤醒微信到前台copy_db()复制 EnMicroMsg.db + WAL + SHM 到 Termux homesqlcipher解密(key=e03ce81, compat=1, page_size=1024)query_new_messages()读取msgId > last_msg_id- 调用 hermes-agent SSE API 生成回复
- 发送侧:目前阻塞中(见下方)
二、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.bLinkedList),不是立即写入 DBu01.f2SyncProcessor 每几秒运行一次,从队列取消息 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.g2u01.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,而是:
- 将消息添加到
u01.g2.b(LinkedList)队列 - 或调用
u01.e2的某个方法 - 或直接调用
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)→ voidb(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) 完全没有被调用。
七、已知问题与限制
- Frida hook 导致微信崩溃:hook 过多方法时,微信进程会崩溃重启。建议最多 hook 2-3 个方法。
- logcat 缓冲区刷新:LSPosed 日志在 logcat 中会被快速刷新,需及时抓取或使用
adb logcat -d。 - FUSE /sdcard 限制:root/system_server 无法读取 /sdcard/ 上的文件,安装 APK 需先
cp到/data/local/tmp/。 - 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 最终验证)
m1.a()必须在后台线程执行:在主线程调用会立即 NPE。- NPE 无害:
u01.m1.a()内部Objects.requireNonNull在发送逻辑完成后抛出,消息已成功入队。 - toUser 用显示名称:
u01.r1.a("文件传输助手")有效。filehelper(wxid)无法成功。 - 无需 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 联系人的发送流程