Commit b4a635a1a730ee2ec24960fb28f15e2b5aa56a2f

Authored by yibo
1 parent 9efb9656

feat(wechathook): 修复 Hermes 破坏的平板登录 + 入库 do_not_break 防护文档

Hermes 重编 APK 时连踩三坑导致平板登录失效:
1. 用 XposedBridgeApi.jar 而非 LSPosed legacy JAR 编译 → 所有 hook NoSuchMethodError
2. xposed_init 放在 APK 根目录而非 assets/ → daemon 验证失败每次重启自动删模块 DB 注册
3. 换了 keystore → INSTALL_FAILED_UPDATE_INCOMPATIBLE

修复:从 wechathook_v2_signed.apk(正确 legacy JAR 编译)提取 DEX 复用,重打包含正确
assets/xposed_init,debug.keystore 签名,fix_lspd_final.py 重建 DB 注册。
2026-07-04 重启后验证平板登录恢复正常,所有 tablet hook 日志无 NoSuchMethodError。

新增文件:
- wechathook_v2_signed.apk: 黄金基准 APK(legacy JAR 编译,DEX 可复用)
- do_not_break.md: 给 Hermes 的禁止事项清单
- hermes_task.md: 已在顶部加 DO NOT BREAK 警告 banner
- analyze_wechathook.py: DEX 字节码分析工具(Tinker patch)
- rebuild_wechathook.sh / update_wechathook_db.sh: Mac 侧重建脚本
- termux_build.sh: Hermes 的手机侧 build script(有编译 JAR bug,留作参考)
- wechat_send_internal/: Hermes 对 WeChat 内部发送 API 的研究产出(进行中)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wechat_send_internal/RESEARCH.md 0 → 100644
  1 +# WeChat 非 A11y 发送消息 — 逆向研究报告
  2 +
  3 +目标:WeChat 8.0.74 / Android 16 / Pixel 10 Pro Fold (rooted + LSPosed JingMatrix Vector v1.11.0)
  4 +约束:不封号,Research only,未触发真实发送
  5 +
  6 +---
  7 +
  8 +## 一、三条路线结论
  9 +
  10 +| 路线 | 原理 | 可行性 | 理由 |
  11 +|---|---|---|---|
  12 +| **Route A** | LSPosed hook 调用 WeChat 内部 `NetSceneSendMsg` API | ✅ **可行** | 已找到完整调用链,同正常用户发送路径完全一致 |
  13 +| **Route B** | Hook 网络层(OkHttp/SSL 拦截) | ❌ **不可行** | 消息发送走 MMTLS(WeChat 自定义加密 TCP),不经 OkHttp/HTTPS |
  14 +| **Route C** | 直写 SQLite `message` 表 | ❌ **不可行** | DB 写入只是本地状态,不触发网络发送,消息对方收不到 |
  15 +
  16 +---
  17 +
  18 +## 二、Route A — 完整调用链(逆向结果)
  19 +
  20 +### 2.1 关键类(DEX 位置)
  21 +
  22 +| 类名 | DEX | 用途 |
  23 +|---|---|---|
  24 +| `w11.s1` | classes9.dex | `SendMsgCgiFactory` 工厂入口(static `a(talker)`)|
  25 +| `w11.r1` | classes11.dex | Builder —— 设置 talker/content/type/pipeline |
  26 +| `w11.p1` | classes9.dex | PPC Pipeline 枚举:`TEXT(d)` / `IMAGE(e)` / `VIDEO(f)` / ... |
  27 +| `w11.r0` | classes12.dex | `NetSceneSendMsg` —— 写 DB + 触发网络发送 |
  28 +| `w11.c1` | classes5.dex | `RevokeMsgManager` —— 发送前的撤回状态检查 |
  29 +| `dk5.s5` | classes9.dex | `SendMsgMgr` —— 高层 API(`Ni(content,talker,isGroup,kl)`)|
  30 +| `tg3.t1` | classes.dex | 服务定位器入口 —— `t1.a()` 返回 `dk5.s5` 单例 |
  31 +| `com.tencent.mm.storage.f9` | - | 消息存储对象(写入 `message` 表的实体)|
  32 +
  33 +### 2.2 调用链(文本消息)
  34 +
  35 +```
  36 +触发方(BroadcastReceiver in WeChat process)
  37 + │
  38 + ├─ [方案 A1 - 直接 Factory]
  39 + │ w11.s1.a(talker) → 创建 w11.r1(l=p1.TEXT)
  40 + │ .e(content) → 设置文本内容(fields: d)
  41 + │ .b() → executeByPPC,无回调
  42 + │
  43 + ├─ [方案 A2 - 高层 API]
  44 + │ (dk5.s5) tg3.t1.a()
  45 + │ .Ni(content, talker, false, null) → 1 对 1 文本
  46 + │ .Ni(content, talker, true, null) → 群聊文本
  47 + │
  48 + └─ 两种方案最终都走:
  49 + w11.r0(talker, content, type=1, ...)
  50 + ├─ f9.setType(1) → message.type = 1(文本)
  51 + ├─ x3.bj().N9(f9, true) → 写入 message 表(status=SENDING=1)
  52 + └─ r0.H() → r0.doScene()
  53 + ├─ lVar.c = "/cgi-bin/micromsg-bin/newsendmsg"
  54 + ├─ lVar.d = 522 (cmdId)
  55 + └─ dispatch() → MMTLS TCP 二进制 protobuf
  56 +```
  57 +
  58 +### 2.3 `w11.r1` Builder 字段映射
  59 +
  60 +```java
  61 +r1.a = "" // 未知(sender相关?)
  62 +r1.b = "" // talker(通过 g(talker) 设置)
  63 +r1.d = "" // content(通过 e(content) 设置)
  64 +r1.e = 1 // 消息类型(默认=1文本;通过 h(type) 设置)
  65 +r1.j = new h7() // 消息附加信息
  66 +r1.l = p1.TEXT // PPC Pipeline 类型(s1.a() 默认设为 TEXT)
  67 +```
  68 +
  69 +### 2.4 `doScene()` CGI 参数(在 w11.r0 中)
  70 +
  71 +```java
  72 +lVar.c = "/cgi-bin/micromsg-bin/newsendmsg"; // CGI 路径
  73 +lVar.d = 522; // CmdId
  74 +lVar.e = 237; // SubCmdId
  75 +lVar.f = 1000000237; // FullCmdId
  76 +```
  77 +
  78 +---
  79 +
  80 +## 三、Route B 为何不可行
  81 +
  82 +WeChat 手机端消息**不走 HTTPS/OkHttp**。发送路径:
  83 +- 持久 TCP 长连接 → MMTLS 加密(WeChat 自研,类似 TLS 但不是 TLS)→ 二进制 protobuf
  84 +- OkHttp 只处理微信里的 Finder(视频号)等 HTTP CGI 业务,不处理 IM 消息
  85 +- SSL Unpinning(Frida/LSPosed)只能解 TLS,对 MMTLS 无效
  86 +- 如要 hook 网络层,需要逆向 `libtencent_mars.so`(Mars 框架的 MMTLS native 实现)——极复杂,不在此路线
  87 +
  88 +---
  89 +
  90 +## 四、Route C 为何不可行
  91 +
  92 +从 `w11.r0` 构造函数逆向结果:
  93 +1. 构造时写入 `message` 表(status = SENDING = 1)
  94 +2. 然后调用 `doScene()` 发起 MMTLS 网络请求
  95 +3. 成功回调后更新 status = SENT = 2
  96 +
  97 +如果直接写 SQLite:
  98 +- 写 status=SENT(2):本地显示已发,但对方服务器从未收到,**对方收不到**
  99 +- 写 status=SENDING(1):消息卡在"发送中",WeChat 会尝试重发,但重发路径依赖正常 send 逻辑,**不一定触发**
  100 +- 结论:直写 DB 无法完成实际消息投递
  101 +
  102 +---
  103 +
  104 +## 五、Route A 实现方案
  105 +
  106 +### 5.1 架构
  107 +
  108 +在 LSPosed 模块中,于 `handleLoadPackage` 里注册一个 `BroadcastReceiver`(在 WeChat 进程内),
  109 +监听 `com.hermes.wechat.SEND_MSG_INTERNAL`。外部通过 `adb shell am broadcast` 触发。
  110 +
  111 +```java
  112 +// 在 WeChat 进程内注册 receiver
  113 +context.registerReceiver(new BroadcastReceiver() {
  114 + public void onReceive(Context ctx, Intent intent) {
  115 + String talker = intent.getStringExtra("to"); // wxid_xxx 或 xxx@chatroom
  116 + String content = intent.getStringExtra("text");
  117 + sendTextViaInternalApi(talker, content);
  118 + }
  119 +}, new IntentFilter("com.hermes.wechat.SEND_MSG_INTERNAL"),
  120 + Context.RECEIVER_NOT_EXPORTED);
  121 +```
  122 +
  123 +### 5.2 发送逻辑(Xposed 内)
  124 +
  125 +```java
  126 +private static void sendTextViaInternalApi(String talker, String content) {
  127 + // 必须在主线程执行(WeChat 的 DB/网络操作绑定主线程)
  128 + new Handler(Looper.getMainLooper()).post(() -> {
  129 + try {
  130 + // 方案 A1(直接 Factory,更底层)
  131 + ClassLoader cl = /* lpparam.classLoader */;
  132 + Class<?> s1 = XposedHelpers.findClass("w11.s1", cl);
  133 + Object r1 = XposedHelpers.callStaticMethod(s1, "a", talker);
  134 + XposedHelpers.callMethod(r1, "e", content); // set content
  135 + XposedHelpers.callMethod(r1, "b"); // execute
  136 + XposedBridge.log("[WeChatHook] Internal send queued: " + talker);
  137 + } catch (Throwable t) {
  138 + XposedBridge.log("[WeChatHook] Internal send failed: " + t);
  139 + }
  140 + });
  141 +}
  142 +```
  143 +
  144 +### 5.3 Talker ID 格式
  145 +
  146 +| 场景 | talker 格式 | 示例 |
  147 +|---|---|---|
  148 +| 1 对 1 | `wxid_` 开头或 `<phone>@stranger` | `wxid_abc123` |
  149 +| 群聊 | `<数字>@chatroom` | `12345678901@chatroom` |
  150 +
  151 +从 `EnMicroMsg.db` 查询:
  152 +```sql
  153 +-- 查 1 对 1 的 talker
  154 +SELECT username FROM rcontact WHERE nickname = '目标昵称' LIMIT 1;
  155 +-- 查群 talker
  156 +SELECT roomname FROM chatroom WHERE roomdisplayname = '群名' LIMIT 1;
  157 +```
  158 +
  159 +### 5.4 封号风险评估
  160 +
  161 +- **Route A 的发送路径与正常用户操作完全一致**:经过同样的 DB 写入、同样的 CGI 请求、同样的 MMTLS 加密
  162 +- 风险主要来自行为模式(频率、时间、内容),而非技术路径
  163 +- 建议:单次发送、不高频、不夜间大量发送
  164 +
  165 +---
  166 +
  167 +## 六、AOT devirtualization 风险(来自平板登录 hook 经验)
  168 +
  169 +WeChat 自身的虚方法(如 `onCreate`/`initView`)在 AOT 编译后绑死直接跳转,Xposed hook 注册成功但不触发。
  170 +
  171 +Route A 不受此影响,因为:
  172 +- `w11.s1.a()` 是**静态方法**,无 vtable 查找
  173 +- `w11.r1.e()/.b()` 是在 r1 新建对象上调用,引用明确,不经 vtable
  174 +- 触发 Receiver 的 `android.content.BroadcastReceiver.onReceive()` 是跨 APK 调用,框架层无法被 WeChat AOT 内联
  175 +
  176 +即:Route A 的 hook 链路天然对 AOT devirtualization 免疫。
  177 +
  178 +---
  179 +
  180 +## 七、后续:图片发送
  181 +
  182 +从 `p1` 枚举可知 pipeline 类型还有 `IMAGE(e)` / `VIDEO(f)` / `CDN_IMAGE(g)`。
  183 +图片发送需先上传图片到 WeChat CDN 获取 `attachid`,然后再构造 app 消息(type=3/43)。
  184 +
  185 +已验证路径(来自 `wechat-image-sender` Hermes skill,见 CLAUDE.md):
  186 +`am start -a android.intent.action.SEND -t image/png` → WeChat UI 选择联系人 → 固定坐标点发送
  187 +
  188 +Route A 图片发送尚未逆向,留待后续。
... ...
wechat_send_internal/WeChatSendHook.java 0 → 100644
  1 +package com.example.wechathook;
  2 +
  3 +import android.content.BroadcastReceiver;
  4 +import android.content.Context;
  5 +import android.content.Intent;
  6 +import android.content.IntentFilter;
  7 +import android.os.Handler;
  8 +import android.os.Looper;
  9 +import de.robv.android.xposed.IXposedHookLoadPackage;
  10 +import de.robv.android.xposed.XposedBridge;
  11 +import de.robv.android.xposed.XposedHelpers;
  12 +import de.robv.android.xposed.callbacks.XC_LoadPackage;
  13 +
  14 +/**
  15 + * WeChatSendHook: Route A — 通过 WeChat 内部 API 发送文本消息。
  16 + *
  17 + * 调用链(逆向 WeChat 8.0.74):
  18 + * 外部 am broadcast → BroadcastReceiver (WeChat 进程内)
  19 + * → w11.s1.a(talker) → w11.r1 (SendMsgCgiFactory, p1.TEXT pipeline)
  20 + * → r1.e(content) → 设置文本
  21 + * → r1.b() → executeByPPC
  22 + * → w11.r0(talker, content, type=1, ...)
  23 + * → message 表写入 (status=SENDING)
  24 + * → /cgi-bin/micromsg-bin/newsendmsg over MMTLS
  25 + *
  26 + * 触发:
  27 + * adb shell am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL \
  28 + * --es to "wxid_xxx" --es text "Hello from hook"
  29 + *
  30 + * Talker 格式:
  31 + * 1 对 1: wxid_xxx (对方 WeChat ID)
  32 + * 群聊: 12345678901@chatroom
  33 + *
  34 + * 注意:发送在 WeChat 主线程执行,与用户正常发送路径完全一致。
  35 + * 不触发 A11y,不走 UI 自动化,消息经 MMTLS 加密发出。
  36 + *
  37 + * 类位置(WeChat 8.0.74 多 DEX):
  38 + * w11.s1 — classes9.dex
  39 + * w11.r1 — classes11.dex
  40 + * w11.r0 — classes12.dex
  41 + * p1.TEXT — classes9.dex (enum value 'd')
  42 + */
  43 +public class WeChatSendHook implements IXposedHookLoadPackage {
  44 +
  45 + private static final String ACTION = "com.hermes.wechat.SEND_MSG_INTERNAL";
  46 + private static ClassLoader sWeChat;
  47 +
  48 + @Override
  49 + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
  50 + if (!"com.tencent.mm".equals(lpparam.packageName)) return;
  51 + // Only hook the main process (not :push/:tools sub-processes)
  52 + if (!lpparam.processName.equals("com.tencent.mm")) return;
  53 +
  54 + sWeChat = lpparam.classLoader;
  55 + XposedBridge.log("[WeChatSendHook] Injected, registering receiver");
  56 +
  57 + // Hook Application.onCreate to get Context for receiver registration.
  58 + // android.app.Application.onCreate() is a framework call — never AOT-inlined.
  59 + XposedHelpers.findAndHookMethod(
  60 + "android.app.Application", null,
  61 + "onCreate",
  62 + new de.robv.android.xposed.XC_MethodHook() {
  63 + @Override
  64 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  65 + Context ctx = (Context) param.thisObject;
  66 + if (ctx.getPackageName().equals("com.tencent.mm")) {
  67 + registerSendReceiver(ctx);
  68 + }
  69 + }
  70 + });
  71 + }
  72 +
  73 + private static void registerSendReceiver(Context ctx) {
  74 + BroadcastReceiver receiver = new BroadcastReceiver() {
  75 + @Override
  76 + public void onReceive(Context context, Intent intent) {
  77 + String talker = intent.getStringExtra("to");
  78 + String text = intent.getStringExtra("text");
  79 + if (talker == null || talker.isEmpty() || text == null || text.isEmpty()) {
  80 + XposedBridge.log("[WeChatSendHook] Received intent missing 'to' or 'text'");
  81 + return;
  82 + }
  83 + XposedBridge.log("[WeChatSendHook] Queuing send to=" + talker + " len=" + text.length());
  84 + sendOnMainThread(talker, text);
  85 + }
  86 + };
  87 + IntentFilter filter = new IntentFilter(ACTION);
  88 + // NOT_EXPORTED (0x4) — only accept from root/same-uid callers via am broadcast
  89 + ctx.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
  90 + XposedBridge.log("[WeChatSendHook] Receiver registered for " + ACTION);
  91 + }
  92 +
  93 + private static void sendOnMainThread(String talker, String text) {
  94 + // WeChat's send path touches SQLite and the network dispatcher from the main thread.
  95 + new Handler(Looper.getMainLooper()).post(() -> {
  96 + try {
  97 + sendTextInternal(talker, text);
  98 + } catch (Throwable t) {
  99 + XposedBridge.log("[WeChatSendHook] Send failed: " + t);
  100 + }
  101 + });
  102 + }
  103 +
  104 + /**
  105 + * Core send via w11.s1 factory (SendMsgCgiFactory):
  106 + * s1.a(talker) → r1 with pipeline=p1.TEXT, talker pre-set
  107 + * r1.e(content) → set field d (content)
  108 + * r1.b() → executeByPPC (no callback)
  109 + *
  110 + * r1 field mapping (WeChat 8.0.74):
  111 + * field b = talker (set by g())
  112 + * field d = content (set by e())
  113 + * field e = type (default 1=text, set by h())
  114 + * field l = pipeline (set to p1.TEXT by s1.a())
  115 + */
  116 + private static void sendTextInternal(String talker, String text) throws Throwable {
  117 + Class<?> s1 = XposedHelpers.findClass("w11.s1", sWeChat);
  118 + // s1.a(talker) → r1 with g(talker) pre-called and l=p1.TEXT
  119 + Object r1 = XposedHelpers.callStaticMethod(s1, "a", talker);
  120 + // r1.e(content) → sets field d
  121 + XposedHelpers.callMethod(r1, "e", text);
  122 + // r1.b() → d(this,null,1,null) → c(q1.d) → executeByPPC
  123 + XposedHelpers.callMethod(r1, "b");
  124 + XposedBridge.log("[WeChatSendHook] Sent: to=" + talker);
  125 + }
  126 +}
... ...
wechat_send_internal/test_send.sh 0 → 100644
  1 +#!/usr/bin/env bash
  2 +# Test Route A send hook via adb
  3 +# Usage: bash test_send.sh <wxid> "<text>"
  4 +# Example: bash test_send.sh wxid_abc123 "Hello from hook"
  5 +#
  6 +# NOTE: This is a TEST SCRIPT — read-only / dry-run by default.
  7 +# Set DRY_RUN=0 to actually trigger the broadcast.
  8 +
  9 +set -euo pipefail
  10 +
  11 +TALKER="${1:-}"
  12 +TEXT="${2:-}"
  13 +DRY_RUN="${DRY_RUN:-1}" # Default: print only, don't fire
  14 +
  15 +if [ -z "$TALKER" ] || [ -z "$TEXT" ]; then
  16 + echo "Usage: DRY_RUN=0 bash test_send.sh <talker> '<text>'"
  17 + echo " talker examples:"
  18 + echo " wxid_abc123 (1-on-1)"
  19 + echo " 12345678901@chatroom (group)"
  20 + exit 1
  21 +fi
  22 +
  23 +CMD="adb shell am broadcast \
  24 + -a com.hermes.wechat.SEND_MSG_INTERNAL \
  25 + --es to \"$TALKER\" \
  26 + --es text \"$TEXT\""
  27 +
  28 +echo "Command to run:"
  29 +echo " $CMD"
  30 +echo ""
  31 +
  32 +if [ "$DRY_RUN" = "1" ]; then
  33 + echo "[DRY RUN] Not executing. Set DRY_RUN=0 to actually send."
  34 +else
  35 + echo "[SENDING] Executing broadcast..."
  36 + eval "$CMD"
  37 + echo ""
  38 + echo "Check logcat for: [WeChatSendHook]"
  39 + echo " adb logcat -s Xposed:D | grep WeChatSendHook"
  40 +fi
... ...
xhs_lsposed_a16_fix/analyze_wechathook.py 0 → 100644
  1 +#!/usr/bin/env python3
  2 +"""
  3 +analyze_wechathook.py — 从 Tinker patch DEX 分析 WeChat 8.0.74 消息发送路径
  4 +在 Termux 上运行: python3 /sdcard/wechathook/analyze_wechathook.py
  5 +
  6 +目标:
  7 +1. 解码 w11.r0 构造器字节码,看它调了什么
  8 +2. 解码 gm0.j1.i() 字节码,确认它检查的是字段 m
  9 +3. 检查 gm0.j1.m 在运行时是否 null(通过分析 gm0.j1.g() 是否被调用)
  10 +"""
  11 +import struct, os, subprocess
  12 +
  13 +# Tinker patch DEX (需要有 root 才能访问)
  14 +TINKER_APK = "/data/user/0/com.tencent.mm/tinker/patch-cf87f687/dex/tinker_classN.apk"
  15 +WORK_DIR = "/data/local/tmp/wh_dex_analysis"
  16 +
  17 +def ensure_dex():
  18 + os.makedirs(WORK_DIR, exist_ok=True)
  19 + dex = f"{WORK_DIR}/classes12.dex"
  20 + if not os.path.exists(dex):
  21 + # 需要 root 拷贝
  22 + r = subprocess.run(["su", "-c", f"cp {TINKER_APK} /data/local/tmp/_tinker.apk && chmod 644 /data/local/tmp/_tinker.apk"],
  23 + capture_output=True, text=True)
  24 + if r.returncode != 0:
  25 + print("需要 root 权限:", r.stderr)
  26 + return None
  27 + import zipfile
  28 + with zipfile.ZipFile("/data/local/tmp/_tinker.apk") as z:
  29 + z.extract("classes12.dex", WORK_DIR)
  30 + print(f"Extracted classes12.dex ({os.path.getsize(dex)} bytes)")
  31 + return dex
  32 +
  33 +def uleb128(data, off):
  34 + r, s = 0, 0
  35 + while True:
  36 + b = data[off]; off += 1
  37 + r |= (b & 0x7F) << s
  38 + if not (b & 0x80): break
  39 + s += 7
  40 + return r, off
  41 +
  42 +def parse_dex(path):
  43 + data = open(path, "rb").read()
  44 + h = lambda o: struct.unpack_from('<I', data, o)[0]
  45 + ss, so = h(56), h(60)
  46 + ts, to = h(64), h(68)
  47 + fs, fo = h(80), h(84)
  48 + ms, mo = h(88), h(92)
  49 + cs, co = h(96), h(100)
  50 + ps, po = h(72), h(76)
  51 +
  52 + def gs(i):
  53 + if i >= ss: return f"?s{i}"
  54 + off = h(so + i*4); _, o2 = uleb128(data, off)
  55 + return data[o2:data.index(b'\x00', o2)].decode('utf-8', errors='replace')
  56 + def gt(i):
  57 + if i >= ts: return f"?t{i}"
  58 + return gs(h(to + i*4))
  59 + def gp(pi):
  60 + if pi >= ps: return "?proto"
  61 + pb = po + pi*12
  62 + ret = gt(h(pb+4)); poff = h(pb+8); params = []
  63 + if poff:
  64 + n = h(poff)
  65 + for k in range(n): params.append(gt(struct.unpack_from('<H', data, poff+4+k*2)[0]))
  66 + return f"({','.join(params)}){ret}"
  67 +
  68 + classes = {}
  69 + for i in range(cs):
  70 + base = co + i*32
  71 + ci = h(base)
  72 + if ci >= ts: continue
  73 + name = gt(ci)
  74 + cdo = h(base+24)
  75 + if cdo == 0: continue
  76 + off = cdo
  77 + sf, off = uleb128(data, off)
  78 + inf, off = uleb128(data, off)
  79 + dm, off = uleb128(data, off)
  80 + vm, off = uleb128(data, off)
  81 +
  82 + fields, methods = [], []
  83 + fi = 0
  84 + for _ in range(sf+inf):
  85 + d, off = uleb128(data, off); fi += d
  86 + fl, off = uleb128(data, off)
  87 + if fi < fs:
  88 + fb = fo+fi*8
  89 + ft = struct.unpack_from('<H', data, fb+2)[0]
  90 + fn = struct.unpack_from('<I', data, fb+4)[0]
  91 + fields.append((gs(fn), gt(ft), fl))
  92 +
  93 + mi = 0
  94 + for _ in range(dm+vm):
  95 + d, off = uleb128(data, off); mi += d
  96 + fl, off = uleb128(data, off)
  97 + co2, off = uleb128(data, off)
  98 + if mi < ms:
  99 + mb = mo+mi*8
  100 + pi = struct.unpack_from('<H', data, mb+2)[0]
  101 + mn = struct.unpack_from('<I', data, mb+4)[0]
  102 + methods.append((gs(mn), gp(pi), fl, co2))
  103 + classes[name] = {'fields': fields, 'methods': methods}
  104 + return classes, data, to, ts, fo, fs, mo, ms, po, ps, so, ss
  105 +
  106 +def dump_bytecode(data, code_off, max_insns=80):
  107 + """简单 Dalvik 字节码 dump(只看调用指令)"""
  108 + if code_off == 0: return " (abstract)"
  109 + # code_item header: registers_size(2) ins_size(2) outs_size(2) tries_size(2) debug_info_off(4) insns_size(4)
  110 + insns_size = struct.unpack_from('<I', data, code_off+12)[0]
  111 + insns_off = code_off + 16
  112 + lines = []
  113 + i = 0
  114 + while i < insns_size and len(lines) < max_insns:
  115 + op = data[insns_off + i*2]
  116 + # Only show invoke-* and sget/sput/iget/iput
  117 + if op in (0x6e, 0x6f, 0x70, 0x71, 0x72, # invoke-virtual, super, direct, static, interface
  118 + 0x74, 0x75, 0x76, 0x77, 0x78, # invoke-virtual/range etc
  119 + 0x60, 0x61, 0x62, 0x63, 0x64, # iget, iget-wide, iget-object, iget-bool, iget-byte
  120 + 0x68, 0x69, 0x6a, 0x6b, 0x6c, # sget, sget-wide, sget-object, sget-bool, sget-byte
  121 + 0x59, 0x5a, 0x5b, 0x5c, 0x5d, # iput, iput-wide, iput-object, iput-bool, iput-byte
  122 + 0x67, 0x69, 0x6b): # sput, sput-object, sput-bool
  123 + ref = struct.unpack_from('<H', data, insns_off + i*2 + 2)[0]
  124 + lines.append(f" +{i*2:04x} op=0x{op:02x} ref={ref}")
  125 + i += 3
  126 + elif op in (0x0e, 0x0f, 0x10, 0x11): # return-void, return, return-wide, return-object
  127 + lines.append(f" +{i*2:04x} return")
  128 + break
  129 + elif op == 0x27: # throw
  130 + lines.append(f" +{i*2:04x} throw")
  131 + i += 2
  132 + else:
  133 + # Rough skip — most insns are 1-2 units
  134 + fmt = (op >> 4) & 0xf
  135 + i += 2 if op in range(0x02, 0x0e) or op in range(0x1a, 0x22) or op >= 0x26 else 1
  136 + return '\n'.join(lines) if lines else " (no invoke/field ops)"
  137 +
  138 +def main():
  139 + dex = ensure_dex()
  140 + if not dex:
  141 + return
  142 +
  143 + print("Parsing DEX...")
  144 + classes, data, to, ts, fo, fs, mo, ms, po, ps, so, ss = parse_dex(dex)
  145 +
  146 + # ── 1. gm0.j1 structure ──────────────────────────────────────────
  147 + print("\n" + "="*60)
  148 + print("1. gm0.j1 class")
  149 + print("="*60)
  150 + j1 = classes.get("Lgm0/j1;")
  151 + if j1:
  152 + print("Static fields:")
  153 + for name, typ, flags in j1['fields']:
  154 + print(f" {name}: {typ} flags=0x{flags:x}")
  155 + print("\nMethods with signatures:")
  156 + for name, proto, flags, co2 in j1['methods']:
  157 + flag_s = '+'.join(f for f, b in [('public',1),('static',8),('sync',0x20),('ctor',0x10000)] if flags&b)
  158 + print(f" {name}: {proto} [{flag_s}]")
  159 + else:
  160 + print("NOT FOUND")
  161 +
  162 + # ── 2. w11.r0 structure ──────────────────────────────────────────
  163 + print("\n" + "="*60)
  164 + print("2. w11.r0 class")
  165 + print("="*60)
  166 + r0 = classes.get("Lw11/r0;")
  167 + if r0:
  168 + print("Static fields:")
  169 + for name, typ, flags in r0['fields']:
  170 + print(f" {name}: {typ}")
  171 + print(f"\nConstructors (looking for (String,String,int,int,long)):")
  172 + for name, proto, flags, co2 in r0['methods']:
  173 + if name == "<init>":
  174 + print(f" <init>{proto} code_off=0x{co2:x}")
  175 + else:
  176 + print("NOT FOUND")
  177 +
  178 + # ── 3. Decode gm0.j1.i() bytecode ───────────────────────────────
  179 + print("\n" + "="*60)
  180 + print("3. gm0.j1.i() bytecode (key ops)")
  181 + print("="*60)
  182 + if j1:
  183 + for name, proto, flags, co2 in j1['methods']:
  184 + if name == "i" and "()Lgm0/j1;" in proto:
  185 + print(f"gm0.j1.i() code_off=0x{co2:x}")
  186 + print(dump_bytecode(data, co2))
  187 + break
  188 +
  189 + # ── 4. Decode w11.r0 (String,String,int,int,long) ctor ──────────
  190 + print("\n" + "="*60)
  191 + print("4. w11.r0 constructor bytecode (key invoke ops)")
  192 + print("="*60)
  193 + if r0:
  194 + for name, proto, flags, co2 in r0['methods']:
  195 + if name == "<init>" and "Ljava/lang/String;Ljava/lang/String;IIJ" in proto:
  196 + print(f"<init>(String,String,int,int,long) code_off=0x{co2:x}")
  197 + print(dump_bytecode(data, co2, max_insns=40))
  198 + break
  199 +
  200 + # ── 5. Who initializes gm0.j1.m? Search for sput-object on field m ─
  201 + print("\n" + "="*60)
  202 + print("5. Searching gm0.j1.g() bytecode")
  203 + print("="*60)
  204 + if j1:
  205 + for name, proto, flags, co2 in j1['methods']:
  206 + if name == "g":
  207 + print(f"gm0.j1.g{proto} code_off=0x{co2:x}")
  208 + print(dump_bytecode(data, co2))
  209 + break
  210 +
  211 + print("\n== Done ==")
  212 +
  213 +if __name__ == "__main__":
  214 + main()
... ...
xhs_lsposed_a16_fix/do_not_break.md 0 → 100644
  1 +# WeChatHook — DO NOT BREAK 清单
  2 +
  3 +## 当前状态(2026-07-04 已验证)
  4 +
  5 +已安装 APK: `com.example.wechathook` (versionCode=1)
  6 +签名: `~/.android/debug.keystore` (CN=Android Debug,O=Android,C=US)
  7 +APK 路径: `/data/app/~~UUcd24uZ9ch3W7ZmJpIxlQ==/com.example.wechathook-Jns-UMdjPSZgIGeeIqFAwQ==/base.apk`
  8 +LSPosed DB: mid=42, scope=(42, com.tencent.mm, 0), enabled=1
  9 +功能: 微信平板登录 + WCDB hook + gm0.j1.g hook + w11.r0 hook
  10 +
  11 +## 绝对禁止事项
  12 +
  13 +### 1. 不要用 XposedBridgeApi.jar 重新编译
  14 +LSPosed Vector 运行时用的是自己混淆的 XposedHelpers,标准 XposedBridgeApi.jar 编译出来的
  15 +APK 所有 findAndHookMethod 调用全部 NoSuchMethodError。
  16 +
  17 +**必须用的 JAR**: `/tmp/LSPosed/legacy/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar`
  18 +(在 Mac 上,Pixel 上没有这个 JAR)
  19 +
  20 +如果 Mac 上没有这个 JAR,**不要重新编译**。只改 Java 源码而不重编是可以的(改 /sdcard/wechathook/WeChatHook.java),但必须用正确 JAR 编译才能安装新版本。
  21 +
  22 +### 2. APK 必须包含 assets/xposed_init
  23 +打包时必须: `zip APK.apk assets/xposed_init ...`(注意 assets/ 子目录层级)
  24 +文件内容: `com.example.wechathook.WeChatHook`
  25 +
  26 +LSPosed daemon 每次启动都验证这个文件。缺失 → 自动从 DB 删除模块 → 平板登录消失。
  27 +
  28 +根目录的 `xposed_init`(不在 assets/ 下)**不够,daemon 不认**。
  29 +
  30 +### 3. 不要换 keystore 签名
  31 +当前已安装版本用 `~/.android/debug.keystore`。
  32 +如果用不同 keystore 重签 → INSTALL_FAILED_UPDATE_INCOMPATIBLE → 必须先 uninstall(会丢失 LSPosed DB 注册)。
  33 +
  34 +### 4. 不要直接改 LSPosed DB 而不做 WAL checkpoint
  35 +改完必须: `PRAGMA wal_checkpoint(TRUNCATE)` + 把主 DB 拷覆盖 .bak .bak2 .bak3 .bak4 四个文件。
  36 +
  37 +## 安全测试约束(严禁违反)
  38 +
  39 +**只用 filehelper(文件传输助手)测试发消息,绝对不能发给真实联系人。**
  40 +微信对异常行为会封号,一旦封号无法找回。
  41 +
  42 +## 如果你需要重新安装 APK
  43 +
  44 +```bash
  45 +# 1. 备份 DB(万一要恢复)
  46 +adb shell su -c 'cp /data/adb/lspd/config/modules_config.db /data/local/tmp/modules_backup.db'
  47 +
  48 +# 2. 卸载旧版本
  49 +adb shell pm uninstall com.example.wechathook
  50 +
  51 +# 3. 安装新版本(必须已按上面规则编译 + 打包 + 签名)
  52 +adb install --no-incremental /path/to/new.apk
  53 +
  54 +# 4. 修复 DB(参考 /sdcard/wechathook/fix_lspd_final.py)
  55 +adb shell su -c '/path/to/python3 /sdcard/wechathook/fix_lspd_final.py'
  56 +
  57 +# 5. 重启
  58 +adb shell su -c reboot
  59 +
  60 +# 6. 验证
  61 +adb shell su -c 'grep -i "WeChatHook\|hookInTablet\|NoSuchMethod" /data/adb/lspd/log/modules_*.log'
  62 +```
  63 +
  64 +## 验证钩子是否正常的命令
  65 +
  66 +```bash
  67 +adb shell su -c 'grep "\[WeChatHook\]" /data/adb/lspd/log/modules_*.log | head -30'
  68 +```
  69 +
  70 +正常输出应包含(无 NoSuchMethodError):
  71 +- Hooked bk.B(x0) + bk.A()
  72 +- Hooked Activity.onResume for tablet button
  73 +- Hooked LoginPasswordUI.onCreate()
  74 +- Hooked MobileInputUI.initView() / LoginUI.initView()
  75 +- WCDB hook OK
  76 +- gm0.j1.g hook OK
... ...
xhs_lsposed_a16_fix/hermes_task.md 0 → 100644
  1 +# 任务:修通 WeChat Xposed 内部发送接口
  2 +
  3 +## 🔒 安全约束(不得违反)
  4 +
  5 +**只能用 `filehelper`(文件传输助手)测试,绝对不能给真实联系人发消息。**
  6 +测试命令固定格式:
  7 +```bash
  8 +su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "filehelper" --es text "测试"'
  9 +```
  10 +
  11 +---
  12 +
  13 +## 目标
  14 +
  15 +让 LSPosed 模块 `com.example.wechathook` 能通过 broadcast 真正把消息发进微信 message 数据库(EnMicroMsg.db),在文件传输助手收到消息。
  16 +
  17 +设备:Pixel 10 Pro Fold,Android 16,WeChat 8.0.74,LSPosed JingMatrix Vector v1.11.0。
  18 +
  19 +---
  20 +
  21 +## 工作目录
  22 +
  23 +```
  24 +/sdcard/wechathook/
  25 + WeChatHook.java ← 当前源码(已是最新版)
  26 + termux_build.sh ← Termux 上编译+安装脚本
  27 + analyze_wechathook.py ← DEX 字节码分析工具(Python,读 Tinker patch)
  28 + XposedBridgeApi.jar ← 编译依赖
  29 + android.jar ← 编译依赖(API 36)
  30 + d8.jar ← DEX 转换工具
  31 + wechathook_current.apk ← 当前安装版本
  32 +```
  33 +
  34 +---
  35 +
  36 +## 当前状态(已做了什么、失败在哪)
  37 +
  38 +### 已实现
  39 +
  40 +模块已注入所有 WeChat 进程(main / push / appbrand0 / appbrand1)。每个进程里:
  41 +1. 注册了 `com.hermes.wechat.SEND_MSG_INTERNAL` broadcast receiver
  42 +2. 挂钩了 `w11.r0` 的 4 种构造器((String,String,int,int,long) 等)
  43 +3. 挂钩了 `w11.r0.H(String)`(发送 dispatch 方法)
  44 +4. 挂钩了 `gm0.j1.g(km0.c)`(DI 内核初始化检测)
  45 +5. 挂钩了 WCDB `insertWithOnConflict`(诊断用)
  46 +
  47 +### 关键发现(失败原因)
  48 +
  49 +**发现 1:`w11.r0` 构造器永远不被 WeChat 自己调用。**
  50 +用户在 UI 点发送时,`w11.r0` 的构造器 hook 从未触发过。说明 WeChat 8.0.74(Tinker patch 版)的文字发送路径**根本不经过 `w11.r0` 这个类**,或走的是完全不同的入口(native / 另一个类名)。
  51 +
  52 +**发现 2:WCDB Java hook 从未触发。**
  53 +`com.tencent.wcdb.database.SQLiteDatabase.insertWithOnConflict` 在发消息时从未被调用。说明消息写库走的是**纯 native 路径**(JNI),绕开了 Java 层的 WCDB wrapper。
  54 +
  55 +**发现 3:`gm0.j1.g(km0.c)` hook 注册成功但从不触发。**
  56 +WeChat 正常运行(包括收发消息)期间,`gm0.j1.g()` 这个我们以为是 DI 初始化点的方法**从未被调用过**。说明 WeChat 用了另一条路初始化 DI 内核(或者混淆后方法名已变)。
  57 +
  58 +**发现 4:我们调用 `w11.r0(String,String,int,int,long)` 时:**
  59 +- main 进程:`NullPointerException`(某个 Context 对象是 null)
  60 +- 其他进程:`iz5.b: Kernel not initialized by MMApplication!`(`gm0.j1.i()` 抛出,说明 `gm0.j1.m` 静态字段是 null)
  61 +
  62 +---
  63 +
  64 +## 需要你做什么
  65 +
  66 +### 第 1 步:用 Frida 找 WeChat 真正的发送路径(最重要)
  67 +
  68 +我们不知道 WeChat 8.0.74 Tinker 版实际用哪个 Java 方法发文字消息。用 Frida 在用户点「发送」按钮时抓调用栈:
  69 +
  70 +```python
  71 +# 在 Mac 端 Frida bridge 调用,或者直接在设备上用 frida-ps / frida-trace
  72 +# 目标:com.tencent.mm(主进程)
  73 +# 找到发送文字消息时调用的 Java 方法
  74 +```
  75 +
  76 +具体建议:hook `com.tencent.wcdb.database.SQLiteDatabase` 的 native 层,或者用 Frida 的 Java.enumerateMethods 搜含 "sendMsg" / "sendTextMsg" 关键词的方法,在用户点发送时看 logcat。
  77 +
  78 +如果 Frida 不方便,另一条路是用 Xposed hook `android.os.MessageQueue.next()` + 过滤调用栈,找 WeChat 发消息时进入的第一个 WeChat 内部类。
  79 +
  80 +### 第 2 步:运行 DEX 分析脚本
  81 +
  82 +```bash
  83 +# 在 Termux 里(需要 root)
  84 +python3 /sdcard/wechathook/analyze_wechathook.py
  85 +```
  86 +
  87 +这个脚本会从 Tinker patch 的 classes12.dex 里提取:
  88 +- `gm0.j1` 的所有字段和方法签名
  89 +- `w11.r0` 的所有构造器签名
  90 +- `gm0.j1.i()` 的字节码(关键调用)
  91 +- `gm0.j1.g()` 的字节码(看它究竟做了什么)
  92 +
  93 +看输出里 `gm0.j1.i()` 的字节码,找它检查的是哪个字段(sget-object 指令);找谁会真正设置那个字段(sput-object 指令在哪个方法里)。
  94 +
  95 +### 第 3 步:根据发现修改 WeChatHook.java
  96 +
  97 +修改路径两选一:
  98 +
  99 +**路径 A(推荐):换用 WeChat 真实的发送方法**
  100 +如果 Frida 或字节码分析找到了真实发送入口(比如 `xxx.sendTextMessage(talker, text)` 这种),直接在 hook 里反射调用它,完全绕开 `w11.r0`。
  101 +
  102 +**路径 B:修复 DI 初始化**
  103 +找到真正初始化 `gm0.j1.m` 的方法(替代 `gm0.j1.g()`),hook 那个方法,在它完成后触发发送。
  104 +
  105 +### 第 4 步:编译 + 安装 + 测试
  106 +
  107 +```bash
  108 +# 修改 WeChatHook.java 后,在 Termux 里:
  109 +cd /sdcard/wechathook
  110 +bash termux_build.sh
  111 +
  112 +# 脚本会自动:编译 → DEX → 签名 → pm install -r → 更新 LSPosed DB
  113 +# 完成后重启 WeChat(脚本末尾会提示命令)
  114 +
  115 +# 然后测试(只用 filehelper):
  116 +su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "filehelper" --es text "测试123"'
  117 +
  118 +# 看 LSPosed 日志确认有没有走到 Send OK:
  119 +su -c 'logcat -d -s WeChatHook:* WeChatHookEarly:* | tail -50'
  120 +```
  121 +
  122 +---
  123 +
  124 +## 关键类名参考(WeChat 8.0.74 Tinker patch,classes12.dex)
  125 +
  126 +| 类 | 作用 |
  127 +|---|---|
  128 +| `gm0.j1` | DI 容器单例;`i()→Lgm0/j1;` 取单例;`m` 是静态字段 |
  129 +| `km0.c` | WeChat kernel/application 对象,传给 `gm0.j1.g()` |
  130 +| `gm0.b0` | mCoreStorage,从 `gm0.j1.u()` 取 |
  131 +| `w11.r0` | 疑似 NetSceneSendMsg,但 WeChat 自己不走这里 |
  132 +| `iz5.b` | 异常类:Kernel not initialized by MMApplication! |
  133 +| `fs.g` | DI enum;`a[0]` 是当前实现 |
  134 +| `fs.k2` | DI 实现枚举;`e` 是目标值 |
  135 +
  136 +Tinker patch APK(root 权限才能访问):
  137 +```
  138 +/data/user/0/com.tencent.mm/tinker/patch-cf87f687/dex/tinker_classN.apk
  139 +```
  140 +包含 classes2.dex ~ classes17.dex,关键类在 classes12.dex。
  141 +
  142 +---
  143 +
  144 +## 当前 WeChatHook.java 的核心逻辑(方便你快速理解)
  145 +
  146 +```
  147 +broadcast 到达
  148 + → trySendWithRetry(talker, text, 0, procName)
  149 + → isDIReady()? 调 gm0.j1.i() 看是否抛异常
  150 + → 如果不 ready:挂 pending,等 gm0.j1.g() hook 触发(但 g() 从没触发过!)
  151 + → 如果 ready:sendViaR0(talker, text)
  152 + → new w11.r0(talker, text, 1, 0, ts) ← 在所有进程都失败
  153 + → r0.H("") ← 从没执行到
  154 +```
  155 +
  156 +**关键 bug**:整条路的前提假设(`gm0.j1.g()` 是初始化点,`w11.r0` 是发送入口)可能都是错的。需要从真实调用栈重新定位。
  157 +
  158 +---
  159 +
  160 +## 输出期望
  161 +
  162 +完成后,以下命令应该能让文件传输助手收到消息:
  163 +```bash
  164 +su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "filehelper" --es text "hello from hermes"'
  165 +```
  166 +
  167 +LSPosed 日志里应该看到 `[WeChatHook] Send OK to=filehelper`。
  168 +
  169 +把修改后的 `WeChatHook.java` 回写到 `/sdcard/wechathook/WeChatHook.java`,方便 Mac 端同步。
... ...
xhs_lsposed_a16_fix/rebuild_wechathook.sh 0 → 100755
  1 +#!/bin/bash
  2 +# rebuild_wechathook.sh — compile WeChatHook, repackage into com.example.wechathook APK
  3 +# Uses the existing APK from device as a base (preserves binary AndroidManifest).
  4 +set -e
  5 +
  6 +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  7 +ADB="${ADB:-$SCRIPT_DIR/../platform-tools/adb}"
  8 +[ ! -f "$ADB" ] && ADB=adb
  9 +
  10 +ANDROID_HOME="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}"
  11 +ANDROID_JAR="$ANDROID_HOME/platforms/android-36/android.jar"
  12 +D8="$ANDROID_HOME/build-tools/36.0.0/d8"
  13 +APKSIGNER="$ANDROID_HOME/build-tools/36.0.0/apksigner"
  14 +LSPOSED_DIR="${LSPOSED_DIR:-/tmp/LSPosed}"
  15 +LEGACY_JAR="$LSPOSED_DIR/legacy/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar"
  16 +
  17 +if [ ! -f "$LEGACY_JAR" ]; then
  18 + echo "LSPosed legacy JAR not found at $LEGACY_JAR"
  19 + echo "Run: git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git $LSPOSED_DIR && cd $LSPOSED_DIR && ./gradlew :legacy:assembleRelease"
  20 + exit 1
  21 +fi
  22 +
  23 +BUILD_DIR=/tmp/wechathook_rebuild
  24 +rm -rf "$BUILD_DIR"
  25 +mkdir -p "$BUILD_DIR/compiled" "$BUILD_DIR/dex" "$BUILD_DIR/pack/assets"
  26 +
  27 +# Pull current APK from device to reuse its binary AndroidManifest and resources.arsc
  28 +echo "== Pulling current wechathook APK from device =="
  29 +WH_PATH=$($ADB shell "pm path com.example.wechathook 2>/dev/null | tr -d '\r' | sed 's/package://'" 2>/dev/null)
  30 +if [ -z "$WH_PATH" ]; then
  31 + echo "ERROR: com.example.wechathook not installed on device"
  32 + exit 1
  33 +fi
  34 +$ADB shell "su -c \"cp $WH_PATH /data/local/tmp/wh_base.apk && chmod 644 /data/local/tmp/wh_base.apk\""
  35 +$ADB pull /data/local/tmp/wh_base.apk "$BUILD_DIR/pack/base.apk" 2>/dev/null
  36 +
  37 +# Extract AndroidManifest and resources from existing APK
  38 +cd "$BUILD_DIR/pack" && unzip -o base.apk AndroidManifest.xml resources.arsc -d . > /dev/null 2>&1
  39 +rm -f base.apk
  40 +
  41 +echo "== Compiling WeChatHook.java =="
  42 +javac --release 11 \
  43 + -classpath "$ANDROID_JAR:$LEGACY_JAR" \
  44 + -d "$BUILD_DIR/compiled" \
  45 + "$SCRIPT_DIR/wechathook_src/WeChatHook.java"
  46 +
  47 +echo "== Converting to DEX =="
  48 +$D8 \
  49 + --classpath "$ANDROID_JAR" \
  50 + --classpath "$LEGACY_JAR" \
  51 + --min-api 27 \
  52 + --output "$BUILD_DIR/dex" \
  53 + $(find "$BUILD_DIR/compiled/com/example/wechathook" -name "*.class")
  54 +
  55 +echo "== Packing APK =="
  56 +cp "$BUILD_DIR/dex/classes.dex" "$BUILD_DIR/pack/"
  57 +# Put xposed_init in assets/ (required by lspd) AND root (belt+suspenders)
  58 +echo "com.example.wechathook.WeChatHook" > "$BUILD_DIR/pack/assets/xposed_init"
  59 +echo "com.example.wechathook.WeChatHook" > "$BUILD_DIR/pack/xposed_init"
  60 +
  61 +cd "$BUILD_DIR/pack"
  62 +zip -0 wechathook.apk AndroidManifest.xml resources.arsc classes.dex xposed_init 2>/dev/null || true
  63 +zip -0 wechathook.apk assets/xposed_init
  64 +
  65 +# Sign
  66 +if [ ! -f /tmp/debug.keystore ]; then
  67 + keytool -genkey -v -keystore /tmp/debug.keystore -alias androiddebugkey \
  68 + -keyalg RSA -keysize 2048 -validity 10000 \
  69 + -storepass android -keypass android \
  70 + -dname "CN=Android Debug,O=Android,C=US" 2>/dev/null
  71 +fi
  72 +
  73 +echo "== Signing =="
  74 +OUT="$SCRIPT_DIR/wechathook_v2_signed.apk"
  75 +$APKSIGNER sign \
  76 + --ks /tmp/debug.keystore \
  77 + --ks-key-alias androiddebugkey \
  78 + --ks-pass pass:android \
  79 + --key-pass pass:android \
  80 + --out "$OUT" \
  81 + "$BUILD_DIR/pack/wechathook.apk" 2>/dev/null
  82 +
  83 +echo "== Done: $OUT =="
  84 +echo "APK size: $(wc -c < "$OUT") bytes"
  85 +echo ""
  86 +echo "Next steps:"
  87 +echo " adb install -r --no-incremental $OUT"
  88 +echo " bash $SCRIPT_DIR/update_wechathook_db.sh"
  89 +echo " adb reboot"
... ...
xhs_lsposed_a16_fix/termux_build.sh 0 → 100644
  1 +#!/data/data/com.termux/files/usr/bin/bash
  2 +# termux_build.sh — 在 Termux 上编译 WeChatHook 并安装
  3 +# 所有依赖已在 /sdcard/wechathook/ 准备好
  4 +set -e
  5 +
  6 +BASE=/sdcard/wechathook
  7 +SRC=$BASE/WeChatHook.java
  8 +XPOSED_JAR=$BASE/XposedBridgeApi.jar
  9 +ANDROID_JAR=$BASE/android.jar
  10 +D8_JAR=$BASE/d8.jar
  11 +
  12 +BUILD=/data/data/com.termux/files/home/wh_build
  13 +rm -rf "$BUILD"
  14 +mkdir -p "$BUILD/compiled" "$BUILD/dex" "$BUILD/pack/assets"
  15 +
  16 +# 1. 取当前 APK 的 AndroidManifest + resources(二进制,不重新生成)
  17 +WH_PATH=$(pm path com.example.wechathook | tr -d '\r' | sed 's/package://')
  18 +echo "Base APK: $WH_PATH"
  19 +su -c "cp '$WH_PATH' /data/local/tmp/wh_base.apk && chmod 644 /data/local/tmp/wh_base.apk"
  20 +cp /data/local/tmp/wh_base.apk "$BUILD/pack/base.apk"
  21 +cd "$BUILD/pack" && unzip -o base.apk AndroidManifest.xml resources.arsc -d . > /dev/null
  22 +rm -f base.apk
  23 +
  24 +# 2. 编译
  25 +echo "== javac =="
  26 +javac --release 11 \
  27 + -classpath "$ANDROID_JAR:$XPOSED_JAR" \
  28 + -d "$BUILD/compiled" \
  29 + "$SRC"
  30 +
  31 +# 3. DEX
  32 +echo "== d8 =="
  33 +java -cp "$D8_JAR" com.android.tools.r8.D8 \
  34 + --classpath "$ANDROID_JAR" \
  35 + --classpath "$XPOSED_JAR" \
  36 + --min-api 27 \
  37 + --output "$BUILD/dex" \
  38 + $(find "$BUILD/compiled" -name "*.class")
  39 +
  40 +# 4. 打包
  41 +echo "== pack =="
  42 +cp "$BUILD/dex/classes.dex" "$BUILD/pack/"
  43 +echo "com.example.wechathook.WeChatHook" > "$BUILD/pack/assets/xposed_init"
  44 +echo "com.example.wechathook.WeChatHook" > "$BUILD/pack/xposed_init"
  45 +cd "$BUILD/pack"
  46 +zip -0 wechathook.apk AndroidManifest.xml resources.arsc classes.dex xposed_init > /dev/null
  47 +zip -0 wechathook.apk assets/xposed_init > /dev/null
  48 +
  49 +# 5. 签名(用 jarsigner,Termux JDK 自带)
  50 +KEYSTORE=/data/data/com.termux/files/home/.wh_debug.keystore
  51 +if [ ! -f "$KEYSTORE" ]; then
  52 + echo "== 生成 keystore =="
  53 + keytool -genkey -v -keystore "$KEYSTORE" -alias androiddebugkey \
  54 + -keyalg RSA -keysize 2048 -validity 10000 \
  55 + -storepass android -keypass android \
  56 + -dname "CN=Android Debug,O=Android,C=US" 2>/dev/null
  57 +fi
  58 +echo "== jarsigner =="
  59 +jarsigner -keystore "$KEYSTORE" -storepass android -keypass android \
  60 + -signedjar "$BUILD/pack/wechathook_signed.apk" \
  61 + "$BUILD/pack/wechathook.apk" androiddebugkey 2>/dev/null
  62 +
  63 +OUT=$BASE/wechathook_new.apk
  64 +cp "$BUILD/pack/wechathook_signed.apk" "$OUT"
  65 +echo "== APK ready: $OUT ($(wc -c < "$OUT") bytes) =="
  66 +
  67 +# 6. 安装
  68 +echo "== pm install =="
  69 +su -c "pm install -r '$OUT'"
  70 +
  71 +# 7. 更新 LSPosed DB(apk_path 变了需要更新)
  72 +NEW_PATH=$(pm path com.example.wechathook | tr -d '\r' | sed 's/package://')
  73 +echo "== 更新 LSPosed DB: $NEW_PATH =="
  74 +su -c "python3 -c \"
  75 +import sqlite3
  76 +db = sqlite3.connect('/data/adb/lspd/config/modules_config.db')
  77 +db.execute(\\\"UPDATE modules SET apk_path='$NEW_PATH' WHERE module_pkg_name='com.example.wechathook'\\\")
  78 +db.commit()
  79 +db.close()
  80 +print('LSPosed DB updated')
  81 +\""
  82 +
  83 +echo ""
  84 +echo "== 完成,需要重启 WeChat 进程(或 reboot)使新模块生效 =="
  85 +echo " su -c 'killall com.tencent.mm' && am start -n com.tencent.mm/.ui.LauncherUI"
... ...
xhs_lsposed_a16_fix/update_wechathook_db.sh 0 → 100755
  1 +#!/bin/bash
  2 +# update_wechathook_db.sh — 重装 wechathook APK 后更新 LSPosed modules_config.db
  3 +set -e
  4 +
  5 +ADB="${ADB:-$(dirname "$0")/../platform-tools/adb}"
  6 +[ ! -f "$ADB" ] && ADB=adb
  7 +
  8 +TERMUX_PY=/data/data/com.termux/files/usr/bin/python3
  9 +NEW_PATH=$($ADB shell 'pm path com.example.wechathook' | tr -d '\r' | sed 's/package://')
  10 +echo "New APK path: $NEW_PATH"
  11 +
  12 +# Phase 1: in-place UPDATE via Python (WAL commit visible to running lspd)
  13 +$ADB shell "su -c \"$TERMUX_PY -c \\\"
  14 +import sqlite3
  15 +db = sqlite3.connect('/data/adb/lspd/config/modules_config.db')
  16 +db.execute(\\\\\\\"UPDATE modules SET apk_path='${NEW_PATH}' WHERE module_pkg_name='com.example.wechathook'\\\\\\\")
  17 +db.commit()
  18 +try:
  19 + db.execute('PRAGMA wal_checkpoint(TRUNCATE)')
  20 +except:
  21 + pass
  22 +db.close()
  23 +print('phase1 done')
  24 +\\\"\"" 2>&1
  25 +
  26 +# Phase 2: pull + checkpoint locally + push clean main file
  27 +$ADB shell 'su -c "cp /data/adb/lspd/config/modules_config.db /data/local/tmp/mc_wh.db && cp /data/adb/lspd/config/modules_config.db-wal /data/local/tmp/mc_wh.db-wal 2>/dev/null; chmod 644 /data/local/tmp/mc_wh.db /data/local/tmp/mc_wh.db-wal 2>/dev/null; true"'
  28 +$ADB pull /data/local/tmp/mc_wh.db /tmp/mc_wh.db 2>/dev/null
  29 +$ADB pull /data/local/tmp/mc_wh.db-wal /tmp/mc_wh.db-wal 2>/dev/null || true
  30 +sqlite3 /tmp/mc_wh.db "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || true
  31 +
  32 +ACTUAL=$(sqlite3 /tmp/mc_wh.db "SELECT apk_path FROM modules WHERE module_pkg_name='com.example.wechathook';")
  33 +echo "Verified path: $ACTUAL"
  34 +
  35 +$ADB push /tmp/mc_wh.db /data/local/tmp/mc_wh_clean.db
  36 +$ADB shell 'su -c "cp /data/local/tmp/mc_wh_clean.db /data/adb/lspd/config/modules_config.db && rm -f /data/adb/lspd/config/modules_config.db-wal /data/adb/lspd/config/modules_config.db-shm && echo phase2 done"'
  37 +
  38 +# Phase 3: verify scope (mid=18, com.tencent.mm)
  39 +MID=$(sqlite3 /tmp/mc_wh.db "SELECT mid FROM modules WHERE module_pkg_name='com.example.wechathook';")
  40 +echo "mid: $MID"
  41 +SCOPE=$(sqlite3 /tmp/mc_wh.db "SELECT count(*) FROM scope WHERE mid=$MID AND app_pkg_name='com.tencent.mm';")
  42 +if [ "$SCOPE" -eq 0 ]; then
  43 + echo "scope missing — inserting mid=$MID → com.tencent.mm"
  44 + sqlite3 /tmp/mc_wh.db "INSERT OR REPLACE INTO scope (mid, app_pkg_name, user_id) VALUES ($MID, 'com.tencent.mm', 0);"
  45 + $ADB push /tmp/mc_wh.db /data/local/tmp/mc_wh_scope.db
  46 + $ADB shell "su -c 'cp /data/local/tmp/mc_wh_scope.db /data/adb/lspd/config/modules_config.db && rm -f /data/adb/lspd/config/modules_config.db-wal /data/adb/lspd/config/modules_config.db-shm && echo scope fixed'"
  47 +else
  48 + echo "scope OK (mid=$MID, com.tencent.mm exists)"
  49 +fi
  50 +
  51 +echo "DB updated. Reboot required."
... ...
xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java 0 → 100644
  1 +package com.example.wechathook;
  2 +
  3 +import android.app.Activity;
  4 +import android.content.BroadcastReceiver;
  5 +import android.content.Context;
  6 +import android.content.Intent;
  7 +import android.content.IntentFilter;
  8 +import android.os.Bundle;
  9 +import android.os.Handler;
  10 +import android.os.Looper;
  11 +import android.view.View;
  12 +import java.lang.reflect.Proxy;
  13 +import de.robv.android.xposed.IXposedHookLoadPackage;
  14 +import de.robv.android.xposed.XC_MethodHook;
  15 +import de.robv.android.xposed.XposedBridge;
  16 +import de.robv.android.xposed.XposedHelpers;
  17 +import de.robv.android.xposed.callbacks.XC_LoadPackage;
  18 +
  19 +public class WeChatHook implements IXposedHookLoadPackage {
  20 +
  21 + private static final String ACTION_SEND = "com.hermes.wechat.SEND_MSG_INTERNAL";
  22 + private static ClassLoader sWeChatCL;
  23 + private static volatile Context sAppContext;
  24 + private static volatile boolean sFsGReady = false;
  25 + // Set true while WE are calling newInstance so r0Hook can distinguish us from WeChat
  26 + private static volatile boolean sCalledByUs = false;
  27 + // Pending send: stored when broadcast arrives before gm0.j1 is initialized
  28 + private static volatile String sPendingTalker = null;
  29 + private static volatile String sPendingText = null;
  30 + private static volatile String sPendingProc = null;
  31 +
  32 + @Override
  33 + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
  34 + if (!"com.tencent.mm".equals(lpparam.packageName)) return;
  35 + // Use android.util.Log directly so it appears even before VectorLegacyBridge is ready
  36 + android.util.Log.i("WeChatHookEarly", "INIT proc=" + lpparam.processName
  37 + + " pid=" + android.os.Process.myPid());
  38 + XposedBridge.log("[WeChatHook] Injected into WeChat " + lpparam.processName
  39 + + " pid=" + android.os.Process.myPid());
  40 + ClassLoader cl = lpparam.classLoader;
  41 + sWeChatCL = cl;
  42 +
  43 + // Tablet login hooks (main process + any process)
  44 + hookInTabletEnv(cl);
  45 + hookProbe(cl);
  46 + hookLoginHistoryUIInitView(cl);
  47 + hookLoginUIInitView(cl);
  48 + hookLoginSelectUICheck(cl);
  49 + hookLoginButtonClick(cl);
  50 +
  51 + // WCDB diagnostic: which process writes to WeChat message table?
  52 + hookWCDB(cl, lpparam.processName);
  53 +
  54 + // Hook gm0.j1.g() to detect when DI kernel is ready — runs in ALL processes.
  55 + // This enables the pending-send mechanism: if broadcast arrives before DI init,
  56 + // we store the args and retry once g() fires.
  57 + hookDIInit(cl, lpparam.processName);
  58 +
  59 + // Send hook in ALL WeChat processes — we need to find which one can actually send.
  60 + // The broadcast receiver registered in each process will all receive the intent;
  61 + // whichever process has gm0.j1 initialized will succeed.
  62 + hookSendMessage(cl, lpparam.processName);
  63 + }
  64 +
  65 + // Diagnostic: hook WeChat's WCDB to find which process writes to the message table.
  66 + // com.tencent.wcdb.database.SQLiteDatabase is WeChat's encrypted SQLite wrapper.
  67 + private void hookWCDB(ClassLoader cl, String procName) {
  68 + try {
  69 + XposedHelpers.findAndHookMethod(
  70 + "com.tencent.wcdb.database.SQLiteDatabase", cl,
  71 + "insertWithOnConflict",
  72 + String.class, String.class, android.content.ContentValues.class, int.class,
  73 + new XC_MethodHook() {
  74 + private boolean logged = false;
  75 + @Override
  76 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  77 + String table = (String) param.args[0];
  78 + if (table == null || (!table.contains("message") && !table.equals("msg"))) return;
  79 + if (!logged) {
  80 + logged = true;
  81 + String stack = android.util.Log.getStackTraceString(new Throwable());
  82 + // Truncate to keep logcat readable
  83 + if (stack.length() > 3000) stack = stack.substring(0, 3000) + "…";
  84 + XposedBridge.log("[WeChatHook] WCDB INSERT table=" + table
  85 + + " proc=" + procName + " pid=" + android.os.Process.myPid()
  86 + + "\n" + stack);
  87 + } else {
  88 + XposedBridge.log("[WeChatHook] WCDB INSERT table=" + table
  89 + + " proc=" + procName);
  90 + }
  91 + }
  92 + });
  93 + XposedBridge.log("[WeChatHook] WCDB hook OK in " + procName);
  94 + } catch (Throwable t) {
  95 + XposedBridge.log("[WeChatHook] WCDB hook failed in " + procName + ": " + t);
  96 + }
  97 + }
  98 +
  99 + // PRIMARY FIX: WeChat's own methods (LoginPasswordUI.onCreate etc.) are AOT-compiled
  100 + // in a way that bypasses ArtMethod entry point replacement. But framework-dispatched
  101 + // base class methods like Activity.onResume() always go through the entry point.
  102 + // We hook onResume, filter for the login screens, and force the tablet button visible.
  103 + // By onResume time, all views are initialized (onCreate + initView have run).
  104 + private void hookProbe(ClassLoader cl) {
  105 + try {
  106 + final Class<?> m4Class = XposedHelpers.findClass(
  107 + "com.tencent.mm.plugin.account.ui.m4", cl);
  108 +
  109 + XposedHelpers.findAndHookMethod(
  110 + "android.app.Activity", null,
  111 + "onResume", new XC_MethodHook() {
  112 + @Override
  113 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  114 + String cls = param.thisObject.getClass().getName();
  115 + if (!cls.equals("com.tencent.mm.plugin.account.ui.LoginPasswordUI")
  116 + && !cls.equals("com.tencent.mm.plugin.account.ui.LoginHistoryUI")) {
  117 + return;
  118 + }
  119 + Object obj = param.thisObject;
  120 + View tabletBtn = null;
  121 + try {
  122 + tabletBtn = (View) XposedHelpers.getObjectField(obj, "N");
  123 + } catch (Throwable e) {
  124 + XposedBridge.log("[WeChatHook] onResume getField N failed: " + e);
  125 + }
  126 + if (tabletBtn == null) {
  127 + XposedBridge.log("[WeChatHook] onResume: N is null for " + cls);
  128 + return;
  129 + }
  130 + tabletBtn.setVisibility(View.VISIBLE);
  131 + try {
  132 + // m4 constructor is m4(LoginHistoryUI), but obj is LoginPasswordUI
  133 + // (a subclass). XposedHelpers.newInstance infers types from runtime
  134 + // class which is LoginPasswordUI, not LoginHistoryUI — no match.
  135 + // Use explicit param type to force the right constructor lookup.
  136 + Class<?> loginHistoryUIClass = XposedHelpers.findClass(
  137 + "com.tencent.mm.plugin.account.ui.LoginHistoryUI",
  138 + obj.getClass().getClassLoader());
  139 + tabletBtn.setOnClickListener(
  140 + (View.OnClickListener) XposedHelpers.newInstance(
  141 + m4Class, new Class<?>[]{loginHistoryUIClass}, obj));
  142 + } catch (Throwable e) {
  143 + XposedBridge.log("[WeChatHook] onResume setListener failed: " + e);
  144 + }
  145 + XposedBridge.log("[WeChatHook] onResume -> N VISIBLE + m4 attached for " + cls);
  146 + }
  147 + });
  148 + XposedBridge.log("[WeChatHook] Hooked Activity.onResume for tablet button");
  149 + } catch (Throwable t) {
  150 + XposedBridge.log("[WeChatHook] hookProbe/onResume failed: " + t);
  151 + }
  152 + }
  153 +
  154 + private void hookInTabletEnv(ClassLoader cl) {
  155 + try {
  156 + Class<?> bkClass = XposedHelpers.findClass("com.tencent.mm.ui.bk", cl);
  157 + Class<?> x0Class = XposedHelpers.findClass("bm5.x0", cl);
  158 + XposedHelpers.findAndHookMethod(bkClass, "B", x0Class, new XC_MethodHook() {
  159 + @Override
  160 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  161 + param.setResult(Boolean.TRUE);
  162 + }
  163 + });
  164 + XposedHelpers.findAndHookMethod(bkClass, "A", new XC_MethodHook() {
  165 + @Override
  166 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  167 + XposedBridge.log("[WeChatHook] bk.A() called -> TRUE");
  168 + param.setResult(Boolean.TRUE);
  169 + }
  170 + });
  171 + XposedBridge.log("[WeChatHook] Hooked bk.B(x0) + bk.A()");
  172 + } catch (Throwable t) {
  173 + XposedBridge.log("[WeChatHook] hookInTabletEnv failed: " + t);
  174 + }
  175 + }
  176 +
  177 + // PRIMARY FIX: Hook LoginPasswordUI.onCreate(Bundle) afterHookedMethod.
  178 + //
  179 + // Previous attempts to hook initView() and LoginHistoryUI.initView() both failed
  180 + // because ART AOT devirtualizes the call to this.initView() inside
  181 + // LoginHistoryUI.onCreate() — the compiled native code jumps directly past the
  182 + // Xposed entry point replacement.
  183 + //
  184 + // onCreate() itself is called by the Android framework via the ArtMethod pointer
  185 + // directly (no vtable devirtualization possible), so this hook fires reliably.
  186 + // By the time afterHookedMethod runs, super.onCreate() has completed which means
  187 + // LoginHistoryUI.initView() has already run and this.N is assigned.
  188 + private void hookLoginHistoryUIInitView(ClassLoader cl) {
  189 + final Class<?> m4Class;
  190 + try {
  191 + m4Class = XposedHelpers.findClass("com.tencent.mm.plugin.account.ui.m4", cl);
  192 + } catch (Throwable t) {
  193 + XposedBridge.log("[WeChatHook] m4 class not found: " + t);
  194 + return;
  195 + }
  196 +
  197 + try {
  198 + XposedHelpers.findAndHookMethod(
  199 + "com.tencent.mm.plugin.account.ui.LoginPasswordUI", cl,
  200 + "onCreate", Bundle.class, new XC_MethodHook() {
  201 + @Override
  202 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  203 + Object obj = param.thisObject;
  204 + View tabletBtn = (View) XposedHelpers.getObjectField(obj, "N");
  205 + if (tabletBtn == null) {
  206 + XposedBridge.log("[WeChatHook] LoginPasswordUI.onCreate: N is null");
  207 + return;
  208 + }
  209 + tabletBtn.setVisibility(View.VISIBLE);
  210 + tabletBtn.setOnClickListener(
  211 + (View.OnClickListener) XposedHelpers.newInstance(m4Class, obj));
  212 + XposedBridge.log("[WeChatHook] LoginPasswordUI.onCreate() -> N VISIBLE + m4 attached");
  213 + }
  214 + });
  215 + XposedBridge.log("[WeChatHook] Hooked LoginPasswordUI.onCreate()");
  216 + } catch (Throwable t) {
  217 + XposedBridge.log("[WeChatHook] hookLoginPasswordUI.onCreate failed: " + t);
  218 + }
  219 +
  220 + // Belt-and-suspenders: also hook LoginHistoryUI.onCreate() for any subclass
  221 + // that doesn't override onCreate but does have the N button.
  222 + try {
  223 + XposedHelpers.findAndHookMethod(
  224 + "com.tencent.mm.plugin.account.ui.LoginHistoryUI", cl,
  225 + "onCreate", Bundle.class, new XC_MethodHook() {
  226 + @Override
  227 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  228 + Object obj = param.thisObject;
  229 + if (obj.getClass().getName().equals(
  230 + "com.tencent.mm.plugin.account.ui.LoginPasswordUI")) return;
  231 + View tabletBtn = (View) XposedHelpers.getObjectField(obj, "N");
  232 + if (tabletBtn == null) {
  233 + XposedBridge.log("[WeChatHook] LoginHistoryUI.onCreate: N is null");
  234 + return;
  235 + }
  236 + tabletBtn.setVisibility(View.VISIBLE);
  237 + tabletBtn.setOnClickListener(
  238 + (View.OnClickListener) XposedHelpers.newInstance(m4Class, obj));
  239 + XposedBridge.log("[WeChatHook] LoginHistoryUI.onCreate() -> N VISIBLE + m4 attached");
  240 + }
  241 + });
  242 + XposedBridge.log("[WeChatHook] Hooked LoginHistoryUI.onCreate()");
  243 + } catch (Throwable t) {
  244 + XposedBridge.log("[WeChatHook] hookLoginHistoryUI.onCreate failed: " + t);
  245 + }
  246 + }
  247 +
  248 + private void hookLoginUIInitView(ClassLoader cl) {
  249 + try {
  250 + final Class<?> wbClass = XposedHelpers.findClass(
  251 + "com.tencent.mm.plugin.account.ui.wb", cl);
  252 + XposedHelpers.findAndHookMethod(
  253 + "com.tencent.mm.plugin.account.ui.MobileInputUI", cl,
  254 + "initView", new XC_MethodHook() {
  255 + @Override
  256 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  257 + Object mobileInputUI = param.thisObject;
  258 + View tabletBtn = (View) XposedHelpers.getObjectField(mobileInputUI, "w");
  259 + if (tabletBtn == null) {
  260 + XposedBridge.log("[WeChatHook] MobileInputUI.w is null");
  261 + return;
  262 + }
  263 + tabletBtn.setVisibility(View.VISIBLE);
  264 + tabletBtn.setOnClickListener(
  265 + (View.OnClickListener) XposedHelpers.newInstance(wbClass, mobileInputUI));
  266 + XposedBridge.log("[WeChatHook] MobileInputUI.initView() -> w VISIBLE + wb attached");
  267 + }
  268 + });
  269 + XposedBridge.log("[WeChatHook] Hooked MobileInputUI.initView()");
  270 + } catch (Throwable t) {
  271 + XposedBridge.log("[WeChatHook] hookLoginUIInitView(MobileInputUI) failed: " + t);
  272 + }
  273 + try {
  274 + final Class<?> g7Class = XposedHelpers.findClass(
  275 + "com.tencent.mm.plugin.account.ui.g7", cl);
  276 + XposedHelpers.findAndHookMethod(
  277 + "com.tencent.mm.plugin.account.ui.LoginUI", cl,
  278 + "initView", new XC_MethodHook() {
  279 + @Override
  280 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  281 + Object loginUI = param.thisObject;
  282 + View tabletBtn = (View) XposedHelpers.getObjectField(loginUI, "f992u");
  283 + if (tabletBtn == null) return;
  284 + tabletBtn.setVisibility(View.VISIBLE);
  285 + tabletBtn.setOnClickListener(
  286 + (View.OnClickListener) XposedHelpers.newInstance(g7Class, loginUI));
  287 + XposedBridge.log("[WeChatHook] LoginUI.initView() -> f992u VISIBLE + g7 attached");
  288 + }
  289 + });
  290 + XposedBridge.log("[WeChatHook] Hooked LoginUI.initView()");
  291 + } catch (Throwable t) {
  292 + XposedBridge.log("[WeChatHook] hookLoginUIInitView(LoginUI) failed: " + t);
  293 + }
  294 + }
  295 +
  296 + private void hookLoginSelectUICheck(ClassLoader cl) {
  297 + try {
  298 + Class<?> callbackClass = XposedHelpers.findClass(
  299 + "com.tencent.mm.plugin.account.ui.LoginSelectUI$a", cl);
  300 + XposedHelpers.findAndHookMethod(callbackClass, "a", boolean.class, new XC_MethodHook() {
  301 + @Override
  302 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  303 + XposedBridge.log("[WeChatHook] LoginSelectUI$a.a(" + param.args[0] + ") -> false");
  304 + param.args[0] = false;
  305 + }
  306 + });
  307 + XposedBridge.log("[WeChatHook] Hooked LoginSelectUI$a.a()");
  308 + } catch (Throwable t) {
  309 + XposedBridge.log("[WeChatHook] hookLoginSelectUICheck failed: " + t);
  310 + }
  311 + }
  312 +
  313 + private void hookLoginButtonClick(ClassLoader cl) {
  314 + try {
  315 + Class<?> ljClass = XposedHelpers.findClass(
  316 + "com.tencent.mm.plugin.account.ui.lj", cl);
  317 + final Class<?> loginSelectUIClass = XposedHelpers.findClass(
  318 + "com.tencent.mm.plugin.account.ui.LoginSelectUI", cl);
  319 + XposedHelpers.findAndHookMethod(ljClass, "onClick", View.class, new XC_MethodHook() {
  320 + @Override
  321 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  322 + XposedBridge.log("[WeChatHook] lj.onClick() -> forcing LoginSelectUI");
  323 + Activity welcomeActivity = (Activity) XposedHelpers.getObjectField(
  324 + param.thisObject, "d");
  325 + Intent intent = new Intent(welcomeActivity, loginSelectUIClass);
  326 + intent.putExtra("mobile_input_purpose", 1);
  327 + welcomeActivity.startActivity(intent);
  328 + param.setResult(null);
  329 + }
  330 + });
  331 + XposedBridge.log("[WeChatHook] Hooked lj.onClick() -> LoginSelectUI");
  332 + } catch (Throwable t) {
  333 + XposedBridge.log("[WeChatHook] hookLoginButtonClick failed: " + t);
  334 + }
  335 + }
  336 +
  337 + // ── Send message via internal API ────────────────────────────────────────
  338 + //
  339 + // Registered in ALL WeChat processes. We don't yet know which process has gm0.j1
  340 + // initialized — the first process where the receiver fires and succeeds wins.
  341 + //
  342 + // Trigger from ADB (must run as root — RECEIVER_NOT_EXPORTED):
  343 + // adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL \
  344 + // --es to "filehelper" --es text "hello"'
  345 + private void hookSendMessage(ClassLoader cl, String procName) {
  346 + try {
  347 + XposedHelpers.findAndHookMethod(
  348 + "android.app.Application", null, "onCreate",
  349 + new XC_MethodHook() {
  350 + @Override
  351 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  352 + Context ctx = (Context) param.thisObject;
  353 + if (!"com.tencent.mm".equals(ctx.getPackageName())) return;
  354 + sAppContext = ctx;
  355 + XposedBridge.log("[WeChatHook] App.onCreate proc=" + procName
  356 + + " pid=" + android.os.Process.myPid());
  357 + registerSendReceiver(ctx, procName);
  358 + }
  359 + });
  360 + XposedBridge.log("[WeChatHook] Send hook registered in " + procName);
  361 + } catch (Throwable t) {
  362 + XposedBridge.log("[WeChatHook] hookSendMessage failed in " + procName + ": " + t);
  363 + }
  364 +
  365 + // ── w11.r0 constructor hooks ───────────────────────────────────────────
  366 + // Log "by=US" vs "by=WeChat" to tell if WeChat's own UI send uses this ctor.
  367 + XC_MethodHook r0Hook = new XC_MethodHook() {
  368 + @Override
  369 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  370 + String by = sCalledByUs ? "US" : "WeChat";
  371 + XposedBridge.log("[WeChatHook] w11.r0 ctor by=" + by
  372 + + " args=" + param.args.length + " proc=" + procName
  373 + + " pid=" + android.os.Process.myPid());
  374 + if (sCalledByUs) {
  375 + forceInitDI();
  376 + diagMCoreStorage("before-r0-ctor");
  377 + }
  378 + }
  379 + @Override
  380 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  381 + String by = sCalledByUs ? "US" : "WeChat";
  382 + if (param.hasThrowable()) {
  383 + Throwable t = param.getThrowable();
  384 + XposedBridge.log("[WeChatHook] w11.r0 ctor THREW by=" + by
  385 + + " proc=" + procName + ": " + t);
  386 + if (sCalledByUs)
  387 + XposedBridge.log("[WeChatHook] r0-ctor-stack: "
  388 + + android.util.Log.getStackTraceString(t));
  389 + return;
  390 + }
  391 + sFsGReady = true;
  392 + XposedBridge.log("[WeChatHook] w11.r0 ctor OK by=" + by + " proc=" + procName);
  393 + }
  394 + };
  395 + for (Class<?>[] sig : new Class<?>[][]{
  396 + {String.class, String.class, int.class, int.class, long.class},
  397 + {String.class, String.class, int.class, int.class, Object.class},
  398 + {long.class, int.class, String.class},
  399 + {}}) {
  400 + try {
  401 + XposedHelpers.findAndHookConstructor("w11.r0", cl,
  402 + appendHook(sig, r0Hook));
  403 + XposedBridge.log("[WeChatHook] Hooked w11.r0(" + sig.length + " params)");
  404 + } catch (Throwable t) {
  405 + XposedBridge.log("[WeChatHook] w11.r0(" + sig.length + "p) hook failed: " + t);
  406 + }
  407 + }
  408 +
  409 + // ── w11.r0.H(String) — the actual dispatch method ─────────────────────
  410 + // Fires for EVERY send (ours AND WeChat UI sends). Shows if WeChat uses r0.
  411 + try {
  412 + XposedHelpers.findAndHookMethod("w11.r0", cl, "H", String.class,
  413 + new XC_MethodHook() {
  414 + @Override
  415 + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
  416 + String by = sCalledByUs ? "US" : "WeChat";
  417 + XposedBridge.log("[WeChatHook] w11.r0.H() called by=" + by
  418 + + " proc=" + procName + " pid=" + android.os.Process.myPid());
  419 + }
  420 + @Override
  421 + protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  422 + if (param.hasThrowable())
  423 + XposedBridge.log("[WeChatHook] w11.r0.H() threw: " + param.getThrowable());
  424 + else
  425 + XposedBridge.log("[WeChatHook] w11.r0.H() OK proc=" + procName);
  426 + }
  427 + });
  428 + XposedBridge.log("[WeChatHook] Hooked w11.r0.H(String) in " + procName);
  429 + } catch (Throwable t) {
  430 + XposedBridge.log("[WeChatHook] w11.r0.H hook failed in " + procName + ": " + t);
  431 + }
  432 +
  433 + // ── Diagnostic probes on the constructor call chain ───────────────────
  434 + // Each probe fires when the method is called from inside the r0 constructor
  435 + // (by WeChat OR by us). They trace which methods succeed before the NPE.
  436 + hookProbeMethod(cl, "gm0.j1", "u", "gm0.j1.u");
  437 + hookProbeMethod(cl, "c01.z1", "r", "c01.z1.r");
  438 + // gm0.b0.c() is a virtual method — probe it too
  439 + try {
  440 + XposedHelpers.findAndHookMethod("gm0.b0", cl, "c",
  441 + new XC_MethodHook() {
  442 + @Override protected void beforeHookedMethod(MethodHookParam p) {
  443 + XposedBridge.log("[WeChatHook] PROBE gm0.b0.c() called");
  444 + }
  445 + @Override protected void afterHookedMethod(MethodHookParam p) {
  446 + if (p.hasThrowable()) XposedBridge.log("[WeChatHook] PROBE gm0.b0.c() THREW: " + p.getThrowable());
  447 + else XposedBridge.log("[WeChatHook] PROBE gm0.b0.c() → " + p.getResult());
  448 + }
  449 + });
  450 + } catch (Throwable t) {
  451 + XposedBridge.log("[WeChatHook] gm0.b0.c probe failed: " + t);
  452 + }
  453 + }
  454 +
  455 + private static Object[] appendHook(Class<?>[] params, XC_MethodHook hook) {
  456 + Object[] result = new Object[params.length + 1];
  457 + System.arraycopy(params, 0, result, 0, params.length);
  458 + result[params.length] = hook;
  459 + return result;
  460 + }
  461 +
  462 + private void hookProbeMethod(ClassLoader cl, String cls, String method, String tag) {
  463 + try {
  464 + XposedHelpers.findAndHookMethod(cls, cl, method,
  465 + new XC_MethodHook() {
  466 + @Override protected void beforeHookedMethod(MethodHookParam p) {
  467 + XposedBridge.log("[WeChatHook] PROBE " + tag + "() called");
  468 + }
  469 + @Override protected void afterHookedMethod(MethodHookParam p) {
  470 + if (p.hasThrowable()) XposedBridge.log("[WeChatHook] PROBE " + tag + "() THREW: " + p.getThrowable());
  471 + else XposedBridge.log("[WeChatHook] PROBE " + tag + "() → " + p.getResult());
  472 + }
  473 + });
  474 + XposedBridge.log("[WeChatHook] Probe hooked: " + tag);
  475 + } catch (Throwable t) {
  476 + XposedBridge.log("[WeChatHook] Probe " + tag + " failed: " + t);
  477 + }
  478 + }
  479 +
  480 + private static boolean isDIReady() {
  481 + if (sWeChatCL == null) return false;
  482 + try {
  483 + XposedHelpers.callStaticMethod(XposedHelpers.findClass("gm0.j1", sWeChatCL), "i");
  484 + return true;
  485 + } catch (Throwable e) { return false; }
  486 + }
  487 +
  488 + // Hook gm0.j1.g(km0.c) to detect when WeChat DI kernel is initialized.
  489 + // gm0.j1.g() sets gm0.j1.m (the singleton) and is called lazily — not during
  490 + // Application.onCreate, so our broadcast often arrives first. After g() fires,
  491 + // gm0.j1.i() will succeed and w11.r0 can be constructed.
  492 + private static void hookDIInit(ClassLoader cl, String procName) {
  493 + try {
  494 + Class<?> km0cCls = XposedHelpers.findClass("km0.c", cl);
  495 + XposedHelpers.findAndHookMethod("gm0.j1", cl, "g", km0cCls,
  496 + new XC_MethodHook() {
  497 + @Override
  498 + protected void afterHookedMethod(MethodHookParam p) throws Throwable {
  499 + XposedBridge.log("[WeChatHook] gm0.j1.g() fired — DI ready in " + procName);
  500 + String t = sPendingTalker, x = sPendingText, pr = sPendingProc;
  501 + if (t != null && x != null && procName.equals(pr)) {
  502 + sPendingTalker = null; sPendingText = null; sPendingProc = null;
  503 + XposedBridge.log("[WeChatHook] resuming pending send to=" + t + " proc=" + procName);
  504 + new Handler(Looper.getMainLooper()).postDelayed(
  505 + () -> trySendWithRetry(t, x, 0, procName), 50);
  506 + }
  507 + }
  508 + });
  509 + XposedBridge.log("[WeChatHook] gm0.j1.g hook OK in " + procName);
  510 + } catch (Throwable t) {
  511 + XposedBridge.log("[WeChatHook] gm0.j1.g hook failed in " + procName + ": " + t);
  512 + }
  513 + }
  514 +
  515 + private static void diagMCoreStorage(String where) {
  516 + if (sWeChatCL == null) return;
  517 + try {
  518 + Class<?> j1Cls = XposedHelpers.findClass("gm0.j1", sWeChatCL);
  519 + Object j1 = XposedHelpers.callStaticMethod(j1Cls, "i");
  520 + Object mCS = (j1 != null) ? XposedHelpers.getObjectField(j1, "d") : null;
  521 + XposedBridge.log("[WeChatHook] " + where + " mCoreStorage=" + mCS);
  522 + } catch (Throwable e) {
  523 + XposedBridge.log("[WeChatHook] " + where + " mCoreStorage-check: " + e);
  524 + }
  525 + }
  526 +
  527 + private static void forceInitDI() {
  528 + if (sWeChatCL == null) return;
  529 + try {
  530 + Class<?> fsgCls = XposedHelpers.findClass("fs.g", sWeChatCL);
  531 + java.lang.reflect.Field faFld = fsgCls.getDeclaredField("a");
  532 + faFld.setAccessible(true);
  533 + Object[] arr = (Object[]) faFld.get(null);
  534 +
  535 + if (arr == null || arr.length == 0 || arr[0] == null) {
  536 + Class<?> k2Cls = XposedHelpers.findClass("fs.k2", sWeChatCL);
  537 + java.lang.reflect.Field k2eFld = k2Cls.getDeclaredField("e");
  538 + k2eFld.setAccessible(true);
  539 + Object k2e = k2eFld.get(null);
  540 + if (k2e == null) {
  541 + XposedBridge.log("[WeChatHook] forceInitDI: fs.k2.e null");
  542 + return;
  543 + }
  544 + if (arr == null || arr.length == 0) {
  545 + arr = new Object[1];
  546 + faFld.set(null, arr);
  547 + }
  548 + arr[0] = k2e;
  549 + XposedBridge.log("[WeChatHook] forceInitDI: fs.g.a[0]=" + k2e.getClass().getName());
  550 + }
  551 + } catch (Throwable e) {
  552 + XposedBridge.log("[WeChatHook] forceInitDI: " + e);
  553 + }
  554 + }
  555 +
  556 + private static void trySendWithRetry(String talker, String text, int attempt, String procName) {
  557 + // If DI not ready: park the args and wait for gm0.j1.g() hook to trigger retry.
  558 + // Only the first process to check parks the pending (others may overwrite, which is fine
  559 + // — the last write wins and gm0.j1.g() fires per-process anyway).
  560 + if (!isDIReady()) {
  561 + if (attempt == 0) {
  562 + XposedBridge.log("[WeChatHook] DI not ready proc=" + procName
  563 + + " — parking pending send to=" + talker);
  564 + sPendingTalker = talker;
  565 + sPendingText = text;
  566 + sPendingProc = procName;
  567 + } else {
  568 + XposedBridge.log("[WeChatHook] DI still not ready proc=" + procName + " attempt=" + attempt);
  569 + }
  570 + return;
  571 + }
  572 + try {
  573 + sendViaR0(talker, text);
  574 + XposedBridge.log("[WeChatHook] Send OK to=" + talker + " proc=" + procName);
  575 + } catch (Throwable t) {
  576 + String tMsg = t.getMessage() != null ? t.getMessage() : t.toString();
  577 + XposedBridge.log("[WeChatHook] Send FAILED proc=" + procName + " to=" + talker + ": " + tMsg);
  578 + if (attempt < 3) {
  579 + long delay = 1000L << attempt;
  580 + new Handler(Looper.getMainLooper()).postDelayed(
  581 + () -> trySendWithRetry(talker, text, attempt + 1, procName), delay);
  582 + }
  583 + }
  584 + }
  585 +
  586 + private static void registerSendReceiver(Context ctx, String procName) {
  587 + BroadcastReceiver recv = new BroadcastReceiver() {
  588 + @Override
  589 + public void onReceive(Context context, Intent intent) {
  590 + String talker = intent.getStringExtra("to");
  591 + String text = intent.getStringExtra("text");
  592 + if (talker == null || talker.isEmpty() || text == null || text.isEmpty()) {
  593 + XposedBridge.log("[WeChatHook] SEND_MSG_INTERNAL: missing 'to' or 'text'");
  594 + return;
  595 + }
  596 + XposedBridge.log("[WeChatHook] SEND_MSG_INTERNAL proc=" + procName
  597 + + " pid=" + android.os.Process.myPid()
  598 + + " to=" + talker + " len=" + text.length());
  599 + new Handler(Looper.getMainLooper()).post(() -> trySendWithRetry(talker, text, 0, procName));
  600 + }
  601 + };
  602 + ctx.registerReceiver(recv, new IntentFilter(ACTION_SEND), Context.RECEIVER_NOT_EXPORTED);
  603 + XposedBridge.log("[WeChatHook] BroadcastReceiver registered in " + procName);
  604 + }
  605 +
  606 + private static void sendViaR0(String talker, String text) throws Throwable {
  607 + Class<?> r0Class = XposedHelpers.findClass("w11.r0", sWeChatCL);
  608 + long ts = System.currentTimeMillis();
  609 + Object msg;
  610 + sCalledByUs = true;
  611 + try {
  612 + try {
  613 + msg = XposedHelpers.newInstance(r0Class,
  614 + new Class<?>[]{String.class, String.class, int.class, int.class, long.class},
  615 + talker, text, 1, 0, ts);
  616 + } catch (Throwable t) {
  617 + msg = XposedHelpers.newInstance(r0Class,
  618 + new Class<?>[]{String.class, String.class, int.class, int.class, Object.class},
  619 + talker, text, 1, 0, (Object) ts);
  620 + }
  621 + } finally {
  622 + sCalledByUs = false;
  623 + }
  624 +
  625 + try {
  626 + Class<?> u0 = XposedHelpers.findClass("com.tencent.mm.modelbase.u0", sWeChatCL);
  627 + Object stub = Proxy.newProxyInstance(sWeChatCL, new Class<?>[]{u0},
  628 + (proxy, method, args) -> {
  629 + XposedBridge.log("[WeChatHook] send-cb: " + method.getName());
  630 + return null;
  631 + });
  632 + XposedHelpers.setObjectField(msg, "d", stub);
  633 + } catch (Throwable ignored) {}
  634 +
  635 + sCalledByUs = true;
  636 + try {
  637 + XposedHelpers.callMethod(msg, "H", "");
  638 + } finally {
  639 + sCalledByUs = false;
  640 + }
  641 + }
  642 +}
... ...
xhs_lsposed_a16_fix/wechathook_v2_signed.apk 0 → 100644
No preview for this file type