feat: MariBank 风控 bypass、澳洲银行 Hook 与 reverse 逆向工作区

新增 MariBank/SeaBank PH Root 与 SHPSSDK bypass、riskToken 净化及 Up/Suncorp/ubank 消息 Hook;整理 reverse/ 脚本与 Frida 工具链,并补充当日工作说明文档。
This commit is contained in:
2026-07-03 17:15:16 +08:00
parent 125dfe583b
commit 59970a84a8
121 changed files with 7606 additions and 37 deletions

11
.gitignore vendored
View File

@@ -16,3 +16,14 @@ local.properties
gradle-local.properties
platform-tools/
xposed-module/build/
reverse/apks/
reverse/extracted/
reverse/output/
reverse/logs/
reverse/tmp/
reverse/frida/bin/
reverse/frida/*.log
reverse/frida/*.log.err
reverse/frida/logcat_capture.txt
reverse/frida/*.out
reverse/frida/*.err

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-04-04T17:31:49.855391Z">
<DropdownSelection timestamp="2026-07-02T07:30:20.554015200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/wchino/.android/avd/Pixel_9_Pro.avd" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=1C081FDF600K5Q" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -77,6 +77,10 @@
<activity
android:name=".Activity.PermissionActivity"
android:exported="false" />
<activity
android:name=".Activity.KeepAliveSettingsActivity"
android:exported="false"
android:theme="@style/Theme.SmsMessage1" />
<!--通知栏获取短信-->
<service

View File

@@ -0,0 +1,172 @@
package com.miraclegarden.smsmessage.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.databinding.ActivityKeepAliveSettingsBinding;
import com.miraclegarden.smsmessage.service.MonitoredAppActivator;
import com.miraclegarden.smsmessage.service.MonitoredAppKeepAliveScheduler;
import com.miraclegarden.smsmessage.service.NotificationService;
import java.util.ArrayList;
import java.util.List;
public class KeepAliveSettingsActivity extends MiracleGardenActivity<ActivityKeepAliveSettingsBinding> {
private boolean loadingUi;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.ivBack.setOnClickListener(v -> finish());
setupSpinners();
loadFromPrefs();
bindListeners();
}
private void setupSpinners() {
binding.spinnerInterval.setAdapter(buildAdapter(formatIntervalOptions()));
binding.spinnerCooldown.setAdapter(buildAdapter(formatCooldownOptions()));
}
private ArrayAdapter<String> buildAdapter(List<String> labels) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
return adapter;
}
private List<String> formatIntervalOptions() {
List<String> labels = new ArrayList<>();
for (int minutes : KeepAliveSettings.INTERVAL_OPTIONS) {
labels.add(minutes + " 分钟");
}
return labels;
}
private List<String> formatCooldownOptions() {
List<String> labels = new ArrayList<>();
for (int minutes : KeepAliveSettings.COOLDOWN_OPTIONS) {
labels.add(minutes + " 分钟");
}
return labels;
}
private void loadFromPrefs() {
loadingUi = true;
binding.switchEnabled.setChecked(KeepAliveSettings.isEnabled(this));
binding.switchStealthMode.setChecked(KeepAliveSettings.isStealthMode(this));
binding.switchPreferRoot.setChecked(KeepAliveSettings.preferRoot(this));
binding.switchReturnHome.setChecked(KeepAliveSettings.returnHome(this));
binding.switchKillBeforeLaunch.setChecked(KeepAliveSettings.killBeforeLaunch(this));
binding.spinnerInterval.setSelection(
KeepAliveSettings.indexOfInterval(KeepAliveSettings.getIntervalMinutes(this)));
binding.spinnerCooldown.setSelection(
KeepAliveSettings.indexOfCooldown(KeepAliveSettings.getCooldownMinutes(this)));
loadingUi = false;
updateControlsEnabled();
}
private void bindListeners() {
binding.switchEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (loadingUi) {
return;
}
KeepAliveSettings.setEnabled(this, isChecked);
updateControlsEnabled();
applySchedule();
});
binding.switchStealthMode.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setStealthMode(this, isChecked);
}
});
binding.switchPreferRoot.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setPreferRoot(this, isChecked);
}
});
binding.switchReturnHome.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setReturnHome(this, isChecked);
}
});
binding.switchKillBeforeLaunch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setKillBeforeLaunch(this, isChecked);
}
});
binding.spinnerInterval.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (loadingUi) {
return;
}
KeepAliveSettings.setIntervalMinutes(
KeepAliveSettingsActivity.this, KeepAliveSettings.INTERVAL_OPTIONS[position]);
applySchedule();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.spinnerCooldown.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (!loadingUi) {
KeepAliveSettings.setCooldownMinutes(
KeepAliveSettingsActivity.this, KeepAliveSettings.COOLDOWN_OPTIONS[position]);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.btnWakeNow.setOnClickListener(v -> {
if (!KeepAliveSettings.isEnabled(this)) {
Toast.makeText(this, R.string.keep_alive_wake_disabled, Toast.LENGTH_SHORT).show();
return;
}
MonitoredAppActivator.activateAllAsync(this);
Toast.makeText(this, R.string.keep_alive_wake_started, Toast.LENGTH_SHORT).show();
});
}
private void applySchedule() {
if (NotificationService.isMonitoringActive(this)) {
if (KeepAliveSettings.isEnabled(this)) {
MonitoredAppKeepAliveScheduler.schedule(this);
} else {
MonitoredAppKeepAliveScheduler.cancel(this);
}
}
}
private void updateControlsEnabled() {
boolean enabled = binding.switchEnabled.isChecked();
binding.spinnerInterval.setEnabled(enabled);
binding.spinnerCooldown.setEnabled(enabled);
binding.switchStealthMode.setEnabled(enabled);
binding.switchPreferRoot.setEnabled(enabled);
binding.switchKillBeforeLaunch.setEnabled(enabled);
binding.switchReturnHome.setEnabled(enabled);
binding.btnWakeNow.setEnabled(enabled);
}
}

View File

@@ -145,7 +145,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
return;
}
if (NotificationService.isMonitoring()) {
if (NotificationService.isMonitoringActive(this)) {
NotificationService.stopMonitoring(this);
sendMessage("正在停止监听...");
} else {
@@ -165,7 +165,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
return;
}
if (NotificationService.isMonitoring()) {
if (NotificationService.isMonitoringActive(activity)) {
activity.binding.btnToggleMonitor.setText("停止监听");
activity.binding.btnToggleMonitor.setEnabled(true);
activity.binding.btnToggleMonitor.setBackgroundTintList(

View File

@@ -56,6 +56,10 @@ public class SettingActivity extends MiracleGardenActivity<ActivitySettingBindin
binding.btnAddApp.setOnClickListener(v -> {
startActivity(new Intent(this, AppListActivity.class));
});
binding.rowKeepAliveSettings.setOnClickListener(v -> {
startActivity(new Intent(this, KeepAliveSettingsActivity.class));
});
}
@Override

View File

@@ -18,6 +18,21 @@ public final class AppConfig {
*/
public static final String DEBUG_SERVER_URL = "http://127.0.0.1:8765";
/** 是否定期唤醒监听列表中的 App保持进程 / Hook 加载) */
public static final boolean ENABLE_MONITORED_APP_KEEP_ALIVE = true;
/** 保活间隔分钟WorkManager 最小 15 */
public static final int MONITORED_APP_KEEP_ALIVE_MINUTES = 15;
/** 同一 App 两次唤醒最短间隔(分钟) */
public static final int MONITORED_APP_WAKE_COOLDOWN_MINUTES = 10;
/** 优先使用 Rootmonkey 唤醒 + 电池白名单),失败则普通 startActivity */
public static final boolean KEEP_ALIVE_PREFER_ROOT = true;
/** Root 唤醒后是否自动返回桌面(减少停留在银行/TG 界面) */
public static final boolean KEEP_ALIVE_RETURN_HOME = true;
private AppConfig() {
}
}

View File

@@ -0,0 +1,115 @@
package com.miraclegarden.smsmessage;
import android.content.Context;
import android.content.SharedPreferences;
/**
* App 保活相关设置SharedPreferences可在设置页修改
*/
public final class KeepAliveSettings {
private static final String PREF = "keep_alive_settings";
private static final String KEY_ENABLED = "enabled";
private static final String KEY_INTERVAL_MINUTES = "interval_minutes";
private static final String KEY_COOLDOWN_MINUTES = "cooldown_minutes";
private static final String KEY_PREFER_ROOT = "prefer_root";
private static final String KEY_RETURN_HOME = "return_home";
private static final String KEY_KILL_BEFORE_LAUNCH = "kill_before_launch";
private static final String KEY_STEALTH_MODE = "stealth_mode";
public static final int MODE_STEALTH = 0;
public static final int MODE_AGGRESSIVE = 1;
public static final int WORK_MANAGER_MIN_MINUTES = 15;
public static final int[] INTERVAL_OPTIONS = {1, 3, 5, 10, 15, 30, 45, 60};
public static final int[] COOLDOWN_OPTIONS = {1, 3, 5, 10, 15, 20, 30};
private KeepAliveSettings() {
}
private static SharedPreferences prefs(Context context) {
return context.getApplicationContext().getSharedPreferences(PREF, Context.MODE_PRIVATE);
}
public static boolean isEnabled(Context context) {
return prefs(context).getBoolean(KEY_ENABLED, AppConfig.ENABLE_MONITORED_APP_KEEP_ALIVE);
}
public static void setEnabled(Context context, boolean enabled) {
prefs(context).edit().putBoolean(KEY_ENABLED, enabled).apply();
}
public static int getIntervalMinutes(Context context) {
int value = prefs(context).getInt(KEY_INTERVAL_MINUTES, AppConfig.MONITORED_APP_KEEP_ALIVE_MINUTES);
return Math.max(1, value);
}
public static void setIntervalMinutes(Context context, int minutes) {
prefs(context).edit().putInt(KEY_INTERVAL_MINUTES, Math.max(1, minutes)).apply();
}
public static int getCooldownMinutes(Context context) {
int value = prefs(context).getInt(KEY_COOLDOWN_MINUTES, AppConfig.MONITORED_APP_WAKE_COOLDOWN_MINUTES);
return Math.max(1, value);
}
public static void setCooldownMinutes(Context context, int minutes) {
prefs(context).edit().putInt(KEY_COOLDOWN_MINUTES, Math.max(1, minutes)).apply();
}
public static boolean preferRoot(Context context) {
return prefs(context).getBoolean(KEY_PREFER_ROOT, AppConfig.KEEP_ALIVE_PREFER_ROOT);
}
public static void setPreferRoot(Context context, boolean preferRoot) {
prefs(context).edit().putBoolean(KEY_PREFER_ROOT, preferRoot).apply();
}
public static boolean returnHome(Context context) {
return prefs(context).getBoolean(KEY_RETURN_HOME, AppConfig.KEEP_ALIVE_RETURN_HOME);
}
public static void setReturnHome(Context context, boolean returnHome) {
prefs(context).edit().putBoolean(KEY_RETURN_HOME, returnHome).apply();
}
/** 启动前 force-stop确保冷启动并重新加载 Hook会稍慢 */
public static boolean killBeforeLaunch(Context context) {
return prefs(context).getBoolean(KEY_KILL_BEFORE_LAUNCH, false);
}
public static void setKillBeforeLaunch(Context context, boolean killBeforeLaunch) {
prefs(context).edit().putBoolean(KEY_KILL_BEFORE_LAUNCH, killBeforeLaunch).apply();
}
/** 静默:进程在就不弹 App激进每次都唤起界面 */
public static boolean isStealthMode(Context context) {
return prefs(context).getInt(KEY_STEALTH_MODE, MODE_STEALTH) == MODE_STEALTH;
}
public static void setStealthMode(Context context, boolean stealth) {
prefs(context).edit()
.putInt(KEY_STEALTH_MODE, stealth ? MODE_STEALTH : MODE_AGGRESSIVE)
.apply();
}
public static int indexOfInterval(int minutes) {
for (int i = 0; i < INTERVAL_OPTIONS.length; i++) {
if (INTERVAL_OPTIONS[i] == minutes) {
return i;
}
}
return 0;
}
public static int indexOfCooldown(int minutes) {
for (int i = 0; i < COOLDOWN_OPTIONS.length; i++) {
if (COOLDOWN_OPTIONS[i] == minutes) {
return i;
}
}
return 1;
}
}

View File

@@ -17,7 +17,9 @@ public class BootReceiver extends BroadcastReceiver {
String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
NotificationService.requestStartMonitoring(context);
if (NotificationService.isMonitoringActive(context)) {
NotificationService.requestStartMonitoring(context);
}
}, BOOT_DELAY_MS);
}
}

View File

@@ -0,0 +1,336 @@
package com.miraclegarden.smsmessage.service;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;
import android.util.Log;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.MessageInfo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 唤醒监听列表中的 App使其进程启动、Xposed Hook 重新加载。
* Root 设备monkey 轻量启动 + 可选返回桌面 + 电池白名单。
*/
public final class MonitoredAppActivator {
private static final String TAG = "MonitoredAppActivator";
private static final String PREF = "keep_alive";
private static final String KEY_LAST_WAKE_PREFIX = "last_wake_";
private MonitoredAppActivator() {
}
public static void activateAllAsync(Context context) {
if (!KeepAliveSettings.isEnabled(context)) {
return;
}
Context appContext = context.getApplicationContext();
new Thread(() -> activateAll(appContext), "MonitoredAppActivator").start();
}
public static void activateAll(Context context) {
if (!KeepAliveSettings.isEnabled(context)) {
return;
}
if (!NotificationService.isMonitoringActive(context)) {
Log.d(TAG, "skip activate: monitoring off");
return;
}
List<MessageInfo> apps = App.getNotiList(context);
if (apps.isEmpty()) {
return;
}
Log.i(TAG, "activating " + apps.size() + " monitored app(s)");
NotificationActivity.sendMessage("正在唤醒监听 App" + apps.size() + " 个)...");
int ok = 0;
for (MessageInfo info : apps) {
if (info == null || TextUtils.isEmpty(info.getPackageName())) {
continue;
}
if (context.getPackageName().equals(info.getPackageName())) {
continue;
}
if (activateOne(context, info)) {
ok++;
}
}
NotificationService.toggleNotificationListenerService(context);
NotificationActivity.sendMessage("App 保活完成: " + ok + "/" + apps.size());
}
private static boolean activateOne(Context context, MessageInfo info) {
String pkg = info.getPackageName();
String label = !TextUtils.isEmpty(info.getAppName()) ? info.getAppName() : pkg;
if (isInCooldown(context, pkg)) {
Log.d(TAG, "skip cooldown: " + pkg);
NotificationActivity.sendMessage("[保活] " + label + " 冷却中,稍后重试");
return false;
}
applyBatteryWhitelist(pkg);
boolean wasAlive = isProcessAlive(context, pkg);
if (wasAlive && KeepAliveSettings.isStealthMode(context)) {
if (!isProcessCached(context, pkg)) {
Log.d(TAG, "stealth skip (active): " + pkg);
return true;
}
Log.i(TAG, "stealth headless wake (cached): " + pkg);
if (wakeHeadlessByRoot(pkg)) {
markWake(context, pkg);
NotificationActivity.sendMessage("[保活] " + label + " 已唤醒后台进程(无界面)");
return true;
}
}
if (KeepAliveSettings.killBeforeLaunch(context) && KeepAliveSettings.preferRoot(context)) {
forceStopByRoot(pkg);
sleepQuietly(800);
wasAlive = false;
}
boolean success = false;
if (KeepAliveSettings.preferRoot(context)) {
success = wakeByRoot(context, pkg);
}
if (!success) {
success = wakeByLaunchIntent(context, pkg);
}
if (success) {
markWake(context, pkg);
if (KeepAliveSettings.returnHome(context) && KeepAliveSettings.preferRoot(context)) {
long homeDelay = KeepAliveSettings.isStealthMode(context) ? 400L : 1500L;
sleepQuietly(homeDelay);
pressHomeByRoot();
}
NotificationActivity.sendMessage("[保活] " + label + " 已启动");
Log.i(TAG, "woke " + pkg);
} else {
NotificationActivity.sendMessage("[保活] " + label + " 唤起失败(检查 Root / Magisk 授权)");
Log.w(TAG, "wake failed " + pkg);
}
return success;
}
private static void forceStopByRoot(String packageName) {
try {
Runtime.getRuntime().exec(new String[]{
"su", "-c", "am force-stop " + packageName
}).waitFor();
} catch (Throwable t) {
Log.w(TAG, "force-stop failed " + packageName + ": " + t.getMessage());
}
}
private static boolean isInCooldown(Context context, String packageName) {
long last = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.getLong(KEY_LAST_WAKE_PREFIX + packageName, 0L);
long cooldownMs = TimeUnit.MINUTES.toMillis(KeepAliveSettings.getCooldownMinutes(context));
return System.currentTimeMillis() - last < cooldownMs;
}
private static void markWake(Context context, String packageName) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.edit()
.putLong(KEY_LAST_WAKE_PREFIX + packageName, System.currentTimeMillis())
.apply();
}
private static boolean isProcessAlive(Context context, String packageName) {
if (isProcessAliveByRoot(packageName)) {
return true;
}
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return false;
}
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info : processes) {
if (packageName.equals(info.processName)) {
return true;
}
}
return false;
}
/** 进程在但已被系统冻结cached此时 TG 可能不处理推送。 */
private static boolean isProcessCached(Context context, String packageName) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return false;
}
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info : processes) {
if (!packageName.equals(info.processName)) {
continue;
}
return info.importance >= ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
}
return false;
}
/** 无界面唤醒 TG 前台 Service让后台继续收推送。 */
private static boolean wakeHeadlessByRoot(String packageName) {
if (!packageName.contains("telegram")) {
return false;
}
String component = packageName + "/org.telegram.messenger.NotificationsService";
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c", "am start-foreground-service -n " + component
});
if (process.waitFor() == 0) {
return true;
}
} catch (Throwable t) {
Log.w(TAG, "headless fgs wake failed " + packageName + ": " + t.getMessage());
}
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c", "am startservice -n " + component
});
return process.waitFor() == 0;
} catch (Throwable t) {
Log.w(TAG, "headless service wake failed " + packageName + ": " + t.getMessage());
return false;
}
}
private static boolean isProcessAliveByRoot(String packageName) {
try {
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "pidof " + packageName});
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
return process.waitFor() == 0 && line != null && !line.trim().isEmpty();
}
} catch (Throwable t) {
return false;
}
}
private static boolean wakeByRoot(Context context, String packageName) {
String component = resolveLauncherComponent(context, packageName);
if (!TextUtils.isEmpty(component)) {
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c",
"am start -n " + component
+ " -a android.intent.action.MAIN"
+ " -c android.intent.category.LAUNCHER"
+ " --activity-brought-to-front"
+ " --activity-no-animation"
});
if (process.waitFor() == 0) {
return true;
}
} catch (Throwable t) {
Log.w(TAG, "am start failed " + packageName + ": " + t.getMessage());
}
}
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c",
"monkey -p " + packageName + " -c android.intent.category.LAUNCHER 1"
});
return process.waitFor() == 0;
} catch (Throwable t) {
Log.w(TAG, "monkey wake failed " + packageName + ": " + t.getMessage());
return false;
}
}
private static String resolveLauncherComponent(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(packageName);
if (intent != null && intent.getComponent() != null) {
return intent.getComponent().flattenToShortString();
}
Intent query = new Intent(Intent.ACTION_MAIN);
query.addCategory(Intent.CATEGORY_LAUNCHER);
query.setPackage(packageName);
List<ResolveInfo> list = pm.queryIntentActivities(query, 0);
if (!list.isEmpty()) {
return list.get(0).activityInfo.packageName + "/"
+ list.get(0).activityInfo.name;
}
} catch (Throwable ignored) {
}
return null;
}
private static boolean wakeByLaunchIntent(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(packageName);
if (intent == null) {
Intent query = new Intent(Intent.ACTION_MAIN);
query.addCategory(Intent.CATEGORY_LAUNCHER);
query.setPackage(packageName);
List<ResolveInfo> list = pm.queryIntentActivities(query, 0);
if (list.isEmpty()) {
return false;
}
intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setClassName(list.get(0).activityInfo.packageName, list.get(0).activityInfo.name);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
context.startActivity(intent);
return true;
} catch (Throwable t) {
Log.w(TAG, "launch wake failed " + packageName + ": " + t.getMessage());
return false;
}
}
private static void applyBatteryWhitelist(String packageName) {
try {
Runtime.getRuntime().exec(new String[]{
"su", "-c", "dumpsys deviceidle whitelist +" + packageName
}).waitFor();
} catch (Throwable ignored) {
}
}
private static void pressHomeByRoot() {
try {
Runtime.getRuntime().exec(new String[]{"su", "-c", "input keyevent 3"}).waitFor();
} catch (Throwable ignored) {
}
}
private static void sleepQuietly(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

View File

@@ -0,0 +1,78 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import java.util.concurrent.TimeUnit;
/**
* 间隔 &lt; 15 分钟时用前台服务 Handler 定时WorkManager 最短 15 分钟)。
*/
public final class MonitoredAppKeepAliveLoop {
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
private static Context appContext;
private static final Runnable TICK = new Runnable() {
@Override
public void run() {
if (appContext == null) {
return;
}
if (!NotificationService.isMonitoringActive(appContext)) {
stop();
return;
}
if (!KeepAliveSettings.isEnabled(appContext)) {
stop();
return;
}
if (!usesFastLoop(appContext)) {
stop();
return;
}
MonitoredAppActivator.activateAllAsync(appContext);
scheduleNext();
}
};
private MonitoredAppKeepAliveLoop() {
}
public static boolean usesFastLoop(Context context) {
return KeepAliveSettings.isEnabled(context)
&& KeepAliveSettings.getIntervalMinutes(context) < KeepAliveSettings.WORK_MANAGER_MIN_MINUTES;
}
public static void start(Context context) {
if (!usesFastLoop(context)) {
stop();
return;
}
appContext = context.getApplicationContext();
HANDLER.removeCallbacks(TICK);
// 先尽快执行一次,再按间隔循环
HANDLER.postDelayed(TICK, 3000);
}
public static void stop() {
HANDLER.removeCallbacks(TICK);
appContext = null;
}
public static void restart(Context context) {
stop();
start(context);
}
private static void scheduleNext() {
if (appContext == null) {
return;
}
long delayMs = TimeUnit.MINUTES.toMillis(KeepAliveSettings.getIntervalMinutes(appContext));
HANDLER.postDelayed(TICK, delayMs);
}
}

View File

@@ -0,0 +1,49 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import java.util.concurrent.TimeUnit;
public final class MonitoredAppKeepAliveScheduler {
private static final String WORK_NAME = "monitored_app_keep_alive";
private MonitoredAppKeepAliveScheduler() {
}
public static void schedule(Context context) {
Context appContext = context.getApplicationContext();
if (!KeepAliveSettings.isEnabled(appContext)) {
cancel(appContext);
return;
}
int interval = KeepAliveSettings.getIntervalMinutes(appContext);
if (interval < KeepAliveSettings.WORK_MANAGER_MIN_MINUTES) {
WorkManager.getInstance(appContext).cancelUniqueWork(WORK_NAME);
MonitoredAppKeepAliveLoop.start(appContext);
return;
}
MonitoredAppKeepAliveLoop.stop();
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(
MonitoredAppKeepAliveWorker.class, interval, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(appContext).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
request);
}
public static void cancel(Context context) {
Context appContext = context.getApplicationContext();
WorkManager.getInstance(appContext).cancelUniqueWork(WORK_NAME);
MonitoredAppKeepAliveLoop.stop();
}
}

View File

@@ -0,0 +1,34 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.miraclegarden.smsmessage.KeepAliveSettings;
public class MonitoredAppKeepAliveWorker extends Worker {
private static final String TAG = "MonitoredAppKeepAlive";
public MonitoredAppKeepAliveWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
if (!KeepAliveSettings.isEnabled(getApplicationContext())) {
return Result.success();
}
if (!NotificationService.isMonitoringActive(getApplicationContext())) {
Log.d(TAG, "monitoring off, skip");
return Result.success();
}
Log.i(TAG, "periodic keep-alive tick");
MonitoredAppActivator.activateAll(getApplicationContext());
return Result.success();
}
}

View File

@@ -2,24 +2,22 @@ package com.miraclegarden.smsmessage.service;
import android.app.Notification;
import android.os.Bundle;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:通知内容多层提取工具,兼容不同厂商字段差异
* 通知内容多层提取工具,兼容不同厂商字段差异。
* Telegram 常用 InboxStyle / MessagingStyle不能只读 EXTRA_TEXT。
*/
public class NotificationExtractor {
private static final String EXTRA_MESSAGES = "android.messages";
private static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
private NotificationExtractor() {
}
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:提取通知标题、内容和提取时间戳
*/
public static Result extract(StatusBarNotification sbn) {
long timestamp = System.currentTimeMillis();
if (sbn == null || sbn.getNotification() == null) {
@@ -35,14 +33,18 @@ public class NotificationExtractor {
if (extras != null) {
title = pickFirstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TITLE),
extras.getCharSequence(Notification.EXTRA_TITLE_BIG)
extras.getCharSequence(Notification.EXTRA_TITLE_BIG),
extras.getCharSequence(EXTRA_CONVERSATION_TITLE),
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT)
);
content = pickFirstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TEXT),
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
extras.getCharSequence(Notification.EXTRA_INFO_TEXT),
extras.getCharSequence(Notification.EXTRA_SUB_TEXT)
extras.getCharSequence(Notification.EXTRA_SUB_TEXT),
extractMessagingStyleContent(extras),
extractInboxLines(extras)
);
}
@@ -54,16 +56,61 @@ public class NotificationExtractor {
content = normalized(notification.tickerText);
}
if (title == null) {
title = "";
}
if (content == null) {
content = "";
if (TextUtils.isEmpty(content) && !TextUtils.isEmpty(title)
&& normalized(notification.tickerText).startsWith(title)) {
String ticker = normalized(notification.tickerText);
if (ticker.length() > title.length()) {
content = ticker.substring(title.length()).trim();
}
}
return new Result(title, content, timestamp);
}
/** Telegram 等 App 的 InboxStyle最新消息在 textLines 末尾。 */
private static String extractInboxLines(Bundle extras) {
CharSequence[] lines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
if (lines == null || lines.length == 0) {
return "";
}
for (int i = lines.length - 1; i >= 0; i--) {
String line = normalized(lines[i]);
if (!TextUtils.isEmpty(line)) {
return line;
}
}
return "";
}
/** MessagingStyle从 android.messages 取最新一条正文。 */
private static String extractMessagingStyleContent(Bundle extras) {
Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
if (messages == null || messages.length == 0) {
return "";
}
for (int i = messages.length - 1; i >= 0; i--) {
String text = extractMessagingMessageText(messages[i]);
if (!TextUtils.isEmpty(text)) {
return text;
}
}
return "";
}
private static String extractMessagingMessageText(Parcelable parcelable) {
if (parcelable == null) {
return "";
}
if (parcelable instanceof Bundle) {
Bundle bundle = (Bundle) parcelable;
return pickFirstNonEmpty(
bundle.getCharSequence("text"),
bundle.getCharSequence(Notification.EXTRA_TEXT)
);
}
return "";
}
private static String pickFirstNonEmpty(CharSequence... values) {
if (values == null || values.length == 0) {
return "";
@@ -103,11 +150,6 @@ public class NotificationExtractor {
return ticker;
}
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:通知提取结果对象
*/
public static class Result {
public String title;
public String content;

View File

@@ -27,6 +27,7 @@ import androidx.core.app.NotificationCompat;
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.AppConfig;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.model.ApiError;
@@ -65,6 +66,8 @@ public class NotificationService extends NotificationListenerService {
private RetryManager retryManager;
private PowerManager.WakeLock wakeLock;
private static boolean isMonitoring = false;
private static final String PREF_SERVER = "server";
private static final String KEY_MONITORING_ACTIVE = "monitoring_active";
private ApiService apiService;
private TokenManager tokenManager;
@@ -100,6 +103,8 @@ public class NotificationService extends NotificationListenerService {
} else if (ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
handleHookMessageIntent(intent);
}
} else if (isMonitoringActive(this)) {
MonitoredAppKeepAliveScheduler.schedule(this);
}
NotificationActivity.sendMessage("监听服务成功!");
return START_STICKY;
@@ -146,20 +151,41 @@ public class NotificationService extends NotificationListenerService {
return isMonitoring;
}
public static boolean isMonitoringActive(Context context) {
if (isMonitoring) {
return true;
}
return context.getSharedPreferences(PREF_SERVER, Context.MODE_PRIVATE)
.getBoolean(KEY_MONITORING_ACTIVE, false);
}
private static void setMonitoringActive(Context context, boolean active) {
isMonitoring = active;
context.getSharedPreferences(PREF_SERVER, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_MONITORING_ACTIVE, active)
.apply();
}
private void startMonitoring() {
if (isMonitoring) return;
isMonitoring = true;
setMonitoringActive(this, true);
toggleNotificationListenerService(this);
MonitoredAppKeepAliveScheduler.schedule(this);
if (KeepAliveSettings.isEnabled(this)) {
MonitoredAppActivator.activateAllAsync(this);
}
updateForegroundNotification(retryManager != null
? (AppConfig.ENABLE_SERVER_UPLOAD ? retryManager.getUploadedCount() : retryManager.getTotalCount())
: 0);
NotificationActivity.sendMessage("已开启持续监听模式");
NotificationActivity.sendMessage("已开启持续监听模式(含 App 保活)");
NotificationActivity.updateUI();
}
public static void stopMonitoring(Context context) {
isMonitoring = false;
setMonitoringActive(context, false);
MonitoredAppKeepAliveScheduler.cancel(context);
NotificationActivity.sendMessage("已停止监听服务");
NotificationActivity.updateUI();
Toast.makeText(context, "监听服务已停止", Toast.LENGTH_SHORT).show();
@@ -205,11 +231,13 @@ public class NotificationService extends NotificationListenerService {
if (messageInfo == null) return;
NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
Log.d(TAG, "onNotificationPosted: pkg=" + sbn.getPackageName()
Log.i(TAG, "onNotificationPosted: pkg=" + sbn.getPackageName()
+ " title=" + result.title + " content=" + result.content);
if (TextUtils.isEmpty(result.title) && TextUtils.isEmpty(result.content)) {
Log.d(TAG, "skip empty notification: " + sbn.getPackageName());
Log.w(TAG, "skip empty notification: " + sbn.getPackageName()
+ " extras=" + (sbn.getNotification().extras != null
? sbn.getNotification().extras.keySet() : "null"));
NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
return;
}
@@ -342,7 +370,6 @@ public class NotificationService extends NotificationListenerService {
@Override
public void onDestroy() {
super.onDestroy();
isMonitoring = false;
NotificationActivity.updateUI();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();

View File

@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/keep_alive_settings_title"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="4dp"
android:text="@string/keep_alive_settings_hint"
android:textColor="#666666"
android:textSize="13sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/white"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/keep_alive_enabled"
android:textColor="#333333"
android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/keep_alive_interval_label"
android:textColor="#333333"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_interval"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/keep_alive_cooldown_label"
android:textColor="#333333"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_cooldown"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/keep_alive_stealth_mode"
android:textColor="#333333"
android:textSize="14sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_stealth_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/keep_alive_prefer_root"
android:textColor="#333333"
android:textSize="14sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_prefer_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/keep_alive_kill_before_launch"
android:textColor="#333333"
android:textSize="14sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_kill_before_launch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/keep_alive_return_home"
android:textColor="#333333"
android:textSize="14sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_return_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btn_wake_now"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:backgroundTint="@color/purple_500"
android:text="@string/keep_alive_wake_now"
android:textColor="@color/white" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -46,6 +46,34 @@
</LinearLayout>
<!-- 添加按钮 -->
<LinearLayout
android:id="@+id/row_keep_alive_settings"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="8dp"
android:background="@color/white"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/keep_alive_settings_entry"
android:textColor="#333333"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#999999"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"

View File

@@ -1,4 +1,17 @@
<resources>
<string name="app_name">通知管理</string>
<string name="key">#$%^*()XCVBNM</string>
<string name="keep_alive_settings_title">App 保活设置</string>
<string name="keep_alive_settings_hint">TG 在后台时主要靠「通知监听」收消息,不必反复打开 TG。静默保活进程还在就不弹 App只有被系统杀掉才短暂启动并立刻回桌面。</string>
<string name="keep_alive_stealth_mode">静默保活(推荐,进程在则不弹 App</string>
<string name="keep_alive_enabled">启用 App 保活</string>
<string name="keep_alive_interval_label">定期间隔114 分钟需保持「监听中」15 分钟起后台也可用)</string>
<string name="keep_alive_cooldown_label">同一 App 唤醒冷却</string>
<string name="keep_alive_prefer_root">优先使用 Root 唤醒</string>
<string name="keep_alive_return_home">唤起后自动返回桌面</string>
<string name="keep_alive_kill_before_launch">启动前先强制停止(彻底重启 / 刷新 Hook</string>
<string name="keep_alive_wake_now">立即唤醒全部监听 App</string>
<string name="keep_alive_wake_started">已开始唤醒,请查看监听日志</string>
<string name="keep_alive_wake_disabled">请先开启 App 保活</string>
<string name="keep_alive_settings_entry">App 保活设置</string>
</resources>

134
docs/BANK_REVERSE.md Normal file
View File

@@ -0,0 +1,134 @@
# 澳大利亚银行 App 逆向报告
从 Pixel 6 设备拉取 APK2026-07-02用 DEX 字符串扫描 + 类名分析确定 Hook 点。
APK 位置(本地,未入库):`reverse/apks/*-base.apk`
---
## 1. 包名与架构
| App | 包名 | 技术栈 | 推送方案 |
|-----|------|--------|----------|
| **Up** | `au.com.up.money` | React Native | 原生 `HandlerService` + RN Firebase |
| **Suncorp** | `au.com.suncorp.marketplace` | Kotlin 原生 | `SuncorpMessagingService` (FCM) |
| **ubank** | `au.com.bank86400` | Capacitor (Web) | MoEngage + Capacitor FCM 插件 |
---
## 2. Hook 点(已实现)
### Up Bank
| 路径 | 类 | 方法 |
|------|-----|------|
| **FCM 主路径** | `au.com.up.money.notifications.HandlerService` | `onMessageReceived(RemoteMessage)` |
| **前台兜底** | `android.app.NotificationManager` | `notify(...)` |
相关类DEX 中发现):
- `au.com.up.money.notifications.handlers.NotificationHandler`
- `au.com.up.money.notifications.Util$NotificationType`
- `Lio/invertase/firebase/messaging/ReactNativeFirebaseMessagingService`RN 层,已由 HandlerService 覆盖)
### Suncorp Bank
| 路径 | 类 | 方法 |
|------|-----|------|
| **FCM 主路径** | `au.com.suncorp.marketplace.base.application.SuncorpMessagingService` | `onMessageReceived(RemoteMessage)` |
| **前台兜底** | `NotificationManager.notify` | extras 取 title/text |
相关:
- `FirebaseService.registerPushNotification`
- `BankingPaymentNotification` / `incomingPaymentNotification`
- `NotificationDetailsPresenter` / `NotificationDetailsActivity`
### ubank
| 路径 | 类 | 方法 |
|------|-----|------|
| **MoEngage FCM** | `com.moengage.firebase.MoEFireBaseMessagingService` | `onMessageReceived(RemoteMessage)` |
| **Capacitor FCM** | `io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService` | `onMessageReceived(RemoteMessage)` |
| **前台兜底** | `NotificationManager.notify` | extras |
日志字符串:`onMessageReceived() : Will try to show push``Not a MoEngage Payload`
---
## 3. 源码位置xposed-module
| 文件 | 说明 |
|------|------|
| `hook/UpBankMessageHook.java` | Up 专用 |
| `hook/SuncorpBankMessageHook.java` | Suncorp 专用 |
| `hook/UbankMessageHook.java` | ubank 专用 |
| `hook/BankHookHelper.java` | RemoteMessage / Notification 解析、去重 |
| `hook/BankNotificationHook.java` | 三家共用的 notify 兜底 |
日志 source 标识:
- `xposed_up` / `xposed_up_notify`
- `xposed_suncorp` / `xposed_suncorp_notify`
- `xposed_ubank` / `xposed_ubank_notify`
---
## 4. 数据提取逻辑
### RemoteMessageFCM
1. `getNotification().getTitle()` / `getBody()`
2. 若 body 为空 → 拼接 `getData()` 键值对
### Notification前台兜底
`extras` 读取:
- `EXTRA_TITLE` / `EXTRA_TEXT`
- `EXTRA_BIG_TEXT`
- `gcm.n.title` / `gcm.n.body`
---
## 5. 安装与验证
```powershell
powershell -ExecutionPolicy Bypass -File scripts\install-full.ps1
```
1. notiMessage **应用列表** 添加三家银行
2. LSPosed 作用域勾选:**银行 App + notiMessage + xposed 模块**
3. 强制停止银行 App 后重开
4. 测试:
```powershell
adb logcat | findstr /i "notiMessageHook/Bank notiMessageHook/Up notiMessageHook/Suncorp notiMessageHook/ubank HookMessageReceiver"
```
| 场景 | 期望 |
|------|------|
| 银行 App **前台** 收到推送 | `[Hook/xposed_up]` 等 |
| 银行 App **后台** 弹通知 | 通知通道 或 `xposed_*_notify` |
---
## 6. 限制与后续
- 银行可能 **不在通知里显示金额**安全策略Hook 也只能拿到 App 愿意展示的内容
- ubank 非 MoEngage 载荷会走 Capacitor 路径,需实测哪条触发
- 若 FCM 加密或仅静默同步,需再 Hook 业务层(如 `TransactionHistoryResponse`)——当前 DEX 未见稳定明文入库点
- 逆向脚本:`reverse/scripts/scan_dex.py``reverse/scripts/scan_target.py`
---
## 7. 复现逆向
```powershell
$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
# 拉 base.apk
& $adb shell pm path au.com.up.money
& $adb pull <path> reverse/apks/up-base.apk
# 扫描
python reverse/scripts/scan_dex.py reverse/extracted
```

248
docs/WORKLOG_2026-07-03.md Normal file
View File

@@ -0,0 +1,248 @@
# 工作说明 — 2026-07-03
本文档汇总 **2026 年 7 月 3 日**`notiMessage` 项目上围绕 **MariBank / SeaBank PH 注册与风控绕过** 所完成的分析、实现、测试与仓库整理工作。
---
## 1. 当日目标
在已 Root 的 Pixel 6Magisk + Zygisk + LSPosed对菲律宾 MariBank App 完成:
1. 绕过本地 Root / 设备风控,进入注册流程;
2. 输入手机号并点击 **Next**,完成 `POST /uapi/v2/register` 注册请求。
**目标 App**
| 项 | 值 |
| --- | ------------------------- |
| 包名 | `ph.seabank.seabank` |
| 显示名 | MariBank |
| 版本 | 3.22.0versionCode 32200 |
---
## 2. 核心结论(当日最终状态)
| 层级 | 状态 | 说明 |
| --------------- | ----------- | ------------------------------------------------------------------------------------------- |
| 本地 Root 弹窗 / 自杀 | **已基本绕过** | 可进入首页与手机号输入页 |
| DFP 上报 | **成功** | `POST /dfp/v1/data/report` 返回 `code=0` |
| riskToken 尾部净化 | **Hook 生效** | `|09|1``|00|0`logcat 可见) |
| 注册接口 | **仍失败** | `POST /uapi/v2/register``**4067012`**(亦见 `4067004` |
| 服务端文案 | 账户安全临时封锁 | 「For your account's security, this service has been temporarily blocked...」(+632) 8424 8050 |
**判断**:本地 bypass 已推进到 DFP 层,但 **服务端仍拒绝注册**。原因可能包括:加密 DFP 载荷校验、设备指纹黑名单、号码/设备组合风控,而非单纯 riskToken 尾部字段。改 token 尾部 **不足以** 通过注册。
---
## 3. Xposed 模块实现(`xposed-module`
### 3.1 新增 / 主要 Hook 文件
| 文件 | 作用 |
| ---------------------------------- | ------------------------------------------------------------ |
| `hook/MariBankRootBypassHook.java` | SafeMode、SHPSSDK、OkHttp 出站净化、4067 错误日志、Root 自杀拦截 |
| `hook/MariBankShpsNativeHook.java` | `/proc` 过滤、native-core、boot 属性伪装、`requestDefense` 等 native 层 |
| `hook/MariBankRiskTokenUtil.java` | riskToken / deviceToken 尾部 `|09|1``|00|0` |
| `MainHook.java` | 对 `ph.seabank.seabank` 加载上述 Hook先 early proc再 deferred |
### 3.2 关键逆向结论(已写入 Hook
| 能力 | 真实类 / 库 |
| -------------- | ------------------------------------------------------- |
| riskToken 生成链 | `vvuuuuvvv.wwvuwuwvu(Context)`classes11.dex |
| SoUtils | `com.shopee.bke.lib.jni.utils.f`log tag `SoUtils` |
| Native 加密包装 | `com.shopee.bke.lib.jni.utils.d` → native `uvwuvwuv` |
| SHPSSDK native | `libshpssdk_bank.so``wvvvuwwu.vvuwuuvuu``wwvwvwuvv` |
| 字符串解密 | `uvuwwuvwv.uvwwuuvvw.uvuwwwuwu(hex, key)` |
### 3.3 明确避免 Hook 的点(实测会崩)
- `**RealInterceptorChain.proceed`**SO 硬编码检测Hook 后易触发风控或异常。
- `**ShpssInstall` / `vuvuwwwuw**`:干扰 `SoUtils.loadSoLibrary`,导致 `libsdkutils` 加载死循环 **白屏**
### 3.4 注册请求体
- register body 为 **native 加密**Java 层 Hook 常见日志:`register request body unreadable (encrypted or one-shot)`
- 当日 **未** 在 logcat 中抓到 register 明文 JSON。
---
## 4. 逆向与静态分析(`reverse/`
### 4.1 已完成
- 从设备 pull split APK / base APK提取 `libsdkutils.so``libshpssdk_bank.so``libbkutils.so` 等至 `reverse/extracted/native/`
- 编写并运行大量 DEX/APK 扫描脚本scan / dump / find 系列),定位:
- SHPSSDK、SafeMode、注册 API、4067 错误上下文;
- JNI 目标与加密类继承关系。
- 脚本:`reverse/frida/pull_split_apk.ps1``reverse/scripts/extract_all_so.py` 等。
### 4.2 主要 native / 加密链路
```
register 请求
→ NativeEncryptUtilsWrapper (utils.d)
→ native uvwuvwuv (libsdkutils.so / libbkutils.so)
→ 加密 body + riskToken / deviceToken
→ POST https://api.seabank.ph/uapi/v2/register
```
---
## 5. Frida 动态分析
### 5.1 环境
- PCFrida **17.15.3**;手机:`/data/local/tmp/frida-server`
- 脚本:`reverse/frida/trace_maribank_register.js`
- 运行器:`run_frida_trace.py`attach / spawn`run_spawn_trace.py``start-mari-trace.ps1`
### 5.2 遇到的问题
| 模式 | 现象 |
| ---------- | ---------------------------------------------------------------------- |
| **attach** | 长时间 `Java.available == false`Java Hook 无法安装;进程名需用 **MariBank** 而非包名查找 |
| **spawn** | native `dlopen` 可 hook但 Java 层 `Java is not defined` / 线程相关异常 |
| CLI spawn | 无 `--no-pause` 时 session 立即退出;需 Python runner 保活 |
### 5.3 当日 Frida 结论
- **未抓到** 有效的 `/register` 明文 body 或完整 HTTP 链日志。
- MariBank 对 Frida 有较强干扰;**当日有效证据主要来自 LSPosed logcat**,而非 Frida trace。
---
## 6. Logcat 实测摘要LSPosed 生效)
**测试号码示例**`9178854266``9133477799``9171243667` 等(均失败)。
**典型成功链路**
```text
POST /dfp/v1/data/report → {"code":0,...}
riskToken sanitized ... -> |00|0
```
**典型失败链路**
```text
POST /uapi/v2/register
→ {"code":4067012,"msg":"For your account's security, this service has been temporarily blocked..."}
```
**其他日志**
- `wvvvuwwu.vuwuuwvw` native 实现未找到(可能与 LSPosed / 环境冲突有关)。
- Root 检测触发 `killProcess` 时被 `blocked finish` 拦截,表现为短暂异常后仍可继续使用。
**推荐 logcat 过滤**
```powershell
powershell -File scripts\logcat-maribank.ps1 -Follow
# 或
adb logcat -s notiMessageHook/MariBankRoot:V MB-TRACE:V
```
---
## 7. 构建与安装脚本
| 脚本 | 用途 |
| ------------------------------ | ------------------------------------ |
| `scripts/build-debug.ps1` | 构建主 App + xposed-module debug APK |
| `scripts/install-debug.ps1` | 安装双 APK |
| `scripts/install-frida.ps1` | PC + 手机 frida-server |
| `scripts/start-mari-trace.ps1` | 启动 Frida attach + MariBank 相关 logcat |
| `scripts/logcat-maribank.ps1` | 过滤 MariBank / Hook 日志 |
| `scripts/organize-reverse.ps1` | 整理 `reverse/` 目录(可重复执行) |
LSPosed 作用域需勾选:**MariBank + notiMessage + xposed 模块**。
---
## 8. 仓库整理
### 8.1 问题
`reverse/` 根目录堆积 **70+ Python 脚本**、临时 `.dex`、解压 APK**2.2 GB / 7 万+ 文件**IDE 与 `git status` 显得极乱。
### 8.2 新目录结构
```
reverse/
├── README.md
├── scripts/ # 逆向 Python 脚本71 个,含 _paths.py
├── frida/ # Frida 脚本与运行器
├── apks/ # APK / zip不入库
├── extracted/ # 解压、dex dump、.so不入库
├── output/ # 分析报告 *.txt
├── logs/ # 运行日志
│ └── frida/ # trace / logcat
└── tmp/ # 临时 *.dex不入库
```
### 8.3 同步修改
- **44 个** 脚本内路径改为相对 `reverse/` 根(`apks/``extracted/``tmp/``output/`)。
- Frida 日志输出改到 `reverse/logs/frida/`
- 更新 `.gitignore``docs/BANK_REVERSE.md``pull_split_apk.ps1``start-mari-trace.ps1`
- `apk_extract/` 迁入 `extracted/apk_extract/``seabank.zip` 迁入 `apks/`
---
## 9. 后续建议(未在当日完成)
| 优先级 | 方向 |
| --- | -------------------------------------------------------------------- |
| 1 | **对照实验**:未 Root 干净机 + 菲律宾 IP + 新号码,区分设备封禁 vs 号码封禁 |
| 2 | **Native 明文**Hook `libsdkutils` / `utils.d` 包装层,抓 register 加密前 body |
| 3 | **riskToken 全链**:在 `vvuuuuvvv.wwvuwuwvu` 最早出口做更深层净化(含加密段) |
| 4 | **Frida**:换未被检测环境,或仅 attach 已运行进程;当前 MariBank 上 Java 桥不稳定 |
| 5 | **停试**:已失败号码/设备冷却 **2448h**,避免加重 `4067012` |
| 6 | **官方解封**(+632) 8424 8050 |
---
## 10. 相关文档与路径索引
| 文档 / 路径 | 说明 |
| ---------------------------------- | ----------------------------------- |
| `docs/BANK_REVERSE.md` | 澳大利亚三家银行 HookUp / Suncorp / ubank |
| `docs/HOOK_GUIDE.md` | 通用 Hook 架构与扩展 |
| `docs/CHANGELOG.md` | 主 App / TG Hook 版本历史 |
| `reverse/README.md` | 逆向工作区目录说明 |
| `reverse/frida/jni_targets.md` | JNI Hook 目标备忘 |
| `xposed-module/.../MariBank*.java` | MariBank bypass 实现 |
---
## 11. 当日工作清单(摘要)
- [x] MariBank Root / SHPSSDK / SafeMode Xposed bypass 实现与迭代
- [x] riskToken 尾部净化与 OkHttp 出站拦截
- [x] DEX / SO / JNI 静态逆向与脚本沉淀
- [x] Frida trace 脚本与 attach/spawn 运行器(受 App 反 Frida 限制)
- [x] LSPosed logcat 分析,确认 **4067012** 为服务端拒绝
- [x] `reverse/` 目录整理与 `.gitignore` 更新
- [ ] 注册成功(**未完成**,阻塞于服务端风控)
---
*记录日期2026-07-03*

19
reverse/README.md Normal file
View File

@@ -0,0 +1,19 @@
# reverse/ 逆向工作区
| 目录 | 内容 |
|------|------|
| `scripts/` | DEX/APK 扫描与分析 Python 脚本 |
| `frida/` | Frida trace 脚本与运行器 |
| `apks/` | 从设备拉取的 APK不入库 |
| `extracted/` | 解压产物、dex dump、`.so`(不入库) |
| `output/` | 脚本输出的 `.txt` 分析报告 |
| `logs/` | 运行日志(含 `logs/frida/` |
| `tmp/` | 临时 `.dex` 文件(不入库) |
常用命令(在项目根目录执行):
```powershell
python reverse/scripts/scan_dex.py reverse/extracted
python reverse/frida/run_frida_trace.py attach
powershell -File reverse/frida/pull_split_apk.ps1
```

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""Quick reference: register crypto JNI targets for Frida."""
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
OUT = Path(__file__).resolve().parent / "frida" / "jni_targets.md"
TARGETS = [
"Lcom/shopee/bke/lib/jni/utils/d;", # NativeEncryptUtilsWrapper
"Lcom/shopee/bke/lib/jni/utils/uvwuvwuv;", # NativeEncryptUtils (sdkutils JNI)
"Lcom/shopee/bke/lib/jni/utils/f;", # SoUtils.loadSoLibrary
"Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;",
"Lcom/shopee/shpssdkbank/wvvvuwwu;",
]
lines = [
"# MariBank v3.22 register / crypto JNI targets",
"",
"## sdkutils (注册 body 加密)",
"- `com.shopee.bke.lib.jni.utils.d` — NativeEncryptUtilsWrapper",
"- `com.shopee.bke.lib.jni.utils.uvwuvwuv` — NativeEncryptUtils (native)",
"- `com.shopee.bke.lib.jni.utils.f` — SoUtils → loads `libsdkutils.so`",
"",
"## shpssdk_bank (riskToken / DFP)",
"- `vvuuuuvvv.wwvuwuwvu(Context)` — getRiskToken 真实入口",
"- `wvvvuwwu` — native bridge (`vvuwuuvuu` → `wwvwvwuvv`)",
"",
"## dexdump natives",
"",
]
with zipfile.ZipFile(str(APK)) as zf:
dex = zf.read("classes8.dex")
tmp = Path(__file__).resolve().parent / "tmp_frida_ref.dex"
tmp.write_bytes(dex)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in TARGETS:
lines.append("### " + target)
cap = False
for line in out.splitlines():
if ("Class descriptor : '" + target + "'") in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap and ("NATIVE" in line or ("name :" in line and "type :" not in line)):
safe = line.encode("ascii", "replace").decode()
if "name :" in safe:
lines.append("- " + safe.strip())
lines.append("")
dex11 = zf.read("classes11.dex")
tmp.write_bytes(dex11)
out11 = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in TARGETS[3:]:
lines.append("### " + target)
cap = False
for line in out11.splitlines():
if ("Class descriptor : '" + target + "'") in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap and "NATIVE" in line:
lines.append("- " + line.encode("ascii", "replace").decode().strip())
lines.append("")
OUT.write_text("\n".join(lines), encoding="utf-8")
print("written", OUT)

View File

@@ -0,0 +1,44 @@
# MariBank v3.22 — Frida trace 目标(注册加密)
## sdkutils注册 body 很可能经此加密)
| 类 | 说明 |
|----|------|
| `com.shopee.bke.lib.jni.utils.f` | SoUtils`loadSoLibrary("sdkutils")` |
| `com.shopee.bke.lib.jni.utils.uvwuvwuv` | NativeEncryptUtils**PUBLIC STATIC NATIVE** |
| `com.shopee.bke.lib.jni.utils.d` | NativeEncryptUtilsWrapper调用 `uvwuvwuv.uvwuuww([B,String,Z,J)[[B` |
logcat 标签:`NativeEncrypt: loading JNI``CharacterCryptoManager`
## libshpssdk_bank.soriskToken / DFP
| 类 / 方法 | 说明 |
|-----------|------|
| `vvuuuuvvv.wwvuwuwvu(Context)` | getRiskToken 真实入口 |
| `wvvvuwwu.vvuwuuvuu(String,ZZ)` | → native `wwvwvwuvv(int,String)` |
| `wvvvuwwu.vuwuuuwv([B,[B)` | requestDefense 解密 |
| `SHPSSDK.requestDefense` | 出站 HTTP 头 `x-sap-fixme` |
## 运行
```powershell
# 1. 手机启动 frida-server (root)
adb push frida-server /data/local/tmp/
adb shell su -c 'chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server -D &'
# 2. PC 安装 frida-tools 后
cd reverse\frida
.\run-frida-trace.ps1 -Mode spawn
# 3. App 内 Sign up → 输入号码 → Next
# 关注 [MB-TRACE] NativeEncryptWrapper / NativeEncryptUtils / HTTP .../register
```
建议测试时**暂时关闭 LSPosed 对 MariBank 的作用域**,避免与 Frida 冲突。
## 预期输出
- `RegisterNatives libsdkutils.so ...` — JNI 符号
- `NativeEncryptWrapper.*` — 加密前明文(若走 Java 包装)
- `HTTP POST .../uapi/v2/register` — 请求/响应 body
- `vvuuuuvvv.wwvuwuwvu ret` — riskToken 全文

View File

@@ -0,0 +1,30 @@
# Pull native libs from connected device (Pixel 6 with MariBank installed)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
$OutDir = Join-Path $Root "extracted\native"
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$AdbCandidates = @(
(Join-Path $Root "..\platform-tools\adb.exe"),
"$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe",
"adb"
)
$Adb = $AdbCandidates | Where-Object { $_ -eq "adb" -or (Test-Path $_) } | Select-Object -First 1
if (-not $Adb) { throw "adb not found" }
$Pkg = "ph.seabank.seabank"
$Base = & $Adb shell pm path $Pkg 2>$null
if (-not $Base) { throw "package $Pkg not installed on device" }
$Paths = ($Base -split "`n" | ForEach-Object { $_.Trim() -replace "^package:", "" })
foreach ($ApkPath in $Paths) {
$Name = Split-Path $ApkPath -Leaf
$LocalApk = Join-Path $OutDir $Name
Write-Host "pull $ApkPath -> $LocalApk"
& $Adb pull $ApkPath $LocalApk | Out-Null
if ($Name -like "split_config.arm64*") {
python (Join-Path $Root "scripts\extract_all_so.py")
}
}
Write-Host "done. SO files in $OutDir"

View File

@@ -0,0 +1,3 @@
# MariBank Frida trace dependencies (host PC)
frida>=16.0.0
frida-tools>=12.0.0

View File

@@ -0,0 +1,66 @@
# Run MariBank register Frida trace on connected device
param(
[ValidateSet("spawn", "attach")]
[string]$Mode = "attach",
[string]$Package = "ph.seabank.seabank"
)
$ErrorActionPreference = "Stop"
$Here = $PSScriptRoot
$LogsDir = Join-Path (Split-Path $Here -Parent) "logs\frida"
New-Item -ItemType Directory -Force -Path $LogsDir | Out-Null
$Script = Join-Path $Here "trace_maribank_register.js"
$LogFile = Join-Path $LogsDir "trace_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
if (-not (Test-Path $Script)) { throw "missing $Script" }
# adb
$AdbCandidates = @(
(Join-Path (Split-Path $Here -Parent) "..\platform-tools\adb.exe"),
"$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe",
"adb"
)
$Adb = $AdbCandidates | Where-Object { $_ -eq "adb" -or (Test-Path $_) } | Select-Object -First 1
if (-not $Adb) { Write-Warning "adb not in PATH — ensure device connected" }
# frida / python module
$FridaCmd = Get-Command frida -ErrorAction SilentlyContinue
if (-not $FridaCmd) {
Write-Host "Installing frida-tools..."
python -m pip install -r (Join-Path $Here "requirements.txt")
}
Write-Host @"
=== MariBank Frida Register Trace ===
Package : $Package
Mode : $Mode
Script : $Script
Log : $LogFile
(Pixel 6):
1. adb devices
2. push frida-server root :
adb push frida-server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server
adb shell su -c '/data/local/tmp/frida-server -D &'
3. LSPosed App Frida
4. : Sign up -> -> Next
"@
$Py312 = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
$Runner = Join-Path $Here "run_frida_trace.py"
if (Test-Path $Py312) -and (Test-Path $Runner) {
Write-Host "Using persistent Python runner: $Runner $Mode"
& $Py312 $Runner $Mode
exit $LASTEXITCODE
}
$FridaArgs = @("-U", "-f", $Package, "-l", $Script, "-o", $LogFile)
if ($Mode -eq "attach") {
$FridaArgs = @("-U", $Package, "-l", $Script, "-o", $LogFile)
}
Write-Host "frida $($FridaArgs -join ' ')"
& frida @FridaArgs

View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""Persistent Frida trace session (avoids CLI exit on piped stdin)."""
import sys
import time
from datetime import datetime
from pathlib import Path
import frida
PKG = "ph.seabank.seabank"
HERE = Path(__file__).resolve().parent
LOGS_DIR = HERE.parent / "logs" / "frida"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
SCRIPT = HERE / "trace_maribank_register.js"
LOG = LOGS_DIR / ("trace_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
def on_message(message, data):
if message.get("type") == "send":
line = message.get("payload")
else:
line = str(message)
text = line if isinstance(line, str) else repr(line)
print(text, flush=True)
with open(LOG, "a", encoding="utf-8") as f:
f.write(text + "\n")
if message.get("type") == "error":
err_log = str(LOG) + ".err"
with open(err_log, "a", encoding="utf-8") as f:
f.write(text + "\n")
def wait_for_process(device, pkg, timeout_sec=30):
deadline = time.time() + timeout_sec
while time.time() < deadline:
for app in device.enumerate_applications():
if app.identifier == pkg and app.pid and app.pid > 0:
return app.pid
for proc in device.enumerate_processes():
if proc.name == pkg:
return proc.pid
params = getattr(proc, "parameters", None) or {}
if params.get("identifier") == pkg:
return proc.pid
time.sleep(0.5)
return None
def launch_app(pkg):
import subprocess
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
subprocess.run(
[adb, "shell", "am", "force-stop", pkg],
check=False,
capture_output=True,
)
time.sleep(1)
subprocess.run(
[adb, "shell", "monkey", "-p", pkg, "-c", "android.intent.category.LAUNCHER", "1"],
check=False,
capture_output=True,
)
def ensure_frida_server():
import subprocess
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
out = subprocess.run(
[adb, "shell", "su", "-c", "pgrep frida-server"],
capture_output=True,
text=True,
)
if out.stdout.strip():
return
subprocess.run(
[adb, "shell", "su", "-c", "/data/local/tmp/frida-server -D &"],
check=False,
capture_output=True,
)
time.sleep(2)
def main():
mode = "attach"
if len(sys.argv) > 1:
mode = sys.argv[1]
ensure_frida_server()
device = frida.get_usb_device(timeout=10)
source = SCRIPT.read_text(encoding="utf-8")
pid = None
if mode == "spawn":
print("Spawning %s ..." % PKG)
pid = device.spawn([PKG])
session = device.attach(pid)
else:
print("Attaching %s ..." % PKG)
pid = wait_for_process(device, PKG, 3)
if pid is None:
print("Launching MariBank ...")
launch_app(PKG)
pid = wait_for_process(device, PKG, 60)
if pid is None:
raise SystemExit("MariBank not running after 60s — open app manually and re-run attach")
print("Found pid=%s" % pid)
session = device.attach(pid)
script = session.create_script(source)
script.on("message", on_message)
script.load()
if mode == "spawn":
device.resume(pid)
print("Resumed pid=%s, waiting for JVM..." % pid)
time.sleep(10)
else:
print("Attached pid=%s" % pid)
time.sleep(3)
print("Trace running. Log: %s" % LOG)
print("操作: Sign up -> 输入号码 -> Next (Ctrl+C 结束)")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Stopping...")
session.detach()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
"""Spawn MariBank, resume after script load, wait for Java."""
import frida
import sys
import time
from datetime import datetime
from pathlib import Path
PKG = "ph.seabank.seabank"
HERE = Path(__file__).resolve().parent
LOGS_DIR = HERE.parent / "logs" / "frida"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
SCRIPT = HERE / "trace_maribank_register.js"
LOG = LOGS_DIR / ("trace_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
def on_message(message, data):
if message.get("type") == "send":
line = message.get("payload")
else:
line = str(message)
text = line if isinstance(line, str) else repr(line)
print(text, flush=True)
with open(LOG, "a", encoding="utf-8") as f:
f.write(text + "\n")
def main():
import subprocess
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
subprocess.run([adb, "shell", "am", "force-stop", PKG], capture_output=True)
time.sleep(1)
d = frida.get_usb_device(10)
source = SCRIPT.read_text(encoding="utf-8")
print("Spawning %s ..." % PKG)
pid = d.spawn([PKG])
session = d.attach(pid)
script = session.create_script(source)
script.on("message", on_message)
script.load()
time.sleep(2)
d.resume(pid)
print("Resumed pid=%s, log=%s" % (pid, LOG))
print("等待 90s 让 Java Hook 就绪,然后 Sign up -> Next")
try:
for _ in range(120):
time.sleep(1)
except KeyboardInterrupt:
pass
session.detach()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
"""Spawn MariBank with Frida trace and keep session alive."""
import sys
import time
from pathlib import Path
import frida
PKG = "ph.seabank.seabank"
HERE = Path(__file__).resolve().parent
LOGS_DIR = HERE.parent / "logs" / "frida"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
SCRIPT = HERE / "trace_maribank_register.js"
LOG = LOGS_DIR / "trace_live.log"
def on_message(message, data):
line = ""
if message.get("type") == "send":
line = str(message.get("payload", ""))
elif message.get("type") == "error":
line = "ERROR: " + str(message.get("stack", message))
else:
line = str(message)
print(line, flush=True)
with open(str(LOG), "a", encoding="utf-8", errors="replace") as f:
f.write(line + "\n")
def main():
LOG.write_text("", encoding="utf-8")
source = SCRIPT.read_text(encoding="utf-8")
device = frida.get_usb_device(timeout=10)
print("device:", device.name, flush=True)
pid = device.spawn([PKG])
print("spawned pid", pid, flush=True)
session = device.attach(pid)
script = session.create_script(source)
script.on("message", on_message)
script.load()
device.resume(pid)
print("resumed — 请在手机: Sign up -> 输入号码 -> Next", flush=True)
print("log:", LOG, flush=True)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("detached", flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import frida
import sys
import time
PKG = "ph.seabank.seabank"
d = frida.get_usb_device(10)
pid = None
for app in d.enumerate_applications():
if app.identifier == PKG and app.pid and app.pid > 0:
print("found", app.name, app.pid)
pid = app.pid
break
if not pid:
sys.exit("MariBank not running")
s = d.attach(pid)
src = open(__file__.replace("test_attach.py", "trace_maribank_register.js"), encoding="utf-8").read()
def on_m(msg, data):
print(msg)
sc = s.create_script(src)
sc.on("message", on_m)
sc.load()
print("loaded, waiting 15s for hooks...")
time.sleep(15)
print("done test")

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
import frida
import subprocess
import time
PKG = "ph.seabank.seabank"
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
subprocess.run([ADB, "shell", "monkey", "-p", PKG, "-c", "android.intent.category.LAUNCHER", "1"], capture_output=True)
d = frida.get_usb_device(10)
pid = None
for a in d.enumerate_applications():
if a.identifier == PKG and a.pid > 0:
pid = a.pid
print("pid", pid, a.name)
break
if not pid:
raise SystemExit("no pid")
s = d.attach(pid)
JS = r"""
var n = 0;
function waitJava() {
if (typeof Java !== 'undefined' && Java.available) {
send({event: 'java_ready', n: n});
Java.perform(function () {
send({event: 'perform_ok'});
});
return;
}
n++;
if (n % 10 === 0) send({event: 'waiting', n: n});
if (n < 120) setTimeout(waitJava, 500);
else send({event: 'timeout', n: n});
}
setImmediate(waitJava);
"""
def on_m(msg, data):
print(msg)
sc = s.create_script(JS)
sc.on("message", on_m)
sc.load()
time.sleep(70)

View File

@@ -0,0 +1,182 @@
'use strict';
/**
* MariBank 注册 trace — attach 模式优先,聚焦 Java 层OkHttp / Gson / 加密包装)
*/
const TAG = '[MB-TRACE]';
const MAX_STR = 2000;
function log(msg) {
console.log(TAG + ' ' + msg);
}
function shouldLogUrl(url) {
if (!url) return false;
const u = String(url).toLowerCase();
return u.indexOf('register') >= 0 || u.indexOf('dfp') >= 0
|| u.indexOf('risk') >= 0 || u.indexOf('uapi') >= 0;
}
function hexPreview(arr, limit) {
const n = Math.min(arr.length, limit || 64);
let hex = '';
for (let i = 0; i < n; i++) {
const b = (arr[i] & 0xff).toString(16);
hex += (b.length === 1 ? '0' : '') + b;
}
if (arr.length > n) hex += '...';
return hex;
}
function dumpJava(tag, obj) {
if (obj === null || obj === undefined) {
log(tag + ' = null');
return;
}
try {
const cls = obj.getClass().getName();
if (cls === '[B') {
const arr = Java.cast(obj, Java.use('[B'));
let text = '';
try {
text = Java.use('java.lang.String').$new(arr, 'UTF-8').toString();
} catch (e) {
text = '<bin>';
}
const show = text.length > MAX_STR ? text.substring(0, MAX_STR) + '...' : text;
log(tag + ' byte[' + arr.length + '] hex=' + hexPreview(arr, 48) + ' text=' + show);
return;
}
if (cls === 'java.lang.String') {
const s = Java.cast(obj, Java.use('java.lang.String')).toString();
const show = s.length > MAX_STR ? s.substring(0, MAX_STR) + '...' : s;
log(tag + ' String(' + s.length + ') ' + show);
return;
}
log(tag + ' ' + cls + ' = ' + obj.toString());
} catch (e) {
log(tag + ' err=' + e);
}
}
function hookOkHttp() {
const RealCall = Java.use('okhttp3.RealCall');
const orig = RealCall.execute;
RealCall.execute.implementation = function () {
const req = this.request();
const url = req.url().toString();
const method = req.method();
if (shouldLogUrl(url)) {
log('HTTP >> ' + method + ' ' + url);
try {
const body = req.body();
if (body) {
const Buffer = Java.use('okio.Buffer');
const buf = Buffer.$new();
body.writeTo(buf);
const bytes = buf.readByteArray();
if (bytes) dumpJava(' reqBody', Java.array('byte', bytes));
}
} catch (e) {
log(' reqBody err: ' + e);
}
}
const resp = orig.call(this);
if (shouldLogUrl(url)) {
try {
const peek = resp.peekBody(Java.use('java.lang.Long').parseLong('1048576'));
const s = peek.string();
const show = s.length > MAX_STR ? s.substring(0, MAX_STR) + '...' : s;
log('HTTP << ' + resp.code() + ' ' + show);
} catch (e) {
log('HTTP resp err: ' + e);
}
}
return resp;
};
log('hooked RealCall.execute');
}
function hookGson() {
const Gson = Java.use('com.google.gson.Gson');
Gson.toJson.overload('java.lang.Object').implementation = function (obj) {
const ret = this.toJson(obj);
if (ret) {
const low = ret.toLowerCase();
if (low.indexOf('mobile') >= 0 || low.indexOf('phone') >= 0
|| low.indexOf('risktoken') >= 0 || low.indexOf('register') >= 0
|| low.indexOf('4067') >= 0) {
const show = ret.length > MAX_STR ? ret.substring(0, MAX_STR) + '...' : ret;
log('Gson.toJson ' + show);
}
}
return ret;
};
log('hooked Gson.toJson');
}
function hookRisk() {
const vv = Java.use('com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv');
vv.wwvuwuwvu.overload('android.content.Context').implementation = function (ctx) {
const ret = this.wwvuwuwvu(ctx);
dumpJava('riskToken', ret);
return ret;
};
log('hooked vvuuuuvvv.wwvuwuwvu');
}
function hookEncryptWrapper() {
const D = Java.use('com.shopee.bke.lib.jni.utils.d');
const methods = D.class.getDeclaredMethods();
for (let i = 0; i < methods.length; i++) {
const m = methods[i];
const name = m.getName();
if (m.getModifiers() & 0x0100) continue;
try {
D[name].overloads.forEach(function (ovl) {
ovl.implementation = function () {
const args = [].slice.call(arguments);
log('>> EncryptWrapper.' + name);
args.forEach(function (a, idx) { dumpJava(' in' + idx, a); });
const ret = ovl.apply(this, args);
if (ret && ret.getClass) {
const cn = ret.getClass().getName();
if (cn === '[Ljava.lang.String;') {
const arr = Java.cast(ret, Java.use('[Ljava.lang.String;'));
for (let j = 0; j < arr.length; j++) dumpJava(' out' + j, arr[j]);
} else {
dumpJava(' ret', ret);
}
}
return ret;
};
});
} catch (e) {}
}
log('hooked NativeEncryptUtilsWrapper (utils.d)');
}
function installAll() {
Java.perform(function () {
log('Java.perform OK pid=' + Process.id);
try { hookOkHttp(); } catch (e) { log('okhttp fail: ' + e); }
try { hookGson(); } catch (e) { log('gson fail: ' + e); }
try { hookRisk(); } catch (e) { log('risk fail: ' + e); }
try { hookEncryptWrapper(); } catch (e) { log('encrypt fail: ' + e); }
log('READY — 请在 App 输入号码点 Next');
});
}
function waitForJava(n) {
n = n || 0;
if (typeof Java === 'undefined' || !Java.available) {
if (n % 5 === 0) log('waiting Java.available attempt=' + n);
setTimeout(function () { waitForJava(n + 1); }, 500);
return;
}
installAll();
}
setImmediate(function () {
log('script loaded pid=' + Process.id);
waitForJava(0);
});

13
reverse/scripts/_paths.py Normal file
View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
"""Shared paths for reverse/ scripts (scripts live in reverse/scripts/)."""
from pathlib import Path
REVERSE_ROOT = Path(__file__).resolve().parent.parent
APKS_DIR = REVERSE_ROOT / "apks"
EXTRACTED_DIR = REVERSE_ROOT / "extracted"
NATIVE_DIR = EXTRACTED_DIR / "native"
TMP_DIR = REVERSE_ROOT / "tmp"
OUTPUT_DIR = REVERSE_ROOT / "output"
LOGS_DIR = REVERSE_ROOT / "logs"
DEFAULT_APK = APKS_DIR / "seabank_ph_base.apk"

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import re
import subprocess
from pathlib import Path
SO = Path(__file__).resolve().parent.parent / "extracted" / "native" / "libshpssdk_bank.so"
ndk = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.4.7075529\toolchains\llvm\prebuilt\windows-x86_64\bin")
readelf = ndk / "llvm-readelf.exe"
out = subprocess.check_output([str(readelf), "-Ws", str(SO)], universal_newlines=True, errors="replace")
print("=== JNI Java_* ===")
for line in out.splitlines():
if "Java_com_shopee" in line:
print(line)
print("\n=== risk/root/token strings in .dynsym FUNC ===")
data = SO.read_bytes()
for m in re.finditer(rb"[\x20-\x7e]{4,}", data):
s = m.group().decode("latin1")
if any(k in s.lower() for k in ["risk", "root", "hook", "token", "proc/", "magisk", "xposed", "emulator", "assess"]):
if len(s) < 120:
print(s)

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
data = open(r"C:\Users\Administrator\Desktop\notiMessage\reverse\extracted\classes6.dex", "rb").read()
idx = data.find(b"WBRootDetectionModule")
print("offset", idx)
print(data[idx-120:idx+200].decode("latin1", "ignore"))

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""Decode SHPSSDK obfuscated hex strings via uvuwwwuwu."""
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
# sample pairs from SHPSSDK.wwwwvwwwu
samples = [
("0F580514065B111F0E1A10", "wuvuvvwvv"),
("100F3C013C1C19113F18271F3A36371F2C253C403F323C053F353C013F1C19123F1F3C4F", "uvvvuvvvv"),
("0E5806170758131F0D1B10", "vuuvwuuvu"),
("120E3D023E1C1A113C1A261E3934371C2C263E413E313A053C353F033E1D1A103F1C3C4C", "wwwuwvuvu"),
]
# dump uvuwwwuwu implementation
with zipfile.ZipFile(str(APK)) as zf:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
cap = False
print("=== uvuwwwuwu implementation ===")
for line in out.splitlines():
if "uvuwwwuwu:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" in line:
cap = True
if cap:
print(line.encode("ascii", "replace").decode())
if cap and "locals :" in line:
break
print("\n=== try XOR decode (common pattern) ===")
for hex_str, key in samples:
data = bytes.fromhex(hex_str) if all(c in "0123456789ABCDEFabcdef" for c in hex_str) else hex_str.encode()
# try simple xor with key bytes cycling
kb = key.encode()
dec = bytes(b ^ kb[i % len(kb)] for i, b in enumerate(data))
try:
txt = dec.decode("utf-8")
except Exception:
txt = dec.decode("latin1", errors="replace")
print(hex_str[:20], "...", "->", repr(txt[:80]))

View File

@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""Dump JNI/native methods for sdkutils crypto + register-related classes."""
import re
import subprocess
import zipfile
from pathlib import Path
from typing import List
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
OUT = Path(__file__).resolve().parent.parent / "output" / "crypto_jni_targets.txt"
CLASS_PATTERNS = [
rb"Lcom/shopee/bke/lib/jni/[^;]{1,80};",
rb"Lcom/shopee/shpssdkbank/wvvvuwwu;",
rb"Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;",
rb"Lcom/shopee/shpssdkbank/uwuvuvvww/[^;]{1,40};",
]
EXTRA_KEYWORDS = (
b"NativeEncrypt",
b"CharacterCrypto",
b"SecurityMain",
b"encrypt",
b"decrypt",
b"register",
)
def dump_class(out: str, target: str) -> List[str]:
lines: list[str] = []
cap = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap:
lines.append(line)
return lines
def main() -> None:
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
classes: set[str] = set()
for pat in CLASS_PATTERNS:
for m in re.finditer(pat, data):
classes.add(m.group().decode()[1:-1].replace("/", "."))
for kw in EXTRA_KEYWORDS:
for m in re.finditer(rb"Lcom/[^;]{0,120};", data):
s = m.group()
if kw in s or kw in data[data.find(s) : data.find(s) + 4000]:
classes.add(s.decode()[1:-1].replace("/", "."))
# log tags -> classes from crash log
for tag in [
"com.shopee.bke.lib.jni.utils.f", # SoUtils
"com.shopee.bke.lib.jni.uwuwuwuw",
"com.shopee.bke.lib.jni.uvuvuvuv",
"com.shopee.bke.lib.jni.uvwwwwuv",
]:
classes.add(tag)
native_entries: List[str] = []
with zipfile.ZipFile(str(APK)) as zf:
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
dex = zf.read(dex_name)
hits = [c for c in classes if c.replace(".", "/") in dex.decode("latin1", errors="ignore")]
if not hits:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_crypto_jni.dex"
tmp.write_bytes(dex)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for cls in sorted(hits):
block = dump_class(out, "L" + cls.replace(".", "/") + ";")
if not block:
continue
native_entries.append(f"\n=== {dex_name} {cls} ===")
for line in block:
if any(
k in line
for k in ("NATIVE", "name :", "type :", "loadLibrary")
):
native_entries.append(line)
text = "\n".join(native_entries)
OUT.write_text(text, encoding="utf-8")
print(text)
print(f"\nwritten {OUT}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
targets = [
"Lcom/shopee/bke/biz/user/errorcodehandler/b;",
"Lcom/shopee/bke/biz/user/errorcodehandler/b$a;",
"Lcom/shopee/bke/biz/user/errorcodehandler/a;",
]
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp6.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes6.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
for target in targets:
print("=" * 60, target)
capture = False
lines = 0
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
capture = True
elif capture and line.startswith(" Class descriptor") and target not in line:
break
if capture:
if "name :" in line or "type :" in line or "Class descriptor" in line:
print(line.strip())
lines += 1
if lines > 200:
break

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
target = "Lcom/shopee/bke/biz/user/errorcodehandler/b$a;"
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp6.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes6.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
capture = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
capture = True
elif capture and line.startswith(" Class descriptor"):
break
if capture and ("name :" in line or "type :" in line or "Class descriptor" in line):
print(line.encode("ascii", "replace").decode())

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp6.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes6.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
cls = "GlobalAuthErrorImpl"
capture = False
for line in out.splitlines():
if f"Lcom/shopee/bke/digitalbank/container/user/errorcodehandler/{cls};" in line and "Class descriptor" in line:
capture = True
if capture:
print(line)
if line.strip() == "" and "Method" not in line and capture:
pass
if capture and line.startswith(" Class descriptor") and cls not in line:
break

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
methods = [
("Lcom/shopee/shpssdkbank/SHPSSDK;", "getRiskToken"),
("Lcom/shopee/shpssdkbank/wvvvuwwu;", "vvuwuuvuu"),
("Lcom/shopee/shpssdkbank/wvvvuwwu;", "vuwuuwvw"),
("Lcom/shopee/shpssdkbank/wvvvuwwu;", "vuwuuuwv"),
]
with zipfile.ZipFile(str(APK)) as zf:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for cls, method in methods:
needle = f"{cls.replace('L', '').replace(';', '').split('/')[-1]}.{method}:"
print("\n" + "=" * 70, cls, method)
cap = False
for line in out.splitlines():
if needle in line.replace("com.shopee.shpssdkbank.", ""):
cap = True
if cap:
print(line.encode("ascii", "replace").decode())
if line.strip().startswith("catches") or (cap and line.strip() == "locals :"):
pass
if cap and line.strip() == "" and "positions" in line:
break
if cap and line.startswith(" name") and method not in line and cap:
# next method
if methods.index((cls, method)) < len(methods) - 1:
break
# classes8 - sdkutils / crypto
with zipfile.ZipFile(str(APK)) as zf:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp8.dex"
tmp.write_bytes(zf.read("classes8.dex"))
out8 = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for tag in ["CharacterCrypto", "SoUtils", "SecurityMain", "sdkutils", "encrypt"]:
print("\n--- search", tag, "in classes8 ---")
for line in out8.splitlines():
if tag.lower() in line.lower() and ("Class descriptor" in line or "name :" in line):
print(line.encode("ascii", "replace").decode()[:200])

View File

@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
targets = [
"Lcom/shopee/shpssdkbank/wvvvuwwu;",
"Lcom/shopee/shpssdkbank/uwuvuvvww/wvvuuwvwu;",
"Lcom/shopee/shpssdkbank/uwuvuvvww/uvwuuuuuw/vvvvuwwvu;",
"Lcom/shopee/shpssdkbank/SHPSSDK;",
"Lcom/shopee/shpssdkbank/ShpssInstall;",
]
with zipfile.ZipFile(str(APK)) as zf:
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
dex = zf.read(dex_name)
hit = any(t.replace("L", "").replace(";", "") in dex.decode("latin1", errors="ignore") for t in targets)
if not hit:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_native.dex"
tmp.write_bytes(dex)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in targets:
if target.replace("L", "").replace(";", "") not in dex.decode("latin1", errors="ignore"):
continue
print("\n" + "=" * 70)
print(dex_name, target)
cap = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap:
safe = line.encode("ascii", "replace").decode()
if any(k in safe for k in ["name", "type", "access", "NATIVE", "Method", "loadLibrary", "register"]):
print(safe)
# find sdkutils / crypto manager classes
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
print("\n--- sdkutils / crypto / dfp classes ---")
for pat in [
rb"Lcom/[^;]{0,80}sdkutils[^;]{0,20};",
rb"Lcom/[^;]{0,80}[Cc]rypto[^;]{0,40};",
rb"Lcom/[^;]{0,80}dfp[^;]{0,30};",
rb"Lcom/[^;]{0,80}SecurityMain[^;]{0,20};",
]:
found = set()
for m in re.finditer(pat, data):
s = m.group().decode()[1:-1].replace("/", ".")
if s not in found:
found.add(s)
print(s)

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
targets = [
"Lcom/shopee/bke/biz/user/viewmodel/PhoneNumViewModel;",
"Lcom/shopee/bke/biz/user/viewmodel/RegisterViewModel;",
]
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
if b"PhoneNumViewModel" not in data and b"RegisterViewModel" not in data:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_user.dex"
tmp.write_bytes(data)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in targets:
if target.replace("L", "").replace(";", "") not in data.decode("latin1", errors="ignore"):
continue
print("=" * 60, name, target)
cap = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap and ("name :" in line or "type :" in line
or "Method" in line or "register" in line.lower()):
safe = line.strip().encode("ascii", "replace").decode()
print(safe)

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for m in re.finditer(rb"Lcom/shopee/bke/[^;]{0,80}RegisterRequest[^;]{0,20};", data):
print(m.group().decode()[1:-1].replace("/", "."))
for name in zf.namelist():
if not name.endswith(".dex"):
continue
dex = zf.read(name)
if b"RegisterRequest" not in dex:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_reg.dex"
tmp.write_bytes(dex)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in re.findall(r"Lcom/shopee/bke/[^;]*RegisterRequest[^;]*;", data.decode("latin1", errors="ignore")):
cap = False
print("\n" + "=" * 60, name, target)
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap:
print(line)

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
targets = [
"Lcom/shopee/bke/biz/user/viewmodel/RegisterViewModel;",
"Lcom/shopee/bke/biz/user/ui/PhoneNumActivity;",
]
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
if b"RegisterViewModel" not in data:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_reg.dex"
tmp.write_bytes(data)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in targets:
if target.replace("L", "").replace(";", "") not in data.decode("latin1", errors="ignore"):
continue
print("=" * 60, name, target)
cap = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor") and target not in line:
break
if cap and ("name :" in line or "type :" in line):
print(line.strip().encode("ascii", "replace").decode())

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
"""Dump riskToken + requestDefense + register crypto call chain."""
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
TARGETS = [
("classes11.dex", "Lcom/shopee/shpssdkbank/SHPSSDK;", "getRiskToken"),
("classes11.dex", "Lcom/shopee/shpssdkbank/SHPSSDK;", "requestDefense"),
("classes11.dex", "Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;", "wwvuwuwvu"),
("classes11.dex", "Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;", "wuvwuvwwu"),
("classes11.dex", "Lcom/shopee/shpssdkbank/wvvvuwwu;", "wwvwvwuvv"),
("classes11.dex", "Lcom/shopee/shpssdkbank/wvvvuwwu;", "vvuwuuvuu"),
]
def dump_method(out, cls, method):
cls_short = cls.replace("L", "").replace(";", "").replace("/", ".")
needle = cls_short + "." + method + ":"
print("\n" + "=" * 72)
print(cls_short, method)
cap = False
lines = []
for line in out.splitlines():
if needle in line:
cap = True
if cap:
lines.append(line)
if len(lines) > 1 and line.strip().startswith("name :") and method not in line:
break
for line in lines[:80]:
print(line.encode("ascii", "replace").decode())
with zipfile.ZipFile(str(APK)) as zf:
for dex_name, cls, method in TARGETS:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "_tmp.dex"
tmp.write_bytes(zf.read(dex_name))
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
dump_method(out, cls, method)
# classes8 CharacterCrypto / SoUtils
print("\n" + "=" * 72, "classes8 crypto classes")
with zipfile.ZipFile(str(APK)) as zf:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "_tmp8.dex"
tmp.write_bytes(zf.read("classes8.dex"))
out8 = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp8)], universal_newlines=True, errors="replace"
)
cap = False
for line in out8.splitlines():
if "Class descriptor" in line and (
"CharacterCrypto" in line or "SoUtils" in line or "sdkutils" in line.lower()
):
print("\n---", line.strip())
cap = True
continue
if cap:
if line.startswith(" Class descriptor") and "CharacterCrypto" not in line:
cap = False
continue
if "name :" in line or "NATIVE" in line or "loadLibrary" in line:
print(line.strip()[:180])

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
import subprocess
import sys
import zipfile
import tempfile
import os
TARGET = "com/shopee/bke/lib/safemode/b;"
APK = sys.argv[1] if len(sys.argv) > 1 else "reverse/apks/seabank_ph_base.apk"
BT = None
def find_build_tools():
base = os.environ.get("ANDROID_HOME") or os.path.expanduser(
r"~\AppData\Local\Android\Sdk"
)
tools = os.path.join(base, "build-tools")
versions = sorted(os.listdir(tools), reverse=True)
return os.path.join(tools, versions[0], "dexdump.exe")
def main():
dexdump = find_build_tools()
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
if TARGET.encode() not in data and b"Lcom/shopee/bke/lib/safemode/b;" not in data:
continue
print("FOUND in", name)
tmp = tempfile.NamedTemporaryFile(suffix=".dex", delete=False)
tmp.write(data)
tmp.close()
try:
out = subprocess.check_output(
[dexdump, "-f", tmp.name], stderr=subprocess.STDOUT, text=True,
errors="ignore"
)
capture = False
for line in out.splitlines():
if "Class descriptor : 'Lcom/shopee/bke/lib/safemode/b;'" in line:
capture = True
elif capture and line.startswith(" Class descriptor"):
break
if capture:
if "name :" in line or "type :" in line or "Class descriptor" in line:
print(line)
finally:
os.unlink(tmp.name)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
target = "Lcom/shopee/shpssdkbank/SHPSSDK;"
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
capture = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
capture = True
elif capture and line.startswith(" Class descriptor"):
break
if capture and ("name :" in line or "type :" in line or "access :" in line):
print(line.encode("ascii", "replace").decode())

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
target = "Lcom/shopee/shpssdkbank/SHPSSDK;"
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
capture = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
capture = True
elif capture and line.startswith(" Class descriptor"):
break
if capture:
print(line.encode("ascii", "replace").decode())

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
targets = [
"Lcom/shopee/shpssdkbank/ShpssInstall;",
"Lcom/shopee/shpssdkbank/vuvuwwwuw;",
"Lcom/shopee/shpssdkbank/vwuuwwvwv;",
"Lcom/shopee/shpssdkbank/uvuwwuvwv/uvwwuuvvw;",
]
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
for target in targets:
print("=" * 60, target)
cap = False
n = 0
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor"):
break
if cap:
print(line.encode("ascii", "replace").decode())
n += 1
if n > 80:
print("...truncated...")
break

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
with zipfile.ZipFile(APK) as zf:
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for line in out.splitlines():
if "SHPSSDK;" in line and any(
k in line for k in ("getRisk", "assessRisk", "getRiskToken", "getExtRisk")
):
print(line.strip())

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
with zipfile.ZipFile(str(APK)) as zf:
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
in_shps = False
for line in out.splitlines():
if "Class descriptor : 'Lcom/shopee/shpssdk" in line:
in_shps = "SHPSSDK;" in line or "shpssdkbank" in line
if in_shps and line.startswith(" Class descriptor") and "shpssdk" not in line:
break
if in_shps and ("NATIVE" in line or "name :" in line):
print(line.strip())

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
import sys
import zipfile
from pathlib import Path
SPLIT = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_config.arm64_v8a.apk"
if not SPLIT.exists():
native_dir = SPLIT.parent
candidates = list(native_dir.glob("split_config.arm64*.apk"))
if not candidates:
print("missing split APK at", SPLIT, file=sys.stderr)
print("run: reverse/frida/pull_split_apk.ps1", file=sys.stderr)
sys.exit(1)
SPLIT = candidates[0]
OUT = SPLIT.parent
with zipfile.ZipFile(str(SPLIT)) as zf:
for name in zf.namelist():
if name.endswith(".so"):
path = OUT / Path(name).name
path.write_bytes(zf.read(name))
print(path.name, path.stat().st_size)

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import os
import zipfile
from pathlib import Path
split = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_arm64.apk"
out = split.parent
with zipfile.ZipFile(str(split)) as zf:
for name in zf.namelist():
if "libshpssdk" in name:
dest = out / Path(name).name
dest.write_bytes(zf.read(name))
print(dest, dest.stat().st_size)

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
import os
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
OUT = Path(__file__).resolve().parent.parent / "extracted" / "native"
OUT.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if "libshpssdk" in name and name.endswith(".so"):
path = OUT / Path(name).name
path.write_bytes(zf.read(name))
print("extracted", path, path.stat().st_size)

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
import os
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
OUT = Path(__file__).resolve().parent.parent / "extracted" / "native"
OUT.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if "libshpssdk" in name and name.endswith(".so"):
dest = OUT / Path(name).name
dest.write_bytes(zf.read(name))
print(dest, dest.stat().st_size)

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import os
import zipfile
from pathlib import Path
SPLIT = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_config.arm64_v8a.apk"
OUT = SPLIT.parent
with zipfile.ZipFile(str(SPLIT)) as zf:
for name in zf.namelist():
if "libshpssdk" in name and name.endswith(".so"):
path = OUT / Path(name).name
path.write_bytes(zf.read(name))
print("extracted", path, path.stat().st_size)

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import json
import glob
import os
from pathlib import Path
root = str(Path(__file__).resolve().parent.parent / "extracted" / "apk_extract")
needle = "this service has been temporarily blocked"
for fp in glob.glob(os.path.join(root, "**", "en.json"), recursive=True):
try:
with open(fp, encoding="utf-8") as f:
d = json.load(f)
if not isinstance(d, dict):
continue
for k, v in d.items():
if isinstance(v, str) and needle in v.lower():
print(fp)
print(k, "->", v)
except Exception:
pass

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
with zipfile.ZipFile(str(APK)) as zf:
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
data = zf.read(dex_name)
if b"CharacterCrypto" not in data and b"SoUtils" not in data:
continue
print("===", dex_name, "===")
for m in re.finditer(rb"L[^;]{0,120};", data):
s = m.group().decode("latin1", errors="replace")
if "CharacterCrypto" in s or "SoUtils" in s or "SecurityMain" in s:
print(s[1:-1].replace("/", "."))

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
with zipfile.ZipFile(APK) as zf:
for name in sorted(zf.namelist()):
if not name.endswith(".dex"):
continue
data = zf.read(name)
found = set()
for m in re.finditer(rb"L[a-zA-Z0-9_$/]+;", data):
s = m.group().decode()[1:-1].replace("/", ".")
low = s.lower()
if any(k in low for k in ("rootdetect", "emulatordetect", "safemode", "risk", "integrity", "xposed", "hookdetect")):
found.add(s)
if "WBRoot" in s or "WBEmulator" in s:
found.add(s)
if found:
print("=== %s ===" % name)
for s in sorted(found):
print(s)

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needle = b"Lcom/shopee/bke/digitalbank/container/user/errorcodehandler/GlobalAuthErrorImpl;"
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if name.endswith(".dex") and needle in zf.read(name):
print("found in", name)

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needles = [b"IV_Monitor", b"register scene", b"dfp is empty", b"CharacterCrypto"]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
print(n.decode(), data.count(n))
idx = data.find(n)
if idx >= 0:
ctx = data[max(0, idx - 80): idx + 120]
import re
for m in re.finditer(rb"Lcom/[^;\x00]{5,120};", ctx):
print(" class", m.group().decode()[1:-1].replace("/", "."))

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
with zipfile.ZipFile(str(APK)) as zf:
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
data = zf.read(dex_name)
if b"loadSoLibrary" not in data:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_loadso.dex"
tmp.write_bytes(data)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
current_class = ""
for line in out.splitlines():
m = re.search(r"Class descriptor\s+:\s+'([^']+)'", line)
if m:
current_class = m.group(1)
if "loadSoLibrary" not in line:
continue
cls = current_class.replace("L", "").replace(";", "").replace("/", ".")
print(f"{dex_name}\t{cls}\t{line.strip()}")

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for kw in [b"NativeEncrypt", b"CharacterCrypto", b"IV_Monitor", b"register scene", b"dfp is empty"]:
print(kw.decode(), data.count(kw))
print("\n--- bke.lib.jni crypto/security ---")
seen = set()
for m in re.finditer(rb"Lcom/shopee/bke/lib/jni/[^;]{1,120};", data):
s = m.group().decode()[1:-1].replace("/", ".")
if s in seen:
continue
if any(x in s.lower() for x in ("crypto", "encrypt", "security", "native", "tee")):
seen.add(s)
print(s)

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
needle = b"NativeEncrypt"
with zipfile.ZipFile(str(APK)) as zf:
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
dex = zf.read(dex_name)
if needle not in dex:
continue
print("===", dex_name, "===")
for m in re.finditer(rb"const-string[^/]*// string@[0-9a-f]+", dex):
pass
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_ne.dex"
tmp.write_bytes(dex)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
cap = False
for line in out.splitlines():
if "NativeEncrypt" in line:
print(line.strip())
if "Class descriptor" in line:
current = line
if "NativeEncrypt" in line:
# print previous class context
idx = out.splitlines().index(line)
for prev in out.splitlines()[max(0, idx - 40) : idx + 5]:
if "Class descriptor" in prev or "name :" in prev or "NATIVE" in prev:
print(prev.strip())

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
split_apk = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_arm64.apk"
base_apk = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
apk = base_apk if base_apk.exists() else None
if apk is None:
# use device base if needed - skip
import sys
print("no base apk")
sys.exit(0)
with zipfile.ZipFile(str(apk)) as zf:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
tmp.write_bytes(zf.read("classes11.dex"))
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
for line in out.splitlines():
if "0x0101" in line or "NATIVE" in line:
print(line.strip())

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needles = [
b"getRiskTokenAsync",
b"getRiskToken",
b"getRiskSync",
b"getRiskAsync",
b"assessRisk",
]
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
hits = [n.decode() for n in needles if n in data]
if not hits:
continue
print(name, hits)
for m in re.finditer(
rb"Lcom/shopee/shpssdk(?:bank)?/SHPSSDK;\.(\w+):\([^)]+\)[^;]+;", data
):
print(" ", m.group().decode())

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
if b"WBRootDetectionModule" not in data:
continue
print("===", name, "===")
for m in re.finditer(rb"Lcom/shopee/[a-zA-Z0-9_$/]+;", data):
s = m.group().decode()[1:-1].replace("/", ".")
if "Root" in s or "Detect" in s or "Emulator" in s or "Safe" in s or "Risk" in s or "WB" in s:
print(s)

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
needles = [b"WBRootDetectionModule", b"WBEmulatorDetectionModule", b"SPSAssessRisk", b"shpssdk"]
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
hits = [n.decode() for n in needles if n in data]
if hits:
print(name, hits)
for m in re.finditer(rb"L[a-zA-Z0-9_$/]*WBRootDetectionModule;", data):
print(" ", m.group().decode()[1:-1].replace("/", "."))
for m in re.finditer(rb"L[a-zA-Z0-9_$/]*SPSAssessRisk[^;]*;", data):
print(" ", m.group().decode()[1:-1].replace("/", "."))
for m in re.finditer(rb"Lcom/shopee/shpssdk[a-zA-Z0-9_$/]+;", data):
s = m.group().decode()[1:-1].replace("/", ".")
if "shpssdk" in s.lower():
print(" ", s)

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import re
data = open(r"C:\Users\Administrator\Desktop\notiMessage\reverse\extracted\classes6.dex", "rb").read()
# method names in dex are plain utf8 strings
methods = set(re.findall(rb"[a-zA-Z][a-zA-Z0-9_]{2,60}", data))
interesting = sorted(
m.decode("ascii", "ignore")
for m in methods
if any(k in m.lower() for k in (
b"root", b"jail", b"safe", b"xposed", b"frida", b"emulator",
b"integrity", b"detect", b"hook", b"debug", b"tamper", b"risk"
))
)
for m in interesting:
print(m)

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import re
data = open(r"C:\Users\Administrator\Desktop\notiMessage\reverse\extracted\classes6.dex", "rb").read()
classes = set()
for m in re.finditer(rb"Lcom/shopee/bke/lib/safemode/[a-zA-Z0-9_$/]+;", data):
s = m.group().decode()[1:-1].replace("/", ".")
if "R$" in s:
continue
classes.add(s)
for c in sorted(classes):
print(c)
print("\n--- interesting strings ---")
for pat in [b"isRoot", b"jailbroken", b"rooted", b"SafeMode", b"checkRoot", b"detect", b"xposed", b"frida", b"integrity"]:
if pat in data:
print(pat.decode(), "YES")

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
import re
from pathlib import Path
so = Path(__file__).resolve().parent.parent / "extracted" / "native" / "libsdkutils.so"
data = so.read_bytes()
for name in sorted(set(m.group().decode() for m in re.finditer(rb"Java_com_shopee_bke_lib_jni_[A-Za-z0-9_]+", data))):
if "utils" in name or "encrypt" in name.lower():
print(name)

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if "alc" in name.lower() and name.endswith(".json"):
print("===", name, "===")
print(zf.read(name).decode("utf-8", "ignore")[:2000])
print()

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
needles = [b"-1201", b"1201", b"ErrorCode", b"error has occurred"]
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
idx = data.find(n)
if idx >= 0:
print(n.decode(), "at", idx, "context:", data[max(0,idx-40):idx+60])

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needle = b"4067"
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
idx = 0
while True:
idx = data.find(needle, idx)
if idx < 0:
break
ctx = data[max(0, idx - 80): idx + 120]
import re
strings = [m.group().decode("latin1") for m in re.finditer(rb"[\x20-\x7e]{3,80}", ctx)]
print("--- at", idx, "---")
for s in strings:
print(" ", s)
idx += 1

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
targets = [
b"Lcom/shopee/bke/lib/commonui/widget/CommonDialog;",
b"Lcom/shopee/bke/lib/commonui/widget/CommonDialog$Builder;",
]
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for t in targets:
print("===", t.decode()[1:-1].replace("/", "."), "===")
idx = 0
c = 0
while c < 5:
idx = data.find(t, idx)
if idx < 0:
break
ctx = data[max(0, idx - 100): idx + 300]
for m in re.finditer(rb"(show|build|create|setMessage|setContent|display)[a-zA-Z0-9_$<>]*", ctx):
print(" ", m.group().decode())
idx += 1
c += 1

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for needle in [b"sdkutils", b"SoUtils", b"CharacterCrypto", b"vvuwuuvuu", b"vuwuuwvw", b"wwvwvwuvv", b"dfp is empty"]:
print(needle.decode(), data.count(needle))
print("\n--- classes referencing sdkutils ---")
for m in re.finditer(rb"Lcom/[^;]{0,120};", data):
s = m.group()
if b"sdkutils" in s.lower() or b"SoUtils" in s or b"Crypto" in s:
print(s.decode()[1:-1].replace("/", "."))
# dump vvuuuuvvv.wwvuwuwvu (getRiskToken core)
target = "Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;"
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
dex = zf.read(dex_name)
if target.encode() not in dex:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_vv.dex"
tmp.write_bytes(dex)
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], text=True, errors="replace")
print("\n===", dex_name, "vvuuuuvvv methods (native only) ===")
cap = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
cap = True
elif cap and line.startswith(" Class descriptor"):
break
if cap and ("NATIVE" in line or "name :" in line):
print(line.encode("ascii", "replace").decode())

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
needles = [
b"CharacterCrypto",
b"sdkutils",
b"SoUtils",
b"dfp is empty",
b"register scene",
b"deviceToken",
b"getRiskToken",
b"encrypt",
b"decrypt",
b"/uapi/v2/register",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
print(n.decode(), data.count(n))
print("\n--- CharacterCrypto classes ---")
for m in re.finditer(rb"L[^;]{0,100}CharacterCrypto[^;]{0,40};", data):
print(m.group().decode()[1:-1].replace("/", "."))
print("\n--- SoUtils classes ---")
for m in re.finditer(rb"L[^;]{0,80}SoUtils[^;]{0,20};", data):
print(m.group().decode()[1:-1].replace("/", "."))
print("\n--- native methods in shpssdkbank ---")
for dex_name in zf.namelist():
if not dex_name.endswith(".dex"):
continue
dex = zf.read(dex_name)
if b"shpssdkbank" not in dex:
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_shps.dex"
tmp.write_bytes(dex)
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
cap = False
cls = ""
for line in out.splitlines():
if "Class descriptor : 'Lcom/shopee/shpssdkbank/" in line:
cap = True
cls = line.split("'")[1]
elif cap and line.startswith(" Class descriptor") and "shpssdkbank" not in line:
cap = False
if cap and ("0x0101" in line or "NATIVE" in line):
print(cls.replace("L", "").replace(";", "").replace("/", "."), line.strip())

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import os, re, sys
def scan_dir(base, pat):
rx = re.compile(pat, re.I)
all_m = set()
for dex in sorted(os.listdir(base)):
if not dex.endswith('.dex'):
continue
data = open(os.path.join(base, dex), 'rb').read()
strs = set(m.group().decode('ascii', 'ignore') for m in re.finditer(rb'[\x20-\x7e]{5,}', data))
all_m |= {s for s in strs if rx.search(s)}
return sorted(all_m)
apps = {
'up': (r'(HandlerService|NotificationHandler|showNotification|RemoteMessage|Util\$NotificationType|processPush|handleMessage|up/money/notifications)'),
'suncorp': (r'SuncorpMessagingService|onMessageReceived|showNotification|NotificationDetails|pushNotification|FirebaseService'),
'ubank': (r'MoEFireBase|MessagingService|onMessageReceived|showNotification|Will try to show|bank86400|MoEngage'),
}
root = sys.argv[1]
for app, pat in apps.items():
print('\n====', app, '====')
for s in scan_dir(os.path.join(root, app), pat)[:60]:
print(s)

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
import os
import re
import sys
PATTERNS = [
r'FirebaseMessagingService',
r'onMessageReceived',
r'NotificationManager',
r'NotificationCompat',
r'NotificationChannel',
r'PushNotification',
r'PushMessage',
r'Transaction',
r'transaction',
r'InboxMessage',
r'AlertMessage',
r'showNotification',
r'postNotification',
r'NotificationReceiver',
r'FCM',
r'FirebaseMessaging',
r'MessagingService',
r'PaymentNotification',
r'TransferNotification',
r'BankNotification',
]
CLASS_LIKE = re.compile(r'[A-Za-z][\w$/]{3,120}')
def extract_strings(data, min_len=4):
out = set()
for m in re.finditer(rb'[\x20-\x7e]{' + str(min_len).encode() + rb',}', data):
try:
out.add(m.group().decode('ascii'))
except Exception:
pass
return out
def scan_file(path):
with open(path, 'rb') as f:
data = f.read()
strings = extract_strings(data, 5)
hits = {}
for pat in PATTERNS:
rx = re.compile(pat, re.I)
matched = sorted({s for s in strings if rx.search(s)})
if matched:
hits[pat] = matched[:40]
# interesting fully-qualified class names
fqcn = sorted({
s for s in strings
if ('/' in s or s.startswith('L')) and any(k in s.lower() for k in (
'notif', 'push', 'fcm', 'firebase', 'message', 'transaction', 'alert', 'inbox', 'payment', 'transfer'
))
})
return hits, fqcn[:80]
def main(root):
for app in sorted(os.listdir(root)):
app_dir = os.path.join(root, app)
if not os.path.isdir(app_dir):
continue
print('\n' + '=' * 70)
print('APP:', app)
print('=' * 70)
dex_files = [f for f in os.listdir(app_dir) if f.endswith('.dex')]
all_hits = {}
all_fqcn = set()
for dex in sorted(dex_files):
path = os.path.join(app_dir, dex)
hits, fqcn = scan_file(path)
for k, v in hits.items():
all_hits.setdefault(k, set()).update(v)
all_fqcn.update(fqcn)
for pat in PATTERNS:
if pat in all_hits:
print('\n[%s]' % pat)
for s in sorted(all_hits[pat])[:25]:
print(' ', s)
print('\n[interesting class-like strings]')
for s in sorted(all_fqcn)[:60]:
print(' ', s)
if __name__ == '__main__':
root = sys.argv[1] if len(sys.argv) > 1 else 'extracted'
main(root)

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for m in re.finditer(rb"Lcom/shopee/[a-zA-Z0-9_$/]*(error|Error|dialog|Dialog|Risk|risk|Otp|otp|Phone|Register)[a-zA-Z0-9_$/]*;", data):
s = m.group().decode()[1:-1].replace("/", ".")
if "bke" in s or "seabank" in s or "shpssdk" in s:
print(s)

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import zipfile
import struct
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
targets = [
b"Lcom/shopee/bke/biz/user/errorcodehandler/a;",
b"Lcom/shopee/bke/biz/user/errorcodehandler/b;",
b"Lcom/shopee/shpssdkbank/SPSAssessRisk;",
b"Lcom/shopee/bke/biz/user/rn/helper/ErrorFlowHelper;",
]
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for t in targets:
print("===", t.decode()[1:-1].replace("/", "."), "===")
idx = 0
while True:
idx = data.find(t, idx)
if idx < 0:
break
ctx = data[max(0, idx - 200): idx + 400]
# crude string extraction nearby
for m in __import__("re").finditer(rb"[\x20-\x7e]{4,80}", ctx):
s = m.group().decode()
if any(k in s.lower() for k in ["1201", "error", "kill", "finish", "risk", "root", "code", "handle"]):
print(" ", s)
idx += 1

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
needles = [b"An error has occurred", b"-1201", b"8424 8050", b"Error --", b"killProcess", b"finishAffinity"]
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
idx = data.find(n)
if idx >= 0:
ctx = re.sub(rb"[^\x20-\x7e]+", b" ", data[max(0, idx - 60): idx + len(n) + 80])
print(n.decode(), "->", ctx.decode())

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
libs = [
b"com/google/gson/Gson",
b"com/fasterxml/jackson",
b"com/alibaba/fastjson",
b"org/json/JSONObject",
b"okhttp3/RequestBody",
b"retrofit2/",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for lib in libs:
print(lib.decode(), data.count(lib))

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
METHOD_HINTS = [
b"isRoot", b"isRooted", b"jailbroken", b"checkRoot", b"detectRoot",
b"detectXposed", b"checkXposed", b"isXposed", b"checkFrida", b"isEmulator",
b"checkIntegrity", b"SafeMode", b"needSafeMode", b"enterSafeMode",
b"showRoot", b"rooted", b"factory settings",
]
with zipfile.ZipFile(APK) as zf:
for name in sorted(zf.namelist()):
if not name.endswith(".dex"):
continue
data = zf.read(name)
hits = [h.decode("ascii", "ignore") for h in METHOD_HINTS if h in data]
if hits:
print(name, ":", ", ".join(hits))

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
TARGETS = [b"isXposed", b"isRooted", b"checkRoot", b"isRoot", b"isEmulator", b"jailbroken"]
with zipfile.ZipFile(APK) as zf:
for dex_name in ["classes6.dex", "classes9.dex", "classes15.dex", "classes3.dex"]:
data = zf.read(dex_name)
print("=== %s ===" % dex_name)
for needle in TARGETS:
if needle not in data:
continue
idx = 0
shown = 0
while shown < 8:
i = data.find(needle, idx)
if i < 0:
break
s = max(0, i - 80)
e = min(len(data), i + 80)
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
if "shopee" in chunk.lower() or "seabank" in chunk.lower() or "bke" in chunk.lower() or "alc" in chunk.lower():
print(" ", needle.decode(), "->", chunk.strip())
shown += 1
idx = i + 1
print()

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needles = [
b"GlobalAuthErrorImpl",
b"errorcodehandler",
b"sendOtp",
b"register",
b"verifyMobile",
b"mobile/register",
b"preRegister",
b"riskToken",
b"risk_token",
b"8424 8050",
b"-1201",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
print(n.decode(), data.count(n))
print("\n--- urls ---")
for m in re.finditer(rb"https?://[a-zA-Z0-9._/-]{8,120}", data):
u = m.group().decode()
if "seabank" in u or "register" in u or "otp" in u or "mobile" in u:
print(u)

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needles = [
b"v2/register",
b"/register",
b"RegisterRequest",
b"registerPhone",
b"signUp",
b"preRegister",
b"riskToken",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
c = data.count(n)
if c:
print(n.decode(), c)
print("\n--- classes near register ---")
for m in re.finditer(rb"Lcom/shopee/bke/[^;]{0,120}register[^;]{0,40};", data, re.I):
print(m.group().decode()[1:-1].replace("/", "."))

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needle = b"uapi/v2/register"
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
idx = data.find(needle)
print("found at", idx)
if idx >= 0:
ctx = data[max(0, idx - 400): idx + 400]
for m in re.finditer(rb"[\x20-\x7e]{4,120}", ctx):
print(" ", m.group().decode("latin1"))

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needles = [
b"mobile",
b"register",
b"otp",
b"signUp",
b"signup",
b"preCheck",
b"checkMobile",
b"sendSms",
b"verifyPhone",
b"4067004",
b"4067",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for n in needles:
if n in data:
print("hit", n.decode())
print("\n--- api paths ---")
for m in re.finditer(rb"/v[0-9]/[a-zA-Z0-9_/-]{6,80}", data):
s = m.group().decode()
if any(k in s.lower() for k in ("user", "auth", "register", "mobile", "otp", "sign", "risk", "phone")):
print(s)

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for needle in [b"riskToken", b"risk_token", b"deviceToken", b"secToken", b"shpsToken", b"mobileNo", b"phoneNo"]:
print(needle.decode(), data.count(needle))
print("\n--- context riskToken ---")
idx = 0
while True:
idx = data.find(b"riskToken", idx)
if idx < 0:
break
ctx = data[max(0, idx - 60): idx + 120]
for m in re.finditer(rb"[\x20-\x7e]{3,60}", ctx):
s = m.group().decode("latin1")
if any(k in s.lower() for k in ["risk", "token", "mobile", "phone", "register", "device"]):
print(" ", s)
idx += 1
if idx > 5000000:
break

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
needle = b"riskToken"
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
idx = data.find(needle)
if idx < 0:
continue
ctx = data[max(0, idx - 300): idx + 300]
print("===", name, "===")
import re
for m in re.finditer(rb"[\x20-\x7e]{4,100}", ctx):
print(" ", m.group().decode("latin1"))

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import re
import sys
import zipfile
KEYS = [
b"rooted", b"jailbroken", b"RootBeer", b"isRoot", b"checkRoot", b"detectRoot",
b"SafetyNet", b"PlayIntegrity", b"magisk", b"/su", b"tamper", b"safemode",
b"SafeMode", b"xposed", b"lsposed", b"frida", b"emulator", b"debuggable",
b"Integrity", b"jailbreak", b"factory settings", b"RiskDevice", b"DeviceRisk",
b"root device", b"seabank", b"SeaBank", b"MariBank", b"alc", b"ALC",
]
def scan_apk(apk_path):
with zipfile.ZipFile(apk_path) as zf:
for name in sorted(zf.namelist()):
if not name.endswith(".dex"):
continue
data = zf.read(name)
print("=== %s (%d bytes) ===" % (name, len(data)))
hits = set()
for key in KEYS:
start = 0
while True:
idx = data.find(key, start)
if idx < 0:
break
s = max(0, idx - 40)
e = min(len(data), idx + len(key) + 60)
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e])
chunk = chunk.decode("ascii", "ignore").strip()
if len(chunk) > 8:
hits.add(chunk)
start = idx + 1
for hit in sorted(hits):
print(" ", hit)
print()
if __name__ == "__main__":
scan_apk(sys.argv[1] if len(sys.argv) > 1 else "reverse/apks/seabank_ph_base.apk")

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
needles = [
b"rooted or jailbroken",
b"factory settings",
b"cannot be accessed",
b"bke_toast_not_support_root",
b"not_support_root",
]
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if not (name.endswith(".dex") or name.endswith(".xml") or name.endswith(".json")):
continue
data = zf.read(name)
for n in needles:
if n in data:
print(name, n.decode())

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
if b"bke_toast_not_support_root" not in data:
continue
print("===", name, "===")
idx = data.find(b"bke_toast_not_support_root")
ctx = re.sub(rb"[^\x20-\x7e]+", b" ", data[max(0, idx - 120): idx + 200])
print(ctx.decode())
for m in re.finditer(rb"Lcom/shopee/bke[^;]{0,120};", data[max(0, idx - 800): idx + 800]):
s = m.group().decode()[1:-1].replace("/", ".")
if "dialog" in s.lower() or "root" in s.lower() or "safemode" in s.lower() or "risk" in s.lower() or "toast" in s.lower():
print(" ", s)

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Scan dex class descriptors for Shopee/SeaBank security SDK."""
import re
import sys
import zipfile
TARGETS = (
"safemode",
"SafeMode",
"alc/",
"ALC",
"integrity",
"rooted",
"jailbroken",
"RootBeer",
"xposed",
"frida",
"isRoot",
"detectRoot",
)
def scan(apk_path):
with zipfile.ZipFile(apk_path) as zf:
for name in sorted(zf.namelist()):
if not name.endswith(".dex"):
continue
data = zf.read(name)
classes = set(re.findall(rb"L[a-zA-Z0-9_$/]+;", data))
hits = []
for raw in classes:
s = raw.decode("ascii", "ignore")
low = s.lower()
if any(t.lower() in low for t in TARGETS):
hits.append(s[1:-1].replace("/", "."))
if hits:
print("=== %s ===" % name)
for h in sorted(set(hits)):
print(h)
print()
if __name__ == "__main__":
scan(sys.argv[1])

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
with zipfile.ZipFile(str(APK)) as zf:
data = zf.read("classes11.dex")
for pat in [b"loadLibrary", b"libshpssdk", b"JNI_OnLoad", b"RegisterNatives", b"native "]:
print(pat.decode(), data.count(pat))
print("\n--- classes with shpssdkbank ---")
for m in re.finditer(rb"Lcom/shopee/shpssdkbank/[a-zA-Z0-9_$;/]+;", data):
s = m.group().decode()
if "uvu" in s or "SPS" in s or "SHPS" in s or "Native" in s:
if s not in []:
pass
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".") for m in re.finditer(rb"Lcom/shopee/shpssdkbank/[a-zA-Z0-9_$]+;", data)))
for c in classes[:60]:
print(c)

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
classes = [
b"Lcom/shopee/shpssdk/SPSRiskTokenCallback;",
b"Lcom/shopee/shpssdkbank/SPSRiskTokenCallback;",
b"Lcom/shopee/shpssdk/SPSResultCallback;",
b"Lcom/shopee/shpssdkbank/SPSResultCallback;",
b"Lcom/shopee/shpssdk/SHPSSDK;",
b"Lcom/shopee/shpssdkbank/SPSAssessRisk;",
]
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for c in classes:
name = c.decode()[1:-1].replace("/", ".")
print("===", name, "===")
idx = 0
while True:
idx = data.find(c, idx)
if idx < 0:
break
ctx = data[max(0, idx - 150): idx + 400]
for m in re.finditer(rb"[a-zA-Z][a-zA-Z0-9_$]{2,40}", ctx):
s = m.group().decode()
if any(k in s.lower() for k in ["token", "risk", "result", "callback", "assess", "get", "on"]):
if len(s) > 4:
print(" ", s)
idx += 1
break

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
import re
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
targets = [
"Lcom/shopee/shpssdkbank/SHPSSDK;",
"Lcom/shopee/shpssdk/SHPSSDK;",
]
with zipfile.ZipFile(str(APK)) as zf:
for dex_name in ["classes11.dex", "classes10.dex", "classes6.dex"]:
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_scan.dex"
tmp.write_bytes(zf.read(dex_name))
out = subprocess.check_output(
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
)
for target in targets:
capture = False
for line in out.splitlines():
if f"Class descriptor : '{target}'" in line:
capture = True
print("===", dex_name, target, "===")
elif capture and line.startswith(" Class descriptor"):
break
if capture and ("native" in line.lower() or "loadLibrary" in line
or "System" in line and "load" in line):
print(line.strip())
if capture and "name :" in line and "type :" in line:
pass
if capture and "access : 0x0101" in line or (
capture and "NATIVE" in line):
print(line.strip())
# also grep dex binary for loadLibrary strings near shpssdk
with zipfile.ZipFile(str(APK)) as zf:
data = zf.read("classes11.dex")
for m in re.finditer(rb"libshpssdk[^\x00]{0,40}", data):
print("str", m.group().decode("latin1"))

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
needles = [
b"SPSRiskTokenCallback",
b"SPSResultCallback",
b"SPSCallback",
b"getRiskToken",
b"riskToken",
b"RiskToken",
b"assessRisk",
b"AssessRisk",
]
with zipfile.ZipFile(APK) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
hits = [n.decode() for n in needles if n in data]
if hits:
print(name, hits)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import re
from pathlib import Path
native = Path(__file__).resolve().parent.parent / "extracted" / "native"
needles = [b"uvwuvwuv", b"NativeEncrypt", b"encryptByRSA", b"aesEncrypt"]
for so in sorted(native.glob("lib*.so")):
data = so.read_bytes()
hits = []
for n in needles:
if n in data:
hits.append(n.decode())
if not hits:
continue
print("\n===", so.name, hits, "===")
for m in sorted(set(re.findall(rb"Java_com_shopee_bke_[A-Za-z0-9_]+", data))):
s = m.decode()
if any(k in s.lower() for k in ("encrypt", "utils", "crypto", "jni")):
print(" ", s)

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import os, re, sys
def extract_strings(data, min_len=5):
out = set()
for m in re.finditer(rb'[\x20-\x7e]{' + str(min_len).encode() + rb',}', data):
out.add(m.group().decode('ascii', 'ignore'))
return out
def scan_app(app_dir, filters):
strings = set()
for name in os.listdir(app_dir):
if not name.endswith('.dex'):
continue
with open(os.path.join(app_dir, name), 'rb') as f:
strings |= extract_strings(f.read())
print('\n===', os.path.basename(app_dir), '===')
for label, rx in filters:
matched = sorted({s for s in strings if re.search(rx, s, re.I)})
print('\n[%s] count=%d' % (label, len(matched)))
for s in matched[:50]:
print(' ', s)
filters = [
('up notifications', r'au\.com\.up\.money\.notifications|Lau/com/up/money/notifications'),
('suncorp messaging', r'au\.com\.suncorp\.marketplace.*(Messaging|Firebase|Notification|Push)'),
('ubank messaging', r'au\.com\.bank86400|bank86400|86400.*(Messaging|Firebase|Notification|Push|MoEngage)'),
('ubank onMessage', r'onMessageReceived|Will try to show push|MoEngage'),
('custom FCM services', r'MessagingService;|HandlerService|FirebaseService'),
]
root = sys.argv[1]
for app in sorted(os.listdir(root)):
p = os.path.join(root, app)
if os.path.isdir(p):
scan_app(p, filters)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import re
from pathlib import Path
SO = Path(__file__).resolve().parent.parent / "extracted" / "native" / "libshpssdk_bank.so"
data = SO.read_bytes()
seen = set()
for m in re.finditer(rb"[\x20-\x7e]{3,}", data):
s = m.group().decode("latin1")
if s in seen or len(s) > 200:
continue
low = s.lower()
if any(k in low for k in [
"proc", "root", "hook", "xposed", "magisk", "frida", "emulator",
"risk", "token", "su", "debug", "maps", "version", "selinux",
"shpssdk", "detect", "jail", "integrity"
]):
seen.add(s)
print(s)

View File

@@ -9,6 +9,9 @@ scopes = [
"org.telegram.messenger.web",
"org.telegram.messenger",
"com.miraclegarden.smsmessage",
"au.com.up.money",
"au.com.suncorp.marketplace",
"au.com.bank86400",
]
shutil.copy2(db_path, db_path + ".bak")

144
scripts/install-frida.ps1 Normal file
View File

@@ -0,0 +1,144 @@
# Install Frida (PC) + frida-server (device) for MariBank trace
param(
[switch]$SkipServer,
[switch]$StartServer
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$FridaDir = Join-Path $ProjectRoot "reverse\frida"
$Req = Join-Path $FridaDir "requirements.txt"
$Sdk = "C:\Users\Administrator\AppData\Local\Android\Sdk"
$Adb = Join-Path $Sdk "platform-tools\adb.exe"
if (-not (Test-Path $Adb)) {
throw "adb not found: $Adb"
}
# Prefer Python 3.8+ (3.6 breaks frida / type hints)
$Py = $null
foreach ($c in @("py -3.12", "py -3", "python3", "python")) {
try {
$v = Invoke-Expression "$c -c `"import sys; print(sys.version_info[:2])`"" 2>$null
if ($v -match "\(3,\s*([89]|1[0-9])\)") {
$Py = $c
break
}
} catch {}
}
if (-not $Py) { $Py = "py -3" }
Write-Host "Using Python: $Py" -ForegroundColor Cyan
$env:SSL_CERT_FILE = $null
$env:REQUESTS_CA_BUNDLE = $null
& Invoke-Expression "$Py -m pip install --upgrade pip --trusted-host pypi.org --trusted-host files.pythonhosted.org" 2>&1 | Out-Null
& Invoke-Expression "$Py -m pip install -r `"$Req`" --trusted-host pypi.org --trusted-host files.pythonhosted.org"
$FridaVer = (& Invoke-Expression "$Py -c `"import frida; print(frida.__version__)`"").Trim()
Write-Host "frida-python $FridaVer installed" -ForegroundColor Green
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + (
& Invoke-Expression "$Py -c `"import sysconfig; import os; print(os.path.join(sysconfig.get_path('scripts')))`""
)
$FridaCli = Get-Command frida -ErrorAction SilentlyContinue
if ($FridaCli) {
Write-Host "frida CLI: $($FridaCli.Source)" -ForegroundColor Green
} else {
Write-Host "frida CLI not on PATH; use: $Py -m frida" -ForegroundColor Yellow
}
Write-Host "`nadb devices:" -ForegroundColor Cyan
& $Adb devices
$serial = (& $Adb devices | Select-String "device$" | Where-Object { $_ -notmatch "List of" } | ForEach-Object { ($_ -split "\s+")[0] } | Select-Object -First 1)
if (-not $serial) {
Write-Warning "No device connected — skip frida-server push. Connect Pixel 6 and re-run."
exit 0
}
if ($SkipServer) { exit 0 }
$Abi = (& $Adb -s $serial shell getprop ro.product.cpu.abi).Trim()
Write-Host "Device ABI: $Abi" -ForegroundColor Cyan
$ArchMap = @{
"arm64-v8a" = "android-arm64"
"armeabi-v7a" = "android-arm"
"x86_64" = "android-x86_64"
"x86" = "android-x86"
}
if (-not $ArchMap.ContainsKey($Abi)) {
throw "Unsupported ABI: $Abi"
}
$FridaAsset = $ArchMap[$Abi]
$ServerName = "frida-server-$FridaVer-$FridaAsset"
$ServerDir = Join-Path $FridaDir "bin"
$ServerBin = Join-Path $ServerDir "frida-server"
$XzFile = Join-Path $ServerDir "$ServerName.xz"
New-Item -ItemType Directory -Force -Path $ServerDir | Out-Null
if (-not (Test-Path $ServerBin)) {
$Url = "https://github.com/frida/frida/releases/download/$FridaVer/$ServerName.xz"
Write-Host "Downloading $Url ..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $Url -OutFile $XzFile -UseBasicParsing
# Windows 10+ tar supports xz in some builds; try 7z or python lzma
$extracted = $false
try {
tar -xf $XzFile -C $ServerDir 2>$null
if (Test-Path (Join-Path $ServerDir $ServerName)) {
Move-Item -Force (Join-Path $ServerDir $ServerName) $ServerBin
$extracted = $true
}
} catch {}
if (-not $extracted) {
$Py312 = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
if (-not (Test-Path $Py312)) { $Py312 = "python" }
& $Py312 -c @"
import lzma
from pathlib import Path
xz = Path(r'$XzFile')
out = Path(r'$ServerBin')
with lzma.open(xz) as f:
out.write_bytes(f.read())
print('extracted', out, out.stat().st_size)
"@
$extracted = Test-Path $ServerBin
}
Remove-Item $XzFile -ErrorAction SilentlyContinue
}
if (-not (Test-Path $ServerBin)) {
throw "frida-server binary missing at $ServerBin"
}
Write-Host "Pushing frida-server to device ..." -ForegroundColor Cyan
& $Adb -s $serial push $ServerBin /data/local/tmp/frida-server
& $Adb -s $serial shell "su -c 'chmod 755 /data/local/tmp/frida-server && pkill -9 frida-server 2>/dev/null; /data/local/tmp/frida-server -D &'" 2>&1 | Out-Null
Start-Sleep -Seconds 2
$check = & $Adb -s $serial shell "su -c 'pgrep frida-server'" 2>&1
if ($check -match "\d") {
Write-Host "frida-server running (pid $check)" -ForegroundColor Green
} else {
Write-Warning "frida-server may not be running. Manual: adb shell su -c '/data/local/tmp/frida-server -D &'"
}
if ($StartServer) {
$Trace = Join-Path $FridaDir "run-frida-trace.ps1"
Write-Host "Starting trace ..." -ForegroundColor Cyan
& $Trace -Mode spawn
}
Write-Host @"
:
PC : frida $FridaVer
: /data/local/tmp/frida-server
:
cd reverse\frida
..\..\scripts\install-frida.ps1 -StartServer
: frida -U -f ph.seabank.seabank -l trace_maribank_register.js
"@ -ForegroundColor Green

View File

@@ -0,0 +1,29 @@
# MariBank / Hook 相关 logcatadb 不在 PATH 时也可用)
param(
[switch]$Clear,
[switch]$Follow
)
$ErrorActionPreference = "Stop"
$sdk = "C:\Users\Administrator\AppData\Local\Android\Sdk"
$adb = Join-Path $sdk "platform-tools\adb.exe"
if (-not (Test-Path $adb)) {
Write-Host "adb not found at $adb" -ForegroundColor Red
exit 1
}
$pattern = "MariBankRoot|1201|seabank|ClashMeta|LSPosed-Bridge.*notiMessage"
if ($Clear) {
& $adb logcat -c
Write-Host "logcat cleared." -ForegroundColor Green
exit 0
}
if ($Follow) {
Write-Host "Following logcat (Ctrl+C to stop)..." -ForegroundColor Cyan
& $adb logcat | Select-String -Pattern $pattern
} else {
& $adb logcat -d | Select-String -Pattern $pattern
}

View File

@@ -0,0 +1,59 @@
# One-time / repeatable layout for reverse/ workspace
$ErrorActionPreference = "Stop"
$Reverse = Join-Path (Split-Path -Parent $PSScriptRoot) "reverse"
$dirs = @(
"scripts", "output", "logs", "logs\frida", "tmp"
)
foreach ($d in $dirs) {
New-Item -ItemType Directory -Force -Path (Join-Path $Reverse $d) | Out-Null
}
# Python scripts at reverse root -> scripts/
Get-ChildItem (Join-Path $Reverse "*.py") -File -ErrorAction SilentlyContinue | ForEach-Object {
Move-Item -Force $_.FullName (Join-Path $Reverse "scripts\$($_.Name)")
}
# Dump outputs
@("native_bridge2.txt", "native_bridge_dump.txt", "phone_vm_dump.txt", "crypto_scan.txt") | ForEach-Object {
$src = Join-Path $Reverse $_
if (Test-Path $src) { Move-Item -Force $src (Join-Path $Reverse "output\$_") }
}
# Logs
$log = Join-Path $Reverse "maribank_crash.log"
if (Test-Path $log) { Move-Item -Force $log (Join-Path $Reverse "logs\maribank_crash.log") }
$fridaDir = Join-Path $Reverse "frida"
@("*.log", "*.log.err", "logcat_capture.txt", "spawn_runner.out", "trace_runner.out", "trace_runner.err", "spawn_runner.err") | ForEach-Object {
Get-ChildItem (Join-Path $fridaDir $_) -File -ErrorAction SilentlyContinue | ForEach-Object {
$dest = Join-Path $Reverse "logs\frida\$($_.Name)"
try {
Move-Item -Force $_.FullName $dest -ErrorAction Stop
} catch {
Write-Warning "skip locked file: $($_.FullName)"
}
}
}
# Temp dex
Get-ChildItem (Join-Path $Reverse "*.dex") -File -ErrorAction SilentlyContinue | ForEach-Object {
Move-Item -Force $_.FullName (Join-Path $Reverse "tmp\$($_.Name)")
}
# APK archive at root
$zip = Join-Path $Reverse "seabank.zip"
if (Test-Path $zip) { Move-Item -Force $zip (Join-Path $Reverse "apks\seabank.zip") }
# Unpacked APK under extracted/
$apkExtract = Join-Path $Reverse "apk_extract"
if (Test-Path $apkExtract) {
$dest = Join-Path $Reverse "extracted\apk_extract"
if (Test-Path $dest) {
Write-Warning "extracted/apk_extract already exists; leaving reverse/apk_extract in place"
} else {
Move-Item -Force $apkExtract $dest
}
}
Write-Host "reverse/ layout done."

View File

@@ -0,0 +1,26 @@
# 启动 MariBank Frida attach trace + logcat
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$FridaDir = Join-Path $Root "reverse\frida"
$Adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
$Py = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
Write-Host "1. 请先在手机上打开 MariBank (Sign up 页面)" -ForegroundColor Yellow
Write-Host "2. 建议暂时关闭 LSPosed 对 MariBank 作用域" -ForegroundColor Yellow
Read-Host "准备好后按 Enter 继续"
& $Adb shell "su -c 'pgrep frida-server || /data/local/tmp/frida-server -D &'" 2>$null | Out-Null
Start-Sleep 1
$logcatOut = Join-Path $Root "reverse\logs\frida\logcat_capture.txt"
New-Item -ItemType Directory -Force -Path (Split-Path $logcatOut) | Out-Null
Start-Process -FilePath $Adb -ArgumentList @(
"logcat","-c"
) -Wait -NoNewWindow
Start-Process -FilePath $Adb -ArgumentList @(
"logcat","-s","notiMessageHook/MariBankRoot:V","MB-TRACE:V","CharacterCryptoManager:V","NativeEncrypt:V"
) -RedirectStandardOutput $logcatOut -WindowStyle Hidden
Write-Host "logcat -> $logcatOut" -ForegroundColor Cyan
Write-Host "Frida attach 启动中..." -ForegroundColor Cyan
& $Py (Join-Path $FridaDir "run_frida_trace.py") attach

View File

@@ -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() {
}

View File

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

View File

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

View File

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

View File

@@ -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|0Root+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);
}
}

View File

@@ -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 PHph.seabank.seabankRoot 检测绕过。
* 逆向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&lt;SPSAssessRisk&gt;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 "";
}
}

View File

@@ -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 相关 HookClassLoader 已就绪)。 */
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;
}
}

View File

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

View File

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

View File

@@ -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 "";

View File

@@ -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.MessagingServiceCapacitor 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);
}
}

View File

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

View File

@@ -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>