# WeChat 非 A11y 发送消息 — 逆向研究报告 目标:WeChat 8.0.74 / Android 16 / Pixel 10 Pro Fold (rooted + LSPosed JingMatrix Vector v1.11.0) 约束:不封号,Research only,未触发真实发送 --- ## 一、三条路线结论 | 路线 | 原理 | 可行性 | 理由 | |---|---|---|---| | **Route A** | LSPosed hook 调用 WeChat 内部 `NetSceneSendMsg` API | ✅ **可行** | 已找到完整调用链,同正常用户发送路径完全一致 | | **Route B** | Hook 网络层(OkHttp/SSL 拦截) | ❌ **不可行** | 消息发送走 MMTLS(WeChat 自定义加密 TCP),不经 OkHttp/HTTPS | | **Route C** | 直写 SQLite `message` 表 | ❌ **不可行** | DB 写入只是本地状态,不触发网络发送,消息对方收不到 | --- ## 二、Route A — 完整调用链(逆向结果) ### 2.1 关键类(DEX 位置) | 类名 | DEX | 用途 | |---|---|---| | `w11.s1` | classes9.dex | `SendMsgCgiFactory` 工厂入口(static `a(talker)`)| | `w11.r1` | classes11.dex | Builder —— 设置 talker/content/type/pipeline | | `w11.p1` | classes9.dex | PPC Pipeline 枚举:`TEXT(d)` / `IMAGE(e)` / `VIDEO(f)` / ... | | `w11.r0` | classes12.dex | `NetSceneSendMsg` —— 写 DB + 触发网络发送 | | `w11.c1` | classes5.dex | `RevokeMsgManager` —— 发送前的撤回状态检查 | | `dk5.s5` | classes9.dex | `SendMsgMgr` —— 高层 API(`Ni(content,talker,isGroup,kl)`)| | `tg3.t1` | classes.dex | 服务定位器入口 —— `t1.a()` 返回 `dk5.s5` 单例 | | `com.tencent.mm.storage.f9` | - | 消息存储对象(写入 `message` 表的实体)| ### 2.2 调用链(文本消息) ``` 触发方(BroadcastReceiver in WeChat process) │ ├─ [方案 A1 - 直接 Factory] │ w11.s1.a(talker) → 创建 w11.r1(l=p1.TEXT) │ .e(content) → 设置文本内容(fields: d) │ .b() → executeByPPC,无回调 │ ├─ [方案 A2 - 高层 API] │ (dk5.s5) tg3.t1.a() │ .Ni(content, talker, false, null) → 1 对 1 文本 │ .Ni(content, talker, true, null) → 群聊文本 │ └─ 两种方案最终都走: w11.r0(talker, content, type=1, ...) ├─ f9.setType(1) → message.type = 1(文本) ├─ x3.bj().N9(f9, true) → 写入 message 表(status=SENDING=1) └─ r0.H() → r0.doScene() ├─ lVar.c = "/cgi-bin/micromsg-bin/newsendmsg" ├─ lVar.d = 522 (cmdId) └─ dispatch() → MMTLS TCP 二进制 protobuf ``` ### 2.3 `w11.r1` Builder 字段映射 ```java r1.a = "" // 未知(sender相关?) r1.b = "" // talker(通过 g(talker) 设置) r1.d = "" // content(通过 e(content) 设置) r1.e = 1 // 消息类型(默认=1文本;通过 h(type) 设置) r1.j = new h7() // 消息附加信息 r1.l = p1.TEXT // PPC Pipeline 类型(s1.a() 默认设为 TEXT) ``` ### 2.4 `doScene()` CGI 参数(在 w11.r0 中) ```java lVar.c = "/cgi-bin/micromsg-bin/newsendmsg"; // CGI 路径 lVar.d = 522; // CmdId lVar.e = 237; // SubCmdId lVar.f = 1000000237; // FullCmdId ``` --- ## 三、Route B 为何不可行 WeChat 手机端消息**不走 HTTPS/OkHttp**。发送路径: - 持久 TCP 长连接 → MMTLS 加密(WeChat 自研,类似 TLS 但不是 TLS)→ 二进制 protobuf - OkHttp 只处理微信里的 Finder(视频号)等 HTTP CGI 业务,不处理 IM 消息 - SSL Unpinning(Frida/LSPosed)只能解 TLS,对 MMTLS 无效 - 如要 hook 网络层,需要逆向 `libtencent_mars.so`(Mars 框架的 MMTLS native 实现)——极复杂,不在此路线 --- ## 四、Route C 为何不可行 从 `w11.r0` 构造函数逆向结果: 1. 构造时写入 `message` 表(status = SENDING = 1) 2. 然后调用 `doScene()` 发起 MMTLS 网络请求 3. 成功回调后更新 status = SENT = 2 如果直接写 SQLite: - 写 status=SENT(2):本地显示已发,但对方服务器从未收到,**对方收不到** - 写 status=SENDING(1):消息卡在"发送中",WeChat 会尝试重发,但重发路径依赖正常 send 逻辑,**不一定触发** - 结论:直写 DB 无法完成实际消息投递 --- ## 五、Route A 实现方案 ### 5.1 架构 在 LSPosed 模块中,于 `handleLoadPackage` 里注册一个 `BroadcastReceiver`(在 WeChat 进程内), 监听 `com.hermes.wechat.SEND_MSG_INTERNAL`。外部通过 `adb shell am broadcast` 触发。 ```java // 在 WeChat 进程内注册 receiver context.registerReceiver(new BroadcastReceiver() { public void onReceive(Context ctx, Intent intent) { String talker = intent.getStringExtra("to"); // wxid_xxx 或 xxx@chatroom String content = intent.getStringExtra("text"); sendTextViaInternalApi(talker, content); } }, new IntentFilter("com.hermes.wechat.SEND_MSG_INTERNAL"), Context.RECEIVER_NOT_EXPORTED); ``` ### 5.2 发送逻辑(Xposed 内) ```java private static void sendTextViaInternalApi(String talker, String content) { // 必须在主线程执行(WeChat 的 DB/网络操作绑定主线程) new Handler(Looper.getMainLooper()).post(() -> { try { // 方案 A1(直接 Factory,更底层) ClassLoader cl = /* lpparam.classLoader */; Class s1 = XposedHelpers.findClass("w11.s1", cl); Object r1 = XposedHelpers.callStaticMethod(s1, "a", talker); XposedHelpers.callMethod(r1, "e", content); // set content XposedHelpers.callMethod(r1, "b"); // execute XposedBridge.log("[WeChatHook] Internal send queued: " + talker); } catch (Throwable t) { XposedBridge.log("[WeChatHook] Internal send failed: " + t); } }); } ``` ### 5.3 Talker ID 格式 | 场景 | talker 格式 | 示例 | |---|---|---| | 1 对 1 | `wxid_` 开头或 `@stranger` | `wxid_abc123` | | 群聊 | `<数字>@chatroom` | `12345678901@chatroom` | 从 `EnMicroMsg.db` 查询: ```sql -- 查 1 对 1 的 talker SELECT username FROM rcontact WHERE nickname = '目标昵称' LIMIT 1; -- 查群 talker SELECT roomname FROM chatroom WHERE roomdisplayname = '群名' LIMIT 1; ``` ### 5.4 封号风险评估 - **Route A 的发送路径与正常用户操作完全一致**:经过同样的 DB 写入、同样的 CGI 请求、同样的 MMTLS 加密 - 风险主要来自行为模式(频率、时间、内容),而非技术路径 - 建议:单次发送、不高频、不夜间大量发送 --- ## 六、AOT devirtualization 风险(来自平板登录 hook 经验) WeChat 自身的虚方法(如 `onCreate`/`initView`)在 AOT 编译后绑死直接跳转,Xposed hook 注册成功但不触发。 Route A 不受此影响,因为: - `w11.s1.a()` 是**静态方法**,无 vtable 查找 - `w11.r1.e()/.b()` 是在 r1 新建对象上调用,引用明确,不经 vtable - 触发 Receiver 的 `android.content.BroadcastReceiver.onReceive()` 是跨 APK 调用,框架层无法被 WeChat AOT 内联 即:Route A 的 hook 链路天然对 AOT devirtualization 免疫。 --- ## 七、后续:图片发送 从 `p1` 枚举可知 pipeline 类型还有 `IMAGE(e)` / `VIDEO(f)` / `CDN_IMAGE(g)`。 图片发送需先上传图片到 WeChat CDN 获取 `attachid`,然后再构造 app 消息(type=3/43)。 已验证路径(来自 `wechat-image-sender` Hermes skill,见 CLAUDE.md): `am start -a android.intent.action.SEND -t image/png` → WeChat UI 选择联系人 → 固定坐标点发送 Route A 图片发送尚未逆向,留待后续。