feat: MariBank 风控 bypass、澳洲银行 Hook 与 reverse 逆向工作区
新增 MariBank/SeaBank PH Root 与 SHPSSDK bypass、riskToken 净化及 Up/Suncorp/ubank 消息 Hook;整理 reverse/ 脚本与 Frida 工具链,并补充当日工作说明文档。
This commit is contained in:
@@ -12,6 +12,12 @@ public final class HookBridge {
|
||||
public static final String SOURCE_XPOSED_SQLITE = "xposed_sqlite";
|
||||
public static final String SOURCE_XPOSED_WECHAT = "xposed_wechat";
|
||||
public static final String SOURCE_XPOSED_TELEGRAM = "xposed_telegram";
|
||||
public static final String SOURCE_XPOSED_UP = "xposed_up";
|
||||
public static final String SOURCE_XPOSED_UP_NOTIFY = "xposed_up_notify";
|
||||
public static final String SOURCE_XPOSED_SUNCORP = "xposed_suncorp";
|
||||
public static final String SOURCE_XPOSED_SUNCORP_NOTIFY = "xposed_suncorp_notify";
|
||||
public static final String SOURCE_XPOSED_UBANK = "xposed_ubank";
|
||||
public static final String SOURCE_XPOSED_UBANK_NOTIFY = "xposed_ubank_notify";
|
||||
|
||||
private HookBridge() {
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package com.miraclegarden.smsmessage.xposed;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.hook.MariBankRootBypassHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.MariBankShpsNativeHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.SuncorpBankMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.SqliteMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.TelegramMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.UpBankMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.UbankMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.WeChatMessageHook;
|
||||
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage;
|
||||
@@ -12,6 +17,10 @@ public class MainHook implements IXposedHookLoadPackage {
|
||||
private static final String WECHAT_PACKAGE = "com.tencent.mm";
|
||||
private static final String TELEGRAM_PACKAGE = "org.telegram.messenger";
|
||||
private static final String TELEGRAM_WEB_PACKAGE = "org.telegram.messenger.web";
|
||||
private static final String UP_BANK_PACKAGE = "au.com.up.money";
|
||||
private static final String SUNCORP_PACKAGE = "au.com.suncorp.marketplace";
|
||||
private static final String UBANK_PACKAGE = "au.com.bank86400";
|
||||
private static final String MARIBANK_PACKAGE = MariBankRootBypassHook.PACKAGE;
|
||||
private static final String MAIN_APP_PACKAGE = "com.miraclegarden.smsmessage";
|
||||
|
||||
@Override
|
||||
@@ -34,6 +43,27 @@ public class MainHook implements IXposedHookLoadPackage {
|
||||
return;
|
||||
}
|
||||
|
||||
if (UP_BANK_PACKAGE.equals(lpparam.packageName)) {
|
||||
UpBankMessageHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SUNCORP_PACKAGE.equals(lpparam.packageName)) {
|
||||
SuncorpBankMessageHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
if (UBANK_PACKAGE.equals(lpparam.packageName)) {
|
||||
UbankMessageHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MARIBANK_PACKAGE.equals(lpparam.packageName)) {
|
||||
MariBankShpsNativeHook.install(lpparam);
|
||||
MariBankRootBypassHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
SqliteMessageHook.install(lpparam);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookForwarder;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
|
||||
/**
|
||||
* 银行 App Hook 公共工具:RemoteMessage / Notification 解析与去重转发。
|
||||
*/
|
||||
public final class BankHookHelper {
|
||||
|
||||
private static final String TAG = "notiMessageHook/Bank";
|
||||
private static final int DEDUP_SIZE = 256;
|
||||
|
||||
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
|
||||
private static final HashSet<String> RECENT_SET = new HashSet<>();
|
||||
|
||||
private BankHookHelper() {
|
||||
}
|
||||
|
||||
public static void hookFcmService(de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String serviceClass, String source) {
|
||||
try {
|
||||
de.robv.android.xposed.XposedHelpers.findAndHookMethod(
|
||||
serviceClass,
|
||||
lpparam.classLoader,
|
||||
"onMessageReceived",
|
||||
"com.google.firebase.messaging.RemoteMessage",
|
||||
new de.robv.android.xposed.XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.args == null || param.args.length == 0 || param.args[0] == null) {
|
||||
return;
|
||||
}
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
CharSequence[] parts = extractRemoteMessage(param.args[0]);
|
||||
forward(context, lpparam.packageName, parts[0], parts[1], source);
|
||||
}
|
||||
}
|
||||
);
|
||||
XposedBridge.log(TAG + " FCM hook installed: " + serviceClass + " (" + lpparam.packageName + ")");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " FCM hook failed " + serviceClass + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void forwardFromNotification(Context context, String packageName,
|
||||
Notification notification, String source) {
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
CharSequence[] parts = extractNotification(notification);
|
||||
forward(context, packageName, parts[0], parts[1], source);
|
||||
}
|
||||
|
||||
private static void forward(Context context, String packageName,
|
||||
CharSequence title, CharSequence content, String source) {
|
||||
String titleStr = title != null ? title.toString().trim() : "";
|
||||
String contentStr = content != null ? content.toString().trim() : "";
|
||||
if (TextUtils.isEmpty(titleStr) && TextUtils.isEmpty(contentStr)) {
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(titleStr)) {
|
||||
titleStr = packageName;
|
||||
}
|
||||
if (TextUtils.isEmpty(contentStr)) {
|
||||
contentStr = titleStr;
|
||||
}
|
||||
String dedupKey = packageName + "|" + titleStr + "|" + contentStr;
|
||||
if (!remember(dedupKey)) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " forward [" + source + "] " + titleStr + " / " + contentStr);
|
||||
HookForwarder.forward(context, packageName, titleStr, contentStr, source);
|
||||
}
|
||||
|
||||
static CharSequence[] extractRemoteMessage(Object remoteMessage) {
|
||||
String title = "";
|
||||
String body = "";
|
||||
try {
|
||||
Object notification = XposedHelpers.callMethod(remoteMessage, "getNotification");
|
||||
if (notification != null) {
|
||||
Object t = XposedHelpers.callMethod(notification, "getTitle");
|
||||
Object b = XposedHelpers.callMethod(notification, "getBody");
|
||||
if (t != null) {
|
||||
title = String.valueOf(t).trim();
|
||||
}
|
||||
if (b != null) {
|
||||
body = String.valueOf(b).trim();
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(body)) {
|
||||
body = stringifyDataMap(remoteMessage);
|
||||
}
|
||||
return new CharSequence[]{title, body};
|
||||
}
|
||||
|
||||
private static String stringifyDataMap(Object remoteMessage) {
|
||||
try {
|
||||
Object dataObj = XposedHelpers.callMethod(remoteMessage, "getData");
|
||||
if (!(dataObj instanceof Map)) {
|
||||
return "";
|
||||
}
|
||||
Map<?, ?> data = (Map<?, ?>) dataObj;
|
||||
if (data.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<?, ?> entry : data.entrySet()) {
|
||||
if (entry.getKey() == null) {
|
||||
continue;
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(entry.getKey()).append('=');
|
||||
if (entry.getValue() != null) {
|
||||
sb.append(entry.getValue());
|
||||
}
|
||||
}
|
||||
return sb.toString().trim();
|
||||
} catch (Throwable ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static CharSequence[] extractNotification(Notification notification) {
|
||||
Bundle extras = notification.extras;
|
||||
if (extras == null) {
|
||||
return new CharSequence[]{"", ""};
|
||||
}
|
||||
CharSequence title = firstNonEmpty(
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE),
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE_BIG),
|
||||
extras.getString(Notification.EXTRA_TITLE)
|
||||
);
|
||||
CharSequence text = firstNonEmpty(
|
||||
extras.getCharSequence(Notification.EXTRA_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT),
|
||||
extras.getString(Notification.EXTRA_TEXT)
|
||||
);
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
text = extras.getString("gcm.n.body");
|
||||
}
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
title = extras.getString("gcm.n.title");
|
||||
}
|
||||
return new CharSequence[]{title, text};
|
||||
}
|
||||
|
||||
private static CharSequence firstNonEmpty(CharSequence... values) {
|
||||
for (CharSequence value : values) {
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static synchronized boolean remember(String key) {
|
||||
if (RECENT_SET.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
RECENT_SET.add(key);
|
||||
RECENT_KEYS.addLast(key);
|
||||
while (RECENT_KEYS.size() > DEDUP_SIZE) {
|
||||
String oldest = RECENT_KEYS.removeFirst();
|
||||
RECENT_SET.remove(oldest);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Context getContext() {
|
||||
try {
|
||||
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
|
||||
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
|
||||
if (app instanceof Context) {
|
||||
return (Context) app;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.app.Notification;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 银行 App 前台兜底:Hook NotificationManager.notify,从 Notification.extras 取标题/正文。
|
||||
*/
|
||||
public final class BankNotificationHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/BankNotify";
|
||||
|
||||
private BankNotificationHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam, String source) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.app.NotificationManager",
|
||||
lpparam.classLoader,
|
||||
"notify",
|
||||
String.class,
|
||||
int.class,
|
||||
Notification.class,
|
||||
new NotifyHook(lpparam.packageName, source)
|
||||
);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.app.NotificationManager",
|
||||
lpparam.classLoader,
|
||||
"notify",
|
||||
int.class,
|
||||
Notification.class,
|
||||
new NotifyHook(lpparam.packageName, source)
|
||||
);
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " install failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NotifyHook extends XC_MethodHook {
|
||||
private final String packageName;
|
||||
private final String source;
|
||||
|
||||
NotifyHook(String packageName, String source) {
|
||||
this.packageName = packageName;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.args == null || param.args.length == 0) {
|
||||
return;
|
||||
}
|
||||
Notification notification = null;
|
||||
for (Object arg : param.args) {
|
||||
if (arg instanceof Notification) {
|
||||
notification = (Notification) arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
android.content.Context context = BankHookHelper.getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
BankHookHelper.forwardFromNotification(context, packageName, notification, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
/**
|
||||
* SHPSSDK riskToken 净化:尾部 |09|1 → |00|0(Root+Emulator+Hook 标记)。
|
||||
*/
|
||||
final class MariBankRiskTokenUtil {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankRoot";
|
||||
private static final Pattern RISK_TOKEN_JSON = Pattern.compile(
|
||||
"\"riskToken\"\\s*:\\s*\"([^\"]+)\"");
|
||||
private static final Pattern DEVICE_TOKEN_JSON = Pattern.compile(
|
||||
"\"deviceToken\"\\s*:\\s*\"([^\"]+)\"");
|
||||
private static final Pattern RISK_TOKEN_BODY = Pattern.compile(
|
||||
"([A-Za-z0-9+/=]{8,}\\|[A-Za-z0-9+/=_-]{8,}\\|[A-Za-z0-9+/=_-]{3,}\\|)\\d+(\\|\\d+)");
|
||||
|
||||
private MariBankRiskTokenUtil() {
|
||||
}
|
||||
|
||||
static String sanitizeRiskToken(String token) {
|
||||
if (token == null || token.isEmpty() || !token.contains("|")) {
|
||||
return token;
|
||||
}
|
||||
if (!RISK_TOKEN_BODY.matcher(token).find() && !token.matches(".*\\|\\d+\\|\\d+$")) {
|
||||
return token;
|
||||
}
|
||||
int secondLast = token.lastIndexOf('|');
|
||||
if (secondLast <= 0) {
|
||||
return token;
|
||||
}
|
||||
secondLast = token.lastIndexOf('|', secondLast - 1);
|
||||
if (secondLast <= 0) {
|
||||
return token;
|
||||
}
|
||||
String oldTail = token.substring(secondLast + 1);
|
||||
String neu = token.substring(0, secondLast) + "|00|0";
|
||||
XposedBridge.log(TAG + " sanitized riskToken tail " + oldTail + " -> 00|0");
|
||||
return neu;
|
||||
}
|
||||
|
||||
static String sanitizeAllInText(String text) {
|
||||
if (text == null || !text.contains("|")) {
|
||||
return text;
|
||||
}
|
||||
Matcher m = RISK_TOKEN_BODY.matcher(text);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
boolean changed = false;
|
||||
while (m.find()) {
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(m.group(1) + "00|0"));
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
String out = sanitizeJsonField(text, RISK_TOKEN_JSON);
|
||||
out = sanitizeJsonField(out, DEVICE_TOKEN_JSON);
|
||||
return out;
|
||||
}
|
||||
m.appendTail(sb);
|
||||
XposedBridge.log(TAG + " sanitized riskToken in text len=" + text.length());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static byte[] sanitizeBytes(byte[] data, int offset, int length) {
|
||||
if (data == null || length <= 0) {
|
||||
return data;
|
||||
}
|
||||
String text = new String(data, offset, length, StandardCharsets.UTF_8);
|
||||
if (!text.contains("|")) {
|
||||
return data;
|
||||
}
|
||||
String sanitized = sanitizeAllInText(text);
|
||||
return sanitized.equals(text) ? data : sanitized.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String sanitizeJsonField(String text, Pattern fieldPattern) {
|
||||
Matcher json = fieldPattern.matcher(text);
|
||||
if (!json.find()) {
|
||||
return text;
|
||||
}
|
||||
String oldToken = json.group(1);
|
||||
String newToken = sanitizeRiskToken(oldToken);
|
||||
return oldToken.equals(newToken) ? text : text.replace(oldToken, newToken);
|
||||
}
|
||||
|
||||
static String tail(String token) {
|
||||
if (token == null || !token.contains("|")) {
|
||||
return "n/a";
|
||||
}
|
||||
int last = token.lastIndexOf('|');
|
||||
int second = token.lastIndexOf('|', last - 1);
|
||||
if (second < 0) {
|
||||
return "n/a";
|
||||
}
|
||||
return token.substring(second + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1738 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Process;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
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 PH(ph.seabank.seabank)Root 检测绕过。
|
||||
* 逆向:SafeMode SDK + SHPSSDK;检测到 Root 后会 Toast 并 Process.killProcess 自杀。
|
||||
*/
|
||||
public final class MariBankRootBypassHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankRoot";
|
||||
public static final String PACKAGE = "ph.seabank.seabank";
|
||||
|
||||
/** 服务端注册被拒错误码(logcat 实测)。 */
|
||||
private static final int ERROR_CODE_SECURITY_BLOCKED = 4067004;
|
||||
private static final int ERROR_CODE_SECURITY_BLOCKED_ALT = 4067012;
|
||||
|
||||
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
|
||||
|
||||
/** killProcess 被拦截后的宽限期:此期间阻止 finish 造成「假闪退」。 */
|
||||
private static volatile long lastBlockedSuicideAt = 0L;
|
||||
private static final long SOFT_CRASH_GUARD_MS = 8000L;
|
||||
|
||||
private static volatile int finishBurstCount = 0;
|
||||
private static volatile long finishBurstStartMs = 0L;
|
||||
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",
|
||||
"com.shopee.bke.biz.user.rn.helper.ErrorFlowHelper",
|
||||
};
|
||||
|
||||
private MariBankRootBypassHook() {
|
||||
}
|
||||
|
||||
private static volatile boolean deferredHooksInstalled = false;
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (!PACKAGE.equals(lpparam.packageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
hookAntiSuicide(lpparam);
|
||||
hookAntiSoftCrash(lpparam);
|
||||
scheduleAppHooks(lpparam);
|
||||
}
|
||||
|
||||
/**
|
||||
* 必须在 BkeApplication.attachBaseContext 完成之后安装:
|
||||
* loadPackage 时 ClassLoader 未绑定 split APK,过早 Hook SHPSSDK 会导致 libsdkutils.so 死循环白屏。
|
||||
*/
|
||||
private static void scheduleAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook afterAttach = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
installDeferredHooks(lpparam);
|
||||
MariBankShpsNativeHook.installDeferred(lpparam);
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.shopee.bke.digitalbank.BkeApplication",
|
||||
lpparam.classLoader,
|
||||
"attachBaseContext",
|
||||
"android.content.Context",
|
||||
afterAttach);
|
||||
XposedBridge.log(TAG + " waiting attachBaseContext for app hooks");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " attachBaseContext hook failed, install now: " + t.getMessage());
|
||||
installDeferredHooks(lpparam);
|
||||
MariBankShpsNativeHook.installDeferred(lpparam);
|
||||
}
|
||||
}
|
||||
|
||||
private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (deferredHooksInstalled) {
|
||||
return;
|
||||
}
|
||||
deferredHooksInstalled = true;
|
||||
|
||||
int hooked = 0;
|
||||
for (String className : SAFE_MODE_CLASSES) {
|
||||
hooked += hookAllBooleanChecks(lpparam, className);
|
||||
}
|
||||
|
||||
hookSafeModeDialog(lpparam);
|
||||
hookRootDialogBlock(lpparam);
|
||||
hookErrorFlowLogging(lpparam);
|
||||
hooked += hookShpsRisk(lpparam);
|
||||
hookShpsToken(lpparam);
|
||||
hookNetworkLogging(lpparam);
|
||||
|
||||
XposedBridge.log(TAG + " app hooks installed, booleanHooks=" + hooked);
|
||||
}
|
||||
|
||||
/** SafeMode 类方法名被混淆,Hook 所有返回 boolean/int 的实例方法。 */
|
||||
private static int hookAllBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType != boolean.class && returnType != Boolean.class
|
||||
&& returnType != int.class && returnType != Integer.class) {
|
||||
continue;
|
||||
}
|
||||
if (method.getParameterTypes().length > 2) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (returnType == boolean.class || returnType == Boolean.class) {
|
||||
param.setResult(false);
|
||||
} else {
|
||||
param.setResult(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + count + " checks in " + className);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** 阻止检测到 Root 后 Process.killProcess / System.exit 自杀。 */
|
||||
private static void hookAntiSuicide(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Process.class,
|
||||
"killProcess",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
int pid = (Integer) param.args[0];
|
||||
if (pid == Process.myPid()) {
|
||||
lastBlockedSuicideAt = System.currentTimeMillis();
|
||||
XposedBridge.log(TAG + " blocked killProcess(self)");
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} 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.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} 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.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " sendSignal hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 阻止 killProcess 失败后通过 finish / finishAffinity 把界面关掉(用户感知为闪退,进程其实还在)。 */
|
||||
private static void hookAntiSoftCrash(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook blockFinishHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.thisObject;
|
||||
String methodName = param.method.getName();
|
||||
if (!shouldBlockFinish(activity, methodName)) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " blocked " + methodName
|
||||
+ " after suicide attempt: " + activity.getClass().getSimpleName());
|
||||
param.setResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(Activity.class, "finish", blockFinishHook);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " finish hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(Activity.class, "finishAfterTransition", blockFinishHook);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " finishAfterTransition hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Activity.class,
|
||||
"finishAffinity",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.thisObject;
|
||||
if (!shouldBlockFinish(activity, "finishAffinity")) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " blocked finishAffinity after suicide attempt: "
|
||||
+ activity.getClass().getSimpleName());
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " finishAffinity hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Activity.class,
|
||||
"finishAndRemoveTask",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.thisObject;
|
||||
if (!shouldBlockFinish(activity, "finishAndRemoveTask")) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " blocked finishAndRemoveTask after suicide attempt: "
|
||||
+ activity.getClass().getSimpleName());
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " finishAndRemoveTask hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(Activity.class, "moveTaskToBack", boolean.class, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.thisObject;
|
||||
if (!shouldBlockFinish(activity, "moveTaskToBack")) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " blocked moveTaskToBack after suicide attempt");
|
||||
param.setResult(null);
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " moveTaskToBack hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldBlockSoftCrash() {
|
||||
return System.currentTimeMillis() - lastBlockedSuicideAt < SOFT_CRASH_GUARD_MS;
|
||||
}
|
||||
|
||||
private static boolean isBkeActivity(Activity activity) {
|
||||
String name = activity.getClass().getName();
|
||||
return name.startsWith("com.shopee.bke") || name.startsWith("com.shopee.bke.digitalbank");
|
||||
}
|
||||
|
||||
private static boolean isUserBackNavigation() {
|
||||
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
|
||||
String method = frame.getMethodName();
|
||||
if ("onBackPressed".equals(method) || "onBackInvoked".equals(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void trackFinishBurst() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - finishBurstStartMs > FINISH_BURST_WINDOW_MS) {
|
||||
finishBurstCount = 0;
|
||||
finishBurstStartMs = now;
|
||||
}
|
||||
finishBurstCount++;
|
||||
if (finishBurstCount >= FINISH_BURST_THRESHOLD) {
|
||||
lastBlockedSuicideAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* finish / finishAffinity 往往先于 killProcess;
|
||||
* finishAffinity 为 Root 检测自杀常用路径,对 bke Activity 直接拦截(保留返回键)。
|
||||
*/
|
||||
private static boolean shouldBlockFinish(Activity activity, String methodName) {
|
||||
String name = activity.getClass().getName();
|
||||
if (name.contains("SafeModeRecoverActivity")) {
|
||||
return false;
|
||||
}
|
||||
if (isUserBackNavigation()) {
|
||||
return false;
|
||||
}
|
||||
if (!isBkeActivity(activity)) {
|
||||
return false;
|
||||
}
|
||||
if ("finishAffinity".equals(methodName) || "finishAndRemoveTask".equals(methodName)) {
|
||||
return true;
|
||||
}
|
||||
trackFinishBurst();
|
||||
if (shouldBlockSoftCrash()) {
|
||||
return true;
|
||||
}
|
||||
if (finishBurstCount >= FINISH_BURST_THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
return isRiskRelatedStackTrace();
|
||||
}
|
||||
|
||||
private static boolean isRiskRelatedStackTrace() {
|
||||
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
|
||||
String cn = frame.getClassName();
|
||||
if (cn.contains("safemode")
|
||||
|| cn.contains("shpssdk")
|
||||
|| cn.contains("com.shopee.bke")
|
||||
|| cn.contains("bke.biz.base.risk")
|
||||
|| cn.contains("errorcodehandler")
|
||||
|| cn.contains("ErrorFlow")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 记录注册/OTP 错误码路径,便于 logcat 定位 -1201 来源。 */
|
||||
private static void hookErrorFlowLogging(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
for (String className : ERROR_FLOW_CLASSES) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (param.args == null || param.args.length == 0) {
|
||||
return;
|
||||
}
|
||||
String args = Arrays.toString(param.args);
|
||||
if (args.contains("1201") || args.contains("-1201")
|
||||
|| args.toLowerCase().contains("error")) {
|
||||
XposedBridge.log(TAG + " " + className + "."
|
||||
+ method.getName() + " args=" + args);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip error flow " + className + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType != boolean.class && returnType != Boolean.class) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
param.setResult(false);
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
XposedBridge.log(TAG + " hooked SHPS boolean checks in " + className);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip SHPS " + className + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* SHPSSDK 风控 token:清空本地 risk 列表,避免 Root/Hook 标记写入 token 上报服务端。
|
||||
* 逆向:getRiskSync / getRiskAsync / assessRisk → List<SPSAssessRisk>,RISK_ROOT=1, RISK_HOOK=4 ...
|
||||
*/
|
||||
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) {
|
||||
hookEmptyRiskList(lpparam, className, "getRiskSync", contextClass);
|
||||
hookEmptyRiskList(lpparam, className, "getExtRiskSync", contextClass);
|
||||
hookRiskAsyncCallback(lpparam, className, "getRiskAsync", contextClass,
|
||||
className.contains("bank")
|
||||
? "com.shopee.shpssdkbank.SPSResultCallback"
|
||||
: "com.shopee.shpssdk.SPSResultCallback");
|
||||
hookRiskAsyncCallback(lpparam, className, "getExtRiskAsync", contextClass,
|
||||
className.contains("bank")
|
||||
? "com.shopee.shpssdkbank.SPSExtResultCallback"
|
||||
: "com.shopee.shpssdk.SPSExtResultCallback");
|
||||
hookRiskTokenAsync(lpparam, className, contextClass);
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
"getRiskToken",
|
||||
contextClass,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object token = param.getResult();
|
||||
if (token instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " getRiskToken len="
|
||||
+ sanitized.length() + " tail="
|
||||
+ MariBankRiskTokenUtil.tail(sanitized));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + className + ".getRiskToken: " + t.getMessage());
|
||||
}
|
||||
hookTokenStringMethod(lpparam, className, "getLongToken");
|
||||
hookTokenStringMethod(lpparam, className, "getShortToken");
|
||||
hookShpsSecData(lpparam, className, contextClass);
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
"getSoftToken",
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object token = param.getResult();
|
||||
if (token instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
|
||||
param.setResult(sanitized);
|
||||
logTokenResult("getSoftToken", sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + className + ".getSoftToken: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.shopee.shpssdkbank.SHPSSDK",
|
||||
lpparam.classLoader,
|
||||
"assessRisk",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
param.setResult(new ArrayList<>());
|
||||
XposedBridge.log(TAG + " assessRisk -> empty");
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
String[] assessRiskClasses = {
|
||||
"com.shopee.shpssdkbank.SPSAssessRisk",
|
||||
"com.shopee.shpssdk.SPSAssessRisk",
|
||||
};
|
||||
for (String className : assessRiskClasses) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
"getType",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
param.setResult(0);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
String[] callbackAdapters = {
|
||||
"com.shopee.shpssdk.SPSCallbackAdapter",
|
||||
"com.shopee.shpssdkbank.SPSCallbackAdapter",
|
||||
};
|
||||
for (String className : callbackAdapters) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
"onGetRiskTokenFail",
|
||||
int.class,
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedBridge.log(TAG + " suppressed onGetRiskTokenFail: " + param.args[1]);
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
hookShpsTokenCore(lpparam);
|
||||
}
|
||||
|
||||
/**
|
||||
* classes11 真实 token 生成链(早于 SHPSSDK 门面):
|
||||
* getRiskToken → vvuuuuvvv.wwvuwuwvu(Context)
|
||||
* getRiskSync → vvuuuuvvv.uuuuuuwvw(Context)
|
||||
*/
|
||||
private static void hookShpsTokenCore(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
final String coreClass = "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv";
|
||||
final String contextClass = "android.content.Context";
|
||||
int hooked = 0;
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
coreClass,
|
||||
lpparam.classLoader,
|
||||
"wwvuwuwvu",
|
||||
contextClass,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object token = param.getResult();
|
||||
if (token instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " core.wwvuwuwvu len="
|
||||
+ sanitized.length() + " tail="
|
||||
+ MariBankRiskTokenUtil.tail(sanitized));
|
||||
}
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip core.wwvuwuwvu: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
coreClass,
|
||||
lpparam.classLoader,
|
||||
"uuuuuuwvw",
|
||||
contextClass,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
param.setResult(new ArrayList<>());
|
||||
XposedBridge.log(TAG + " core.uuuuuuwvw -> empty");
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip core.uuuuuuwvw: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
coreClass,
|
||||
lpparam.classLoader,
|
||||
"wwwuvwwuu",
|
||||
contextClass,
|
||||
String.class,
|
||||
boolean.class,
|
||||
boolean.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (result instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText((String) result);
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " core.wwwuvwwuu len=" + sanitized.length());
|
||||
}
|
||||
}
|
||||
});
|
||||
hooked++;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip core.wwwuvwwuu: " + t.getMessage());
|
||||
}
|
||||
|
||||
if (hooked > 0) {
|
||||
XposedBridge.log(TAG + " shps token core hooks=" + hooked);
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookEmptyRiskList(
|
||||
XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String className,
|
||||
String methodName,
|
||||
String contextClassName) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
methodName,
|
||||
contextClassName,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
param.setResult(new ArrayList<>());
|
||||
XposedBridge.log(TAG + " " + methodName + " -> empty");
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookRiskAsyncCallback(
|
||||
XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String sdkClass,
|
||||
String methodName,
|
||||
String contextClassName,
|
||||
String callbackClassName) {
|
||||
try {
|
||||
Class<?> callbackClass = XposedHelpers.findClass(callbackClassName, lpparam.classLoader);
|
||||
XposedHelpers.findAndHookMethod(sdkClass, lpparam.classLoader, methodName,
|
||||
contextClassName, callbackClass, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Object original = param.args[1];
|
||||
if (original == null) {
|
||||
return;
|
||||
}
|
||||
param.args[1] = wrapRiskCallback(lpparam.classLoader, callbackClass, original);
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip async " + sdkClass + "." + methodName + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Object wrapRiskCallback(
|
||||
ClassLoader loader,
|
||||
Class<?> callbackClass,
|
||||
Object original) {
|
||||
return Proxy.newProxyInstance(loader, new Class[]{callbackClass}, new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if ("result".equals(method.getName()) && args != null && args.length > 0
|
||||
&& args[0] instanceof List) {
|
||||
List<?> list = (List<?>) args[0];
|
||||
XposedBridge.log(TAG + " async risk callback cleared size=" + list.size());
|
||||
args[0] = new ArrayList<>();
|
||||
}
|
||||
return method.invoke(original, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void hookRiskTokenAsync(
|
||||
XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String sdkClass,
|
||||
String contextClassName) {
|
||||
String callbackClassName = sdkClass.contains("bank")
|
||||
? "com.shopee.shpssdkbank.SPSRiskTokenCallback"
|
||||
: "com.shopee.shpssdk.SPSRiskTokenCallback";
|
||||
try {
|
||||
Class<?> callbackClass = XposedHelpers.findClass(callbackClassName, lpparam.classLoader);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
sdkClass,
|
||||
lpparam.classLoader,
|
||||
"getRiskTokenAsync",
|
||||
contextClassName,
|
||||
callbackClass,
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Object original = param.args[1];
|
||||
if (original == null) {
|
||||
return;
|
||||
}
|
||||
param.args[1] = wrapRiskTokenCallback(
|
||||
lpparam.classLoader, callbackClass, original);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + sdkClass + ".getRiskTokenAsync: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Object wrapRiskTokenCallback(
|
||||
ClassLoader loader,
|
||||
Class<?> callbackClass,
|
||||
Object original) {
|
||||
return Proxy.newProxyInstance(loader, new Class[]{callbackClass}, new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if ("onResult".equals(method.getName()) && args != null && args.length > 0) {
|
||||
if (args[0] instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) args[0]);
|
||||
XposedBridge.log(TAG + " getRiskTokenAsync onResult len="
|
||||
+ sanitized.length() + " tail="
|
||||
+ MariBankRiskTokenUtil.tail(sanitized));
|
||||
args[0] = sanitized;
|
||||
}
|
||||
}
|
||||
return method.invoke(original, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void hookShpsSecData(
|
||||
XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String className,
|
||||
String contextClass) {
|
||||
String[][] methods = {
|
||||
{"getSHPSECData", contextClass, "java.lang.String", "boolean"},
|
||||
{"getSHPSECAllData", contextClass, "java.lang.String", "boolean"},
|
||||
};
|
||||
for (String[] sig : methods) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
sig[0],
|
||||
sig[1],
|
||||
sig[2],
|
||||
"boolean",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.getResult() instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(
|
||||
(String) param.getResult());
|
||||
param.setResult(sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookTokenStringMethod(
|
||||
XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String className,
|
||||
String methodName) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
methodName,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object token = param.getResult();
|
||||
if (token instanceof String) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
|
||||
param.setResult(sanitized);
|
||||
logTokenResult(methodName, sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void logTokenResult(String methodName, Object token) {
|
||||
if (token instanceof String) {
|
||||
String s = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
|
||||
XposedBridge.log(TAG + " " + methodName + " len=" + s.length()
|
||||
+ " tail=" + MariBankRiskTokenUtil.tail(s));
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录注册 API 响应,净化 riskToken JSON,定位 4067004 来源 URL。 */
|
||||
private static void hookNetworkLogging(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
// 勿 Hook RealInterceptorChain.proceed — libshpssdk.so 字符串硬编码检测该 Hook。
|
||||
hookRequestBuilderBuild(lpparam);
|
||||
hookRequestBuilderBody(lpparam);
|
||||
hookOkHttpNewCall(lpparam);
|
||||
hookRealCallExecute(lpparam);
|
||||
hookGsonFromJson(lpparam);
|
||||
hookRequestBodyWriteTo(lpparam);
|
||||
hookOkHttpResponseUrl(lpparam);
|
||||
hookOkioBufferWrite(lpparam);
|
||||
hookOutgoingRequestBody(lpparam);
|
||||
hookOutgoingRequestBytes(lpparam);
|
||||
hookJsonRiskTokenPut(lpparam);
|
||||
hookGsonRiskToken(lpparam);
|
||||
hookRetrofitGsonConverter(lpparam);
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.ResponseBody",
|
||||
lpparam.classLoader,
|
||||
"string",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String body = (String) param.getResult();
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
|
||||
if (!sanitized.equals(body)) {
|
||||
param.setResult(sanitized);
|
||||
body = sanitized;
|
||||
}
|
||||
String lower = body.toLowerCase();
|
||||
if (lower.contains("blocked")
|
||||
|| body.contains(String.valueOf(ERROR_CODE_SECURITY_BLOCKED))
|
||||
|| body.contains(String.valueOf(ERROR_CODE_SECURITY_BLOCKED_ALT))
|
||||
|| lower.contains("risktoken")
|
||||
|| lower.contains("\"code\"")) {
|
||||
String url = CURRENT_REQUEST_URL.get();
|
||||
String snippet = body.length() > 600
|
||||
? body.substring(0, 600) + "..." : body;
|
||||
XposedBridge.log(TAG + " HTTP"
|
||||
+ (url != null ? " " + url : "")
|
||||
+ " body: " + snippet);
|
||||
}
|
||||
CURRENT_REQUEST_URL.remove();
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip okhttp ResponseBody.string: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 出站 Request 构建时记录 URL,并在 post/put 阶段净化 body。 */
|
||||
private static void hookRequestBuilderBuild(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.Request$Builder",
|
||||
lpparam.classLoader,
|
||||
"build",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object url = XposedHelpers.callMethod(param.getResult(), "url");
|
||||
if (url != null) {
|
||||
String urlStr = String.valueOf(url);
|
||||
CURRENT_REQUEST_URL.set(urlStr);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip Request.Builder.build: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 在 RequestBody 挂到 Request 时净化(比抽象 writeTo Hook 更可靠)。 */
|
||||
private static void hookRequestBuilderBody(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook bodyHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
for (int i = 0; i < param.args.length; i++) {
|
||||
Object arg = param.args[i];
|
||||
if (arg == null || !isRequestBody(lpparam.classLoader, arg)) {
|
||||
continue;
|
||||
}
|
||||
Object sanitized = sanitizeRequestBody(lpparam.classLoader, arg);
|
||||
if (sanitized != arg) {
|
||||
param.args[i] = sanitized;
|
||||
XposedBridge.log(TAG + " Request.Builder body riskToken sanitized");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
String[][] sigs = {
|
||||
{"post", "okhttp3.RequestBody"},
|
||||
{"put", "okhttp3.RequestBody"},
|
||||
{"patch", "okhttp3.RequestBody"},
|
||||
};
|
||||
for (String[] sig : sigs) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.Request$Builder",
|
||||
lpparam.classLoader,
|
||||
sig[0],
|
||||
sig[1],
|
||||
bodyHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.Request$Builder",
|
||||
lpparam.classLoader,
|
||||
"method",
|
||||
"java.lang.String",
|
||||
"okhttp3.RequestBody",
|
||||
bodyHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookOkHttpNewCall(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook callHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object sanitized = sanitizeOkHttpRequest(lpparam.classLoader, param.args[0]);
|
||||
if (sanitized != param.args[0]) {
|
||||
param.args[0] = sanitized;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.OkHttpClient",
|
||||
lpparam.classLoader,
|
||||
"newCall",
|
||||
"okhttp3.Request",
|
||||
callHook);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip OkHttpClient.newCall: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrofit 异步/同步最终走 RealCall.execute/enqueue。 */
|
||||
private static void hookRealCallExecute(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook execHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object request = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
Object sanitized = sanitizeOkHttpRequest(lpparam.classLoader, request);
|
||||
if (sanitized != request) {
|
||||
XposedHelpers.setObjectField(param.thisObject, "originalRequest", sanitized);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
};
|
||||
for (String className : new String[]{"okhttp3.RealCall", "okhttp3.internal.connection.RealCall"}) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "execute", execHook);
|
||||
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "enqueue",
|
||||
"okhttp3.Callback", execHook);
|
||||
XposedBridge.log(TAG + " hooked " + className);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Object sanitizeOkHttpRequest(ClassLoader loader, Object request) {
|
||||
try {
|
||||
Object url = XposedHelpers.callMethod(request, "url");
|
||||
if (url == null) {
|
||||
return request;
|
||||
}
|
||||
String urlStr = String.valueOf(url);
|
||||
CURRENT_REQUEST_URL.set(urlStr);
|
||||
|
||||
Object body = XposedHelpers.callMethod(request, "body");
|
||||
if (body == null) {
|
||||
if (urlStr.contains("/register")) {
|
||||
XposedBridge.log(TAG + " register request has null body");
|
||||
}
|
||||
return request;
|
||||
}
|
||||
String content = readRequestBodyText(loader, body);
|
||||
if (content.isEmpty()) {
|
||||
if (urlStr.contains("/register")) {
|
||||
XposedBridge.log(TAG + " register request body unreadable (encrypted or one-shot)");
|
||||
}
|
||||
return request;
|
||||
}
|
||||
if (urlStr.contains("/register")) {
|
||||
int show = Math.min(content.length(), 500);
|
||||
XposedBridge.log(TAG + " outbound register body: "
|
||||
+ content.substring(0, show)
|
||||
+ (content.length() > show ? "..." : ""));
|
||||
}
|
||||
Object sanitizedBody = sanitizeRequestBody(loader, body);
|
||||
if (sanitizedBody == body) {
|
||||
return request;
|
||||
}
|
||||
String method = (String) XposedHelpers.callMethod(request, "method");
|
||||
Object builder = XposedHelpers.callMethod(request, "newBuilder");
|
||||
XposedHelpers.callMethod(builder, "method", method, sanitizedBody);
|
||||
Object newRequest = XposedHelpers.callMethod(builder, "build");
|
||||
XposedBridge.log(TAG + " sanitized outbound body for " + urlStr);
|
||||
return newRequest;
|
||||
} catch (Throwable t) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
/** dfp 响应若走 Gson.fromJson(String) 而非 ResponseBody.string,需净化入参。 */
|
||||
private static void hookGsonFromJson(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook fromJsonHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (!(param.args[0] instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String json = (String) param.args[0];
|
||||
if (!json.contains("riskToken") && !json.contains("deviceToken")) {
|
||||
return;
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(json);
|
||||
if (!sanitized.equals(json)) {
|
||||
param.args[0] = sanitized;
|
||||
XposedBridge.log(TAG + " Gson.fromJson riskToken sanitized");
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.google.gson.Gson",
|
||||
lpparam.classLoader,
|
||||
"fromJson",
|
||||
String.class,
|
||||
Class.class,
|
||||
fromJsonHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.google.gson.Gson",
|
||||
lpparam.classLoader,
|
||||
"fromJson",
|
||||
String.class,
|
||||
"java.lang.reflect.Type",
|
||||
fromJsonHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRequestBody(ClassLoader loader, Object obj) {
|
||||
try {
|
||||
Class<?> rb = XposedHelpers.findClass("okhttp3.RequestBody", loader);
|
||||
return rb.isInstance(obj);
|
||||
} catch (Throwable t) {
|
||||
return obj.getClass().getName().contains("RequestBody");
|
||||
}
|
||||
}
|
||||
|
||||
private static Object sanitizeRequestBody(ClassLoader loader, Object body) {
|
||||
try {
|
||||
ClassLoader effective = loaderFor(body, loader);
|
||||
String content = readRequestBodyText(effective, body);
|
||||
if (content.isEmpty()) {
|
||||
return body;
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
|
||||
if (sanitized.equals(content)) {
|
||||
return body;
|
||||
}
|
||||
Object mediaType = XposedHelpers.callMethod(body, "contentType");
|
||||
Class<?> rbClass = findClassSafe(effective, "okhttp3.RequestBody");
|
||||
return XposedHelpers.callStaticMethod(rbClass, "create", mediaType, sanitized);
|
||||
} catch (Throwable t) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readRequestBodyText(ClassLoader loader, Object body) {
|
||||
try {
|
||||
ClassLoader effective = loaderFor(body, loader);
|
||||
Class<?> bufferClass = findClassSafe(effective, "okio.Buffer");
|
||||
Object buffer = XposedHelpers.newInstance(bufferClass);
|
||||
XposedHelpers.callMethod(body, "writeTo", buffer);
|
||||
return (String) XposedHelpers.callMethod(buffer, "readUtf8");
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static ClassLoader loaderFor(Object obj, ClassLoader fallback) {
|
||||
if (obj != null) {
|
||||
ClassLoader cl = obj.getClass().getClassLoader();
|
||||
if (cl != null) {
|
||||
return cl;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static Class<?> findClassSafe(ClassLoader loader, String name) {
|
||||
try {
|
||||
return XposedHelpers.findClass(name, loader);
|
||||
} catch (Throwable first) {
|
||||
ClassLoader ctx = Thread.currentThread().getContextClassLoader();
|
||||
if (ctx != null && ctx != loader) {
|
||||
return XposedHelpers.findClass(name, ctx);
|
||||
}
|
||||
throw first;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截 RequestBody 写入:注册 JSON 只写一次,create/Buffer Hook 可能漏掉。
|
||||
* 读出 body → 净化 riskToken → 写入 sink,跳过原方法。
|
||||
*/
|
||||
private static void hookRequestBodyWriteTo(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> rbClass = findClassSafe(lpparam.classLoader, "okhttp3.RequestBody");
|
||||
Class<?> sinkClass = findClassSafe(lpparam.classLoader, "okio.BufferedSink");
|
||||
XposedHelpers.findAndHookMethod(
|
||||
rbClass,
|
||||
"writeTo",
|
||||
sinkClass,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
|
||||
ClassLoader cl = loaderFor(param.thisObject, lpparam.classLoader);
|
||||
Class<?> bufferClass = findClassSafe(cl, "okio.Buffer");
|
||||
Object buffer = XposedHelpers.newInstance(bufferClass);
|
||||
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 (url != null && url.contains("/register") && !content.isEmpty()) {
|
||||
int show = Math.min(content.length(), 400);
|
||||
XposedBridge.log(TAG + " register body(raw): "
|
||||
+ content.substring(0, show)
|
||||
+ (content.length() > show ? "..." : ""));
|
||||
}
|
||||
if (!sanitized.equals(content)) {
|
||||
XposedBridge.log(TAG + " RequestBody.writeTo riskToken sanitized");
|
||||
}
|
||||
XposedHelpers.callMethod(param.args[0], "writeUtf8", sanitized);
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip RequestBody.writeTo: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookRetrofitGsonConverter(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"retrofit2.converter.gson.GsonRequestBodyConverter",
|
||||
lpparam.classLoader,
|
||||
"convert",
|
||||
Object.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object body = param.getResult();
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ClassLoader cl = loaderFor(body, lpparam.classLoader);
|
||||
Object buffer = XposedHelpers.newInstance(
|
||||
findClassSafe(cl, "okio.Buffer"));
|
||||
XposedHelpers.callMethod(body, "writeTo", buffer);
|
||||
String content = (String) XposedHelpers.callMethod(buffer, "readUtf8");
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
|
||||
if (!sanitized.equals(content)) {
|
||||
Object mediaType = XposedHelpers.callMethod(body, "contentType");
|
||||
param.setResult(XposedHelpers.callStaticMethod(
|
||||
XposedHelpers.findClass("okhttp3.RequestBody", lpparam.classLoader),
|
||||
"create",
|
||||
mediaType,
|
||||
sanitized));
|
||||
XposedBridge.log(TAG + " GsonRequestBodyConverter riskToken sanitized");
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip GsonRequestBodyConverter: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 出站 body 写入 okio.Buffer 时净化 riskToken(不 Hook proceed)。 */
|
||||
private static void hookOkioBufferWrite(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook sanitizeHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (param.args.length == 0 || !(param.args[0] instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String s = (String) param.args[0];
|
||||
if (!s.contains("|")) {
|
||||
return;
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(s);
|
||||
if (!sanitized.equals(s)) {
|
||||
param.args[0] = sanitized;
|
||||
XposedBridge.log(TAG + " okio.Buffer write riskToken sanitized");
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
|
||||
XposedHelpers.findAndHookMethod(
|
||||
bufferClass, "writeUtf8", String.class, sanitizeHook);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip okio.Buffer.writeUtf8: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
|
||||
XposedHelpers.findAndHookMethod(
|
||||
bufferClass, "writeString",
|
||||
String.class, java.nio.charset.Charset.class, sanitizeHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
|
||||
XposedHelpers.findAndHookMethod(
|
||||
bufferClass, "write",
|
||||
byte[].class, int.class, int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
byte[] data = (byte[]) param.args[0];
|
||||
int off = (int) param.args[1];
|
||||
int len = (int) param.args[2];
|
||||
byte[] sanitized = MariBankRiskTokenUtil.sanitizeBytes(data, off, len);
|
||||
if (sanitized != data) {
|
||||
param.args[0] = sanitized;
|
||||
param.args[1] = 0;
|
||||
param.args[2] = sanitized.length;
|
||||
XposedBridge.log(TAG + " okio.Buffer write bytes riskToken sanitized");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrofit 常用 byte[] RequestBody。 */
|
||||
private static void hookOutgoingRequestBytes(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook byteHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
for (int i = 0; i < param.args.length; i++) {
|
||||
if (!(param.args[i] instanceof byte[])) {
|
||||
continue;
|
||||
}
|
||||
byte[] data = (byte[]) param.args[i];
|
||||
byte[] sanitized = MariBankRiskTokenUtil.sanitizeBytes(data, 0, data.length);
|
||||
if (sanitized != data) {
|
||||
param.args[i] = sanitized;
|
||||
XposedBridge.log(TAG + " outbound RequestBody bytes riskToken sanitized");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.RequestBody",
|
||||
lpparam.classLoader,
|
||||
"create",
|
||||
"okhttp3.MediaType",
|
||||
byte[].class,
|
||||
byteHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.RequestBody",
|
||||
lpparam.classLoader,
|
||||
"create",
|
||||
byte[].class,
|
||||
"okhttp3.MediaType",
|
||||
byteHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookOkHttpResponseUrl(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.Response",
|
||||
lpparam.classLoader,
|
||||
"body",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object request = XposedHelpers.callMethod(param.thisObject, "request");
|
||||
Object url = XposedHelpers.callMethod(request, "url");
|
||||
if (url != null) {
|
||||
CURRENT_REQUEST_URL.set(String.valueOf(url));
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip okhttp Response.body url: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 出站 JSON 请求体:注册接口 /uapi/v2/register 会携带 riskToken。 */
|
||||
private static void hookOutgoingRequestBody(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook sanitizeHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
for (int i = 0; i < param.args.length; i++) {
|
||||
if (param.args[i] instanceof String) {
|
||||
String body = (String) param.args[i];
|
||||
if (body.contains("riskToken") && body.contains("|")) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
|
||||
if (!sanitized.equals(body)) {
|
||||
param.args[i] = sanitized;
|
||||
XposedBridge.log(TAG + " outbound RequestBody riskToken sanitized");
|
||||
}
|
||||
} else if (body.contains("deviceToken") && body.contains("|")) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
|
||||
if (!sanitized.equals(body)) {
|
||||
param.args[i] = sanitized;
|
||||
XposedBridge.log(TAG + " outbound RequestBody deviceToken sanitized");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
String[][] createSigs = {
|
||||
{"okhttp3.MediaType", "java.lang.String"},
|
||||
{"java.lang.String", "okhttp3.MediaType"},
|
||||
};
|
||||
for (String[] sig : createSigs) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.RequestBody",
|
||||
lpparam.classLoader,
|
||||
"create",
|
||||
sig[0],
|
||||
sig[1],
|
||||
sanitizeHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookGsonRiskToken(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook gsonHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (!(param.getResult() instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String json = (String) param.getResult();
|
||||
if (json.contains("mobileNo") || json.contains("phoneNo")) {
|
||||
int show = Math.min(json.length(), 500);
|
||||
XposedBridge.log(TAG + " Gson.toJson mobile: "
|
||||
+ json.substring(0, show)
|
||||
+ (json.length() > show ? "..." : ""));
|
||||
}
|
||||
if (!json.contains("riskToken") && !json.contains("deviceToken")) {
|
||||
return;
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(json);
|
||||
if (!sanitized.equals(json)) {
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " Gson.toJson riskToken sanitized");
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.google.gson.Gson",
|
||||
lpparam.classLoader,
|
||||
"toJson",
|
||||
Object.class,
|
||||
gsonHook);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip Gson.toJson: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.google.gson.Gson",
|
||||
lpparam.classLoader,
|
||||
"toJson",
|
||||
Object.class,
|
||||
"java.lang.reflect.Type",
|
||||
gsonHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookJsonRiskTokenPut(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"org.json.JSONObject",
|
||||
lpparam.classLoader,
|
||||
"put",
|
||||
String.class,
|
||||
Object.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (!"riskToken".equals(param.args[0]) && !"deviceToken".equals(param.args[0])) {
|
||||
return;
|
||||
}
|
||||
if (!(param.args[1] instanceof String)) {
|
||||
return;
|
||||
}
|
||||
param.args[1] = MariBankRiskTokenUtil.sanitizeRiskToken((String) param.args[1]);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip JSONObject.put riskToken: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookSafeModeDialog(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.shopee.bke.lib.safemode.activity.SafeModeRecoverActivity",
|
||||
lpparam.classLoader,
|
||||
"onCreate",
|
||||
"android.os.Bundle",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedHelpers.callMethod(param.thisObject, "finish");
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 拦截 Root/Hook/模拟器 警告弹窗与 Toast(文案见 bke_toast_not_support_*)。 */
|
||||
private static void hookRootDialogBlock(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook blankRootTextHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (param.args.length > 0 && isRootBlockText(String.valueOf(param.args[0]))) {
|
||||
param.args[0] = " ";
|
||||
XposedBridge.log(TAG + " blanked root dialog message");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
String[] messageSetters = {
|
||||
"android.app.AlertDialog$Builder",
|
||||
"androidx.appcompat.app.AlertDialog$Builder",
|
||||
"com.shopee.bke.lib.commonui.widget.CommonDialog$Builder",
|
||||
};
|
||||
for (String className : messageSetters) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className, lpparam.classLoader, "setMessage", CharSequence.class, blankRootTextHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className, lpparam.classLoader, "setTitle", CharSequence.class, blankRootTextHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Resources.class,
|
||||
"getString",
|
||||
int.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String s = (String) param.getResult();
|
||||
if (isRootBlockText(s)) {
|
||||
param.setResult(" ");
|
||||
XposedBridge.log(TAG + " blanked root string resource");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " getString hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Dialog.class,
|
||||
"show",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Dialog dialog = (Dialog) param.thisObject;
|
||||
if (isRootBlockText(extractDialogText(dialog))) {
|
||||
XposedBridge.log(TAG + " blocked root Dialog.show: "
|
||||
+ dialog.getClass().getSimpleName());
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Toast.class,
|
||||
"show",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String text = extractToastText((Toast) param.thisObject);
|
||||
if (isSecurityBlockText(text)) {
|
||||
XposedBridge.log(TAG + " security block Toast: " + text);
|
||||
logBriefStack();
|
||||
}
|
||||
if (isRootBlockText(text)) {
|
||||
XposedBridge.log(TAG + " blocked root Toast.show");
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Toast.show hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRootBlockText(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = text.toLowerCase();
|
||||
return lower.contains("rooted or jailbroken")
|
||||
|| lower.contains("modified device")
|
||||
|| lower.contains("magisk/xposed/frida")
|
||||
|| lower.contains("cannot be accessed on such devices")
|
||||
|| lower.contains("restore to factory settings");
|
||||
}
|
||||
|
||||
private static boolean isSecurityBlockText(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = text.toLowerCase();
|
||||
return lower.contains("temporarily blocked")
|
||||
|| lower.contains("8424 8050");
|
||||
}
|
||||
|
||||
private static void logBriefStack() {
|
||||
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int n = 0;
|
||||
for (StackTraceElement frame : stack) {
|
||||
String cn = frame.getClassName();
|
||||
if (cn.contains("miraclegarden") || cn.contains("lsposed") || cn.contains("XposedBridge")) {
|
||||
continue;
|
||||
}
|
||||
if (cn.startsWith("android.widget.") || cn.startsWith("android.view.")) {
|
||||
continue;
|
||||
}
|
||||
sb.append("\n at ").append(cn).append(".").append(frame.getMethodName());
|
||||
if (++n >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
XposedBridge.log(TAG + " stack:" + sb);
|
||||
}
|
||||
|
||||
private static String extractDialogText(Dialog dialog) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try {
|
||||
Object alert = XposedHelpers.getObjectField(dialog, "mAlert");
|
||||
if (alert != null) {
|
||||
appendFieldText(sb, alert, "mMessage");
|
||||
appendFieldText(sb, alert, "mTitle");
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
if (dialog.getWindow() != null) {
|
||||
collectTextViews(dialog.getWindow().getDecorView(), sb);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void appendFieldText(StringBuilder sb, Object target, String field) {
|
||||
try {
|
||||
Object value = XposedHelpers.getObjectField(target, field);
|
||||
if (value != null) {
|
||||
sb.append(value);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectTextViews(View view, StringBuilder sb) {
|
||||
if (view instanceof TextView) {
|
||||
CharSequence text = ((TextView) view).getText();
|
||||
if (text != null) {
|
||||
sb.append(text);
|
||||
}
|
||||
}
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
collectTextViews(group.getChildAt(i), sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractToastText(Toast toast) {
|
||||
try {
|
||||
View view = toast.getView();
|
||||
if (view instanceof TextView) {
|
||||
CharSequence text = ((TextView) view).getText();
|
||||
return text != null ? text.toString() : "";
|
||||
}
|
||||
if (view instanceof ViewGroup) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
collectTextViews(view, sb);
|
||||
return sb.toString();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
Object text = XposedHelpers.getObjectField(toast, "mText");
|
||||
return text != null ? text.toString() : "";
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* libshpssdk.so / libshpssdk_bank.so native 检测绕过(Java 层拦截 native 读路径)。
|
||||
* 逆向字符串:/proc/self/maps、hook 库名、RealInterceptorChain.proceed 等。
|
||||
*/
|
||||
public final class MariBankShpsNativeHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankNative";
|
||||
|
||||
private static final Set<String> PROC_SENSITIVE = new HashSet<>(Arrays.asList(
|
||||
"/proc/self/maps",
|
||||
"/proc/version",
|
||||
"/proc/self/status",
|
||||
"/proc/mounts",
|
||||
"/proc/cpuinfo",
|
||||
"/proc/self/attr/current",
|
||||
"/proc/self/mountinfo",
|
||||
"/proc/net/unix",
|
||||
"/proc/bootconfig",
|
||||
"/proc/self/cgroup"
|
||||
));
|
||||
|
||||
private static final String[] MAPS_HIDE_MARKERS = {
|
||||
"xposed", "lsposed", "edxposed", "magisk", "frida", "substrate",
|
||||
"libpine", "pine.so", "zygisk", "riru", "shamiko", "notimessage",
|
||||
"miraclegarden", "libbytehook", "libapmhook", "libspxhook",
|
||||
"liblubanhook", "libsulfuras", "libbugsnag-root-detection",
|
||||
"libreact_debug", "libmobileffmpeg_abidetect",
|
||||
"playintegrityfix", "libgadget", "libfrida", "libriru",
|
||||
"liblspd", "libzygisk", "libvector", "zygisk_vector",
|
||||
};
|
||||
|
||||
private static final WeakHashMap<Object, String> TRACKED_INPUTS = new WeakHashMap<>();
|
||||
|
||||
private static final String[] BOOT_SPOOF_KEYS = {
|
||||
"ro.boot.verifiedbootstate",
|
||||
"ro.boot.flash.locked",
|
||||
"ro.boot.vbmeta.device_state",
|
||||
"ro.boot.veritymode",
|
||||
"ro.boot.warranty_bit",
|
||||
"ro.boot.avb_version",
|
||||
"vendor.boot.vbmeta.device_state",
|
||||
"ro.crypto.state",
|
||||
};
|
||||
|
||||
private static final String FAKE_SELINUX_CTX =
|
||||
"u:r:untrusted_app:s0:c512,c768";
|
||||
|
||||
private MariBankShpsNativeHook() {
|
||||
}
|
||||
|
||||
private static volatile boolean deferredInstalled = false;
|
||||
|
||||
/** loadPackage 阶段只装 /proc 过滤,避免过早触发 SHPSSDK / libsdkutils 死循环白屏。 */
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
hookProcAccess(lpparam);
|
||||
hookProcViaRandomAccessFile(lpparam);
|
||||
hookBufferedReader(lpparam);
|
||||
hookSystemProperties(lpparam);
|
||||
XposedBridge.log(TAG + " early hooks OK (proc only)");
|
||||
}
|
||||
|
||||
/** attachBaseContext 之后安装 SHPSSDK 相关 Hook(ClassLoader 已就绪)。 */
|
||||
public static void installDeferred(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (deferredInstalled) {
|
||||
return;
|
||||
}
|
||||
deferredInstalled = true;
|
||||
hookLoadLibrary(lpparam);
|
||||
hookShpssInstall(lpparam);
|
||||
hookRequestDefense(lpparam);
|
||||
hookShpsNativeBridge(lpparam);
|
||||
hookShpsNativeCore(lpparam);
|
||||
hookBuildFields(lpparam);
|
||||
XposedBridge.log(TAG + " deferred hooks installed for " + lpparam.packageName);
|
||||
}
|
||||
|
||||
/** native 直接读 /proc/self/maps 查 hook 库;过滤内容。 */
|
||||
private static void hookProcAccess(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookConstructor(
|
||||
FileInputStream.class,
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String path = normalizeProcPath((String) param.args[0]);
|
||||
if (path != null) {
|
||||
TRACKED_INPUTS.put(param.getResult(), path);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " FileInputStream hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookConstructor(
|
||||
FileInputStream.class,
|
||||
File.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
File file = (File) param.args[0];
|
||||
if (file != null) {
|
||||
String path = normalizeProcPath(file.getAbsolutePath());
|
||||
if (path != null) {
|
||||
TRACKED_INPUTS.put(param.getResult(), path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
XC_MethodHook readFilter = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String path = TRACKED_INPUTS.get(param.thisObject);
|
||||
if (path == null || param.getResult() == null) {
|
||||
return;
|
||||
}
|
||||
if (param.getResult() instanceof Integer) {
|
||||
int read = (Integer) param.getResult();
|
||||
if (read <= 0 || param.args.length == 0 || !(param.args[0] instanceof byte[])) {
|
||||
return;
|
||||
}
|
||||
byte[] buf = (byte[]) param.args[0];
|
||||
int off = param.args.length > 1 ? (Integer) param.args[1] : 0;
|
||||
filterProcBytes(path, buf, off, read);
|
||||
} else if (param.getResult() instanceof byte[]) {
|
||||
byte[] data = (byte[]) param.getResult();
|
||||
param.setResult(filterProcBytesAll(path, data));
|
||||
} else if (param.getResult() instanceof String) {
|
||||
param.setResult(filterProcText(path, (String) param.getResult()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
FileInputStream.class, "read", byte[].class, readFilter);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
FileInputStream.class, "read", byte[].class, int.class, int.class, readFilter);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " FileInputStream.read hook failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"java.nio.file.Files",
|
||||
lpparam.classLoader,
|
||||
"readAllBytes",
|
||||
"java.nio.file.Path",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (!(param.getResult() instanceof byte[])) {
|
||||
return;
|
||||
}
|
||||
String path = String.valueOf(param.args[0]);
|
||||
String norm = normalizeProcPath(path);
|
||||
if (norm != null) {
|
||||
param.setResult(filterProcBytesAll(
|
||||
norm, (byte[]) param.getResult()));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookBufferedReader(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
BufferedReader.class,
|
||||
"readLine",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (!(param.getResult() instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String line = (String) param.getResult();
|
||||
if (shouldHideMapsLine(line)) {
|
||||
param.setResult(readNextSafeLine((BufferedReader) param.thisObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " BufferedReader hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String readNextSafeLine(BufferedReader reader) {
|
||||
try {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!shouldHideMapsLine(line)) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static void hookLoadLibrary(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook logHook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String lib = String.valueOf(param.args[param.args.length - 1]);
|
||||
if (lib.contains("shpssdk")) {
|
||||
XposedBridge.log(TAG + " loading native lib: " + lib);
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Runtime.class, "loadLibrary0", ClassLoader.class, String.class, logHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
System.class, "loadLibrary", String.class, logHook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookShpssInstall(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
// 勿 Hook ShpssInstall / vuvuwwwuw:会干扰 SoUtils.loadSoLibrary,导致 libsdkutils.so 死循环白屏。
|
||||
}
|
||||
|
||||
private static void hookRequestDefense(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
for (String className : new String[]{
|
||||
"com.shopee.shpssdkbank.SHPSSDK",
|
||||
"com.shopee.shpssdk.SHPSSDK",
|
||||
}) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
"requestDefense",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedBridge.log(TAG + " blocked requestDefense");
|
||||
param.setResult(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** shpssdkbank 混淆 native 桥接类:int/boolean 返回值强制安全。 */
|
||||
private static void hookShpsNativeBridge(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
String[] classes = {
|
||||
"com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw",
|
||||
"com.shopee.shpssdkbank.a",
|
||||
"com.shopee.shpssdkbank.b",
|
||||
"com.shopee.shpssdkbank.c",
|
||||
"com.shopee.shpssdkbank.d",
|
||||
"com.shopee.shpssdkbank.e",
|
||||
"com.shopee.shpssdkbank.f",
|
||||
"com.shopee.shpssdkbank.g",
|
||||
"com.shopee.shpssdkbank.vuvuwwwuw",
|
||||
"com.shopee.shpssdkbank.vwuuwwvwv",
|
||||
"com.shopee.shpssdkbank.vwwuwuuuv",
|
||||
"com.shopee.shpssdkbank.wvvvuuwuu",
|
||||
"com.shopee.shpssdkbank.wvvvuuww",
|
||||
"com.shopee.shpssdkbank.wvvvuvvv",
|
||||
"com.shopee.shpssdkbank.wvvvuvww",
|
||||
"com.shopee.shpssdkbank.wvvvuwwu",
|
||||
};
|
||||
int total = 0;
|
||||
for (String className : classes) {
|
||||
total += hookAllIntBooleanMethods(lpparam, className);
|
||||
total += hookAllStringSanitize(lpparam, className);
|
||||
}
|
||||
XposedBridge.log(TAG + " native-bridge total hooks=" + total);
|
||||
}
|
||||
|
||||
/** native 桥接可能直接返回 riskToken 字符串。 */
|
||||
private static int hookAllStringSanitize(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (method.getReturnType() != String.class) {
|
||||
continue;
|
||||
}
|
||||
if (method.getParameterTypes().length > 6) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (!(result instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String s = (String) result;
|
||||
if (!s.contains("|")) {
|
||||
return;
|
||||
}
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
|
||||
if (!sanitized.equals(s)) {
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " native String sanitized in "
|
||||
+ className + "#" + method.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int hookAllIntBooleanMethods(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
Class<?> rt = method.getReturnType();
|
||||
if (rt != boolean.class && rt != Boolean.class
|
||||
&& rt != int.class && rt != Integer.class) {
|
||||
continue;
|
||||
}
|
||||
if (method.getParameterTypes().length > 4) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (rt == boolean.class || rt == Boolean.class) {
|
||||
param.setResult(false);
|
||||
} else {
|
||||
param.setResult(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + count + " native-bridge checks in " + className);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
total += hookNativeCoreClass(lpparam, className);
|
||||
}
|
||||
XposedBridge.log(TAG + " native-core total hooks=" + total);
|
||||
}
|
||||
|
||||
private static int hookNativeCoreClass(
|
||||
XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
int count = 0;
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
Class<?> rt = method.getReturnType();
|
||||
if (rt == String.class) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (!(result instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String s = (String) result;
|
||||
if (s.length() > 80 && s.contains("|")) {
|
||||
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
|
||||
if (!sanitized.equals(s)) {
|
||||
param.setResult(sanitized);
|
||||
XposedBridge.log(TAG + " core String sanitized "
|
||||
+ className + "#" + method.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
} else if (rt == boolean.class || rt == Boolean.class
|
||||
|| rt == int.class || rt == Integer.class) {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (rt == boolean.class || rt == Boolean.class) {
|
||||
param.setResult(false);
|
||||
} else {
|
||||
param.setResult(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + count + " core natives in " + className);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip core " + className + ": " + t.getMessage());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Build.TAGS / FINGERPRINT 等 Java 层可读字段伪装。 */
|
||||
private static void hookBuildFields(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.setStaticObjectField(Build.class, "TAGS", "release-keys");
|
||||
if (String.valueOf(Build.FINGERPRINT).contains("test-keys")) {
|
||||
XposedHelpers.setStaticObjectField(Build.class, "FINGERPRINT",
|
||||
Build.FINGERPRINT.replace("test-keys", "release-keys"));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
XposedHelpers.setStaticObjectField(Build.class, "BOOTLOADER", "unknown");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
XposedBridge.log(TAG + " Build fields spoofed");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Build spoof failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookProcViaRandomAccessFile(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookConstructor(
|
||||
"java.io.RandomAccessFile",
|
||||
lpparam.classLoader,
|
||||
String.class,
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String path = normalizeProcPath((String) param.args[0]);
|
||||
if (path != null) {
|
||||
TRACKED_INPUTS.put(param.getResult(), path);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"java.io.RandomAccessFile",
|
||||
lpparam.classLoader,
|
||||
"read",
|
||||
byte[].class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String path = TRACKED_INPUTS.get(param.thisObject);
|
||||
if (path == null || !(param.getResult() instanceof Integer)) {
|
||||
return;
|
||||
}
|
||||
int read = (Integer) param.getResult();
|
||||
if (read > 0 && param.args[0] instanceof byte[]) {
|
||||
filterProcBytes(path, (byte[]) param.args[0], 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookSystemProperties(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> sp = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader);
|
||||
for (Method method : sp.getDeclaredMethods()) {
|
||||
if (!"get".equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.args.length == 0 || !(param.args[0] instanceof String)) {
|
||||
return;
|
||||
}
|
||||
String key = (String) param.args[0];
|
||||
String spoofed = spoofProperty(key, param.getResult());
|
||||
if (spoofed != null) {
|
||||
param.setResult(spoofed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " SystemProperties hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String spoofProperty(String key, Object current) {
|
||||
if ("ro.debuggable".equals(key)) {
|
||||
return "0";
|
||||
}
|
||||
if ("ro.secure".equals(key)) {
|
||||
return "1";
|
||||
}
|
||||
if ("ro.build.tags".equals(key)) {
|
||||
if (current instanceof String && String.valueOf(current).contains("test-keys")) {
|
||||
return "release-keys";
|
||||
}
|
||||
}
|
||||
if ("ro.boot.verifiedbootstate".equals(key)) {
|
||||
return "green";
|
||||
}
|
||||
if ("ro.boot.flash.locked".equals(key)) {
|
||||
return "1";
|
||||
}
|
||||
if ("ro.boot.vbmeta.device_state".equals(key)
|
||||
|| "vendor.boot.vbmeta.device_state".equals(key)) {
|
||||
return "locked";
|
||||
}
|
||||
if ("ro.boot.veritymode".equals(key)) {
|
||||
return "enforcing";
|
||||
}
|
||||
if ("ro.boot.warranty_bit".equals(key)) {
|
||||
return "0";
|
||||
}
|
||||
if ("ro.crypto.state".equals(key)) {
|
||||
return "encrypted";
|
||||
}
|
||||
for (String bootKey : BOOT_SPOOF_KEYS) {
|
||||
if (bootKey.equals(key) && key.startsWith("ro.boot")) {
|
||||
// already handled above for known keys
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeProcPath(String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
String norm = path.trim();
|
||||
for (String p : PROC_SENSITIVE) {
|
||||
if (norm.equals(p) || norm.endsWith(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] filterProcBytesAll(String path, byte[] data) {
|
||||
return filterProcText(path, new String(data)).getBytes();
|
||||
}
|
||||
|
||||
private static void filterProcBytes(String path, byte[] buf, int off, int len) {
|
||||
String text = new String(buf, off, len);
|
||||
String filtered = filterProcText(path, text);
|
||||
if (filtered.equals(text)) {
|
||||
return;
|
||||
}
|
||||
byte[] out = filtered.getBytes();
|
||||
int copy = Math.min(len, out.length);
|
||||
System.arraycopy(out, 0, buf, off, copy);
|
||||
if (copy < len) {
|
||||
Arrays.fill(buf, off + copy, off + len, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static String filterProcText(String path, String text) {
|
||||
if ("/proc/self/maps".equals(path) || "/proc/self/mountinfo".equals(path)
|
||||
|| "/proc/mounts".equals(path)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String line : text.split("\n")) {
|
||||
if (!shouldHideMapsLine(line)) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
if ("/proc/self/attr/current".equals(path)) {
|
||||
String lower = text.toLowerCase(Locale.US);
|
||||
if (lower.contains("magisk") || lower.contains("su") || lower.contains("zygisk")
|
||||
|| lower.contains("xposed")) {
|
||||
return FAKE_SELINUX_CTX;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
if ("/proc/version".equals(path)) {
|
||||
return text.replace("dirty", "").replace("test-keys", "release-keys");
|
||||
}
|
||||
if ("/proc/self/status".equals(path)) {
|
||||
return text.replaceAll("(?m)^TracerPid:\\s*[1-9]\\d*",
|
||||
"TracerPid:\t0");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static boolean shouldHideMapsLine(String line) {
|
||||
if (line == null || line.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = line.toLowerCase(Locale.US);
|
||||
for (String marker : MAPS_HIDE_MARKERS) {
|
||||
if (lower.contains(marker)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Root / Hook / 模拟器检测通用绕过辅助。
|
||||
*/
|
||||
public final class RootBypassHelper {
|
||||
|
||||
private static final String TAG = "notiMessageHook/RootBypass";
|
||||
|
||||
private static final Pattern UNSAFE_NAME = Pattern.compile(
|
||||
".*(root|jail|hook|frida|xposed|lsposed|emulator|simulator|debug|tamper|"
|
||||
+ "integrity|unsafe|risk|magisk|su|cheat|mock).*",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Pattern SAFE_NAME = Pattern.compile(
|
||||
".*(safe|secure|valid|passed|pass|clean|trusted|normal|ok).*",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Set<String> ROOT_PATH_MARKERS = new HashSet<>(Arrays.asList(
|
||||
"/su",
|
||||
"magisk",
|
||||
"supersu",
|
||||
"busybox",
|
||||
"/xbin/su",
|
||||
"/sbin/su",
|
||||
"de.robv.android.xposed",
|
||||
"org.lsposed",
|
||||
"com.topjohnwu.magisk"
|
||||
));
|
||||
|
||||
private RootBypassHelper() {
|
||||
}
|
||||
|
||||
public static void hookSecurityClass(XC_LoadPackage.LoadPackageParam lpparam, String className) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
hookMethodIfSecurityCheck(className, method);
|
||||
}
|
||||
XposedBridge.log(TAG + " hooked methods in " + className);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip class " + className + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookMethodIfSecurityCheck(String className, Method method) {
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType != boolean.class
|
||||
&& returnType != Boolean.class
|
||||
&& returnType != int.class
|
||||
&& returnType != Integer.class) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = method.getName();
|
||||
if (!looksLikeSecurityMethod(name) && !className.toLowerCase(Locale.US).contains("safemode")
|
||||
&& !className.toLowerCase(Locale.US).contains("risk")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (returnType == boolean.class || returnType == Boolean.class) {
|
||||
param.setResult(shouldReturnTrue(name));
|
||||
} else {
|
||||
param.setResult(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " hook failed " + className + "#" + name + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean looksLikeSecurityMethod(String name) {
|
||||
return UNSAFE_NAME.matcher(name).matches() || SAFE_NAME.matcher(name).matches();
|
||||
}
|
||||
|
||||
private static boolean shouldReturnTrue(String methodName) {
|
||||
if (UNSAFE_NAME.matcher(methodName).matches()) {
|
||||
return false;
|
||||
}
|
||||
if (SAFE_NAME.matcher(methodName).matches()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void hookFileExists(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
File.class,
|
||||
"exists",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
File file = (File) param.thisObject;
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
String path = file.getAbsolutePath().toLowerCase(Locale.US);
|
||||
for (String marker : ROOT_PATH_MARKERS) {
|
||||
if (path.contains(marker)) {
|
||||
param.setResult(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " File.exists hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void hookRuntimeExec(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Runtime.class,
|
||||
"exec",
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String cmd = (String) param.args[0];
|
||||
if (cmd == null) {
|
||||
return;
|
||||
}
|
||||
String lower = cmd.toLowerCase(Locale.US);
|
||||
if (lower.contains("su") || lower.contains("magisk") || lower.contains("which su")) {
|
||||
throw new SecurityException("blocked root probe");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Runtime.exec hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void hookSystemGetProperty(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
System.class,
|
||||
"getProperty",
|
||||
String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
String key = (String) param.args[0];
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if ("ro.debuggable".equals(key) || "ro.secure".equals(key)) {
|
||||
param.setResult("ro.secure".equals(key) ? "1" : "0");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " System.getProperty hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* Suncorp Bank — Kotlin 原生 App。
|
||||
* 逆向结论:au.com.suncorp.marketplace.base.application.SuncorpMessagingService#onMessageReceived
|
||||
*/
|
||||
public final class SuncorpBankMessageHook {
|
||||
|
||||
private static final String MESSAGING_SERVICE =
|
||||
"au.com.suncorp.marketplace.base.application.SuncorpMessagingService";
|
||||
|
||||
private SuncorpBankMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
BankHookHelper.hookFcmService(lpparam, MESSAGING_SERVICE, HookBridge.SOURCE_XPOSED_SUNCORP);
|
||||
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_SUNCORP_NOTIFY);
|
||||
XposedBridge.log("notiMessageHook/Suncorp installed for " + lpparam.packageName);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public final class TelegramMessageHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/Telegram";
|
||||
private static final String NOTIFICATION_CENTER = "org.telegram.messenger.NotificationCenter";
|
||||
private static final String NOTIFICATIONS_CONTROLLER = "org.telegram.messenger.NotificationsController";
|
||||
private static final String MESSAGE_OBJECT = "org.telegram.messenger.MessageObject";
|
||||
private static final int DEDUP_SIZE = 512;
|
||||
|
||||
@@ -64,12 +65,16 @@ public final class TelegramMessageHook {
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof List) {
|
||||
processMessageList(context, lpparam.packageName, (List<?>) arg);
|
||||
} else if (isMessageObject(arg)) {
|
||||
forwardMessageObject(context, lpparam.packageName, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
installNotificationsControllerHook(lpparam);
|
||||
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName
|
||||
+ ", didReceiveNewMessages=" + didReceiveNewMessages);
|
||||
} catch (Throwable t) {
|
||||
@@ -100,6 +105,38 @@ public final class TelegramMessageHook {
|
||||
}
|
||||
}
|
||||
|
||||
/** 后台弹通知路径:NotificationsController.appendMessage(MessageObject) */
|
||||
private static void installNotificationsControllerHook(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> controllerClass = XposedHelpers.findClass(
|
||||
NOTIFICATIONS_CONTROLLER, lpparam.classLoader);
|
||||
Class<?> messageObjectClass = XposedHelpers.findClass(
|
||||
MESSAGE_OBJECT, lpparam.classLoader);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
controllerClass,
|
||||
"appendMessage",
|
||||
messageObjectClass,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Context context = getContext();
|
||||
if (context == null || param.args[0] == null) {
|
||||
return;
|
||||
}
|
||||
forwardMessageObject(context, lpparam.packageName, param.args[0]);
|
||||
}
|
||||
}
|
||||
);
|
||||
XposedBridge.log(TAG + " appendMessage hook installed for " + lpparam.packageName);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " appendMessage hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMessageObject(Object arg) {
|
||||
return arg != null && MESSAGE_OBJECT.equals(arg.getClass().getName());
|
||||
}
|
||||
|
||||
private static void processMessageList(Context context, String packageName, List<?> messages) {
|
||||
for (Object item : messages) {
|
||||
if (item == null) {
|
||||
@@ -356,12 +393,7 @@ public final class TelegramMessageHook {
|
||||
"org.telegram.messenger.MessagesController", cl);
|
||||
Object mc = XposedHelpers.callStaticMethod(mcClass, "getInstance", account);
|
||||
|
||||
Object title = null;
|
||||
try {
|
||||
title = XposedHelpers.callMethod(mc, "getPeerTitle", dialogId, false);
|
||||
} catch (Throwable ignored) {
|
||||
title = XposedHelpers.callMethod(mc, "getPeerTitle", dialogId);
|
||||
}
|
||||
Object title = invokeGetPeerTitle(mc, dialogId);
|
||||
|
||||
String text = safeText(title);
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
@@ -392,6 +424,28 @@ public final class TelegramMessageHook {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object invokeGetPeerTitle(Object mc, long dialogId) {
|
||||
try {
|
||||
return XposedHelpers.callMethod(mc, "getPeerTitle", dialogId, false);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
return XposedHelpers.callMethod(mc, "getPeerTitle", dialogId);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
java.lang.reflect.Method method = mc.getClass().getMethod("getPeerTitle", long.class, boolean.class);
|
||||
return method.invoke(mc, dialogId, false);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
java.lang.reflect.Method method = mc.getClass().getMethod("getPeerTitle", long.class);
|
||||
return method.invoke(mc, dialogId);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String formatUserName(Object user) {
|
||||
if (user == null) {
|
||||
return "";
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* ubank — Capacitor + MoEngage 推送。
|
||||
* 逆向结论:
|
||||
* - com.moengage.firebase.MoEFireBaseMessagingService(交易/营销推送主路径)
|
||||
* - io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService(Capacitor FCM 插件)
|
||||
*/
|
||||
public final class UbankMessageHook {
|
||||
|
||||
private static final String MOE_SERVICE = "com.moengage.firebase.MoEFireBaseMessagingService";
|
||||
private static final String CAPACITOR_SERVICE =
|
||||
"io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService";
|
||||
|
||||
private UbankMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
BankHookHelper.hookFcmService(lpparam, MOE_SERVICE, HookBridge.SOURCE_XPOSED_UBANK);
|
||||
BankHookHelper.hookFcmService(lpparam, CAPACITOR_SERVICE, HookBridge.SOURCE_XPOSED_UBANK);
|
||||
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_UBANK_NOTIFY);
|
||||
XposedBridge.log("notiMessageHook/ubank installed for " + lpparam.packageName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* Up Bank — React Native + 原生 FCM HandlerService。
|
||||
* 逆向结论:au.com.up.money.notifications.HandlerService#onMessageReceived(RemoteMessage)
|
||||
*/
|
||||
public final class UpBankMessageHook {
|
||||
|
||||
private static final String HANDLER_SERVICE = "au.com.up.money.notifications.HandlerService";
|
||||
|
||||
private UpBankMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
BankHookHelper.hookFcmService(lpparam, HANDLER_SERVICE, HookBridge.SOURCE_XPOSED_UP);
|
||||
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_UP_NOTIFY);
|
||||
XposedBridge.log("notiMessageHook/Up installed for " + lpparam.packageName);
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,9 @@
|
||||
<item>org.telegram.messenger.web</item>
|
||||
<item>com.tencent.mm</item>
|
||||
<item>com.google.android.gm</item>
|
||||
<item>au.com.up.money</item>
|
||||
<item>au.com.suncorp.marketplace</item>
|
||||
<item>au.com.bank86400</item>
|
||||
<item>ph.seabank.seabank</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user