diff --git a/wechat_send_internal/RESEARCH.md b/wechat_send_internal/RESEARCH.md new file mode 100644 index 0000000..fe4f781 --- /dev/null +++ b/wechat_send_internal/RESEARCH.md @@ -0,0 +1,188 @@ +# 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 图片发送尚未逆向,留待后续。 diff --git a/wechat_send_internal/WeChatSendHook.java b/wechat_send_internal/WeChatSendHook.java new file mode 100644 index 0000000..b86faec --- /dev/null +++ b/wechat_send_internal/WeChatSendHook.java @@ -0,0 +1,126 @@ +package com.example.wechathook; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * WeChatSendHook: Route A — 通过 WeChat 内部 API 发送文本消息。 + * + * 调用链(逆向 WeChat 8.0.74): + * 外部 am broadcast → BroadcastReceiver (WeChat 进程内) + * → w11.s1.a(talker) → w11.r1 (SendMsgCgiFactory, p1.TEXT pipeline) + * → r1.e(content) → 设置文本 + * → r1.b() → executeByPPC + * → w11.r0(talker, content, type=1, ...) + * → message 表写入 (status=SENDING) + * → /cgi-bin/micromsg-bin/newsendmsg over MMTLS + * + * 触发: + * adb shell am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL \ + * --es to "wxid_xxx" --es text "Hello from hook" + * + * Talker 格式: + * 1 对 1: wxid_xxx (对方 WeChat ID) + * 群聊: 12345678901@chatroom + * + * 注意:发送在 WeChat 主线程执行,与用户正常发送路径完全一致。 + * 不触发 A11y,不走 UI 自动化,消息经 MMTLS 加密发出。 + * + * 类位置(WeChat 8.0.74 多 DEX): + * w11.s1 — classes9.dex + * w11.r1 — classes11.dex + * w11.r0 — classes12.dex + * p1.TEXT — classes9.dex (enum value 'd') + */ +public class WeChatSendHook implements IXposedHookLoadPackage { + + private static final String ACTION = "com.hermes.wechat.SEND_MSG_INTERNAL"; + private static ClassLoader sWeChat; + + @Override + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { + if (!"com.tencent.mm".equals(lpparam.packageName)) return; + // Only hook the main process (not :push/:tools sub-processes) + if (!lpparam.processName.equals("com.tencent.mm")) return; + + sWeChat = lpparam.classLoader; + XposedBridge.log("[WeChatSendHook] Injected, registering receiver"); + + // Hook Application.onCreate to get Context for receiver registration. + // android.app.Application.onCreate() is a framework call — never AOT-inlined. + XposedHelpers.findAndHookMethod( + "android.app.Application", null, + "onCreate", + new de.robv.android.xposed.XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Context ctx = (Context) param.thisObject; + if (ctx.getPackageName().equals("com.tencent.mm")) { + registerSendReceiver(ctx); + } + } + }); + } + + private static void registerSendReceiver(Context ctx) { + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String talker = intent.getStringExtra("to"); + String text = intent.getStringExtra("text"); + if (talker == null || talker.isEmpty() || text == null || text.isEmpty()) { + XposedBridge.log("[WeChatSendHook] Received intent missing 'to' or 'text'"); + return; + } + XposedBridge.log("[WeChatSendHook] Queuing send to=" + talker + " len=" + text.length()); + sendOnMainThread(talker, text); + } + }; + IntentFilter filter = new IntentFilter(ACTION); + // NOT_EXPORTED (0x4) — only accept from root/same-uid callers via am broadcast + ctx.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); + XposedBridge.log("[WeChatSendHook] Receiver registered for " + ACTION); + } + + private static void sendOnMainThread(String talker, String text) { + // WeChat's send path touches SQLite and the network dispatcher from the main thread. + new Handler(Looper.getMainLooper()).post(() -> { + try { + sendTextInternal(talker, text); + } catch (Throwable t) { + XposedBridge.log("[WeChatSendHook] Send failed: " + t); + } + }); + } + + /** + * Core send via w11.s1 factory (SendMsgCgiFactory): + * s1.a(talker) → r1 with pipeline=p1.TEXT, talker pre-set + * r1.e(content) → set field d (content) + * r1.b() → executeByPPC (no callback) + * + * r1 field mapping (WeChat 8.0.74): + * field b = talker (set by g()) + * field d = content (set by e()) + * field e = type (default 1=text, set by h()) + * field l = pipeline (set to p1.TEXT by s1.a()) + */ + private static void sendTextInternal(String talker, String text) throws Throwable { + Class s1 = XposedHelpers.findClass("w11.s1", sWeChat); + // s1.a(talker) → r1 with g(talker) pre-called and l=p1.TEXT + Object r1 = XposedHelpers.callStaticMethod(s1, "a", talker); + // r1.e(content) → sets field d + XposedHelpers.callMethod(r1, "e", text); + // r1.b() → d(this,null,1,null) → c(q1.d) → executeByPPC + XposedHelpers.callMethod(r1, "b"); + XposedBridge.log("[WeChatSendHook] Sent: to=" + talker); + } +} diff --git a/wechat_send_internal/test_send.sh b/wechat_send_internal/test_send.sh new file mode 100644 index 0000000..76d65a9 --- /dev/null +++ b/wechat_send_internal/test_send.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Test Route A send hook via adb +# Usage: bash test_send.sh "" +# Example: bash test_send.sh wxid_abc123 "Hello from hook" +# +# NOTE: This is a TEST SCRIPT — read-only / dry-run by default. +# Set DRY_RUN=0 to actually trigger the broadcast. + +set -euo pipefail + +TALKER="${1:-}" +TEXT="${2:-}" +DRY_RUN="${DRY_RUN:-1}" # Default: print only, don't fire + +if [ -z "$TALKER" ] || [ -z "$TEXT" ]; then + echo "Usage: DRY_RUN=0 bash test_send.sh ''" + echo " talker examples:" + echo " wxid_abc123 (1-on-1)" + echo " 12345678901@chatroom (group)" + exit 1 +fi + +CMD="adb shell am broadcast \ + -a com.hermes.wechat.SEND_MSG_INTERNAL \ + --es to \"$TALKER\" \ + --es text \"$TEXT\"" + +echo "Command to run:" +echo " $CMD" +echo "" + +if [ "$DRY_RUN" = "1" ]; then + echo "[DRY RUN] Not executing. Set DRY_RUN=0 to actually send." +else + echo "[SENDING] Executing broadcast..." + eval "$CMD" + echo "" + echo "Check logcat for: [WeChatSendHook]" + echo " adb logcat -s Xposed:D | grep WeChatSendHook" +fi diff --git a/xhs_lsposed_a16_fix/analyze_wechathook.py b/xhs_lsposed_a16_fix/analyze_wechathook.py new file mode 100644 index 0000000..d48bdb3 --- /dev/null +++ b/xhs_lsposed_a16_fix/analyze_wechathook.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +analyze_wechathook.py — 从 Tinker patch DEX 分析 WeChat 8.0.74 消息发送路径 +在 Termux 上运行: python3 /sdcard/wechathook/analyze_wechathook.py + +目标: +1. 解码 w11.r0 构造器字节码,看它调了什么 +2. 解码 gm0.j1.i() 字节码,确认它检查的是字段 m +3. 检查 gm0.j1.m 在运行时是否 null(通过分析 gm0.j1.g() 是否被调用) +""" +import struct, os, subprocess + +# Tinker patch DEX (需要有 root 才能访问) +TINKER_APK = "/data/user/0/com.tencent.mm/tinker/patch-cf87f687/dex/tinker_classN.apk" +WORK_DIR = "/data/local/tmp/wh_dex_analysis" + +def ensure_dex(): + os.makedirs(WORK_DIR, exist_ok=True) + dex = f"{WORK_DIR}/classes12.dex" + if not os.path.exists(dex): + # 需要 root 拷贝 + r = subprocess.run(["su", "-c", f"cp {TINKER_APK} /data/local/tmp/_tinker.apk && chmod 644 /data/local/tmp/_tinker.apk"], + capture_output=True, text=True) + if r.returncode != 0: + print("需要 root 权限:", r.stderr) + return None + import zipfile + with zipfile.ZipFile("/data/local/tmp/_tinker.apk") as z: + z.extract("classes12.dex", WORK_DIR) + print(f"Extracted classes12.dex ({os.path.getsize(dex)} bytes)") + return dex + +def uleb128(data, off): + r, s = 0, 0 + while True: + b = data[off]; off += 1 + r |= (b & 0x7F) << s + if not (b & 0x80): break + s += 7 + return r, off + +def parse_dex(path): + data = open(path, "rb").read() + h = lambda o: struct.unpack_from('= ss: return f"?s{i}" + off = h(so + i*4); _, o2 = uleb128(data, off) + return data[o2:data.index(b'\x00', o2)].decode('utf-8', errors='replace') + def gt(i): + if i >= ts: return f"?t{i}" + return gs(h(to + i*4)) + def gp(pi): + if pi >= ps: return "?proto" + pb = po + pi*12 + ret = gt(h(pb+4)); poff = h(pb+8); params = [] + if poff: + n = h(poff) + for k in range(n): params.append(gt(struct.unpack_from('= ts: continue + name = gt(ci) + cdo = h(base+24) + if cdo == 0: continue + off = cdo + sf, off = uleb128(data, off) + inf, off = uleb128(data, off) + dm, off = uleb128(data, off) + vm, off = uleb128(data, off) + + fields, methods = [], [] + fi = 0 + for _ in range(sf+inf): + d, off = uleb128(data, off); fi += d + fl, off = uleb128(data, off) + if fi < fs: + fb = fo+fi*8 + ft = struct.unpack_from('> 4) & 0xf + i += 2 if op in range(0x02, 0x0e) or op in range(0x1a, 0x22) or op >= 0x26 else 1 + return '\n'.join(lines) if lines else " (no invoke/field ops)" + +def main(): + dex = ensure_dex() + if not dex: + return + + print("Parsing DEX...") + classes, data, to, ts, fo, fs, mo, ms, po, ps, so, ss = parse_dex(dex) + + # ── 1. gm0.j1 structure ────────────────────────────────────────── + print("\n" + "="*60) + print("1. gm0.j1 class") + print("="*60) + j1 = classes.get("Lgm0/j1;") + if j1: + print("Static fields:") + for name, typ, flags in j1['fields']: + print(f" {name}: {typ} flags=0x{flags:x}") + print("\nMethods with signatures:") + for name, proto, flags, co2 in j1['methods']: + flag_s = '+'.join(f for f, b in [('public',1),('static',8),('sync',0x20),('ctor',0x10000)] if flags&b) + print(f" {name}: {proto} [{flag_s}]") + else: + print("NOT FOUND") + + # ── 2. w11.r0 structure ────────────────────────────────────────── + print("\n" + "="*60) + print("2. w11.r0 class") + print("="*60) + r0 = classes.get("Lw11/r0;") + if r0: + print("Static fields:") + for name, typ, flags in r0['fields']: + print(f" {name}: {typ}") + print(f"\nConstructors (looking for (String,String,int,int,long)):") + for name, proto, flags, co2 in r0['methods']: + if name == "": + print(f" {proto} code_off=0x{co2:x}") + else: + print("NOT FOUND") + + # ── 3. Decode gm0.j1.i() bytecode ─────────────────────────────── + print("\n" + "="*60) + print("3. gm0.j1.i() bytecode (key ops)") + print("="*60) + if j1: + for name, proto, flags, co2 in j1['methods']: + if name == "i" and "()Lgm0/j1;" in proto: + print(f"gm0.j1.i() code_off=0x{co2:x}") + print(dump_bytecode(data, co2)) + break + + # ── 4. Decode w11.r0 (String,String,int,int,long) ctor ────────── + print("\n" + "="*60) + print("4. w11.r0 constructor bytecode (key invoke ops)") + print("="*60) + if r0: + for name, proto, flags, co2 in r0['methods']: + if name == "" and "Ljava/lang/String;Ljava/lang/String;IIJ" in proto: + print(f"(String,String,int,int,long) code_off=0x{co2:x}") + print(dump_bytecode(data, co2, max_insns=40)) + break + + # ── 5. Who initializes gm0.j1.m? Search for sput-object on field m ─ + print("\n" + "="*60) + print("5. Searching gm0.j1.g() bytecode") + print("="*60) + if j1: + for name, proto, flags, co2 in j1['methods']: + if name == "g": + print(f"gm0.j1.g{proto} code_off=0x{co2:x}") + print(dump_bytecode(data, co2)) + break + + print("\n== Done ==") + +if __name__ == "__main__": + main() diff --git a/xhs_lsposed_a16_fix/do_not_break.md b/xhs_lsposed_a16_fix/do_not_break.md new file mode 100644 index 0000000..e3e91e5 --- /dev/null +++ b/xhs_lsposed_a16_fix/do_not_break.md @@ -0,0 +1,76 @@ +# WeChatHook — DO NOT BREAK 清单 + +## 当前状态(2026-07-04 已验证) + +已安装 APK: `com.example.wechathook` (versionCode=1) +签名: `~/.android/debug.keystore` (CN=Android Debug,O=Android,C=US) +APK 路径: `/data/app/~~UUcd24uZ9ch3W7ZmJpIxlQ==/com.example.wechathook-Jns-UMdjPSZgIGeeIqFAwQ==/base.apk` +LSPosed DB: mid=42, scope=(42, com.tencent.mm, 0), enabled=1 +功能: 微信平板登录 + WCDB hook + gm0.j1.g hook + w11.r0 hook + +## 绝对禁止事项 + +### 1. 不要用 XposedBridgeApi.jar 重新编译 +LSPosed Vector 运行时用的是自己混淆的 XposedHelpers,标准 XposedBridgeApi.jar 编译出来的 +APK 所有 findAndHookMethod 调用全部 NoSuchMethodError。 + +**必须用的 JAR**: `/tmp/LSPosed/legacy/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar` +(在 Mac 上,Pixel 上没有这个 JAR) + +如果 Mac 上没有这个 JAR,**不要重新编译**。只改 Java 源码而不重编是可以的(改 /sdcard/wechathook/WeChatHook.java),但必须用正确 JAR 编译才能安装新版本。 + +### 2. APK 必须包含 assets/xposed_init +打包时必须: `zip APK.apk assets/xposed_init ...`(注意 assets/ 子目录层级) +文件内容: `com.example.wechathook.WeChatHook` + +LSPosed daemon 每次启动都验证这个文件。缺失 → 自动从 DB 删除模块 → 平板登录消失。 + +根目录的 `xposed_init`(不在 assets/ 下)**不够,daemon 不认**。 + +### 3. 不要换 keystore 签名 +当前已安装版本用 `~/.android/debug.keystore`。 +如果用不同 keystore 重签 → INSTALL_FAILED_UPDATE_INCOMPATIBLE → 必须先 uninstall(会丢失 LSPosed DB 注册)。 + +### 4. 不要直接改 LSPosed DB 而不做 WAL checkpoint +改完必须: `PRAGMA wal_checkpoint(TRUNCATE)` + 把主 DB 拷覆盖 .bak .bak2 .bak3 .bak4 四个文件。 + +## 安全测试约束(严禁违反) + +**只用 filehelper(文件传输助手)测试发消息,绝对不能发给真实联系人。** +微信对异常行为会封号,一旦封号无法找回。 + +## 如果你需要重新安装 APK + +```bash +# 1. 备份 DB(万一要恢复) +adb shell su -c 'cp /data/adb/lspd/config/modules_config.db /data/local/tmp/modules_backup.db' + +# 2. 卸载旧版本 +adb shell pm uninstall com.example.wechathook + +# 3. 安装新版本(必须已按上面规则编译 + 打包 + 签名) +adb install --no-incremental /path/to/new.apk + +# 4. 修复 DB(参考 /sdcard/wechathook/fix_lspd_final.py) +adb shell su -c '/path/to/python3 /sdcard/wechathook/fix_lspd_final.py' + +# 5. 重启 +adb shell su -c reboot + +# 6. 验证 +adb shell su -c 'grep -i "WeChatHook\|hookInTablet\|NoSuchMethod" /data/adb/lspd/log/modules_*.log' +``` + +## 验证钩子是否正常的命令 + +```bash +adb shell su -c 'grep "\[WeChatHook\]" /data/adb/lspd/log/modules_*.log | head -30' +``` + +正常输出应包含(无 NoSuchMethodError): +- Hooked bk.B(x0) + bk.A() +- Hooked Activity.onResume for tablet button +- Hooked LoginPasswordUI.onCreate() +- Hooked MobileInputUI.initView() / LoginUI.initView() +- WCDB hook OK +- gm0.j1.g hook OK diff --git a/xhs_lsposed_a16_fix/hermes_task.md b/xhs_lsposed_a16_fix/hermes_task.md new file mode 100644 index 0000000..cd3bd06 --- /dev/null +++ b/xhs_lsposed_a16_fix/hermes_task.md @@ -0,0 +1,169 @@ +# 任务:修通 WeChat Xposed 内部发送接口 + +## 🔒 安全约束(不得违反) + +**只能用 `filehelper`(文件传输助手)测试,绝对不能给真实联系人发消息。** +测试命令固定格式: +```bash +su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "filehelper" --es text "测试"' +``` + +--- + +## 目标 + +让 LSPosed 模块 `com.example.wechathook` 能通过 broadcast 真正把消息发进微信 message 数据库(EnMicroMsg.db),在文件传输助手收到消息。 + +设备:Pixel 10 Pro Fold,Android 16,WeChat 8.0.74,LSPosed JingMatrix Vector v1.11.0。 + +--- + +## 工作目录 + +``` +/sdcard/wechathook/ + WeChatHook.java ← 当前源码(已是最新版) + termux_build.sh ← Termux 上编译+安装脚本 + analyze_wechathook.py ← DEX 字节码分析工具(Python,读 Tinker patch) + XposedBridgeApi.jar ← 编译依赖 + android.jar ← 编译依赖(API 36) + d8.jar ← DEX 转换工具 + wechathook_current.apk ← 当前安装版本 +``` + +--- + +## 当前状态(已做了什么、失败在哪) + +### 已实现 + +模块已注入所有 WeChat 进程(main / push / appbrand0 / appbrand1)。每个进程里: +1. 注册了 `com.hermes.wechat.SEND_MSG_INTERNAL` broadcast receiver +2. 挂钩了 `w11.r0` 的 4 种构造器((String,String,int,int,long) 等) +3. 挂钩了 `w11.r0.H(String)`(发送 dispatch 方法) +4. 挂钩了 `gm0.j1.g(km0.c)`(DI 内核初始化检测) +5. 挂钩了 WCDB `insertWithOnConflict`(诊断用) + +### 关键发现(失败原因) + +**发现 1:`w11.r0` 构造器永远不被 WeChat 自己调用。** +用户在 UI 点发送时,`w11.r0` 的构造器 hook 从未触发过。说明 WeChat 8.0.74(Tinker patch 版)的文字发送路径**根本不经过 `w11.r0` 这个类**,或走的是完全不同的入口(native / 另一个类名)。 + +**发现 2:WCDB Java hook 从未触发。** +`com.tencent.wcdb.database.SQLiteDatabase.insertWithOnConflict` 在发消息时从未被调用。说明消息写库走的是**纯 native 路径**(JNI),绕开了 Java 层的 WCDB wrapper。 + +**发现 3:`gm0.j1.g(km0.c)` hook 注册成功但从不触发。** +WeChat 正常运行(包括收发消息)期间,`gm0.j1.g()` 这个我们以为是 DI 初始化点的方法**从未被调用过**。说明 WeChat 用了另一条路初始化 DI 内核(或者混淆后方法名已变)。 + +**发现 4:我们调用 `w11.r0(String,String,int,int,long)` 时:** +- main 进程:`NullPointerException`(某个 Context 对象是 null) +- 其他进程:`iz5.b: Kernel not initialized by MMApplication!`(`gm0.j1.i()` 抛出,说明 `gm0.j1.m` 静态字段是 null) + +--- + +## 需要你做什么 + +### 第 1 步:用 Frida 找 WeChat 真正的发送路径(最重要) + +我们不知道 WeChat 8.0.74 Tinker 版实际用哪个 Java 方法发文字消息。用 Frida 在用户点「发送」按钮时抓调用栈: + +```python +# 在 Mac 端 Frida bridge 调用,或者直接在设备上用 frida-ps / frida-trace +# 目标:com.tencent.mm(主进程) +# 找到发送文字消息时调用的 Java 方法 +``` + +具体建议:hook `com.tencent.wcdb.database.SQLiteDatabase` 的 native 层,或者用 Frida 的 Java.enumerateMethods 搜含 "sendMsg" / "sendTextMsg" 关键词的方法,在用户点发送时看 logcat。 + +如果 Frida 不方便,另一条路是用 Xposed hook `android.os.MessageQueue.next()` + 过滤调用栈,找 WeChat 发消息时进入的第一个 WeChat 内部类。 + +### 第 2 步:运行 DEX 分析脚本 + +```bash +# 在 Termux 里(需要 root) +python3 /sdcard/wechathook/analyze_wechathook.py +``` + +这个脚本会从 Tinker patch 的 classes12.dex 里提取: +- `gm0.j1` 的所有字段和方法签名 +- `w11.r0` 的所有构造器签名 +- `gm0.j1.i()` 的字节码(关键调用) +- `gm0.j1.g()` 的字节码(看它究竟做了什么) + +看输出里 `gm0.j1.i()` 的字节码,找它检查的是哪个字段(sget-object 指令);找谁会真正设置那个字段(sput-object 指令在哪个方法里)。 + +### 第 3 步:根据发现修改 WeChatHook.java + +修改路径两选一: + +**路径 A(推荐):换用 WeChat 真实的发送方法** +如果 Frida 或字节码分析找到了真实发送入口(比如 `xxx.sendTextMessage(talker, text)` 这种),直接在 hook 里反射调用它,完全绕开 `w11.r0`。 + +**路径 B:修复 DI 初始化** +找到真正初始化 `gm0.j1.m` 的方法(替代 `gm0.j1.g()`),hook 那个方法,在它完成后触发发送。 + +### 第 4 步:编译 + 安装 + 测试 + +```bash +# 修改 WeChatHook.java 后,在 Termux 里: +cd /sdcard/wechathook +bash termux_build.sh + +# 脚本会自动:编译 → DEX → 签名 → pm install -r → 更新 LSPosed DB +# 完成后重启 WeChat(脚本末尾会提示命令) + +# 然后测试(只用 filehelper): +su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "filehelper" --es text "测试123"' + +# 看 LSPosed 日志确认有没有走到 Send OK: +su -c 'logcat -d -s WeChatHook:* WeChatHookEarly:* | tail -50' +``` + +--- + +## 关键类名参考(WeChat 8.0.74 Tinker patch,classes12.dex) + +| 类 | 作用 | +|---|---| +| `gm0.j1` | DI 容器单例;`i()→Lgm0/j1;` 取单例;`m` 是静态字段 | +| `km0.c` | WeChat kernel/application 对象,传给 `gm0.j1.g()` | +| `gm0.b0` | mCoreStorage,从 `gm0.j1.u()` 取 | +| `w11.r0` | 疑似 NetSceneSendMsg,但 WeChat 自己不走这里 | +| `iz5.b` | 异常类:Kernel not initialized by MMApplication! | +| `fs.g` | DI enum;`a[0]` 是当前实现 | +| `fs.k2` | DI 实现枚举;`e` 是目标值 | + +Tinker patch APK(root 权限才能访问): +``` +/data/user/0/com.tencent.mm/tinker/patch-cf87f687/dex/tinker_classN.apk +``` +包含 classes2.dex ~ classes17.dex,关键类在 classes12.dex。 + +--- + +## 当前 WeChatHook.java 的核心逻辑(方便你快速理解) + +``` +broadcast 到达 + → trySendWithRetry(talker, text, 0, procName) + → isDIReady()? 调 gm0.j1.i() 看是否抛异常 + → 如果不 ready:挂 pending,等 gm0.j1.g() hook 触发(但 g() 从没触发过!) + → 如果 ready:sendViaR0(talker, text) + → new w11.r0(talker, text, 1, 0, ts) ← 在所有进程都失败 + → r0.H("") ← 从没执行到 +``` + +**关键 bug**:整条路的前提假设(`gm0.j1.g()` 是初始化点,`w11.r0` 是发送入口)可能都是错的。需要从真实调用栈重新定位。 + +--- + +## 输出期望 + +完成后,以下命令应该能让文件传输助手收到消息: +```bash +su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "filehelper" --es text "hello from hermes"' +``` + +LSPosed 日志里应该看到 `[WeChatHook] Send OK to=filehelper`。 + +把修改后的 `WeChatHook.java` 回写到 `/sdcard/wechathook/WeChatHook.java`,方便 Mac 端同步。 diff --git a/xhs_lsposed_a16_fix/rebuild_wechathook.sh b/xhs_lsposed_a16_fix/rebuild_wechathook.sh new file mode 100755 index 0000000..00991cf --- /dev/null +++ b/xhs_lsposed_a16_fix/rebuild_wechathook.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# rebuild_wechathook.sh — compile WeChatHook, repackage into com.example.wechathook APK +# Uses the existing APK from device as a base (preserves binary AndroidManifest). +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ADB="${ADB:-$SCRIPT_DIR/../platform-tools/adb}" +[ ! -f "$ADB" ] && ADB=adb + +ANDROID_HOME="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}" +ANDROID_JAR="$ANDROID_HOME/platforms/android-36/android.jar" +D8="$ANDROID_HOME/build-tools/36.0.0/d8" +APKSIGNER="$ANDROID_HOME/build-tools/36.0.0/apksigner" +LSPOSED_DIR="${LSPOSED_DIR:-/tmp/LSPosed}" +LEGACY_JAR="$LSPOSED_DIR/legacy/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar" + +if [ ! -f "$LEGACY_JAR" ]; then + echo "LSPosed legacy JAR not found at $LEGACY_JAR" + echo "Run: git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git $LSPOSED_DIR && cd $LSPOSED_DIR && ./gradlew :legacy:assembleRelease" + exit 1 +fi + +BUILD_DIR=/tmp/wechathook_rebuild +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR/compiled" "$BUILD_DIR/dex" "$BUILD_DIR/pack/assets" + +# Pull current APK from device to reuse its binary AndroidManifest and resources.arsc +echo "== Pulling current wechathook APK from device ==" +WH_PATH=$($ADB shell "pm path com.example.wechathook 2>/dev/null | tr -d '\r' | sed 's/package://'" 2>/dev/null) +if [ -z "$WH_PATH" ]; then + echo "ERROR: com.example.wechathook not installed on device" + exit 1 +fi +$ADB shell "su -c \"cp $WH_PATH /data/local/tmp/wh_base.apk && chmod 644 /data/local/tmp/wh_base.apk\"" +$ADB pull /data/local/tmp/wh_base.apk "$BUILD_DIR/pack/base.apk" 2>/dev/null + +# Extract AndroidManifest and resources from existing APK +cd "$BUILD_DIR/pack" && unzip -o base.apk AndroidManifest.xml resources.arsc -d . > /dev/null 2>&1 +rm -f base.apk + +echo "== Compiling WeChatHook.java ==" +javac --release 11 \ + -classpath "$ANDROID_JAR:$LEGACY_JAR" \ + -d "$BUILD_DIR/compiled" \ + "$SCRIPT_DIR/wechathook_src/WeChatHook.java" + +echo "== Converting to DEX ==" +$D8 \ + --classpath "$ANDROID_JAR" \ + --classpath "$LEGACY_JAR" \ + --min-api 27 \ + --output "$BUILD_DIR/dex" \ + $(find "$BUILD_DIR/compiled/com/example/wechathook" -name "*.class") + +echo "== Packing APK ==" +cp "$BUILD_DIR/dex/classes.dex" "$BUILD_DIR/pack/" +# Put xposed_init in assets/ (required by lspd) AND root (belt+suspenders) +echo "com.example.wechathook.WeChatHook" > "$BUILD_DIR/pack/assets/xposed_init" +echo "com.example.wechathook.WeChatHook" > "$BUILD_DIR/pack/xposed_init" + +cd "$BUILD_DIR/pack" +zip -0 wechathook.apk AndroidManifest.xml resources.arsc classes.dex xposed_init 2>/dev/null || true +zip -0 wechathook.apk assets/xposed_init + +# Sign +if [ ! -f /tmp/debug.keystore ]; then + keytool -genkey -v -keystore /tmp/debug.keystore -alias androiddebugkey \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass android -keypass android \ + -dname "CN=Android Debug,O=Android,C=US" 2>/dev/null +fi + +echo "== Signing ==" +OUT="$SCRIPT_DIR/wechathook_v2_signed.apk" +$APKSIGNER sign \ + --ks /tmp/debug.keystore \ + --ks-key-alias androiddebugkey \ + --ks-pass pass:android \ + --key-pass pass:android \ + --out "$OUT" \ + "$BUILD_DIR/pack/wechathook.apk" 2>/dev/null + +echo "== Done: $OUT ==" +echo "APK size: $(wc -c < "$OUT") bytes" +echo "" +echo "Next steps:" +echo " adb install -r --no-incremental $OUT" +echo " bash $SCRIPT_DIR/update_wechathook_db.sh" +echo " adb reboot" diff --git a/xhs_lsposed_a16_fix/termux_build.sh b/xhs_lsposed_a16_fix/termux_build.sh new file mode 100644 index 0000000..c66875d --- /dev/null +++ b/xhs_lsposed_a16_fix/termux_build.sh @@ -0,0 +1,85 @@ +#!/data/data/com.termux/files/usr/bin/bash +# termux_build.sh — 在 Termux 上编译 WeChatHook 并安装 +# 所有依赖已在 /sdcard/wechathook/ 准备好 +set -e + +BASE=/sdcard/wechathook +SRC=$BASE/WeChatHook.java +XPOSED_JAR=$BASE/XposedBridgeApi.jar +ANDROID_JAR=$BASE/android.jar +D8_JAR=$BASE/d8.jar + +BUILD=/data/data/com.termux/files/home/wh_build +rm -rf "$BUILD" +mkdir -p "$BUILD/compiled" "$BUILD/dex" "$BUILD/pack/assets" + +# 1. 取当前 APK 的 AndroidManifest + resources(二进制,不重新生成) +WH_PATH=$(pm path com.example.wechathook | tr -d '\r' | sed 's/package://') +echo "Base APK: $WH_PATH" +su -c "cp '$WH_PATH' /data/local/tmp/wh_base.apk && chmod 644 /data/local/tmp/wh_base.apk" +cp /data/local/tmp/wh_base.apk "$BUILD/pack/base.apk" +cd "$BUILD/pack" && unzip -o base.apk AndroidManifest.xml resources.arsc -d . > /dev/null +rm -f base.apk + +# 2. 编译 +echo "== javac ==" +javac --release 11 \ + -classpath "$ANDROID_JAR:$XPOSED_JAR" \ + -d "$BUILD/compiled" \ + "$SRC" + +# 3. DEX +echo "== d8 ==" +java -cp "$D8_JAR" com.android.tools.r8.D8 \ + --classpath "$ANDROID_JAR" \ + --classpath "$XPOSED_JAR" \ + --min-api 27 \ + --output "$BUILD/dex" \ + $(find "$BUILD/compiled" -name "*.class") + +# 4. 打包 +echo "== pack ==" +cp "$BUILD/dex/classes.dex" "$BUILD/pack/" +echo "com.example.wechathook.WeChatHook" > "$BUILD/pack/assets/xposed_init" +echo "com.example.wechathook.WeChatHook" > "$BUILD/pack/xposed_init" +cd "$BUILD/pack" +zip -0 wechathook.apk AndroidManifest.xml resources.arsc classes.dex xposed_init > /dev/null +zip -0 wechathook.apk assets/xposed_init > /dev/null + +# 5. 签名(用 jarsigner,Termux JDK 自带) +KEYSTORE=/data/data/com.termux/files/home/.wh_debug.keystore +if [ ! -f "$KEYSTORE" ]; then + echo "== 生成 keystore ==" + keytool -genkey -v -keystore "$KEYSTORE" -alias androiddebugkey \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass android -keypass android \ + -dname "CN=Android Debug,O=Android,C=US" 2>/dev/null +fi +echo "== jarsigner ==" +jarsigner -keystore "$KEYSTORE" -storepass android -keypass android \ + -signedjar "$BUILD/pack/wechathook_signed.apk" \ + "$BUILD/pack/wechathook.apk" androiddebugkey 2>/dev/null + +OUT=$BASE/wechathook_new.apk +cp "$BUILD/pack/wechathook_signed.apk" "$OUT" +echo "== APK ready: $OUT ($(wc -c < "$OUT") bytes) ==" + +# 6. 安装 +echo "== pm install ==" +su -c "pm install -r '$OUT'" + +# 7. 更新 LSPosed DB(apk_path 变了需要更新) +NEW_PATH=$(pm path com.example.wechathook | tr -d '\r' | sed 's/package://') +echo "== 更新 LSPosed DB: $NEW_PATH ==" +su -c "python3 -c \" +import sqlite3 +db = sqlite3.connect('/data/adb/lspd/config/modules_config.db') +db.execute(\\\"UPDATE modules SET apk_path='$NEW_PATH' WHERE module_pkg_name='com.example.wechathook'\\\") +db.commit() +db.close() +print('LSPosed DB updated') +\"" + +echo "" +echo "== 完成,需要重启 WeChat 进程(或 reboot)使新模块生效 ==" +echo " su -c 'killall com.tencent.mm' && am start -n com.tencent.mm/.ui.LauncherUI" diff --git a/xhs_lsposed_a16_fix/update_wechathook_db.sh b/xhs_lsposed_a16_fix/update_wechathook_db.sh new file mode 100755 index 0000000..8c957a6 --- /dev/null +++ b/xhs_lsposed_a16_fix/update_wechathook_db.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# update_wechathook_db.sh — 重装 wechathook APK 后更新 LSPosed modules_config.db +set -e + +ADB="${ADB:-$(dirname "$0")/../platform-tools/adb}" +[ ! -f "$ADB" ] && ADB=adb + +TERMUX_PY=/data/data/com.termux/files/usr/bin/python3 +NEW_PATH=$($ADB shell 'pm path com.example.wechathook' | tr -d '\r' | sed 's/package://') +echo "New APK path: $NEW_PATH" + +# Phase 1: in-place UPDATE via Python (WAL commit visible to running lspd) +$ADB shell "su -c \"$TERMUX_PY -c \\\" +import sqlite3 +db = sqlite3.connect('/data/adb/lspd/config/modules_config.db') +db.execute(\\\\\\\"UPDATE modules SET apk_path='${NEW_PATH}' WHERE module_pkg_name='com.example.wechathook'\\\\\\\") +db.commit() +try: + db.execute('PRAGMA wal_checkpoint(TRUNCATE)') +except: + pass +db.close() +print('phase1 done') +\\\"\"" 2>&1 + +# Phase 2: pull + checkpoint locally + push clean main file +$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"' +$ADB pull /data/local/tmp/mc_wh.db /tmp/mc_wh.db 2>/dev/null +$ADB pull /data/local/tmp/mc_wh.db-wal /tmp/mc_wh.db-wal 2>/dev/null || true +sqlite3 /tmp/mc_wh.db "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || true + +ACTUAL=$(sqlite3 /tmp/mc_wh.db "SELECT apk_path FROM modules WHERE module_pkg_name='com.example.wechathook';") +echo "Verified path: $ACTUAL" + +$ADB push /tmp/mc_wh.db /data/local/tmp/mc_wh_clean.db +$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"' + +# Phase 3: verify scope (mid=18, com.tencent.mm) +MID=$(sqlite3 /tmp/mc_wh.db "SELECT mid FROM modules WHERE module_pkg_name='com.example.wechathook';") +echo "mid: $MID" +SCOPE=$(sqlite3 /tmp/mc_wh.db "SELECT count(*) FROM scope WHERE mid=$MID AND app_pkg_name='com.tencent.mm';") +if [ "$SCOPE" -eq 0 ]; then + echo "scope missing — inserting mid=$MID → com.tencent.mm" + sqlite3 /tmp/mc_wh.db "INSERT OR REPLACE INTO scope (mid, app_pkg_name, user_id) VALUES ($MID, 'com.tencent.mm', 0);" + $ADB push /tmp/mc_wh.db /data/local/tmp/mc_wh_scope.db + $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'" +else + echo "scope OK (mid=$MID, com.tencent.mm exists)" +fi + +echo "DB updated. Reboot required." diff --git a/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java b/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java new file mode 100644 index 0000000..ebcd2e3 --- /dev/null +++ b/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java @@ -0,0 +1,642 @@ +package com.example.wechathook; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import java.lang.reflect.Proxy; +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +public class WeChatHook implements IXposedHookLoadPackage { + + private static final String ACTION_SEND = "com.hermes.wechat.SEND_MSG_INTERNAL"; + private static ClassLoader sWeChatCL; + private static volatile Context sAppContext; + private static volatile boolean sFsGReady = false; + // Set true while WE are calling newInstance so r0Hook can distinguish us from WeChat + private static volatile boolean sCalledByUs = false; + // Pending send: stored when broadcast arrives before gm0.j1 is initialized + private static volatile String sPendingTalker = null; + private static volatile String sPendingText = null; + private static volatile String sPendingProc = null; + + @Override + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { + if (!"com.tencent.mm".equals(lpparam.packageName)) return; + // Use android.util.Log directly so it appears even before VectorLegacyBridge is ready + android.util.Log.i("WeChatHookEarly", "INIT proc=" + lpparam.processName + + " pid=" + android.os.Process.myPid()); + XposedBridge.log("[WeChatHook] Injected into WeChat " + lpparam.processName + + " pid=" + android.os.Process.myPid()); + ClassLoader cl = lpparam.classLoader; + sWeChatCL = cl; + + // Tablet login hooks (main process + any process) + hookInTabletEnv(cl); + hookProbe(cl); + hookLoginHistoryUIInitView(cl); + hookLoginUIInitView(cl); + hookLoginSelectUICheck(cl); + hookLoginButtonClick(cl); + + // WCDB diagnostic: which process writes to WeChat message table? + hookWCDB(cl, lpparam.processName); + + // Hook gm0.j1.g() to detect when DI kernel is ready — runs in ALL processes. + // This enables the pending-send mechanism: if broadcast arrives before DI init, + // we store the args and retry once g() fires. + hookDIInit(cl, lpparam.processName); + + // Send hook in ALL WeChat processes — we need to find which one can actually send. + // The broadcast receiver registered in each process will all receive the intent; + // whichever process has gm0.j1 initialized will succeed. + hookSendMessage(cl, lpparam.processName); + } + + // Diagnostic: hook WeChat's WCDB to find which process writes to the message table. + // com.tencent.wcdb.database.SQLiteDatabase is WeChat's encrypted SQLite wrapper. + private void hookWCDB(ClassLoader cl, String procName) { + try { + XposedHelpers.findAndHookMethod( + "com.tencent.wcdb.database.SQLiteDatabase", cl, + "insertWithOnConflict", + String.class, String.class, android.content.ContentValues.class, int.class, + new XC_MethodHook() { + private boolean logged = false; + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + String table = (String) param.args[0]; + if (table == null || (!table.contains("message") && !table.equals("msg"))) return; + if (!logged) { + logged = true; + String stack = android.util.Log.getStackTraceString(new Throwable()); + // Truncate to keep logcat readable + if (stack.length() > 3000) stack = stack.substring(0, 3000) + "…"; + XposedBridge.log("[WeChatHook] WCDB INSERT table=" + table + + " proc=" + procName + " pid=" + android.os.Process.myPid() + + "\n" + stack); + } else { + XposedBridge.log("[WeChatHook] WCDB INSERT table=" + table + + " proc=" + procName); + } + } + }); + XposedBridge.log("[WeChatHook] WCDB hook OK in " + procName); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] WCDB hook failed in " + procName + ": " + t); + } + } + + // PRIMARY FIX: WeChat's own methods (LoginPasswordUI.onCreate etc.) are AOT-compiled + // in a way that bypasses ArtMethod entry point replacement. But framework-dispatched + // base class methods like Activity.onResume() always go through the entry point. + // We hook onResume, filter for the login screens, and force the tablet button visible. + // By onResume time, all views are initialized (onCreate + initView have run). + private void hookProbe(ClassLoader cl) { + try { + final Class m4Class = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.m4", cl); + + XposedHelpers.findAndHookMethod( + "android.app.Activity", null, + "onResume", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + String cls = param.thisObject.getClass().getName(); + if (!cls.equals("com.tencent.mm.plugin.account.ui.LoginPasswordUI") + && !cls.equals("com.tencent.mm.plugin.account.ui.LoginHistoryUI")) { + return; + } + Object obj = param.thisObject; + View tabletBtn = null; + try { + tabletBtn = (View) XposedHelpers.getObjectField(obj, "N"); + } catch (Throwable e) { + XposedBridge.log("[WeChatHook] onResume getField N failed: " + e); + } + if (tabletBtn == null) { + XposedBridge.log("[WeChatHook] onResume: N is null for " + cls); + return; + } + tabletBtn.setVisibility(View.VISIBLE); + try { + // m4 constructor is m4(LoginHistoryUI), but obj is LoginPasswordUI + // (a subclass). XposedHelpers.newInstance infers types from runtime + // class which is LoginPasswordUI, not LoginHistoryUI — no match. + // Use explicit param type to force the right constructor lookup. + Class loginHistoryUIClass = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.LoginHistoryUI", + obj.getClass().getClassLoader()); + tabletBtn.setOnClickListener( + (View.OnClickListener) XposedHelpers.newInstance( + m4Class, new Class[]{loginHistoryUIClass}, obj)); + } catch (Throwable e) { + XposedBridge.log("[WeChatHook] onResume setListener failed: " + e); + } + XposedBridge.log("[WeChatHook] onResume -> N VISIBLE + m4 attached for " + cls); + } + }); + XposedBridge.log("[WeChatHook] Hooked Activity.onResume for tablet button"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookProbe/onResume failed: " + t); + } + } + + private void hookInTabletEnv(ClassLoader cl) { + try { + Class bkClass = XposedHelpers.findClass("com.tencent.mm.ui.bk", cl); + Class x0Class = XposedHelpers.findClass("bm5.x0", cl); + XposedHelpers.findAndHookMethod(bkClass, "B", x0Class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + param.setResult(Boolean.TRUE); + } + }); + XposedHelpers.findAndHookMethod(bkClass, "A", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("[WeChatHook] bk.A() called -> TRUE"); + param.setResult(Boolean.TRUE); + } + }); + XposedBridge.log("[WeChatHook] Hooked bk.B(x0) + bk.A()"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookInTabletEnv failed: " + t); + } + } + + // PRIMARY FIX: Hook LoginPasswordUI.onCreate(Bundle) afterHookedMethod. + // + // Previous attempts to hook initView() and LoginHistoryUI.initView() both failed + // because ART AOT devirtualizes the call to this.initView() inside + // LoginHistoryUI.onCreate() — the compiled native code jumps directly past the + // Xposed entry point replacement. + // + // onCreate() itself is called by the Android framework via the ArtMethod pointer + // directly (no vtable devirtualization possible), so this hook fires reliably. + // By the time afterHookedMethod runs, super.onCreate() has completed which means + // LoginHistoryUI.initView() has already run and this.N is assigned. + private void hookLoginHistoryUIInitView(ClassLoader cl) { + final Class m4Class; + try { + m4Class = XposedHelpers.findClass("com.tencent.mm.plugin.account.ui.m4", cl); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] m4 class not found: " + t); + return; + } + + try { + XposedHelpers.findAndHookMethod( + "com.tencent.mm.plugin.account.ui.LoginPasswordUI", cl, + "onCreate", Bundle.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Object obj = param.thisObject; + View tabletBtn = (View) XposedHelpers.getObjectField(obj, "N"); + if (tabletBtn == null) { + XposedBridge.log("[WeChatHook] LoginPasswordUI.onCreate: N is null"); + return; + } + tabletBtn.setVisibility(View.VISIBLE); + tabletBtn.setOnClickListener( + (View.OnClickListener) XposedHelpers.newInstance(m4Class, obj)); + XposedBridge.log("[WeChatHook] LoginPasswordUI.onCreate() -> N VISIBLE + m4 attached"); + } + }); + XposedBridge.log("[WeChatHook] Hooked LoginPasswordUI.onCreate()"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookLoginPasswordUI.onCreate failed: " + t); + } + + // Belt-and-suspenders: also hook LoginHistoryUI.onCreate() for any subclass + // that doesn't override onCreate but does have the N button. + try { + XposedHelpers.findAndHookMethod( + "com.tencent.mm.plugin.account.ui.LoginHistoryUI", cl, + "onCreate", Bundle.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Object obj = param.thisObject; + if (obj.getClass().getName().equals( + "com.tencent.mm.plugin.account.ui.LoginPasswordUI")) return; + View tabletBtn = (View) XposedHelpers.getObjectField(obj, "N"); + if (tabletBtn == null) { + XposedBridge.log("[WeChatHook] LoginHistoryUI.onCreate: N is null"); + return; + } + tabletBtn.setVisibility(View.VISIBLE); + tabletBtn.setOnClickListener( + (View.OnClickListener) XposedHelpers.newInstance(m4Class, obj)); + XposedBridge.log("[WeChatHook] LoginHistoryUI.onCreate() -> N VISIBLE + m4 attached"); + } + }); + XposedBridge.log("[WeChatHook] Hooked LoginHistoryUI.onCreate()"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookLoginHistoryUI.onCreate failed: " + t); + } + } + + private void hookLoginUIInitView(ClassLoader cl) { + try { + final Class wbClass = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.wb", cl); + XposedHelpers.findAndHookMethod( + "com.tencent.mm.plugin.account.ui.MobileInputUI", cl, + "initView", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Object mobileInputUI = param.thisObject; + View tabletBtn = (View) XposedHelpers.getObjectField(mobileInputUI, "w"); + if (tabletBtn == null) { + XposedBridge.log("[WeChatHook] MobileInputUI.w is null"); + return; + } + tabletBtn.setVisibility(View.VISIBLE); + tabletBtn.setOnClickListener( + (View.OnClickListener) XposedHelpers.newInstance(wbClass, mobileInputUI)); + XposedBridge.log("[WeChatHook] MobileInputUI.initView() -> w VISIBLE + wb attached"); + } + }); + XposedBridge.log("[WeChatHook] Hooked MobileInputUI.initView()"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookLoginUIInitView(MobileInputUI) failed: " + t); + } + try { + final Class g7Class = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.g7", cl); + XposedHelpers.findAndHookMethod( + "com.tencent.mm.plugin.account.ui.LoginUI", cl, + "initView", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Object loginUI = param.thisObject; + View tabletBtn = (View) XposedHelpers.getObjectField(loginUI, "f992u"); + if (tabletBtn == null) return; + tabletBtn.setVisibility(View.VISIBLE); + tabletBtn.setOnClickListener( + (View.OnClickListener) XposedHelpers.newInstance(g7Class, loginUI)); + XposedBridge.log("[WeChatHook] LoginUI.initView() -> f992u VISIBLE + g7 attached"); + } + }); + XposedBridge.log("[WeChatHook] Hooked LoginUI.initView()"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookLoginUIInitView(LoginUI) failed: " + t); + } + } + + private void hookLoginSelectUICheck(ClassLoader cl) { + try { + Class callbackClass = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.LoginSelectUI$a", cl); + XposedHelpers.findAndHookMethod(callbackClass, "a", boolean.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("[WeChatHook] LoginSelectUI$a.a(" + param.args[0] + ") -> false"); + param.args[0] = false; + } + }); + XposedBridge.log("[WeChatHook] Hooked LoginSelectUI$a.a()"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookLoginSelectUICheck failed: " + t); + } + } + + private void hookLoginButtonClick(ClassLoader cl) { + try { + Class ljClass = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.lj", cl); + final Class loginSelectUIClass = XposedHelpers.findClass( + "com.tencent.mm.plugin.account.ui.LoginSelectUI", cl); + XposedHelpers.findAndHookMethod(ljClass, "onClick", View.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("[WeChatHook] lj.onClick() -> forcing LoginSelectUI"); + Activity welcomeActivity = (Activity) XposedHelpers.getObjectField( + param.thisObject, "d"); + Intent intent = new Intent(welcomeActivity, loginSelectUIClass); + intent.putExtra("mobile_input_purpose", 1); + welcomeActivity.startActivity(intent); + param.setResult(null); + } + }); + XposedBridge.log("[WeChatHook] Hooked lj.onClick() -> LoginSelectUI"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookLoginButtonClick failed: " + t); + } + } + + // ── Send message via internal API ──────────────────────────────────────── + // + // Registered in ALL WeChat processes. We don't yet know which process has gm0.j1 + // initialized — the first process where the receiver fires and succeeds wins. + // + // Trigger from ADB (must run as root — RECEIVER_NOT_EXPORTED): + // adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL \ + // --es to "filehelper" --es text "hello"' + private void hookSendMessage(ClassLoader cl, String procName) { + try { + XposedHelpers.findAndHookMethod( + "android.app.Application", null, "onCreate", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Context ctx = (Context) param.thisObject; + if (!"com.tencent.mm".equals(ctx.getPackageName())) return; + sAppContext = ctx; + XposedBridge.log("[WeChatHook] App.onCreate proc=" + procName + + " pid=" + android.os.Process.myPid()); + registerSendReceiver(ctx, procName); + } + }); + XposedBridge.log("[WeChatHook] Send hook registered in " + procName); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookSendMessage failed in " + procName + ": " + t); + } + + // ── w11.r0 constructor hooks ─────────────────────────────────────────── + // Log "by=US" vs "by=WeChat" to tell if WeChat's own UI send uses this ctor. + XC_MethodHook r0Hook = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + String by = sCalledByUs ? "US" : "WeChat"; + XposedBridge.log("[WeChatHook] w11.r0 ctor by=" + by + + " args=" + param.args.length + " proc=" + procName + + " pid=" + android.os.Process.myPid()); + if (sCalledByUs) { + forceInitDI(); + diagMCoreStorage("before-r0-ctor"); + } + } + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + String by = sCalledByUs ? "US" : "WeChat"; + if (param.hasThrowable()) { + Throwable t = param.getThrowable(); + XposedBridge.log("[WeChatHook] w11.r0 ctor THREW by=" + by + + " proc=" + procName + ": " + t); + if (sCalledByUs) + XposedBridge.log("[WeChatHook] r0-ctor-stack: " + + android.util.Log.getStackTraceString(t)); + return; + } + sFsGReady = true; + XposedBridge.log("[WeChatHook] w11.r0 ctor OK by=" + by + " proc=" + procName); + } + }; + for (Class[] sig : new Class[][]{ + {String.class, String.class, int.class, int.class, long.class}, + {String.class, String.class, int.class, int.class, Object.class}, + {long.class, int.class, String.class}, + {}}) { + try { + XposedHelpers.findAndHookConstructor("w11.r0", cl, + appendHook(sig, r0Hook)); + XposedBridge.log("[WeChatHook] Hooked w11.r0(" + sig.length + " params)"); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] w11.r0(" + sig.length + "p) hook failed: " + t); + } + } + + // ── w11.r0.H(String) — the actual dispatch method ───────────────────── + // Fires for EVERY send (ours AND WeChat UI sends). Shows if WeChat uses r0. + try { + XposedHelpers.findAndHookMethod("w11.r0", cl, "H", String.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + String by = sCalledByUs ? "US" : "WeChat"; + XposedBridge.log("[WeChatHook] w11.r0.H() called by=" + by + + " proc=" + procName + " pid=" + android.os.Process.myPid()); + } + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + if (param.hasThrowable()) + XposedBridge.log("[WeChatHook] w11.r0.H() threw: " + param.getThrowable()); + else + XposedBridge.log("[WeChatHook] w11.r0.H() OK proc=" + procName); + } + }); + XposedBridge.log("[WeChatHook] Hooked w11.r0.H(String) in " + procName); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] w11.r0.H hook failed in " + procName + ": " + t); + } + + // ── Diagnostic probes on the constructor call chain ─────────────────── + // Each probe fires when the method is called from inside the r0 constructor + // (by WeChat OR by us). They trace which methods succeed before the NPE. + hookProbeMethod(cl, "gm0.j1", "u", "gm0.j1.u"); + hookProbeMethod(cl, "c01.z1", "r", "c01.z1.r"); + // gm0.b0.c() is a virtual method — probe it too + try { + XposedHelpers.findAndHookMethod("gm0.b0", cl, "c", + new XC_MethodHook() { + @Override protected void beforeHookedMethod(MethodHookParam p) { + XposedBridge.log("[WeChatHook] PROBE gm0.b0.c() called"); + } + @Override protected void afterHookedMethod(MethodHookParam p) { + if (p.hasThrowable()) XposedBridge.log("[WeChatHook] PROBE gm0.b0.c() THREW: " + p.getThrowable()); + else XposedBridge.log("[WeChatHook] PROBE gm0.b0.c() → " + p.getResult()); + } + }); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] gm0.b0.c probe failed: " + t); + } + } + + private static Object[] appendHook(Class[] params, XC_MethodHook hook) { + Object[] result = new Object[params.length + 1]; + System.arraycopy(params, 0, result, 0, params.length); + result[params.length] = hook; + return result; + } + + private void hookProbeMethod(ClassLoader cl, String cls, String method, String tag) { + try { + XposedHelpers.findAndHookMethod(cls, cl, method, + new XC_MethodHook() { + @Override protected void beforeHookedMethod(MethodHookParam p) { + XposedBridge.log("[WeChatHook] PROBE " + tag + "() called"); + } + @Override protected void afterHookedMethod(MethodHookParam p) { + if (p.hasThrowable()) XposedBridge.log("[WeChatHook] PROBE " + tag + "() THREW: " + p.getThrowable()); + else XposedBridge.log("[WeChatHook] PROBE " + tag + "() → " + p.getResult()); + } + }); + XposedBridge.log("[WeChatHook] Probe hooked: " + tag); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] Probe " + tag + " failed: " + t); + } + } + + private static boolean isDIReady() { + if (sWeChatCL == null) return false; + try { + XposedHelpers.callStaticMethod(XposedHelpers.findClass("gm0.j1", sWeChatCL), "i"); + return true; + } catch (Throwable e) { return false; } + } + + // Hook gm0.j1.g(km0.c) to detect when WeChat DI kernel is initialized. + // gm0.j1.g() sets gm0.j1.m (the singleton) and is called lazily — not during + // Application.onCreate, so our broadcast often arrives first. After g() fires, + // gm0.j1.i() will succeed and w11.r0 can be constructed. + private static void hookDIInit(ClassLoader cl, String procName) { + try { + Class km0cCls = XposedHelpers.findClass("km0.c", cl); + XposedHelpers.findAndHookMethod("gm0.j1", cl, "g", km0cCls, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam p) throws Throwable { + XposedBridge.log("[WeChatHook] gm0.j1.g() fired — DI ready in " + procName); + String t = sPendingTalker, x = sPendingText, pr = sPendingProc; + if (t != null && x != null && procName.equals(pr)) { + sPendingTalker = null; sPendingText = null; sPendingProc = null; + XposedBridge.log("[WeChatHook] resuming pending send to=" + t + " proc=" + procName); + new Handler(Looper.getMainLooper()).postDelayed( + () -> trySendWithRetry(t, x, 0, procName), 50); + } + } + }); + XposedBridge.log("[WeChatHook] gm0.j1.g hook OK in " + procName); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] gm0.j1.g hook failed in " + procName + ": " + t); + } + } + + private static void diagMCoreStorage(String where) { + if (sWeChatCL == null) return; + try { + Class j1Cls = XposedHelpers.findClass("gm0.j1", sWeChatCL); + Object j1 = XposedHelpers.callStaticMethod(j1Cls, "i"); + Object mCS = (j1 != null) ? XposedHelpers.getObjectField(j1, "d") : null; + XposedBridge.log("[WeChatHook] " + where + " mCoreStorage=" + mCS); + } catch (Throwable e) { + XposedBridge.log("[WeChatHook] " + where + " mCoreStorage-check: " + e); + } + } + + private static void forceInitDI() { + if (sWeChatCL == null) return; + try { + Class fsgCls = XposedHelpers.findClass("fs.g", sWeChatCL); + java.lang.reflect.Field faFld = fsgCls.getDeclaredField("a"); + faFld.setAccessible(true); + Object[] arr = (Object[]) faFld.get(null); + + if (arr == null || arr.length == 0 || arr[0] == null) { + Class k2Cls = XposedHelpers.findClass("fs.k2", sWeChatCL); + java.lang.reflect.Field k2eFld = k2Cls.getDeclaredField("e"); + k2eFld.setAccessible(true); + Object k2e = k2eFld.get(null); + if (k2e == null) { + XposedBridge.log("[WeChatHook] forceInitDI: fs.k2.e null"); + return; + } + if (arr == null || arr.length == 0) { + arr = new Object[1]; + faFld.set(null, arr); + } + arr[0] = k2e; + XposedBridge.log("[WeChatHook] forceInitDI: fs.g.a[0]=" + k2e.getClass().getName()); + } + } catch (Throwable e) { + XposedBridge.log("[WeChatHook] forceInitDI: " + e); + } + } + + private static void trySendWithRetry(String talker, String text, int attempt, String procName) { + // If DI not ready: park the args and wait for gm0.j1.g() hook to trigger retry. + // Only the first process to check parks the pending (others may overwrite, which is fine + // — the last write wins and gm0.j1.g() fires per-process anyway). + if (!isDIReady()) { + if (attempt == 0) { + XposedBridge.log("[WeChatHook] DI not ready proc=" + procName + + " — parking pending send to=" + talker); + sPendingTalker = talker; + sPendingText = text; + sPendingProc = procName; + } else { + XposedBridge.log("[WeChatHook] DI still not ready proc=" + procName + " attempt=" + attempt); + } + return; + } + try { + sendViaR0(talker, text); + XposedBridge.log("[WeChatHook] Send OK to=" + talker + " proc=" + procName); + } catch (Throwable t) { + String tMsg = t.getMessage() != null ? t.getMessage() : t.toString(); + XposedBridge.log("[WeChatHook] Send FAILED proc=" + procName + " to=" + talker + ": " + tMsg); + if (attempt < 3) { + long delay = 1000L << attempt; + new Handler(Looper.getMainLooper()).postDelayed( + () -> trySendWithRetry(talker, text, attempt + 1, procName), delay); + } + } + } + + private static void registerSendReceiver(Context ctx, String procName) { + BroadcastReceiver recv = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String talker = intent.getStringExtra("to"); + String text = intent.getStringExtra("text"); + if (talker == null || talker.isEmpty() || text == null || text.isEmpty()) { + XposedBridge.log("[WeChatHook] SEND_MSG_INTERNAL: missing 'to' or 'text'"); + return; + } + XposedBridge.log("[WeChatHook] SEND_MSG_INTERNAL proc=" + procName + + " pid=" + android.os.Process.myPid() + + " to=" + talker + " len=" + text.length()); + new Handler(Looper.getMainLooper()).post(() -> trySendWithRetry(talker, text, 0, procName)); + } + }; + ctx.registerReceiver(recv, new IntentFilter(ACTION_SEND), Context.RECEIVER_NOT_EXPORTED); + XposedBridge.log("[WeChatHook] BroadcastReceiver registered in " + procName); + } + + private static void sendViaR0(String talker, String text) throws Throwable { + Class r0Class = XposedHelpers.findClass("w11.r0", sWeChatCL); + long ts = System.currentTimeMillis(); + Object msg; + sCalledByUs = true; + try { + try { + msg = XposedHelpers.newInstance(r0Class, + new Class[]{String.class, String.class, int.class, int.class, long.class}, + talker, text, 1, 0, ts); + } catch (Throwable t) { + msg = XposedHelpers.newInstance(r0Class, + new Class[]{String.class, String.class, int.class, int.class, Object.class}, + talker, text, 1, 0, (Object) ts); + } + } finally { + sCalledByUs = false; + } + + try { + Class u0 = XposedHelpers.findClass("com.tencent.mm.modelbase.u0", sWeChatCL); + Object stub = Proxy.newProxyInstance(sWeChatCL, new Class[]{u0}, + (proxy, method, args) -> { + XposedBridge.log("[WeChatHook] send-cb: " + method.getName()); + return null; + }); + XposedHelpers.setObjectField(msg, "d", stub); + } catch (Throwable ignored) {} + + sCalledByUs = true; + try { + XposedHelpers.callMethod(msg, "H", ""); + } finally { + sCalledByUs = false; + } + } +} diff --git a/xhs_lsposed_a16_fix/wechathook_v2_signed.apk b/xhs_lsposed_a16_fix/wechathook_v2_signed.apk new file mode 100644 index 0000000..513ad86 --- /dev/null +++ b/xhs_lsposed_a16_fix/wechathook_v2_signed.apk