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