Compare commits

..

4 Commits

Author SHA1 Message Date
d488a0759f docs: MariBank 实现说明与登录/注册对比
补充 MARIBANK_IMPLEMENTATION 专文及工作说明中的登录路径分析、API 对照与测试建议。
2026-07-03 17:33:39 +08:00
59970a84a8 feat: MariBank 风控 bypass、澳洲银行 Hook 与 reverse 逆向工作区
新增 MariBank/SeaBank PH Root 与 SHPSSDK bypass、riskToken 净化及 Up/Suncorp/ubank 消息 Hook;整理 reverse/ 脚本与 Frida 工具链,并补充当日工作说明文档。
2026-07-03 17:15:16 +08:00
125dfe583b feat: Telegram 双通道抓取、PC 调试台与 Hook 后台转发修复
v2.2.1 更新说明:

- 新增 xposed-module(Telegram/微信/SQLite Hook),双 APK + LSPosed 作用域

- HookMessageReceiver 后台直接 DebugForwarder + goAsync,修复 notiMessage 退后台丢消息

- MessageLogStore 日志持久化;AppConfig 调试/上传开关;PC 调试台 debug-server

- 健康检查去掉联网限制;通知/Hook 通道增加诊断日志

- 安装脚本 install-full/configure-lsposed/start-debug-server;文档 CHANGELOG + HOOK_GUIDE
2026-07-02 16:50:12 +08:00
7c80b7073a 添加 Hook 消息接收与本地调试模式,便于 LSPosed/Telegram 联调。
新增 HookMessageReceiver 与通知服务 Hook 上报链路,支持未登录本地监听;补充 build/install 脚本并忽略 platform-tools 与 xposed 构建产物。
2026-07-02 12:36:08 +08:00
153 changed files with 10537 additions and 134 deletions

14
.gitignore vendored
View File

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

View File

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

View File

@@ -1,6 +1,8 @@
# AGENTS.md — SmsMessage (notiMessage)
Android 应用,监听通知栏短信/通知内容并上传至服务器。适配华为、MIUI 等手机
Android 应用,监听通知栏消息并通过 **双通道**(通知监听 + Xposed Hook抓取内容支持 PC 本地调试台转发
> **Hook 架构与扩展指南**:详见 [docs/HOOK_GUIDE.md](docs/HOOK_GUIDE.md)Telegram 实现、接入新 App 步骤)
## 项目结构
@@ -10,24 +12,43 @@ notiMessage/
│ ├── src/main/java/.../
│ │ ├── Activity/ # Activity 类 (MainActivity, NotificationActivity, SettingActivity, AppListActivity)
│ │ ├── service/ # Service 类 (NotificationService — 通知监听核心)
│ │ ├── network/ # ApiService, DebugForwarder, TokenManager
│ │ ├── comm/ # 通用组件 (CommonAdapter, ViewHolder)
│ │ ├── App.java # Application 类,管理通知列表持久化
│ │ ├── AppConfig.java # 调试转发 / 正式上传开关
│ │ ├── MessageLogStore.java # 监听日志持久化
│ │ ├── MessageInfo.java, AppInfo.java # 数据模型
│ │ └── GsonUtils.java # JSON 工具类
│ └── src/main/res/ # 资源文件
├── xposed-module/ # Xposed Hook 模块 (com.miraclegarden.smsmessage.xposed)
│ └── src/main/java/.../hook/
│ ├── TelegramMessageHook.java # Telegram 专用
│ ├── WeChatMessageHook.java # 微信专用
│ └── SqliteMessageHook.java # 通用 SQLite 兜底
├── debug-server/ # PC 本地调试台 (server.py, 端口 8765)
├── scripts/ # build-debug.ps1, install-full.ps1, start-debug-server.ps1
├── library/ # 基础库模块 (com.miraclegarden.library)
│ └── MiracleGardenActivity<T> # ViewBinding 基类
├── build.gradle # 根构建文件 (AGP 7.3.0)
├── settings.gradle # 模块声明 + 仓库配置
├── docs/HOOK_GUIDE.md # Hook 架构与扩展文档
├── build.gradle # 根构建文件
├── settings.gradle # 模块: app, library, xposed-module
└── gradle.properties # Gradle 配置
```
## 构建命令
```bash
# 构建
./gradlew assembleDebug # 构建 debug APK
./gradlew assembleRelease # 构建 release APK
# 构建(主 App + Xposed 双 APK
./gradlew :app:assembleDebug :xposed-module:assembleDebug
# Windows 一键完整安装(推荐)
powershell -ExecutionPolicy Bypass -File scripts/install-full.ps1
# PC 调试台
powershell -ExecutionPolicy Bypass -File scripts/start-debug-server.ps1
# 构建 release仅主 App
./gradlew assembleRelease
./gradlew build # 完整构建(编译 + lint + 测试)
# Lint
@@ -51,12 +72,12 @@ notiMessage/
## SDK 与依赖版本
| 配置项 | app 模块 | library 模块 |
|--------|---------|-------------|
| compileSdk | 32 | 32 |
| minSdk | 24 | 21 |
| targetSdk | 32 | 32 |
| Java | 17 | 17 |
| 配置项 | app 模块 | library 模块 | xposed-module |
|--------|---------|-------------|---------------|
| compileSdk | 34 | 32 | 34 |
| minSdk | 24 | 21 | 24 |
| targetSdk | 34 | 32 | 34 |
| Java | 17 | 17 | 17 |
**关键依赖**: OkHttp 5.0.0-alpha.10 (网络), Gson 2.9.0 (JSON), ViewBinding (UI绑定), Material 1.6.1
**测试框架**: JUnit 4.13.2, Espresso 3.4.0, AndroidX Test JUnit 1.1.3

View File

@@ -77,6 +77,10 @@
<activity
android:name=".Activity.PermissionActivity"
android:exported="false" />
<activity
android:name=".Activity.KeepAliveSettingsActivity"
android:exported="false"
android:theme="@style/Theme.SmsMessage1" />
<!--通知栏获取短信-->
<service
@@ -101,6 +105,16 @@
</intent-filter>
</receiver>
<!-- Xposed Hook 消息接收 -->
<receiver
android:name=".service.HookMessageReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.miraclegarden.smsmessage.action.HOOK_MESSAGE" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -16,6 +16,7 @@ import com.miraclegarden.smsmessage.databinding.DialogActionConfirmBinding;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.BankInfo;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.TokenManager;
import java.util.ArrayList;
import java.util.List;
@@ -30,8 +31,10 @@ public class ActionConfirmDialog extends Dialog {
int index;
DialogActionConfirmBinding actionConfirmBinding;
ApiService apiService;
TokenManager tokenManager;
List<BankInfo> bankList = new ArrayList<>();
BankInfo selectedBank = null;
private boolean localMode = false;
public interface OnToActionListener {
void toSumbit(AppInfo appInfo);
@@ -47,6 +50,7 @@ public class ActionConfirmDialog extends Dialog {
this.context = context;
this.appInfo = appInfo;
this.apiService = new ApiService(context);
this.tokenManager = TokenManager.getInstance(context);
}
@Override
@@ -59,10 +63,19 @@ public class ActionConfirmDialog extends Dialog {
actionConfirmBinding.tvAppname.setText(appInfo.getAppName());
actionConfirmBinding.tvPackage.setText(appInfo.getPackageName());
// 加载银行账户列表
loadBankList();
// 未登录时走本地模式,不请求服务器银行账户
if (!tokenManager.isLoggedIn()) {
setupLocalMode();
} else {
loadBankList();
}
actionConfirmBinding.sumbitTv.setOnClickListener(view -> {
if (localMode) {
submitLocalMode();
return;
}
if (selectedBank == null) {
Toast.makeText(context, "请选择关联的银行账户", Toast.LENGTH_SHORT).show();
return;
@@ -124,14 +137,43 @@ public class ActionConfirmDialog extends Dialog {
public void onError(ApiError error) {
if (context instanceof android.app.Activity) {
((android.app.Activity) context).runOnUiThread(() -> {
Toast.makeText(context, "加载银行账户失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
dismiss();
Toast.makeText(context, "无法加载银行账户,已切换本地模式", Toast.LENGTH_SHORT).show();
setupLocalMode();
});
}
}
});
}
private void setupLocalMode() {
localMode = true;
selectedBank = null;
actionConfirmBinding.spinnerBank.setVisibility(View.GONE);
View bankSection = (View) actionConfirmBinding.spinnerBank.getParent();
if (bankSection != null) {
for (int i = 0; i < ((android.view.ViewGroup) bankSection).getChildCount(); i++) {
((android.view.ViewGroup) bankSection).getChildAt(i).setVisibility(View.GONE);
}
}
}
private void submitLocalMode() {
if (TextUtils.isEmpty(appInfo.getBankInfoId())) {
appInfo.setBankInfoId("local:" + appInfo.getPackageName());
}
if (TextUtils.isEmpty(appInfo.getName())) {
appInfo.setName(appInfo.getAppName());
}
if (TextUtils.isEmpty(appInfo.getCode())) {
appInfo.setCode(appInfo.getPackageName());
}
if (onToActionListener != null) {
dismiss();
onToActionListener.toSumbit(appInfo);
}
}
private void setupSpinner() {
List<String> bankNames = new ArrayList<>();
bankNames.add("请选择银行账户");

View File

@@ -33,11 +33,12 @@ public class AppListActivity extends MiracleGardenActivity<AppListSettingBinding
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
initView();
}

View File

@@ -27,11 +27,12 @@ public class BankEditActivity extends MiracleGardenActivity<ActivityBankEditBind
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
apiService = new ApiService(this);

View File

@@ -37,11 +37,12 @@ public class BankListActivity extends MiracleGardenActivity<ActivityBankListBind
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
apiService = new ApiService(this);

View File

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

View File

@@ -32,11 +32,12 @@ public class MainActivity extends MiracleGardenActivity<ActivityMainBinding> {
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
initView();
}
@@ -44,11 +45,12 @@ public class MainActivity extends MiracleGardenActivity<ActivityMainBinding> {
@Override
protected void onResume() {
super.onResume();
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
updatePermissionStatus();
}

View File

@@ -1,5 +1,6 @@
package com.miraclegarden.smsmessage.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
@@ -17,6 +18,8 @@ import androidx.annotation.Nullable;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.AppConfig;
import com.miraclegarden.smsmessage.MessageLogStore;
import com.miraclegarden.smsmessage.databinding.ActivityNotificationBinding;
import com.miraclegarden.smsmessage.network.TokenManager;
import com.miraclegarden.smsmessage.service.NotificationService;
@@ -36,19 +39,25 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
String str = (String) msg.obj;
if (str != null && binding != null && binding.tvLog != null) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
String t = format.format(new Date());
binding.tvLog.append(t + " " + str + "\n");
trimLogIfNeeded();
binding.scrollView.post(() ->
binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN));
}
restoreLogFromStore();
}
};
private void restoreLogFromStore() {
if (binding == null || binding.tvLog == null) {
return;
}
binding.tvLog.setText(MessageLogStore.getDisplayText(this));
binding.scrollView.post(() ->
binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN));
}
public static void sendMessage(String str) {
Context appContext = App.getAppContext();
if (appContext != null) {
MessageLogStore.append(appContext, str);
}
NotificationActivity activity = instanceRef != null ? instanceRef.get() : null;
if (activity != null && activity.handler != null) {
Message message = Message.obtain();
@@ -62,22 +71,25 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
instanceRef = new WeakReference<>(this);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
initView();
restoreLogFromStore();
}
@Override
protected void onResume() {
super.onResume();
restoreLogFromStore();
updateUI();
handler.post(statsUpdateRunnable);
}
@@ -98,9 +110,18 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
}
private void initView() {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
if (AppConfig.ENABLE_SERVER_UPLOAD) {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
} else {
binding.tvUsername.setVisibility(android.view.View.GONE);
}
if (!AppConfig.ENABLE_SERVER_UPLOAD) {
binding.tvUploadedCount.setVisibility(android.view.View.GONE);
binding.tvSuccessRate.setVisibility(android.view.View.GONE);
}
binding.ivBack.setOnClickListener(v -> finish());
@@ -110,6 +131,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
});
binding.btnClearLog.setOnClickListener(v -> {
MessageLogStore.clear(this);
binding.tvLog.setText("");
});
@@ -123,7 +145,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
return;
}
if (NotificationService.isMonitoring()) {
if (NotificationService.isMonitoringActive(this)) {
NotificationService.stopMonitoring(this);
sendMessage("正在停止监听...");
} else {
@@ -143,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(
@@ -172,14 +194,16 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
int configuredCount = App.getNotiList(this).size();
binding.tvConfiguredApps.setText("已配置: " + configuredCount + "");
binding.tvMonitoredCount.setText("监听: " + totalCount);
binding.tvUploadedCount.setText("上传: " + uploadedCount);
binding.tvMonitoredCount.setText("已抓取: " + totalCount);
if (totalCount > 0) {
int successRate = (uploadedCount * 100) / totalCount;
binding.tvSuccessRate.setText("成功率: " + successRate + "%");
} else {
binding.tvSuccessRate.setText("成功率: --");
if (AppConfig.ENABLE_SERVER_UPLOAD) {
binding.tvUploadedCount.setText("上传: " + uploadedCount);
if (totalCount > 0) {
int successRate = (uploadedCount * 100) / totalCount;
binding.tvSuccessRate.setText("成功率: " + successRate + "%");
} else {
binding.tvSuccessRate.setText("成功率: --");
}
}
}

View File

@@ -29,11 +29,12 @@ public class PermissionActivity extends MiracleGardenActivity<ActivityPermission
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
initView();
}

View File

@@ -34,11 +34,12 @@ public class SettingActivity extends MiracleGardenActivity<ActivitySettingBindin
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// 调试用:暂时跳过登录
// if (!tokenManager.isLoggedIn()) {
// startActivity(new Intent(this, LoginActivity.class));
// finish();
// return;
// }
initView();
initList();
@@ -55,6 +56,10 @@ public class SettingActivity extends MiracleGardenActivity<ActivitySettingBindin
binding.btnAddApp.setOnClickListener(v -> {
startActivity(new Intent(this, AppListActivity.class));
});
binding.rowKeepAliveSettings.setOnClickListener(v -> {
startActivity(new Intent(this, KeepAliveSettingsActivity.class));
});
}
@Override

View File

@@ -10,9 +10,7 @@ import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
@@ -29,10 +27,12 @@ import java.util.concurrent.TimeUnit;
public class App extends Application {
private static final String TAG = "App";
public static String Hash_value;
private static App instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
String packageName = getApplicationContext().getPackageName();
MessageDigest messageDigest = getMessageDigest();
String signature = getSignature(this, packageName);
@@ -40,14 +40,13 @@ public class App extends Application {
scheduleHealthCheck();
}
private void scheduleHealthCheck() {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
public static Context getAppContext() {
return instance;
}
private void scheduleHealthCheck() {
PeriodicWorkRequest healthCheck = new PeriodicWorkRequest.Builder(
NotificationHealthCheckWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"notification_health_check",

View File

@@ -0,0 +1,38 @@
package com.miraclegarden.smsmessage;
/**
* 应用运行配置。
*/
public final class AppConfig {
/** 是否上传到正式后端judy88.xin */
public static final boolean ENABLE_SERVER_UPLOAD = false;
/** 是否转发到 PC 本地调试服务 */
public static final boolean ENABLE_DEBUG_FORWARD = true;
/**
* 调试服务地址。
* USB + adb reverse: http://127.0.0.1:8765
* Wi-Fi: http://&lt;电脑局域网IP&gt;:8765
*/
public static final String DEBUG_SERVER_URL = "http://127.0.0.1:8765";
/** 是否定期唤醒监听列表中的 App保持进程 / Hook 加载) */
public static final boolean ENABLE_MONITORED_APP_KEEP_ALIVE = true;
/** 保活间隔分钟WorkManager 最小 15 */
public static final int MONITORED_APP_KEEP_ALIVE_MINUTES = 15;
/** 同一 App 两次唤醒最短间隔(分钟) */
public static final int MONITORED_APP_WAKE_COOLDOWN_MINUTES = 10;
/** 优先使用 Rootmonkey 唤醒 + 电池白名单),失败则普通 startActivity */
public static final boolean KEEP_ALIVE_PREFER_ROOT = true;
/** Root 唤醒后是否自动返回桌面(减少停留在银行/TG 界面) */
public static final boolean KEEP_ALIVE_RETURN_HOME = true;
private AppConfig() {
}
}

View File

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

View File

@@ -0,0 +1,101 @@
package com.miraclegarden.smsmessage;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* 监听日志持久化App 在后台时也能保留历史,回到监听页可恢复显示。
*/
public final class MessageLogStore {
private static final String SP_NAME = "message_log";
private static final String KEY_LINES = "lines";
private static final int MAX_LINES = 500;
private static final String SEP = "\u001E";
private MessageLogStore() {
}
public static void append(Context context, String message) {
if (context == null || TextUtils.isEmpty(message)) {
return;
}
Context appContext = context.getApplicationContext();
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
String line = format.format(new Date()) + " " + message;
SharedPreferences sp = appContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String raw = sp.getString(KEY_LINES, "");
List<String> lines = decode(raw);
lines.add(line);
while (lines.size() > MAX_LINES) {
lines.remove(0);
}
sp.edit().putString(KEY_LINES, encode(lines)).apply();
}
public static String getDisplayText(Context context) {
if (context == null) {
return "";
}
StringBuilder sb = new StringBuilder();
for (String line : getLines(context)) {
sb.append(line).append('\n');
}
return sb.toString();
}
public static List<String> getLines(Context context) {
if (context == null) {
return new ArrayList<>();
}
SharedPreferences sp = context.getApplicationContext()
.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
return decode(sp.getString(KEY_LINES, ""));
}
public static void clear(Context context) {
if (context == null) {
return;
}
context.getApplicationContext()
.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
.edit()
.remove(KEY_LINES)
.apply();
}
private static List<String> decode(String raw) {
List<String> lines = new ArrayList<>();
if (TextUtils.isEmpty(raw)) {
return lines;
}
for (String part : raw.split(SEP, -1)) {
if (!TextUtils.isEmpty(part)) {
lines.add(part);
}
}
return lines;
}
private static String encode(List<String> lines) {
if (lines == null || lines.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lines.size(); i++) {
if (i > 0) {
sb.append(SEP);
}
sb.append(lines.get(i));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,113 @@
package com.miraclegarden.smsmessage.network;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.miraclegarden.smsmessage.AppConfig;
import com.miraclegarden.smsmessage.MessageInfo;
import org.json.JSONObject;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 将抓取到的消息转发到 PC 本地调试服务,便于浏览器查看。
*/
public final class DebugForwarder {
private static final String TAG = "DebugForwarder";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
private DebugForwarder() {
}
public static void forward(Context context, MessageInfo messageInfo,
String title, String content, long timestamp, String source) {
forward(context, messageInfo, title, content, timestamp, source, null);
}
public static void forward(Context context, MessageInfo messageInfo,
String title, String content, long timestamp, String source,
Runnable onComplete) {
if (!AppConfig.ENABLE_DEBUG_FORWARD || messageInfo == null) {
if (onComplete != null) {
onComplete.run();
}
return;
}
try {
JSONObject json = new JSONObject();
json.put("source", source != null ? source : "unknown");
json.put("packageName", messageInfo.getPackageName());
json.put("appName", messageInfo.getAppName());
json.put("title", title != null ? title : "");
json.put("group", resolveGroup(title, messageInfo));
json.put("content", content != null ? content : "");
json.put("timestamp", timestamp);
Request request = new Request.Builder()
.url(AppConfig.DEBUG_SERVER_URL + "/api/debug/push")
.header("Content-Type", "application/json")
.post(RequestBody.create(json.toString(), JSON))
.build();
Log.d(TAG, "forwarding [" + source + "] " + messageInfo.getPackageName()
+ " -> " + AppConfig.DEBUG_SERVER_URL);
CLIENT.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, java.io.IOException e) {
Log.w(TAG, "forward failed [" + source + "]: " + e.getMessage());
if (onComplete != null) {
onComplete.run();
}
}
@Override
public void onResponse(Call call, Response response) {
int code = response.code();
response.close();
if (code >= 200 && code < 300) {
Log.d(TAG, "forward ok [" + source + "] " + title);
} else {
Log.w(TAG, "forward http " + code + " [" + source + "] " + title);
}
if (onComplete != null) {
onComplete.run();
}
}
});
} catch (Exception e) {
Log.w(TAG, "forward build failed: " + e.getMessage());
if (onComplete != null) {
onComplete.run();
}
}
}
private static String resolveGroup(String title, MessageInfo messageInfo) {
if (!TextUtils.isEmpty(title)) {
return title.trim();
}
if (messageInfo != null && !TextUtils.isEmpty(messageInfo.getAppName())) {
return messageInfo.getAppName();
}
return "未分类";
}
}

View File

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

View File

@@ -0,0 +1,80 @@
package com.miraclegarden.smsmessage.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.AppConfig;
import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.network.DebugForwarder;
/**
* 接收 Xposed 模块转发的消息Hook 通道)。
* 在 Receiver 内直接转发到 PC避免依赖 startService后台易被系统拦截
*/
public class HookMessageReceiver extends BroadcastReceiver {
private static final String TAG = "HookMessageReceiver";
public static final String ACTION_HOOK_MESSAGE = "com.miraclegarden.smsmessage.action.HOOK_MESSAGE";
public static final String EXTRA_PACKAGE_NAME = "packageName";
public static final String EXTRA_TITLE = "title";
public static final String EXTRA_CONTENT = "content";
public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_SOURCE = "source";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
return;
}
String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
String title = intent.getStringExtra(EXTRA_TITLE);
String content = intent.getStringExtra(EXTRA_CONTENT);
long timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, System.currentTimeMillis());
String source = intent.getStringExtra(EXTRA_SOURCE);
if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(content)) {
Log.w(TAG, "ignore hook: empty package or content");
return;
}
MessageInfo messageInfo = App.getMessageByNotiList(context, packageName);
if (messageInfo == null) {
Log.w(TAG, "ignore hook: not in monitor list, pkg=" + packageName);
NotificationActivity.sendMessage("[Hook] 未配置监听: " + packageName);
return;
}
if (TextUtils.isEmpty(title)) {
title = messageInfo.getAppName();
}
Log.i(TAG, "hook received: pkg=" + packageName + " source=" + source + " title=" + title);
final String finalTitle = title;
NotificationActivity.sendMessage("[Hook/" + source + "] " + finalTitle + " " + content);
Context appContext = context.getApplicationContext();
PendingResult pendingResult = goAsync();
Runnable finish = pendingResult::finish;
Runnable afterForward = () -> {
if (AppConfig.ENABLE_SERVER_UPLOAD) {
NotificationService.submitFromHook(appContext, messageInfo, finalTitle, content, timestamp, source);
}
finish.run();
};
if (AppConfig.ENABLE_DEBUG_FORWARD) {
DebugForwarder.forward(appContext, messageInfo, finalTitle, content, timestamp, source, afterForward);
} else {
afterForward.run();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
@@ -10,6 +11,8 @@ import androidx.work.WorkerParameters;
public class NotificationHealthCheckWorker extends Worker {
private static final String TAG = "NotiHealthCheck";
public NotificationHealthCheckWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@@ -18,7 +21,10 @@ public class NotificationHealthCheckWorker extends Worker {
@Override
public Result doWork() {
if (!isNotificationListenerEnabled()) {
Log.w(TAG, "notification listener disabled, attempting recovery");
NotificationService.toggleNotificationListenerService(getApplicationContext());
} else {
Log.d(TAG, "notification listener ok");
}
return Result.success();
}

View File

@@ -26,11 +26,14 @@ 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;
import com.miraclegarden.smsmessage.model.UploadNotificationResponse;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.DebugForwarder;
import com.miraclegarden.smsmessage.network.TokenManager;
import org.json.JSONException;
@@ -63,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;
@@ -82,35 +87,105 @@ public class NotificationService extends NotificationListenerService {
tokenManager = TokenManager.getInstance(this);
retryManager = new RetryManager(this);
retryManager.setUploadCallback(this::performUpload);
retryManager.restoreFromPersistence();
if (AppConfig.ENABLE_SERVER_UPLOAD) {
retryManager.setUploadCallback(this::performUpload);
retryManager.restoreFromPersistence();
}
}
private static final String ACTION_HOOK_MESSAGE = "ACTION_HOOK_MESSAGE";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && "ACTION_START_MONITORING".equals(intent.getAction())) {
startMonitoring();
if (intent != null) {
if ("ACTION_START_MONITORING".equals(intent.getAction())) {
startMonitoring();
} else if (ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
handleHookMessageIntent(intent);
}
} else if (isMonitoringActive(this)) {
MonitoredAppKeepAliveScheduler.schedule(this);
}
NotificationActivity.sendMessage("监听服务成功!");
return START_STICKY;
}
public static void submitFromHook(Context context, MessageInfo messageInfo,
String title, String content, long timestamp, String source) {
Intent intent = new Intent(context, NotificationService.class);
intent.setAction(ACTION_HOOK_MESSAGE);
intent.putExtra("hook_package", messageInfo.getPackageName());
intent.putExtra("hook_title", title);
intent.putExtra("hook_content", content);
intent.putExtra("hook_timestamp", timestamp);
intent.putExtra("hook_source", source);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
Log.d(TAG, "submitFromHook: service started for " + messageInfo.getPackageName());
} catch (Exception e) {
Log.e(TAG, "submitFromHook failed: " + e.getMessage(), e);
NotificationActivity.sendMessage("[Hook] 服务启动失败: " + e.getMessage());
}
}
private void handleHookMessageIntent(Intent intent) {
String packageName = intent.getStringExtra("hook_package");
MessageInfo messageInfo = App.getMessageByNotiList(this, packageName);
if (messageInfo == null) {
return;
}
String source = intent.getStringExtra("hook_source");
NotificationExtractor.Result result = new NotificationExtractor.Result(
intent.getStringExtra("hook_title"),
intent.getStringExtra("hook_content"),
intent.getLongExtra("hook_timestamp", System.currentTimeMillis())
);
submitNotification(messageInfo, result, source != null ? source : "hook");
}
public static boolean isMonitoring() {
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);
updateForegroundNotification(retryManager != null ? retryManager.getUploadedCount() : 0);
NotificationActivity.sendMessage("已开启持续监听模式");
MonitoredAppKeepAliveScheduler.schedule(this);
if (KeepAliveSettings.isEnabled(this)) {
MonitoredAppActivator.activateAllAsync(this);
}
updateForegroundNotification(retryManager != null
? (AppConfig.ENABLE_SERVER_UPLOAD ? retryManager.getUploadedCount() : retryManager.getTotalCount())
: 0);
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();
@@ -136,7 +211,11 @@ public class NotificationService extends NotificationListenerService {
try {
Intent intent = new Intent(context, NotificationService.class);
intent.setAction("ACTION_START_MONITORING");
context.startService(intent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} catch (Exception e) {
Log.e(TAG, "启动监听失败", e);
}
@@ -152,8 +231,13 @@ public class NotificationService extends NotificationListenerService {
if (messageInfo == null) return;
NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
Log.i(TAG, "onNotificationPosted: pkg=" + sbn.getPackageName()
+ " title=" + result.title + " content=" + result.content);
if (TextUtils.isEmpty(result.title) && TextUtils.isEmpty(result.content)) {
Log.w(TAG, "skip empty notification: " + sbn.getPackageName()
+ " extras=" + (sbn.getNotification().extras != null
? sbn.getNotification().extras.keySet() : "null"));
NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
return;
}
@@ -163,17 +247,24 @@ public class NotificationService extends NotificationListenerService {
}
NotificationActivity.sendMessage(result.title + " " + result.content);
submitNotification(messageInfo, result);
submitNotification(messageInfo, result, "notification");
}
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result) {
// 检查是否已登录
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result,
String debugSource) {
DebugForwarder.forward(this, messageInfo, result.title, result.content,
result.timestamp, debugSource);
if (!AppConfig.ENABLE_SERVER_UPLOAD) {
recordLocalCapture();
return;
}
if (!tokenManager.isLoggedIn()) {
NotificationActivity.sendMessage("未登录,请先登录");
return;
}
// 检查是否关联了银行账户
if (TextUtils.isEmpty(messageInfo.getBankInfoId())) {
NotificationActivity.sendMessage("该应用未关联银行账户,请先配置");
return;
@@ -181,32 +272,35 @@ public class NotificationService extends NotificationListenerService {
try {
JSONObject jsonObject = new JSONObject();
// 根据文档bankInfoId 是建议字段
jsonObject.put("bankInfoId", messageInfo.getBankInfoId());
// data 字段包含通知内容
JSONObject dataObject = new JSONObject();
dataObject.put("title", result.title);
dataObject.put("context", result.content);
dataObject.put("timestamp", result.timestamp);
jsonObject.put("data", dataObject);
// 保留原有字段用于兼容性
jsonObject.put("name", messageInfo.getName());
jsonObject.put("code", messageInfo.getCode());
jsonObject.put("remark", messageInfo.getRemark());
jsonObject.put("appName", messageInfo.getAppName());
jsonObject.put("packageName", messageInfo.getPackageName());
String payload = jsonObject.toString();
NotificationActivity.sendMessage("准备发送服务器:成功");
retryManager.enqueue(payload);
retryManager.enqueue(jsonObject.toString());
} catch (JSONException e) {
Log.e(TAG, "JSON构建失败", e);
NotificationActivity.sendMessage("JSON构建失败: " + e.getMessage());
}
}
private void recordLocalCapture() {
if (retryManager != null) {
retryManager.recordLocalCapture();
updateForegroundNotification(retryManager.getTotalCount());
}
NotificationActivity.updateUI();
}
private void performUpload(String jsonPayload, RetryManager.UploadResultListener listener) {
acquireWakeLockForUpload();
@@ -276,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();
@@ -314,11 +407,12 @@ public class NotificationService extends NotificationListenerService {
}
}
private Notification buildForegroundNotification(int uploadedCount, boolean monitoring) {
private Notification buildForegroundNotification(int count, boolean monitoring) {
String title = monitoring ? "持续监听中" : "通知监听服务";
String countLabel = AppConfig.ENABLE_SERVER_UPLOAD ? "已上传" : "已抓取";
String text = monitoring
? "已上传 " + uploadedCount + " 条 · 持续运行中"
: "已上传 " + uploadedCount + "";
? countLabel + " " + count + " 条 · 持续运行中"
: countLabel + " " + count + "";
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
@@ -330,10 +424,10 @@ public class NotificationService extends NotificationListenerService {
.build();
}
private void updateForegroundNotification(int uploadedCount) {
private void updateForegroundNotification(int count) {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(uploadedCount, isMonitoring));
nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(count, isMonitoring));
}
}
}

View File

@@ -178,6 +178,11 @@ public class RetryManager {
return failedCount;
}
/** 本地监听模式下仅累计抓取条数,不上传服务器 */
public void recordLocalCapture() {
incrementTotalCount();
}
private void incrementTotalCount() {
totalCount++;
sharedPreferences.edit()

View File

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

View File

@@ -107,7 +107,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="监听: 0"
android:text="已抓取: 0"
android:textColor="#333333"
android:textSize="14sp" />

View File

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

View File

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

311
debug-server/server.py Normal file
View File

@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""notiMessage 本地调试转发服务 — 在 PC 浏览器查看手机抓取的消息。"""
import json
import threading
from collections import deque
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
HOST = "0.0.0.0"
PORT = 8765
MAX_MESSAGES = 500
_messages = deque(maxlen=MAX_MESSAGES)
_lock = threading.Lock()
def _now_iso():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _resolve_group(payload):
group = (payload.get("group") or payload.get("title") or "").strip()
if group and "TLRPC$" not in group and "org.telegram.tgnet." not in group:
return group
app = payload.get("appName") or payload.get("packageName") or ""
return app + " / 未分类" if app else "未分类"
def _add_message(payload):
item = dict(payload)
item["group"] = _resolve_group(payload)
item["receivedAt"] = _now_iso()
with _lock:
_messages.appendleft(item)
item["id"] = len(_messages)
print("[{0}] [{1}] [{2}] {3} | {4}".format(
_now_iso(),
item["group"],
payload.get("source", "?"),
payload.get("appName", payload.get("packageName", "")),
(payload.get("content", "") or "")[:80],
))
return item
def _json_response(handler, status, data):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
def _group_messages(messages):
groups = {}
for msg in messages:
key = msg.get("group") or _resolve_group(msg)
if key not in groups:
groups[key] = {
"key": key,
"appName": msg.get("appName") or msg.get("packageName") or "",
"count": 0,
"latestAt": msg.get("receivedAt", ""),
"messages": [],
}
g = groups[key]
g["count"] += 1
g["messages"].append(msg)
if (msg.get("receivedAt") or "") > (g.get("latestAt") or ""):
g["latestAt"] = msg.get("receivedAt", "")
result = sorted(groups.values(), key=lambda x: x.get("latestAt", ""), reverse=True)
for g in result:
g["messages"].sort(key=lambda m: m.get("receivedAt", ""), reverse=True)
return result
def _html_page():
html = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>notiMessage 调试台</title>
<style>
* { box-sizing: border-box; }
body { font-family: ui-monospace, Consolas, monospace; margin: 0; background: #0f1115; color: #e6edf3; height: 100vh; display: flex; flex-direction: column; }
header { padding: 14px 20px; background: #161b22; border-bottom: 1px solid #30363d; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; flex-shrink: 0; }
h1 { margin: 0; font-size: 18px; }
.stat { color: #8b949e; font-size: 13px; }
button { background: #238636; color: #fff; border: 0; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 13px; }
button.secondary { background: #21262d; border: 1px solid #30363d; }
.layout { display: flex; flex: 1; min-height: 0; }
.sidebar { width: 280px; border-right: 1px solid #30363d; background: #161b22; overflow-y: auto; flex-shrink: 0; }
.sidebar h2 { margin: 0; padding: 14px 16px 8px; font-size: 13px; color: #8b949e; font-weight: 600; }
.group-item { padding: 12px 16px; border-bottom: 1px solid #21262d; cursor: pointer; }
.group-item:hover { background: #1c2128; }
.group-item.active { background: #1f2937; border-left: 3px solid #58a6ff; padding-left: 13px; }
.group-name { color: #e6edf3; font-size: 13px; word-break: break-word; }
.group-meta { color: #8b949e; font-size: 11px; margin-top: 4px; }
.content-panel { flex: 1; overflow-y: auto; padding: 16px 20px; }
.panel-title { font-size: 16px; color: #ffa657; margin: 0 0 12px; word-break: break-word; }
.msg { padding: 12px 14px; margin-bottom: 10px; background: #161b22; border: 1px solid #21262d; border-radius: 8px; }
.msg-head { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; font-size: 11px; color: #8b949e; }
.msg-source { color: #58a6ff; }
.msg-sender { color: #d2a8ff; }
.msg-body { color: #e6edf3; font-size: 13px; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
.empty { padding: 40px; text-align: center; color: #8b949e; }
</style>
</head>
<body>
<header>
<h1>notiMessage 调试台</h1>
<span class="stat" id="count">0 条</span>
<span class="stat" id="groupCount">0 群</span>
<span class="stat" id="updated">-</span>
<button onclick="loadMessages()">刷新</button>
<button class="secondary" onclick="clearMessages()">清空</button>
</header>
<div class="layout">
<aside class="sidebar">
<h2>群 / 会话</h2>
<div id="groups"></div>
</aside>
<main class="content-panel">
<h2 class="panel-title" id="panelTitle">请选择左侧群聊</h2>
<div id="messages"></div>
<div class="empty" id="empty" style="display:none">暂无消息,请在手机上开启监听并收一条 Telegram 消息</div>
</main>
</div>
<script>
let groups = [];
let activeGroup = null;
function groupKey(g) { return g.key; }
async function loadMessages() {
const res = await fetch('/api/groups');
groups = await res.json();
document.getElementById('count').textContent =
groups.reduce((n, g) => n + g.count, 0) + '';
document.getElementById('groupCount').textContent = groups.length + '';
document.getElementById('updated').textContent = '更新: ' + new Date().toLocaleTimeString();
const empty = document.getElementById('empty');
const groupsEl = document.getElementById('groups');
groupsEl.innerHTML = '';
if (!groups.length) {
empty.style.display = 'block';
document.getElementById('messages').innerHTML = '';
document.getElementById('panelTitle').textContent = '请选择左侧群聊';
activeGroup = null;
return;
}
empty.style.display = 'none';
if (!activeGroup || !groups.find(g => groupKey(g) === activeGroup)) {
activeGroup = groupKey(groups[0]);
}
for (const g of groups) {
const div = document.createElement('div');
div.className = 'group-item' + (groupKey(g) === activeGroup ? ' active' : '');
div.onclick = () => { activeGroup = groupKey(g); loadMessages(); };
div.innerHTML = `
<div class="group-name">${esc(g.key)}</div>
<div class="group-meta">${esc(g.appName || '')} · ${g.count} 条 · ${esc(g.latestAt || '')}</div>`;
groupsEl.appendChild(div);
}
renderActiveGroup();
}
function renderActiveGroup() {
const g = groups.find(x => groupKey(x) === activeGroup);
const panel = document.getElementById('messages');
if (!g) {
panel.innerHTML = '';
return;
}
document.getElementById('panelTitle').textContent = g.key + '' + g.count + ' 条)';
panel.innerHTML = '';
for (const m of g.messages) {
const div = document.createElement('div');
div.className = 'msg';
const sender = (m.title && m.title !== g.key) ? m.title : '';
div.innerHTML = `
<div class="msg-head">
<span>${esc(m.receivedAt || '')}</span>
<span class="msg-source">${esc(m.source || '')}</span>
${sender ? `<span class="msg-sender">${esc(sender)}</span>` : ''}
</div>
<div class="msg-body">${esc(m.content || '')}</div>`;
panel.appendChild(div);
}
}
async function clearMessages() {
await fetch('/api/messages', { method: 'DELETE' });
activeGroup = null;
loadMessages();
}
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
loadMessages();
setInterval(loadMessages, 2000);
</script>
</body>
</html>"""
return html.encode("utf-8")
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
if self.path.startswith("/api/messages") and self.command == "GET":
return
BaseHTTPRequestHandler.log_message(self, fmt, *args)
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def do_GET(self):
path = urlparse(self.path).path
if path == "/":
body = _html_page()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if path == "/api/messages":
with _lock:
data = list(_messages)
_json_response(self, 200, data)
return
if path == "/api/groups":
with _lock:
data = _group_messages(list(_messages))
_json_response(self, 200, data)
return
if path == "/health":
_json_response(self, 200, {"ok": True})
return
_json_response(self, 404, {"error": "not found"})
def do_POST(self):
path = urlparse(self.path).path
if path not in ("/api/messages", "/api/debug/push", "/api/bills/app-upload"):
_json_response(self, 404, {"error": "not found"})
return
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length) if length else b"{}"
try:
payload = json.loads(raw.decode("utf-8") or "{}")
except ValueError:
_json_response(self, 400, {"error": "invalid json"})
return
if path == "/api/bills/app-upload":
data = payload.get("data") or {}
normalized = {
"source": "app-upload",
"packageName": payload.get("packageName", ""),
"appName": payload.get("appName", ""),
"title": data.get("title", ""),
"content": data.get("context", data.get("content", "")),
"timestamp": data.get("timestamp"),
"raw": payload,
}
else:
normalized = payload
if not normalized.get("group"):
normalized["group"] = _resolve_group(normalized)
item = _add_message(normalized)
_json_response(self, 200, {"ok": True, "id": item.get("id")})
def do_DELETE(self):
path = urlparse(self.path).path
if path != "/api/messages":
_json_response(self, 404, {"error": "not found"})
return
with _lock:
_messages.clear()
_json_response(self, 200, {"ok": True})
def main():
server = ThreadingHTTPServer((HOST, PORT), Handler)
print("notiMessage debug server: http://127.0.0.1:{0}".format(PORT))
print("浏览器打开上述地址即可查看消息")
print("手机经 USB 调试时先执行: adb reverse tcp:8765 tcp:8765")
print("Wi-Fi 调试时将 AppConfig.DEBUG_SERVER_URL 改为 http://<PC局域网IP>:8765")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nstopped")
if __name__ == "__main__":
main()

134
docs/BANK_REVERSE.md Normal file
View File

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

99
docs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,99 @@
# 更新说明
## v2.2.12026-07-02
### 修复Hook 通道在 notiMessage 后台时丢失消息
**问题**
TG 在前台、notiMessage 切到后台时Hook 虽能收到 Xposed 广播,但原先需经 `startService` 才转发到 PC。Android 8+ 会限制后台启动服务,导致 PC 调试台和日志经常收不到 `[Hook/...]` 消息,表现为「必须两个 App 都在当前页面才行」。
**改动**
| 模块 | 说明 |
|------|------|
| `HookMessageReceiver` | 收到广播后**直接在 Receiver 内**调用 `DebugForwarder`;使用 `goAsync()` 等待 HTTP 完成,避免进程被提前回收 |
| `DebugForwarder` | 增加完成回调与详细日志(成功 / HTTP 错误 / 网络失败) |
| `NotificationService` | `submitFromHook` / `requestStartMonitoring` 改用 `startForegroundService`(正式上传模式);通知通道增加诊断日志 |
| `NotificationHealthCheckWorker` | 健康检查**不再要求联网**;每 15 分钟自动尝试恢复通知监听 |
**诊断 logcat 标签**
```text
HookMessageReceiver — Hook 广播到达 / 被跳过
DebugForwarder — PC 转发结果
NotificationService — 系统通知到达 / 空通知跳过
NotiHealthCheck — 定时健康检查
```
```powershell
adb logcat | findstr /i "HookMessageReceiver DebugForwarder NotificationService NotiHealthCheck"
```
**验证步骤**
1. 安装新版主 App`scripts\install-debug.ps1`
2. 启动 PC 调试台:`scripts\start-debug-server.ps1`
3. notiMessage 点「开始监听」后**切到桌面**
4. 打开 TG 发消息 → PC 应出现 `[Hook/xposed_telegram]`
> **说明**TG 在后台时仍走**通知通道**,需 TG 弹出系统通知。若 TG 被系统冻结或未弹通知,请给 TG / notiMessage 设「电池不受限制」,并确认群未静音。
---
## v2.2 + Xposed v1.1.0
### 新功能
- **Telegram 专用 Hook**`TelegramMessageHook`):前台抓取已解密消息,支持文字 / 图片说明 / 群名
- **双 APK 架构**:主 App + `xposed-module`LSPosed 作用域需勾选 Telegram 与 notiMessage
- **PC 本地调试台**`debug-server/server.py`,端口 8765按群/会话分组展示
- **`MessageLogStore`**监听页日志持久化App 后台再打开可恢复历史
- **`DebugForwarder`**:消息 POST 到 PC`AppConfig.ENABLE_DEBUG_FORWARD`
- **安装脚本**`build-debug.ps1``install-debug.ps1``install-full.ps1``configure-lsposed.py`
### 配置(`AppConfig.java`
| 开关 | 当前默认值 | 含义 |
|------|------------|------|
| `ENABLE_SERVER_UPLOAD` | `false` | 正式后端上传 |
| `ENABLE_DEBUG_FORWARD` | `true` | 转发到 PC 调试台 |
| `DEBUG_SERVER_URL` | `http://127.0.0.1:8765` | 调试服务地址USB 用 adb reverse |
### 双通道机制
| 场景 | 通道 | 日志标识 |
|------|------|----------|
| TG 后台 / 锁屏有系统通知 | 通知监听 | 无前缀 |
| TG 前台(通常无通知) | Xposed Hook | `[Hook/xposed_telegram]` |
### 已知限制
- 通用 SQLite Hook 对 Telegram **无效**(消息在加密 `data` 字段)
- 监听包名须与实际安装一致,如 `org.telegram.messenger.web`
- 空内容通知(纯图片无文字)会被跳过
- Android Studio 直接 Run 只装主 App完整能力需 `install-full.ps1`
### 文档
- 架构与扩展:`docs/HOOK_GUIDE.md`
- Agent 说明:`AGENTS.md`
---
## 安装与升级
```powershell
# 构建
powershell -ExecutionPolicy Bypass -File scripts\build-debug.ps1
# 安装双 APK
powershell -ExecutionPolicy Bypass -File scripts\install-debug.ps1
# 完整安装(含 LSPosed 作用域 + adb reverse + 电池白名单)
powershell -ExecutionPolicy Bypass -File scripts\install-full.ps1
# PC 调试台
powershell -ExecutionPolicy Bypass -File scripts\start-debug-server.ps1
```
升级 v2.2.1 后**至少需重装主 App**;若只改了主 App 代码Xposed 模块无需重装。

290
docs/HOOK_GUIDE.md Normal file
View File

@@ -0,0 +1,290 @@
# 消息抓取架构与 Hook 扩展指南
本文说明 notiMessage 的双通道抓取机制、Telegram 的实现方式,以及日后接入其他 App 的步骤。
---
## 1. 总体架构
notiMessage 使用 **两条独立通道** 抓取消息,互为补充:
```
┌─────────────────────────────────────────────────────────────────┐
│ 目标 App如 Telegram
└───────────────┬─────────────────────────────┬───────────────────┘
│ │
后台/锁屏弹系统通知 前台打开 App、消息入库/解码
│ │
▼ ▼
NotificationListenerService Xposed Hook 模块
(NotificationService) (xposed-module)
│ │
│ │ Broadcast
│ ▼
│ HookMessageReceiver
│ │ goAsync()
│ ├─ MessageLogStore立即写入
│ ├─ DebugForwarder直接转发 PC
│ └─ submitFromHook仅正式上传模式
│ │
└──────────────┬──────────────┘
NotificationService.submitNotification()(通知通道)
┌────────────────┼────────────────┐
▼ ▼ ▼
MessageLogStore DebugForwarder (可选)正式后端上传
App 本地日志) PC 调试台) AppConfig.ENABLE_SERVER_UPLOAD
```
| 通道 | 触发条件 | 代码入口 | 日志前缀 |
|------|----------|----------|----------|
| **通知监听** | 目标 App 在后台且弹出系统通知 | `NotificationService.onNotificationPosted()` | 无前缀 |
| **Hook** | 目标 App 进程收到新消息(通常在前台) | `HookMessageReceiver` → 直接 `DebugForwarder` | `[Hook/xposed_telegram]` 等 |
**重要**:仅开通知监听时,目标 App 在前台通常 **不会** 产生系统通知,因此抓不到消息。要覆盖前台场景,必须启用 Xposed Hook。
---
## 2. Telegram 实现方式
### 2.1 包名与作用域
Pixel 6 等设备上 Telegram 常见包名:
| 安装来源 | 包名 |
|----------|------|
| Play / 官网 APK | `org.telegram.messenger` |
| 部分渠道 / Web 版 | `org.telegram.messenger.web` |
LSPosed 作用域需勾选 **目标 Telegram 包名** + **notiMessage 主 App**
默认作用域见 `xposed-module/src/main/res/values/arrays.xml`
### 2.2 为何不 Hook SQLite
Telegram 消息存在 SQLite 的 **`data` 字段(加密二进制 TL 序列化)**,不是明文 `content`/`text`
通用 `SqliteMessageHook` 只能读明文列,对 Telegram **无效**。因此单独实现 `TelegramMessageHook`
### 2.3 Hook 点
**主路径**Hook `NotificationCenter.postNotificationName(int, Object[])`
- 读取静态字段 `NotificationCenter.didReceiveNewMessages` 作为事件 ID
-`id == didReceiveNewMessages` 时,从参数里的 `List<MessageObject>` 取新消息
- 只处理 **非 outgoing**`messageOwner.out == false`)的消息
**兜底路径**Hook `MessageObject` 全部构造函数(主路径失败时启用)
源码位置:`xposed-module/.../hook/TelegramMessageHook.java`
### 2.4 字段提取逻辑
| 字段 | 提取方式 | 用途 |
|------|----------|------|
| **群名/会话名** | `MessagesController.getPeerTitle(dialogId)``MessageObject.getName()``getChat().title` | 分组标题、转发 `group` |
| **发送者** | `MessageObject.getFromName()` | 群内消息前缀 `发送者: 内容` |
| **正文** | `messageText``messageOwner.message``caption` | 文本内容 |
| **图片+说明** | 识别 `[图片]` 等媒体标签 + 合并 `caption` | 避免只显示「图片」丢失说明 |
| **dialogId** | `MessageObject.getDialogId()` | 去重键 `dialogId:messageId` |
**注意**:不要把 `messageOwner.from_id``TLRPC$TL_peerUser` 对象)直接 `toString()` 当标题,会出现 `TLRPC$TL_peerUser@xxxx`
### 2.5 转发到主 App
Hook 模块通过 `HookForwarder` 发送广播:
```
Action: com.miraclegarden.smsmessage.action.HOOK_MESSAGE
Package: com.miraclegarden.smsmessage
Extras: packageName, title, content, timestamp, source=xposed_telegram
```
主 App 的 `HookMessageReceiver` 接收后:
1. 检查该 `packageName` 是否在监听列表(`App.getMessageByNotiList`
2. 写本地日志(`MessageLogStore`,立即持久化)
3. **直接**转发 PC`DebugForwarder`,不依赖 `startService`,后台可用)
4. 若开启正式上传(`ENABLE_SERVER_UPLOAD`),再经 `submitFromHook` 交给 `NotificationService`
---
## 3. 主 App 相关模块
| 文件 | 职责 |
|------|------|
| `NotificationService.java` | 通知监听 + 统一提交入口 |
| `HookMessageReceiver.java` | 接收 Xposed 广播 |
| `MessageLogStore.java` | 日志持久化(后台也能保留历史) |
| `DebugForwarder.java` | POST 到 PC 调试服务 |
| `AppConfig.java` | `ENABLE_DEBUG_FORWARD``ENABLE_SERVER_UPLOAD` 开关 |
### PC 调试台
```powershell
# 启动本地服务(含 adb reverse
powershell -ExecutionPolicy Bypass -File scripts\start-debug-server.ps1
# 浏览器打开 http://127.0.0.1:8765 ,按群/会话分组展示
```
---
## 4. 日后接入其他 App 的步骤
### 4.1 先判断用哪种 Hook 策略
```
目标 App 前台消息是否需要抓取?
├─ 否 → 仅通知监听即可,在 App「监听设置」里添加包名
└─ 是 → 需要 Xposed Hook
├─ 消息以明文写入 SQLitecontent/text/body 等)?
│ └─ 是 → 可复用 SqliteMessageHook默认兜底
├─ 微信?
│ └─ 是 → 已有 WeChatMessageHookHook WCDB message 表)
└─ 其他Telegram、WhatsApp、银行 App 等)
└─ 需编写 **专用 Hook 类**
```
### 4.2 新增专用 Hook 的标准流程
以新 App `com.example.chat` 为例:
#### 步骤 1分析目标 App
1.`adb shell pm path <包名>` 拉出 APK
2. jadx / dexdump 找消息解码后的类(类似 Telegram 的 `MessageObject`
3. 确认:消息明文出现在哪个字段、哪个时机(构造函数 / 事件总线 / 通知回调)
#### 步骤 2编写 Hook 类
`xposed-module/src/main/java/.../hook/` 新建 `ExampleChatMessageHook.java`
```java
public final class ExampleChatMessageHook {
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
// 1. findClass + findAndHookMethod / hookAllConstructors
// 2. 提取 title群名、content正文、过滤 outgoing/系统消息
// 3. 去重messageId + dialogId
// 4. HookForwarder.forward(context, lpparam.packageName, title, content, "xposed_example");
}
}
```
`HookBridge.java` 增加 source 常量,例如 `SOURCE_XPOSED_EXAMPLE = "xposed_example"`
#### 步骤 3注册到 MainHook
编辑 `MainHook.java`
```java
if ("com.example.chat".equals(lpparam.packageName)) {
ExampleChatMessageHook.install(lpparam);
return;
}
```
#### 步骤 4更新 LSPosed 作用域
编辑 `xposed-module/src/main/res/values/arrays.xml`
```xml
<item>com.example.chat</item>
```
#### 步骤 5主 App 添加监听
在 notiMessage「监听设置」中添加该 App或通过 `shared_prefs/server.xml``saveNotiList`)。
`bankInfoId` 本地调试可用 `local:<包名>`
#### 步骤 6构建、安装、验证
```powershell
# 完整安装(主 App + Xposed + LSPosed 作用域)
powershell -ExecutionPolicy Bypass -File scripts\install-full.ps1
```
验证清单:
- [ ] LSPosed 中模块已启用,作用域勾选 **目标 App + notiMessage**
- [ ] 强制停止并重启目标 App让 Hook 重新加载)
- [ ] notiMessage 监听控制台「开始监听」
- [ ] **后台场景**:收消息 → 通知通道有日志
- [ ] **前台场景**:收消息 → `[Hook/xxx]` 日志 + PC 调试台有数据
- [ ] `adb logcat | grep notiMessageHook` 可见 `installed for <包名>`
### 4.3 复用通用 SqliteMessageHook 的条件
`SqliteMessageHook` 会在 `MainHook` 中作为 **默认兜底** 安装(非微信、非 Telegram 时)。
适用 App 特征:
- 使用标准 `SQLiteDatabase` / `FrameworkSQLiteDatabase`
- 表名含 `message` / `messages`
- `ContentValues` 中有明文列:`content``text``body``msg`
**不适用**Telegram、WhatsApp、Signal 等端到端加密或二进制 `data` 列的 App。
### 4.4 已有专用 Hook 参考
| App | 类 | Hook 目标 |
|-----|-----|-----------|
| 微信 | `WeChatMessageHook` | `com.tencent.wcdb.database.SQLiteDatabase` insert`message` 表 |
| Telegram | `TelegramMessageHook` | `NotificationCenter.didReceiveNewMessages` + `MessageObject` |
| 其他 | `SqliteMessageHook` | 通用 SQLite insert 兜底 |
---
## 5. 构建与安装
| 脚本 | 用途 |
|------|------|
| `scripts/build-debug.ps1` | 构建主 App + Xposed 双 APK |
| `scripts/install-debug.ps1` | 仅安装双 APK |
| `scripts/install-full.ps1` | **推荐**:构建 + 双 APK + LSPosed 作用域 + adb reverse + 电池白名单 |
| `scripts/start-debug-server.ps1` | 启动 PC 调试台 |
**注意**Android Studio 点 Run 只安装 `:app` 主模块,**不会**安装 Xposed 模块。完整功能必须用 `install-full.ps1` 或手动安装两个 APK。
---
## 6. 常见问题
| 现象 | 原因 | 处理 |
|------|------|------|
| 前台有消息App 无日志 | 仅通知通道Hook 未生效 | 检查 LSPosed 作用域、重启目标 App |
| 标题显示 `TLRPC$...` | 误把对象当字符串 | 用 `getPeerTitle` 等 API见 TelegramMessageHook |
| 图片消息只有「图片」 | 说明在 caption 字段 | 合并 `caption` + 媒体标签 |
| PC 有数据App 无历史 | 旧版日志未持久化 | 已用 `MessageLogStore` 修复,更新主 App |
| Hook 日志前缀 `[Hook/...]` 无 | Xposed 未注入 | 确认 Magisk + LSPosed + 模块启用 |
---
## 7. 相关文件索引
```
xposed-module/
├── src/main/assets/xposed_init # 入口 MainHook
├── src/main/java/.../MainHook.java # 包名路由
├── src/main/java/.../HookForwarder.java # 广播转发
├── src/main/java/.../hook/
│ ├── TelegramMessageHook.java
│ ├── WeChatMessageHook.java
│ └── SqliteMessageHook.java
└── src/main/res/values/arrays.xml # xposed_scope 默认作用域
app/
├── src/main/java/.../service/
│ ├── NotificationService.java
│ └── HookMessageReceiver.java
├── src/main/java/.../MessageLogStore.java
├── src/main/java/.../network/DebugForwarder.java
└── src/main/java/.../AppConfig.java
debug-server/server.py # PC 调试台
scripts/install-full.ps1 # 一键完整部署
```

View File

@@ -0,0 +1,568 @@
# MariBank 实现说明与问题记录
> **日期**2026-07-03
> **App**MariBank / SeaBank PH · `ph.seabank.seabank` · v3.22.0 (32200)
> **设备**Pixel 6 · Magisk + Zygisk + LSPosed
> **源码**`xposed-module/.../hook/MariBank*.java`
本文档专门说明当日对 MariBank **注册流程风控绕过** 的 Xposed 实现细节,以及开发/测试中遇到的全部主要问题。通用工作汇总见 [`WORKLOG_2026-07-03.md`](WORKLOG_2026-07-03.md)。
---
## 1. 目标与结果概览
### 1.1 目标
在已 Root 设备上完成:
```
打开 App → Sign up → 输入菲律宾手机号 → Next
→ POST https://api.seabank.ph/uapi/v2/register
→ 进入 OTP / 下一步
```
### 1.2 当日最终结果
| 阶段 | 结果 |
| -------------------- | -------------------------------------- |
| 本地 Root 检测 / 自杀 / 弹窗 | **大部分绕过**,可稳定进入注册页 |
| DFP 设备指纹上报 | **成功**`code=0` |
| riskToken 尾部字段净化 | **Hook 生效**`\|09\|1``\|00\|0` |
| 注册接口 | **失败**,服务端返回 **4067012** / **4067004** |
| 注册请求明文 | **未抓到**body native 加密) |
**结论**:本地层 bypass 已推进到能正常发起注册请求,但 **服务端风控仍拒绝**;当前实现不足以完成注册。
---
## 2. App 风控架构(逆向结论)
MariBank 基于 Shopee BKE 技术栈,风控分 **本地 SDK 层****服务端校验层**
```
┌─────────────────────────────────────────────────────────┐
│ MariBank App │
├─────────────────────────────────────────────────────────┤
│ SafeMode SDK │ Root/篡改检测 → Toast + 自杀 │
│ SHPSSDK (bank) │ riskToken / DFP / requestDefense │
│ libshpssdk_bank.so │ native/proc/maps、hook 库扫描 │
│ libsdkutils.so │ register body native 加密 │
├─────────────────────────────────────────────────────────┤
│ OkHttp / Retrofit │ 出站 HTTP │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
POST /dfp/v1/data/report POST /uapi/v2/register
(设备指纹 + riskToken) (加密 body + 手机号)
│ │
└────────── api.seabank.ph ────┘
4067012 服务端拒绝
```
### 2.1 关键类与 SOv3.22.0
| 组件 | 真实符号 | 说明 |
| ----------- | ------------------------------------------- | ------------------------------------------------- |
| Application | `com.shopee.bke.digitalbank.BkeApplication` | `attachBaseContext` 后 ClassLoader 才完整 |
| SafeMode | `com.shopee.bke.lib.safemode.b` 等 | 方法名混淆,多返回 `boolean`/`int` |
| SHPSSDK | `com.shopee.shpssdkbank.SHPSSDK` | `getRiskToken` / `getRiskSync` / `requestDefense` |
| riskToken 链 | `vvuuuuvvv.wwvuwuwvu(Context)` | classes11.dex真实 token 生成入口 |
| SoUtils | `com.shopee.bke.lib.jni.utils.f` | 加载 `libsdkutils.so`log tag `SoUtils` |
| 加密包装 | `com.shopee.bke.lib.jni.utils.d` | Java 层加密入口 |
| Native 加密 | `com.shopee.bke.lib.jni.utils.uvwuvwuv` | JNI`libsdkutils.so` / `libbkutils.so` |
| SHPS native | `wvvvuwwu.vvuwuuvuu``wwvwvwuvv` | `libshpssdk_bank.so` |
| 字符串解密 | `uvuwwuvwv.uvwwuuvvw.uvuwwwuwu` | 解密 `x-sap-fixme` 等 |
### 2.2 注册相关 API
| 接口 | 作用 | 当日观测 |
| -------------------------- | ------------- | ---------------- |
| `POST /dfp/v1/data/report` | 设备指纹 / DFP 上报 | **成功** `code=0` |
| `POST /uapi/v2/register` | 手机号注册 | **失败** `4067012` |
### 2.3 riskToken 格式(尾部字段)
token 为 pipe 分隔字符串,**最后两段**表示风险标记,例如:
```text
...Base64段|Base64段|xx|09|1
↑ ↑
风险码 Hook/Root 等标志
```
当日 Hook 将尾部 `09|1` 改为 `00|0`(认为表示 Root + Hook 命中。logcat 可见净化日志,但 **register 仍被拒**,说明服务端还校验 token 其他段或独立 DFP 载荷。
### 2.4 登录 vs 注册(当日未测登录,以下为逆向 + 推断)
当日实测路径为 **Sign up → 注册****Log in 登录尚未在设备上验证**。以下基于 APK 字符串扫描与 Hook 架构整理。
#### 2.4.1 现有 Hook 是否覆盖登录
**是。** 当前 Xposed 按 **整包** `ph.seabank.seabank` 加载,不区分注册/登录:
| 能力 | 登录是否同样生效 |
|------|------------------|
| SafeMode / Root 自杀、弹窗拦截 | ✅ |
| SHPSSDK / riskToken、deviceToken 出站净化 | ✅ 所有 OkHttp 请求 |
| DFP `POST /dfp/v1/data/report` | ✅ App 启动即上报 |
| `/proc` 过滤、boot 属性伪装 | ✅ |
**无需为登录单独写 Hook**;能稳定进入 App登录页同样受益。
#### 2.4.2 API 路径不同
登录 **不走** `/uapi/v2/register`,而是独立 auth 链路v3.22.0 base APK DEX 扫描):
| 场景 | 典型路径 |
|------|----------|
| **注册(已实测)** | `POST /uapi/v2/register` |
| **登录预检** | `/v2/auth/precheck``/v1/pin/auth/precheck` |
| **OTP 登录** | `/v2/app/login/otp/get``/v2/app/login/otp/verify` |
| **PIN 登录** | `/v1/pin/auth` |
| **受限环境** | `/v2/restricted/auth``/v2/restricted/auth/precheck` |
| **通用 OTP** | `/v1/otp/send``/v1/otp/verify` |
| **设备指纹(共用)** | `POST /dfp/v1/data/report` |
ViewModel 层面:`RegisterViewModel``LoginViewModel` / `PinCodeViewModel` / `OneTimePasswordViewModel` 分离;**`PhoneNumViewModel` 可能与注册共用**(输入手机号 UI 类似)。
错误展示由 `GlobalAuthErrorImpl`**通用鉴权错误处理** 负责(`matchScene` 多场景4067 封锁弹窗 **不限于注册接口**
#### 2.4.3 登录可能的结果(推断)
| 情况 | 登录表现(推断) |
|------|------------------|
| **设备指纹已被服务端拉黑** | 可能在 `/v2/auth/precheck``/v2/app/login/otp/get` 即返回 **4067012**,与注册类似 |
| **仅「新开户」风控更严** | 若设备未全局封禁,**已有账户 + 正确 PIN/OTP** 的登录 **有机会** 通过 precheck |
| **本地 bypass 足够、服务端仍拒** | DFP `code=0` 但 auth 接口 4067 — 与注册相同,改 riskToken 尾部 **不够** |
#### 2.4.4 注册 vs 登录对比
| 维度 | 注册 (Sign up) | 登录 (Log in) |
|------|----------------|---------------|
| 前提 | 新菲律宾手机号 | **已有 MariBank 账户** + PIN 或 OTP |
| 关键 API | `/uapi/v2/register` | `/v2/auth/precheck``/login/otp/*``/pin/auth` |
| 风控强度 | 新户反欺诈通常 **更严** | 老户相对松,**设备黑名单仍生效** |
| 当日实测 | ❌ 4067012 | ❓ **未测** |
| Hook 专用日志 | 代码中对 `/register` 有较详细 body 日志 | 其它 URL 仅有通用 HTTP / 4067 日志 |
#### 2.4.5 若改测登录
1.**Log in**(非 Sign up输入 **已开户** 手机号
2. logcat`powershell -File scripts\logcat-maribank.ps1 -Follow`
3. 重点观察:`/v2/auth/precheck``/v2/app/login/otp/get``/v1/pin/auth``/dfp/v1/data/report` 的响应码
4. 若仍为 **4067012** → 倾向 **设备/环境级封锁**,换登录路径无法绕过
5. 若 precheck 通过、仅 OTP/PIN 失败 → 多为 **账户凭证** 问题,非 Root bypass 范畴
多次注册失败可能已加重设备风控,登录测试前建议 **冷却 2448h** 或使用未试过的环境。
---
## 3. Xposed 实现架构
### 3.1 入口与加载顺序
`MainHook.java``ph.seabank.seabank` 加载两个 Hook 类:
```java
MariBankShpsNativeHook.install(lpparam); // 第一阶段early
MariBankRootBypassHook.install(lpparam); // 第一阶段 + 调度 deferred
```
**两阶段加载**是当日最关键的工程决策之一:
| 阶段 | 时机 | 安装内容 |
| ------------ | ----------------------------------------- | ------------------------------------------- |
| **Early** | `loadPackage` 立即 | `/proc` 读过滤、系统属性伪装、`Process.killProcess` 拦截 |
| **Deferred** | `BkeApplication.attachBaseContext` **之后** | SafeMode、SHPSSDK、OkHttp 出站净化、riskToken Hook |
**原因**:过早 Hook SHPSSDK / `SoUtils.loadSoLibrary` 会导致 `**libsdkutils.so` 加载死循环 → 白屏**(见 §5.2)。
### 3.2 文件职责
| 文件 | 职责 |
| ----------------------------- | --------------------------------------------------------------------- |
| `MariBankShpsNativeHook.java` | Native 层检测绕过:`/proc/self/maps` 过滤、boot 属性伪装、Build 字段、`requestDefense` |
| `MariBankRootBypassHook.java` | Java 层SafeMode、SHPSSDK risk、OkHttp 出站净化、弹窗/自杀拦截、4067 日志 |
| `MariBankRiskTokenUtil.java` | riskToken / deviceToken 尾部净化工具类 |
---
## 4. 实现细节(按模块)
### 4.1 反自杀与反「假闪退」(`MariBankRootBypassHook`
**问题背景**SafeMode 检测到 Root 后会 `Toast` + `Process.killProcess` + `Activity.finishAffinity`,表现为闪退。
**实现**
1. Hook `Process.killProcess(self)``System.exit` → 直接拦截
2. Hook `Activity.finish` / `finishAffinity` / `finishAndRemoveTask` / `moveTaskToBack`
- 在 **8 秒宽限期**内(`SOFT_CRASH_GUARD_MS`)阻止 BKE 相关 Activity 被风控栈关闭
- 通过栈追踪识别 `safemode` / `shpssdk` / `errorcodehandler` 等调用来源
- **保留用户按返回键**`isUserBackNavigation()`
3. 拦截 Root 相关 Toast 文案(`rooted or jailbroken``magisk/xposed/frida` 等)
**logcat 典型输出**
```text
notiMessageHook/MariBankRoot: blocked killProcess(self)
notiMessageHook/MariBankRoot: blocked finishAffinity on com.shopee.bke...
```
### 4.2 SafeMode 检测绕过
**目标类**(方法名混淆,无法按名 Hook
- `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`
**策略**Hook 所有 **非 static、参数 ≤2、返回 boolean/int** 的实例方法,统一返回 `false` / `0`
**弹窗**Hook `SafeModeRecoverActivity` 相关 Dialog / AlertDialog.show阻止 Root 提示框。
### 4.3 SHPSSDK 风控Java 层)
**boolean 检测类**
- `com.shopee.shpssdkbank.SPSAssessRisk`
- `com.shopee.shpssdk.SPSAssessRisk`
**Token 相关 Hook**`hookShpsToken`
| 方法 | 处理 |
| ------------------------------------ | -------------------------------------------------- |
| `getRiskSync` / `getRiskAsync` | 返回 **空 risk 列表**,避免 `RISK_ROOT=1``RISK_HOOK=4` 写入 |
| `getRiskToken` / `getRiskTokenAsync` | 返回值经 `MariBankRiskTokenUtil.sanitizeRiskToken` |
| `vvuuuuvvv.wwvuwuwvu(Context)` | 真实 token 生成链,最早出口净化 |
| `getSecData` 等 | 字符串返回值净化 |
**RISK 常量(逆向)**`RISK_ROOT=1``RISK_HOOK=4` 等会编码进 token 尾部。
### 4.4 Native 层检测绕过(`MariBankShpsNativeHook`
`**/proc` 过滤**
- Hook `FileInputStream` / `BufferedReader` / `RandomAccessFile`
-`/proc/self/maps``/proc/version``/proc/self/status` 等读结果 **过滤敏感行**
- 隐藏 marker`xposed``lsposed``magisk``frida``zygisk``miraclegarden`
**Boot / 环境伪装**
- `SystemProperties.get` 伪装 `ro.boot.verifiedbootstate``ro.boot.vbmeta.device_state`
- `Build` 字段、`/proc/self/attr/current` → 伪造 SELinux 上下文
**SHPS native 桥**
- `requestDefense` → 解密/处理 `x-sap-fixme` 请求头
- `hookShpsNativeCore` → native 桥接类 boolean 返回 false
**刻意不 Hook**
```java
// MariBankShpsNativeHook.hookShpssInstall()
// 勿 Hook ShpssInstall / vuvuwwwuw会干扰 SoUtils.loadSoLibrary导致 libsdkutils.so 死循环白屏。
```
### 4.5 网络出站净化OkHttp 链)
**不能 Hook `RealInterceptorChain.proceed`**SO 硬编码检测),改为在更外层拦截:
| Hook 点 | 作用 |
| -------------------------------------- | ------------------- |
| `OkHttpClient.newCall` | 替换 `Request` 为净化版 |
| `RealCall.execute` / `enqueue` | 同上 |
| `Request.Builder.build` / `post(body)` | body 写入时净化 |
| `RequestBody.writeTo` | 写出字节时净化 |
| `Gson.toJson` / `fromJson` | JSON 中 riskToken 字段 |
| `JSONObject.put("riskToken", ...)` | 直接改字段 |
| `Response.request().url()` | 记录 URL关联 4067 响应 |
**净化逻辑**`MariBankRiskTokenUtil`
- 正则匹配 pipe-token 尾部 `\d+\|\d+` → 改为 `00|0`
- JSON 字段 `"riskToken"` / `"deviceToken"` 同步处理
**register body 日志**
```text
register request body unreadable (encrypted or one-shot)
```
说明 register 的 body 在 Java 层已是 **加密或一次性 RequestBody**,无法直接读明文。
### 4.6 错误码与弹窗监控
**服务端错误码**logcat 实测):
| code | 含义 |
| --------- | ------------------ |
| `4067004` | 安全封锁(与 4067012 同类) |
| `4067012` | 账户安全临时封锁 |
**客户端弹窗文案**(服务端下发,非本地 Root 弹窗):
```text
For your account's security, this service has been temporarily blocked...
(+632) 8424 8050
```
Hook 对含 `temporarily blocked` / `8424 8050` 的 Toast 会 **记录栈**`logBriefStack`),便于区分本地 vs 服务端弹窗。
---
## 5. 遇到的问题(完整清单)
### 5.1 服务端注册拒绝 — 4067012核心未解问题
**现象**
- 能进入 Sign up 页
- 请求发出后弹服务端封锁弹窗
- 多个号码(`9178854266``9133477799``9171243667` 等)均失败
**logcat**
```text
POST /uapi/v2/register
→ {"code":4067012,"msg":"For your account's security, this service has been temporarily blocked..."}
```
**分析**
1. **不是本地 Root 弹窗**——本地 bypass 后已能到达注册页并成功打 DFP
2. DFP 成功仅说明 **上报链路通**,不代表服务端认可设备
3. riskToken 尾部改掉 **不够**——token 主体可能含加密指纹,或服务端维护设备/号码黑名单
4. 多次失败可能 **加重封锁**(建议冷却 2448h
**待验证**
- 未 Root 干净机 + 菲律宾 IP + 新号码 → 区分设备封禁 vs 号码封禁
- Hook native 加密前明文,看清 register body 到底带了什么
---
### 5.2 过早 Hook 导致白屏libsdkutils 死循环)
**现象**App 启动白屏logcat 反复出现 `SoUtils` / `loadSoLibrary("sdkutils")`
**原因**
- `loadPackage` 时 split APK 的 ClassLoader **尚未绑定**
- 过早 Hook `ShpssInstall``vuvuwwwuw` 或完整 SHPSSDK 链 → 干扰 SO 加载顺序
**解决**
- Early 阶段 **只装** `/proc` 过滤 + 反自杀
-`BkeApplication.attachBaseContext` 回调后再 `installDeferredHooks`
- **永不 Hook** `ShpssInstall` / `vuvuwwwuw`
---
### 5.3 不能 Hook RealInterceptorChain.proceed
**现象**Hook 后触发 native 硬编码检测,行为异常或加重风控。
**逆向依据**`libshpssdk_bank.so` 字符串含 `RealInterceptorChain.proceed`
**解决**:改 Hook `OkHttpClient.newCall``RealCall.execute``RequestBody.writeTo` 等外层点。
---
### 5.4 register 请求体无法读取
**现象**logcat 始终为:
```text
register request body unreadable (encrypted or one-shot)
```
**原因**
- Body 经 `NativeEncryptUtilsWrapper` → native `uvwuvwuv` 加密
- 可能是 one-shot `RequestBody`,写入后不可重读
**影响**:无法在 Java 层确认手机号 JSON 明文内容,也无法在 Xposed 层直接改 register 字段(除 riskToken 出站净化外)。
**后续方向**Frida/native Hook `libsdkutils.so``utils.d` 包装方法。
---
### 5.5 wvvvuwwu.vuwuuwvw native 未找到
**现象**logcat 出现 native 方法实现找不到的警告。
**可能原因**
- LSPosed 与 Frida/Clash 等环境叠加干扰
- 混淆类在不同 split / 版本下位移
- Hook 时机与 ClassLoader 边界问题
**影响**:部分 SHPS native 桥接 Hook 可能未完全生效;但 DFP 已成功,说明主链路部分工作。
---
### 5.6 Frida 动态分析失败
**环境**Frida 17.15.3 + `/data/local/tmp/frida-server`
| 问题 | 详情 |
| --------------- | ------------------------------------------------- |
| attach 找不到 Java | 60s 内 `Java.available == false` |
| 进程名 | 需用 **MariBank** 而非 `ph.seabank.seabank` |
| spawn | native dlopen 可 hookJava 层 `Java is not defined` |
| CLI 退出 | spawn 无保活脚本会立刻 detach |
| 与 LSPosed 冲突 | 建议测 Frida 时 **关闭 LSPosed 对 MariBank 作用域** |
**结果**:未抓到 `/register` 明文 body当日分析 **主要依赖 LSPosed logcat**
脚本位置:`reverse/frida/trace_maribank_register.js``run_frida_trace.py`
---
---
### 5.7 本地 Root 检测 vs 服务端封锁(易混淆)
| 类型 | 文案特征 | 来源 | 当日状态 |
| ------- | -------------------------------------------- | ------------------ | ------------------- |
| 本地 Root | `rooted or jailbroken``magisk/xposed/frida` | SafeMode / SHPSSDK | **已拦截 Toast可继续使用** |
| 服务端封锁 | `temporarily blocked``8424 8050` | API 4067012 响应 | **未绕过** |
---
## 6. 实测 logcat 参考
### 6.1 成功片段DFP + token 净化)
```text
notiMessageHook/MariBankRoot: sanitized riskToken tail 09|1 -> 00|0
notiMessageHook/MariBankRoot: HTTP .../dfp/v1/data/report ... code=0
```
### 6.2 失败片段(注册)
```text
notiMessageHook/MariBankRoot: register request body unreadable (encrypted or one-shot)
notiMessageHook/MariBankRoot: HTTP .../uapi/v2/register ... code=4067012
notiMessageHook/MariBankRoot: security block Toast: For your account's security...
```
### 6.3 推荐过滤命令
```powershell
powershell -File scripts\logcat-maribank.ps1 -Follow
# 或
adb logcat -s notiMessageHook/MariBankRoot:V notiMessageHook/MariBankNative:V
```
---
## 7. 部署与验证
### 7.1 构建安装
```powershell
powershell -ExecutionPolicy Bypass -File scripts\build-debug.ps1
powershell -ExecutionPolicy Bypass -File scripts\install-debug.ps1
```
### 7.2 LSPosed 作用域
必须勾选:
- `ph.seabank.seabank`MariBank
- `com.miraclegarden.smsmessage`(主 App
- Xposed 模块自身
修改 Hook 后:**强制停止 MariBank** 再冷启动。
### 7.3 测试路径
**注册(当日已测)**
```
Sign up → 输入 10 位菲律宾号码(如 9XXXXXXXXX→ Next
观察 logcat 中 /dfp 与 /register 响应
```
**登录(当日未测,供对照)**
```
Log in → 输入已开户手机号 → 继续
观察 /v2/auth/precheck、/login/otp/* 或 /pin/auth 响应
详见 §2.4
```
---
## 8. 已知限制
1. **仅改 riskToken 尾部**不足以通过服务端 register 校验
2. **register body 加密**Java Hook 无法读明文、难改字段
3. **RealInterceptorChain** 等 SO 检测点不可 Hook
4. **ShpssInstall** 不可 Hook限制 SO 加载链上的干预手段
5. **Frida** 在当前 App 上 Java 桥不稳定
6. **多次失败** 可能导致号码/设备进入服务端黑名单
7. **登录流程未实测**;若设备已被 4067012 封锁,登录很可能同样失败(见 §2.4
---
## 9. 后续工作建议
| 优先级 | 方向 | 说明 |
| --- | -------------- | ---------------------------------------- |
| P0 | 对照实验 | 干净机 + PH IP + 新号,确认封禁维度;**有老户时可对比登录 precheck** |
| P1 | Native 明文 Hook | `utils.d` / `libsdkutils.so` 加密前抓包 |
| P2 | riskToken 全链 | 不仅尾部,需理解 pipe 各段含义及 DFP 载荷 |
| P3 | Frida 环境 | 无 LSPosed 冲突、或 Zygisk 隐藏 Frida 后再 attach |
| P4 | 停试冷却 | 已失败号码/设备 2448h 内勿反复请求 |
| — | 官方渠道 | (+632) 8424 8050 |
---
## 10. 相关文件索引
| 路径 | 说明 |
| ----------------------------------------------- | --------------------- |
| `xposed-module/.../MariBankRootBypassHook.java` | Java 层主 Hook~1700 行) |
| `xposed-module/.../MariBankShpsNativeHook.java` | Native/proc 层 Hook |
| `xposed-module/.../MariBankRiskTokenUtil.java` | Token 净化 |
| `xposed-module/.../MainHook.java` | 包名路由 |
| `reverse/frida/jni_targets.md` | JNI / Frida 目标 |
| `reverse/frida/trace_maribank_register.js` | Frida trace 脚本 |
| `scripts/logcat-maribank.ps1` | logcat 过滤 |
| `scripts/start-mari-trace.ps1` | Frida + logcat 联启 |
| `docs/WORKLOG_2026-07-03.md` | 当日全项目工作汇总 |
---
*文档版本2026-07-03 · 对应 commit 分支 `ysc`*

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

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

19
reverse/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

26
scripts/build-debug.ps1 Normal file
View File

@@ -0,0 +1,26 @@
# Build debug APKs for notiMessage (app + xposed-module)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
Set-Location $ProjectRoot
$env:JAVA_HOME = "C:\Program Files\Java\jdk-17"
$javaHomeArg = "-Dorg.gradle.java.home=C:\Program Files\Java\jdk-17"
Write-Host "JAVA_HOME=$env:JAVA_HOME"
Write-Host "Building debug APKs..."
& "$ProjectRoot\gradlew.bat" $javaHomeArg :app:assembleDebug :xposed-module:assembleDebug
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$appApk = Join-Path $ProjectRoot "app\build\outputs\apk\debug\app-debug.apk"
$xposedApk = Join-Path $ProjectRoot "xposed-module\build\outputs\apk\debug\xposed-module-debug.apk"
Write-Host ""
if (Test-Path $appApk) {
Write-Host "App OK: $appApk"
} else {
Write-Host "App APK not found." -ForegroundColor Yellow
}
if (Test-Path $xposedApk) {
Write-Host "Xposed OK: $xposedApk"
} else {
Write-Host "Xposed APK not found." -ForegroundColor Yellow
}

View File

@@ -0,0 +1,34 @@
import shutil
import sqlite3
import sys
db_path = sys.argv[1]
apk_path = sys.argv[2]
hook_pkg = "com.miraclegarden.smsmessage.xposed"
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")
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO modules (mid, module_pkg_name, apk_path, enabled, auto_include) "
"VALUES (?, ?, ?, ?, ?)",
(2, hook_pkg, apk_path, 1, 0),
)
c.execute("DELETE FROM scope WHERE mid = 2")
for pkg in scopes:
c.execute(
"INSERT OR REPLACE INTO scope (mid, app_pkg_name, user_id) VALUES (?, ?, ?)",
(2, pkg, 0),
)
conn.commit()
conn.close()
print("configured", hook_pkg, "scopes=", scopes)

41
scripts/install-debug.ps1 Normal file
View File

@@ -0,0 +1,41 @@
# Install debug APKs to connected Android device via adb
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$appApk = Join-Path $ProjectRoot "app\build\outputs\apk\debug\app-debug.apk"
$xposedApk = Join-Path $ProjectRoot "xposed-module\build\outputs\apk\debug\xposed-module-debug.apk"
if (-not (Test-Path $appApk)) {
Write-Host "App APK not found. Run scripts\build-debug.ps1 first." -ForegroundColor Red
exit 1
}
$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
}
& $adb devices
Write-Host "Installing app: $appApk ..."
& $adb shell settings put global verifier_verify_adb_installs 0 2>$null
& $adb install -r -t -g $appApk
if ($LASTEXITCODE -ne 0) {
Write-Host "Retry install with --bypass-low-target-sdk-block ..."
& $adb install -r -t -g --bypass-low-target-sdk-block $appApk
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if (Test-Path $xposedApk) {
Write-Host "Installing xposed module: $xposedApk ..."
& $adb install -r -t -g $xposedApk
if ($LASTEXITCODE -ne 0) {
& $adb install -r -t -g --bypass-low-target-sdk-block $xposedApk
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else {
Write-Host "Xposed APK not found, skipped." -ForegroundColor Yellow
}
Write-Host "Install OK." -ForegroundColor Green
Write-Host "Remember to enable the module in LSPosed and scope Telegram + notiMessage."

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

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

View File

@@ -0,0 +1,5 @@
# 在 Android Studio 中安装完整版(主 App + Xposed 模块)
# 用法: Run 选 app 只会装主包;完整版请运行此脚本或 install-full.ps1
$ErrorActionPreference = "Stop"
& "$PSScriptRoot\install-full.ps1"

72
scripts/install-full.ps1 Normal file
View File

@@ -0,0 +1,72 @@
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
Set-Location $ProjectRoot
$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: $adb" -ForegroundColor Red
exit 1
}
Write-Host "=== Step 1/5 Build ===" -ForegroundColor Cyan
& "$ProjectRoot\scripts\build-debug.ps1"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$appApk = Join-Path $ProjectRoot "app\build\outputs\apk\debug\app-debug.apk"
$xposedApk = Join-Path $ProjectRoot "xposed-module\build\outputs\apk\debug\xposed-module-debug.apk"
Write-Host "`n=== Step 2/5 Device ===" -ForegroundColor Cyan
& $adb devices
$device = (& $adb devices | Select-String "device$" | Select-Object -First 1)
if (-not $device) {
Write-Host "No authorized adb device" -ForegroundColor Red
exit 1
}
Write-Host "`n=== Step 3/5 Install APKs ===" -ForegroundColor Cyan
& $adb shell settings put global verifier_verify_adb_installs 0 2>$null
& $adb install -r -t -g $appApk
if ($LASTEXITCODE -ne 0) {
& $adb install -r -t -g --bypass-low-target-sdk-block $appApk
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& $adb install -r -t -g $xposedApk
if ($LASTEXITCODE -ne 0) {
& $adb install -r -t -g --bypass-low-target-sdk-block $xposedApk
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "`n=== Step 4/5 LSPosed scope ===" -ForegroundColor Cyan
$apkPath = (& $adb shell pm path com.miraclegarden.smsmessage.xposed 2>$null) -replace '^package:', ''
$apkPath = $apkPath.Trim()
if (-not $apkPath) {
Write-Host "Failed to get xposed module path" -ForegroundColor Red
exit 1
}
Write-Host "xposed apk: $apkPath"
& $adb shell "su -c 'cp /data/adb/lspd/config/modules_config.db /sdcard/Download/modules_config.db; cp /data/adb/lspd/config/modules_config.db-wal /sdcard/Download/modules_config.db-wal 2>/dev/null; cp /data/adb/lspd/config/modules_config.db-shm /sdcard/Download/modules_config.db-shm 2>/dev/null; chmod 644 /sdcard/Download/modules_config.db*'"
$db = Join-Path $env:TEMP "modules_config_full.db"
$prevEap = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& $adb pull /sdcard/Download/modules_config.db $db 2>&1 | Out-Null
& $adb pull /sdcard/Download/modules_config.db-wal "$db-wal" 2>&1 | Out-Null
& $adb pull /sdcard/Download/modules_config.db-shm "$db-shm" 2>&1 | Out-Null
$ErrorActionPreference = $prevEap
python "$ProjectRoot\scripts\configure-lsposed.py" $db $apkPath
& $adb push $db /sdcard/Download/modules_config.db | Out-Null
& $adb shell "su -c 'cp /sdcard/Download/modules_config.db /data/adb/lspd/config/modules_config.db; rm -f /data/adb/lspd/config/modules_config.db-wal /data/adb/lspd/config/modules_config.db-shm; chmod 660 /data/adb/lspd/config/modules_config.db'"
Write-Host "`n=== Step 5/5 System tweaks ===" -ForegroundColor Cyan
& $adb reverse tcp:8765 tcp:8765 2>$null
& $adb shell "su -c 'dumpsys deviceidle whitelist +com.miraclegarden.smsmessage'" 2>$null
& $adb shell "cmd notification allow_listener com.miraclegarden.smsmessage/com.miraclegarden.smsmessage.service.NotificationService" 2>$null
& $adb shell "am force-stop org.telegram.messenger.web"
& $adb shell "am force-stop com.miraclegarden.smsmessage"
Write-Host ""
Write-Host "Full install done." -ForegroundColor Green
Write-Host "Installed: app + xposed module"
Write-Host "Configured: LSPosed scope, adb reverse, battery whitelist, notification listener"
Write-Host "Next: open notiMessage -> start monitoring -> reopen Telegram"

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
# 启动 PC 调试转发服务
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$Server = Join-Path $ProjectRoot "debug-server\server.py"
$sdk = "C:\Users\Administrator\AppData\Local\Android\Sdk"
$adb = Join-Path $sdk "platform-tools\adb.exe"
if (Test-Path $adb) {
Write-Host "Setting adb reverse tcp:8765 ..."
& $adb reverse tcp:8765 tcp:8765 2>$null
}
Write-Host "Starting debug server on http://127.0.0.1:8765"
if (Get-Command py -ErrorAction SilentlyContinue) {
py -3 $Server
} else {
python $Server
}

View File

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

View File

@@ -10,6 +10,7 @@ pluginManagement {
maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' }
maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' }
maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
maven { url 'https://api.xposed.info/' }
}
}
dependencyResolutionManagement {
@@ -24,8 +25,10 @@ dependencyResolutionManagement {
maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' }
maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' }
maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
maven { url 'https://api.xposed.info/' }
}
}
rootProject.name = "SmsMessage"
include ':app'
include ':library'
include ':xposed-module'

View File

@@ -0,0 +1,37 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.miraclegarden.smsmessage.xposed'
compileSdk 34
defaultConfig {
applicationId "com.miraclegarden.smsmessage.xposed"
minSdk 24
targetSdk 34
versionCode 2
versionName "1.1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
abortOnError false
}
}
dependencies {
compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'
}

1
xposed-module/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1 @@
-keep class com.miraclegarden.smsmessage.xposed.** { *; }

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:label="@string/module_name">
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="@string/xposed_description" />
<meta-data
android:name="xposedminversion"
android:value="82" />
<meta-data
android:name="xposedscope"
android:resource="@array/xposed_scope" />
</application>
</manifest>

View File

@@ -0,0 +1 @@
com.miraclegarden.smsmessage.xposed.MainHook

View File

@@ -0,0 +1,24 @@
package com.miraclegarden.smsmessage.xposed;
public final class HookBridge {
public static final String TARGET_APP_PACKAGE = "com.miraclegarden.smsmessage";
public static final String ACTION_HOOK_MESSAGE = "com.miraclegarden.smsmessage.action.HOOK_MESSAGE";
public static final String EXTRA_PACKAGE_NAME = "packageName";
public static final String EXTRA_TITLE = "title";
public static final String EXTRA_CONTENT = "content";
public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_SOURCE = "source";
public static final String SOURCE_XPOSED_SQLITE = "xposed_sqlite";
public static final String SOURCE_XPOSED_WECHAT = "xposed_wechat";
public static final String SOURCE_XPOSED_TELEGRAM = "xposed_telegram";
public static final String SOURCE_XPOSED_UP = "xposed_up";
public static final String SOURCE_XPOSED_UP_NOTIFY = "xposed_up_notify";
public static final String SOURCE_XPOSED_SUNCORP = "xposed_suncorp";
public static final String SOURCE_XPOSED_SUNCORP_NOTIFY = "xposed_suncorp_notify";
public static final String SOURCE_XPOSED_UBANK = "xposed_ubank";
public static final String SOURCE_XPOSED_UBANK_NOTIFY = "xposed_ubank_notify";
private HookBridge() {
}
}

View File

@@ -0,0 +1,36 @@
package com.miraclegarden.smsmessage.xposed;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import de.robv.android.xposed.XposedBridge;
public final class HookForwarder {
private static final String TAG = "notiMessageHook";
private HookForwarder() {
}
public static void forward(Context context, String packageName, String title,
String content, String source) {
if (context == null || TextUtils.isEmpty(packageName) || TextUtils.isEmpty(content)) {
return;
}
try {
Intent intent = new Intent(HookBridge.ACTION_HOOK_MESSAGE);
intent.setPackage(HookBridge.TARGET_APP_PACKAGE);
intent.putExtra(HookBridge.EXTRA_PACKAGE_NAME, packageName);
intent.putExtra(HookBridge.EXTRA_TITLE,
TextUtils.isEmpty(title) ? packageName : title);
intent.putExtra(HookBridge.EXTRA_CONTENT, content);
intent.putExtra(HookBridge.EXTRA_TIMESTAMP, System.currentTimeMillis());
intent.putExtra(HookBridge.EXTRA_SOURCE, source);
context.sendBroadcast(intent);
} catch (Throwable t) {
XposedBridge.log(TAG + " sendBroadcast failed: " + t.getMessage());
}
}
}

View File

@@ -0,0 +1,69 @@
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;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
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
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (lpparam == null || lpparam.packageName == null) {
return;
}
if (MAIN_APP_PACKAGE.equals(lpparam.packageName)) {
return;
}
if (WECHAT_PACKAGE.equals(lpparam.packageName)) {
WeChatMessageHook.install(lpparam);
return;
}
if (TELEGRAM_PACKAGE.equals(lpparam.packageName)
|| TELEGRAM_WEB_PACKAGE.equals(lpparam.packageName)) {
TelegramMessageHook.install(lpparam);
return;
}
if (UP_BANK_PACKAGE.equals(lpparam.packageName)) {
UpBankMessageHook.install(lpparam);
return;
}
if (SUNCORP_PACKAGE.equals(lpparam.packageName)) {
SuncorpBankMessageHook.install(lpparam);
return;
}
if (UBANK_PACKAGE.equals(lpparam.packageName)) {
UbankMessageHook.install(lpparam);
return;
}
if (MARIBANK_PACKAGE.equals(lpparam.packageName)) {
MariBankShpsNativeHook.install(lpparam);
MariBankRootBypassHook.install(lpparam);
return;
}
SqliteMessageHook.install(lpparam);
}
}

View File

@@ -0,0 +1,201 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.app.Notification;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import com.miraclegarden.smsmessage.xposed.HookForwarder;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Map;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
/**
* 银行 App Hook 公共工具RemoteMessage / Notification 解析与去重转发。
*/
public final class BankHookHelper {
private static final String TAG = "notiMessageHook/Bank";
private static final int DEDUP_SIZE = 256;
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
private static final HashSet<String> RECENT_SET = new HashSet<>();
private BankHookHelper() {
}
public static void hookFcmService(de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam lpparam,
String serviceClass, String source) {
try {
de.robv.android.xposed.XposedHelpers.findAndHookMethod(
serviceClass,
lpparam.classLoader,
"onMessageReceived",
"com.google.firebase.messaging.RemoteMessage",
new de.robv.android.xposed.XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.args == null || param.args.length == 0 || param.args[0] == null) {
return;
}
Context context = getContext();
if (context == null) {
return;
}
CharSequence[] parts = extractRemoteMessage(param.args[0]);
forward(context, lpparam.packageName, parts[0], parts[1], source);
}
}
);
XposedBridge.log(TAG + " FCM hook installed: " + serviceClass + " (" + lpparam.packageName + ")");
} catch (Throwable t) {
XposedBridge.log(TAG + " FCM hook failed " + serviceClass + ": " + t.getMessage());
}
}
public static void forwardFromNotification(Context context, String packageName,
Notification notification, String source) {
if (notification == null) {
return;
}
CharSequence[] parts = extractNotification(notification);
forward(context, packageName, parts[0], parts[1], source);
}
private static void forward(Context context, String packageName,
CharSequence title, CharSequence content, String source) {
String titleStr = title != null ? title.toString().trim() : "";
String contentStr = content != null ? content.toString().trim() : "";
if (TextUtils.isEmpty(titleStr) && TextUtils.isEmpty(contentStr)) {
return;
}
if (TextUtils.isEmpty(titleStr)) {
titleStr = packageName;
}
if (TextUtils.isEmpty(contentStr)) {
contentStr = titleStr;
}
String dedupKey = packageName + "|" + titleStr + "|" + contentStr;
if (!remember(dedupKey)) {
return;
}
XposedBridge.log(TAG + " forward [" + source + "] " + titleStr + " / " + contentStr);
HookForwarder.forward(context, packageName, titleStr, contentStr, source);
}
static CharSequence[] extractRemoteMessage(Object remoteMessage) {
String title = "";
String body = "";
try {
Object notification = XposedHelpers.callMethod(remoteMessage, "getNotification");
if (notification != null) {
Object t = XposedHelpers.callMethod(notification, "getTitle");
Object b = XposedHelpers.callMethod(notification, "getBody");
if (t != null) {
title = String.valueOf(t).trim();
}
if (b != null) {
body = String.valueOf(b).trim();
}
}
} catch (Throwable ignored) {
}
if (TextUtils.isEmpty(body)) {
body = stringifyDataMap(remoteMessage);
}
return new CharSequence[]{title, body};
}
private static String stringifyDataMap(Object remoteMessage) {
try {
Object dataObj = XposedHelpers.callMethod(remoteMessage, "getData");
if (!(dataObj instanceof Map)) {
return "";
}
Map<?, ?> data = (Map<?, ?>) dataObj;
if (data.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<?, ?> entry : data.entrySet()) {
if (entry.getKey() == null) {
continue;
}
if (sb.length() > 0) {
sb.append('\n');
}
sb.append(entry.getKey()).append('=');
if (entry.getValue() != null) {
sb.append(entry.getValue());
}
}
return sb.toString().trim();
} catch (Throwable ignored) {
return "";
}
}
static CharSequence[] extractNotification(Notification notification) {
Bundle extras = notification.extras;
if (extras == null) {
return new CharSequence[]{"", ""};
}
CharSequence title = firstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TITLE),
extras.getCharSequence(Notification.EXTRA_TITLE_BIG),
extras.getString(Notification.EXTRA_TITLE)
);
CharSequence text = firstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TEXT),
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT),
extras.getString(Notification.EXTRA_TEXT)
);
if (TextUtils.isEmpty(text)) {
text = extras.getString("gcm.n.body");
}
if (TextUtils.isEmpty(title)) {
title = extras.getString("gcm.n.title");
}
return new CharSequence[]{title, text};
}
private static CharSequence firstNonEmpty(CharSequence... values) {
for (CharSequence value : values) {
if (!TextUtils.isEmpty(value)) {
return value;
}
}
return "";
}
private static synchronized boolean remember(String key) {
if (RECENT_SET.contains(key)) {
return false;
}
RECENT_SET.add(key);
RECENT_KEYS.addLast(key);
while (RECENT_KEYS.size() > DEDUP_SIZE) {
String oldest = RECENT_KEYS.removeFirst();
RECENT_SET.remove(oldest);
}
return true;
}
static Context getContext() {
try {
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
if (app instanceof Context) {
return (Context) app;
}
} catch (Throwable ignored) {
}
return null;
}
}

View File

@@ -0,0 +1,76 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.app.Notification;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* 银行 App 前台兜底Hook NotificationManager.notify从 Notification.extras 取标题/正文。
*/
public final class BankNotificationHook {
private static final String TAG = "notiMessageHook/BankNotify";
private BankNotificationHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam, String source) {
try {
XposedHelpers.findAndHookMethod(
"android.app.NotificationManager",
lpparam.classLoader,
"notify",
String.class,
int.class,
Notification.class,
new NotifyHook(lpparam.packageName, source)
);
XposedHelpers.findAndHookMethod(
"android.app.NotificationManager",
lpparam.classLoader,
"notify",
int.class,
Notification.class,
new NotifyHook(lpparam.packageName, source)
);
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
} catch (Throwable t) {
XposedBridge.log(TAG + " install failed: " + t.getMessage());
}
}
private static final class NotifyHook extends XC_MethodHook {
private final String packageName;
private final String source;
NotifyHook(String packageName, String source) {
this.packageName = packageName;
this.source = source;
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.args == null || param.args.length == 0) {
return;
}
Notification notification = null;
for (Object arg : param.args) {
if (arg instanceof Notification) {
notification = (Notification) arg;
break;
}
}
if (notification == null) {
return;
}
android.content.Context context = BankHookHelper.getContext();
if (context == null) {
return;
}
BankHookHelper.forwardFromNotification(context, packageName, notification, source);
}
}
}

View File

@@ -0,0 +1,100 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.robv.android.xposed.XposedBridge;
/**
* SHPSSDK riskToken 净化:尾部 |09|1 → |00|0Root+Emulator+Hook 标记)。
*/
final class MariBankRiskTokenUtil {
private static final String TAG = "notiMessageHook/MariBankRoot";
private static final Pattern RISK_TOKEN_JSON = Pattern.compile(
"\"riskToken\"\\s*:\\s*\"([^\"]+)\"");
private static final Pattern DEVICE_TOKEN_JSON = Pattern.compile(
"\"deviceToken\"\\s*:\\s*\"([^\"]+)\"");
private static final Pattern RISK_TOKEN_BODY = Pattern.compile(
"([A-Za-z0-9+/=]{8,}\\|[A-Za-z0-9+/=_-]{8,}\\|[A-Za-z0-9+/=_-]{3,}\\|)\\d+(\\|\\d+)");
private MariBankRiskTokenUtil() {
}
static String sanitizeRiskToken(String token) {
if (token == null || token.isEmpty() || !token.contains("|")) {
return token;
}
if (!RISK_TOKEN_BODY.matcher(token).find() && !token.matches(".*\\|\\d+\\|\\d+$")) {
return token;
}
int secondLast = token.lastIndexOf('|');
if (secondLast <= 0) {
return token;
}
secondLast = token.lastIndexOf('|', secondLast - 1);
if (secondLast <= 0) {
return token;
}
String oldTail = token.substring(secondLast + 1);
String neu = token.substring(0, secondLast) + "|00|0";
XposedBridge.log(TAG + " sanitized riskToken tail " + oldTail + " -> 00|0");
return neu;
}
static String sanitizeAllInText(String text) {
if (text == null || !text.contains("|")) {
return text;
}
Matcher m = RISK_TOKEN_BODY.matcher(text);
StringBuffer sb = new StringBuffer();
boolean changed = false;
while (m.find()) {
m.appendReplacement(sb, Matcher.quoteReplacement(m.group(1) + "00|0"));
changed = true;
}
if (!changed) {
String out = sanitizeJsonField(text, RISK_TOKEN_JSON);
out = sanitizeJsonField(out, DEVICE_TOKEN_JSON);
return out;
}
m.appendTail(sb);
XposedBridge.log(TAG + " sanitized riskToken in text len=" + text.length());
return sb.toString();
}
static byte[] sanitizeBytes(byte[] data, int offset, int length) {
if (data == null || length <= 0) {
return data;
}
String text = new String(data, offset, length, StandardCharsets.UTF_8);
if (!text.contains("|")) {
return data;
}
String sanitized = sanitizeAllInText(text);
return sanitized.equals(text) ? data : sanitized.getBytes(StandardCharsets.UTF_8);
}
private static String sanitizeJsonField(String text, Pattern fieldPattern) {
Matcher json = fieldPattern.matcher(text);
if (!json.find()) {
return text;
}
String oldToken = json.group(1);
String newToken = sanitizeRiskToken(oldToken);
return oldToken.equals(newToken) ? text : text.replace(oldToken, newToken);
}
static String tail(String token) {
if (token == null || !token.contains("|")) {
return "n/a";
}
int last = token.lastIndexOf('|');
int second = token.lastIndexOf('|', last - 1);
if (second < 0) {
return "n/a";
}
return token.substring(second + 1);
}
}

View File

@@ -0,0 +1,1738 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.app.Activity;
import android.app.Dialog;
import android.content.res.Resources;
import android.os.Process;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* MariBank / SeaBank PHph.seabank.seabankRoot 检测绕过。
* 逆向SafeMode SDK + SHPSSDK检测到 Root 后会 Toast 并 Process.killProcess 自杀。
*/
public final class MariBankRootBypassHook {
private static final String TAG = "notiMessageHook/MariBankRoot";
public static final String PACKAGE = "ph.seabank.seabank";
/** 服务端注册被拒错误码logcat 实测)。 */
private static final int ERROR_CODE_SECURITY_BLOCKED = 4067004;
private static final int ERROR_CODE_SECURITY_BLOCKED_ALT = 4067012;
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
/** killProcess 被拦截后的宽限期:此期间阻止 finish 造成「假闪退」。 */
private static volatile long lastBlockedSuicideAt = 0L;
private static final long SOFT_CRASH_GUARD_MS = 8000L;
private static volatile int finishBurstCount = 0;
private static volatile long finishBurstStartMs = 0L;
private static final long FINISH_BURST_WINDOW_MS = 800L;
private static final int FINISH_BURST_THRESHOLD = 2;
private static final String[] SAFE_MODE_CLASSES = {
"com.shopee.bke.lib.safemode.b",
"com.shopee.bke.lib.safemode.catchs.a",
"com.shopee.bke.lib.safemode.util.c",
"com.shopee.bke.biz.base.risk.a",
};
private static final String[] ERROR_FLOW_CLASSES = {
"com.shopee.bke.biz.user.errorcodehandler.a",
"com.shopee.bke.biz.user.errorcodehandler.b",
"com.shopee.bke.biz.user.rn.helper.ErrorFlowHelper",
};
private MariBankRootBypassHook() {
}
private static volatile boolean deferredHooksInstalled = false;
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!PACKAGE.equals(lpparam.packageName)) {
return;
}
hookAntiSuicide(lpparam);
hookAntiSoftCrash(lpparam);
scheduleAppHooks(lpparam);
}
/**
* 必须在 BkeApplication.attachBaseContext 完成之后安装:
* loadPackage 时 ClassLoader 未绑定 split APK过早 Hook SHPSSDK 会导致 libsdkutils.so 死循环白屏。
*/
private static void scheduleAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook afterAttach = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
installDeferredHooks(lpparam);
MariBankShpsNativeHook.installDeferred(lpparam);
}
};
try {
XposedHelpers.findAndHookMethod(
"com.shopee.bke.digitalbank.BkeApplication",
lpparam.classLoader,
"attachBaseContext",
"android.content.Context",
afterAttach);
XposedBridge.log(TAG + " waiting attachBaseContext for app hooks");
} catch (Throwable t) {
XposedBridge.log(TAG + " attachBaseContext hook failed, install now: " + t.getMessage());
installDeferredHooks(lpparam);
MariBankShpsNativeHook.installDeferred(lpparam);
}
}
private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) {
if (deferredHooksInstalled) {
return;
}
deferredHooksInstalled = true;
int hooked = 0;
for (String className : SAFE_MODE_CLASSES) {
hooked += hookAllBooleanChecks(lpparam, className);
}
hookSafeModeDialog(lpparam);
hookRootDialogBlock(lpparam);
hookErrorFlowLogging(lpparam);
hooked += hookShpsRisk(lpparam);
hookShpsToken(lpparam);
hookNetworkLogging(lpparam);
XposedBridge.log(TAG + " app hooks installed, booleanHooks=" + hooked);
}
/** SafeMode 类方法名被混淆Hook 所有返回 boolean/int 的实例方法。 */
private static int hookAllBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam, String className) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
continue;
}
Class<?> returnType = method.getReturnType();
if (returnType != boolean.class && returnType != Boolean.class
&& returnType != int.class && returnType != Integer.class) {
continue;
}
if (method.getParameterTypes().length > 2) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (returnType == boolean.class || returnType == Boolean.class) {
param.setResult(false);
} else {
param.setResult(0);
}
}
});
count++;
}
if (count > 0) {
XposedBridge.log(TAG + " hooked " + count + " checks in " + className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
}
return count;
}
/** 阻止检测到 Root 后 Process.killProcess / System.exit 自杀。 */
private static void hookAntiSuicide(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
Process.class,
"killProcess",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
int pid = (Integer) param.args[0];
if (pid == Process.myPid()) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked killProcess(self)");
param.setResult(null);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
System.class,
"exit",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked System.exit");
param.setResult(null);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Runtime.class,
"exit",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked Runtime.exit");
param.setResult(null);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Process.class,
"sendSignal",
int.class,
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
int pid = (Integer) param.args[0];
int signal = (Integer) param.args[1];
if (pid == Process.myPid() && (signal == 9 || signal == 15)) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked sendSignal(self, " + signal + ")");
param.setResult(null);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " sendSignal hook failed: " + t.getMessage());
}
}
/** 阻止 killProcess 失败后通过 finish / finishAffinity 把界面关掉(用户感知为闪退,进程其实还在)。 */
private static void hookAntiSoftCrash(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook blockFinishHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
String methodName = param.method.getName();
if (!shouldBlockFinish(activity, methodName)) {
return;
}
XposedBridge.log(TAG + " blocked " + methodName
+ " after suicide attempt: " + activity.getClass().getSimpleName());
param.setResult(null);
}
};
try {
XposedHelpers.findAndHookMethod(Activity.class, "finish", blockFinishHook);
} catch (Throwable t) {
XposedBridge.log(TAG + " finish hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(Activity.class, "finishAfterTransition", blockFinishHook);
} catch (Throwable t) {
XposedBridge.log(TAG + " finishAfterTransition hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Activity.class,
"finishAffinity",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
if (!shouldBlockFinish(activity, "finishAffinity")) {
return;
}
XposedBridge.log(TAG + " blocked finishAffinity after suicide attempt: "
+ activity.getClass().getSimpleName());
param.setResult(null);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " finishAffinity hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Activity.class,
"finishAndRemoveTask",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
if (!shouldBlockFinish(activity, "finishAndRemoveTask")) {
return;
}
XposedBridge.log(TAG + " blocked finishAndRemoveTask after suicide attempt: "
+ activity.getClass().getSimpleName());
param.setResult(null);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " finishAndRemoveTask hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(Activity.class, "moveTaskToBack", boolean.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
if (!shouldBlockFinish(activity, "moveTaskToBack")) {
return;
}
XposedBridge.log(TAG + " blocked moveTaskToBack after suicide attempt");
param.setResult(null);
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " moveTaskToBack hook failed: " + t.getMessage());
}
}
private static boolean shouldBlockSoftCrash() {
return System.currentTimeMillis() - lastBlockedSuicideAt < SOFT_CRASH_GUARD_MS;
}
private static boolean isBkeActivity(Activity activity) {
String name = activity.getClass().getName();
return name.startsWith("com.shopee.bke") || name.startsWith("com.shopee.bke.digitalbank");
}
private static boolean isUserBackNavigation() {
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
String method = frame.getMethodName();
if ("onBackPressed".equals(method) || "onBackInvoked".equals(method)) {
return true;
}
}
return false;
}
private static void trackFinishBurst() {
long now = System.currentTimeMillis();
if (now - finishBurstStartMs > FINISH_BURST_WINDOW_MS) {
finishBurstCount = 0;
finishBurstStartMs = now;
}
finishBurstCount++;
if (finishBurstCount >= FINISH_BURST_THRESHOLD) {
lastBlockedSuicideAt = now;
}
}
/**
* finish / finishAffinity 往往先于 killProcess
* finishAffinity 为 Root 检测自杀常用路径,对 bke Activity 直接拦截(保留返回键)。
*/
private static boolean shouldBlockFinish(Activity activity, String methodName) {
String name = activity.getClass().getName();
if (name.contains("SafeModeRecoverActivity")) {
return false;
}
if (isUserBackNavigation()) {
return false;
}
if (!isBkeActivity(activity)) {
return false;
}
if ("finishAffinity".equals(methodName) || "finishAndRemoveTask".equals(methodName)) {
return true;
}
trackFinishBurst();
if (shouldBlockSoftCrash()) {
return true;
}
if (finishBurstCount >= FINISH_BURST_THRESHOLD) {
return true;
}
return isRiskRelatedStackTrace();
}
private static boolean isRiskRelatedStackTrace() {
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
String cn = frame.getClassName();
if (cn.contains("safemode")
|| cn.contains("shpssdk")
|| cn.contains("com.shopee.bke")
|| cn.contains("bke.biz.base.risk")
|| cn.contains("errorcodehandler")
|| cn.contains("ErrorFlow")) {
return true;
}
}
return false;
}
/** 记录注册/OTP 错误码路径,便于 logcat 定位 -1201 来源。 */
private static void hookErrorFlowLogging(XC_LoadPackage.LoadPackageParam lpparam) {
for (String className : ERROR_FLOW_CLASSES) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args == null || param.args.length == 0) {
return;
}
String args = Arrays.toString(param.args);
if (args.contains("1201") || args.contains("-1201")
|| args.toLowerCase().contains("error")) {
XposedBridge.log(TAG + " " + className + "."
+ method.getName() + " args=" + args);
}
}
});
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip error flow " + className + ": " + t.getMessage());
}
}
}
/** SHPSSDK 风控:仅 Hook 返回 boolean 的实例方法。 */
private static int hookShpsRisk(XC_LoadPackage.LoadPackageParam lpparam) {
int count = 0;
String[] riskClasses = {
"com.shopee.shpssdkbank.SPSAssessRisk",
"com.shopee.shpssdk.SPSAssessRisk",
};
for (String className : riskClasses) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
continue;
}
Class<?> returnType = method.getReturnType();
if (returnType != boolean.class && returnType != Boolean.class) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(false);
}
});
count++;
}
if (count > 0) {
XposedBridge.log(TAG + " hooked SHPS boolean checks in " + className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip SHPS " + className + ": " + t.getMessage());
}
}
return count;
}
/**
* SHPSSDK 风控 token清空本地 risk 列表,避免 Root/Hook 标记写入 token 上报服务端。
* 逆向getRiskSync / getRiskAsync / assessRisk → List&lt;SPSAssessRisk&gt;RISK_ROOT=1, RISK_HOOK=4 ...
*/
private static void hookShpsToken(XC_LoadPackage.LoadPackageParam lpparam) {
final String contextClass = "android.content.Context";
String[] shpsSdkClasses = {
"com.shopee.shpssdk.SHPSSDK",
"com.shopee.shpssdkbank.SHPSSDK",
};
for (String className : shpsSdkClasses) {
hookEmptyRiskList(lpparam, className, "getRiskSync", contextClass);
hookEmptyRiskList(lpparam, className, "getExtRiskSync", contextClass);
hookRiskAsyncCallback(lpparam, className, "getRiskAsync", contextClass,
className.contains("bank")
? "com.shopee.shpssdkbank.SPSResultCallback"
: "com.shopee.shpssdk.SPSResultCallback");
hookRiskAsyncCallback(lpparam, className, "getExtRiskAsync", contextClass,
className.contains("bank")
? "com.shopee.shpssdkbank.SPSExtResultCallback"
: "com.shopee.shpssdk.SPSExtResultCallback");
hookRiskTokenAsync(lpparam, className, contextClass);
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"getRiskToken",
contextClass,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object token = param.getResult();
if (token instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
param.setResult(sanitized);
XposedBridge.log(TAG + " getRiskToken len="
+ sanitized.length() + " tail="
+ MariBankRiskTokenUtil.tail(sanitized));
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ".getRiskToken: " + t.getMessage());
}
hookTokenStringMethod(lpparam, className, "getLongToken");
hookTokenStringMethod(lpparam, className, "getShortToken");
hookShpsSecData(lpparam, className, contextClass);
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"getSoftToken",
String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object token = param.getResult();
if (token instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
param.setResult(sanitized);
logTokenResult("getSoftToken", sanitized);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ".getSoftToken: " + t.getMessage());
}
}
try {
XposedHelpers.findAndHookMethod(
"com.shopee.shpssdkbank.SHPSSDK",
lpparam.classLoader,
"assessRisk",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(new ArrayList<>());
XposedBridge.log(TAG + " assessRisk -> empty");
}
}
);
} catch (Throwable ignored) {
}
String[] assessRiskClasses = {
"com.shopee.shpssdkbank.SPSAssessRisk",
"com.shopee.shpssdk.SPSAssessRisk",
};
for (String className : assessRiskClasses) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"getType",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(0);
}
}
);
} catch (Throwable ignored) {
}
}
String[] callbackAdapters = {
"com.shopee.shpssdk.SPSCallbackAdapter",
"com.shopee.shpssdkbank.SPSCallbackAdapter",
};
for (String className : callbackAdapters) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"onGetRiskTokenFail",
int.class,
String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " suppressed onGetRiskTokenFail: " + param.args[1]);
param.setResult(null);
}
}
);
} catch (Throwable ignored) {
}
}
hookShpsTokenCore(lpparam);
}
/**
* classes11 真实 token 生成链(早于 SHPSSDK 门面):
* getRiskToken → vvuuuuvvv.wwvuwuwvu(Context)
* getRiskSync → vvuuuuvvv.uuuuuuwvw(Context)
*/
private static void hookShpsTokenCore(XC_LoadPackage.LoadPackageParam lpparam) {
final String coreClass = "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv";
final String contextClass = "android.content.Context";
int hooked = 0;
try {
XposedHelpers.findAndHookMethod(
coreClass,
lpparam.classLoader,
"wwvuwuwvu",
contextClass,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object token = param.getResult();
if (token instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
param.setResult(sanitized);
XposedBridge.log(TAG + " core.wwvuwuwvu len="
+ sanitized.length() + " tail="
+ MariBankRiskTokenUtil.tail(sanitized));
}
}
});
hooked++;
} catch (Throwable t) {
XposedBridge.log(TAG + " skip core.wwvuwuwvu: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
coreClass,
lpparam.classLoader,
"uuuuuuwvw",
contextClass,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(new ArrayList<>());
XposedBridge.log(TAG + " core.uuuuuuwvw -> empty");
}
});
hooked++;
} catch (Throwable t) {
XposedBridge.log(TAG + " skip core.uuuuuuwvw: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
coreClass,
lpparam.classLoader,
"wwwuvwwuu",
contextClass,
String.class,
boolean.class,
boolean.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText((String) result);
param.setResult(sanitized);
XposedBridge.log(TAG + " core.wwwuvwwuu len=" + sanitized.length());
}
}
});
hooked++;
} catch (Throwable t) {
XposedBridge.log(TAG + " skip core.wwwuvwwuu: " + t.getMessage());
}
if (hooked > 0) {
XposedBridge.log(TAG + " shps token core hooks=" + hooked);
}
}
private static void hookEmptyRiskList(
XC_LoadPackage.LoadPackageParam lpparam,
String className,
String methodName,
String contextClassName) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
methodName,
contextClassName,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(new ArrayList<>());
XposedBridge.log(TAG + " " + methodName + " -> empty");
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
}
}
private static void hookRiskAsyncCallback(
XC_LoadPackage.LoadPackageParam lpparam,
String sdkClass,
String methodName,
String contextClassName,
String callbackClassName) {
try {
Class<?> callbackClass = XposedHelpers.findClass(callbackClassName, lpparam.classLoader);
XposedHelpers.findAndHookMethod(sdkClass, lpparam.classLoader, methodName,
contextClassName, callbackClass, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Object original = param.args[1];
if (original == null) {
return;
}
param.args[1] = wrapRiskCallback(lpparam.classLoader, callbackClass, original);
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " skip async " + sdkClass + "." + methodName + ": " + t.getMessage());
}
}
private static Object wrapRiskCallback(
ClassLoader loader,
Class<?> callbackClass,
Object original) {
return Proxy.newProxyInstance(loader, new Class[]{callbackClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("result".equals(method.getName()) && args != null && args.length > 0
&& args[0] instanceof List) {
List<?> list = (List<?>) args[0];
XposedBridge.log(TAG + " async risk callback cleared size=" + list.size());
args[0] = new ArrayList<>();
}
return method.invoke(original, args);
}
});
}
private static void hookRiskTokenAsync(
XC_LoadPackage.LoadPackageParam lpparam,
String sdkClass,
String contextClassName) {
String callbackClassName = sdkClass.contains("bank")
? "com.shopee.shpssdkbank.SPSRiskTokenCallback"
: "com.shopee.shpssdk.SPSRiskTokenCallback";
try {
Class<?> callbackClass = XposedHelpers.findClass(callbackClassName, lpparam.classLoader);
XposedHelpers.findAndHookMethod(
sdkClass,
lpparam.classLoader,
"getRiskTokenAsync",
contextClassName,
callbackClass,
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Object original = param.args[1];
if (original == null) {
return;
}
param.args[1] = wrapRiskTokenCallback(
lpparam.classLoader, callbackClass, original);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + sdkClass + ".getRiskTokenAsync: " + t.getMessage());
}
}
private static Object wrapRiskTokenCallback(
ClassLoader loader,
Class<?> callbackClass,
Object original) {
return Proxy.newProxyInstance(loader, new Class[]{callbackClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("onResult".equals(method.getName()) && args != null && args.length > 0) {
if (args[0] instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) args[0]);
XposedBridge.log(TAG + " getRiskTokenAsync onResult len="
+ sanitized.length() + " tail="
+ MariBankRiskTokenUtil.tail(sanitized));
args[0] = sanitized;
}
}
return method.invoke(original, args);
}
});
}
private static void hookShpsSecData(
XC_LoadPackage.LoadPackageParam lpparam,
String className,
String contextClass) {
String[][] methods = {
{"getSHPSECData", contextClass, "java.lang.String", "boolean"},
{"getSHPSECAllData", contextClass, "java.lang.String", "boolean"},
};
for (String[] sig : methods) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
sig[0],
sig[1],
sig[2],
"boolean",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getResult() instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(
(String) param.getResult());
param.setResult(sanitized);
}
}
}
);
} catch (Throwable ignored) {
}
}
}
private static void hookTokenStringMethod(
XC_LoadPackage.LoadPackageParam lpparam,
String className,
String methodName) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
methodName,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object token = param.getResult();
if (token instanceof String) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
param.setResult(sanitized);
logTokenResult(methodName, sanitized);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
}
}
private static void logTokenResult(String methodName, Object token) {
if (token instanceof String) {
String s = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
XposedBridge.log(TAG + " " + methodName + " len=" + s.length()
+ " tail=" + MariBankRiskTokenUtil.tail(s));
}
}
/** 记录注册 API 响应,净化 riskToken JSON定位 4067004 来源 URL。 */
private static void hookNetworkLogging(XC_LoadPackage.LoadPackageParam lpparam) {
// 勿 Hook RealInterceptorChain.proceed — libshpssdk.so 字符串硬编码检测该 Hook。
hookRequestBuilderBuild(lpparam);
hookRequestBuilderBody(lpparam);
hookOkHttpNewCall(lpparam);
hookRealCallExecute(lpparam);
hookGsonFromJson(lpparam);
hookRequestBodyWriteTo(lpparam);
hookOkHttpResponseUrl(lpparam);
hookOkioBufferWrite(lpparam);
hookOutgoingRequestBody(lpparam);
hookOutgoingRequestBytes(lpparam);
hookJsonRiskTokenPut(lpparam);
hookGsonRiskToken(lpparam);
hookRetrofitGsonConverter(lpparam);
try {
XposedHelpers.findAndHookMethod(
"okhttp3.ResponseBody",
lpparam.classLoader,
"string",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String body = (String) param.getResult();
if (body == null) {
return;
}
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
if (!sanitized.equals(body)) {
param.setResult(sanitized);
body = sanitized;
}
String lower = body.toLowerCase();
if (lower.contains("blocked")
|| body.contains(String.valueOf(ERROR_CODE_SECURITY_BLOCKED))
|| body.contains(String.valueOf(ERROR_CODE_SECURITY_BLOCKED_ALT))
|| lower.contains("risktoken")
|| lower.contains("\"code\"")) {
String url = CURRENT_REQUEST_URL.get();
String snippet = body.length() > 600
? body.substring(0, 600) + "..." : body;
XposedBridge.log(TAG + " HTTP"
+ (url != null ? " " + url : "")
+ " body: " + snippet);
}
CURRENT_REQUEST_URL.remove();
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip okhttp ResponseBody.string: " + t.getMessage());
}
}
/** 出站 Request 构建时记录 URL并在 post/put 阶段净化 body。 */
private static void hookRequestBuilderBuild(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"okhttp3.Request$Builder",
lpparam.classLoader,
"build",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
try {
Object url = XposedHelpers.callMethod(param.getResult(), "url");
if (url != null) {
String urlStr = String.valueOf(url);
CURRENT_REQUEST_URL.set(urlStr);
}
} catch (Throwable ignored) {
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip Request.Builder.build: " + t.getMessage());
}
}
/** 在 RequestBody 挂到 Request 时净化(比抽象 writeTo Hook 更可靠)。 */
private static void hookRequestBuilderBody(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook bodyHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
for (int i = 0; i < param.args.length; i++) {
Object arg = param.args[i];
if (arg == null || !isRequestBody(lpparam.classLoader, arg)) {
continue;
}
Object sanitized = sanitizeRequestBody(lpparam.classLoader, arg);
if (sanitized != arg) {
param.args[i] = sanitized;
XposedBridge.log(TAG + " Request.Builder body riskToken sanitized");
}
}
}
};
String[][] sigs = {
{"post", "okhttp3.RequestBody"},
{"put", "okhttp3.RequestBody"},
{"patch", "okhttp3.RequestBody"},
};
for (String[] sig : sigs) {
try {
XposedHelpers.findAndHookMethod(
"okhttp3.Request$Builder",
lpparam.classLoader,
sig[0],
sig[1],
bodyHook);
} catch (Throwable ignored) {
}
}
try {
XposedHelpers.findAndHookMethod(
"okhttp3.Request$Builder",
lpparam.classLoader,
"method",
"java.lang.String",
"okhttp3.RequestBody",
bodyHook);
} catch (Throwable ignored) {
}
}
private static void hookOkHttpNewCall(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook callHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object sanitized = sanitizeOkHttpRequest(lpparam.classLoader, param.args[0]);
if (sanitized != param.args[0]) {
param.args[0] = sanitized;
}
} catch (Throwable ignored) {
}
}
};
try {
XposedHelpers.findAndHookMethod(
"okhttp3.OkHttpClient",
lpparam.classLoader,
"newCall",
"okhttp3.Request",
callHook);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip OkHttpClient.newCall: " + t.getMessage());
}
}
/** Retrofit 异步/同步最终走 RealCall.execute/enqueue。 */
private static void hookRealCallExecute(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook execHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object request = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
if (request == null) {
return;
}
Object sanitized = sanitizeOkHttpRequest(lpparam.classLoader, request);
if (sanitized != request) {
XposedHelpers.setObjectField(param.thisObject, "originalRequest", sanitized);
}
} catch (Throwable ignored) {
}
}
};
for (String className : new String[]{"okhttp3.RealCall", "okhttp3.internal.connection.RealCall"}) {
try {
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "execute", execHook);
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "enqueue",
"okhttp3.Callback", execHook);
XposedBridge.log(TAG + " hooked " + className);
} catch (Throwable ignored) {
}
}
}
private static Object sanitizeOkHttpRequest(ClassLoader loader, Object request) {
try {
Object url = XposedHelpers.callMethod(request, "url");
if (url == null) {
return request;
}
String urlStr = String.valueOf(url);
CURRENT_REQUEST_URL.set(urlStr);
Object body = XposedHelpers.callMethod(request, "body");
if (body == null) {
if (urlStr.contains("/register")) {
XposedBridge.log(TAG + " register request has null body");
}
return request;
}
String content = readRequestBodyText(loader, body);
if (content.isEmpty()) {
if (urlStr.contains("/register")) {
XposedBridge.log(TAG + " register request body unreadable (encrypted or one-shot)");
}
return request;
}
if (urlStr.contains("/register")) {
int show = Math.min(content.length(), 500);
XposedBridge.log(TAG + " outbound register body: "
+ content.substring(0, show)
+ (content.length() > show ? "..." : ""));
}
Object sanitizedBody = sanitizeRequestBody(loader, body);
if (sanitizedBody == body) {
return request;
}
String method = (String) XposedHelpers.callMethod(request, "method");
Object builder = XposedHelpers.callMethod(request, "newBuilder");
XposedHelpers.callMethod(builder, "method", method, sanitizedBody);
Object newRequest = XposedHelpers.callMethod(builder, "build");
XposedBridge.log(TAG + " sanitized outbound body for " + urlStr);
return newRequest;
} catch (Throwable t) {
return request;
}
}
/** dfp 响应若走 Gson.fromJson(String) 而非 ResponseBody.string需净化入参。 */
private static void hookGsonFromJson(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook fromJsonHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (!(param.args[0] instanceof String)) {
return;
}
String json = (String) param.args[0];
if (!json.contains("riskToken") && !json.contains("deviceToken")) {
return;
}
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(json);
if (!sanitized.equals(json)) {
param.args[0] = sanitized;
XposedBridge.log(TAG + " Gson.fromJson riskToken sanitized");
}
}
};
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"fromJson",
String.class,
Class.class,
fromJsonHook);
} catch (Throwable ignored) {
}
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"fromJson",
String.class,
"java.lang.reflect.Type",
fromJsonHook);
} catch (Throwable ignored) {
}
}
private static boolean isRequestBody(ClassLoader loader, Object obj) {
try {
Class<?> rb = XposedHelpers.findClass("okhttp3.RequestBody", loader);
return rb.isInstance(obj);
} catch (Throwable t) {
return obj.getClass().getName().contains("RequestBody");
}
}
private static Object sanitizeRequestBody(ClassLoader loader, Object body) {
try {
ClassLoader effective = loaderFor(body, loader);
String content = readRequestBodyText(effective, body);
if (content.isEmpty()) {
return body;
}
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
if (sanitized.equals(content)) {
return body;
}
Object mediaType = XposedHelpers.callMethod(body, "contentType");
Class<?> rbClass = findClassSafe(effective, "okhttp3.RequestBody");
return XposedHelpers.callStaticMethod(rbClass, "create", mediaType, sanitized);
} catch (Throwable t) {
return body;
}
}
private static String readRequestBodyText(ClassLoader loader, Object body) {
try {
ClassLoader effective = loaderFor(body, loader);
Class<?> bufferClass = findClassSafe(effective, "okio.Buffer");
Object buffer = XposedHelpers.newInstance(bufferClass);
XposedHelpers.callMethod(body, "writeTo", buffer);
return (String) XposedHelpers.callMethod(buffer, "readUtf8");
} catch (Throwable t) {
return "";
}
}
private static ClassLoader loaderFor(Object obj, ClassLoader fallback) {
if (obj != null) {
ClassLoader cl = obj.getClass().getClassLoader();
if (cl != null) {
return cl;
}
}
return fallback;
}
private static Class<?> findClassSafe(ClassLoader loader, String name) {
try {
return XposedHelpers.findClass(name, loader);
} catch (Throwable first) {
ClassLoader ctx = Thread.currentThread().getContextClassLoader();
if (ctx != null && ctx != loader) {
return XposedHelpers.findClass(name, ctx);
}
throw first;
}
}
/**
* 拦截 RequestBody 写入:注册 JSON 只写一次create/Buffer Hook 可能漏掉。
* 读出 body → 净化 riskToken → 写入 sink跳过原方法。
*/
private static void hookRequestBodyWriteTo(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> rbClass = findClassSafe(lpparam.classLoader, "okhttp3.RequestBody");
Class<?> sinkClass = findClassSafe(lpparam.classLoader, "okio.BufferedSink");
XposedHelpers.findAndHookMethod(
rbClass,
"writeTo",
sinkClass,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
ClassLoader cl = loaderFor(param.thisObject, lpparam.classLoader);
Class<?> bufferClass = findClassSafe(cl, "okio.Buffer");
Object buffer = XposedHelpers.newInstance(bufferClass);
XposedBridge.invokeOriginalMethod(
param.method, param.thisObject, new Object[]{buffer});
String content = (String) XposedHelpers.callMethod(buffer, "readUtf8");
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
String url = CURRENT_REQUEST_URL.get();
if (url != null && url.contains("/register") && !content.isEmpty()) {
int show = Math.min(content.length(), 400);
XposedBridge.log(TAG + " register body(raw): "
+ content.substring(0, show)
+ (content.length() > show ? "..." : ""));
}
if (!sanitized.equals(content)) {
XposedBridge.log(TAG + " RequestBody.writeTo riskToken sanitized");
}
XposedHelpers.callMethod(param.args[0], "writeUtf8", sanitized);
param.setResult(null);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip RequestBody.writeTo: " + t.getMessage());
}
}
private static void hookRetrofitGsonConverter(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"retrofit2.converter.gson.GsonRequestBodyConverter",
lpparam.classLoader,
"convert",
Object.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object body = param.getResult();
if (body == null) {
return;
}
try {
ClassLoader cl = loaderFor(body, lpparam.classLoader);
Object buffer = XposedHelpers.newInstance(
findClassSafe(cl, "okio.Buffer"));
XposedHelpers.callMethod(body, "writeTo", buffer);
String content = (String) XposedHelpers.callMethod(buffer, "readUtf8");
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
if (!sanitized.equals(content)) {
Object mediaType = XposedHelpers.callMethod(body, "contentType");
param.setResult(XposedHelpers.callStaticMethod(
XposedHelpers.findClass("okhttp3.RequestBody", lpparam.classLoader),
"create",
mediaType,
sanitized));
XposedBridge.log(TAG + " GsonRequestBodyConverter riskToken sanitized");
}
} catch (Throwable ignored) {
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip GsonRequestBodyConverter: " + t.getMessage());
}
}
/** 出站 body 写入 okio.Buffer 时净化 riskToken不 Hook proceed。 */
private static void hookOkioBufferWrite(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook sanitizeHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args.length == 0 || !(param.args[0] instanceof String)) {
return;
}
String s = (String) param.args[0];
if (!s.contains("|")) {
return;
}
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(s);
if (!sanitized.equals(s)) {
param.args[0] = sanitized;
XposedBridge.log(TAG + " okio.Buffer write riskToken sanitized");
}
}
};
try {
Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
XposedHelpers.findAndHookMethod(
bufferClass, "writeUtf8", String.class, sanitizeHook);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip okio.Buffer.writeUtf8: " + t.getMessage());
}
try {
Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
XposedHelpers.findAndHookMethod(
bufferClass, "writeString",
String.class, java.nio.charset.Charset.class, sanitizeHook);
} catch (Throwable ignored) {
}
try {
Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
XposedHelpers.findAndHookMethod(
bufferClass, "write",
byte[].class, int.class, int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
byte[] data = (byte[]) param.args[0];
int off = (int) param.args[1];
int len = (int) param.args[2];
byte[] sanitized = MariBankRiskTokenUtil.sanitizeBytes(data, off, len);
if (sanitized != data) {
param.args[0] = sanitized;
param.args[1] = 0;
param.args[2] = sanitized.length;
XposedBridge.log(TAG + " okio.Buffer write bytes riskToken sanitized");
}
}
});
} catch (Throwable ignored) {
}
}
/** Retrofit 常用 byte[] RequestBody。 */
private static void hookOutgoingRequestBytes(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook byteHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
for (int i = 0; i < param.args.length; i++) {
if (!(param.args[i] instanceof byte[])) {
continue;
}
byte[] data = (byte[]) param.args[i];
byte[] sanitized = MariBankRiskTokenUtil.sanitizeBytes(data, 0, data.length);
if (sanitized != data) {
param.args[i] = sanitized;
XposedBridge.log(TAG + " outbound RequestBody bytes riskToken sanitized");
}
}
}
};
try {
XposedHelpers.findAndHookMethod(
"okhttp3.RequestBody",
lpparam.classLoader,
"create",
"okhttp3.MediaType",
byte[].class,
byteHook);
} catch (Throwable ignored) {
}
try {
XposedHelpers.findAndHookMethod(
"okhttp3.RequestBody",
lpparam.classLoader,
"create",
byte[].class,
"okhttp3.MediaType",
byteHook);
} catch (Throwable ignored) {
}
}
private static void hookOkHttpResponseUrl(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"okhttp3.Response",
lpparam.classLoader,
"body",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
try {
Object request = XposedHelpers.callMethod(param.thisObject, "request");
Object url = XposedHelpers.callMethod(request, "url");
if (url != null) {
CURRENT_REQUEST_URL.set(String.valueOf(url));
}
} catch (Throwable ignored) {
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip okhttp Response.body url: " + t.getMessage());
}
}
/** 出站 JSON 请求体:注册接口 /uapi/v2/register 会携带 riskToken。 */
private static void hookOutgoingRequestBody(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook sanitizeHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
for (int i = 0; i < param.args.length; i++) {
if (param.args[i] instanceof String) {
String body = (String) param.args[i];
if (body.contains("riskToken") && body.contains("|")) {
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
if (!sanitized.equals(body)) {
param.args[i] = sanitized;
XposedBridge.log(TAG + " outbound RequestBody riskToken sanitized");
}
} else if (body.contains("deviceToken") && body.contains("|")) {
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
if (!sanitized.equals(body)) {
param.args[i] = sanitized;
XposedBridge.log(TAG + " outbound RequestBody deviceToken sanitized");
}
}
}
}
}
};
String[][] createSigs = {
{"okhttp3.MediaType", "java.lang.String"},
{"java.lang.String", "okhttp3.MediaType"},
};
for (String[] sig : createSigs) {
try {
XposedHelpers.findAndHookMethod(
"okhttp3.RequestBody",
lpparam.classLoader,
"create",
sig[0],
sig[1],
sanitizeHook);
} catch (Throwable ignored) {
}
}
}
private static void hookGsonRiskToken(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook gsonHook = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!(param.getResult() instanceof String)) {
return;
}
String json = (String) param.getResult();
if (json.contains("mobileNo") || json.contains("phoneNo")) {
int show = Math.min(json.length(), 500);
XposedBridge.log(TAG + " Gson.toJson mobile: "
+ json.substring(0, show)
+ (json.length() > show ? "..." : ""));
}
if (!json.contains("riskToken") && !json.contains("deviceToken")) {
return;
}
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(json);
if (!sanitized.equals(json)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " Gson.toJson riskToken sanitized");
}
}
};
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"toJson",
Object.class,
gsonHook);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip Gson.toJson: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"toJson",
Object.class,
"java.lang.reflect.Type",
gsonHook);
} catch (Throwable ignored) {
}
}
private static void hookJsonRiskTokenPut(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"org.json.JSONObject",
lpparam.classLoader,
"put",
String.class,
Object.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (!"riskToken".equals(param.args[0]) && !"deviceToken".equals(param.args[0])) {
return;
}
if (!(param.args[1] instanceof String)) {
return;
}
param.args[1] = MariBankRiskTokenUtil.sanitizeRiskToken((String) param.args[1]);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip JSONObject.put riskToken: " + t.getMessage());
}
}
private static void hookSafeModeDialog(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"com.shopee.bke.lib.safemode.activity.SafeModeRecoverActivity",
lpparam.classLoader,
"onCreate",
"android.os.Bundle",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedHelpers.callMethod(param.thisObject, "finish");
param.setResult(null);
}
}
);
} catch (Throwable ignored) {
}
}
/** 拦截 Root/Hook/模拟器 警告弹窗与 Toast文案见 bke_toast_not_support_*)。 */
private static void hookRootDialogBlock(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook blankRootTextHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args.length > 0 && isRootBlockText(String.valueOf(param.args[0]))) {
param.args[0] = " ";
XposedBridge.log(TAG + " blanked root dialog message");
}
}
};
String[] messageSetters = {
"android.app.AlertDialog$Builder",
"androidx.appcompat.app.AlertDialog$Builder",
"com.shopee.bke.lib.commonui.widget.CommonDialog$Builder",
};
for (String className : messageSetters) {
try {
XposedHelpers.findAndHookMethod(
className, lpparam.classLoader, "setMessage", CharSequence.class, blankRootTextHook);
} catch (Throwable ignored) {
}
try {
XposedHelpers.findAndHookMethod(
className, lpparam.classLoader, "setTitle", CharSequence.class, blankRootTextHook);
} catch (Throwable ignored) {
}
}
try {
XposedHelpers.findAndHookMethod(
Resources.class,
"getString",
int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String s = (String) param.getResult();
if (isRootBlockText(s)) {
param.setResult(" ");
XposedBridge.log(TAG + " blanked root string resource");
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " getString hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Dialog.class,
"show",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Dialog dialog = (Dialog) param.thisObject;
if (isRootBlockText(extractDialogText(dialog))) {
XposedBridge.log(TAG + " blocked root Dialog.show: "
+ dialog.getClass().getSimpleName());
param.setResult(null);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Toast.class,
"show",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String text = extractToastText((Toast) param.thisObject);
if (isSecurityBlockText(text)) {
XposedBridge.log(TAG + " security block Toast: " + text);
logBriefStack();
}
if (isRootBlockText(text)) {
XposedBridge.log(TAG + " blocked root Toast.show");
param.setResult(null);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " Toast.show hook failed: " + t.getMessage());
}
}
private static boolean isRootBlockText(String text) {
if (text == null || text.isEmpty()) {
return false;
}
String lower = text.toLowerCase();
return lower.contains("rooted or jailbroken")
|| lower.contains("modified device")
|| lower.contains("magisk/xposed/frida")
|| lower.contains("cannot be accessed on such devices")
|| lower.contains("restore to factory settings");
}
private static boolean isSecurityBlockText(String text) {
if (text == null || text.isEmpty()) {
return false;
}
String lower = text.toLowerCase();
return lower.contains("temporarily blocked")
|| lower.contains("8424 8050");
}
private static void logBriefStack() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder();
int n = 0;
for (StackTraceElement frame : stack) {
String cn = frame.getClassName();
if (cn.contains("miraclegarden") || cn.contains("lsposed") || cn.contains("XposedBridge")) {
continue;
}
if (cn.startsWith("android.widget.") || cn.startsWith("android.view.")) {
continue;
}
sb.append("\n at ").append(cn).append(".").append(frame.getMethodName());
if (++n >= 10) {
break;
}
}
XposedBridge.log(TAG + " stack:" + sb);
}
private static String extractDialogText(Dialog dialog) {
StringBuilder sb = new StringBuilder();
try {
Object alert = XposedHelpers.getObjectField(dialog, "mAlert");
if (alert != null) {
appendFieldText(sb, alert, "mMessage");
appendFieldText(sb, alert, "mTitle");
}
} catch (Throwable ignored) {
}
try {
if (dialog.getWindow() != null) {
collectTextViews(dialog.getWindow().getDecorView(), sb);
}
} catch (Throwable ignored) {
}
return sb.toString();
}
private static void appendFieldText(StringBuilder sb, Object target, String field) {
try {
Object value = XposedHelpers.getObjectField(target, field);
if (value != null) {
sb.append(value);
}
} catch (Throwable ignored) {
}
}
private static void collectTextViews(View view, StringBuilder sb) {
if (view instanceof TextView) {
CharSequence text = ((TextView) view).getText();
if (text != null) {
sb.append(text);
}
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
collectTextViews(group.getChildAt(i), sb);
}
}
}
private static String extractToastText(Toast toast) {
try {
View view = toast.getView();
if (view instanceof TextView) {
CharSequence text = ((TextView) view).getText();
return text != null ? text.toString() : "";
}
if (view instanceof ViewGroup) {
StringBuilder sb = new StringBuilder();
collectTextViews(view, sb);
return sb.toString();
}
} catch (Throwable ignored) {
}
try {
Object text = XposedHelpers.getObjectField(toast, "mText");
return text != null ? text.toString() : "";
} catch (Throwable ignored) {
}
return "";
}
}

View File

@@ -0,0 +1,661 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.os.Build;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* libshpssdk.so / libshpssdk_bank.so native 检测绕过Java 层拦截 native 读路径)。
* 逆向字符串:/proc/self/maps、hook 库名、RealInterceptorChain.proceed 等。
*/
public final class MariBankShpsNativeHook {
private static final String TAG = "notiMessageHook/MariBankNative";
private static final Set<String> PROC_SENSITIVE = new HashSet<>(Arrays.asList(
"/proc/self/maps",
"/proc/version",
"/proc/self/status",
"/proc/mounts",
"/proc/cpuinfo",
"/proc/self/attr/current",
"/proc/self/mountinfo",
"/proc/net/unix",
"/proc/bootconfig",
"/proc/self/cgroup"
));
private static final String[] MAPS_HIDE_MARKERS = {
"xposed", "lsposed", "edxposed", "magisk", "frida", "substrate",
"libpine", "pine.so", "zygisk", "riru", "shamiko", "notimessage",
"miraclegarden", "libbytehook", "libapmhook", "libspxhook",
"liblubanhook", "libsulfuras", "libbugsnag-root-detection",
"libreact_debug", "libmobileffmpeg_abidetect",
"playintegrityfix", "libgadget", "libfrida", "libriru",
"liblspd", "libzygisk", "libvector", "zygisk_vector",
};
private static final WeakHashMap<Object, String> TRACKED_INPUTS = new WeakHashMap<>();
private static final String[] BOOT_SPOOF_KEYS = {
"ro.boot.verifiedbootstate",
"ro.boot.flash.locked",
"ro.boot.vbmeta.device_state",
"ro.boot.veritymode",
"ro.boot.warranty_bit",
"ro.boot.avb_version",
"vendor.boot.vbmeta.device_state",
"ro.crypto.state",
};
private static final String FAKE_SELINUX_CTX =
"u:r:untrusted_app:s0:c512,c768";
private MariBankShpsNativeHook() {
}
private static volatile boolean deferredInstalled = false;
/** loadPackage 阶段只装 /proc 过滤,避免过早触发 SHPSSDK / libsdkutils 死循环白屏。 */
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
hookProcAccess(lpparam);
hookProcViaRandomAccessFile(lpparam);
hookBufferedReader(lpparam);
hookSystemProperties(lpparam);
XposedBridge.log(TAG + " early hooks OK (proc only)");
}
/** attachBaseContext 之后安装 SHPSSDK 相关 HookClassLoader 已就绪)。 */
public static void installDeferred(XC_LoadPackage.LoadPackageParam lpparam) {
if (deferredInstalled) {
return;
}
deferredInstalled = true;
hookLoadLibrary(lpparam);
hookShpssInstall(lpparam);
hookRequestDefense(lpparam);
hookShpsNativeBridge(lpparam);
hookShpsNativeCore(lpparam);
hookBuildFields(lpparam);
XposedBridge.log(TAG + " deferred hooks installed for " + lpparam.packageName);
}
/** native 直接读 /proc/self/maps 查 hook 库;过滤内容。 */
private static void hookProcAccess(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookConstructor(
FileInputStream.class,
String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String path = normalizeProcPath((String) param.args[0]);
if (path != null) {
TRACKED_INPUTS.put(param.getResult(), path);
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " FileInputStream hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookConstructor(
FileInputStream.class,
File.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
File file = (File) param.args[0];
if (file != null) {
String path = normalizeProcPath(file.getAbsolutePath());
if (path != null) {
TRACKED_INPUTS.put(param.getResult(), path);
}
}
}
}
);
} catch (Throwable ignored) {
}
XC_MethodHook readFilter = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String path = TRACKED_INPUTS.get(param.thisObject);
if (path == null || param.getResult() == null) {
return;
}
if (param.getResult() instanceof Integer) {
int read = (Integer) param.getResult();
if (read <= 0 || param.args.length == 0 || !(param.args[0] instanceof byte[])) {
return;
}
byte[] buf = (byte[]) param.args[0];
int off = param.args.length > 1 ? (Integer) param.args[1] : 0;
filterProcBytes(path, buf, off, read);
} else if (param.getResult() instanceof byte[]) {
byte[] data = (byte[]) param.getResult();
param.setResult(filterProcBytesAll(path, data));
} else if (param.getResult() instanceof String) {
param.setResult(filterProcText(path, (String) param.getResult()));
}
}
};
try {
XposedHelpers.findAndHookMethod(
FileInputStream.class, "read", byte[].class, readFilter);
XposedHelpers.findAndHookMethod(
FileInputStream.class, "read", byte[].class, int.class, int.class, readFilter);
} catch (Throwable t) {
XposedBridge.log(TAG + " FileInputStream.read hook failed: " + t.getMessage());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
XposedHelpers.findAndHookMethod(
"java.nio.file.Files",
lpparam.classLoader,
"readAllBytes",
"java.nio.file.Path",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!(param.getResult() instanceof byte[])) {
return;
}
String path = String.valueOf(param.args[0]);
String norm = normalizeProcPath(path);
if (norm != null) {
param.setResult(filterProcBytesAll(
norm, (byte[]) param.getResult()));
}
}
}
);
} catch (Throwable ignored) {
}
}
}
private static void hookBufferedReader(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
BufferedReader.class,
"readLine",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!(param.getResult() instanceof String)) {
return;
}
String line = (String) param.getResult();
if (shouldHideMapsLine(line)) {
param.setResult(readNextSafeLine((BufferedReader) param.thisObject));
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " BufferedReader hook failed: " + t.getMessage());
}
}
private static String readNextSafeLine(BufferedReader reader) {
try {
String line;
while ((line = reader.readLine()) != null) {
if (!shouldHideMapsLine(line)) {
return line;
}
}
} catch (Throwable ignored) {
}
return "";
}
private static void hookLoadLibrary(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook logHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String lib = String.valueOf(param.args[param.args.length - 1]);
if (lib.contains("shpssdk")) {
XposedBridge.log(TAG + " loading native lib: " + lib);
}
}
};
try {
XposedHelpers.findAndHookMethod(
Runtime.class, "loadLibrary0", ClassLoader.class, String.class, logHook);
} catch (Throwable ignored) {
}
try {
XposedHelpers.findAndHookMethod(
System.class, "loadLibrary", String.class, logHook);
} catch (Throwable ignored) {
}
}
private static void hookShpssInstall(XC_LoadPackage.LoadPackageParam lpparam) {
// 勿 Hook ShpssInstall / vuvuwwwuw会干扰 SoUtils.loadSoLibrary导致 libsdkutils.so 死循环白屏。
}
private static void hookRequestDefense(XC_LoadPackage.LoadPackageParam lpparam) {
for (String className : new String[]{
"com.shopee.shpssdkbank.SHPSSDK",
"com.shopee.shpssdk.SHPSSDK",
}) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"requestDefense",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked requestDefense");
param.setResult(null);
}
}
);
} catch (Throwable ignored) {
}
}
}
/** shpssdkbank 混淆 native 桥接类int/boolean 返回值强制安全。 */
private static void hookShpsNativeBridge(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw",
"com.shopee.shpssdkbank.a",
"com.shopee.shpssdkbank.b",
"com.shopee.shpssdkbank.c",
"com.shopee.shpssdkbank.d",
"com.shopee.shpssdkbank.e",
"com.shopee.shpssdkbank.f",
"com.shopee.shpssdkbank.g",
"com.shopee.shpssdkbank.vuvuwwwuw",
"com.shopee.shpssdkbank.vwuuwwvwv",
"com.shopee.shpssdkbank.vwwuwuuuv",
"com.shopee.shpssdkbank.wvvvuuwuu",
"com.shopee.shpssdkbank.wvvvuuww",
"com.shopee.shpssdkbank.wvvvuvvv",
"com.shopee.shpssdkbank.wvvvuvww",
"com.shopee.shpssdkbank.wvvvuwwu",
};
int total = 0;
for (String className : classes) {
total += hookAllIntBooleanMethods(lpparam, className);
total += hookAllStringSanitize(lpparam, className);
}
XposedBridge.log(TAG + " native-bridge total hooks=" + total);
}
/** native 桥接可能直接返回 riskToken 字符串。 */
private static int hookAllStringSanitize(
XC_LoadPackage.LoadPackageParam lpparam, String className) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (method.getReturnType() != String.class) {
continue;
}
if (method.getParameterTypes().length > 6) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (!(result instanceof String)) {
return;
}
String s = (String) result;
if (!s.contains("|")) {
return;
}
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
if (!sanitized.equals(s)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " native String sanitized in "
+ className + "#" + method.getName());
}
}
});
count++;
}
} catch (Throwable ignored) {
}
return count;
}
private static int hookAllIntBooleanMethods(
XC_LoadPackage.LoadPackageParam lpparam, String className) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
Class<?> rt = method.getReturnType();
if (rt != boolean.class && rt != Boolean.class
&& rt != int.class && rt != Integer.class) {
continue;
}
if (method.getParameterTypes().length > 4) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (rt == boolean.class || rt == Boolean.class) {
param.setResult(false);
} else {
param.setResult(0);
}
}
});
count++;
}
if (count > 0) {
XposedBridge.log(TAG + " hooked " + count + " native-bridge checks in " + className);
}
} catch (Throwable ignored) {
}
return count;
}
/**
* SHPSSDK 核心 native 桥wvvvuwwu.wwvwvwuvv / vvuwuuvuu 等直接生成 risk 数据。
*/
private static void hookShpsNativeCore(XC_LoadPackage.LoadPackageParam lpparam) {
String[] coreClasses = {
"com.shopee.shpssdkbank.wvvvuwwu",
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
"com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
"com.shopee.shpssdk.wvvvuwwu",
};
int total = 0;
for (String className : coreClasses) {
total += hookNativeCoreClass(lpparam, className);
}
XposedBridge.log(TAG + " native-core total hooks=" + total);
}
private static int hookNativeCoreClass(
XC_LoadPackage.LoadPackageParam lpparam, String className) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (!Modifier.isStatic(method.getModifiers())) {
continue;
}
Class<?> rt = method.getReturnType();
if (rt == String.class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (!(result instanceof String)) {
return;
}
String s = (String) result;
if (s.length() > 80 && s.contains("|")) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
if (!sanitized.equals(s)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " core String sanitized "
+ className + "#" + method.getName());
}
}
}
});
count++;
} else if (rt == boolean.class || rt == Boolean.class
|| rt == int.class || rt == Integer.class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (rt == boolean.class || rt == Boolean.class) {
param.setResult(false);
} else {
param.setResult(0);
}
}
});
count++;
}
}
if (count > 0) {
XposedBridge.log(TAG + " hooked " + count + " core natives in " + className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip core " + className + ": " + t.getMessage());
}
return count;
}
/** Build.TAGS / FINGERPRINT 等 Java 层可读字段伪装。 */
private static void hookBuildFields(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.setStaticObjectField(Build.class, "TAGS", "release-keys");
if (String.valueOf(Build.FINGERPRINT).contains("test-keys")) {
XposedHelpers.setStaticObjectField(Build.class, "FINGERPRINT",
Build.FINGERPRINT.replace("test-keys", "release-keys"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
XposedHelpers.setStaticObjectField(Build.class, "BOOTLOADER", "unknown");
} catch (Throwable ignored) {
}
}
XposedBridge.log(TAG + " Build fields spoofed");
} catch (Throwable t) {
XposedBridge.log(TAG + " Build spoof failed: " + t.getMessage());
}
}
private static void hookProcViaRandomAccessFile(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookConstructor(
"java.io.RandomAccessFile",
lpparam.classLoader,
String.class,
String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String path = normalizeProcPath((String) param.args[0]);
if (path != null) {
TRACKED_INPUTS.put(param.getResult(), path);
}
}
}
);
} catch (Throwable ignored) {
}
try {
XposedHelpers.findAndHookMethod(
"java.io.RandomAccessFile",
lpparam.classLoader,
"read",
byte[].class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String path = TRACKED_INPUTS.get(param.thisObject);
if (path == null || !(param.getResult() instanceof Integer)) {
return;
}
int read = (Integer) param.getResult();
if (read > 0 && param.args[0] instanceof byte[]) {
filterProcBytes(path, (byte[]) param.args[0], 0, read);
}
}
}
);
} catch (Throwable ignored) {
}
}
private static void hookSystemProperties(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> sp = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader);
for (Method method : sp.getDeclaredMethods()) {
if (!"get".equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.args.length == 0 || !(param.args[0] instanceof String)) {
return;
}
String key = (String) param.args[0];
String spoofed = spoofProperty(key, param.getResult());
if (spoofed != null) {
param.setResult(spoofed);
}
}
});
}
} catch (Throwable t) {
XposedBridge.log(TAG + " SystemProperties hook failed: " + t.getMessage());
}
}
private static String spoofProperty(String key, Object current) {
if ("ro.debuggable".equals(key)) {
return "0";
}
if ("ro.secure".equals(key)) {
return "1";
}
if ("ro.build.tags".equals(key)) {
if (current instanceof String && String.valueOf(current).contains("test-keys")) {
return "release-keys";
}
}
if ("ro.boot.verifiedbootstate".equals(key)) {
return "green";
}
if ("ro.boot.flash.locked".equals(key)) {
return "1";
}
if ("ro.boot.vbmeta.device_state".equals(key)
|| "vendor.boot.vbmeta.device_state".equals(key)) {
return "locked";
}
if ("ro.boot.veritymode".equals(key)) {
return "enforcing";
}
if ("ro.boot.warranty_bit".equals(key)) {
return "0";
}
if ("ro.crypto.state".equals(key)) {
return "encrypted";
}
for (String bootKey : BOOT_SPOOF_KEYS) {
if (bootKey.equals(key) && key.startsWith("ro.boot")) {
// already handled above for known keys
break;
}
}
return null;
}
private static String normalizeProcPath(String path) {
if (path == null) {
return null;
}
String norm = path.trim();
for (String p : PROC_SENSITIVE) {
if (norm.equals(p) || norm.endsWith(p)) {
return p;
}
}
return null;
}
private static byte[] filterProcBytesAll(String path, byte[] data) {
return filterProcText(path, new String(data)).getBytes();
}
private static void filterProcBytes(String path, byte[] buf, int off, int len) {
String text = new String(buf, off, len);
String filtered = filterProcText(path, text);
if (filtered.equals(text)) {
return;
}
byte[] out = filtered.getBytes();
int copy = Math.min(len, out.length);
System.arraycopy(out, 0, buf, off, copy);
if (copy < len) {
Arrays.fill(buf, off + copy, off + len, (byte) 0);
}
}
private static String filterProcText(String path, String text) {
if ("/proc/self/maps".equals(path) || "/proc/self/mountinfo".equals(path)
|| "/proc/mounts".equals(path)) {
StringBuilder sb = new StringBuilder();
for (String line : text.split("\n")) {
if (!shouldHideMapsLine(line)) {
if (sb.length() > 0) {
sb.append('\n');
}
sb.append(line);
}
}
return sb.toString();
}
if ("/proc/self/attr/current".equals(path)) {
String lower = text.toLowerCase(Locale.US);
if (lower.contains("magisk") || lower.contains("su") || lower.contains("zygisk")
|| lower.contains("xposed")) {
return FAKE_SELINUX_CTX;
}
return text;
}
if ("/proc/version".equals(path)) {
return text.replace("dirty", "").replace("test-keys", "release-keys");
}
if ("/proc/self/status".equals(path)) {
return text.replaceAll("(?m)^TracerPid:\\s*[1-9]\\d*",
"TracerPid:\t0");
}
return text;
}
private static boolean shouldHideMapsLine(String line) {
if (line == null || line.isEmpty()) {
return false;
}
String lower = line.toLowerCase(Locale.US);
for (String marker : MAPS_HIDE_MARKERS) {
if (lower.contains(marker)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,181 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* Root / Hook / 模拟器检测通用绕过辅助。
*/
public final class RootBypassHelper {
private static final String TAG = "notiMessageHook/RootBypass";
private static final Pattern UNSAFE_NAME = Pattern.compile(
".*(root|jail|hook|frida|xposed|lsposed|emulator|simulator|debug|tamper|"
+ "integrity|unsafe|risk|magisk|su|cheat|mock).*",
Pattern.CASE_INSENSITIVE
);
private static final Pattern SAFE_NAME = Pattern.compile(
".*(safe|secure|valid|passed|pass|clean|trusted|normal|ok).*",
Pattern.CASE_INSENSITIVE
);
private static final Set<String> ROOT_PATH_MARKERS = new HashSet<>(Arrays.asList(
"/su",
"magisk",
"supersu",
"busybox",
"/xbin/su",
"/sbin/su",
"de.robv.android.xposed",
"org.lsposed",
"com.topjohnwu.magisk"
));
private RootBypassHelper() {
}
public static void hookSecurityClass(XC_LoadPackage.LoadPackageParam lpparam, String className) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
hookMethodIfSecurityCheck(className, method);
}
XposedBridge.log(TAG + " hooked methods in " + className);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip class " + className + ": " + t.getMessage());
}
}
private static void hookMethodIfSecurityCheck(String className, Method method) {
Class<?> returnType = method.getReturnType();
if (returnType != boolean.class
&& returnType != Boolean.class
&& returnType != int.class
&& returnType != Integer.class) {
return;
}
String name = method.getName();
if (!looksLikeSecurityMethod(name) && !className.toLowerCase(Locale.US).contains("safemode")
&& !className.toLowerCase(Locale.US).contains("risk")) {
return;
}
try {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (returnType == boolean.class || returnType == Boolean.class) {
param.setResult(shouldReturnTrue(name));
} else {
param.setResult(0);
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " hook failed " + className + "#" + name + ": " + t.getMessage());
}
}
private static boolean looksLikeSecurityMethod(String name) {
return UNSAFE_NAME.matcher(name).matches() || SAFE_NAME.matcher(name).matches();
}
private static boolean shouldReturnTrue(String methodName) {
if (UNSAFE_NAME.matcher(methodName).matches()) {
return false;
}
if (SAFE_NAME.matcher(methodName).matches()) {
return true;
}
return false;
}
public static void hookFileExists(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
File.class,
"exists",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
File file = (File) param.thisObject;
if (file == null) {
return;
}
String path = file.getAbsolutePath().toLowerCase(Locale.US);
for (String marker : ROOT_PATH_MARKERS) {
if (path.contains(marker)) {
param.setResult(false);
return;
}
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " File.exists hook failed: " + t.getMessage());
}
}
public static void hookRuntimeExec(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
Runtime.class,
"exec",
String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String cmd = (String) param.args[0];
if (cmd == null) {
return;
}
String lower = cmd.toLowerCase(Locale.US);
if (lower.contains("su") || lower.contains("magisk") || lower.contains("which su")) {
throw new SecurityException("blocked root probe");
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " Runtime.exec hook failed: " + t.getMessage());
}
}
public static void hookSystemGetProperty(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
System.class,
"getProperty",
String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String key = (String) param.args[0];
if (key == null) {
return;
}
if ("ro.debuggable".equals(key) || "ro.secure".equals(key)) {
param.setResult("ro.secure".equals(key) ? "1" : "0");
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " System.getProperty hook failed: " + t.getMessage());
}
}
}

View File

@@ -0,0 +1,123 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.content.ContentValues;
import android.content.Context;
import android.text.TextUtils;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import com.miraclegarden.smsmessage.xposed.HookForwarder;
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;
public final class SqliteMessageHook {
private static final String TAG = "notiMessageHook/Sqlite";
private static final String[] SQLITE_CLASSES = {
"android.database.sqlite.SQLiteDatabase",
"androidx.sqlite.db.framework.FrameworkSQLiteDatabase"
};
private SqliteMessageHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
for (String sqliteClass : SQLITE_CLASSES) {
try {
hookInsert(lpparam, sqliteClass, "insert");
hookInsert(lpparam, sqliteClass, "insertWithOnConflict");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + sqliteClass + ": " + t.getMessage());
}
}
}
private static void hookInsert(XC_LoadPackage.LoadPackageParam lpparam,
String className, String methodName) {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
methodName,
String.class,
String.class,
ContentValues.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
handleInsert(lpparam.packageName, param);
}
}
);
}
private static void handleInsert(String packageName, XC_MethodHook.MethodHookParam param) {
if (param.args == null || param.args.length < 3) {
return;
}
Object tableArg = param.args[0];
Object valuesArg = param.args[2];
if (!(tableArg instanceof String) || !(valuesArg instanceof ContentValues)) {
return;
}
String table = ((String) tableArg).toLowerCase();
if (!isMessageTable(table)) {
return;
}
ContentValues values = (ContentValues) valuesArg;
String title = firstNonEmpty(values, "talker", "sender", "from", "title", "name");
String content = firstNonEmpty(values, "content", "text", "body", "msg", "message");
if (TextUtils.isEmpty(content)) {
return;
}
Context context = getContext(param.thisObject);
if (context == null) {
return;
}
HookForwarder.forward(context, packageName, title, content,
HookBridge.SOURCE_XPOSED_SQLITE);
}
private static boolean isMessageTable(String table) {
return "message".equals(table)
|| "messages".equals(table)
|| table.endsWith("_message")
|| table.contains("message");
}
private static String firstNonEmpty(ContentValues values, String... keys) {
for (String key : keys) {
String value = values.getAsString(key);
if (!TextUtils.isEmpty(value)) {
return value;
}
}
return "";
}
private static Context getContext(Object database) {
try {
Object ctx = XposedHelpers.callMethod(database, "getContext");
if (ctx instanceof Context) {
return (Context) ctx;
}
} catch (Throwable ignored) {
}
try {
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
if (app instanceof Context) {
return (Context) app;
}
} catch (Throwable ignored) {
}
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.miraclegarden.smsmessage.xposed.hook;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* Suncorp Bank — Kotlin 原生 App。
* 逆向结论au.com.suncorp.marketplace.base.application.SuncorpMessagingService#onMessageReceived
*/
public final class SuncorpBankMessageHook {
private static final String MESSAGING_SERVICE =
"au.com.suncorp.marketplace.base.application.SuncorpMessagingService";
private SuncorpBankMessageHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
BankHookHelper.hookFcmService(lpparam, MESSAGING_SERVICE, HookBridge.SOURCE_XPOSED_SUNCORP);
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_SUNCORP_NOTIFY);
XposedBridge.log("notiMessageHook/Suncorp installed for " + lpparam.packageName);
}
}

View File

@@ -0,0 +1,533 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.content.Context;
import android.text.TextUtils;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import com.miraclegarden.smsmessage.xposed.HookForwarder;
import java.util.ArrayDeque;
import java.util.HashSet;
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;
/**
* Telegram 专用 Hook监听 NotificationCenter.didReceiveNewMessages
* 从 MessageObject 提取已解密的 messageText。
*/
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;
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
private static final HashSet<String> RECENT_SET = new HashSet<>();
private TelegramMessageHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> notificationCenter = XposedHelpers.findClass(
NOTIFICATION_CENTER, lpparam.classLoader);
final int didReceiveNewMessages = XposedHelpers.getStaticIntField(
notificationCenter, "didReceiveNewMessages");
XposedHelpers.findAndHookMethod(
notificationCenter,
"postNotificationName",
int.class,
Object[].class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
int id = (int) param.args[0];
if (id != didReceiveNewMessages) {
return;
}
Object[] args = (Object[]) param.args[1];
if (args == null || args.length == 0) {
return;
}
Context context = getContext();
if (context == null) {
return;
}
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) {
XposedBridge.log(TAG + " install failed: " + t.getMessage());
installMessageObjectFallback(lpparam);
}
}
/** NotificationCenter Hook 失败时的兜底Hook MessageObject 构造完成后的消息。 */
private static void installMessageObjectFallback(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> messageObjectClass = XposedHelpers.findClass(
MESSAGE_OBJECT, lpparam.classLoader);
XposedBridge.hookAllConstructors(messageObjectClass, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Context context = getContext();
if (context == null) {
return;
}
forwardMessageObject(context, lpparam.packageName, param.thisObject);
}
});
XposedBridge.log(TAG + " fallback MessageObject hook installed for "
+ lpparam.packageName);
} catch (Throwable t) {
XposedBridge.log(TAG + " fallback install failed: " + t.getMessage());
}
}
/** 后台弹通知路径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) {
continue;
}
if (!MESSAGE_OBJECT.equals(item.getClass().getName())) {
continue;
}
forwardMessageObject(context, packageName, item);
}
}
private static void forwardMessageObject(Context context, String packageName, Object messageObject) {
try {
Object messageOwner = XposedHelpers.getObjectField(messageObject, "messageOwner");
if (messageOwner == null) {
return;
}
if (XposedHelpers.getBooleanField(messageOwner, "out")) {
return;
}
int messageId = XposedHelpers.getIntField(messageOwner, "id");
long dialogId = extractDialogId(messageObject, messageOwner);
if (!rememberMessage(dialogId, messageId)) {
return;
}
String content = extractContent(messageObject, messageOwner);
if (TextUtils.isEmpty(content)) {
return;
}
String title = extractTitle(messageObject, messageOwner, dialogId);
long timestamp = XposedHelpers.getIntField(messageOwner, "date") * 1000L;
if (timestamp <= 0) {
timestamp = System.currentTimeMillis();
}
String sender = extractSenderName(messageObject);
String body = content;
if (!TextUtils.isEmpty(sender) && !sender.equals(title)) {
body = sender + ": " + content;
}
HookForwarder.forward(context, packageName, title, body,
HookBridge.SOURCE_XPOSED_TELEGRAM);
} catch (Throwable t) {
XposedBridge.log(TAG + " forward failed: " + t.getMessage());
}
}
private static long extractDialogId(Object messageObject, Object messageOwner) {
try {
return ((Number) XposedHelpers.callMethod(messageObject, "getDialogId")).longValue();
} catch (Throwable ignored) {
}
try {
return XposedHelpers.getLongField(messageOwner, "dialog_id");
} catch (Throwable ignored) {
}
return 0L;
}
private static String extractContent(Object messageObject, Object messageOwner) {
String caption = firstNonEmpty(
safeText(XposedHelpers.getObjectField(messageObject, "caption")),
safeText(XposedHelpers.getObjectField(messageOwner, "message")),
safeText(callMethodSafe(messageObject, "getCaption"))
);
String messageText = firstNonEmpty(
safeText(XposedHelpers.getObjectField(messageObject, "messageText")),
safeText(callMethodSafe(messageObject, "getMessageText"))
);
String mediaLabel = detectMediaLabel(messageObject, messageOwner);
if (!TextUtils.isEmpty(caption)) {
if (!TextUtils.isEmpty(mediaLabel)) {
return mediaLabel + "\n" + caption;
}
if (!TextUtils.isEmpty(messageText) && !isMediaPlaceholder(messageText)
&& !messageText.equals(caption)) {
return messageText + "\n" + caption;
}
return caption;
}
if (!TextUtils.isEmpty(messageText) && !isMediaPlaceholder(messageText)) {
return messageText;
}
if (!TextUtils.isEmpty(mediaLabel)) {
return mediaLabel;
}
if (!TextUtils.isEmpty(messageText)) {
return messageText;
}
return "";
}
private static String detectMediaLabel(Object messageObject, Object messageOwner) {
try {
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isSticker"))) {
return "[贴纸]";
}
} catch (Throwable ignored) {
}
try {
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isPhoto"))) {
return "[图片]";
}
} catch (Throwable ignored) {
}
try {
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isVideo"))) {
return "[视频]";
}
} catch (Throwable ignored) {
}
try {
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isGif"))) {
return "[GIF]";
}
} catch (Throwable ignored) {
}
try {
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isVoice"))) {
return "[语音]";
}
} catch (Throwable ignored) {
}
try {
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isMusic"))) {
return "[音频]";
}
} catch (Throwable ignored) {
}
Object media = XposedHelpers.getObjectField(messageOwner, "media");
if (media == null) {
return "";
}
String mediaClass = media.getClass().getName();
if (mediaClass.contains("Photo")) {
return "[图片]";
}
if (mediaClass.contains("Document")) {
return "[文件]";
}
if (mediaClass.contains("Video")) {
return "[视频]";
}
if (mediaClass.contains("Geo")) {
return "[位置]";
}
if (mediaClass.contains("Contact")) {
return "[联系人]";
}
return "[媒体消息]";
}
private static boolean isMediaPlaceholder(String text) {
if (TextUtils.isEmpty(text)) {
return true;
}
String value = text.trim();
return value.equals("图片")
|| value.equals("Photo")
|| value.equals("照片")
|| value.equals("贴纸")
|| value.equals("Sticker")
|| value.equals("Video")
|| value.equals("视频")
|| value.equals("GIF")
|| value.equals("动画")
|| value.equals("文件")
|| value.equals("Document")
|| value.equals("语音")
|| value.equals("Voice")
|| value.equals("音频")
|| value.equals("Audio")
|| value.equals("位置")
|| value.equals("Location")
|| value.startsWith("[媒体")
|| value.startsWith("[图片")
|| value.startsWith("[贴纸")
|| value.startsWith("[视频");
}
private static String firstNonEmpty(String... values) {
for (String value : values) {
if (!TextUtils.isEmpty(value)) {
return value;
}
}
return "";
}
private static String callMethodSafe(Object target, String methodName) {
try {
return safeText(XposedHelpers.callMethod(target, methodName));
} catch (Throwable ignored) {
return "";
}
}
private static String extractTitle(Object messageObject, Object messageOwner, long dialogId) {
String viaController = resolveTitleViaController(messageObject, dialogId);
if (!TextUtils.isEmpty(viaController)) {
return viaController;
}
try {
Object dialogName = XposedHelpers.callMethod(messageObject, "getName");
String name = safeText(dialogName);
if (!TextUtils.isEmpty(name)) {
return name;
}
} catch (Throwable ignored) {
}
try {
Object chat = XposedHelpers.callMethod(messageObject, "getChat");
if (chat != null) {
String chatTitle = safeText(XposedHelpers.getObjectField(chat, "title"));
if (!TextUtils.isEmpty(chatTitle)) {
return chatTitle;
}
}
} catch (Throwable ignored) {
}
String localName = safeText(XposedHelpers.getObjectField(messageObject, "localName"));
if (!TextUtils.isEmpty(localName)) {
return localName;
}
return dialogId != 0 ? ("对话 " + dialogId) : "Telegram";
}
private static String resolveTitleViaController(Object messageObject, long dialogId) {
if (dialogId == 0) {
return null;
}
try {
int account = XposedHelpers.getIntField(messageObject, "currentAccount");
ClassLoader cl = messageObject.getClass().getClassLoader();
Class<?> mcClass = XposedHelpers.findClass(
"org.telegram.messenger.MessagesController", cl);
Object mc = XposedHelpers.callStaticMethod(mcClass, "getInstance", account);
Object title = invokeGetPeerTitle(mc, dialogId);
String text = safeText(title);
if (!TextUtils.isEmpty(text)) {
return text;
}
if (dialogId < 0) {
Object chat = XposedHelpers.callMethod(mc, "getChat", -dialogId);
if (chat == null) {
chat = XposedHelpers.callMethod(mc, "getChat", dialogId);
}
if (chat != null) {
text = safeText(XposedHelpers.getObjectField(chat, "title"));
if (!TextUtils.isEmpty(text)) {
return text;
}
}
} else {
Object user = XposedHelpers.callMethod(mc, "getUser", dialogId);
text = formatUserName(user);
if (!TextUtils.isEmpty(text)) {
return text;
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " resolveTitleViaController: " + t.getMessage());
}
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 "";
}
String first = safeText(XposedHelpers.getObjectField(user, "first_name"));
String last = safeText(XposedHelpers.getObjectField(user, "last_name"));
String username = safeText(XposedHelpers.getObjectField(user, "username"));
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(first)) {
sb.append(first);
}
if (!TextUtils.isEmpty(last)) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(last);
}
if (sb.length() > 0) {
return sb.toString();
}
if (!TextUtils.isEmpty(username)) {
return "@" + username;
}
return "";
}
private static String safeText(Object value) {
if (value == null) {
return "";
}
if (!(value instanceof CharSequence) && !(value instanceof Number) && !(value instanceof Boolean)) {
String className = value.getClass().getName();
if (className.startsWith("org.telegram.tgnet.")
|| className.contains("TLRPC$")
|| value.getClass().getName().contains("Peer")) {
return "";
}
}
String text = String.valueOf(value).trim();
if (text.contains("TLRPC$") || text.contains("org.telegram.tgnet.")) {
return "";
}
return text;
}
private static String extractSenderName(Object messageObject) {
try {
String fromName = safeText(XposedHelpers.callMethod(messageObject, "getFromName"));
if (!TextUtils.isEmpty(fromName)) {
return fromName;
}
} catch (Throwable ignored) {
}
return "";
}
private static synchronized boolean rememberMessage(long dialogId, int messageId) {
if (messageId <= 0) {
return true;
}
String key = dialogId + ":" + messageId;
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;
}
private static Context getContext() {
try {
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
if (app instanceof Context) {
return (Context) app;
}
} catch (Throwable ignored) {
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
package com.miraclegarden.smsmessage.xposed.hook;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* ubank — Capacitor + MoEngage 推送。
* 逆向结论:
* - com.moengage.firebase.MoEFireBaseMessagingService交易/营销推送主路径)
* - io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingServiceCapacitor FCM 插件)
*/
public final class UbankMessageHook {
private static final String MOE_SERVICE = "com.moengage.firebase.MoEFireBaseMessagingService";
private static final String CAPACITOR_SERVICE =
"io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService";
private UbankMessageHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
BankHookHelper.hookFcmService(lpparam, MOE_SERVICE, HookBridge.SOURCE_XPOSED_UBANK);
BankHookHelper.hookFcmService(lpparam, CAPACITOR_SERVICE, HookBridge.SOURCE_XPOSED_UBANK);
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_UBANK_NOTIFY);
XposedBridge.log("notiMessageHook/ubank installed for " + lpparam.packageName);
}
}

View File

@@ -0,0 +1,24 @@
package com.miraclegarden.smsmessage.xposed.hook;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* Up Bank — React Native + 原生 FCM HandlerService。
* 逆向结论au.com.up.money.notifications.HandlerService#onMessageReceived(RemoteMessage)
*/
public final class UpBankMessageHook {
private static final String HANDLER_SERVICE = "au.com.up.money.notifications.HandlerService";
private UpBankMessageHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
BankHookHelper.hookFcmService(lpparam, HANDLER_SERVICE, HookBridge.SOURCE_XPOSED_UP);
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_UP_NOTIFY);
XposedBridge.log("notiMessageHook/Up installed for " + lpparam.packageName);
}
}

View File

@@ -0,0 +1,98 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.content.ContentValues;
import android.content.Context;
import android.text.TextUtils;
import com.miraclegarden.smsmessage.xposed.HookBridge;
import com.miraclegarden.smsmessage.xposed.HookForwarder;
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;
public final class WeChatMessageHook {
private static final String TAG = "notiMessageHook/WeChat";
private static final String WCDB_CLASS = "com.tencent.wcdb.database.SQLiteDatabase";
private WeChatMessageHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
try {
hookWcdbInsert(lpparam, "insert");
hookWcdbInsert(lpparam, "insertWithOnConflict");
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
} catch (Throwable t) {
XposedBridge.log(TAG + " install failed: " + t.getMessage());
}
}
private static void hookWcdbInsert(XC_LoadPackage.LoadPackageParam lpparam, String methodName) {
XposedHelpers.findAndHookMethod(
WCDB_CLASS,
lpparam.classLoader,
methodName,
String.class,
String.class,
ContentValues.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
handleWeChatInsert(param);
}
}
);
}
private static void handleWeChatInsert(XC_MethodHook.MethodHookParam param) {
if (param.args == null || param.args.length < 3) {
return;
}
Object tableArg = param.args[0];
Object valuesArg = param.args[2];
if (!(tableArg instanceof String) || !(valuesArg instanceof ContentValues)) {
return;
}
String table = (String) tableArg;
if (!"message".equalsIgnoreCase(table)) {
return;
}
ContentValues values = (ContentValues) valuesArg;
String talker = values.getAsString("talker");
String content = values.getAsString("content");
Integer type = values.getAsInteger("type");
if (TextUtils.isEmpty(content)) {
return;
}
if (type != null && (type == 10000 || type == 10002)) {
return;
}
Context context = getContext();
if (context == null) {
return;
}
HookForwarder.forward(context, "com.tencent.mm", talker, content,
HookBridge.SOURCE_XPOSED_WECHAT);
}
private static Context getContext() {
try {
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
if (app instanceof Context) {
return (Context) app;
}
} catch (Throwable ignored) {
}
return null;
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="xposed_scope">
<item>org.telegram.messenger</item>
<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>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="module_name">notiMessage Hook</string>
<string name="xposed_description">Hook 目标 App 消息,前台也能同步到 notiMessage。需在 LSPosed 中勾选目标 App 并启用本模块。</string>
</resources>