WeChatSendHook.java 5.35 KB
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);
    }
}