fix(tng): 修复区号页 HWUI 闪退并放行 Compose HW 绘制

拦截 HardwareRenderer.setName,校验/缓存 libandroid.so,避免软件绘制撞 hardware bitmap;附带 Money Packet hook 与 mitm 脚本。
This commit is contained in:
mars
2026-08-04 12:26:27 +08:00
parent 609635aba1
commit 5378a34f58
8 changed files with 1394 additions and 76 deletions

View File

@@ -18,6 +18,7 @@ public final class HookBridge {
public static final String SOURCE_XPOSED_SUNCORP_NOTIFY = "xposed_suncorp_notify";
public static final String SOURCE_XPOSED_UBANK = "xposed_ubank";
public static final String SOURCE_XPOSED_UBANK_NOTIFY = "xposed_ubank_notify";
public static final String SOURCE_XPOSED_TNG_MMP = "xposed_tng_mmp";
private HookBridge() {
}

View File

@@ -3,6 +3,7 @@ package com.miraclegarden.smsmessage.xposed;
import com.miraclegarden.smsmessage.xposed.hook.MariBankRootBypassHook;
import com.miraclegarden.smsmessage.xposed.hook.MariBankShpsNativeHook;
import com.miraclegarden.smsmessage.xposed.hook.SuncorpBankMessageHook;
import com.miraclegarden.smsmessage.xposed.hook.TngMoneyPacketHook;
import com.miraclegarden.smsmessage.xposed.hook.TngRootBypassHook;
import com.miraclegarden.smsmessage.xposed.hook.SqliteMessageHook;
import com.miraclegarden.smsmessage.xposed.hook.TelegramMessageHook;
@@ -66,6 +67,7 @@ public class MainHook implements IXposedHookLoadPackage {
if (TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
TngRootBypassHook.install(lpparam);
TngMoneyPacketHook.install(lpparam);
return;
}

View File

@@ -0,0 +1,579 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.content.Context;
import android.text.TextUtils;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import com.miraclegarden.smsmessage.xposed.HookForwarder;
import org.json.JSONArray;
import org.json.JSONObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
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;
/**
* TNG eWallet Money Packet群红包领取统计。
* 从 HTTP 响应 JSON / Gson 反序列化结果中提取 receiverList昵称 + 金额),转发到 notiMessage。
*/
public final class TngMoneyPacketHook {
private static final String TAG = "notiMessageHook/TngMmp";
private static final String PACKAGE = TngRootBypassHook.PACKAGE;
private static final int DEDUP_SIZE = 256;
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
private static final HashSet<String> RECENT_SET = new HashSet<>();
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
private TngMoneyPacketHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
return;
}
hookOkHttpUrl(lpparam);
hookResponseBody(lpparam);
hookGsonFromJson(lpparam);
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
}
private static void hookOkHttpUrl(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"okhttp3.Request$Builder",
lpparam.classLoader,
"build",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
try {
Object url = XposedHelpers.callMethod(param.getResult(), "url");
if (url != null) {
CURRENT_REQUEST_URL.set(String.valueOf(url));
}
} catch (Throwable ignored) {
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " Request.Builder.build hook failed: " + t.getMessage());
}
}
private static void hookResponseBody(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"okhttp3.ResponseBody",
lpparam.classLoader,
"string",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String body = (String) param.getResult();
String url = CURRENT_REQUEST_URL.get();
CURRENT_REQUEST_URL.remove();
if (!isMmpPayload(url, body)) {
return;
}
forwardParsedJson(body, url, "http");
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " ResponseBody.string hook failed: " + t.getMessage());
}
}
private static void hookGsonFromJson(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook hook = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result == null) {
return;
}
String className = result.getClass().getName();
if (!isMmpModelClass(className)) {
return;
}
try {
String json = buildJsonFromObject(result);
if (TextUtils.isEmpty(json)) {
return;
}
forwardParsedJson(json, className, "gson");
} catch (Throwable t) {
XposedBridge.log(TAG + " gson capture failed: " + t.getMessage());
}
}
};
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"fromJson",
String.class,
Class.class,
hook);
} catch (Throwable t) {
XposedBridge.log(TAG + " Gson.fromJson(Class) hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"fromJson",
String.class,
java.lang.reflect.Type.class,
hook);
} catch (Throwable ignored) {
}
}
private static boolean isMmpPayload(String url, String body) {
if (TextUtils.isEmpty(body) || body.length() < 24) {
return false;
}
String lower = body.toLowerCase(Locale.US);
boolean jsonHit = lower.contains("receiverlist")
|| lower.contains("\"claimedamount\"")
|| lower.contains("mmpreceiver")
|| (lower.contains("nick") && lower.contains("amount") && lower.contains("mmp"));
if (jsonHit) {
return true;
}
if (TextUtils.isEmpty(url)) {
return false;
}
String urlLower = url.toLowerCase(Locale.US);
return urlLower.contains("mmp") || urlLower.contains("moneypacket");
}
private static boolean isMmpModelClass(String className) {
if (TextUtils.isEmpty(className)) {
return false;
}
return className.contains("MmpDetailResult")
|| className.contains("MmpClaimQueryResult")
|| className.contains("MmpClaimResult")
|| className.contains("MmpReceiver")
|| className.contains("MmpDetailLeaderboard");
}
private static void forwardParsedJson(String json, String sourceHint, String channel) {
MmpSnapshot snapshot = parseSnapshot(json);
if (snapshot == null || snapshot.claims.isEmpty()) {
return;
}
String dedupKey = snapshot.dedupKey();
if (!remember(dedupKey)) {
return;
}
Context context = getContext();
if (context == null) {
return;
}
String title = TextUtils.isEmpty(snapshot.title) ? "TNG Money Packet" : snapshot.title;
String content = snapshot.formatForForward(channel, sourceHint);
HookForwarder.forward(context, PACKAGE, title, content, HookBridge.SOURCE_XPOSED_TNG_MMP);
XposedBridge.log(TAG + " captured packet=" + snapshot.packetId
+ " claims=" + snapshot.claims.size() + " via " + channel);
}
private static MmpSnapshot parseSnapshot(String json) {
try {
JSONObject root = new JSONObject(json.trim());
return parseSnapshot(root, null);
} catch (Throwable t) {
return null;
}
}
private static MmpSnapshot parseSnapshot(JSONObject root, MmpSnapshot base) {
MmpSnapshot snapshot = base != null ? base : new MmpSnapshot();
fillMeta(root, snapshot);
JSONArray receiverList = findReceiverList(root);
if (receiverList != null) {
for (int i = 0; i < receiverList.length(); i++) {
JSONObject item = receiverList.optJSONObject(i);
if (item == null) {
continue;
}
ClaimLine line = parseClaimLine(item);
if (line != null) {
snapshot.claims.add(line);
}
}
}
Iterator<String> keys = root.keys();
while (keys.hasNext()) {
String key = keys.next();
Object val = root.opt(key);
if (val instanceof JSONObject) {
parseSnapshot((JSONObject) val, snapshot);
} else if (val instanceof JSONArray) {
JSONArray arr = (JSONArray) val;
for (int i = 0; i < arr.length(); i++) {
Object elem = arr.opt(i);
if (elem instanceof JSONObject) {
ClaimLine line = parseClaimLine((JSONObject) elem);
if (line != null && looksLikeClaimRow((JSONObject) elem)) {
snapshot.claims.add(line);
}
parseSnapshot((JSONObject) elem, snapshot);
}
}
}
}
return snapshot;
}
private static JSONArray findReceiverList(JSONObject obj) {
Iterator<String> keys = obj.keys();
while (keys.hasNext()) {
String key = keys.next();
Object val = obj.opt(key);
if ("receiverList".equalsIgnoreCase(key) && val instanceof JSONArray) {
return (JSONArray) val;
}
if (val instanceof JSONObject) {
JSONArray nested = findReceiverList((JSONObject) val);
if (nested != null) {
return nested;
}
}
}
return null;
}
private static void fillMeta(JSONObject obj, MmpSnapshot snapshot) {
putIfPresent(obj, snapshot, "packetId", "mmpId", "moneyPacketId", "id");
putIfPresent(obj, snapshot, "groupId", "chatId", "conversationId");
putIfPresent(obj, snapshot, "groupName", "chatName", "conversationName");
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName");
putIfPresent(obj, snapshot, "totalAmount", "packetAmount", "amount");
if (TextUtils.isEmpty(snapshot.title)) {
String merchant = firstNonEmpty(
obj.optString("merchantName", null),
obj.optString("shopName", null));
if (!TextUtils.isEmpty(merchant)) {
snapshot.title = merchant;
}
}
}
private static void putIfPresent(JSONObject obj, MmpSnapshot snapshot, String... keys) {
for (String key : keys) {
if (!obj.has(key)) {
continue;
}
String val = obj.optString(key, null);
if (TextUtils.isEmpty(val) || "null".equalsIgnoreCase(val)) {
continue;
}
if (key.toLowerCase(Locale.US).contains("packet") || key.equals("mmpId") || key.equals("id")) {
if (TextUtils.isEmpty(snapshot.packetId)) {
snapshot.packetId = val;
}
} else if (key.toLowerCase(Locale.US).contains("group")) {
if (TextUtils.isEmpty(snapshot.groupId)) {
snapshot.groupId = val;
}
} else if (key.toLowerCase(Locale.US).contains("name") && key.toLowerCase(Locale.US).contains("group")) {
snapshot.title = val;
} else if (key.toLowerCase(Locale.US).contains("sender")
|| key.toLowerCase(Locale.US).contains("operator")) {
if (TextUtils.isEmpty(snapshot.senderName)) {
snapshot.senderName = val;
}
} else if (key.toLowerCase(Locale.US).contains("total") || key.equals("amount")) {
if (TextUtils.isEmpty(snapshot.totalAmount)) {
snapshot.totalAmount = val;
}
}
}
}
private static boolean looksLikeClaimRow(JSONObject obj) {
String name = firstNonEmpty(
obj.optString("nickName", null),
obj.optString("displayName", null),
obj.optString("userName", null),
obj.optString("receiverName", null));
String amount = firstNonEmpty(
obj.optString("claimedAmount", null),
obj.optString("receiveAmount", null),
obj.optString("amount", null));
return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(amount);
}
private static ClaimLine parseClaimLine(JSONObject obj) {
String name = firstNonEmpty(
obj.optString("nickName", null),
obj.optString("displayName", null),
obj.optString("userName", null),
obj.optString("receiverName", null),
obj.optString("name", null));
String amount = firstNonEmpty(
obj.optString("claimedAmount", null),
obj.optString("receiveAmount", null),
obj.optString("amount", null));
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(amount)) {
return null;
}
ClaimLine line = new ClaimLine();
line.nickname = name;
line.amount = amount;
line.claimTime = firstNonEmpty(
obj.optString("claimTime", null),
obj.optString("claimedTime", null),
obj.optString("receiveTime", null));
return line;
}
private static String buildJsonFromObject(Object root) {
JSONObject json = objectToJson(root, new HashSet<Integer>(), 0);
return json != null ? json.toString() : null;
}
private static JSONObject objectToJson(Object obj, Set<Integer> visited, int depth) {
if (obj == null || depth > 6) {
return null;
}
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
JSONObject wrap = new JSONObject();
try {
wrap.put("value", obj);
} catch (Throwable ignored) {
}
return wrap;
}
int identity = System.identityHashCode(obj);
if (visited.contains(identity)) {
return null;
}
visited.add(identity);
JSONObject out = new JSONObject();
Class<?> clazz = obj.getClass();
if (clazz.isArray()) {
JSONArray arr = new JSONArray();
int len = Array.getLength(obj);
for (int i = 0; i < len; i++) {
Object elem = Array.get(obj, i);
JSONObject child = objectToJson(elem, visited, depth + 1);
if (child != null) {
arr.put(child);
}
}
try {
out.put("array", arr);
} catch (Throwable ignored) {
}
return out;
}
if (obj instanceof Iterable) {
JSONArray arr = new JSONArray();
for (Object elem : (Iterable<?>) obj) {
JSONObject child = objectToJson(elem, visited, depth + 1);
if (child != null) {
arr.put(child);
}
}
try {
out.put("receiverList", arr);
} catch (Throwable ignored) {
}
return out;
}
for (Field field : clazz.getDeclaredFields()) {
String fname = field.getName();
if (fname.contains("$") || fname.startsWith("CREATOR")) {
continue;
}
try {
field.setAccessible(true);
Object val = field.get(obj);
if (val == null) {
continue;
}
if (isPrimitiveLike(val)) {
out.put(fname, String.valueOf(val));
} else if (val instanceof Iterable || val.getClass().isArray()) {
JSONArray arr = new JSONArray();
if (val instanceof Iterable) {
for (Object elem : (Iterable<?>) val) {
putJsonValue(arr, elem, visited, depth + 1);
}
} else {
int len = Array.getLength(val);
for (int i = 0; i < len; i++) {
putJsonValue(arr, Array.get(val, i), visited, depth + 1);
}
}
out.put(fname, arr);
} else if (val.getClass().getName().startsWith("my.com.tngdigital")
|| val.getClass().getName().contains("Mmp")) {
JSONObject child = objectToJson(val, visited, depth + 1);
if (child != null) {
out.put(fname, child);
}
} else if (isPrimitiveLikeViaGetter(obj, fname)) {
out.put(fname, String.valueOf(val));
}
} catch (Throwable ignored) {
}
}
return out.length() > 0 ? out : null;
}
private static void putJsonValue(JSONArray arr, Object elem, Set<Integer> visited, int depth) {
if (elem == null) {
return;
}
if (isPrimitiveLike(elem)) {
arr.put(String.valueOf(elem));
return;
}
JSONObject child = objectToJson(elem, visited, depth);
if (child != null) {
arr.put(child);
}
}
private static boolean isPrimitiveLike(Object val) {
return val instanceof String
|| val instanceof Number
|| val instanceof Boolean
|| val instanceof Character;
}
private static boolean isPrimitiveLikeViaGetter(Object obj, String fieldName) {
try {
String suffix = fieldName.substring(0, 1).toUpperCase(Locale.US) + fieldName.substring(1);
for (String prefix : new String[]{"get", "is"}) {
try {
Method m = obj.getClass().getMethod(prefix + suffix);
Class<?> rt = m.getReturnType();
return rt == String.class || Number.class.isAssignableFrom(rt)
|| rt == boolean.class || rt == Boolean.class;
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return false;
}
private static boolean remember(String key) {
if (TextUtils.isEmpty(key)) {
return false;
}
if (RECENT_SET.contains(key)) {
return false;
}
RECENT_SET.add(key);
RECENT_KEYS.addLast(key);
while (RECENT_KEYS.size() > DEDUP_SIZE) {
String old = RECENT_KEYS.removeFirst();
RECENT_SET.remove(old);
}
return true;
}
private static String firstNonEmpty(String... values) {
if (values == null) {
return null;
}
for (String v : values) {
if (!TextUtils.isEmpty(v) && !"null".equalsIgnoreCase(v)) {
return v;
}
}
return null;
}
private static Context getContext() {
try {
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
if (app instanceof Context) {
return (Context) app;
}
} catch (Throwable ignored) {
}
return null;
}
private static final class MmpSnapshot {
String packetId;
String groupId;
String title;
String senderName;
String totalAmount;
final List<ClaimLine> claims = new ArrayList<>();
String dedupKey() {
StringBuilder sb = new StringBuilder();
sb.append(TextUtils.isEmpty(packetId) ? "?" : packetId).append('|');
for (ClaimLine line : claims) {
sb.append(line.nickname).append('=').append(line.amount).append(';');
}
return sb.toString();
}
String formatForForward(String channel, String sourceHint) {
StringBuilder sb = new StringBuilder();
sb.append("[MMP统计] ");
if (!TextUtils.isEmpty(packetId)) {
sb.append("packet=").append(packetId).append(' ');
}
if (!TextUtils.isEmpty(groupId)) {
sb.append("group=").append(groupId).append(' ');
}
if (!TextUtils.isEmpty(senderName)) {
sb.append("sender=").append(senderName).append(' ');
}
if (!TextUtils.isEmpty(totalAmount)) {
sb.append("total=").append(totalAmount).append(' ');
}
sb.append("via=").append(channel);
if (!TextUtils.isEmpty(sourceHint)) {
sb.append(" src=").append(sourceHint.length() > 120
? sourceHint.substring(0, 120) + "..." : sourceHint);
}
sb.append("\n");
for (ClaimLine line : claims) {
sb.append(line.nickname).append(" -> ").append(line.amount);
if (!TextUtils.isEmpty(line.claimTime)) {
sb.append(" (").append(line.claimTime).append(')');
}
sb.append('\n');
}
return sb.toString().trim();
}
}
private static final class ClaimLine {
String nickname;
String amount;
String claimTime;
}
}

View File

@@ -45,6 +45,12 @@ public final class TngRootBypassHook {
/** Promon 混淆包名1.9.10 为 vhvlnqgy旧版为 xwwqazamx。 */
private static final String[] PROMON_PKG_PREFIXES = {"vhvlnqgy", "xwwqazamx"};
/**
* 登录优先:少 Hook、少短路 native-bridge避免 JNI DeleteLocalRef 损坏 → Runtime abort。
* 能进 UserLogin 后再逐步打开诊断 Hook。
*/
private static final boolean LOGIN_FIRST_MINIMAL = true;
private static final String SECURITY_ERROR_ACTIVITY =
"my.com.tngdigital.common.security.ui.SecurityErrorActivity";
@@ -230,11 +236,15 @@ public final class TngRootBypassHook {
}
XposedBridge.log(TAG + " install for " + lpparam.packageName
+ " pid=" + Process.myPid()
+ " proc=" + getProcessName());
+ " proc=" + getProcessName()
+ " minimal=" + LOGIN_FIRST_MINIMAL);
// Splash 常卡死Instrumentation 强拉即可。双通道会抢跑导致 Login 重载/卡死。
hookEarlyAttachLog(lpparam);
hookConscryptStatsLogGuard();
if (LOGIN_FIRST_MINIMAL) {
installLoginFirstMinimal(lpparam);
return;
}
hookRegistrationFlowGuard(lpparam);
hookSplashForceLogin(lpparam);
hookLoginDismissSplash(lpparam);
@@ -277,6 +287,43 @@ public final class TngRootBypassHook {
hookWebViewErrorDiag(lpparam);
}
/** 最小集:藏 root + 拦自杀 Java 链 + bl#a/b不碰 native-bridge 探测短路。 */
private static void installLoginFirstMinimal(XC_LoadPackage.LoadPackageParam lpparam) {
hookSplashForceLogin(lpparam);
hookLoginDismissSplash(lpparam);
// 点区号会走 onCountryClick → i7.l loading真 show → HWUI setName/gralloc abort
hookLoadingDialogSkip();
// setName 一律 noopA16+Zygisk 下 native setName→dlopen libandroid 易 ART abort
// 区号 Compose 必须 HW软件绘制会 IllegalArgumentException(hardware bitmaps)。
hookHardwareRendererSetNameNoop();
hookCallingCodeAllowHwSurface(lpparam);
RootBypassHelper.hookFileExists(lpparam);
RootBypassHelper.hookRuntimeExec(lpparam);
RootBypassHelper.hookSystemGetProperty(lpparam);
ProcMapsFilterHook.install(lpparam);
hookAntiSuicide();
hookPromonSuicideUpstream(lpparam);
hookUncaughtPromonException(lpparam);
hookKillApplicationHandler(lpparam);
hookBlockSecurityErrorLaunch(lpparam);
hookPromonNativeGuard(lpparam);
hookPromonLifecycle(lpparam);
hookActivityThreadExit(lpparam);
hookFinishAllActivityAndKillApp(lpparam);
hookSecurityUrlOpeners(lpparam);
hookJailBroken(lpparam);
hookJailBrokenRpc(lpparam);
hookAppSecurityManager(lpparam);
hookAppSecurityCallbacks(lpparam);
hookSecurityErrorActivity(lpparam);
// seccomp 开着时必须 stub TigerTally init否则 fork getprop 永不退出 → App.onCreate ANR
hookTigerTally(lpparam);
hookTigerTallyAppWrappers(lpparam);
XposedBridge.log(TAG + " login-first minimal hooks armed");
}
/**
* Zygisk/命名空间下 android.util.StatsLog native 常 UnsatisfiedLinkError
* Conscrypt TLS 指标线程一写就炸 → ART fatal。直接 noop 指标写入。
@@ -1066,30 +1113,130 @@ public final class TngRootBypassHook {
}
/**
* 非区号:清 HW flag + 拦 enableHW成对防半开黑屏
* 区号 Compose / BottomSelect完整 HW软件绘制 Compose 必黑)
* Dialog.show 窗口期可拦 setName。
* 一律跳过 HardwareRenderer.setName。
* Pixel/A16 + Zygisk 命名空间下 native setName → dlopen("libandroid.so") 失败会 ART abort
*/
private static void hookHwuiBySurfacePolicy(XC_LoadPackage.LoadPackageParam lpparam) {
private static void hookHardwareRendererSetNameNoop() {
try {
XposedHelpers.findAndHookMethod(
"android.graphics.HardwareRenderer", null, "setName", String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (Looper.myLooper() != Looper.getMainLooper()) {
return;
}
if (!shouldBlockSetNameOnMain()) {
return;
}
param.setResult(null);
}
});
XposedBridge.log(TAG + " hooked HardwareRenderer.setName (dialog-only block)");
XposedBridge.log(TAG + " hooked HardwareRenderer.setName (always noop)");
} catch (Throwable t) {
XposedBridge.log(TAG + " HardwareRenderer.setName hook failed: " + t.getMessage());
XposedBridge.log(TAG + " HardwareRenderer.setName noop failed: " + t.getMessage());
}
}
/**
* 区号 Compose标记 surface + 保证 HW flag勿清、勿拦 enableHW
* 软件绘制会崩Software rendering doesn't support hardware bitmaps。
*/
private static void hookCallingCodeAllowHwSurface(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook markOn = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Intent intent = extractIntent(param.args);
if (isCallingCodeIntent(intent)) {
callingCodeSurfaceActive = true;
XposedBridge.log(TAG + " callingCode surface ON (HW path)");
}
}
};
try {
XposedHelpers.findAndHookMethod(
Activity.class, "startActivity", Intent.class, markOn);
XposedHelpers.findAndHookMethod(
Activity.class, "startActivity", Intent.class, Bundle.class, markOn);
} catch (Throwable t) {
XposedBridge.log(TAG + " startActivity callingCode mark failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"execStartActivity",
Context.class,
android.os.IBinder.class,
android.os.IBinder.class,
Activity.class,
Intent.class,
int.class,
Bundle.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Intent intent = (Intent) param.args[4];
if (isCallingCodeIntent(intent)) {
callingCodeSurfaceActive = true;
XposedBridge.log(TAG
+ " callingCode surface ON (execStart HW)");
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " execStart callingCode mark failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnCreate",
Activity.class,
Bundle.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
if (isCallingCodeActivity(activity.getClass().getName())) {
callingCodeSurfaceActive = true;
try {
Window window = activity.getWindow();
if (window != null) {
window.addFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
} catch (Throwable ignored) {
}
XposedBridge.log(TAG
+ " callingCode onCreate — keep HW for Compose");
}
}
});
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnDestroy",
Activity.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.args[0];
if (activity != null
&& isCallingCodeActivity(activity.getClass().getName())) {
callingCodeSurfaceActive = false;
XposedBridge.log(TAG + " callingCode surface OFF");
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " callingCode lifecycle mark failed: " + t.getMessage());
}
XposedBridge.log(TAG + " callingCode allow-HW surface armed");
}
/**
* 非区号:清 HW flag + 拦 enableHW成对防半开黑屏
* 区号 Compose / BottomSelect完整 HW软件绘制 Compose 必黑)。
* Dialog.show 窗口期可拦 setName。
*/
private static void hookHwuiBySurfacePolicy(XC_LoadPackage.LoadPackageParam lpparam) {
// setName 一律 noopA16 Zygisk 下 native setName 会 abort
hookHardwareRendererSetNameNoop();
try {
Class<?> vri = XposedHelpers.findClass("android.view.ViewRootImpl", null);
@@ -1976,7 +2123,9 @@ public final class TngRootBypassHook {
}
/**
* Promon lifecycle1.9.10 用 vhvlnqgy.u旧版用 w。只吞异常,不全拦。
* Promon lifecycle1.9.10 用 vhvlnqgy.u旧版用 w。
* 实测 afterHook 仍跑 native → DeleteLocalRef 损坏 → ART Runtime abort (signal 6)。
* 登录优先onActivity* / onApplication* 全部 noop不调原 native。
*/
private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> promonExc = findPromonClass(lpparam.classLoader, "W");
@@ -2005,25 +2154,37 @@ public final class TngRootBypassHook {
if (!name.startsWith("onActivity") && !name.startsWith("onApplication")) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!param.hasThrowable()) {
return;
if (LOGIN_FIRST_MINIMAL) {
// 不调原 native避免 JNI DeleteLocalRef → ART abort
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
setSafeHookResult(param, method);
}
Throwable t = param.getThrowable();
if (isPromonThrowable(t, promonExc)) {
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
+ " in " + lifecycleName + "#" + method.getName());
param.setThrowable(null);
});
} else {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!param.hasThrowable()) {
return;
}
Throwable t = param.getThrowable();
if (isPromonThrowable(t, promonExc)) {
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
+ " in " + lifecycleName + "#" + method.getName());
param.setThrowable(null);
}
}
}
});
});
}
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked
+ " " + lifecycleName + " lifecycle method(s) (afterHook only)");
XposedBridge.log(TAG + " hooked " + hooked + " " + lifecycleName
+ " lifecycle ("
+ (LOGIN_FIRST_MINIMAL ? "noop (login-first)" : "afterHook only")
+ ")");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " " + lifecycleName + " lifecycle hook failed: "
@@ -2061,8 +2222,8 @@ public final class TngRootBypassHook {
}
/**
* bl/R 全方法 short-circuita/b 之外的方法仍会跑 native~40s 后 stack_chk/SEGV。
* a.run 是 bl#b 后台 Runnable必须 beforeHook 直接 return
* 登录优先:只 short-circuit bl#a / bl#b文档有效路径。全拦 bl/R 六个方法会破坏
* u.onActivityCreated JNI → ART aborta.run 仍单独 noop
*/
private static void hookPromonBlSwallowExceptions(XC_LoadPackage.LoadPackageParam lpparam) {
for (String simple : new String[]{"bl", "R"}) {
@@ -2079,6 +2240,10 @@ public final class TngRootBypassHook {
try {
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
String methodName = method.getName();
if (!"a".equals(methodName) && !"b".equals(methodName)) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
@@ -2089,7 +2254,7 @@ public final class TngRootBypassHook {
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " " + clazz.getName()
+ " method(s), all short-circuit");
+ " method(s), a/b short-circuit only");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " " + clazz.getName() + " short-circuit failed: "
@@ -2423,18 +2588,56 @@ public final class TngRootBypassHook {
}
/**
* TigerTallyinit/initCommon + genericNt1 必须短路
* 只短路 genericNt1 而放行 init 会导致 encodeUid 缺失(cloudauth 403)、JNI 损坏、主线程 ABRT 无响应
* seccomp 开着时:必须 stub init/initCommon/genericNt1,否则 fork getprop 卡死
* seccomp 关且非登录优先:只 stub genericNt1 探测
*/
private static boolean isTigerShortCircuit(String className, String methodName) {
if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className)
&& ("init".equals(methodName) || "initCommon".equals(methodName))) {
return true;
|| "com.aliyun.TigerTally.t.B".equals(className)
|| "com.aliyun.TigerTally.t.C".equals(className)) {
if ("init".equals(methodName) || "initCommon".equals(methodName)
|| "genericNt1".equals(methodName) || "initialize".equals(methodName)) {
return true;
}
}
return "com.aliyun.TigerTally.t.B".equals(className)
&& "genericNt1".equals(methodName);
}
/** TNG 封装层CaptchaInitializer / TigerTallyApiWrapper 直接 noop避免 Startup latch 卡住。 */
private static void hookTigerTallyAppWrappers(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"my.com.tngdigital.captcha.TigerTallyApiWrapper",
"my.com.tngdigital.app.launcher.initializer.CaptchaInitializer",
};
for (String className : classes) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int n = 0;
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
if (!"initialize".equals(name) && !"init".equals(name)
&& !"create".equals(name)) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " TigerTally wrapper SC "
+ className + "#" + method.getName());
setSafeHookResult(param, method);
}
});
n++;
}
XposedBridge.log(TAG + " hooked TigerTally wrapper " + className + " n=" + n);
} catch (Throwable t) {
XposedBridge.log(TAG + " TigerTally wrapper " + className
+ " skipped: " + t.getMessage());
}
}
}
private static Object safeJnicReturn(int cmd, Object[] args) {
if (args != null) {
for (Object arg : args) {