chore: 备份 TNG 注册/captcha 逆向与 MariBank SG bypass 进展
TngRootBypassHook 增强 captcha 诊断、TigerTally/JNIC 分层与 HWUI 策略;新增逆向脚本、Frida 工具与 UI dump;同步 MariBank SG hook 与 tng_exit_guard 更新。
This commit is contained in:
@@ -3,6 +3,8 @@ package com.miraclegarden.smsmessage.xposed.hook;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
@@ -17,25 +19,70 @@ public final class MariBankAttestationHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankAttest";
|
||||
|
||||
private static volatile boolean installed = false;
|
||||
private static volatile boolean probesInstalled = false;
|
||||
private static volatile boolean loadClassWatcherInstalled = false;
|
||||
private static final Set<String> HOOKED_ATTESTATION_CLASSES = new HashSet<>();
|
||||
|
||||
private MariBankAttestationHook() {
|
||||
}
|
||||
|
||||
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (installed) {
|
||||
return;
|
||||
public static int installLate(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (!probesInstalled) {
|
||||
hookEnvironmentProbes(lpparam);
|
||||
probesInstalled = true;
|
||||
}
|
||||
installed = true;
|
||||
installLoadClassWatcher(lpparam);
|
||||
RootBypassHelper.hookSecurityClass(lpparam, "com.shopee.bke.biz.base.risk.a");
|
||||
|
||||
int n = 0;
|
||||
n += hookAttestationClass(lpparam, "com.shopee.shpssdkbank.wvvvuwwu");
|
||||
n += hookAttestationClass(lpparam, "com.shopee.shpssdk.wvvvuwwu");
|
||||
n += hookAttestationClass(lpparam,
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu");
|
||||
for (String className : MariBankRegionProfile.shpsAttestationCoreClasses(lpparam.packageName)) {
|
||||
n += hookAttestationClass(lpparam, className);
|
||||
}
|
||||
n += hookKnownAttestationMethods(lpparam);
|
||||
hookEnvironmentProbes(lpparam);
|
||||
XposedBridge.log(TAG + " attestation hooks=" + n);
|
||||
if (n > 0) {
|
||||
XposedBridge.log(TAG + " attestation hooks=" + n
|
||||
+ " region=" + MariBankRegionProfile.label(lpparam.packageName)
|
||||
+ " classes=" + HOOKED_ATTESTATION_CLASSES.size());
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static void installLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (loadClassWatcherInstalled) {
|
||||
return;
|
||||
}
|
||||
loadClassWatcherInstalled = true;
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
ClassLoader.class,
|
||||
"loadClass",
|
||||
String.class,
|
||||
boolean.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.getThrowable() != null) {
|
||||
return;
|
||||
}
|
||||
String name = (String) param.args[0];
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
if (name.contains("shpssdk")
|
||||
|| name.contains("CharacterCrypto")
|
||||
|| name.contains("bke.biz.base.risk")) {
|
||||
if (MariBankRegionProfile.isSingapore(lpparam.packageName)
|
||||
&& MariBankRegionProfile.isShpssdkLegacyPackage(name)) {
|
||||
return;
|
||||
}
|
||||
installLate(lpparam);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " ClassLoader attestation watcher OK");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip ClassLoader watcher: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 逆向确认的 attestation / requestDefense 桥接方法。 */
|
||||
@@ -44,7 +91,11 @@ public final class MariBankAttestationHook {
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vvuwuuvuu");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "wwvwvwuvv");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuuwv");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuwvw");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuwvvw");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuwvu");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv", "wuvwuvwwu");
|
||||
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv", "wwvuwuwvu");
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -69,6 +120,9 @@ public final class MariBankAttestationHook {
|
||||
|
||||
private static int hookAttestationClass(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
if (HOOKED_ATTESTATION_CLASSES.contains(className)) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
@@ -80,6 +134,9 @@ public final class MariBankAttestationHook {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
HOOKED_ATTESTATION_CLASSES.add(className);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip class " + className + ": " + t.getMessage());
|
||||
}
|
||||
@@ -87,9 +144,28 @@ public final class MariBankAttestationHook {
|
||||
}
|
||||
|
||||
private static boolean hookAttestationMethod(String className, Method method) {
|
||||
final String methodName = method.getName();
|
||||
final boolean isVuwuuwvw = "vuwuuwvw".equals(methodName);
|
||||
Class<?> rt = method.getReturnType();
|
||||
if (rt == String.class) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
private byte[][] vuwuInputs;
|
||||
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (!isVuwuuwvw) {
|
||||
return;
|
||||
}
|
||||
vuwuInputs = new byte[param.args.length][];
|
||||
for (int i = 0; i < param.args.length; i++) {
|
||||
if (param.args[i] instanceof byte[]) {
|
||||
byte[] b = (byte[]) param.args[i];
|
||||
vuwuInputs[i] = b;
|
||||
XposedBridge.log(TAG + " >> vuwuuwvw in" + i + " len=" + b.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
@@ -97,6 +173,9 @@ public final class MariBankAttestationHook {
|
||||
return;
|
||||
}
|
||||
String s = (String) result;
|
||||
if (isVuwuuwvw) {
|
||||
MariBankAttestationJsonUtil.logVuwuuwvwCall(vuwuInputs, s);
|
||||
}
|
||||
String sanitized = sanitizeAttestationString(s);
|
||||
if (!sanitized.equals(s)) {
|
||||
param.setResult(sanitized);
|
||||
@@ -215,12 +294,9 @@ public final class MariBankAttestationHook {
|
||||
if (pb == null || pb.command() == null) {
|
||||
return;
|
||||
}
|
||||
String joined = String.join(" ", pb.command()).toLowerCase();
|
||||
if (joined.contains(" su") || joined.startsWith("su")
|
||||
|| joined.contains("magisk") || joined.contains("which su")
|
||||
|| joined.contains("getprop ro.debuggable")) {
|
||||
XposedBridge.log(TAG + " blocked ProcessBuilder: " + joined);
|
||||
throw new SecurityException("blocked root probe");
|
||||
if (ProbeGuard.isBlockedCommand(pb.command())) {
|
||||
param.setResult(ProbeGuard.fakeFailedProcess(
|
||||
String.join(" ", pb.command())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
/**
|
||||
* 解析 {@code vuwuuwvw} 返回的 obfuscated-key JSON(key 多为 8 位 hex)。
|
||||
*/
|
||||
final class MariBankAttestationJsonUtil {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankAttestJson";
|
||||
private static final Pattern HEX_KEY = Pattern.compile("^[0-9a-fA-F]{8}$");
|
||||
private static final Pattern SUSPICIOUS = Pattern.compile(
|
||||
"(root|hook|xposed|lsposed|magisk|frida|substrate|emulator|debug|adb|"
|
||||
+ "selinux|su\\b|/proc/|zygisk|riru|shamiko|tamper|integrity|"
|
||||
+ "jailbreak|virtual|mock|proxy|vpn|developer)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private MariBankAttestationJsonUtil() {
|
||||
}
|
||||
|
||||
static void logVuwuuwvwCall(byte[][] inputs, String output) {
|
||||
if (inputs != null) {
|
||||
for (int i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i] != null && inputs[i].length > 0) {
|
||||
MariBankCaptureUtil.logBytes("vuwuuwvw.in" + i, inputs[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (output == null || output.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean registerLike = isRegisterAttestationCall(inputs, output);
|
||||
String label = registerLike ? "vuwuuwvw.out REGISTER" : "vuwuuwvw.out";
|
||||
MariBankCaptureUtil.logText(label, output);
|
||||
parseAndSummarize(output, registerLike);
|
||||
}
|
||||
|
||||
static boolean isRegisterAttestationCall(byte[][] inputs, String output) {
|
||||
if (inputs != null) {
|
||||
for (byte[] in : inputs) {
|
||||
if (in != null && in.length >= 200 && in.length <= 400) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return output != null && output.length() >= 2200 && output.length() <= 2500;
|
||||
}
|
||||
|
||||
static void parseAndSummarize(String json, boolean highlight) {
|
||||
try {
|
||||
JSONObject obj = new JSONObject(json);
|
||||
List<String> keys = new ArrayList<>();
|
||||
Iterator<String> it = obj.keys();
|
||||
while (it.hasNext()) {
|
||||
keys.add(it.next());
|
||||
}
|
||||
Collections.sort(keys);
|
||||
XposedBridge.log(TAG + " keys=" + keys.size()
|
||||
+ (highlight ? " [REGISTER-LIKE]" : "")
|
||||
+ " sample=" + keys.subList(0, Math.min(6, keys.size())));
|
||||
|
||||
List<String> hits = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
Object val = obj.get(key);
|
||||
scanValue(key, val, hits);
|
||||
}
|
||||
if (!hits.isEmpty()) {
|
||||
XposedBridge.log(TAG + " SUSPICIOUS count=" + hits.size());
|
||||
for (int i = 0; i < Math.min(hits.size(), 24); i++) {
|
||||
XposedBridge.log(TAG + " " + hits.get(i));
|
||||
}
|
||||
} else {
|
||||
XposedBridge.log(TAG + " no plain suspicious strings in values");
|
||||
}
|
||||
logKeyGuesses(keys);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " parse fail: " + t.getMessage()
|
||||
+ " head=" + json.substring(0, Math.min(120, json.length())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanValue(String key, Object val, List<String> hits) {
|
||||
if (val instanceof String) {
|
||||
String s = (String) val;
|
||||
noteIfSuspicious(key, "str", s, hits);
|
||||
if (s.length() >= 8 && s.length() <= 512 && looksBase64(s)) {
|
||||
byte[] decoded = tryBase64(s);
|
||||
if (decoded != null) {
|
||||
String inner = new String(decoded, StandardCharsets.UTF_8);
|
||||
if (isMostlyPrintable(inner)) {
|
||||
noteIfSuspicious(key, "b64utf8", inner, hits);
|
||||
} else {
|
||||
noteIfSuspicious(key, "b64hex", hexPreview(decoded, 32), hits);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (val instanceof Number || val instanceof Boolean) {
|
||||
String s = String.valueOf(val);
|
||||
if ("1".equals(s) || "true".equalsIgnoreCase(s)) {
|
||||
hits.add(key + " =" + s + " (flag?)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (val instanceof JSONObject) {
|
||||
JSONObject nested = (JSONObject) val;
|
||||
Iterator<String> it = nested.keys();
|
||||
while (it.hasNext()) {
|
||||
String nk = it.next();
|
||||
try {
|
||||
scanValue(key + "." + nk, nested.get(nk), hits);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (val instanceof JSONArray) {
|
||||
JSONArray arr = (JSONArray) val;
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
try {
|
||||
scanValue(key + "[" + i + "]", arr.get(i), hits);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void noteIfSuspicious(String key, String kind, String text, List<String> hits) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Matcher m = SUSPICIOUS.matcher(text);
|
||||
if (m.find()) {
|
||||
String show = text.length() > 120 ? text.substring(0, 120) + "..." : text;
|
||||
hits.add(key + " " + kind + " hit=" + m.group().toLowerCase(Locale.ROOT) + " val=" + show);
|
||||
}
|
||||
}
|
||||
|
||||
private static void logKeyGuesses(List<String> keys) {
|
||||
Set<String> candidates = knownFieldNames();
|
||||
List<String> matched = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
if (!HEX_KEY.matcher(key).matches()) {
|
||||
continue;
|
||||
}
|
||||
String lower = key.toLowerCase(Locale.ROOT);
|
||||
for (String name : candidates) {
|
||||
if (hashKey(name).equals(lower)) {
|
||||
matched.add(key + "=>" + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matched.isEmpty()) {
|
||||
XposedBridge.log(TAG + " key guesses: " + matched);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> knownFieldNames() {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
String[] base = {
|
||||
"root", "hook", "xposed", "lsposed", "magisk", "frida", "adb", "debug",
|
||||
"debuggable", "emulator", "simulator", "vpn", "proxy", "mock",
|
||||
"selinux", "su", "supersu", "zygisk", "riru", "shamiko", "substrate",
|
||||
"integrity", "safetynet", "playIntegrity", "deviceId", "androidId",
|
||||
"serial", "fingerprint", "model", "brand", "manufacturer", "board",
|
||||
"host", "tags", "type", "user", "display", "product", "hardware",
|
||||
"usb", "wifi", "adb_enabled", "development_settings_enabled",
|
||||
"RISK_ROOT", "RISK_HOOK", "RISK_USB_ADB", "RISK_WIFI_ADB", "RISK_ADB",
|
||||
"RISK_EMULATOR", "RISK_DEBUG", "RISK_VPN", "RISK_PROXY", "RISK_MOCK",
|
||||
"rdVerifyInfo", "deviceFingerprint", "data", "dataKey", "riskToken",
|
||||
"isRoot", "isHook", "isDebug", "isAdb", "isEmulator", "isVirtual",
|
||||
"tamper", "jailbreak", "bootloader", "verifiedbootstate", "vbmeta",
|
||||
"init.svc.adbd", "/proc/self/maps", "RealInterceptorChain",
|
||||
};
|
||||
Collections.addAll(names, base);
|
||||
return names;
|
||||
}
|
||||
|
||||
private static String hashKey(String name) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] dig = md.digest(name.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sb.append(String.format(Locale.ROOT, "%02x", dig[i] & 0xff));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean looksBase64(String s) {
|
||||
return s.matches("^[A-Za-z0-9+/=]+$");
|
||||
}
|
||||
|
||||
private static byte[] tryBase64(String s) {
|
||||
try {
|
||||
return Base64.decode(s, Base64.DEFAULT);
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMostlyPrintable(String s) {
|
||||
if (s == null || s.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int ok = 0;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c >= 0x20 && c < 0x7f) {
|
||||
ok++;
|
||||
}
|
||||
}
|
||||
return ok * 100 / s.length() >= 85;
|
||||
}
|
||||
|
||||
private static String hexPreview(byte[] b, int max) {
|
||||
if (b == null) {
|
||||
return "";
|
||||
}
|
||||
int n = Math.min(b.length, max);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < n; i++) {
|
||||
sb.append(String.format(Locale.ROOT, "%02x", b[i] & 0xff));
|
||||
}
|
||||
if (b.length > n) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
/**
|
||||
* 分段 logcat 输出,避免单条超过 ~4KB 被截断。
|
||||
*/
|
||||
final class MariBankCaptureUtil {
|
||||
|
||||
static final String TAG = "notiMessageHook/MariBankCapture";
|
||||
private static final int CHUNK = 3500;
|
||||
private static final int HEX_PREVIEW = 96;
|
||||
|
||||
private MariBankCaptureUtil() {
|
||||
}
|
||||
|
||||
static void logText(String section, String text) {
|
||||
if (text == null) {
|
||||
XposedBridge.log(TAG + " [" + section + "] null");
|
||||
return;
|
||||
}
|
||||
if (text.length() <= CHUNK) {
|
||||
XposedBridge.log(TAG + " [" + section + "] len=" + text.length() + " " + text);
|
||||
return;
|
||||
}
|
||||
int parts = (text.length() + CHUNK - 1) / CHUNK;
|
||||
XposedBridge.log(TAG + " [" + section + "] len=" + text.length() + " parts=" + parts);
|
||||
for (int i = 0; i < parts; i++) {
|
||||
int start = i * CHUNK;
|
||||
int end = Math.min(start + CHUNK, text.length());
|
||||
XposedBridge.log(TAG + " [" + section + "] " + (i + 1) + "/" + parts + " "
|
||||
+ text.substring(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
static void logBytes(String section, byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
XposedBridge.log(TAG + " [" + section + "] null");
|
||||
return;
|
||||
}
|
||||
String utf8 = tryUtf8(bytes);
|
||||
if (isMostlyPrintable(utf8)) {
|
||||
logText(section + " utf8", utf8);
|
||||
} else {
|
||||
XposedBridge.log(TAG + " [" + section + "] byte[" + bytes.length + "] hex="
|
||||
+ hexPreview(bytes) + " b64=" + base64(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
static void logArg(String section, Object arg) {
|
||||
if (arg == null) {
|
||||
XposedBridge.log(TAG + " [" + section + "] null");
|
||||
return;
|
||||
}
|
||||
if (arg instanceof String) {
|
||||
logText(section, (String) arg);
|
||||
return;
|
||||
}
|
||||
if (arg instanceof byte[]) {
|
||||
logBytes(section, (byte[]) arg);
|
||||
return;
|
||||
}
|
||||
if (arg instanceof String[]) {
|
||||
String[] arr = (String[]) arg;
|
||||
XposedBridge.log(TAG + " [" + section + "] String[" + arr.length + "]");
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
logText(section + "[" + i + "]", arr[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (arg instanceof byte[][]) {
|
||||
byte[][] arr = (byte[][]) arg;
|
||||
XposedBridge.log(TAG + " [" + section + "] byte[][] len=" + arr.length);
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
logBytes(section + "[" + i + "]", arr[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " [" + section + "] " + arg.getClass().getName()
|
||||
+ " = " + String.valueOf(arg));
|
||||
}
|
||||
|
||||
static boolean isBankUrl(String url) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = url.toLowerCase();
|
||||
return lower.contains("maribank.com")
|
||||
|| lower.contains("seabank.ph")
|
||||
|| lower.contains("/uapi/");
|
||||
}
|
||||
|
||||
private static String tryUtf8(byte[] bytes) {
|
||||
try {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isMostlyPrintable(String s) {
|
||||
if (s == null || s.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int printable = 0;
|
||||
int sample = Math.min(s.length(), 512);
|
||||
for (int i = 0; i < sample; i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\n' || c == '\r' || c == '\t' || (c >= 32 && c < 127)) {
|
||||
printable++;
|
||||
}
|
||||
}
|
||||
return printable * 100 / sample >= 85;
|
||||
}
|
||||
|
||||
private static String hexPreview(byte[] bytes) {
|
||||
int n = Math.min(bytes.length, HEX_PREVIEW);
|
||||
StringBuilder sb = new StringBuilder(n * 2);
|
||||
for (int i = 0; i < n; i++) {
|
||||
sb.append(String.format("%02x", bytes[i] & 0xff));
|
||||
}
|
||||
if (bytes.length > n) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String base64(byte[] bytes) {
|
||||
try {
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||
} catch (Throwable t) {
|
||||
return "<b64 err>";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
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;
|
||||
|
||||
/**
|
||||
* DFP(device fingerprint)初始化诊断:register 前若 dfp 为空会弹
|
||||
* "The system is currently unavailable",且不会发 /uapi/v2/register。
|
||||
*/
|
||||
public final class MariBankDfpHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankDfp";
|
||||
private static final Set<String> HOOKED_CLASSES = new HashSet<>();
|
||||
|
||||
private static volatile boolean loadClassWatcherInstalled = false;
|
||||
private static volatile boolean registerVmHooked = false;
|
||||
|
||||
private MariBankDfpHook() {
|
||||
}
|
||||
|
||||
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
hookRegisterViewModel(lpparam);
|
||||
hookKnownDfpClasses(lpparam);
|
||||
installLoadClassWatcher(lpparam);
|
||||
}
|
||||
|
||||
private static void hookRegisterViewModel(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (registerVmHooked) {
|
||||
return;
|
||||
}
|
||||
String className = "com.shopee.bke.biz.user.viewmodel.RegisterViewModel";
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
registerVmHooked = true;
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String args = param.args == null ? "[]" : Arrays.toString(param.args);
|
||||
if (args.length() > 800) {
|
||||
args = args.substring(0, 800) + "...";
|
||||
}
|
||||
XposedBridge.log(TAG + " RegisterViewModel."
|
||||
+ method.getName() + " args=" + args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
String rs = String.valueOf(result);
|
||||
if (rs.length() > 600) {
|
||||
rs = rs.substring(0, 600) + "...";
|
||||
}
|
||||
if (rs.isEmpty()
|
||||
|| rs.toLowerCase().contains("dfp")
|
||||
|| rs.toLowerCase().contains("error")
|
||||
|| rs.toLowerCase().contains("unavailable")
|
||||
|| rs.contains("3100012")
|
||||
|| rs.contains("4067")) {
|
||||
XposedBridge.log(TAG + " RegisterViewModel."
|
||||
+ method.getName() + " result=" + rs);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
XposedBridge.log(TAG + " RegisterViewModel hooked methods="
|
||||
+ clazz.getDeclaredMethods().length);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip RegisterViewModel: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动时类可能已加载,先尝试已知命名。 */
|
||||
private static void hookKnownDfpClasses(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
String[] candidates = {
|
||||
"com.shopee.bke.lib.jni.security.CharacterCryptoManager",
|
||||
"com.shopee.bke.lib.jni.security.CharacterCryptoManagerWrapper",
|
||||
"com.shopee.bke.lib.jni.utils.NativeEncryptUtilsWrapper",
|
||||
};
|
||||
for (String cn : candidates) {
|
||||
tryHookDfpClass(lpparam, cn);
|
||||
}
|
||||
}
|
||||
|
||||
private static void installLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (loadClassWatcherInstalled) {
|
||||
return;
|
||||
}
|
||||
loadClassWatcherInstalled = true;
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
ClassLoader.class,
|
||||
"loadClass",
|
||||
String.class,
|
||||
boolean.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.getThrowable() != null) {
|
||||
return;
|
||||
}
|
||||
String name = (String) param.args[0];
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
if (name.contains("CharacterCrypto")
|
||||
|| name.contains("NativeEncryptUtils")
|
||||
|| (name.contains("dfp") && name.contains("bke"))) {
|
||||
tryHookDfpClass(lpparam, name);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " ClassLoader dfp watcher OK");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip ClassLoader watcher: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryHookDfpClass(XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
if (HOOKED_CLASSES.contains(className)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
int count = 0;
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
String mn = method.getName();
|
||||
if (!mn.startsWith("getDfp") && !mn.contains("Fingerprint")) {
|
||||
continue;
|
||||
}
|
||||
if (method.getReturnType() != String.class) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
String s = result instanceof String ? (String) result : null;
|
||||
if (s == null || s.isEmpty()) {
|
||||
XposedBridge.log(TAG + " EMPTY " + className + "."
|
||||
+ method.getName() + " → register may fail");
|
||||
} else {
|
||||
String tail = s.length() > 80
|
||||
? s.substring(0, 40) + "..." + s.substring(s.length() - 20)
|
||||
: s;
|
||||
XposedBridge.log(TAG + " " + className + "."
|
||||
+ method.getName() + " len=" + s.length() + " val=" + tail);
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
HOOKED_CLASSES.add(className);
|
||||
XposedBridge.log(TAG + " hooked " + count + " dfp getters in " + className);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 拦截 dfp 空检查日志源(含 register scene 文案的类方法)。 */
|
||||
public static void hookDfpEmptyGuards(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.util.Log",
|
||||
lpparam.classLoader,
|
||||
"e",
|
||||
String.class,
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String msg = param.args[1] instanceof String ? (String) param.args[1] : null;
|
||||
if (msg == null) {
|
||||
return;
|
||||
}
|
||||
String lower = msg.toLowerCase();
|
||||
if (lower.contains("dfp is empty")
|
||||
|| lower.contains("getdfp empty")
|
||||
|| lower.contains("getdfp onerror")) {
|
||||
XposedBridge.log(TAG + " Log.e: " + msg);
|
||||
logStack();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip Log.e dfp guard: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void logStack() {
|
||||
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
|
||||
String cn = frame.getClassName();
|
||||
if (cn.startsWith("com.shopee.bke") || cn.contains("CharacterCrypto")) {
|
||||
XposedBridge.log(TAG + " at " + cn + "." + frame.getMethodName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* MariBank / SeaBank HTTP 全量抓包(仅 OkHttp 头 + 响应体)。
|
||||
* 加密明文 / attestation 由 {@link MariBankSdkUtilsHook} 负责,勿在此重复 Hook Gson/native。
|
||||
*/
|
||||
public final class MariBankFullCaptureHook {
|
||||
|
||||
private static final String TAG = MariBankCaptureUtil.TAG;
|
||||
private static final AtomicInteger SEQ = new AtomicInteger();
|
||||
|
||||
private static volatile boolean installed = false;
|
||||
|
||||
private MariBankFullCaptureHook() {
|
||||
}
|
||||
|
||||
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (installed) {
|
||||
return;
|
||||
}
|
||||
installed = true;
|
||||
|
||||
int n = hookOkHttpRealCall(lpparam);
|
||||
XposedBridge.log(TAG + " http capture hooks=" + n);
|
||||
}
|
||||
|
||||
private static int hookOkHttpRealCall(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook hook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object request = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
captureRequest(request);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " req capture err: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (!"execute".equals(param.method.getName())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object response = param.getResult();
|
||||
if (response != null) {
|
||||
captureResponse(response);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " resp capture err: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
int count = 0;
|
||||
for (String className : new String[]{
|
||||
"okhttp3.RealCall", "okhttp3.internal.connection.RealCall"
|
||||
}) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "execute", hook);
|
||||
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "enqueue",
|
||||
"okhttp3.Callback", hook);
|
||||
count += 2;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void captureRequest(Object request) {
|
||||
Object urlObj = XposedHelpers.callMethod(request, "url");
|
||||
String url = urlObj != null ? String.valueOf(urlObj) : "?";
|
||||
if (!MariBankCaptureUtil.isBankUrl(url)) {
|
||||
return;
|
||||
}
|
||||
int seq = SEQ.incrementAndGet();
|
||||
String method = (String) XposedHelpers.callMethod(request, "method");
|
||||
XposedBridge.log(TAG + " >>> #" + seq + " " + method + " " + url);
|
||||
logHeaders("reqHdr #" + seq, XposedHelpers.callMethod(request, "headers"));
|
||||
}
|
||||
|
||||
private static void captureResponse(Object response) {
|
||||
try {
|
||||
Object request = XposedHelpers.callMethod(response, "request");
|
||||
Object urlObj = request != null ? XposedHelpers.callMethod(request, "url") : null;
|
||||
String url = urlObj != null ? String.valueOf(urlObj) : "?";
|
||||
if (!MariBankCaptureUtil.isBankUrl(url)) {
|
||||
return;
|
||||
}
|
||||
int code = (int) XposedHelpers.callMethod(response, "code");
|
||||
XposedBridge.log(TAG + " <<< HTTP " + code + " " + url);
|
||||
logHeaders("respHdr", XposedHelpers.callMethod(response, "headers"));
|
||||
try {
|
||||
Object peek = XposedHelpers.callMethod(response, "peekBody", 256L * 1024L);
|
||||
if (peek != null) {
|
||||
Object ctObj = XposedHelpers.callMethod(peek, "contentType");
|
||||
String contentType = ctObj != null ? String.valueOf(ctObj).toLowerCase() : "";
|
||||
if (contentType.contains("pdf") || contentType.contains("octet-stream")) {
|
||||
long size = 0L;
|
||||
try {
|
||||
size = (long) XposedHelpers.callMethod(peek, "contentLength");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
XposedBridge.log(TAG + " <<< respBody skipped binary ct="
|
||||
+ contentType + " len=" + size + " " + url);
|
||||
} else {
|
||||
String body = (String) XposedHelpers.callMethod(peek, "string");
|
||||
if (body != null && !body.isEmpty()) {
|
||||
if (body.length() > 512
|
||||
&& !MariBankCaptureUtil.isMostlyPrintable(body)) {
|
||||
XposedBridge.log(TAG + " <<< respBody skipped non-text len="
|
||||
+ body.length() + " " + url);
|
||||
} else {
|
||||
MariBankCaptureUtil.logText("respBody", body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " peekBody err: " + t.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " captureResponse err: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void logHeaders(String section, Object headers) {
|
||||
if (headers == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int size = (int) XposedHelpers.callMethod(headers, "size");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < size; i++) {
|
||||
String name = (String) XposedHelpers.callMethod(headers, "name", i);
|
||||
String value = (String) XposedHelpers.callMethod(headers, "value", i);
|
||||
sb.append(name).append(": ").append(value).append("\n");
|
||||
}
|
||||
MariBankCaptureUtil.logText(section, sb.toString().trim());
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " [" + section + "] headers err: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
/**
|
||||
* PH vs SG MariBank 差异配置(基于 reverse/apks 扫描,非「换包名即相同」)。
|
||||
* <p>
|
||||
* SG 3.2.2:dex 内无 {@code com.shopee.shpssdk.*}、无 {@code safemode.b/catchs.a/util.c}、
|
||||
* 无 {@code jni.utils.d};风控仅 {@code shpssdkbank}。
|
||||
*/
|
||||
public final class MariBankRegionProfile {
|
||||
|
||||
private MariBankRegionProfile() {
|
||||
}
|
||||
|
||||
public static boolean isSingapore(String packageName) {
|
||||
return MariBankRootBypassHook.PACKAGE_SG.equals(packageName);
|
||||
}
|
||||
|
||||
public static boolean isPhilippines(String packageName) {
|
||||
return MariBankRootBypassHook.PACKAGE.equals(packageName);
|
||||
}
|
||||
|
||||
public static String label(String packageName) {
|
||||
if (isSingapore(packageName)) {
|
||||
return "SG";
|
||||
}
|
||||
if (isPhilippines(packageName)) {
|
||||
return "PH";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
/** PH SafeMode boolean 类;SG base/split dex 均无,勿 findClass。 */
|
||||
public static String[] safeModeBooleanClasses(String packageName) {
|
||||
if (isPhilippines(packageName)) {
|
||||
return new String[]{
|
||||
"com.shopee.bke.lib.safemode.b",
|
||||
"com.shopee.bke.lib.safemode.catchs.a",
|
||||
"com.shopee.bke.lib.safemode.util.c",
|
||||
};
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
/** 两区共有或 SG 专用 risk 类(classes3.dex)。 */
|
||||
public static String[] sharedRiskClasses() {
|
||||
return new String[]{"com.shopee.bke.biz.base.risk.a"};
|
||||
}
|
||||
|
||||
/** SHPSSDK Java 入口:SG 仅 bank 包。 */
|
||||
public static String[] shpsSdkClasses(String packageName) {
|
||||
if (isSingapore(packageName)) {
|
||||
return new String[]{"com.shopee.shpssdkbank.SHPSSDK"};
|
||||
}
|
||||
return new String[]{
|
||||
"com.shopee.shpssdk.SHPSSDK",
|
||||
"com.shopee.shpssdkbank.SHPSSDK",
|
||||
};
|
||||
}
|
||||
|
||||
public static String[] shpsAssessRiskClasses(String packageName) {
|
||||
if (isSingapore(packageName)) {
|
||||
return new String[]{"com.shopee.shpssdkbank.SPSAssessRisk"};
|
||||
}
|
||||
return new String[]{
|
||||
"com.shopee.shpssdkbank.SPSAssessRisk",
|
||||
"com.shopee.shpssdk.SPSAssessRisk",
|
||||
};
|
||||
}
|
||||
|
||||
public static String[] shpsCallbackAdapterClasses(String packageName) {
|
||||
if (isSingapore(packageName)) {
|
||||
return new String[]{"com.shopee.shpssdkbank.SPSCallbackAdapter"};
|
||||
}
|
||||
return new String[]{
|
||||
"com.shopee.shpssdk.SPSCallbackAdapter",
|
||||
"com.shopee.shpssdkbank.SPSCallbackAdapter",
|
||||
};
|
||||
}
|
||||
|
||||
/** attestation / native-core:SG 无 com.shopee.shpssdk.wvvvuwwu。 */
|
||||
public static String[] shpsAttestationCoreClasses(String packageName) {
|
||||
if (isSingapore(packageName)) {
|
||||
return new String[]{
|
||||
"com.shopee.shpssdkbank.wvvvuwwu",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
|
||||
"com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw",
|
||||
};
|
||||
}
|
||||
return new String[]{
|
||||
"com.shopee.shpssdkbank.wvvvuwwu",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
|
||||
"com.shopee.shpssdk.wvvvuwwu",
|
||||
};
|
||||
}
|
||||
|
||||
/** 加密 JNI:PH 有 utils.d 包装;SG 仅 uvwuvwuv + uvwwwwuv(classes6.dex)。 */
|
||||
public static boolean hasEncryptWrapperD(String packageName) {
|
||||
return isPhilippines(packageName);
|
||||
}
|
||||
|
||||
public static boolean hasEncryptHelperUvwwwwuv(String packageName) {
|
||||
return isSingapore(packageName);
|
||||
}
|
||||
|
||||
/** late hooks 完成条件:SG 不依赖 PH safemode booleanHooks。 */
|
||||
public static boolean isLateHooksReady(
|
||||
String packageName, int booleanHooks, int encryptHooks, int attestHooks) {
|
||||
if (isSingapore(packageName)) {
|
||||
return encryptHooks > 0 || attestHooks > 0;
|
||||
}
|
||||
return booleanHooks > 0 || encryptHooks > 0 || attestHooks > 0;
|
||||
}
|
||||
|
||||
public static boolean isShpssdkLegacyPackage(String className) {
|
||||
return className != null
|
||||
&& className.startsWith("com.shopee.shpssdk.")
|
||||
&& !className.startsWith("com.shopee.shpssdkbank.");
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,45 @@ final class MariBankRegisterPayloadUtil {
|
||||
out = sb.toString();
|
||||
XposedBridge.log(TAG + " deviceFingerprint sanitized in register JSON");
|
||||
}
|
||||
logRegisterSummary(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void logRegisterSummary(String json) {
|
||||
if (json == null) {
|
||||
return;
|
||||
}
|
||||
String cy = extractJsonString(json, "cyCode");
|
||||
String scene = extractJsonString(json, "scene");
|
||||
String step = extractJsonString(json, "step");
|
||||
Matcher fp = DEVICE_FINGERPRINT.matcher(json);
|
||||
String fpTail = fp.find() ? MariBankRiskTokenUtil.tail(fp.group(1)) : "?";
|
||||
int dataLen = lengthOfJsonString(json, "\"data\"");
|
||||
int dataKeyLen = lengthOfJsonString(json, "\"dataKey\"");
|
||||
XposedBridge.log(TAG + " register summary cy=" + cy + " scene=" + scene
|
||||
+ " step=" + step + " fpTail=" + fpTail
|
||||
+ " dataLen=" + dataLen + " dataKeyLen=" + dataKeyLen);
|
||||
}
|
||||
|
||||
private static String extractJsonString(String json, String key) {
|
||||
Pattern p = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*\"([^\"]*)\"");
|
||||
Matcher m = p.matcher(json);
|
||||
return m.find() ? m.group(1) : "?";
|
||||
}
|
||||
|
||||
private static int lengthOfJsonString(String json, String key) {
|
||||
int idx = json.indexOf(key);
|
||||
if (idx < 0) {
|
||||
return -1;
|
||||
}
|
||||
int start = json.indexOf('"', idx + key.length());
|
||||
if (start < 0) {
|
||||
return -1;
|
||||
}
|
||||
int end = json.indexOf('"', start + 1);
|
||||
if (end < 0) {
|
||||
return -1;
|
||||
}
|
||||
return end - start - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -15,7 +17,9 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
@@ -63,13 +67,6 @@ public final class MariBankRootBypassHook {
|
||||
private static final long FINISH_BURST_WINDOW_MS = 800L;
|
||||
private static final int FINISH_BURST_THRESHOLD = 2;
|
||||
|
||||
private static final String[] SAFE_MODE_CLASSES = {
|
||||
"com.shopee.bke.lib.safemode.b",
|
||||
"com.shopee.bke.lib.safemode.catchs.a",
|
||||
"com.shopee.bke.lib.safemode.util.c",
|
||||
"com.shopee.bke.biz.base.risk.a",
|
||||
};
|
||||
|
||||
private static final String[] ERROR_FLOW_CLASSES = {
|
||||
"com.shopee.bke.biz.user.errorcodehandler.a",
|
||||
"com.shopee.bke.biz.user.errorcodehandler.b",
|
||||
@@ -77,18 +74,33 @@ public final class MariBankRootBypassHook {
|
||||
"com.shopee.bke.biz.user.viewmodel.RegisterViewModel",
|
||||
};
|
||||
|
||||
private static String[] booleanHookTargets(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
String[] ph = MariBankRegionProfile.safeModeBooleanClasses(lpparam.packageName);
|
||||
String[] shared = MariBankRegionProfile.sharedRiskClasses();
|
||||
String[] out = new String[ph.length + shared.length];
|
||||
System.arraycopy(ph, 0, out, 0, ph.length);
|
||||
System.arraycopy(shared, 0, out, ph.length, shared.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
private MariBankRootBypassHook() {
|
||||
}
|
||||
|
||||
private static volatile boolean deferredHooksInstalled = false;
|
||||
private static volatile boolean lateAppHooksInstalled = false;
|
||||
private static volatile boolean lateHooksComplete = false;
|
||||
private static volatile boolean lateHooksCoreInstalled = false;
|
||||
private static volatile boolean lateHandlerRetriesDone = false;
|
||||
private static volatile int lateHookAttempts = 0;
|
||||
private static final int MAX_LATE_HOOK_ATTEMPTS = 10;
|
||||
private static final Set<String> HOOKED_SAFE_MODE_CLASSES = new HashSet<>();
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (!isTargetPackage(lpparam.packageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
XposedBridge.log(TAG + " install for " + lpparam.packageName);
|
||||
XposedBridge.log(TAG + " install for " + lpparam.packageName
|
||||
+ " region=" + MariBankRegionProfile.label(lpparam.packageName));
|
||||
hookAntiSuicide(lpparam);
|
||||
hookAntiSoftCrash(lpparam);
|
||||
scheduleAppHooks(lpparam);
|
||||
@@ -110,7 +122,7 @@ public final class MariBankRootBypassHook {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
MariBankShpsNativeHook.installLateNativeHooks(lpparam);
|
||||
installLateAppHooks(lpparam);
|
||||
scheduleLateAppHooks(lpparam);
|
||||
}
|
||||
};
|
||||
try {
|
||||
@@ -125,34 +137,159 @@ public final class MariBankRootBypassHook {
|
||||
lpparam.classLoader,
|
||||
"onCreate",
|
||||
afterOnCreate);
|
||||
hookMainActivityForLateHooks(lpparam);
|
||||
XposedBridge.log(TAG + " waiting attachBaseContext + onCreate for app hooks");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " BkeApplication hook failed, install now: " + t.getMessage());
|
||||
installDeferredHooks(lpparam);
|
||||
MariBankShpsNativeHook.installDeferred(lpparam);
|
||||
MariBankShpsNativeHook.installLateNativeHooks(lpparam);
|
||||
installLateAppHooks(lpparam);
|
||||
scheduleLateAppHooks(lpparam);
|
||||
}
|
||||
}
|
||||
|
||||
/** onCreate 之后补装:此时 classes11 / SHPSSDK 与 RN SO 均已就绪。 */
|
||||
private static void installLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (lateAppHooksInstalled) {
|
||||
/** SG split-dex 在 onCreate 时类可能未加载,MainActivity 再试。 */
|
||||
private static void hookMainActivityForLateHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.shopee.bke.digitalbank.ui.MainActivity",
|
||||
lpparam.classLoader,
|
||||
"onResume",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
scheduleLateAppHooks(lpparam);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip MainActivity.onResume retry: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void scheduleLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
tryInstallLateAppHooks(lpparam, false);
|
||||
}
|
||||
|
||||
/** onCreate 之后补装;SG 3.2.2 split dex 常需延迟重试。 */
|
||||
private static void tryInstallLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
tryInstallLateAppHooks(lpparam, false);
|
||||
}
|
||||
|
||||
private static void tryInstallLateAppHooks(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, boolean fromSplitDex) {
|
||||
if (lateHooksComplete) {
|
||||
return;
|
||||
}
|
||||
lateAppHooksInstalled = true;
|
||||
|
||||
int hooked = 0;
|
||||
for (String className : SAFE_MODE_CLASSES) {
|
||||
hooked += hookAllBooleanChecks(lpparam, className);
|
||||
if (!lateHooksCoreInstalled) {
|
||||
lateHooksCoreInstalled = true;
|
||||
hookShpsRisk(lpparam);
|
||||
hookShpsToken(lpparam);
|
||||
hookErrorFlowLogging(lpparam);
|
||||
MariBankFullCaptureHook.installLate(lpparam);
|
||||
}
|
||||
hooked += hookShpsRisk(lpparam);
|
||||
hookShpsToken(lpparam);
|
||||
hookErrorFlowLogging(lpparam);
|
||||
MariBankSdkUtilsHook.installLate(lpparam);
|
||||
MariBankAttestationHook.installLate(lpparam);
|
||||
|
||||
XposedBridge.log(TAG + " late app hooks installed, booleanHooks=" + hooked);
|
||||
int booleanHooks = 0;
|
||||
for (String className : booleanHookTargets(lpparam)) {
|
||||
booleanHooks += hookAllBooleanChecks(lpparam, className);
|
||||
}
|
||||
int encryptHooks = MariBankSdkUtilsHook.installLate(lpparam);
|
||||
int attestHooks = MariBankAttestationHook.installLate(lpparam);
|
||||
MariBankDfpHook.installLate(lpparam);
|
||||
MariBankDfpHook.hookDfpEmptyGuards(lpparam);
|
||||
|
||||
if (MariBankRegionProfile.isLateHooksReady(
|
||||
lpparam.packageName, booleanHooks, encryptHooks, attestHooks)) {
|
||||
lateHooksComplete = true;
|
||||
XposedBridge.log(TAG + " late app hooks ready region="
|
||||
+ MariBankRegionProfile.label(lpparam.packageName)
|
||||
+ " booleanHooks=" + booleanHooks
|
||||
+ " encryptHooks=" + encryptHooks + " attestHooks=" + attestHooks
|
||||
+ " attempts=" + lateHookAttempts
|
||||
+ (fromSplitDex ? " viaSplitDex" : ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromSplitDex) {
|
||||
XposedBridge.log(TAG + " split-dex class loaded, safemode still pending encryptHooks="
|
||||
+ encryptHooks + " attestHooks=" + attestHooks);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lateHandlerRetriesDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
lateHookAttempts++;
|
||||
if (lateHookAttempts >= MAX_LATE_HOOK_ATTEMPTS) {
|
||||
lateHandlerRetriesDone = true;
|
||||
XposedBridge.log(TAG + " late handler retries done (split-dex may load later) attempts="
|
||||
+ lateHookAttempts + " encryptHooks=" + encryptHooks
|
||||
+ " attestHooks=" + attestHooks);
|
||||
return;
|
||||
}
|
||||
|
||||
long delay = Math.min(400L * lateHookAttempts, 2500L);
|
||||
XposedBridge.log(TAG + " late hooks retry #" + lateHookAttempts + " in " + delay + "ms");
|
||||
new Handler(Looper.getMainLooper()).postDelayed(
|
||||
() -> tryInstallLateAppHooks(lpparam, false), delay);
|
||||
}
|
||||
|
||||
private static boolean isLateHookTargetClass(String name, String packageName) {
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
for (String cn : MariBankRegionProfile.safeModeBooleanClasses(packageName)) {
|
||||
if (cn.equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (String cn : MariBankRegionProfile.sharedRiskClasses()) {
|
||||
if (cn.equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (String prefix : ERROR_FLOW_CLASSES) {
|
||||
if (name.equals(prefix) || name.startsWith(prefix.substring(0, prefix.lastIndexOf('.') + 1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return name.startsWith("com.shopee.bke.lib.safemode.")
|
||||
|| name.equals("com.shopee.bke.lib.jni.utils.uvwuvwuv")
|
||||
|| name.equals("com.shopee.bke.lib.jni.utils.uvwwwwuv")
|
||||
|| (MariBankRegionProfile.hasEncryptWrapperD(packageName)
|
||||
&& name.equals("com.shopee.bke.lib.jni.utils.d"))
|
||||
|| name.startsWith("com.shopee.shpssdkbank.")
|
||||
|| (!MariBankRegionProfile.isSingapore(packageName)
|
||||
&& name.startsWith("com.shopee.shpssdk."));
|
||||
}
|
||||
|
||||
/** SG split-dex:safemode / utils.d 延迟加载时在 loadClass 补装 Hook。 */
|
||||
private static void hookClassLoaderSplitDex(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
ClassLoader.class,
|
||||
"loadClass",
|
||||
String.class,
|
||||
boolean.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.getThrowable() != null) {
|
||||
return;
|
||||
}
|
||||
String name = (String) param.args[0];
|
||||
if (isLateHookTargetClass(name, lpparam.packageName)) {
|
||||
tryInstallLateAppHooks(lpparam, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
XposedBridge.log(TAG + " ClassLoader.loadClass split-dex watcher OK");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip ClassLoader.loadClass watcher: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
@@ -165,10 +302,14 @@ public final class MariBankRootBypassHook {
|
||||
hookRootDialogBlock(lpparam);
|
||||
hookAdbBypass(lpparam);
|
||||
hookNetworkLogging(lpparam);
|
||||
hookClassLoaderSplitDex(lpparam);
|
||||
|
||||
XposedBridge.log(TAG + " early app hooks installed");
|
||||
}
|
||||
private static int hookAllBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
if (HOOKED_SAFE_MODE_CLASSES.contains(className)) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
@@ -197,6 +338,7 @@ public final class MariBankRootBypassHook {
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
HOOKED_SAFE_MODE_CLASSES.add(className);
|
||||
XposedBridge.log(TAG + " hooked " + count + " checks in " + className);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
@@ -480,11 +622,7 @@ public final class MariBankRootBypassHook {
|
||||
/** SHPSSDK 风控:仅 Hook 返回 boolean 的实例方法。 */
|
||||
private static int hookShpsRisk(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
int count = 0;
|
||||
String[] riskClasses = {
|
||||
"com.shopee.shpssdkbank.SPSAssessRisk",
|
||||
"com.shopee.shpssdk.SPSAssessRisk",
|
||||
};
|
||||
for (String className : riskClasses) {
|
||||
for (String className : MariBankRegionProfile.shpsAssessRiskClasses(lpparam.packageName)) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
@@ -519,11 +657,7 @@ public final class MariBankRootBypassHook {
|
||||
*/
|
||||
private static void hookShpsToken(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
final String contextClass = "android.content.Context";
|
||||
String[] shpsSdkClasses = {
|
||||
"com.shopee.shpssdk.SHPSSDK",
|
||||
"com.shopee.shpssdkbank.SHPSSDK",
|
||||
};
|
||||
for (String className : shpsSdkClasses) {
|
||||
for (String className : MariBankRegionProfile.shpsSdkClasses(lpparam.packageName)) {
|
||||
hookEmptyRiskList(lpparam, className, "getRiskSync", contextClass);
|
||||
hookEmptyRiskList(lpparam, className, "getExtRiskSync", contextClass);
|
||||
hookRiskAsyncCallback(lpparam, className, "getRiskAsync", contextClass,
|
||||
@@ -601,11 +735,7 @@ public final class MariBankRootBypassHook {
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
String[] assessRiskClasses = {
|
||||
"com.shopee.shpssdkbank.SPSAssessRisk",
|
||||
"com.shopee.shpssdk.SPSAssessRisk",
|
||||
};
|
||||
for (String className : assessRiskClasses) {
|
||||
for (String className : MariBankRegionProfile.shpsAssessRiskClasses(lpparam.packageName)) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
@@ -622,11 +752,7 @@ public final class MariBankRootBypassHook {
|
||||
}
|
||||
}
|
||||
|
||||
String[] callbackAdapters = {
|
||||
"com.shopee.shpssdk.SPSCallbackAdapter",
|
||||
"com.shopee.shpssdkbank.SPSCallbackAdapter",
|
||||
};
|
||||
for (String className : callbackAdapters) {
|
||||
for (String className : MariBankRegionProfile.shpsCallbackAdapterClasses(lpparam.packageName)) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
@@ -1285,8 +1411,11 @@ public final class MariBankRootBypassHook {
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
param.method, param.thisObject, new Object[]{buffer});
|
||||
String content = (String) XposedHelpers.callMethod(buffer, "readUtf8");
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
|
||||
String url = CURRENT_REQUEST_URL.get();
|
||||
if (!content.isEmpty() && url != null && MariBankCaptureUtil.isBankUrl(url)) {
|
||||
MariBankCaptureUtil.logText("reqBody wire " + url, content);
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
|
||||
if (url != null && url.contains("/register") && !content.isEmpty()) {
|
||||
int show = Math.min(content.length(), 400);
|
||||
XposedBridge.log(TAG + " register body(raw): "
|
||||
@@ -1633,6 +1762,19 @@ public final class MariBankRootBypassHook {
|
||||
XposedBridge.hookMethod(method, fakeDisabled);
|
||||
}
|
||||
}
|
||||
// getInt(cr, key, def) — 部分 SDK 走三参数重载
|
||||
if ("getInt".equals(target[1])) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
clazz,
|
||||
"getInt",
|
||||
ContentResolver.class,
|
||||
String.class,
|
||||
int.class,
|
||||
fakeDisabled);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip Settings hook " + target[0] + ": " + t.getMessage());
|
||||
}
|
||||
|
||||
@@ -16,26 +16,124 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
public final class MariBankSdkUtilsHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankEncrypt";
|
||||
private static final int MAX_LOG = 2000;
|
||||
|
||||
private static final String WRAPPER = "com.shopee.bke.lib.jni.utils.d";
|
||||
private static final String NATIVE_ENCRYPT = "com.shopee.bke.lib.jni.utils.uvwuvwuv";
|
||||
private static final String SG_ENCRYPT_HELPER = "com.shopee.bke.lib.jni.utils.uvwwwwuv";
|
||||
|
||||
private static volatile boolean installed = false;
|
||||
private static volatile boolean encryptHooksInstalled = false;
|
||||
private static volatile boolean gsonHookInstalled = false;
|
||||
private static volatile boolean encryptLoadClassWatcherInstalled = false;
|
||||
private static volatile String installedForPackage;
|
||||
|
||||
private MariBankSdkUtilsHook() {
|
||||
}
|
||||
|
||||
/** BkeApplication.onCreate 之后安装(libsdkutils 已加载)。 */
|
||||
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (installed) {
|
||||
public static int installLate(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
installEncryptLoadClassWatcher(lpparam);
|
||||
if (encryptHooksInstalled && lpparam.packageName.equals(installedForPackage)) {
|
||||
return 0;
|
||||
}
|
||||
int n = 0;
|
||||
if (MariBankRegionProfile.hasEncryptWrapperD(lpparam.packageName)) {
|
||||
n += hookEncryptWrapper(lpparam);
|
||||
}
|
||||
n += hookNativeEncryptUtils(lpparam);
|
||||
if (MariBankRegionProfile.hasEncryptHelperUvwwwwuv(lpparam.packageName)) {
|
||||
n += hookEncryptHelperClass(lpparam, SG_ENCRYPT_HELPER, "uvwwwwuv");
|
||||
}
|
||||
if (n > 0) {
|
||||
encryptHooksInstalled = true;
|
||||
installedForPackage = lpparam.packageName;
|
||||
}
|
||||
if (!gsonHookInstalled) {
|
||||
int g = hookGsonRegister(lpparam);
|
||||
if (g > 0) {
|
||||
gsonHookInstalled = true;
|
||||
n += g;
|
||||
}
|
||||
}
|
||||
if (n > 0) {
|
||||
XposedBridge.log(TAG + " late encrypt hooks=" + n + " region="
|
||||
+ MariBankRegionProfile.label(lpparam.packageName));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static void installEncryptLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (encryptLoadClassWatcherInstalled) {
|
||||
return;
|
||||
}
|
||||
installed = true;
|
||||
int n = hookEncryptWrapper(lpparam);
|
||||
n += hookNativeEncryptUtils(lpparam);
|
||||
n += hookGsonRegister(lpparam);
|
||||
XposedBridge.log(TAG + " late encrypt hooks=" + n);
|
||||
encryptLoadClassWatcherInstalled = true;
|
||||
final String pkg = lpparam.packageName;
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
ClassLoader.class,
|
||||
"loadClass",
|
||||
String.class,
|
||||
boolean.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.getThrowable() != null) {
|
||||
return;
|
||||
}
|
||||
String name = (String) param.args[0];
|
||||
if (NATIVE_ENCRYPT.equals(name)
|
||||
|| SG_ENCRYPT_HELPER.equals(name)
|
||||
|| (MariBankRegionProfile.hasEncryptWrapperD(pkg)
|
||||
&& WRAPPER.equals(name))) {
|
||||
installLate(lpparam);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " ClassLoader encrypt watcher OK");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip encrypt ClassLoader watcher: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static int hookEncryptHelperClass(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, String className, String logLabel) {
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedBridge.log(TAG + " >> " + logLabel + "." + method.getName());
|
||||
for (int i = 0; i < param.args.length; i++) {
|
||||
Object sanitized = sanitizeArg(param.args[i]);
|
||||
if (sanitized != param.args[i]) {
|
||||
param.args[i] = sanitized;
|
||||
}
|
||||
logArg(" in" + i, param.args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (result instanceof byte[]) {
|
||||
logArg(" out", result);
|
||||
} else if (result instanceof String) {
|
||||
logArg(" out", result);
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + logLabel + " methods=" + count);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + logLabel + ": " + t.getMessage());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int hookEncryptWrapper(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
@@ -143,7 +241,7 @@ public final class MariBankSdkUtilsHook {
|
||||
json = sanitized;
|
||||
}
|
||||
if (MariBankRegisterPayloadUtil.isRegistrationPayload(json)) {
|
||||
XposedBridge.log(TAG + " Gson REGISTRATION: " + truncate(json));
|
||||
MariBankCaptureUtil.logText("Gson REGISTRATION plaintext", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,7 +293,7 @@ public final class MariBankSdkUtilsHook {
|
||||
if (arg instanceof String) {
|
||||
String s = (String) arg;
|
||||
if (s.length() > 4 || s.contains("|") || looksLikeRegisterJson(s)) {
|
||||
XposedBridge.log(TAG + label + " String(" + s.length() + ") " + truncate(s));
|
||||
MariBankCaptureUtil.logText(label, s);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -207,19 +305,9 @@ public final class MariBankSdkUtilsHook {
|
||||
} catch (Throwable t) {
|
||||
text = "<bin>";
|
||||
}
|
||||
if (text.contains("|") || looksLikeRegisterJson(text) || bytes.length < 512) {
|
||||
XposedBridge.log(TAG + label + " byte[" + bytes.length + "] " + truncate(text));
|
||||
if (text.contains("|") || looksLikeRegisterJson(text) || bytes.length < 8192) {
|
||||
MariBankCaptureUtil.logBytes(label, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncate(String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
if (s.length() <= MAX_LOG) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, MAX_LOG) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,9 @@ public final class MariBankShpsNativeHook {
|
||||
}
|
||||
|
||||
private static volatile boolean deferredInstalled = false;
|
||||
private static volatile boolean lateNativeInstalled = false;
|
||||
private static volatile boolean nativeBridgeInstalled = false;
|
||||
private static volatile boolean nativeLoadClassWatcherInstalled = false;
|
||||
private static final Set<String> HOOKED_NATIVE_CORE_CLASSES = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 勿 Hook:负责 SoUtils.loadSoLibrary / libshpssdk_bank.so 加载,Hook 会导致 SO 找不到。
|
||||
@@ -104,13 +106,45 @@ public final class MariBankShpsNativeHook {
|
||||
|
||||
/** BkeApplication.onCreate 之后:RN / shpssdk SO 已加载,再装 native 桥接 Hook。 */
|
||||
public static void installLateNativeHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (lateNativeInstalled) {
|
||||
if (!nativeBridgeInstalled) {
|
||||
nativeBridgeInstalled = true;
|
||||
hookShpsNativeBridge(lpparam);
|
||||
installNativeLoadClassWatcher(lpparam);
|
||||
}
|
||||
hookShpsNativeCore(lpparam);
|
||||
}
|
||||
|
||||
private static void installNativeLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (nativeLoadClassWatcherInstalled) {
|
||||
return;
|
||||
}
|
||||
lateNativeInstalled = true;
|
||||
hookShpsNativeBridge(lpparam);
|
||||
hookShpsNativeCore(lpparam);
|
||||
XposedBridge.log(TAG + " late native hooks installed for " + lpparam.packageName);
|
||||
nativeLoadClassWatcherInstalled = true;
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
ClassLoader.class,
|
||||
"loadClass",
|
||||
String.class,
|
||||
boolean.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.getThrowable() != null) {
|
||||
return;
|
||||
}
|
||||
String name = (String) param.args[0];
|
||||
if (name != null && name.contains("shpssdk")) {
|
||||
if (MariBankRegionProfile.isSingapore(lpparam.packageName)
|
||||
&& MariBankRegionProfile.isShpssdkLegacyPackage(name)) {
|
||||
return;
|
||||
}
|
||||
hookShpsNativeCore(lpparam);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " ClassLoader native-core watcher OK");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip native ClassLoader watcher: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** native 直接读 /proc/self/maps 查 hook 库;过滤内容。 */
|
||||
@@ -251,10 +285,7 @@ public final class MariBankShpsNativeHook {
|
||||
|
||||
/** 保留 requestDefense 执行(生成 x-sap-fixme),仅净化返回值中的 risk 字段。 */
|
||||
private static void hookRequestDefense(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
for (String className : new String[]{
|
||||
"com.shopee.shpssdkbank.SHPSSDK",
|
||||
"com.shopee.shpssdk.SHPSSDK",
|
||||
}) {
|
||||
for (String className : MariBankRegionProfile.shpsSdkClasses(lpparam.packageName)) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
@@ -403,21 +434,19 @@ public final class MariBankShpsNativeHook {
|
||||
* SHPSSDK 核心 native 桥:wvvvuwwu.wwvwvwuvv / vvuwuuvuu 等直接生成 risk 数据。
|
||||
*/
|
||||
private static void hookShpsNativeCore(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
String[] coreClasses = {
|
||||
"com.shopee.shpssdkbank.wvvvuwwu",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
|
||||
"com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
|
||||
"com.shopee.shpssdk.wvvvuwwu",
|
||||
};
|
||||
int total = 0;
|
||||
for (String className : coreClasses) {
|
||||
for (String className : MariBankRegionProfile.shpsAttestationCoreClasses(lpparam.packageName)) {
|
||||
total += hookNativeCoreClass(lpparam, className);
|
||||
}
|
||||
XposedBridge.log(TAG + " native-core total hooks=" + total);
|
||||
XposedBridge.log(TAG + " native-core total hooks=" + total + " region="
|
||||
+ MariBankRegionProfile.label(lpparam.packageName));
|
||||
}
|
||||
|
||||
private static int hookNativeCoreClass(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
if (HOOKED_NATIVE_CORE_CLASSES.contains(className)) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
@@ -425,9 +454,6 @@ public final class MariBankShpsNativeHook {
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
if (Modifier.isNative(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
Class<?> rt = method.getReturnType();
|
||||
if (rt == String.class) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@@ -443,12 +469,41 @@ public final class MariBankShpsNativeHook {
|
||||
if (!sanitized.equals(s)) {
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " core String sanitized "
|
||||
+ className + "#" + method.getName()
|
||||
+ (Modifier.isNative(method.getModifiers()) ? " (native)" : ""));
|
||||
}
|
||||
} else if (s.length() > 40) {
|
||||
String sanitized = MariBankRegisterPayloadUtil.isRegistrationPayload(s)
|
||||
? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s)
|
||||
: MariBankRiskTokenUtil.sanitizeAllInText(s);
|
||||
if (!sanitized.equals(s)) {
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " core register String sanitized "
|
||||
+ className + "#" + method.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
} else if (rt == byte[].class) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (!(result instanceof byte[])) {
|
||||
return;
|
||||
}
|
||||
byte[] bytes = (byte[]) result;
|
||||
byte[] out = MariBankRegisterPayloadUtil.sanitizeRegistrationBytes(bytes);
|
||||
if (out != bytes) {
|
||||
param.setResult(out);
|
||||
XposedBridge.log(TAG + " core byte[] sanitized "
|
||||
+ className + "#" + method.getName()
|
||||
+ (Modifier.isNative(method.getModifiers()) ? " (native)" : ""));
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
} else if (rt == boolean.class || rt == Boolean.class
|
||||
|| rt == int.class || rt == Integer.class) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@@ -465,7 +520,8 @@ public final class MariBankShpsNativeHook {
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + count + " core natives in " + className);
|
||||
HOOKED_NATIVE_CORE_CLASSES.add(className);
|
||||
XposedBridge.log(TAG + " hooked " + count + " core methods in " + className);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip core " + className + ": " + t.getMessage());
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
/**
|
||||
* 拦截 root 探测命令时返回「空 / 失败」假进程,避免抛异常被 SHPSSDK 记为 tamper。
|
||||
*/
|
||||
public final class ProbeGuard {
|
||||
|
||||
private static final String TAG = "notiMessageHook/ProbeGuard";
|
||||
|
||||
private ProbeGuard() {
|
||||
}
|
||||
|
||||
public static boolean isBlockedCommand(String command) {
|
||||
if (command == null || command.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = command.toLowerCase(Locale.US);
|
||||
return lower.contains(" magisk")
|
||||
|| lower.contains("/su")
|
||||
|| lower.startsWith("su")
|
||||
|| lower.contains("which su")
|
||||
|| lower.contains("getprop ro.debuggable")
|
||||
|| lower.contains("busybox")
|
||||
|| lower.equals("su");
|
||||
}
|
||||
|
||||
public static boolean isBlockedCommand(List<String> commands) {
|
||||
if (commands == null || commands.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return isBlockedCommand(String.join(" ", commands));
|
||||
}
|
||||
|
||||
public static Process fakeFailedProcess(String reason) {
|
||||
XposedBridge.log(TAG + " fake probe process: " + reason);
|
||||
return new FakeProcess();
|
||||
}
|
||||
|
||||
private static final class FakeProcess extends Process {
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return OutputStream.nullOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
return new ByteArrayInputStream(new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitFor() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int exitValue() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import android.os.Message;
|
||||
|
||||
@@ -20,7 +23,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
@@ -78,10 +84,13 @@ public final class TngRootBypassHook {
|
||||
"EmailOtpVerificationActivity",
|
||||
};
|
||||
|
||||
private static volatile int captchaDialogShowCount = 0;
|
||||
|
||||
private static final String[] REGISTRATION_RPC_MARKERS = {
|
||||
"phonecheck", "com.abl.wallet.phone", "com.abl.wallet.otp",
|
||||
"customer.registration", "customer.verify", "customer.login",
|
||||
"login.options", "callingcode", "pin.token", "module.whitelist",
|
||||
"secauth",
|
||||
};
|
||||
|
||||
private TngRootBypassHook() {
|
||||
@@ -212,6 +221,7 @@ public final class TngRootBypassHook {
|
||||
private static final String[] HTTP_LOG_MARKERS = {
|
||||
"otp", "verify", "pin", "register", "auth", "login", "sms", "mobile",
|
||||
"risk", "token", "error", "code", "unexpected", "reference",
|
||||
"captcha", "nc_token", "nvc", "slide", "security",
|
||||
};
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
@@ -224,11 +234,14 @@ public final class TngRootBypassHook {
|
||||
|
||||
// Splash 常卡死;Instrumentation 强拉即可。双通道会抢跑导致 Login 重载/卡死。
|
||||
hookEarlyAttachLog(lpparam);
|
||||
hookConscryptStatsLogGuard();
|
||||
hookRegistrationFlowGuard(lpparam);
|
||||
hookSplashForceLogin(lpparam);
|
||||
hookLoginDismissSplash(lpparam);
|
||||
hookHardwareRendererSetName();
|
||||
hookHwuiBySurfacePolicy(lpparam);
|
||||
hookLoadingDialogSkip();
|
||||
hookBottomSelectDialogDiag(lpparam);
|
||||
hookCaptchaDiag(lpparam);
|
||||
hookActivityLifecycleDiag(lpparam);
|
||||
hookPromonApService(lpparam);
|
||||
hookPromonBroadcastReceiver(lpparam);
|
||||
@@ -240,6 +253,7 @@ public final class TngRootBypassHook {
|
||||
ProcMapsFilterHook.install(lpparam);
|
||||
|
||||
hookAntiSuicide();
|
||||
hookPromonSuicideUpstream(lpparam);
|
||||
hookUncaughtPromonException(lpparam);
|
||||
hookKillApplicationHandler(lpparam);
|
||||
hookBlockSecurityErrorLaunch(lpparam);
|
||||
@@ -263,6 +277,47 @@ public final class TngRootBypassHook {
|
||||
hookWebViewErrorDiag(lpparam);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zygisk/命名空间下 android.util.StatsLog native 常 UnsatisfiedLinkError,
|
||||
* Conscrypt TLS 指标线程一写就炸 → ART fatal。直接 noop 指标写入。
|
||||
*/
|
||||
private static void hookConscryptStatsLogGuard() {
|
||||
int hooked = 0;
|
||||
for (String className : new String[]{
|
||||
"com.android.org.conscrypt.metrics.StatsLogImpl",
|
||||
"com.android.org.conscrypt.metrics.ConscryptStatsLog",
|
||||
"com.google.android.gms.org.conscrypt.metrics.StatsLogImpl",
|
||||
}) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
String n = method.getName();
|
||||
if (!n.startsWith("write")
|
||||
&& !n.startsWith("report")
|
||||
&& !n.startsWith("count")
|
||||
&& !"startWriterThread".equals(n)) {
|
||||
continue;
|
||||
}
|
||||
hookMethodNoopSilent(method);
|
||||
hooked++;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
Class<?> statsLog = Class.forName("android.util.StatsLog");
|
||||
for (Method method : statsLog.getDeclaredMethods()) {
|
||||
String n = method.getName();
|
||||
if ("loadNativeLibrary".equals(n) || n.startsWith("write")) {
|
||||
hookMethodNoopSilent(method);
|
||||
hooked++;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
XposedBridge.log(TAG + " conscrypt/StatsLog guard hooked n=" + hooked);
|
||||
}
|
||||
|
||||
private static void logActivityDiag(String phase, Activity activity) {
|
||||
if (activity == null) {
|
||||
return;
|
||||
@@ -392,8 +447,14 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
String lower = url.toLowerCase(Locale.US);
|
||||
return lower.contains("tngdigital")
|
||||
|| lower.contains("ebuckler")
|
||||
|| lower.contains("imgw")
|
||||
|| lower.contains("abl.wallet")
|
||||
|| lower.contains("alipay")
|
||||
|| lower.contains("aliyun")
|
||||
|| lower.contains("captcha")
|
||||
|| lower.contains("dun.163")
|
||||
|| lower.contains("nc_token")
|
||||
|| lower.contains("otp")
|
||||
|| lower.contains("verify")
|
||||
|| lower.contains("register")
|
||||
@@ -457,22 +518,6 @@ public final class TngRootBypassHook {
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " WebView error hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Dialog.class,
|
||||
"show",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String owner = param.thisObject.getClass().getName();
|
||||
if (owner.contains("TNG") || owner.contains("Dialog")
|
||||
|| owner.contains("Error") || owner.contains("i7.")) {
|
||||
XposedBridge.log(TAG + " Dialog.show " + owner);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 仅记录注册/登录链 Activity 生命周期,便于 logcat 定位卡点。 */
|
||||
@@ -528,44 +573,722 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
|
||||
/**
|
||||
* 区号选择弹窗诊断。勿关 HW 加速——Android 16 上会导致列表黑屏。
|
||||
* ANR 由 HardwareRenderer.setName 拦截兜底。
|
||||
* Android 16 HWUI 策略(经 APK 逆向核对):
|
||||
* - i7.l:一律 skip show(真 show → gralloc ABRT)
|
||||
* - Login / Splash / 普通 View 页:清 HW flag + 拦 enableHW(软件绘制)
|
||||
* - UserSearchCallingCodeActivity:Compose CallingListScreen/LazyColumn,
|
||||
* 软件绘制 = 黑屏;必须完整 HW(与 BottomSelect 相同)
|
||||
* - BottomSelect:完整 HW(关了会黑)
|
||||
* - captcha:软件层弹窗,不放行 enableHW
|
||||
*/
|
||||
private static void hookBottomSelectDialogDiag(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
private static final ThreadLocal<Boolean> blockSetNameForDialog = new ThreadLocal<>();
|
||||
/** BottomSelect Dialog.show 期间放行 enableHW。 */
|
||||
private static final ThreadLocal<Boolean> allowHwForDialog = new ThreadLocal<>();
|
||||
|
||||
/** 区号 BottomSelect 弹窗可见。 */
|
||||
private static volatile boolean bottomSelectDialogActive = false;
|
||||
|
||||
/**
|
||||
* 区号 Compose Activity 生命周期标记。
|
||||
* 必须在 execStart/onCreate 前设为 true,供 enableHardwareAcceleration 放行。
|
||||
*/
|
||||
private static volatile boolean callingCodeSurfaceActive = false;
|
||||
|
||||
private static boolean isCaptchaDialog(String owner) {
|
||||
return owner != null
|
||||
&& (owner.contains("CaptchaWebViewDialog") || owner.contains("com.aliyun.captcha"));
|
||||
}
|
||||
|
||||
private static boolean isLoadingDialog(String owner) {
|
||||
return owner != null
|
||||
&& (owner.contains("i7.") || owner.contains("Loading") || owner.contains("Progress"));
|
||||
}
|
||||
|
||||
private static boolean isCallingCodeActivity(String className) {
|
||||
return className != null && className.contains("UserSearchCallingCodeActivity");
|
||||
}
|
||||
|
||||
private static boolean isCallingCodeIntent(Intent intent) {
|
||||
if (intent == null) {
|
||||
return false;
|
||||
}
|
||||
if (intent.getComponent() != null) {
|
||||
String cn = intent.getComponent().getClassName();
|
||||
if (isCallingCodeActivity(cn)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String action = intent.getAction();
|
||||
return action != null && action.contains("CallingCode");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose 区号页 + BottomSelect 必须放行 enableHW。
|
||||
* 其它页面拦 enableHW 且清 FLAG,避免半开 HW 黑屏。
|
||||
*/
|
||||
private static boolean shouldAllowHwSurface() {
|
||||
return callingCodeSurfaceActive
|
||||
|| bottomSelectDialogActive
|
||||
|| Boolean.TRUE.equals(allowHwForDialog.get());
|
||||
}
|
||||
|
||||
private static boolean shouldBlockSetNameOnMain() {
|
||||
if (shouldAllowHwSurface()) {
|
||||
return false;
|
||||
}
|
||||
// 区号页绘制期也不拦 setName(软件层下 setName 较少);仅 Dialog 窗口期拦
|
||||
return Boolean.TRUE.equals(blockSetNameForDialog.get());
|
||||
}
|
||||
|
||||
/** 必须在 ViewRoot 附着前清 flag,否则拦 enableHW 会黑屏。 */
|
||||
private static void clearHardwareAcceleratedFlag(Activity activity) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.common.widget.BottomSelectDialogFragment",
|
||||
lpparam.classLoader);
|
||||
XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart");
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (diag only)");
|
||||
Window window = activity.getWindow();
|
||||
if (window != null) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage());
|
||||
XposedBridge.log(TAG + " clear HW flag failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ANR 栈:Dialog.show → enableHardwareAcceleration → HardwareRenderer.setName → future.get 卡死。
|
||||
* 仅拦截 setName;勿全局关 Dialog HW(区号选择会黑屏)。
|
||||
* 区号 BottomSelect:须 hook 父类 DialogFragment.show/onStart(Kotlin 子类不 override show)。
|
||||
* 勿关 BottomSheet HW 加速(Android 16 黑屏);可见期间放行主线程 setName。
|
||||
*/
|
||||
private static void hookHardwareRendererSetName() {
|
||||
private static void hookBottomSelectDialogDiag(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
int hooked = 0;
|
||||
try {
|
||||
Class<?> df = XposedHelpers.findClass(
|
||||
"androidx.fragment.app.DialogFragment", lpparam.classLoader);
|
||||
Class<?> fm = XposedHelpers.findClass(
|
||||
"androidx.fragment.app.FragmentManager", lpparam.classLoader);
|
||||
XposedHelpers.findAndHookMethod(df, "show", fm, String.class, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String owner = param.thisObject.getClass().getName();
|
||||
if (!owner.contains("BottomSelect")) {
|
||||
return;
|
||||
}
|
||||
bottomSelectDialogActive = true;
|
||||
XposedBridge.log(TAG + " BottomSelect show " + owner);
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
XposedHelpers.findAndHookMethod(df, "onStart", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String owner = param.thisObject.getClass().getName();
|
||||
if (!owner.contains("BottomSelect")) {
|
||||
return;
|
||||
}
|
||||
bottomSelectDialogActive = true;
|
||||
XposedBridge.log(TAG + " BottomSelect onStart " + owner);
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
XC_MethodHook deactivateHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String owner = param.thisObject.getClass().getName();
|
||||
if (!owner.contains("BottomSelect")) {
|
||||
return;
|
||||
}
|
||||
bottomSelectDialogActive = false;
|
||||
XposedBridge.log(TAG + " BottomSelect inactive "
|
||||
+ param.method.getName() + " " + owner);
|
||||
}
|
||||
};
|
||||
XposedHelpers.findAndHookMethod(df, "onStop", deactivateHook);
|
||||
XposedHelpers.findAndHookMethod(df, "onDestroyView", deactivateHook);
|
||||
hooked += 2;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " DialogFragment BottomSelect hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
Class<?> sheet = XposedHelpers.findClass(
|
||||
"com.google.android.material.bottomsheet.BottomSheetDialog", lpparam.classLoader);
|
||||
XposedHelpers.findAndHookMethod(sheet, "show", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
bottomSelectDialogActive = true;
|
||||
XposedBridge.log(TAG + " BottomSheetDialog.show active");
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(sheet, "dismiss", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
bottomSelectDialogActive = false;
|
||||
XposedBridge.log(TAG + " BottomSheetDialog.dismiss inactive");
|
||||
}
|
||||
});
|
||||
hooked += 2;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " BottomSheetDialog hook skip: " + t.getMessage());
|
||||
}
|
||||
XposedBridge.log(TAG + " hooked BottomSelect lifecycle n=" + hooked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全验证诊断:只 hook 关键类 + WebView,避免全量 58 方法拖垮主线程。
|
||||
*/
|
||||
private static void hookCaptchaDiag(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
int total = 0;
|
||||
for (String className : new String[]{
|
||||
"com.aliyun.captcha.Captcha",
|
||||
"com.aliyun.captcha.CaptchaWebViewDialog",
|
||||
}) {
|
||||
total += hookAllConcreteMethods(lpparam.classLoader, className, "captcha");
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Class<?> inner : clazz.getDeclaredClasses()) {
|
||||
String n = inner.getSimpleName();
|
||||
if (!n.contains("Callback") && !n.contains("Listener")
|
||||
&& !n.contains("Completion")) {
|
||||
continue;
|
||||
}
|
||||
total += hookAllConcreteMethodsOnClass(inner, "captcha");
|
||||
XposedBridge.log(TAG + " captcha inner " + inner.getName());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " captcha inner scan " + className + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
hookCaptchaWebView(lpparam);
|
||||
XposedBridge.log(TAG + " captcha diag hooked methods n=" + total);
|
||||
}
|
||||
|
||||
private static int hookAllConcreteMethods(
|
||||
ClassLoader loader, String className, String tag) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, loader);
|
||||
return hookAllConcreteMethodsOnClass(clazz, tag);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " " + tag + " class skip " + className + ": " + t.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int hookAllConcreteMethodsOnClass(Class<?> clazz, String tag) {
|
||||
int hooked = 0;
|
||||
final String simple = clazz.getName();
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (Modifier.isAbstract(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
// 跳过高频噪声
|
||||
String name = method.getName();
|
||||
if ("toString".equals(name) || "hashCode".equals(name)
|
||||
|| "equals".equals(name) || "clone".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
final String hookLabel = simple + "#" + name;
|
||||
final Method hookedMethod = method;
|
||||
try {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedBridge.log(TAG + " " + tag + " CALL " + hookLabel
|
||||
+ " args=" + describeHookArgs(param.args));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.hasThrowable()) {
|
||||
XposedBridge.log(TAG + " " + tag + " ERR " + hookLabel
|
||||
+ " " + param.getThrowable().getClass().getSimpleName()
|
||||
+ ": " + param.getThrowable().getMessage());
|
||||
return;
|
||||
}
|
||||
Object result = param.getResult();
|
||||
if (result == null
|
||||
&& (hookedMethod.getReturnType() == void.class
|
||||
|| hookedMethod.getReturnType() == Void.class)) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " " + tag + " RET " + hookLabel
|
||||
+ " -> " + describeHookResult(result));
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " " + tag + " hook fail " + hookLabel
|
||||
+ ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
if (hooked > 0) {
|
||||
XposedBridge.log(TAG + " " + tag + " hooked " + hooked + " on " + simple);
|
||||
}
|
||||
return hooked;
|
||||
}
|
||||
|
||||
private static String describeHookResult(Object result) {
|
||||
if (result == null) {
|
||||
return "null";
|
||||
}
|
||||
if (result instanceof String) {
|
||||
String text = (String) result;
|
||||
return "String(" + (text.length() > 200 ? text.substring(0, 200) + "..." : text) + ")";
|
||||
}
|
||||
if (result instanceof byte[]) {
|
||||
return "byte[" + ((byte[]) result).length + "]";
|
||||
}
|
||||
if (result instanceof Boolean || result instanceof Number) {
|
||||
return String.valueOf(result);
|
||||
}
|
||||
return result.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(result));
|
||||
}
|
||||
|
||||
private static String shortStack(int maxFrames) {
|
||||
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int n = 0;
|
||||
for (StackTraceElement el : stack) {
|
||||
String cn = el.getClassName();
|
||||
if (cn.startsWith("java.") || cn.startsWith("dalvik.")
|
||||
|| cn.startsWith("android.os.") || cn.startsWith("de.robv.android.xposed")
|
||||
|| cn.contains("TngRootBypassHook") || cn.contains("LSPosed")
|
||||
|| cn.contains("XposedBridge")) {
|
||||
continue;
|
||||
}
|
||||
if (n > 0) {
|
||||
sb.append(" <- ");
|
||||
}
|
||||
sb.append(el.getClassName()).append("#").append(el.getMethodName())
|
||||
.append(":").append(el.getLineNumber());
|
||||
if (++n >= maxFrames) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.length() == 0 ? "(empty)" : sb.toString();
|
||||
}
|
||||
|
||||
/** Captcha WebView:页面加载/错误/JS 回调全打点。 */
|
||||
private static void hookCaptchaWebView(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.webkit.WebView", lpparam.classLoader, "loadUrl", String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String url = (String) param.args[0];
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
String lower = url.toLowerCase(Locale.US);
|
||||
if (lower.contains("captcha") || lower.contains("aliyun")
|
||||
|| lower.contains("nvc") || lower.contains("nc_")
|
||||
|| lower.contains("slide") || lower.startsWith("javascript:")) {
|
||||
XposedBridge.log(TAG + " captcha WebView.loadUrl "
|
||||
+ (url.length() > 300 ? url.substring(0, 300) + "..." : url));
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked WebView.loadUrl (captcha filter)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " WebView.loadUrl captcha hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.webkit.WebView", lpparam.classLoader,
|
||||
"evaluateJavascript", String.class,
|
||||
"android.webkit.ValueCallback",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String script = (String) param.args[0];
|
||||
if (script == null) {
|
||||
return;
|
||||
}
|
||||
String lower = script.toLowerCase(Locale.US);
|
||||
if (lower.contains("captcha") || lower.contains("token")
|
||||
|| lower.contains("verify") || lower.contains("nc")
|
||||
|| lower.contains("slide") || lower.contains("callback")) {
|
||||
XposedBridge.log(TAG + " captcha WebView.evalJS "
|
||||
+ (script.length() > 240 ? script.substring(0, 240) + "..." : script));
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked WebView.evaluateJavascript (captcha filter)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " WebView.evalJS captcha hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.webkit.WebViewClient",
|
||||
lpparam.classLoader,
|
||||
"onPageFinished",
|
||||
"android.webkit.WebView",
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String url = (String) param.args[1];
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
String lower = url.toLowerCase(Locale.US);
|
||||
if (lower.contains("captcha") || lower.contains("aliyun")
|
||||
|| lower.contains("nvc") || lower.contains("nc_")) {
|
||||
XposedBridge.log(TAG + " captcha WebView.onPageFinished " + url);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " WebViewClient.onPageFinished captcha hook failed: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
// 已有 onReceivedError diag;再补 captcha 专用标记
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.webkit.WebViewClient",
|
||||
lpparam.classLoader,
|
||||
"onReceivedError",
|
||||
"android.webkit.WebView",
|
||||
"android.webkit.WebResourceRequest",
|
||||
"android.webkit.WebResourceError",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object req = param.args[1];
|
||||
Object err = param.args[2];
|
||||
Object url = XposedHelpers.callMethod(req, "getUrl");
|
||||
Object desc = XposedHelpers.callMethod(err, "getDescription");
|
||||
String urlStr = String.valueOf(url);
|
||||
if (urlStr.toLowerCase(Locale.US).contains("captcha")
|
||||
|| urlStr.toLowerCase(Locale.US).contains("aliyun")
|
||||
|| urlStr.toLowerCase(Locale.US).contains("nvc")) {
|
||||
XposedBridge.log(TAG + " captcha WebView.error url="
|
||||
+ urlStr + " desc=" + desc);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static int hookCaptchaCallbackClass(Class<?> clazz, String label) {
|
||||
return hookAllConcreteMethodsOnClass(clazz, "captcha");
|
||||
}
|
||||
|
||||
/** 已 skip show 的 loading Dialog,让 isShowing=true 避免业务层卡死。 */
|
||||
private static final Set<Dialog> skippedLoadingDialogs =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
|
||||
/**
|
||||
* i7.l 等 loading:一律 skip show + 伪装 isShowing。
|
||||
* 不在此处开 HW 白名单——区号列表由 startActivity 标记。
|
||||
*/
|
||||
private static void hookLoadingDialogSkip() {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(Dialog.class, "show", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Dialog dialog = (Dialog) param.thisObject;
|
||||
String owner = dialog.getClass().getName();
|
||||
if (isLoadingDialog(owner)) {
|
||||
skippedLoadingDialogs.add(dialog);
|
||||
XposedBridge.log(TAG + " Dialog.show skip loading " + owner
|
||||
+ " callingCode=" + callingCodeSurfaceActive
|
||||
+ " regFlow=" + registrationFlowActive);
|
||||
param.setResult(null);
|
||||
return;
|
||||
}
|
||||
if (owner.contains("BottomSelect") || owner.contains("BottomSheet")) {
|
||||
bottomSelectDialogActive = true;
|
||||
allowHwForDialog.set(Boolean.TRUE);
|
||||
} else if (isCaptchaDialog(owner)) {
|
||||
// Android 16:captcha 放行 enableHW → gralloc ABRT 黑屏闪退
|
||||
// 改为软件层弹窗,不进白名单
|
||||
try {
|
||||
Dialog captchaDialog = (Dialog) param.thisObject;
|
||||
Window cw = captchaDialog.getWindow();
|
||||
if (cw != null) {
|
||||
cw.clearFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
captchaDialogShowCount++;
|
||||
XposedBridge.log(TAG + " Dialog.show CaptchaWebViewDialog #"
|
||||
+ captchaDialogShowCount
|
||||
+ " owner=" + owner
|
||||
+ " hw=software"
|
||||
+ " regFlow=" + registrationFlowActive
|
||||
+ " stack=" + shortStack(8));
|
||||
} else if (!callingCodeSurfaceActive) {
|
||||
blockSetNameForDialog.set(Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
blockSetNameForDialog.remove();
|
||||
allowHwForDialog.remove();
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked Dialog.show (skip loading only)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(Dialog.class, "isShowing", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (skippedLoadingDialogs.contains(param.thisObject)) {
|
||||
param.setResult(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked Dialog.isShowing (skipped loading)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Dialog.isShowing hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(Dialog.class, "dismiss", new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String owner = param.thisObject.getClass().getName();
|
||||
if (owner.contains("BottomSelect") || owner.contains("BottomSheet")) {
|
||||
bottomSelectDialogActive = false;
|
||||
}
|
||||
if (isLoadingDialog(owner)) {
|
||||
skippedLoadingDialogs.remove(param.thisObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked Dialog.dismiss (loading cleanup)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Dialog.dismiss hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非区号:清 HW flag + 拦 enableHW(成对,防半开黑屏);
|
||||
* 区号 Compose / BottomSelect:完整 HW(软件绘制 Compose 必黑)。
|
||||
* Dialog.show 窗口期可拦 setName。
|
||||
*/
|
||||
private static void hookHwuiBySurfacePolicy(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
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 (blocked)");
|
||||
XposedBridge.log(TAG + " hooked HardwareRenderer.setName (dialog-only block)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " HardwareRenderer.setName hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> vri = XposedHelpers.findClass("android.view.ViewRootImpl", null);
|
||||
int hooked = 0;
|
||||
for (Method method : vri.getDeclaredMethods()) {
|
||||
if (!"enableHardwareAcceleration".equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (shouldAllowHwSurface()) {
|
||||
return;
|
||||
}
|
||||
param.setResult(null);
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
}
|
||||
XposedBridge.log(TAG + " hooked ViewRootImpl.enableHardwareAcceleration n=" + hooked);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " ViewRootImpl HW hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
XC_MethodHook markCallingCodeLaunch = 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 (launch, HW allowed for Compose)");
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Activity.class, "startActivity", Intent.class, markCallingCodeLaunch);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Activity.class, "startActivity", Intent.class, Bundle.class, markCallingCodeLaunch);
|
||||
} 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 allowed for Compose)");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " execStartActivity 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;
|
||||
}
|
||||
String name = activity.getClass().getName();
|
||||
if (isCallingCodeActivity(name)) {
|
||||
callingCodeSurfaceActive = true;
|
||||
XposedBridge.log(TAG
|
||||
+ " callingCode onCreate BEFORE — keep HW for Compose");
|
||||
// Compose 禁止清 FLAG
|
||||
return;
|
||||
}
|
||||
if (!shouldAllowHwSurface()) {
|
||||
clearHardwareAcceleratedFlag(activity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.args[0];
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
String name = activity.getClass().getName();
|
||||
if (isCallingCodeActivity(name)) {
|
||||
boolean hw = false;
|
||||
try {
|
||||
View decor = activity.getWindow() != null
|
||||
? activity.getWindow().getDecorView()
|
||||
: null;
|
||||
hw = decor != null && decor.isHardwareAccelerated();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
XposedBridge.log(TAG
|
||||
+ " callingCode onCreate AFTER hwAccelerated=" + hw
|
||||
+ " allow=" + shouldAllowHwSurface());
|
||||
return;
|
||||
}
|
||||
if (shouldAllowHwSurface()) {
|
||||
return;
|
||||
}
|
||||
clearHardwareAcceleratedFlag(activity);
|
||||
XposedBridge.log(TAG + " clear HW flag "
|
||||
+ activity.getClass().getSimpleName());
|
||||
}
|
||||
});
|
||||
// Android 16 可能走 PersistableBundle 重载
|
||||
try {
|
||||
Class<?> persistable = Class.forName("android.os.PersistableBundle");
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Instrumentation.class,
|
||||
"callActivityOnCreate",
|
||||
Activity.class,
|
||||
Bundle.class,
|
||||
persistable,
|
||||
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;
|
||||
XposedBridge.log(TAG
|
||||
+ " callingCode onCreate(Persistable) — keep HW");
|
||||
return;
|
||||
}
|
||||
if (!shouldAllowHwSurface()) {
|
||||
clearHardwareAcceleratedFlag(activity);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
if (isCallingCodeActivity(activity.getClass().getName())) {
|
||||
callingCodeSurfaceActive = false;
|
||||
XposedBridge.log(TAG + " callingCode surface OFF (onDestroy)");
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked HWUI surface policy (Compose callingCode=HW)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " HWUI surface policy failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String describeHookArgs(Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return "[]";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
Object arg = args[i];
|
||||
if (arg == null) {
|
||||
sb.append("null");
|
||||
} else if (arg instanceof String) {
|
||||
String text = (String) arg;
|
||||
sb.append("String(").append(text.length() > 120
|
||||
? text.substring(0, 120) + "..." : text).append(")");
|
||||
} else if (arg instanceof byte[]) {
|
||||
sb.append("byte[").append(((byte[]) arg).length).append("]");
|
||||
} else {
|
||||
sb.append(arg.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Promon 隔离 Service;打点确认 :goacqowmmt 进程 hook 已注入。 */
|
||||
@@ -622,6 +1345,9 @@ public final class TngRootBypassHook {
|
||||
XposedBridge.log(TAG + " registration flow active: " + name);
|
||||
} else if (name.endsWith(".SplashActivity")) {
|
||||
registrationFlowActive = false;
|
||||
callingCodeSurfaceActive = false;
|
||||
} else if (name.endsWith(".UserLoginActivity")) {
|
||||
registrationFlowActive = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -908,93 +1634,76 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile long lastSuicideLogAt = 0L;
|
||||
private static volatile int suicideBlockCount = 0;
|
||||
|
||||
private static void hookAntiSuicide() {
|
||||
XC_MethodHook blockSelfKill = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (!shouldBlockThisSuicideCall(param)) {
|
||||
return;
|
||||
}
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
suicideBlockCount++;
|
||||
long now = lastBlockedSuicideAt;
|
||||
if (now - lastSuicideLogAt > 3000L) {
|
||||
lastSuicideLogAt = now;
|
||||
XposedBridge.log(TAG + " blocked suicide #" + suicideBlockCount
|
||||
+ " " + param.method.getDeclaringClass().getSimpleName()
|
||||
+ "#" + param.method.getName()
|
||||
+ argsSummary(param.args)
|
||||
+ " thread=" + Thread.currentThread().getName()
|
||||
+ " stack=" + shortStack(6));
|
||||
}
|
||||
param.setResult(null);
|
||||
// 非主线程:打断自杀循环线程,避免只拦 kill 却卡死 UI
|
||||
freezeSuicideCallerThread();
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Process.class,
|
||||
"killProcess",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (((Integer) param.args[0]) == Process.myPid()) {
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
XposedBridge.log(TAG + " blocked killProcess(self)");
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(Process.class, "killProcess", int.class, blockSelfKill);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
System.class,
|
||||
"exit",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
XposedBridge.log(TAG + " blocked System.exit(" + param.args[0] + ")");
|
||||
param.setResult(null);
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(System.class, "exit", int.class, blockSelfKill);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Runtime.class,
|
||||
"exit",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
XposedBridge.log(TAG + " blocked Runtime.exit(" + param.args[0] + ")");
|
||||
param.setResult(null);
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(Runtime.class, "exit", int.class, blockSelfKill);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Runtime.class,
|
||||
"halt",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
XposedBridge.log(TAG + " blocked Runtime.halt(" + param.args[0] + ")");
|
||||
param.setResult(null);
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(Runtime.class, "halt", int.class, blockSelfKill);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Runtime.halt hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Process.class,
|
||||
"sendSignal",
|
||||
int.class,
|
||||
int.class,
|
||||
Process.class, "sendSignal", int.class, int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
int pid = (Integer) param.args[0];
|
||||
int signal = (Integer) param.args[1];
|
||||
if (pid == Process.myPid() && (signal == 9 || signal == 15)) {
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
XposedBridge.log(TAG + " blocked sendSignal(self, " + signal + ")");
|
||||
param.setResult(null);
|
||||
if (pid != Process.myPid()) {
|
||||
return;
|
||||
}
|
||||
if (signal != 9 && signal != 15 && signal != 6) {
|
||||
return;
|
||||
}
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
suicideBlockCount++;
|
||||
if (lastBlockedSuicideAt - lastSuicideLogAt > 3000L) {
|
||||
lastSuicideLogAt = lastBlockedSuicideAt;
|
||||
XposedBridge.log(TAG + " blocked sendSignal(self," + signal
|
||||
+ ") #" + suicideBlockCount);
|
||||
}
|
||||
param.setResult(null);
|
||||
freezeSuicideCallerThread();
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
@@ -1023,6 +1732,109 @@ public final class TngRootBypassHook {
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " finish hooks failed: " + t.getMessage());
|
||||
}
|
||||
XposedBridge.log(TAG + " anti-suicide armed (rate-limited + freeze caller)");
|
||||
}
|
||||
|
||||
private static boolean shouldBlockThisSuicideCall(XC_MethodHook.MethodHookParam param) {
|
||||
// 真崩溃(KillApplicationHandler)放行,避免僵尸进程卡 Splash/黑屏
|
||||
if (stackHasClass("com.android.internal.os.RuntimeInit$KillApplicationHandler")) {
|
||||
return false;
|
||||
}
|
||||
String name = param.method.getName();
|
||||
if ("killProcess".equals(name)) {
|
||||
return ((Integer) param.args[0]) == Process.myPid();
|
||||
}
|
||||
// System/Runtime.exit/halt:一律拦(TNG 正常退出极少走这里)
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean stackHasClass(String className) {
|
||||
try {
|
||||
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
|
||||
if (className.equals(e.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String argsSummary(Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return "()";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("(");
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(args[i]);
|
||||
}
|
||||
return sb.append(")").toString();
|
||||
}
|
||||
|
||||
private static void freezeSuicideCallerThread() {
|
||||
Thread t = Thread.currentThread();
|
||||
if (t.getId() == Looper.getMainLooper().getThread().getId()) {
|
||||
return;
|
||||
}
|
||||
// 栈里有 Promon 包才打断,避免误伤业务线程
|
||||
if (!stackHasPromon()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
t.interrupt();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean stackHasPromon() {
|
||||
try {
|
||||
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
|
||||
if (isPromonPackageClass(e.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自杀链上游:只补 AppSecurityManager 杀进程路径 noop。
|
||||
* 勿拦 Handler.post / 勿 short-circuit G/H 等:会弄死 ContentProvider 初始化,Splash 僵尸卡住。
|
||||
* bl/R/a 仍由 hookPromonNativeGuard 处理。
|
||||
*/
|
||||
private static void hookPromonSuicideUpstream(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
hookAppSecurityKillPaths(lpparam);
|
||||
XposedBridge.log(TAG + " promon suicide upstream armed (kill-path only)");
|
||||
}
|
||||
|
||||
/** AppSecurityManager 里明确杀进程/退出的路径直接 noop(勿匹配 destroy/shutdown)。 */
|
||||
private static void hookAppSecurityKillPaths(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.common.security.shielding.AppSecurityManager",
|
||||
lpparam.classLoader);
|
||||
int hooked = 0;
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
String lower = method.getName().toLowerCase(Locale.US);
|
||||
if (!lower.contains("kill")
|
||||
&& !lower.contains("exit")
|
||||
&& !lower.contains("force")
|
||||
&& !lower.contains("die")
|
||||
&& !lower.contains("suicide")
|
||||
&& !lower.contains("terminate")) {
|
||||
continue;
|
||||
}
|
||||
hookMethodNoopSilent(method);
|
||||
hooked++;
|
||||
}
|
||||
XposedBridge.log(TAG + " AppSecurityManager kill-path noop n=" + hooked);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " AppSecurityManager kill-path failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSecurityRelatedActivity(Activity activity) {
|
||||
@@ -1230,12 +2042,16 @@ public final class TngRootBypassHook {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Throwable t = (Throwable) param.args[0];
|
||||
if (t == null || !isPromonThrowableName(t.getClass().getName())) {
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " swallowed uncaught " + t.getClass().getSimpleName()
|
||||
+ " in " + lpparam.processName);
|
||||
param.setResult(null);
|
||||
if (isPromonThrowableName(t.getClass().getName())
|
||||
|| isStatsLogNoise(t)) {
|
||||
XposedBridge.log(TAG + " swallowed uncaught " + t.getClass().getSimpleName()
|
||||
+ " in " + lpparam.processName
|
||||
+ " msg=" + t.getMessage());
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked Thread.dispatchUncaughtException");
|
||||
@@ -1379,6 +2195,25 @@ public final class TngRootBypassHook {
|
||||
return simple.length() == 1;
|
||||
}
|
||||
|
||||
private static boolean isStatsLogNoise(Throwable t) {
|
||||
for (Throwable cur = t; cur != null; cur = cur.getCause()) {
|
||||
String name = cur.getClass().getName();
|
||||
String msg = cur.getMessage();
|
||||
if (name != null && name.contains("StatsLog")) {
|
||||
return true;
|
||||
}
|
||||
if (msg != null && msg.contains("StatsLog")) {
|
||||
return true;
|
||||
}
|
||||
if (cur instanceof UnsatisfiedLinkError
|
||||
&& msg != null
|
||||
&& (msg.contains("stats") || msg.contains("Stats"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isPromonPackageClass(String className) {
|
||||
for (String prefix : PROMON_PKG_PREFIXES) {
|
||||
if (className.startsWith(prefix + ".")) {
|
||||
@@ -1587,7 +2422,10 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
}
|
||||
|
||||
/** 短路 TigerTally 启动初始化(native 会 fork getprop 等待 → seccomp 卡死 ANR)。 */
|
||||
/**
|
||||
* TigerTally:init/initCommon + genericNt1 必须短路。
|
||||
* 只短路 genericNt1 而放行 init 会导致 encodeUid 缺失(cloudauth 403)、JNI 损坏、主线程 ABRT 无响应。
|
||||
*/
|
||||
private static boolean isTigerShortCircuit(String className, String methodName) {
|
||||
if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className)
|
||||
&& ("init".equals(methodName) || "initCommon".equals(methodName))) {
|
||||
@@ -2154,8 +2992,8 @@ public final class TngRootBypassHook {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String op = extractRpcOperation(param.args);
|
||||
if (op != null && isRegistrationRpc(op)) {
|
||||
XposedBridge.log(TAG + " RPC reg/login op=" + op + " "
|
||||
if (op != null && (isRegistrationRpc(op) || op.contains("phoneCheck"))) {
|
||||
XposedBridge.log(TAG + " RPC req op=" + op + " "
|
||||
+ describeRpcInvocation(param.args));
|
||||
}
|
||||
if (!isJailBrokenRpcInvocation(param.args)) {
|
||||
@@ -2170,6 +3008,26 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
param.setResult(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String op = extractRpcOperation(param.args);
|
||||
if (op == null || (!isRegistrationRpc(op) && !op.contains("phoneCheck"))) {
|
||||
return;
|
||||
}
|
||||
if (param.hasThrowable()) {
|
||||
XposedBridge.log(TAG + " RPC rsp op=" + op + " err "
|
||||
+ param.getThrowable().getClass().getSimpleName()
|
||||
+ ": " + param.getThrowable().getMessage());
|
||||
return;
|
||||
}
|
||||
Object result = param.getResult();
|
||||
String snippet = result != null ? String.valueOf(result) : "null";
|
||||
if (snippet.length() > 800) {
|
||||
snippet = snippet.substring(0, 800) + "...";
|
||||
}
|
||||
XposedBridge.log(TAG + " RPC rsp op=" + op + " body=" + snippet);
|
||||
}
|
||||
};
|
||||
for (String className : rpcClasses) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user