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); } } }