WeChatHook.java
11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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);
}
}
}