WeChatHook.java 11.1 KB
package com.example.wechathook;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
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;

/**
 * WeChatHook: 在 Pixel 10 Pro Fold (Android 16 / WeChat 8.0.74) 上开启「以平板登录」选项。
 *
 * 目标:LoginPasswordUI(密码登录屏)里有一个隐藏的平板登录按钮(字段 N),
 *       由 LoginHistoryUI.initView() 在 `if (bk.A() || bk.Q())` 为 true 时显示。
 *       Pixel 的品牌/型号不在微信白名单内,两者均返回 false。
 *
 * 双保险策略:
 *   1. hookInTabletEnv: 直接 hook bk.A()/bk.B() 返回 true。
 *      静态方法不走 vtable,AOT 编译后仍可被 Xposed 拦截(若调用方没有将其内联)。
 *      实测:bk.A() hook 在 initView() 的 AOT 编译调用路径上有效,
 *      使 initView() 自动显示按钮并绑好 m4 click listener。
 *
 *   2. hookTabletButtonViaOnResume: hook Android 框架的 Activity.onResume(),
 *      过滤 LoginPasswordUI,在 onResume 时强制将按钮设为 VISIBLE。
 *      框架跨 APK 调用不可能被 WeChat 的 AOT 内联,所以这个 hook 必然触发。
 *      兜底:即使 bk.A() hook 在某些 call site 被 AOT 内联绕过,onResume hook 也能补救。
 */
public class WeChatHook implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        if (!"com.tencent.mm".equals(lpparam.packageName)) return;
        XposedBridge.log("[WeChatHook] Injected into WeChat " + lpparam.processName);
        ClassLoader cl = lpparam.classLoader;
        hookInTabletEnv(cl);
        hookTabletButtonViaOnResume(cl);
        hookMobileInputUI(cl);
        hookLoginUI(cl);
        hookLoginSelectUICheck(cl);
        hookWelcomeButtonClick(cl);
    }

    // 策略 1: hook bk.A()/bk.B() 直接返回 true。
    // 静态方法没有 vtable,Xposed 的 ArtMethod 入口替换对静态调用有效。
    // bk.A() = inTabletEnv() 缓存层;bk.B(x0) = 实际逻辑。
    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) {
                    param.setResult(Boolean.TRUE);
                }
            });
            XposedHelpers.findAndHookMethod(bkClass, "A", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) {
                    XposedBridge.log("[WeChatHook] bk.A() -> TRUE");
                    param.setResult(Boolean.TRUE);
                }
            });
            XposedBridge.log("[WeChatHook] Hooked bk.A()/bk.B()");
        } catch (Throwable t) {
            XposedBridge.log("[WeChatHook] hookInTabletEnv failed: " + t);
        }
    }

    // 策略 2: hook Activity.onResume(),过滤 LoginPasswordUI / LoginHistoryUI,
    // 强制 this.N 可见并绑定 m4 click listener。
    //
    // 关键发现:WeChat 的虚方法(initView/onCreate)被 AOT devirtualize 成 native 直跳,
    // Xposed 的 ArtMethod 入口替换被绕过,hook 注册成功但回调永远不触发。
    // 而 Android 框架调 Activity.onResume() 是跨 APK 的 virtual dispatch,
    // 不可能被 WeChat 自身的 AOT 编译内联,所以这里的 hook 稳定触发。
    //
    // 类层次:LoginPasswordUI extends LoginHistoryUI extends MMActivity extends Activity
    // 平板按钮:LoginHistoryUI.N (Button, resource id 2131312963)
    // Click listener:m4(LoginHistoryUI) → l.h(loginUI, "login_exdevice", ".ui.LoginAsExDeviceUI")
    private void hookTabletButtonViaOnResume(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] getField N failed: " + e);
                        return;
                    }
                    if (tabletBtn == null) {
                        XposedBridge.log("[WeChatHook] N is null for " + cls);
                        return;
                    }
                    tabletBtn.setVisibility(View.VISIBLE);
                    try {
                        // XposedHelpers.newInstance 按运行时类型查找构造函数,
                        // obj 运行时类型是 LoginPasswordUI,但 m4 构造函数接收 LoginHistoryUI。
                        // 必须显式传 parameterTypes 才能匹配到正确的构造函数。
                        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] setListener failed: " + e);
                    }
                    XposedBridge.log("[WeChatHook] onResume -> N VISIBLE for " + cls);
                }
            });
            XposedBridge.log("[WeChatHook] Hooked Activity.onResume (tablet button)");
        } catch (Throwable t) {
            XposedBridge.log("[WeChatHook] hookTabletButtonViaOnResume failed: " + t);
        }
    }

    // 兜底:MobileInputUI 是首次登录流程里的手机号输入界面,也有平板按钮(字段 w)
    private void hookMobileInputUI(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 obj = param.thisObject;
                    View btn = (View) XposedHelpers.getObjectField(obj, "w");
                    if (btn == null) return;
                    btn.setVisibility(View.VISIBLE);
                    btn.setOnClickListener(
                            (View.OnClickListener) XposedHelpers.newInstance(wbClass, obj));
                    XposedBridge.log("[WeChatHook] MobileInputUI.w VISIBLE");
                }
            });
        } catch (Throwable t) {
            XposedBridge.log("[WeChatHook] hookMobileInputUI failed: " + t);
        }
    }

    // 兜底:LoginUI 是通过 WelcomeActivity → 登录按钮 进入的界面,平板按钮字段 f992u
    private void hookLoginUI(ClassLoader cl) {
        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 obj = param.thisObject;
                    View btn = (View) XposedHelpers.getObjectField(obj, "f992u");
                    if (btn == null) return;
                    btn.setVisibility(View.VISIBLE);
                    btn.setOnClickListener(
                            (View.OnClickListener) XposedHelpers.newInstance(g7Class, obj));
                    XposedBridge.log("[WeChatHook] LoginUI.f992u VISIBLE");
                }
            });
        } catch (Throwable t) {
            XposedBridge.log("[WeChatHook] hookLoginUI failed: " + t);
        }
    }

    // 拦截 LoginSelectUI 的服务端品牌校验回调,阻止服务端隐藏「phone_and_pad」选项
    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) {
                    param.args[0] = false;
                    XposedBridge.log("[WeChatHook] LoginSelectUI$a.a -> false");
                }
            });
            XposedBridge.log("[WeChatHook] Hooked LoginSelectUI$a.a()");
        } catch (Throwable t) {
            XposedBridge.log("[WeChatHook] hookLoginSelectUICheck failed: " + t);
        }
    }

    // 兜底:WelcomeActivity 的登录按钮(lj)直接跳 LoginSelectUI 而非 LoginPasswordUI
    private void hookWelcomeButtonClick(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 {
                    Activity welcome = (Activity) XposedHelpers.getObjectField(
                            param.thisObject, "d");
                    Intent intent = new Intent(welcome, loginSelectUIClass);
                    intent.putExtra("mobile_input_purpose", 1);
                    welcome.startActivity(intent);
                    param.setResult(null);
                    XposedBridge.log("[WeChatHook] lj.onClick -> LoginSelectUI");
                }
            });
            XposedBridge.log("[WeChatHook] Hooked lj.onClick()");
        } catch (Throwable t) {
            XposedBridge.log("[WeChatHook] hookWelcomeButtonClick failed: " + t);
        }
    }
}