RESEARCH.md 7.63 KB

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 字段映射

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 中)

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 触发。

// 在 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 内)

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_ 开头或 <phone>@stranger wxid_abc123
群聊 <数字>@chatroom 12345678901@chatroom

从 EnMicroMsg.db 查询:

-- 查 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 图片发送尚未逆向,留待后续。