Compare commits

...

10 Commits

Author SHA1 Message Date
mars
193c04a24b fix(tng): 修复地区选择黑屏并加固注册链 native abort 防护 2026-08-03 11:22:51 +08:00
mars
18ae42ec63 fix(tng): 适配 Promon 1.9.10 vhvlnqgy 包名并加固 Zygisk 信号跳过
1.9.10 将 xwwqazamx 重命名为 vhvlnqgy,Login 闪退为 vhvlnqgy.bd:16;Xposed 双包名解析并 short-circuit R/bl、拦截 bd 异常。Zygisk 扩展 non-promon SEGV pc+4 与 ABRT/TRAP 一律 pc+4,修复 Login 稳定;注册页 stack_chk abort 待续攻。
2026-08-03 11:02:46 +08:00
mars
818b2f4f51 fix(tng): 防止 SIGABRT 跳 LR 死循环,改 pc+4 与 streak freeze
吞 ABRT 时若跳回近距 LR 会瞬间再 abort 刷屏;改为优先 pc+4,同 tid+pc 超阈值后冻结工作线程,主线程仅 bail。
2026-08-03 10:26:48 +08:00
mars
13e407623b feat(tng): TNG eWallet Promon bypass、Zygisk 信号守卫与注册链保护
新增 TngRootBypassHook 与 tng_exit_guard 模块,修复 BAL 强拉、SIGABRT/pc==lr SEGV 崩溃,验证可进入注册页。
2026-07-31 17:55:13 +08:00
mars
5a232213f3 docs: 新增 Telegram 抓消息专文并更新交叉引用 2026-07-06 14:17:01 +08:00
mars
81119e0ff9 feat(maribank): PH 注册 OTP 突破、SG bypass 与中文文档
菲律宾 SeaBank 在 Root+LSPosed+Shamiko 下 register 已通过并触发 OTP;扩展 SG 包名、加密前 Hook、Magisk 设备伪装脚本,并将 docs 整理为中文操作与风控说明。
2026-07-06 14:01:49 +08:00
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
193 changed files with 17398 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,11 @@
# AGENTS.md — SmsMessage (notiMessage)
Android 应用,监听通知栏短信/通知内容并上传至服务器。适配华为、MIUI 等手机
Android 应用,监听通知栏消息并通过 **双通道**(通知监听 + Xposed Hook抓取内容支持 PC 本地调试台转发
> **Hook 架构与扩展指南**:详见 [docs/Hook指南.md](docs/Hook指南.md)(含 **Xposed / LSPosed**、接入新 App
> **Telegram 抓消息专文**[docs/Telegram抓消息说明.md](docs/Telegram抓消息说明.md)
> **手机部署与银行 bypass 操作**:详见 [docs/手机操作手册.md](docs/手机操作手册.md)
> **MariBank 风控与 register 载荷**[docs/MariBank风控与载荷说明.md](docs/MariBank风控与载荷说明.md)
## 项目结构
@@ -10,24 +15,45 @@ 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指南.md # Hook 架构、Xposed/LSPosed、扩展
├── docs/手机操作手册.md # Root 机部署与日常操作
├── docs/MariBank风控与载荷说明.md # register 字段与风控说明
├── 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 +77,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());
// 加载银行账户列表
// 未登录时走本地模式,不请求服务器银行账户
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,13 +137,42 @@ 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();
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<>();

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,10 +110,19 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
}
private void initView() {
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,9 +194,10 @@ 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 (AppConfig.ENABLE_SERVER_UPLOAD) {
binding.tvUploadedCount.setText("上传: " + uploadedCount);
if (totalCount > 0) {
int successRate = (uploadedCount * 100) / totalCount;
binding.tvSuccessRate.setText("成功率: " + successRate + "%");
@@ -182,6 +205,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
binding.tvSuccessRate.setText("成功率: --");
}
}
}
private final Runnable statsUpdateRunnable = new Runnable() {
@Override

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(() -> {
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 (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();
}
if (content == null) {
content = "";
}
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);
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())) {
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");
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,
String debugSource) {
DebugForwarder.forward(this, messageInfo, result.title, result.content,
result.timestamp, debugSource);
if (!AppConfig.ENABLE_SERVER_UPLOAD) {
recordLocalCapture();
return;
}
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result) {
// 检查是否已登录
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()

329
docs/Hook指南.md Normal file
View File

@@ -0,0 +1,329 @@
# 消息抓取架构与 Hook 扩展指南
本文说明 notiMessage 的双通道抓取机制、Telegram 的实现方式、**Xposed 与 LSPosed**,以及日后接入其他 App 的步骤。
> 手机部署步骤见 [`手机操作手册.md`](手机操作手册.md) · **Telegram 专文**见 [`Telegram抓消息说明.md`](Telegram抓消息说明.md) · MariBank 见 [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md)
---
## 0. Xposed 与 LSPosed
### 0.1 是什么关系
| | **Xposed经典** | **LSPosed** |
|--|-------------------|-------------|
| 性质 | Hook 框架概念 + 老实现(改 `/system` | 现代实现,**不动 system** |
| 依赖 | 老 Root / Recovery | **Magisk + Zygisk** |
| 作用域 | 全局或 Installer 里选 | **按 App 勾选** |
| 模块 API | `IXposedHookLoadPackage` 等 | **同一套 API** |
| 本项目 | 源码在 `xposed-module/` | **手机上实际加载框架** |
日常说法:**「Xposed 模块」= APK****「启用 Xposed」= 在 LSPosed 里打开模块并勾选作用域**。
### 0.2 与本项目的关系
```
Magisk → Zygisk → LSPosed → com.miraclegarden.smsmessage.xposed
├── MariBankRootBypassHook银行
├── TelegramMessageHookTelegram
└── …
```
测银行 App 时通常还需 **Shamiko**Hide Magisk与 LSPosed 分工见 [`MariBank风控与载荷说明.md` §6](MariBank风控与载荷说明.md)。
### 0.3 与其他工具对比
| 工具 | 特点 |
|------|------|
| **LSPosed** | 常驻、开机自动,适合银行 bypass |
| **Frida** | 临时 attach与 LSPosed 同时开易冲突 |
| **Magisk 模块** | 改系统属性serial不是 Hook Java |
---
## 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 实现方式
> **完整安装、双通道、排错、logcat**:见专文 [`Telegram抓消息说明.md`](Telegram抓消息说明.md)。本节保留架构摘要。
### 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,255 @@
# MariBank 菲律宾注册突破 & 新加坡现状2026-07-06
> **设备**Pixel 6`1C081FDF600K5Q`)· Magisk 30.7 + Zygisk + LSPosed
> **结论****菲律宾 SeaBank 已能发 OTP****新加坡 MariBank 仍 3100012**
> **关联**[`MariBank实现说明.md`](MariBank实现说明.md) · [`MariBank新加坡逆向.md`](MariBank新加坡逆向.md)
---
## 1. 结果一览
| 地区 | 包名 | API | 注册结果 | 错误码 |
|------|------|-----|----------|--------|
| **菲律宾** | `ph.seabank.seabank` | `https://api.seabank.ph` | ✅ **成功 → OTP 短信** | `code=0` |
| **新加坡** | `sg.com.maribankmobile.digitalbank` | `https://api.maribank.com.sg` | ❌ 仍失败 | **3100012** |
**关键意义**:同一台 Root 机、同一套 Xposed + Magisk 伪装下,**PH 服务端已放行注册**,说明 Hook 链与 attestation 载荷 **对 PH 有效**SG 失败更可能是 **区域风控策略差异****SG 侧设备黑名单**而非「Root bypass 完全无效」。
---
## 2. 测试环境
| 项 | 值 |
|----|-----|
| 手机 | Google Pixel 6oriole |
| Root | Magisk **30.7**`MAGISK:R` |
| Magisk 模块 | `maribank_device_spoof``zygisk_shamiko``playintegrityfix``zygisk_vector` |
| DenyList | MariBank PH/SG 均已加入;**Enforce DenyList = OFF**Shamiko 要求) |
| LSPosed 模块 | `com.miraclegarden.smsmessage.xposed` |
| LSPosed 作用域 | `ph.seabank.seabank``sg.com.maribankmobile.digitalbank` 等 |
| 伪装 serial | `Y1Rr2fxhOI0ZCQSb`resetprop + Magisk 模块) |
| 伪装 android_id | `87ec910ab5d6f423` |
| 对照干净机 | `25078RA3EY`:关 USB 调试可进 SG OTP无 LSPosed |
---
## 3. 菲律宾突破 — log 证据2026-07-06 13:40
### 3.1 第一次 registerstep=BE
```text
POST https://api.seabank.ph/uapi/v2/register
```
加密前明文(`MariBankEncrypt in0 byte[1180]`)核心字段:
```json
{
"cyCode": "63",
"phone": "<RSA 加密>",
"rdVerifyInfo": {
"bioStatus": 0,
"data": "tPNHr/0eZzaPXs9oimvMNVWI/...",
"dataKey": "H/A6AqA8NhXZoaL0alG5VGRovcfuJykY92U...",
"deviceFingerprint": "jrKyNF/Fx4gcnzVpf1u9bw==|WJMksio9SaKKuyk1KMSilUGivdF+SHbqjB+aL65fucQcOCdojhBN3GKoY58sxVomiCfi5w==|TXeh8FyEf/8qHdAS|00|0",
"afExtInfo": { "modeInCall": "N", "modeInCommunication": "N", "modeCallScreening": "N" }
},
"scene": "REGISTRATION",
"step": "BE",
"source": "app"
}
```
服务端响应:
```json
{"code":0,"msg":"success","data":{"scene":"REGISTRATION","step":"BSO","tranId":"7f21098a-2113-40fb-bb6a-e2e651460f53",...}}
```
### 3.2 第二次 registerOTP 触发step=BSO
```json
{
"cyCode": "63",
"rdVerifyInfo": {
"action": "OTP_SMS_TRIGGER",
"deviceFingerprint": "jrKyNF/...|00|0",
"operationId": "61f9bcc6-dc0c-4f3d-8566-e1d3dc02653f1783316458430"
},
"scene": "REGISTRATION",
"step": "BSO",
"tranId": "7f21098a-2113-40fb-bb6a-e2e651460f53"
}
```
响应仍为 **`code=0`** → **App 可发验证码**
### 3.3 与 7 月 3 日对比
| 日期 | PH register | 说明 |
|------|-------------|------|
| 2026-07-03 | ❌ 4067012 | 仅 riskToken 尾部净化,无加密前 Hook |
| 2026-07-06 | ✅ code=0 → OTP | 加密前 Hook + Attestation Hook + 方案 B 换 ID + Shamiko |
---
## 4. 新加坡仍失败 — log 证据
### 4.1 典型失败11:50换 ID 前)
```text
POST https://api.maribank.com.sg/uapi/v2/register
→ {"code":3100012,"msg":"Unexpected error occurred. Please try again later. "}
```
加密前 `deviceFingerprint` 示例:
```text
X4Ln9cRV1v6aabPt4Ymnqw==|RmdlV34wu8nk8n9nj9A+32Z0BjF/29py3UVnqGWHBJRj5HuTmmOPX+pfJWkZBOn/n41pYw==|3ailMzmgCFzRDFGv|00|0
```
尾部 `\|00\|0` 已净化,**仍 3100012**。
### 4.2 SG 错误码含义(推断)
| 码 | 地区 | 文案 |
|----|------|------|
| 4067012 | PH | For your account's security, temporarily blocked... |
| 3100012 | SG | Unexpected error occurred. Please try again later. |
同属 **服务端风控拒绝**,非本地 Root 弹窗。
---
## 5. 已实现的 bypass 架构
### 5.1 分层模型
```
客户端本地层 服务端层
───────────────── ─────────────────
SafeMode Root 弹窗/自杀 → (不决定 OTP只决定能否进 App
SG ADB 全屏拦截页 →
SHPSSDK assessRisk → 解密 rdVerifyInfo.data/dataKey
riskToken / deviceFingerprint → 查 deviceHash 黑名单
Magisk resetprop 换 serial → 区域策略PH 松 / SG 严)
```
### 5.2 Xposed 模块文件
| 文件 | 作用 |
|------|------|
| `MariBankRootBypassHook.java` | SafeMode、SHPSSDK、ADB Settings、OkHttp 日志、4067/3100012 记录 |
| `MariBankShpsNativeHook.java` | `/proc` 过滤、SystemProperties、requestDefense 净化(不再 block |
| `MariBankSdkUtilsHook.java` | 加密前 Hook `utils.d` / `uvwuvwuv` / Gson |
| `MariBankRegisterPayloadUtil.java` | `scene=REGISTRATION` JSON 净化 |
| `MariBankAttestationHook.java` | native 桥 attestation 链 + 环境探测拦截 |
| `MariBankRiskTokenUtil.java` | riskToken 尾部 `\|09\|1``\|00\|0` |
### 5.3 方案 BMagisk 设备 ID 伪装
路径:`scripts/magisk/maribank-device-spoof/`
| 脚本 | 时机 | 内容 |
|------|------|------|
| `post-fs-data.sh` | 早期启动 | `resetprop` serial、boot 属性、关 adb 属性 |
| `service.sh` | 开机完成后 | 写入新 `Settings.Secure.android_id` |
PC 一键:
```powershell
.\scripts\maribank-spoof-device.ps1 # 一次性 resetprop
.\scripts\maribank-spoof-device.ps1 -InstallModule # 安装持久模块
.\scripts\maribank-scheme-b-finish.ps1 # LSPosed 作用域 + pm clear
```
### 5.4 Shamiko 配置要点
1. Magisk → 开启 **Zygisk**
2. **Configure DenyList** 开启 → 勾选 MariBank PH/SG 全部进程
3. **Enforce DenyList 必须关闭**(否则 Shamiko 不生效)
4. 安装 **Shamiko** 模块并重启
---
## 6. 为何 PH 能过、SG 不过(推断)
> **详细展开**(菲律宾真实原因、`afExtInfo`、USB 调试与 Hook见 [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md) §4§5。
| 维度 | PH ✅ | SG ❌ |
|------|-------|-------|
| 同一 deviceFingerprint 结构 | 有,尾部 `\|00\|0` | 有,尾部 `\|00\|0` |
| `data`/`dataKey` attestation | 服务端接受 | 服务端拒绝3100012 |
| 测试次数 / 黑名单 | 较少失败记录 | Pixel 6 多次测 SG 注册 |
| API 风控强度 | 当日实测 **通过** | 当日实测 **拒绝** |
| 干净机对照 | 未详测 PH | 25078RA3EY 关 ADB 可 OTP |
**结论**
1. **改 riskToken 尾部 alone 不够**,但加上 **加密前 Hook + Shamiko + 换 ID****PH 足够**
2. SG 可能在 attestation 解密后仍有 **更严规则**Play Integrity、设备信誉、区域黑名单
3. PH 成功 **不能** 直接类推 SG**diff 两次 register 明文****换未测过 SG 的环境** 再试。
---
## 7. 已知问题与踩坑
| 问题 | 现象 | 处理 |
|------|------|------|
| Gson Hook 误报 | i18n 文案含 "register" 刷屏 | 已改为只匹配 `scene=REGISTRATION` |
| `requestDefense` 被 block | 缺 `x-sap-fixme` | 已改为执行后净化,不 block |
| ProcessBuilder 抛异常 | `which su` → SecurityException | **待修**:应返回空进程而非抛异常 |
| Magisk zip 在 Windows 打包 | 只解压 module.prop | 用 `tar -a -cf` 或 adb 手动 push sh |
| 关 USB 调试后 adb 断开 | 收尾脚本卡住 | `maribank-scheme-b-finish.ps1` 已加 60s 超时 |
| Attest native Hook 命中少 | 仅 8 个 hook | `data`/`dataKey` 仍未在 native 层改写 |
### 禁止操作(会导致白屏/崩溃)
- Hook `System.loadLibrary`SG RN 崩溃)
- Hook `RealInterceptorChain.proceed`
- 过早 Hook `ShpssInstall` / `vuvuwwwuw`
---
## 8. 常用命令
```powershell
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
# 构建安装
.\scripts\build-debug.ps1
.\scripts\install-debug.ps1
# 抓注册 log先 logcat -c再点 Next
& $adb logcat -d | Select-String "MariBankEncrypt in0 byte\[1|MariBankRoot HTTP.*register|3100012|4067012|OTP_SMS"
# 验证伪装 ID
& $adb shell su -c "getprop ro.serialno; settings get secure android_id"
```
---
## 9. 新加坡后续建议
1. **用 PH 成功时的同一环境** 测 SG关 USB 调试、`pm clear` SG 包、冷启动
2. **diff PH vs SG** 加密前 register JSON`deviceFingerprint` 段、`data` 长度与字段)
3. 修复 **ProcessBuilder** 不抛异常,避免 SHPSSDK 检测 tamper
4. 若仍 3100012考虑 **未测过 SG 的干净机****native hook `libshpssdk_bank.so`**
5. 菲律宾侧:继续走完 **OTP → 开户** 验证全流程稳定性
---
## 10. 相关文档
| 文档 | 说明 |
|------|------|
| [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md) | **register 字段、afExtInfo、USB 调试、PH 真实原因、Xposed/LSPosed** |
| [`手机操作手册.md`](手机操作手册.md) | **Root 机部署、LSPosed、日常操作、抓 log** |
| [`Hook指南.md`](Hook指南.md) | Xposed/LSPosed 概念、Telegram Hook、扩展 App |
| [`MariBank实现说明.md`](MariBank实现说明.md) | PH Hook 实现细节(含 7/3 失败记录,已追加 7/6 突破) |
| [`MariBank新加坡逆向.md`](MariBank新加坡逆向.md) | SG 逆向与 ADB 检测 |
| [`工作日志_2026-07-03.md`](工作日志_2026-07-03.md) | 7/3 工作记录 |
---
*记录日期2026-07-06 · 最后更新:文档体系整理(操作手册 + 风控载荷说明)*

View File

@@ -0,0 +1,588 @@
# MariBank 实现说明与问题记录
> **日期**2026-07-03**2026-07-06 更新PH 注册已成功发 OTP**,见 [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)
> **App**MariBank / SeaBank PH · `ph.seabank.seabank` · v3.22.0 (32200)
> **设备**Pixel 6 · Magisk + Zygisk + LSPosed + Shamiko
> **源码**`xposed-module/.../hook/MariBank*.java`
本文档专门说明当日对 MariBank **注册流程风控绕过** 的 Xposed 实现细节,以及开发/测试中遇到的全部主要问题。通用工作汇总见 [`工作日志_2026-07-03.md`](工作日志_2026-07-03.md)。
---
## 1. 目标与结果概览
### 1.1 目标
在已 Root 设备上完成:
```
打开 App → Sign up → 输入菲律宾手机号 → Next
→ POST https://api.seabank.ph/uapi/v2/register
→ 进入 OTP / 下一步
```
### 1.2 最终结果2026-07-06 更新)
| 阶段 | 2026-07-03 | **2026-07-06** |
| -------------------- | ---------- | -------------- |
| 本地 Root 检测 / 自杀 / 弹窗 | **大部分绕过** | ✅ 稳定 |
| DFP 设备指纹上报 | **成功** `code=0` | ✅ |
| riskToken 尾部净化 | ✅ Hook 生效 | ✅ |
| 加密前 register 明文 | ❌ 未抓到 | ✅ `MariBankSdkUtilsHook` |
| 注册接口 PH | ❌ **4067012** | ✅ **`code=0` → OTPBSO** |
| 注册接口 SG | — | ❌ **3100012**(见 SG 文档) |
**结论2026-07-06**:在 Pixel 6 + Shamiko + 方案 B 换 ID + 全套 Hook 下,**菲律宾 SeaBank 注册已通过服务端校验并可发验证码**;新加坡 MariBank 仍被拒。详见 [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)。
### 1.2.1 当日最终结果2026-07-03 历史记录,已被 7/6 突破)
| 阶段 | 结果 |
| -------------------- | -------------------------------------- |
| 本地 Root 检测 / 自杀 / 弹窗 | **大部分绕过**,可稳定进入注册页 |
| DFP 设备指纹上报 | **成功**`code=0` |
| riskToken 尾部字段净化 | **Hook 生效**`\|09\|1``\|00\|0` |
| 注册接口 | **失败**,服务端返回 **4067012** / **4067004** |
| 注册请求明文 | **未抓到**body native 加密) |
**结论2026-07-03**:本地层 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 服务端注册拒绝 — 40670122026-07-06 已突破 PH
> **更新**2026-07-06 在 Shamiko + 方案 B + `MariBankSdkUtilsHook` / `MariBankAttestationHook` 下PH register 返回 **`code=0`step=BSOOTP_SMS_TRIGGER 成功**。详见 [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)。
> 以下内容为 **7 月 3 日** 失败时的记录,保留作对照。
**现象2026-07-03**
- 能进入 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/工作日志_2026-07-03.md` | 当日全项目工作汇总 |
| `docs/MariBank_2026-07-06_菲律宾突破.md` | PH OTP 突破 + SG 现状 |
| `docs/MariBank风控与载荷说明.md` | register 字段、afExtInfo、USB 调试 |
| `docs/手机操作手册.md` | Root 机部署与日常操作 |
---
*文档版本2026-07-03 · 对应 commit 分支 `ysc`*

View File

@@ -0,0 +1,217 @@
# MariBank 新加坡版逆向报告
> **日期**2026-07-06
> **设备**Pixel 6已 Root + LSPosed + ADB 开启)
> **APK 来源**:从本机 pull`reverse/apks/maribank_sg_*.apk`
---
## 1. 基本信息
| 项 | 值 |
|----|-----|
| **包名** | `sg.com.maribankmobile.digitalbank` |
| **显示名** | MariBank新加坡 |
| **版本** | 3.2.2versionCode **30220** |
| **对比 PH 版** | `ph.seabank.seabank` · v3.22.0 (32200) |
| **API 域名** | `https://api.maribank.com.sg`staging: `api.staging.maribank.com.sg` |
| **Application** | `com.shopee.bke.digitalbank.BkeApplication`(与 PH **相同** |
本地 APK
```
reverse/apks/maribank_sg_base.apk
reverse/apks/maribank_sg_arm64.apk → SO 在 reverse/extracted/native/
```
---
## 2. 截图现象(你当前遇到的)
启动后出现 **两层拦截**
| 现象 | 类型 | 说明 |
|------|------|------|
| 全屏 **「ADB/Wireless ADB Detected」** | 环境检测页 | 文案 key`bke_title_adb_wirelessadb_control``bke_btn_turn_off_adb_wireless_adb` |
| Toast **「Does not support root device」** | Root 检测 | SHPSSDK / SafeMode 典型提示 |
**含义**
1. **ADB 检测****Root 检测****独立项**——新加坡版在 PH 版基础上 **明显加强了 USB/无线调试检测**
2. 状态栏 **VPN 小钥匙** 可能额外触发 `RISK_*` 或网络风控(需 logcat 确认)。
3. 当前 **PH 版 Xposed Hook 未作用到 SG 包名**(只 Hook `ph.seabank.seabank`),所以 SG 版 **完全未 bypass**
**临时验证(无 Hook**:关闭 **USB 调试 + 无线调试** 后冷启动,可确认 ADB 页是否消失Root Toast 仍会存在。
---
## 3. 技术栈对比SG vs PH
| 组件 | SG 3.2.2 | PH 3.22.0 | 结论 |
|------|----------|-----------|------|
| Shopee BKE | ✅ | ✅ | 同框架 |
| `BkeApplication` | ✅ | ✅ | Hook 时机相同(`attachBaseContext` 后) |
| SafeMode | `com.shopee.bke.lib.safemode.b` 等 | 相同 | 可复用 boolean Hook |
| `SafeModeRecoverActivity` | ✅ | ✅ | Root 弹窗/恢复页 |
| SHPSSDK Bank | `com.shopee.shpssdkbank.SHPSSDK` | 相同 | 可复用 riskToken / requestDefense 链 |
| Native SO | `libshpssdk_bank.so``libsdkutils.so``libbkutils.so` | 相同 | SG 额外有 **`libshpssdk.so`** |
| 字符串解密 | `uvuwwuvwv.uvwwuuvvw` | 相同 | 混淆包名可能不同 |
**结论**SG 与 PH 是 **同一套 Shopee 银行 SDK**PH 上已写的 Hook **大部分可迁移**,需改 **包名****补充 ADB 风控**
---
## 4. 新加坡版新增ADB / 无线调试检测
### 4.1 SHPSSDK 风险常量DEX 内)
```
RISK_USB_ADB ← USB 调试
RISK_WIFI_ADB ← 无线调试
RISK_ADB
RISK_ROOT
RISK_HOOK
RISK_DEBUG
RISK_DEVELOPER_MODE
RISK_EMULATOR
...
```
与截图中 **ADB 拦截页 + Root Toast** 对应。
### 4.2 UI 字符串(资源 key
| Key | 用途 |
|-----|------|
| `bke_title_adb_wirelessadb_control` | 标题「ADB/Wireless ADB Detected」 |
| `bke_btn_turn_off_adb_wireless_adb` | 按钮「Turn Off ADB/Wireless ADB」 |
| `bke_desc_explain_detect_adb_1/2` | 说明文案 |
| `bke_desc_safeguard_bank_app_from_adb_wirelessadb_1/2` | 安全说明 |
| `AdbDetected` / `WifiAdbDetected` | RN / 业务路由标识 |
### 4.3 检测思路(推断)
1. **Settings.Global** / `adb_enabled`、无线调试相关属性
2. SHPSSDK `getRiskSync` → 命中 `RISK_USB_ADB` / `RISK_WIFI_ADB`
3. 进入 **ADB 专用拦截 UI**(非仅 SafeModeRecoverActivity
4. Root 仍走 `RISK_ROOT` → Toast「Does not support root device」
---
## 5. API 与登录SG
PH 实测重点为 `POST /uapi/v2/register`SG DEX 内可见:
| 类型 | 路径示例 |
|------|----------|
| 登录 | `/v3/login``/v4/login``/v2/login/bke/linkage` |
| 注册 | `/v2/register` |
| 登录前 | `/v1/list/latest-notice/pre-login` |
| 人脸 | `/v3/login/facial/verification` |
| DFP | `/dfp/`(与 PH 同类) |
| 通用 uapi | `/uapi/`47 处引用) |
**Host**`https://api.maribank.com.sg`(非 PH 的 `api.seabank.ph`)。
---
## 6. Native 库arm64 split
已从 `maribank_sg_arm64.apk` 解出至 `reverse/extracted/native/`,与风控相关:
| SO | 大小(约) | 作用 |
|----|-----------|------|
| `libshpssdk_bank.so` | 8.5 MB | SHPSSDK 银行风控、riskToken |
| `libshpssdk.so` | 6.3 MB | **SG 额外**PH 侧以 bank 为主) |
| `libsdkutils.so` | 789 KB | 注册/body 加密 |
| `libbkutils.so` | 102 KB | JNI 工具 |
| `libmemory_security.so` | 3.8 MB | 内存/安全相关 |
---
## 7. 与现有 Xposed 模块的关系
当前 `MariBankRootBypassHook` / `MariBankShpsNativeHook` **仅绑定**
```java
public static final String PACKAGE = "ph.seabank.seabank";
```
**SG 包名未纳入**,因此截图中的拦截 **预期行为**
### 7.1 可复用(改包名即可试)
- SafeMode boolean Hook`safemode.b` 等)
- `Process.killProcess` / `finishAffinity` 反自杀
- `/proc/self/maps` 过滤、`SystemProperties` 伪装
- SHPSSDK `getRiskSync` 清空、`getRiskToken` 净化
- OkHttp 出站 riskToken 净化
**已实现2026-07-06**`MariBankRootBypassHook` 已支持 `sg.com.maribankmobile.digitalbank`,含 ADB Settings 伪装、ADB 全屏页 finish、Root/ADB Toast 拦截。
### 7.2 SG 专用(已部分实现)
| 项 | 状态 |
|----|------|
| **包名** | ✅ `MainHook` + `arrays.xml` |
| **ADB 风险** | ✅ `Settings.Global/Secure` adb 键 → 0`init.svc.adbd` → stopped |
| **ADB 拦截页** | ✅ `Activity.onResume` 检测 ADB 文案后 `finish()` |
| **Root Toast** | ✅ 「Does not support root device」 |
### 7.3 不建议照搬 PH 服务端结论
PH 的 **4067012****菲律宾服务端** 结论SG 需单独测 `api.maribank.com.sg`
---
## 8. 逆向脚本
```powershell
# 已从手机 pull APK 后可本地扫描
python reverse/scripts/scan_maribank_sg.py
python reverse/scripts/find_sg_adb_strings.py
python reverse/scripts/find_sg_adb_classes.py
python reverse/scripts/list_safemode.py reverse/apks/maribank_sg_base.apk
```
从手机 pull已执行过
```powershell
adb shell pm path sg.com.maribankmobile.digitalbank
adb pull <base.apk> reverse/apks/maribank_sg_base.apk
```
---
## 9. 建议下一步2026-07-06 更新)
1. ~~LSPosed 增加 SG 包名~~ ✅ 已完成
2. ~~扩展 ADB bypass~~ ✅ 已完成
3. **SG**diff PH/SG register 明文;修复 ProcessBuilder 异常;仍 3100012 则试干净机
4. **PH**:走完 OTP → 开户全流程,确认稳定性
5. logcat`MariBankEncrypt in0 byte``3100012``OTP_SMS`
---
## 10. 实测状态2026-07-06
| 包 | 注册 API | 结果 |
|----|----------|------|
| `ph.seabank.seabank` | `api.seabank.ph/uapi/v2/register` | ✅ **code=0 → OTP** |
| `sg.com.maribankmobile.digitalbank` | `api.maribank.com.sg/uapi/v2/register` | ❌ **3100012** |
完整说明:[`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)
---
## 11. 相关文档
| 文档 | 说明 |
|------|------|
| [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md) | **PH OTP 突破 + SG 现状 + 方案 B** |
| [`MariBank实现说明.md`](MariBank实现说明.md) | PH Hook 实现与问题清单 |
| [`工作日志_2026-07-03.md`](工作日志_2026-07-03.md) | PH 版 7/3 工作记录 |
---
*记录日期2026-07-06 · 最后更新:菲律宾 OTP 突破*

View File

@@ -0,0 +1,215 @@
# MariBank 风控与注册载荷说明
> 客户端检测分层、register 请求字段含义、菲律宾突破原因、USB 调试与 Hook 关系。
> 关联:[`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md) · [`MariBank实现说明.md`](MariBank实现说明.md) · [`手机操作手册.md`](手机操作手册.md)
---
## 1. 风控两层模型
| 层级 | 谁在做 | 失败表现 | bypass 方式 |
|------|--------|----------|-------------|
| **客户端本地** | SafeMode、SHPSSDK、SG ADB 页 | 弹窗、全屏拦截、自杀 | Xposed boolean / Settings / finish Activity |
| **服务端** | `api.seabank.ph` / `api.maribank.com.sg` | 4067012 / 3100012 | 让出站 attestation「够干净」+ 区域策略 |
**DFP `code=0` 只表示上报收到,不等于注册会通过。**
干净机对照25078RA3EY**开 USB 调试 → SG 本地 ADB 页;关调试 → SG OTP 可过**。说明本地检测与服务端决策 **相互独立**
---
## 2. 注册请求载荷结构App 发出)
加密前 JSON`MariBankEncrypt` / `uvwuvwuv.uvwvuww` 入口)典型结构:
```json
{
"cyCode": "63",
"phone": "<RSA 加密>",
"scene": "REGISTRATION",
"step": "BE",
"rdVerifyInfo": {
"bioStatus": 0,
"data": "<SHPSSDK native attestation 密文>",
"dataKey": "<attestation 密钥>",
"deviceFingerprint": "段1|段2|段3|00|0",
"fvInfo": {},
"afExtInfo": {
"modeInCall": "N",
"modeInCommunication": "N",
"modeCallScreening": "N"
},
"publicKeyAuthen": "pk2",
"random": "...",
"softTokenActivated": false
}
}
```
流程:**App 本地采集 → 拼 JSON → native 加密 → POST `/uapi/v2/register`**
---
## 3. 字段说明
### 3.1 `deviceFingerprint`
pipe 分隔字符串,示例:
```text
jrKyNF/Fx4gcnzVpf1u9bw==|WJMksio9SaKKuyk1KMSilUGivdF+...|TXeh8FyEf/8qHdAS|00|0
↑ Base64 段1 ↑ Base64 段2设备哈希 ↑ 短码 ↑ 风险码|标志
```
| 部分 | 含义 | Hook 能否改 |
|------|------|-------------|
| 前两段 Base64 | 设备 / 环境哈希,服务端可索引黑名单 | 换 serial/android_id 后 **重新生成** |
| 第三段 | 短码 | 随 SDK 变 |
| 最后 `\|xx\|y` | 风险摘要(如 Root+Hook `\|09\|1` | ✅ 已净化为 `\|00\|0` |
**只改尾部不够**:服务端还验 `data`/`dataKey` 解密内容。
### 3.2 `data` / `dataKey`
-**`libshpssdk_bank.so` native** 生成并签名
- 内含 Root / Hook / 调试等 attestation**Java 层改 JSON 字段无法重写密文**
- SG 3100012 时多半此层或设备黑名单未过PH 7/6 成功时服务端 **接受了** 当前密文
### 3.3 `afExtInfo`(银行 App 上报,非 PC 添加)
```json
"afExtInfo": {
"modeInCall": "N",
"modeInCommunication": "N",
"modeCallScreening": "N"
}
```
| 字段 | 推测含义 | `"N"` |
|------|----------|-------|
| `modeInCall` | 是否正在通话 | No |
| `modeInCommunication` | 是否通信/音频占用 | No |
| `modeCallScreening` | 来电筛选等相关状态 | No |
- **来源**App / SDK 读系统状态写入 `rdVerifyInfo`
- **去向**:随 register 加密后发给服务端
- **当前模块****未专门修改** 这三项(原样上报)
- **用途**:反欺诈辅助信号,**不是** USB 调试开关
### 3.4 其他常见字段
| 字段 | 说明 |
|------|------|
| `cyCode` | 国家码PH=`63`SG=`65` |
| `step` | `BE` 首次注册 → 成功后可到 `BSO`OTP |
| `action` | `OTP_SMS_TRIGGER` 触发短信验证码 |
| `bioStatus` | 生物识别状态,注册时多为 `0` |
---
## 4. 为何 USB 调试开着仍能发 OTP菲律宾实测
### 4.1 两个「USB 调试」不是一回事
| | PC 侧 adb | App 内部读到的值 |
|--|-----------|------------------|
| 你开 USB 调试 | ✅ 能 `adb shell` | 被 Hook **伪装为关闭** |
### 4.2 模块已做的 ADB bypass`MariBankRootBypassHook`
**Java Settings**
- `adb_enabled` / `adb_wifi_enabled` / `development_settings_enabled``0`
**系统属性(`MariBankShpsNativeHook`**
- `init.svc.adbd``stopped`
- `persist.sys.adb_enable``0`
**UI**
- 全屏「ADB / Wireless ADB Detected」页 → `Activity.finish()`
- 相关 Toast 拦截
因此:**PC 连着 adbApp 仍可能认为未开调试**本地不拦、register 载荷也不带「adb 开」的自报字段。
### 4.3 与干净机对照
| 环境 | USB 调试 | 结果 |
|------|----------|------|
| 干净机(无 Hook | 开 | 本地 ADB 页,进不了 OTP |
| 干净机 | 关 | ✅ OTP |
| Pixel 6 + LSPosed + Shamiko | **开** | ✅ PH OTPHook 伪装) |
### 4.4 注意
- native 仍可能用 Hook 未覆盖的路径读调试状态
- **SG 更严**,开 adb 风险高于 PH
- 文档仍建议测 SG 时 **关 USB 调试**
---
## 5. 菲律宾能发验证码的真实原因
### 5.1 能确定
- `api.seabank.ph` 返回 **`code=0`**step 进入 **BSO****OTP_SMS_TRIGGER** 成功
- 同一台机、同一套 Hook 下 **SG 仍 3100012** → 差异在 **服务端**,非「没风控」
### 5.2 7/3 失败 → 7/6 成功的变化
| 能力 | 7/3 | 7/6 |
|------|-----|-----|
| riskToken 尾部净化 | ✅ | ✅ |
| 加密前 register 明文 Hook | ❌ | ✅ |
| Attestation / 环境 Hook | 弱 | ✅ |
| Shamiko 藏 Magisk | 无/未配 | ✅ |
| 换 serial / android_id | 无 | ✅ |
**不是单一开关**,而是 **Shamiko + 新设备 ID + 加密前/采集链净化** 组合后PH 服务端认为 attestation **可接受**
### 5.3 高概率推断
1. **PH / SG 两套 API、两套规则** — 同一载荷 PH 过、SG 不过
2. **PH 侧设备黑名单** — Pixel 6 在 SG 测多次PH 可能未标记
3. **7/3 的 4067012** — bypass 不完整非「PH 永远不能 Root 注册」
### 5.4 无法无源码 100% 证实
- PH 后台具体哪条规则放行
- `data` 解密后哪一位从拒变收
---
## 6. Magisk / LSPosed / Shamiko 分工
```
MagiskRoot 权限)
└── Zygisk
├── LSPosed → 加载 xposed-module改 Java 检测、抓加密前 JSON
└── Shamiko → 对银行进程隐藏 Magisk/LSPosed
Magisk 模块 maribank_device_spoof → resetprop 换 serial 等
```
| 组件 | 作用 |
|------|------|
| **Magisk** | Root |
| **LSPosed** | 运行 Xposed 模块(与经典 Xposed 同 API见 [`Hook指南.md` §0](Hook指南.md) |
| **Shamiko** | 进程内 Hide Root改善 native attestation |
| **Xposed 模块** | SafeMode / ADB / 加密前净化 |
---
## 7. 服务端可能校验项(反推)
| 校验项 | 说明 |
|--------|------|
| `rdVerifyInfo.data` / `dataKey` | native attestation 解密 |
| `deviceFingerprint` 段 1/2 | 设备黑名单 |
| DFP 历史画像 | 与 register 是否一致 |
| 区域策略 | PH 松 / SG 严(实测) |
| IP / 号码 / 频率 | 次要,非本次主因 |
---
*最后更新2026-07-06*

View File

@@ -0,0 +1,85 @@
# TNG eWallet 逆向笔记 — 自杀链2026-07-31
## 运行时铁证
| 现象 | 含义 |
|------|------|
| `Fatal signal 6 (SIGABRT), code 0 (SI_USER)` | 用户态 `kill(pid, SIGABRT)`**不是** `exit_group`,也不是 `abort()``SI_TKILL` |
| PLT hook `kill/raise` **从不打日志** | Promon 走 **内联 SVC**,不经 PLT |
| `E Report : Exiting:` + `xwwqazamx.W: 16` + `bl.a` | Java 层检测入口仍是 `xwwqazamx.bl#a/#b` |
| `Displayed ...UserLoginActivity` | UI 能到登录页;随后数秒内被杀 |
| `Lcom/aliyun/TigerTally/t/B` 反射 `Thread.dispatchUncaughtException` | 另有一套阿里 TigerTally SDK |
## 自杀阶梯(当前理解)
```
Promon native 检测 (Thread-N 扫 /proc、selinux、wifi prop…)
→ xwwqazamx.bl#a / #b
→ 抛 W:16Report 打 Exiting
→ 同时/随后 inline kill(SIGABRT, SI_USER)
→ KillApplicationHandlerXposed 可拦)
→ 若 ABRT 被 ignore可能改 SIGKILL / BRK待证
```
并行:
- `AppSecurityManager.handle*Callback`Xposed 已拦)
- `openSecurityUrl` Root FAQ已拦
- `UnhandledEvent``finishAllActivityAndKillApp`(已 hook
- ForceExitCountdown已 hook
- TigerTally / SecurityGuard `JNICLibrary.doCommand`(当前 stub可能过度
## 静态 SO
`libtngdigital_ewallet.so`~7.7MB,打包态):
- `brk` 假阳性/加密指令很多imm 离散)
- 真实 `svc #0` 极少;运行时解密后才出现 exit 序列
- **禁止**在 `JNI_OnLoad` 前/中 patch SO会 Bad JNI / SIGILL
## 已验证无效/有害手段
- seccomp 拦 `exit_group` → Promon 改 UDF/SIGILL
- 改 libc exit SVC→RET → 同上
- 改 Promon SO 字节 → Bad JNI / SIGILL
- 高频 `sigaction` 重装 → 易被当成 hook 指纹
## 较有效手段
- Xposed拦 FAQ / KillApplicationHandler / handle*Callback / ForceExit / UnhandledEvent
- Xposed`bl#a/#b` **beforeHook short-circuit**(避免跑进 native 杀进程)
- ZygiskPLT 观察 + `SIGABRT` ignore对 SI_USER 理论有效Promon 可能复位 handler
## 新发现2026-07-31 续)
### `libtiger_tally.so`~4.6MB
- 与 Promon `libtngdigital_ewallet.so` **并行加载**
- 动态依赖:`abort@LIBC``signal@LIBC`**走 PLT**,可被 Zygisk PLT hook
- 内嵌少量 syscall stubopenat/read/faccessat…**无** kill/exit stub
- `BIND_NOW`
### 打包态 SO
- `libtngdigital_ewallet.so` / `libtiger_tally.so` 静态 **0**`movz x8,#129 + svc`
- 自杀用的 inline `kill` 仍可能在 **运行时解密** 后出现 → Zygisk **延迟 2s** 扫描 patch
### 日志结论2026-07-31 11:3711:47
| 现象 | 含义 |
|------|------|
| `exited cleanly (1)` | inline `exit_group(1)`PLT 拦不住) |
| kill SVC→RET 后 `signal 9` | SO 完整性/备用路径 → **SIGKILL** |
| 过早 patch exit_group | 启动即崩CEM |
| seccomp 拦 `exit`+`exit_group` | ~1s 死(误伤线程 exit |
| seccomp **只拦 exit_group** | 存活 **~16s**,随后 **SIGILL 风暴**count 7000+ |
| `caught sig=4 si_code=1` | Promon UDF 兜底;+4 skip 无效 |
### Rest 配置2026-07-31 12:55已装机
| 层 | 策略 |
|----|------|
| Zygisk | PLT 拦 exit/abort/kill**exit_group-only seccomp @2s****禁止**改 SO会 SIGKILL |
| 信号 | ABRT ignoreSEGV/ILL **+4 skip****不** freeze 线程freeze 会 ANR |
| Xposed | Splash `reportFullyDrawn` + **2.5s 强拉 UserLogin** + finish Splashbl SC / 自杀 Java / JNIC stub |
实测:进程 **≥40s** 存活,焦点 `UserLoginActivity``phase done seccomp=1 caught=0`
注意:点亮屏幕后再开 App`svc power stayon true`);勿把 TNG 放进 Magisk DenyList。

View File

@@ -0,0 +1,295 @@
# Telegram 抓消息说明
> **用途**:在 notiMessage 中抓取 Telegram 消息(前台 + 后台),转发到 App 本地日志与 PC 调试台。
> **关联**[`Hook指南.md`](Hook指南.md)通用架构、Xposed/LSPosed· [`手机操作手册.md`](手机操作手册.md) · [`更新说明.md`](更新说明.md)
---
## 1. 能力概览
| 场景 | 通道 | 是否需要 Root / LSPosed | 日志标识 |
|------|------|-------------------------|----------|
| TG **后台** / 锁屏,系统弹出通知 | 通知监听 `NotificationService` | ❌ 不需要(仅需通知使用权) | 无前缀 |
| TG **前台** 打开聊天(通常无系统通知) | Xposed `TelegramMessageHook` | ✅ 需要 Magisk + LSPosed + 模块 | `[Hook/xposed_telegram]` |
**重要**:只开通知监听、不装 Xposed 时,**TG 在前台收消息抓不到**。要完整覆盖,必须启用 Hook 模块。
---
## 2. 必须安装什么
### 2.1 完整能力(前台 + 后台)
| 序号 | 组件 | 说明 |
|------|------|------|
| 1 | **Telegram** | 包名见 §3 |
| 2 | **notiMessage 主 App** | `com.miraclegarden.smsmessage` |
| 3 | **Xposed 模块 APK** | `com.miraclegarden.smsmessage.xposed`(与主 App 同仓库 `:xposed-module` |
| 4 | **Magisk + Zygisk** | Root 环境 |
| 5 | **LSPosed** | 加载 Xposed 模块 |
抓 TG **不需要** Shamiko那是 MariBank 等银行 App 用的)。
### 2.2 仅后台通知(不抓前台)
| 必须 | 不需要 |
|------|--------|
| notiMessage 主 App | Xposed 模块 |
| Telegram | LSPosed / Root |
| 通知监听权限 | |
### 2.3 PC 端(调试台,可选)
| 组件 | 用途 |
|------|------|
| 本仓库源码 + JDK + Android SDK | 编译安装 |
| **adb** | USB 安装、`adb reverse` |
| **Python 3** | `configure-lsposed.py``install-full.ps1` 用) |
| `debug-server/server.py` | PC 浏览器查看消息(端口 **8765** |
---
## 3. 包名与 LSPosed 作用域
Pixel 6 等设备常见包名:
| 安装来源 | 包名 |
|----------|------|
| Play / 官网 APK | `org.telegram.messenger` |
| 部分渠道 / Web 版 | `org.telegram.messenger.web` |
**必须勾选与实际安装一致的包名**(两个都装则两个都勾)。
LSPosed 作用域需勾选:
1. **目标 Telegram 包名**(上表)
2. **notiMessage 主 App** `com.miraclegarden.smsmessage`(接收 Hook 广播、写日志)
3. 模块本身已在 LSPosed 里 **启用**
默认作用域见 `xposed-module/src/main/res/values/arrays.xml`
一键安装时 `scripts/configure-lsposed.py` 会写入上述包名。
**每次更新 Xposed 模块 APK 后**LSPosed 里对 Telegram **软重启**(或整机重启),否则 Hook 不注入。
---
## 4. 安装与配置(推荐流程)
### 4.1 PC 一键安装
```powershell
cd C:\Users\Administrator\Desktop\notiMessage
.\scripts\install-full.ps1
```
自动完成:编译 → 安装双 APK → 写入 LSPosed 作用域 → `adb reverse` → 电池白名单 → 通知监听授权。
仅编译安装、不写作用域:
```powershell
.\scripts\build-debug.ps1
.\scripts\install-debug.ps1
```
> **注意**Android Studio 直接 Run 只装主 App**不会**装 Xposed 模块。完整 TG 抓取必须用 `install-full.ps1` 或手动装两个 APK。
### 4.2 手机上
1. **LSPosed** → 启用模块 **notiMessage Xposed** → 作用域勾选 Telegram + 主 App → **软重启 Telegram**
2. 打开 **notiMessage****监听设置** → 添加 **Telegram**(包名须与 LSPosed 一致)
3. 回到 **监听控制台****开始监听** → 授予通知使用权
4. (可选)设置 → 给 Telegram / notiMessage **电池不受限制**
### 4.3 PC 调试台
```powershell
.\scripts\start-debug-server.ps1
```
浏览器打开 **http://127.0.0.1:8765**,按群/会话分组展示。
脚本会自动执行 `adb reverse tcp:8765 tcp:8765`
### 4.4 验证
1. 启动调试台(可选)
2. notiMessage 开始监听后 **切到桌面**
3. 打开 Telegram在任意群/私聊 **收一条别人发的消息**(前台场景)
4. 期望:
- App 监听页出现 `[Hook/xposed_telegram] 群名 发送者: 内容`
- PC 调试台同内容
5. 再把 TG 切后台,收一条 **会弹系统通知** 的消息 → 走通知通道(无前缀)
---
## 5. 双通道架构
```
Telegram
┌──────────────┴──────────────┐
│ │
后台弹系统通知 前台收到新消息
│ │
▼ ▼
NotificationService TelegramMessageHook
(NotificationListener) (xposed-module 进程内)
│ │
│ │ 广播 HOOK_MESSAGE
│ ▼
│ HookMessageReceiver
│ ├─ MessageLogStore
│ ├─ DebugForwarder → PC :8765
│ └─ (可选)正式后端上传
└──────────────┬──────────────┘
监听页 / PC 调试台
```
| 配置项 | 文件 | 默认值 |
|--------|------|--------|
| PC 转发开关 | `AppConfig.ENABLE_DEBUG_FORWARD` | `true` |
| 调试台地址 | `AppConfig.DEBUG_SERVER_URL` | `http://127.0.0.1:8765` |
| 正式后端上传 | `AppConfig.ENABLE_SERVER_UPLOAD` | `false` |
Hook 广播格式:
```text
Action: com.miraclegarden.smsmessage.action.HOOK_MESSAGE
Package: com.miraclegarden.smsmessage
Extras: packageName, title, content, timestamp, source=xposed_telegram
```
`HookMessageReceiver` 会检查 Telegram 是否在 **监听列表**;未添加则日志 `[Hook] 未配置监听: org.telegram...` 并丢弃。
---
## 6. 实现原理TelegramMessageHook
源码:`xposed-module/src/main/java/.../hook/TelegramMessageHook.java`
### 6.1 为何不 Hook SQLite
Telegram 消息存在 SQLite 的 **`data` 字段TL 二进制序列化)**,不是明文 `content`/`text`
通用 `SqliteMessageHook` **对 Telegram 无效**,因此单独实现本 Hook。
### 6.2 Hook 路径(三条,互为补充)
| 优先级 | Hook 点 | 触发时机 |
|--------|---------|----------|
| **主路径** | `NotificationCenter.postNotificationName(int, Object[])`,当 `id == didReceiveNewMessages` | 前台收到新消息、消息已解密为 `MessageObject` |
| **通知路径** | `NotificationsController.appendMessage(MessageObject)` | 后台准备弹系统通知时 |
| **兜底** | `MessageObject` 全部构造函数 | 主路径安装失败时 |
### 6.3 过滤与去重
- 只处理 **非 outgoing**`messageOwner.out == false`),忽略自己发出的消息
- 去重键:`dialogId:messageId`,内存保留最近 512 条
### 6.4 字段提取
| 字段 | 提取方式 | 展示 |
|------|----------|------|
| **群名/会话名** | `MessagesController.getPeerTitle(dialogId)``getName()``getChat().title` | 广播 `title` |
| **发送者** | `MessageObject.getFromName()` | 群内:`发送者: 正文` |
| **正文** | `messageText``messageOwner.message``caption` | 广播 `content` |
| **媒体** | `isPhoto` / `isVideo` 等 → `[图片]``[视频]`… | 与 caption 合并 |
支持的媒体标签:`[图片]``[视频]``[GIF]``[贴纸]``[语音]``[音频]``[文件]``[位置]``[联系人]``[媒体消息]`
**注意**:不要把 `TLRPC$...` 对象直接 `toString()` 当标题;代码里 `safeText()` 会过滤这类字符串。
---
## 7. 常见问题
| 现象 | 原因 | 处理 |
|------|------|------|
| 前台有消息,无 `[Hook/...]` | Hook 未注入 | LSPosed 启用模块;作用域勾 **实际 TG 包名****软重启 TG** |
| 日志 `[Hook] 未配置监听` | 监听列表未加 Telegram | 监听设置里添加对应包名 |
| 只有后台有、前台没有 | 未装 Xposed / 作用域错误 | 安装 xposed 模块 + `install-full.ps1` |
| PC 无数据 | `adb reverse` 未设或调试台未开 | 跑 `start-debug-server.ps1` |
| notiMessage 切后台后 PC 断 | 旧版依赖 startService | 更新到 v2.2.1+`HookMessageReceiver` 内直接转发 |
| 标题显示 `TLRPC$...` | 旧版或异常路径 | 更新模块;见 `TelegramMessageHook.extractTitle` |
| 图片只有「图片」无说明 | 说明在 caption | 已合并 caption仍空则 TG 未带说明 |
| 群静音 / 无通知 | 后台通道无触发 | 前台靠 Hook后台需 TG 允许通知且未静音 |
| TG 被系统冻结 | 省电策略 | TG、notiMessage 设「不受限制」 |
| logcat 无 `notiMessageHook/Telegram` | Hook 未加载 | 查 LSPosed 日志;确认包名是否为 `.web` 版 |
---
## 8. 诊断 logcat
```powershell
$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
# Hook 是否安装
& $adb logcat -d | Select-String "notiMessageHook/Telegram"
# 主 App 是否收到广播
& $adb logcat -d | Select-String "HookMessageReceiver|DebugForwarder"
# 通知通道
& $adb logcat -d | Select-String "NotificationService"
```
期望看到:
```text
notiMessageHook/Telegram: installed for org.telegram.messenger.web, didReceiveNewMessages=...
notiMessageHook/Telegram: appendMessage hook installed for ...
HookMessageReceiver: hook received: pkg=org.telegram.messenger.web source=xposed_telegram title=...
```
---
## 9. 与 MariBank 模块的关系
本仓库 **同一个 Xposed APK** 里还包含 MariBank bypass、澳银 Hook 等;**Telegram 抓消息与银行 bypass 互不依赖**
| 能力 | 是否需要 Telegram Hook | 是否需要 MariBank Hook |
|------|------------------------|------------------------|
| 抓 TG 消息 | ✅ | ❌ |
| MariBank 注册 bypass | ❌ | ✅ |
LSPosed 作用域可以 **只勾 Telegram + 主 App**,不勾银行包名。
详见 [`Hook指南.md`](Hook指南.md) 中「模块与通知管理的关系」说明。
---
## 10. 相关文件
```text
xposed-module/
├── src/main/assets/xposed_init
├── src/main/java/.../MainHook.java # 路由到 TelegramMessageHook
├── src/main/java/.../HookForwarder.java
├── src/main/java/.../hook/TelegramMessageHook.java
└── src/main/res/values/arrays.xml # 默认 xposed_scope
app/
├── src/main/java/.../service/HookMessageReceiver.java
├── src/main/java/.../service/NotificationService.java
├── src/main/java/.../MessageLogStore.java
├── src/main/java/.../network/DebugForwarder.java
└── src/main/java/.../AppConfig.java
debug-server/server.py
scripts/install-full.ps1
scripts/start-debug-server.ps1
```
---
## 11. 最短路径速查
```text
PC: install-full.ps1 → start-debug-server.ps1
手机: LSPosed 启用模块 → 勾 Telegram + 主 App → 软重启 TG
App: 监听设置添加 Telegram → 开始监听
验证: TG 前台收消息 → 出现 [Hook/xposed_telegram]
```
---
*最后更新2026-07-06*

View File

@@ -0,0 +1,252 @@
# 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/银行逆向.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实现说明.md` | **MariBank 实现细节与问题记录(专文)** |
| `docs/MariBank_2026-07-06_菲律宾突破.md` | PH OTP 突破 + SG 现状 |
| `docs/MariBank风控与载荷说明.md` | register 字段、afExtInfo、USB 调试 |
| `docs/手机操作手册.md` | Root 机部署与日常操作 |
| `docs/银行逆向.md` | 澳大利亚三家银行 HookUp / Suncorp / ubank |
| `docs/Hook指南.md` | 通用 Hook 架构、Xposed/LSPosed、扩展 |
| `docs/更新说明.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*

160
docs/手机操作手册.md Normal file
View File

@@ -0,0 +1,160 @@
# 手机操作手册
> 在 Root 手机上部署 **notiMessage** 主 App + **Xposed 模块**,以及 **MariBank / SeaBank** bypass 的日常步骤。
> 关联:[`Hook指南.md`](Hook指南.md) · [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md) · [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md)
---
## 1. 手机前置条件(一次性)
| 步骤 | 操作 |
|------|------|
| 1 | 刷 **Magisk**Magisk 设置里开启 **Zygisk** |
| 2 | 安装 **LSPosed**Magisk 模块或 Manager APK |
| 3 | 安装 **Shamiko**(银行 App **强烈建议** |
| 4 | Magisk → **配置排除列表** → 开启 |
| 5 | 排除列表勾选目标银行 **全部子进程**PH / SG 包名见下) |
| 6 | **不要** 开启「强制启用排除列表」Shamiko 要求 Enforce = OFF |
| 7 | (可选)安装 Magisk 模块 `maribank-device-spoof` 持久换 serial / android_id |
### 目标包名
| App | 包名 |
|-----|------|
| 菲律宾 SeaBank | `ph.seabank.seabank` |
| 新加坡 MariBank | `sg.com.maribankmobile.digitalbank` |
| notiMessage 主 App | `com.miraclegarden.smsmessage` |
| Xposed 模块 | `com.miraclegarden.smsmessage.xposed` |
| Telegram抓消息 | `org.telegram.messenger``org.telegram.messenger.web` |
---
## 2. PC 安装 / 更新USB 连手机)
### 2.1 一键全套(推荐)
```powershell
cd C:\Users\Administrator\Desktop\notiMessage
.\scripts\install-full.ps1
```
自动完成:编译 → 安装双 APK → 配置 LSPosed 作用域 → 电池白名单 → 通知监听授权等。
### 2.2 仅编译安装 APK
```powershell
.\scripts\build-debug.ps1
.\scripts\install-debug.ps1
```
### 2.3 MariBank 测前收尾
```powershell
.\scripts\maribank-scheme-b-finish.ps1
```
作用:确认 Magisk 模块、写入 LSPosed 作用域、关 USB 调试(可选)、`pm clear` 银行 App。
换设备 ID
```powershell
.\scripts\maribank-spoof-device.ps1 -InstallModule # 持久模块
.\scripts\maribank-spoof-device.ps1 -ClearMariBank # 一次性 resetprop + 清数据
```
---
## 3. 手机上 LSPosed 必做步骤
**每次新装或更新 Xposed 模块 APK 后:**
1. 打开 **LSPosed**
2. 找到模块 **notiMessage Xposed****启用**
3. 进入 **作用域**,勾选需要的 App见 §1 包名表)
4.**每个已勾选 App** 执行 **软重启**(或整机重启)
未软重启时 Hook **不会** 注入目标 App。
---
## 4. 日常使用
### 4.1 菲律宾 SeaBank 注册(当前可 OTP
1. 确认 LSPosed 已启用且作用域含 `ph.seabank.seabank`
2. (建议)`pm clear ph.seabank.seabank` 或跑 `maribank-scheme-b-finish.ps1`
3. 打开 App → **Sign up** → 输入菲律宾手机号 → **Next**
4. 等待 OTP 短信
> **说明**:实测 **USB 调试可保持开启** 仍能 OTP因 Xposed 对 App 伪装 adb 状态(详见 [`MariBank风控与载荷说明.md` §4](MariBank风控与载荷说明.md))。测 SG 或求稳时仍建议关调试。
### 4.2 新加坡 MariBank
流程同上,包名为 `sg.com.maribankmobile.digitalbank`。当前仍可能 **3100012**,见 [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)。
### 4.3 notiMessage 抓 Telegram
详见 **[`Telegram抓消息说明.md`](Telegram抓消息说明.md)**(安装、作用域、双通道、排错)。
简要步骤:
1. 打开 **notiMessage****监听设置** 添加 Telegram → **开始监听** → 授予通知使用权
2. LSPosed 作用域勾选 Telegram + 主 App → **软重启 Telegram**
3. PC 调试台(可选):`.\scripts\start-debug-server.ps1`
---
## 5. 抓 logPC
```powershell
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
& $adb logcat -c
# 手机操作Sign up → Next
& $adb logcat -d | Select-String "MariBankEncrypt in0 byte\[1|MariBankRoot HTTP.*register|3100012|4067012|OTP_SMS"
```
验证伪装 ID
```powershell
& $adb shell su -c "getprop ro.serialno; settings get secure android_id"
```
---
## 6. 换机 / 新机迁移
1. 新机 Root + Magisk + Zygisk + LSPosed + Shamiko
2. PC 执行 `install-full.ps1`
3. LSPosed 启用模块 + 作用域 + 软重启
4. 安装 `maribank-device-spoof` → 重启 → `maribank-scheme-b-finish.ps1`
5. DenyList 重新勾选银行 App
---
## 7. 常见问题
| 现象 | 处理 |
|------|------|
| 银行 App 白屏 | LSPosed 软重启;勿改 Hook 加载时机(见实现说明) |
| Root / ADB 弹窗 | 确认模块启用 + 作用域已勾银行 App |
| adb 连不上 | 开发者选项重新开 **USB 调试**,解锁点允许 |
| 菲律宾又 4067012 | `pm clear ph.seabank.seabank`,必要时重跑 spoof |
| Hook 不生效 | LSPosed **软重启**目标 App不是只杀进程 |
| 收尾脚本卡住 | adb 断开;先开 USB 调试连 PC跑完脚本再关 |
---
## 8. 最短路径速查
```
PC: install-full.ps1
手机: LSPosed 启用 → 勾选 App → 软重启
银行: maribank-scheme-b-finish.ps1 → Sign up → Next
抓包: logcat -c → Next → logcat -d | Select-String register
```
---
*最后更新2026-07-06*

114
docs/更新说明.md Normal file
View File

@@ -0,0 +1,114 @@
# 更新说明
## MariBank 风控 bypass2026-07-06
- **菲律宾 SeaBank**`ph.seabank.seabank`Root Pixel 6 上 **注册成功、可发 OTP**`api.seabank.ph``code=0`
- **新加坡 MariBank**`sg.com.maribankmobile.digitalbank`):仍 **3100012**
- 新增 / 更新文档:
- [`docs/MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md) — 突破记录与 PH/SG 对比
- [`docs/MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md) — register 字段、afExtInfo、USB 调试、真实原因
- [`docs/手机操作手册.md`](手机操作手册.md) — Root 机部署与日常操作
- [`docs/Hook指南.md`](Hook指南.md) — 新增 §0 Xposed 与 LSPosed
- 脚本:`scripts/maribank-spoof-device.ps1``scripts/maribank-scheme-b-finish.ps1``scripts/magisk/maribank-device-spoof/`
- 文档文件名已全部改为中文(见 `docs/` 目录)
---
## 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`
### 文档
- Telegram 专文:`docs/Telegram抓消息说明.md`
- 架构与扩展:`docs/Hook指南.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 模块无需重装。

134
docs/银行逆向.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
```

View File

@@ -0,0 +1,11 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := tng_exit_guard
LOCAL_SRC_FILES := main.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_LDLIBS := -llog -ldl
LOCAL_CFLAGS := -Wall -Wextra -fno-rtti -fvisibility=hidden
LOCAL_CPPFLAGS := -std=c++17
LOCAL_LDFLAGS := -Wl,--exclude-libs,ALL
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,4 @@
APP_ABI := arm64-v8a
APP_PLATFORM := android-24
APP_STL := c++_static
APP_CPPFLAGS := -std=c++17

View File

@@ -0,0 +1,540 @@
/*
* TNG eWallet — Zygisk companion.
*
* stable: PLT + ABRT/SIGTRAP swallow + exit_group seccomp@400ms.
* Promon worker SIGSEGV (libtngdigital_ewallet.so null deref): LR-return skip (cap N).
* pc==lr 循环 SEGV 也 skiplibc++abi __cxa_guard_acquire → SIGABRT 吞掉。
*/
#include <android/log.h>
#include <errno.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <pthread.h>
#include <signal.h>
#include <stddef.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <ucontext.h>
#include <unistd.h>
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include "zygisk.hpp"
#define SC_RET_ALLOW 0x7fff0000U
#define SC_RET_ERRNO_EPERM (0x00050000U | 1U)
#define LOG_TAG "TngExitGuard"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
static constexpr const char *kTargetPkg = "my.com.tngdigital.ewallet";
static constexpr const char *kPromonSo = "libtngdigital_ewallet.so";
static bool g_enabled = false;
static zygisk::Api *g_api = nullptr;
static std::atomic<int> g_cxx_plt{0};
static std::atomic<int> g_seccomp_ok{0};
static std::atomic<int> g_stack_chk{0};
static std::atomic<int> g_promon_segv{0};
static std::atomic<uintptr_t> g_promon_start{0};
static std::atomic<uintptr_t> g_promon_end{0};
static std::atomic<pid_t> g_main_tid{0};
static std::atomic<int> g_abrt_swallow{0};
static std::atomic<pid_t> g_abrt_last_tid{0};
static std::atomic<uintptr_t> g_abrt_last_pc{0};
static std::atomic<int> g_abrt_streak{0};
/* 隔离进程 :goacqowmmt 会循环 SEGV主进程 worker 也狂刷。cap 后 freeze 该线程 */
static constexpr int kMaxPromonSegvSkip = 200;
static constexpr int kMaxAbrtStreak = 3;
static std::atomic<int> g_soft_sig_logged{0};
static void freeze_forever() {
for (;;) pause();
}
static void refresh_promon_so_range() {
FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) return;
char line[1024];
uintptr_t start = 0, end = 0;
while (fgets(line, sizeof(line), fp)) {
unsigned long s = 0, e = 0;
char path[512] = {};
int n = sscanf(line, "%lx-%lx %*s %*s %*s %*s %511[^\n]", &s, &e, path);
if (n < 3) continue;
char *p = path;
while (*p == ' ') ++p;
if (strstr(p, kPromonSo) == nullptr) continue;
if (start == 0 || s < start) start = s;
if (e > end) end = e;
}
fclose(fp);
if (start != 0 && end > start) {
g_promon_start.store(start);
g_promon_end.store(end);
}
}
static bool pc_in_promon_so(uintptr_t pc) {
uintptr_t start = g_promon_start.load();
uintptr_t end = g_promon_end.load();
return start != 0 && pc >= start && pc < end;
}
static void promon_segv_handler(int sig, siginfo_t *info, void *ctx) {
(void)sig;
(void)info;
ucontext_t *uc = reinterpret_cast<ucontext_t *>(ctx);
#if defined(__aarch64__)
uintptr_t pc = uc->uc_mcontext.pc;
uintptr_t lr = uc->uc_mcontext.regs[30];
#else
uintptr_t pc = 0;
uintptr_t lr = 0;
#endif
refresh_promon_so_range();
/* Promon 典型 pc==lr 自旋 null derefmaps 尚未刷新时也按此 skip */
if (pc != 0 && pc == lr) {
int n = ++g_promon_segv;
if (n <= 3 || n % 50 == 0) {
LOGI("promon pc==lr SIGSEGV tid=%d pc=%lx n=%d — skip to pc+4",
(int)gettid(), (unsigned long)pc, n);
}
uc->uc_mcontext.pc = pc + 4;
return;
}
if (pc_in_promon_so(pc) || pc_in_promon_so(lr)) {
int n = ++g_promon_segv;
uintptr_t target = lr;
if (target == 0 || target == pc) {
target = pc + 4;
}
if (kMaxPromonSegvSkip <= 0 || n <= kMaxPromonSegvSkip) {
if (n <= 3 || n % 50 == 0) {
LOGI("promon SIGSEGV tid=%d pc=%lx lr=%lx n=%d — skip to %lx",
(int)gettid(), (unsigned long)pc, (unsigned long)lr, n,
(unsigned long)target);
}
uc->uc_mcontext.pc = target;
return;
}
LOGI("promon SIGSEGV tid=%d n=%d — cap hit, freeze", (int)gettid(), n);
freeze_forever();
}
/* 1.9.10Login 后出现 Promon so 外 SEGV → 旧逻辑 re-raise 直接闪退 */
{
int n = ++g_promon_segv;
pid_t tid = gettid();
if (n <= 5 || n % 50 == 0) {
LOGI("non-promon SIGSEGV tid=%d pc=%lx lr=%lx n=%d — pc+4",
(int)tid, (unsigned long)pc, (unsigned long)lr, n);
}
if (n > 200 && tid != g_main_tid.load()) {
LOGI("non-promon SEGV storm tid=%d — freeze", (int)tid);
freeze_forever();
}
if (pc != 0) {
uc->uc_mcontext.pc = pc + 4;
return;
}
}
freeze_forever();
}
/**
* ABRT/TRAP一律 pc+4。跳远距 LR 会弄坏主线程 Looper闪退观感
* 工作线程同 PC 连 abort 超限 → freeze主线程始终 pc+4。
*/
static void fatal_skip_handler(int sig, siginfo_t *info, void *ctx) {
(void)info;
ucontext_t *uc = reinterpret_cast<ucontext_t *>(ctx);
#if defined(__aarch64__)
uintptr_t pc = uc->uc_mcontext.pc;
uintptr_t lr = uc->uc_mcontext.regs[30];
pid_t tid = gettid();
if (sig == SIGABRT) {
int streak = 1;
if (g_abrt_last_tid.load() == tid && g_abrt_last_pc.load() == pc) {
streak = g_abrt_streak.fetch_add(1) + 1;
} else {
g_abrt_last_tid.store(tid);
g_abrt_last_pc.store(pc);
g_abrt_streak.store(1);
}
if (streak > kMaxAbrtStreak && tid != g_main_tid.load()) {
LOGI("ABRT streak cap tid=%d pc=%lx n=%d — freeze worker", (int)tid,
(unsigned long)pc, streak);
freeze_forever();
}
int n = ++g_abrt_swallow;
if (n <= 5 || n % 50 == 0) {
LOGI("ABRT pc+4 tid=%d pc=%lx lr=%lx streak=%d", (int)tid,
(unsigned long)pc, (unsigned long)lr, streak);
}
uc->uc_mcontext.pc = pc + 4;
return;
}
if (sig == SIGTRAP) {
int n = ++g_abrt_swallow;
if (n <= 5 || n % 50 == 0) {
LOGI("TRAP pc+4 tid=%d pc=%lx", (int)tid, (unsigned long)pc);
}
uc->uc_mcontext.pc = pc != 0 ? pc + 4 : lr;
return;
}
#endif
freeze_forever();
}
static void install_fatal_skip_handlers() {
struct sigaction sa {};
sa.sa_sigaction = fatal_skip_handler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigemptyset(&sa.sa_mask);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGTRAP, &sa, nullptr);
LOGI("fatal skip handlers (ABRT/TRAP always pc+4)");
}
static void install_promon_segv_handler() {
refresh_promon_so_range();
struct sigaction sa {};
sa.sa_sigaction = promon_segv_handler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, nullptr) != 0) {
LOGE("SIGSEGV handler install failed errno=%d", errno);
return;
}
LOGI("promon SIGSEGV handler armed range=%lx-%lx",
(unsigned long)g_promon_start.load(),
(unsigned long)g_promon_end.load());
}
static void install_soft_signals() {
struct sigaction sa {};
sa.sa_sigaction = fatal_skip_handler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigemptyset(&sa.sa_mask);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGTRAP, &sa, nullptr);
if (g_soft_sig_logged.fetch_add(1) == 0) {
LOGI("soft signals (ABRT/TRAP always pc+4)");
}
}
#if defined(__aarch64__)
#define _BPFI(code, jt, jf, k) \
((struct sock_filter){(unsigned short)(code), (jt), (jf), (unsigned int)(k)})
static int install_seccomp_exit_group_only() {
struct sock_filter filter[] = {
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, arch)),
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 1, 0, AUDIT_ARCH_AARCH64),
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW),
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, nr)),
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 94),
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM),
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
long rc = syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_TSYNC, &prog);
if (rc != 0) {
rc = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
if (rc != 0) {
LOGE("seccomp failed errno=%d", errno);
return -1;
}
LOGI("seccomp exit_group via prctl");
} else {
LOGI("seccomp exit_group via TSYNC");
}
g_seccomp_ok.store(1);
return 0;
}
#else
static int install_seccomp_exit_group_only() { return -1; }
#endif
using exit_fn = void (*)(int);
using kill_fn = int (*)(pid_t, int);
using tgkill_fn = int (*)(int, int, int);
using raise_fn = int (*)(int);
using pthread_kill_fn = int (*)(pthread_t, int);
using cxa_guard_acquire_fn = int (*)(void *);
using cxa_guard_abort_fn = void (*)();
using dlopen_fn = void *(*)(const char *, int);
static cxa_guard_acquire_fn orig_cxa_guard_acquire = nullptr;
static cxa_guard_abort_fn orig_cxa_guard_abort = nullptr;
static dlopen_fn orig_dlopen = nullptr;
static exit_fn orig_exit = nullptr;
static exit_fn orig__exit = nullptr;
static void (*orig_abort)() = nullptr;
static void (*orig_stack_chk_fail)() = nullptr;
static kill_fn orig_kill = nullptr;
static tgkill_fn orig_tgkill = nullptr;
static raise_fn orig_raise = nullptr;
static pthread_kill_fn orig_pthread_kill = nullptr;
static bool deadly(int sig) {
return sig == SIGKILL || sig == SIGABRT || sig == SIGTERM ||
sig == SIGTRAP || sig == SIGILL ||
sig == 9 || sig == 6 || sig == 5 || sig == 4 || sig == 15;
}
static void hooked_exit(int code) { LOGI("blocked exit(%d)", code); }
static void hooked__exit(int code) { LOGI("blocked _exit(%d)", code); }
static void hooked_abort() {
LOGI("blocked abort() tid=%d", (int)gettid());
}
static void hooked_stack_chk_fail() {
int n = ++g_stack_chk;
LOGI("blocked __stack_chk_fail tid=%d n=%d", (int)gettid(), n);
}
static int hooked_raise(int sig) {
if (deadly(sig)) {
LOGI("blocked raise(%d)", sig);
return 0;
}
return orig_raise ? orig_raise(sig) : -1;
}
static int hooked_kill(pid_t pid, int sig) {
if (deadly(sig)) {
LOGI("blocked kill(%d,%d)", (int)pid, sig);
return 0;
}
return orig_kill ? orig_kill(pid, sig) : -1;
}
static int hooked_tgkill(int tgid, int tid, int sig) {
if (deadly(sig)) {
LOGI("blocked tgkill(%d,%d,%d)", tgid, tid, sig);
return 0;
}
return orig_tgkill ? orig_tgkill(tgid, tid, sig) : -1;
}
static int hooked_pthread_kill(pthread_t thread, int sig) {
if (deadly(sig)) {
LOGI("blocked pthread_kill(sig=%d)", sig);
return 0;
}
return orig_pthread_kill ? orig_pthread_kill(thread, sig) : -1;
}
/** Promon/libc++ 静态局部量递归初始化会 abort 主进程Registration 页 HWUI 线程)。 */
static int hooked_cxa_guard_acquire(void *guard) {
(void)guard;
return 1;
}
static void hooked_cxa_guard_abort() {
LOGI("blocked __cxa_guard_abort tid=%d", (int)gettid());
}
static void *hooked_dlopen(const char *name, int flags) {
void *handle = orig_dlopen ? orig_dlopen(name, flags) : nullptr;
if (handle != nullptr || name == nullptr) {
return handle;
}
if (strstr(name, "libandroid.so") != nullptr) {
static const char *kFallbacks[] = {
"/system/lib64/libandroid.so",
"/system/lib/libandroid.so",
"libandroid.so",
};
for (const char *path : kFallbacks) {
handle = orig_dlopen ? orig_dlopen(path, flags) : nullptr;
if (handle != nullptr) {
LOGI("dlopen fallback %s -> %p (from %s)", path, handle, name);
return handle;
}
}
LOGI("dlopen libandroid.so failed tid=%d", (int)gettid());
}
return handle;
}
static bool find_lib_match(const char *suffix, const char *contains,
dev_t *dev, ino_t *ino) {
FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) return false;
char line[1024];
bool ok = false;
while (fgets(line, sizeof(line), fp)) {
uintptr_t start = 0, end = 0;
char perms[8] = {};
unsigned long long offset = 0;
char deststr[32] = {};
unsigned long inode = 0;
char path[512] = {};
int n = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]",
&start, &end, perms, &offset, &deststr, &inode, path);
if (n < 7 || inode == 0) continue;
char *p = path;
while (*p == ' ') ++p;
bool match = false;
if (suffix != nullptr) {
size_t plen = strlen(p);
size_t slen = strlen(suffix);
match = plen >= slen && strcmp(p + plen - slen, suffix) == 0;
} else if (contains != nullptr) {
match = strstr(p, contains) != nullptr;
}
if (!match) continue;
unsigned maj = 0, min = 0;
if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue;
*dev = makedev(maj, min);
*ino = inode;
ok = true;
break;
}
fclose(fp);
return ok;
}
static bool find_lib_by_suffix(const char *suffix, dev_t *dev, ino_t *ino) {
return find_lib_match(suffix, nullptr, dev, ino);
}
static bool find_lib_contains(const char *needle, dev_t *dev, ino_t *ino) {
return find_lib_match(nullptr, needle, dev, ino);
}
static bool find_libc(dev_t *dev, ino_t *ino) {
return find_lib_by_suffix("libc.so", dev, ino);
}
static void register_plt(zygisk::Api *api, dev_t dev, ino_t ino,
const char *sym, void *hook, void **orig) {
if (!api || dev == 0 || ino == 0) return;
api->pltHookRegister(dev, ino, sym, hook, orig);
}
static void install_plt(zygisk::Api *api) {
if (!api) return;
dev_t dev = 0;
ino_t ino = 0;
if (find_libc(&dev, &ino)) {
register_plt(api, dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit);
register_plt(api, dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit);
register_plt(api, dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort);
register_plt(api, dev, ino, "__stack_chk_fail",
(void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail);
register_plt(api, dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise);
register_plt(api, dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill);
register_plt(api, dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill);
register_plt(api, dev, ino, "pthread_kill",
(void *)hooked_pthread_kill, (void **)&orig_pthread_kill);
register_plt(api, dev, ino, "dlopen", (void *)hooked_dlopen, (void **)&orig_dlopen);
}
bool ok = api->pltHookCommit();
LOGI("PLT commit=%d", ok ? 1 : 0);
}
static void try_install_cxx_guard_plt() {
if (g_cxx_plt.load() || !g_api) return;
dev_t dev = 0;
ino_t ino = 0;
bool any = false;
if (find_lib_by_suffix("libc++_shared.so", &dev, &ino)
|| find_lib_contains("libc++", &dev, &ino)) {
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(g_api, dev, ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
any = true;
}
if (find_lib_by_suffix("libtngdigital_ewallet.so", &dev, &ino)) {
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(g_api, dev, ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
any = true;
}
if (!any) return;
if (g_api->pltHookCommit()) {
g_cxx_plt.store(1);
LOGI("PLT cxx guards committed");
}
}
static void *phase_thread(void *) {
install_promon_segv_handler();
usleep(400 * 1000);
install_seccomp_exit_group_only();
LOGI("seccomp armed @400ms ok=%d", g_seccomp_ok.load());
for (int i = 0; i < 40; i++) {
usleep(1000 * 1000);
install_soft_signals();
install_promon_segv_handler();
try_install_cxx_guard_plt();
if (i % 5 == 0) refresh_promon_so_range();
}
LOGI("phase done seccomp=%d stack_chk=%d promon_segv=%d",
g_seccomp_ok.load(), g_stack_chk.load(), g_promon_segv.load());
return nullptr;
}
static void install_all(zygisk::Api *api) {
g_api = api;
g_main_tid.store(gettid());
LOGI("install pid=%d main_tid=%d (PLT+cxx-guard+ABRT-pc+4+SEGV-skip+exit_group@400ms)",
getpid(), (int)g_main_tid.load());
install_fatal_skip_handlers();
install_soft_signals();
install_plt(api);
pthread_t th;
if (pthread_create(&th, nullptr, phase_thread, nullptr) == 0) {
pthread_detach(th);
}
LOGI("ready");
}
class TngExitGuardModule : public zygisk::ModuleBase {
public:
void onLoad(zygisk::Api *api, JNIEnv *env) override {
this->api = api;
this->env = env;
}
void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
const char *nice = nullptr;
if (args->nice_name) {
nice = env->GetStringUTFChars(args->nice_name, nullptr);
}
bool match = nice && (
std::strncmp(nice, kTargetPkg, std::strlen(kTargetPkg)) == 0);
if (nice) env->ReleaseStringUTFChars(args->nice_name, nice);
g_enabled = match;
if (!match) {
api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
return;
}
LOGI("preAppSpecialize matched");
}
void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override {
(void)args;
if (!g_enabled) return;
install_all(api);
}
private:
zygisk::Api *api = nullptr;
JNIEnv *env = nullptr;
};
REGISTER_ZYGISK_MODULE(TngExitGuardModule)

View File

@@ -0,0 +1,391 @@
/* Copyright 2022-2023 John "topjohnwu" Wu
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
// This is the public API for Zygisk modules.
// DO NOT MODIFY ANY CODE IN THIS HEADER.
#pragma once
#include <jni.h>
#define ZYGISK_API_VERSION 4
/*
***************
* Introduction
***************
On Android, all app processes are forked from a special daemon called "Zygote".
For each new app process, zygote will fork a new process and perform "specialization".
This specialization operation enforces the Android security sandbox on the newly forked
process to make sure that 3rd party application code is only loaded after it is being
restricted within a sandbox.
On Android, there is also this special process called "system_server". This single
process hosts a significant portion of system services, which controls how the
Android operating system and apps interact with each other.
The Zygisk framework provides a way to allow developers to build modules and run custom
code before and after system_server and any app processes' specialization.
This enable developers to inject code and alter the behavior of system_server and app processes.
Please note that modules will only be loaded after zygote has forked the child process.
THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON!
*********************
* Development Guide
*********************
Define a class and inherit zygisk::ModuleBase to implement the functionality of your module.
Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk.
Example code:
static jint (*orig_logger_entry_max)(JNIEnv *env);
static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); }
class ExampleModule : public zygisk::ModuleBase {
public:
void onLoad(zygisk::Api *api, JNIEnv *env) override {
this->api = api;
this->env = env;
}
void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
JNINativeMethod methods[] = {
{ "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max },
};
api->hookJniNativeMethods(env, "android/util/Log", methods, 1);
*(void **) &orig_logger_entry_max = methods[0].fnPtr;
}
private:
zygisk::Api *api;
JNIEnv *env;
};
REGISTER_ZYGISK_MODULE(ExampleModule)
-----------------------------------------------------------------------------------------
Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize,
or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class
never runs in a true superuser environment.
If your module require access to superuser permissions, you can create and register
a root companion handler function. This function runs in a separate root companion
daemon process, and an Unix domain socket is provided to allow you to perform IPC between
your target process and the root companion process.
Example code:
static void example_handler(int socket) { ... }
REGISTER_ZYGISK_COMPANION(example_handler)
*/
namespace zygisk {
struct Api;
struct AppSpecializeArgs;
struct ServerSpecializeArgs;
class ModuleBase {
public:
// This method is called as soon as the module is loaded into the target process.
// A Zygisk API handle will be passed as an argument.
virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {}
// This method is called before the app process is specialized.
// At this point, the process just got forked from zygote, but no app specific specialization
// is applied. This means that the process does not have any sandbox restrictions and
// still runs with the same privilege of zygote.
//
// All the arguments that will be sent and used for app specialization is passed as a single
// AppSpecializeArgs object. You can read and overwrite these arguments to change how the app
// process will be specialized.
//
// If you need to run some operations as superuser, you can call Api::connectCompanion() to
// get a socket to do IPC calls with a root companion process.
// See Api::connectCompanion() for more info.
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {}
// This method is called after the app process is specialized.
// At this point, the process has all sandbox restrictions enabled for this application.
// This means that this method runs with the same privilege of the app's own code.
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {}
// This method is called before the system server process is specialized.
// See preAppSpecialize(args) for more info.
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {}
// This method is called after the system server process is specialized.
// At this point, the process runs with the privilege of system_server.
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {}
};
struct AppSpecializeArgs {
// Required arguments. These arguments are guaranteed to exist on all Android versions.
jint &uid;
jint &gid;
jintArray &gids;
jint &runtime_flags;
jobjectArray &rlimits;
jint &mount_external;
jstring &se_info;
jstring &nice_name;
jstring &instruction_set;
jstring &app_data_dir;
// Optional arguments. Please check whether the pointer is null before de-referencing
jintArray *const fds_to_ignore;
jboolean *const is_child_zygote;
jboolean *const is_top_app;
jobjectArray *const pkg_data_info_list;
jobjectArray *const whitelisted_data_info_list;
jboolean *const mount_data_dirs;
jboolean *const mount_storage_dirs;
AppSpecializeArgs() = delete;
};
struct ServerSpecializeArgs {
jint &uid;
jint &gid;
jintArray &gids;
jint &runtime_flags;
jlong &permitted_capabilities;
jlong &effective_capabilities;
ServerSpecializeArgs() = delete;
};
namespace internal {
struct api_table;
template <class T> void entry_impl(api_table *, JNIEnv *);
}
// These values are used in Api::setOption(Option)
enum Option : int {
// Force Magisk's denylist unmount routines to run on this process.
//
// Setting this option only makes sense in preAppSpecialize.
// The actual unmounting happens during app process specialization.
//
// Set this option to force all Magisk and modules' files to be unmounted from the
// mount namespace of the process, regardless of the denylist enforcement status.
FORCE_DENYLIST_UNMOUNT = 0,
// When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize.
// Be aware that after dlclose-ing your module, all of your code will be unmapped from memory.
// YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS.
DLCLOSE_MODULE_LIBRARY = 1,
};
// Bit masks of the return value of Api::getFlags()
enum StateFlag : uint32_t {
// The user has granted root access to the current process
PROCESS_GRANTED_ROOT = (1u << 0),
// The current process was added on the denylist
PROCESS_ON_DENYLIST = (1u << 1),
};
// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded
// from the specialized process afterwards.
struct Api {
// Connect to a root companion process and get a Unix domain socket for IPC.
//
// This API only works in the pre[XXX]Specialize methods due to SELinux restrictions.
//
// The pre[XXX]Specialize methods run with the same privilege of zygote.
// If you would like to do some operations with superuser permissions, register a handler
// function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func).
// Another good use case for a companion process is that if you want to share some resources
// across multiple processes, hold the resources in the companion process and pass it over.
//
// The root companion process is ABI aware; that is, when calling this method from a 32-bit
// process, you will be connected to a 32-bit companion process, and vice versa for 64-bit.
//
// Returns a file descriptor to a socket that is connected to the socket passed to your
// module's companion request handler. Returns -1 if the connection attempt failed.
int connectCompanion();
// Get the file descriptor of the root folder of the current module.
//
// This API only works in the pre[XXX]Specialize methods.
// Accessing the directory returned is only possible in the pre[XXX]Specialize methods
// or in the root companion process (assuming that you sent the fd over the socket).
// Both restrictions are due to SELinux and UID.
//
// Returns -1 if errors occurred.
int getModuleDir();
// Set various options for your module.
// Please note that this method accepts one single option at a time.
// Check zygisk::Option for the full list of options available.
void setOption(Option opt);
// Get information about the current process.
// Returns bitwise-or'd zygisk::StateFlag values.
uint32_t getFlags();
// Exempt the provided file descriptor from being automatically closed.
//
// This API only make sense in preAppSpecialize; calling this method in any other situation
// is either a no-op (returns true) or an error (returns false).
//
// When false is returned, the provided file descriptor will eventually be closed by zygote.
bool exemptFd(int fd);
// Hook JNI native methods for a class
//
// Lookup all registered JNI native methods and replace it with your own methods.
// The original function pointer will be saved in each JNINativeMethod's fnPtr.
// If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr
// will be set to nullptr.
void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods);
// Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory.
//
// Parsing /proc/[PID]/maps will give you the memory map of a process. As an example:
//
// <address> <perms> <offset> <dev> <inode> <pathname>
// 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64
// (More details: https://man7.org/linux/man-pages/man5/proc.5.html)
//
// The `dev` and `inode` pair uniquely identifies a file being mapped into memory.
// For matching ELFs loaded in memory, replace function `symbol` with `newFunc`.
// If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`.
void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc);
// Commit all the hooks that was previously registered.
// Returns false if an error occurred.
bool pltHookCommit();
private:
internal::api_table *tbl;
template <class T> friend void internal::entry_impl(internal::api_table *, JNIEnv *);
};
// Register a class as a Zygisk module
#define REGISTER_ZYGISK_MODULE(clazz) \
void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \
zygisk::internal::entry_impl<clazz>(table, env); \
}
// Register a root companion request handler function for your module
//
// The function runs in a superuser daemon process and handles a root companion request from
// your module running in a target process. The function has to accept an integer value,
// which is a Unix domain socket that is connected to the target process.
// See Api::connectCompanion() for more info.
//
// NOTE: the function can run concurrently on multiple threads.
// Be aware of race conditions if you have globally shared resources.
#define REGISTER_ZYGISK_COMPANION(func) \
void zygisk_companion_entry(int client) { func(client); }
/*********************************************************
* The following is internal ABI implementation detail.
* You do not have to understand what it is doing.
*********************************************************/
namespace internal {
struct module_abi {
long api_version;
ModuleBase *impl;
void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *);
void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *);
void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *);
void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *);
module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) {
preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); };
postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); };
preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); };
postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); };
}
};
struct api_table {
// Base
void *impl;
bool (*registerModule)(api_table *, module_abi *);
void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int);
void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **);
bool (*exemptFd)(int);
bool (*pltHookCommit)();
int (*connectCompanion)(void * /* impl */);
void (*setOption)(void * /* impl */, Option);
int (*getModuleDir)(void * /* impl */);
uint32_t (*getFlags)(void * /* impl */);
};
template <class T>
void entry_impl(api_table *table, JNIEnv *env) {
static Api api;
api.tbl = table;
static T module;
ModuleBase *m = &module;
static module_abi abi(m);
if (!table->registerModule(table, &abi)) return;
m->onLoad(&api, env);
}
} // namespace internal
inline int Api::connectCompanion() {
return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1;
}
inline int Api::getModuleDir() {
return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1;
}
inline void Api::setOption(Option opt) {
if (tbl->setOption) tbl->setOption(tbl->impl, opt);
}
inline uint32_t Api::getFlags() {
return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0;
}
inline bool Api::exemptFd(int fd) {
return tbl->exemptFd != nullptr && tbl->exemptFd(fd);
}
inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) {
if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods);
}
inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) {
if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc);
}
inline bool Api::pltHookCommit() {
return tbl->pltHookCommit != nullptr && tbl->pltHookCommit();
}
} // namespace zygisk
extern "C" {
[[gnu::visibility("default"), maybe_unused]]
void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *);
[[gnu::visibility("default"), maybe_unused]]
void zygisk_companion_entry(int);
} // extern "C"

View File

@@ -0,0 +1,6 @@
id=tng_exit_guard
name=TNG Exit Guard
version=v1.0
versionCode=1
author=miraclegarden
description=Zygisk: block Promon native exit_group for my.com.tngdigital.ewallet. Pair with notiMessage Xposed TngRoot hooks. Do NOT put TNG on Magisk DenyList.

Binary file not shown.

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

View File

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,14 @@
import zipfile
import re
z = zipfile.ZipFile(r"reverse/dumps/tng_1.9.10_base.apk")
names = [n for n in z.namelist() if n.endswith(".dex")]
pat = re.compile(rb"Lvhvlnqgy/([^;\s]{1,80});")
found = set()
for n in names:
data = z.read(n)
for m in pat.findall(data):
found.add(m.decode("ascii", errors="ignore"))
print("vhvlnqgy classes", len(found))
for c in sorted(found):
print(f"vhvlnqgy.{c.replace('/', '.')}")

View File

@@ -0,0 +1,24 @@
"""Dump vhvlnqgy bl/R/bd method refs from TNG 1.9.10 dex."""
import re
import zipfile
APK = r"reverse/dumps/tng_1.9.10_base.apk"
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
for cls in ["Lvhvlnqgy/bl;", "Lvhvlnqgy/R;", "Lvhvlnqgy/bd;", "Lvhvlnqgy/w;", "Lvhvlnqgy/W;", "Lvhvlnqgy/a;"]:
print("===", cls, "===")
refs = sorted(set(re.findall(cls.encode() + rb"->[^\x00]{1,80}", data)))
for r in refs[:30]:
print(r.decode("ascii", "ignore"))
print()
print("=== R callers (who invokes R.a/R.b) ===")
for m in sorted(set(re.findall(rb"Lvhvlnqgy/[^;]+;->[a-zA-Z]+[^\x00]{0,40}Lvhvlnqgy/R;", data))):
print(m.decode("ascii", "ignore"))
print("\n=== bd throw sites ===")
for m in sorted(set(re.findall(rb"[^\x00]{0,40}Lvhvlnqgy/bd;", data))):
s = m.decode("ascii", "ignore")
if "vhvlnqgy" in s:
print(s)

View File

@@ -0,0 +1,15 @@
import re
import zipfile
APK = r"reverse/dumps/tng_1.9.10_base.apk"
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
for cls in ["Lvhvlnqgy/w;", "Lvhvlnqgy/u;"]:
print("===", cls, "refs ===")
pat = cls.encode() + rb"[^\x00]{0,120}"
hits = sorted(set(re.findall(pat, data)))
for h in hits[:40]:
print(h.decode("ascii", "ignore"))
print("count", len(hits))
print()

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,38 @@
# -*- coding: utf-8 -*-
import subprocess
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
TARGETS = [
"Lcom/shopee/bke/lib/safemode/model/ErrorType;",
"Lcom/shopee/bke/lib/safemode/activity/SafeModeRecoverActivity;",
"Lcom/shopee/bke/lib/safemode/b;",
]
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
if not any(t.replace("L", "").replace(";", "").encode() in data for t in TARGETS):
continue
tmp = Path(__file__).resolve().parent.parent / "tmp" / "sg_safemode.dex"
tmp.parent.mkdir(parents=True, exist_ok=True)
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 "const-string" in line or "ErrorType" in line):
print(line.strip()[:140])

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,28 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
print("=== Adb / SafeMode related classes ===")
for m in re.finditer(rb"L[\w$/]*(Adb|ADB|SafeMode|safemode|Recover)[\w$/]*;", data):
c = m.group().decode()
if "shopee" in c or "bke" in c or "maribank" in c.lower():
print(c)
print("\n=== shpssdk bank ===")
for m in re.finditer(rb"Lcom/shopee/shpssdk[\w$/]*;", data):
print(m.group().decode())
print("\n=== api.seabank / maribank hosts ===")
for m in re.finditer(rb"https?://[a-zA-Z0-9._/-]{8,80}", data):
u = m.group().decode()
if "maribank" in u or "seabank" in u:
print(u)
print("\n=== login/register uapi ===")
for m in re.finditer(rb"/uapi/[a-zA-Z0-9_/-]+", data):
print(m.group().decode())

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_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 pat in [
b"Does not support",
b"Turn Off ADB",
b"Wireless ADB",
b"ADB/Wireless",
b"support root",
b"RISK_USB_ADB",
b"RISK_WIFI_ADB",
b"KEY_ALLOW_ADB",
b"SafeModeRecover",
b"sg.com.maribank",
]:
print(pat.decode(), "->", data.count(pat))
print("\n--- UI strings ---")
for m in re.finditer(rb"[\x20-\x7e]{10,200}", data):
s = m.group().decode("ascii", "ignore")
sl = s.lower()
if ("adb" in sl and ("detect" in sl or "turn" in sl or "wireless" in sl or "debug" in sl)) or "does not support root" in sl:
print(s)
print("\n--- Application ---")
for m in re.finditer(rb"Lcom/shopee/bke/[\w$/]*Application[\w$/]*;", data):
print(m.group().decode())
print("\n--- ADB risk classes ---")
for m in re.finditer(rb"L[\w$/]*(adb|Adb|ADB)[\w$/]*;", data):
c = m.group().decode()
if "shopee" in c.lower() or "bke" in c.lower() or "shps" in c.lower():
print(c)

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,14 @@
import sys
path = sys.argv[1]
lines = open(path, encoding="utf-8", errors="ignore").read().splitlines()
keys = (
"Runtime aborting", "Aborting", "stack corruption", "blocked __stack",
"vhvlnqgy.u", "UserRegistration", "F libc", "has died",
)
for i, line in enumerate(lines):
if any(k in line for k in keys):
start = max(0, i - 2)
end = min(len(lines), i + 6)
print("---")
print("\n".join(lines[start:end]))

View File

@@ -0,0 +1,20 @@
import sys
import xml.etree.ElementTree as ET
path = sys.argv[1] if len(sys.argv) > 1 else "reverse/dumps/ui_login.xml"
root = ET.parse(path).getroot()
keywords = [
"register", "sign", "login", "mobile", "phone", "continue", "next",
"create", "otp", "skip", "started", "email", "password", "pin",
]
for node in root.iter("node"):
text = node.get("text", "")
desc = node.get("content-desc", "")
clickable = node.get("clickable", "")
bounds = node.get("bounds", "")
label = (text or desc).strip()
if not label and clickable != "true":
continue
hay = (text + " " + desc).lower()
if clickable == "true" or any(k in hay for k in keywords):
print(f"{label!r} bounds={bounds} clickable={clickable}")

View File

@@ -0,0 +1,13 @@
import sys
import xml.etree.ElementTree as ET
path = sys.argv[1]
root = ET.parse(path).getroot()
for node in root.iter("node"):
text = (node.get("text") or "").strip()
desc = (node.get("content-desc") or "").strip()
rid = node.get("resource-id") or ""
bounds = node.get("bounds") or ""
clickable = node.get("clickable") or "false"
if text or desc or "login" in rid.lower() or "register" in rid.lower():
print(f"text={text!r} desc={desc!r} id={rid} bounds={bounds} click={clickable}")

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,82 @@
# -*- coding: utf-8 -*-
"""Quick DEX scan for MariBank SG detection / auth strings."""
import re
import sys
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
if len(sys.argv) > 1:
APK = Path(sys.argv[1])
needles = [
b"ADB",
b"Wireless ADB",
b"USB debugging",
b"Wireless debugging",
b"Does not support root",
b"root device",
b"rooted",
b"jailbroken",
b"safemode",
b"SafeMode",
b"shpssdk",
b"SHPSSDK",
b"getRiskToken",
b"requestDefense",
b"BkeApplication",
b"errorcodehandler",
b"4067012",
b"4067004",
b"/uapi/",
b"/dfp/",
b"register",
b"login",
b"auth/precheck",
]
class_needles = [
rb"Lcom/shopee/bke/[\w$/]+;",
rb"Lcom/shopee/shpssdk[\w$/]*;",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
print("APK:", APK.name, "dex bytes:", len(data))
print("\n=== string hits ===")
for n in needles:
c = data.count(n)
if c:
print(f" {n.decode(errors='replace')!r}: {c}")
print("\n=== api paths (sample) ===")
paths = sorted(set(m.group().decode() for m in re.finditer(rb"/v[0-9]/[a-zA-Z0-9_/-]{4,80}", data)))
for p in paths:
pl = p.lower()
if any(k in pl for k in ("auth", "login", "register", "otp", "dfp", "user", "mobile", "pin")):
print(" ", p)
print("\n=== shopee/bke classes (sample) ===")
classes = sorted(set(m.group().decode() for m in re.finditer(rb"Lcom/shopee/bke/[\w$/]{8,120};", data)))
keywords = ("safemode", "risk", "adb", "debug", "root", "error", "user", "digitalbank", "Application")
shown = 0
for c in classes:
cl = c.lower()
if any(k in cl for k in keywords):
print(" ", c)
shown += 1
if shown >= 40:
break
print(f" ... total bke classes: {len(classes)}")
print("\n=== context: ADB Detected ===")
idx = data.find(b"ADB")
while idx >= 0 and idx < len(data):
chunk = data[max(0, idx - 30) : idx + 80]
if b"Detect" in chunk or b"debug" in chunk.lower() or b"Wireless" in chunk:
s = re.sub(rb"[^\x20-\x7e]+", b" ", chunk).decode("ascii", "ignore").strip()
if len(s) > 20:
print(" ", s[:120])
idx = data.find(b"ADB", idx + 1)
if idx > 0 and data.find(b"ADB", idx + 1) == -1:
break

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
ADB = ["adb"]
def adb(*args):
subprocess.run(ADB + list(args), check=False)
def main():
adb("shell", "am", "force-stop", "my.com.tngdigital.ewallet")
time.sleep(2)
adb("shell", "am", "start", "-n", "my.com.tngdigital.ewallet/.ui.SplashActivity")
time.sleep(16)
adb("shell", "uiautomator", "dump", "/sdcard/ui_cc.xml")
xml = subprocess.check_output(
ADB + ["shell", "cat", "/sdcard/ui_cc.xml"], text=True, errors="ignore")
root = ET.fromstring(xml)
target = None
register_btn = None
for node in root.iter("node"):
rid = node.get("resource-id") or ""
text = node.get("text") or ""
if "tv_left" in rid or (text.strip().startswith("+") and len(text.strip()) < 8):
target = node
if "注册" in text and node.get("clickable") == "true":
register_btn = node
if target is None and register_btn is not None:
bounds = register_btn.get("bounds", "")
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
if m:
x1, y1, x2, y2 = map(int, m.groups())
x, y = (x1 + x2) // 2, (y1 + y2) // 2
print(f"tap register at {x},{y}")
adb("shell", "input", "tap", str(x), str(y))
time.sleep(4)
adb("shell", "uiautomator", "dump", "/sdcard/ui_cc.xml")
xml = subprocess.check_output(
ADB + ["shell", "cat", "/sdcard/ui_cc.xml"], text=True, errors="ignore")
root = ET.fromstring(xml)
for node in root.iter("node"):
rid = node.get("resource-id") or ""
text = node.get("text") or ""
if "tv_left" in rid or (text.strip().startswith("+") and len(text.strip()) < 8):
target = node
break
if target is None:
for node in root.iter("node"):
text = node.get("text") or ""
if "注册" in text or "Register" in text.lower():
print("login screen text:", text[:40])
print("ERROR: country code control not found")
return 1
bounds = target.get("bounds", "")
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
if not m:
print("bad bounds", bounds)
return 1
x1, y1, x2, y2 = map(int, m.groups())
x, y = (x1 + x2) // 2, (y1 + y2) // 2
print(f"tap country {target.get('text','')} at {x},{y}")
adb("shell", "input", "tap", str(x), str(y))
time.sleep(3)
adb("shell", "uiautomator", "dump", "/sdcard/ui_cc2.xml")
xml2 = subprocess.check_output(
ADB + ["shell", "cat", "/sdcard/ui_cc2.xml"], text=True, errors="ignore")
root2 = ET.fromstring(xml2)
countries = []
for node in root2.iter("node"):
text = (node.get("text") or "").strip()
if "+61" in text or "+86" in text or "Australia" in text or "Malaysia" in text:
countries.append(text)
pid = subprocess.check_output(
ADB + ["shell", "pidof", "my.com.tngdigital.ewallet"], text=True).strip()
print("pid:", pid or "DEAD")
print("countries visible:", countries[:8])
if countries:
print("OK region picker visible")
return 0
print("FAIL region picker empty/black")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Automate TNG login/register tap test and report crashes."""
import re
import subprocess
import sys
import time
PKG = "my.com.tngdigital.ewallet"
TAGS = re.compile(
r"FATAL EXCEPTION|AndroidRuntime.*Process: " + PKG
+ r"|has died|exited due to signal|vhvlnqgy\.bd|blocked killProcess|blocked System\.exit"
+ r"|UserRegistration|UserOtp|UserLogin|Displayed.*tngdigital|ACT on(Create|Resume)"
+ r"|TngExitGuard.*ABRT|TngRoot hooked 6 vhvlnqgy\.R",
re.I,
)
TAPS = [
("register_continue", 540, 959, 8),
("maybe_otp_continue", 540, 959, 6),
("back_to_login", None, None, 2),
("login_tab", 540, 1168, 5),
]
def adb(*args, timeout=30):
cmd = ["adb"] + list(args)
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
out = (r.stdout or "") + (r.stderr or "")
return r.returncode, out.strip()
def pid_alive():
code, out = adb("shell", "pidof", PKG)
return code == 0 and out.strip().split()
def top_activity():
code, out = adb("shell", "dumpsys", "activity", "activities")
if code != 0:
return "?"
for line in out.splitlines():
if "topResumedActivity=" in line:
m = re.search(r"/([^/]+)$", line.strip())
if m:
return m.group(1).rstrip("}")
return line.strip()
return "?"
def logcat_since(start):
code, out = adb("shell", "logcat", "-d", "-t", start)
hits = []
if code == 0:
for line in out.splitlines():
if TAGS.search(line):
hits.append(line)
return hits
def main():
adb("logcat", "-c")
adb("shell", "am", "force-stop", PKG)
time.sleep(1)
adb("shell", "am", "start", "-n", f"{PKG}/.ui.SplashActivity")
time.sleep(12)
print("=== after cold start ===")
print("pids:", pid_alive())
print("activity:", top_activity())
# dismiss country picker if open
adb("shell", "input", "keyevent", "KEYCODE_BACK")
time.sleep(1)
results = []
for name, x, y, wait_s in TAPS:
if x is not None:
adb("shell", "input", "tap", str(x), str(y))
print(f"\n=== tap {name} ({x},{y}) ===")
else:
adb("shell", "input", "keyevent", "KEYCODE_BACK")
print(f"\n=== {name} ===")
time.sleep(wait_s)
pids = pid_alive()
act = top_activity()
crash_buf = adb("shell", "logcat", "-d", "-b", "crash", "-t", "50")[1]
fatal = [l for l in crash_buf.splitlines() if PKG in l or "vhvlnqgy" in l]
results.append((name, pids, act, fatal))
print("pids:", pids or "DEAD")
print("activity:", act)
if fatal:
print("CRASH:", fatal[-3:])
print("\n=== summary ===")
ok = True
for name, pids, act, fatal in results:
status = "OK" if pids and not fatal else "FAIL"
if status == "FAIL":
ok = False
print(f"{status} {name}: pids={pids} activity={act} crash_lines={len(fatal)}")
hits = logcat_since("2000")
print(f"\n=== key log lines ({len(hits)}) ===")
for line in hits[-40:]:
print(line)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())

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,55 @@
# Build + install TNG Zygisk exit guard
param(
[switch]$SkipInstall,
[switch]$NoReboot
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
$Mod = Join-Path $Root "magisk-modules\tng_exit_guard"
$Ndk = "C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.4.7075529"
$NdkBuild = Join-Path $Ndk "ndk-build.cmd"
$Adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
if (-not (Test-Path $NdkBuild)) {
throw "ndk-build not found: $NdkBuild"
}
Write-Host "=== ndk-build ===" -ForegroundColor Cyan
Push-Location $Mod
& $NdkBuild NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=jni/Android.mk NDK_APPLICATION_MK=jni/Application.mk -j8
if ($LASTEXITCODE -ne 0) { Pop-Location; throw "ndk-build failed" }
$built = Join-Path $Mod "libs\arm64-v8a\libtng_exit_guard.so"
if (-not (Test-Path $built)) {
# some ndk versions omit lib prefix based on LOCAL_MODULE
$built = Get-ChildItem (Join-Path $Mod "libs\arm64-v8a") -Filter "*.so" | Select-Object -First 1 -ExpandProperty FullName
}
if (-not $built -or -not (Test-Path $built)) { Pop-Location; throw "built .so missing" }
$zygiskDir = Join-Path $Mod "zygisk"
New-Item -ItemType Directory -Force -Path $zygiskDir | Out-Null
Copy-Item $built (Join-Path $zygiskDir "arm64-v8a.so") -Force
Write-Host "built -> zygisk\arm64-v8a.so ($((Get-Item (Join-Path $zygiskDir 'arm64-v8a.so')).Length) bytes)"
Pop-Location
if ($SkipInstall) { return }
Write-Host "=== install Magisk module ===" -ForegroundColor Cyan
& $Adb wait-for-device
& $Adb shell "su -c 'mkdir -p /data/adb/modules/tng_exit_guard/zygisk'"
& $Adb push (Join-Path $Mod "module.prop") /data/local/tmp/tng_exit_guard_module.prop
& $Adb push (Join-Path $zygiskDir "arm64-v8a.so") /data/local/tmp/tng_exit_guard.so
& $Adb shell "su -c 'cp /data/local/tmp/tng_exit_guard_module.prop /data/adb/modules/tng_exit_guard/module.prop; cp /data/local/tmp/tng_exit_guard.so /data/adb/modules/tng_exit_guard/zygisk/arm64-v8a.so; chmod 644 /data/adb/modules/tng_exit_guard/module.prop; chmod 755 /data/adb/modules/tng_exit_guard/zygisk/arm64-v8a.so; rm -f /data/adb/modules/tng_exit_guard/disable /data/adb/modules/tng_exit_guard/remove; ls -la /data/adb/modules/tng_exit_guard/ /data/adb/modules/tng_exit_guard/zygisk/'"
if (-not $NoReboot) {
Write-Host "=== soft reboot zygote (module loads on next specialize) ===" -ForegroundColor Yellow
Write-Host "Magisk Zygisk modules usually need a FULL reboot. Rebooting device..."
& $Adb reboot
Write-Host "Waiting for device..."
& $Adb wait-for-device
Start-Sleep -Seconds 25
& $Adb shell "getprop sys.boot_completed"
}
Write-Host "Done. Launch TNG and check: adb logcat -s TngExitGuard:I LSPosed-Bridge:I" -ForegroundColor Green

View File

@@ -0,0 +1,36 @@
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",
"sg.com.maribankmobile.digitalbank",
"ph.seabank.seabank",
"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)

View File

@@ -0,0 +1,21 @@
#!/system/bin/sh
set -x
echo "=== magisk ver ==="
magisk -c
magisk -v
echo "=== zygisk setting ==="
magisk --sqlite 'SELECT * FROM settings'
echo "=== path ==="
MAGISK_PATH=$(magisk --path)
echo "MAGISK_PATH=$MAGISK_PATH"
ls -la "$MAGISK_PATH" 2>/dev/null | head -30
ls -la "$MAGISK_PATH/zygisk" 2>/dev/null
ls -la /data/adb/modules/
ls -la /data/adb/modules/tng_exit_guard/
ls -la /data/adb/modules/tng_exit_guard/zygisk/
echo "=== denylist tng ==="
magisk --denylist ls 2>/dev/null | grep -i tng || echo "TNG not on denylist"
echo "=== processes ==="
ps -A | grep -iE 'magiskd|zygisk|lspd|vector|tngdigital' || true
echo "=== logcat zygisk ==="
logcat -d | grep -iE 'zygisk|TngExit|tng_exit' | tail -40

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,15 @@
# Capture MariBank SG hook logs (clears buffer first if -Clear switch passed)
param([switch]$Clear)
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
if (-not (Test-Path $adb)) {
Write-Host "adb not found: $adb" -ForegroundColor Red
exit 1
}
if ($Clear) {
& $adb logcat -c
Write-Host "Logcat cleared. Now click Next in MariBank, then run without -Clear:" -ForegroundColor Yellow
Write-Host " .\scripts\logcat-maribank.ps1"
exit 0
}
& $adb logcat -d 2>&1 | Select-String -Pattern "MariBankRoot|MariBankNative" |
Select-String -Pattern "HTTP|outbound|register|faked|finish adb|blocked|ErrorFlow|RegisterViewModel|skip error|late app|late native|assessRisk|risk callback"

14
scripts/logcat-tng.ps1 Normal file
View File

@@ -0,0 +1,14 @@
# Capture TNG eWallet hook logs (clears buffer first if -Clear switch passed)
param([switch]$Clear)
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
if (-not (Test-Path $adb)) {
$adb = "adb"
}
if ($Clear) {
& $adb logcat -c
Write-Host "Logcat cleared. Launch TNG eWallet, then run without -Clear:" -ForegroundColor Yellow
Write-Host " .\scripts\logcat-tng.ps1"
exit 0
}
& $adb logcat -d 2>&1 | Select-String -Pattern "notiMessageHook/TngRoot|LSPosed-Bridge.*TngRoot|LSPosed-Bridge.*notiMessageHook|support.tngdigital|SecurityError|xwwqazamx|UserLogin|blocked intent|blocked Promon" |
Select-Object -Last 80

View File

@@ -0,0 +1,6 @@
id=maribank_device_spoof
name=MariBank Device Spoof
version=v1.0
versionCode=1
author=miraclegarden
description=Spoof serial/boot/build props for MariBank SHPSSDK fingerprint. Pair with Shamiko + DenyList for sg.com.maribankmobile.digitalbank. Generates a stable fake serial and android_id on first boot.

View File

@@ -0,0 +1,50 @@
#!/system/bin/sh
# Early boot: spoof read-only props before most apps start.
# resetprop is provided by Magisk.
MODDIR=${0%/*}
LOGTAG="maribank_device_spoof"
log() {
echo "[$LOGTAG] $*" >> /cache/maribank_device_spoof.log 2>/dev/null
echo "[$LOGTAG] $*"
}
if [ ! -f "$MODDIR/serial.txt" ]; then
# 16-char alphanumeric serial, stable across reboots
SERIAL=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-16)
[ -z "$SERIAL" ] && SERIAL="MB$(date +%s | tail -c 9)"
echo "$SERIAL" > "$MODDIR/serial.txt"
fi
SERIAL=$(cat "$MODDIR/serial.txt")
log "serial=$SERIAL"
# --- device identity (SHPSSDK / attestation often reads these) ---
resetprop -n ro.serialno "$SERIAL"
resetprop -n ro.boot.serialno "$SERIAL"
resetprop -n ro.boot.serialno "$SERIAL"
resetprop -n persist.sys.serialno "$SERIAL"
# --- hide root / debug fingerprint ---
resetprop -n ro.debuggable 0
resetprop -n ro.secure 1
resetprop -n ro.adb.secure 1
resetprop -n ro.build.type user
resetprop -n ro.build.tags release-keys
resetprop -n ro.boot.verifiedbootstate green
resetprop -n ro.boot.flash.locked 1
resetprop -n ro.boot.vbmeta.device_state locked
resetprop -n vendor.boot.vbmeta.device_state locked
resetprop -n ro.boot.veritymode enforcing
resetprop -n ro.boot.warranty_bit 0
resetprop -n ro.crypto.state encrypted
# --- adb off (match Java-layer bypass) ---
resetprop -n init.svc.adbd stopped
resetprop -n init.svc.adb stopped
resetprop -n service.adb.root 0
resetprop -n persist.sys.adb_enable 0
resetprop -n persist.adb.wifi.enabled 0
log "post-fs-data done"

View File

@@ -0,0 +1,37 @@
#!/system/bin/sh
# After boot: rotate Settings.Secure.android_id once (global, affects all apps).
MODDIR=${0%/*}
LOGTAG="maribank_device_spoof"
log() {
echo "[$LOGTAG] $*" >> /cache/maribank_device_spoof.log 2>/dev/null
}
# Wait for SettingsProvider
i=0
while [ "$(getprop sys.boot_completed)" != "1" ] && [ "$i" -lt 120 ]; do
sleep 1
i=$((i + 1))
done
sleep 3
if [ ! -f "$MODDIR/android_id.txt" ]; then
# 16 hex chars (standard ANDROID_ID format)
AID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-16)
[ -z "$AID" ] && AID="$(date +%s | md5sum 2>/dev/null | cut -c1-16)"
echo "$AID" > "$MODDIR/android_id.txt"
fi
AID=$(cat "$MODDIR/android_id.txt")
settings put secure android_id "$AID" 2>/dev/null
log "android_id=$AID"
# Clear MariBank cache so SHPSSDK re-collects with new props (optional, user can disable)
PKG="sg.com.maribankmobile.digitalbank"
if [ -f "$MODDIR/clear_maribank_on_boot" ]; then
pm clear "$PKG" 2>/dev/null
log "pm clear $PKG"
fi
log "service.sh done"

View File

@@ -0,0 +1,75 @@
# Finish scheme B after reboot: LSPosed scope + verify spoof + clear MariBank
param([switch]$Reboot)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
$Pkg = "sg.com.maribankmobile.digitalbank"
$WaitSeconds = 60
Write-Host "Checking adb ($WaitSeconds s timeout)..." -ForegroundColor Cyan
$deadline = (Get-Date).AddSeconds($WaitSeconds)
$ready = $false
while ((Get-Date) -lt $deadline) {
$lines = & $adb devices 2>&1
if ($lines -match "1C081FDF600K5Q\s+device") {
$ready = $true
break
}
if ($lines -match "\tunauthorized") {
Write-Host "Device connected but UNAUTHORIZED — unlock phone and tap Allow USB debugging." -ForegroundColor Red
}
Start-Sleep -Seconds 2
}
if (-not $ready) {
Write-Host @"
No adb device found after ${WaitSeconds}s.
On Pixel 6:
1. USB cable connected (data port, not charge-only)
2. Settings -> Developer options -> USB debugging ON
3. USB mode: File transfer / PTP
4. Unlock screen -> tap Allow on RSA prompt
5. Re-run: .\scripts\maribank-scheme-b-finish.ps1
If USB debugging was turned off earlier, you must enable it on the phone first.
"@ -ForegroundColor Yellow
exit 1
}
Start-Sleep -Seconds 2
Write-Host "`n=== Modules ===" -ForegroundColor Cyan
& $adb shell su -c "ls /data/adb/modules/"
& $adb shell su -c "magisk --denylist status"
Write-Host "`n=== Identity ===" -ForegroundColor Cyan
& $adb shell su -c "getprop ro.serialno; getprop ro.boot.serialno; settings get secure android_id"
& $adb shell su -c "cat /data/adb/modules/maribank_device_spoof/serial.txt 2>/dev/null; cat /data/adb/modules/maribank_device_spoof/android_id.txt 2>/dev/null"
Write-Host "`n=== LSPosed scope (MariBank SG) ===" -ForegroundColor Cyan
$apkPath = (& $adb shell pm path com.miraclegarden.smsmessage.xposed 2>$null) -replace '^package:', ''
$apkPath = $apkPath.Trim()
if (-not $apkPath) {
Write-Host "Xposed module not installed" -ForegroundColor Red
exit 1
}
& $adb shell su -c "cp /data/adb/lspd/config/modules_config.db /sdcard/Download/modules_config.db; chmod 644 /sdcard/Download/modules_config.db"
$db = Join-Path $env:TEMP "modules_config_finish.db"
& $adb pull /sdcard/Download/modules_config.db $db | Out-Null
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=== Disable USB debug + clear MariBank ===" -ForegroundColor Cyan
& $adb shell su -c "settings put global adb_enabled 0; settings put global development_settings_enabled 0"
& $adb shell su -c "pm clear $Pkg"
Write-Host "`n=== Done ===" -ForegroundColor Green
Write-Host "Open MariBank -> Sign up -> enter phone -> Next"
Write-Host "Log: adb logcat -d | Select-String 'MariBankEncrypt|MariBankAttest|3100012|deviceFingerprint'"
if ($Reboot) {
Write-Host "Rebooting..." -ForegroundColor Yellow
& $adb reboot
}

View File

@@ -0,0 +1,121 @@
# MariBank device spoof — Magisk resetprop + optional module install
# Usage:
# .\scripts\maribank-spoof-device.ps1 # apply resetprop once via adb su
# .\scripts\maribank-spoof-device.ps1 -InstallModule # zip & push Magisk module
# .\scripts\maribank-spoof-device.ps1 -NewIdentity # regenerate serial/android_id files on device
# .\scripts\maribank-spoof-device.ps1 -ClearMariBank # pm clear MariBank after spoof
param(
[switch]$InstallModule,
[switch]$NewIdentity,
[switch]$ClearMariBank,
[string]$DeviceSerial = ""
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
$ModuleDir = Join-Path $PSScriptRoot "magisk\maribank-device-spoof"
$Pkg = "sg.com.maribankmobile.digitalbank"
if (-not (Test-Path $adb)) {
Write-Host "adb not found: $adb" -ForegroundColor Red
exit 1
}
function Invoke-AdbShell($cmd) {
& $adb shell "su -c '$cmd'" 2>&1
}
function Test-Magisk {
$m = Invoke-AdbShell "command -v resetprop 2>/dev/null || ls /data/adb/magisk/magisk 2>/dev/null"
return ($LASTEXITCODE -eq 0 -and "$m" -match "resetprop|magisk")
}
Write-Host "=== MariBank Device Spoof (方案 B: Magisk resetprop) ===" -ForegroundColor Cyan
& $adb devices -l
if (-not (Test-Magisk)) {
Write-Host "Magisk/resetprop not found on device. Install Magisk first." -ForegroundColor Red
exit 1
}
if ($InstallModule) {
$zipPath = Join-Path $env:TEMP "maribank-device-spoof.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
# Use tar (Windows 10+) for Unix paths; Magisk needs post-fs-data.sh at zip root
Push-Location $ModuleDir
tar -a -cf $zipPath module.prop post-fs-data.sh service.sh
Pop-Location
Write-Host "Pushing module to /sdcard/Download/ ..."
& $adb push $zipPath /sdcard/Download/maribank-device-spoof.zip
Write-Host @"
Module zip pushed. On phone:
1. Magisk -> Modules -> Install from storage -> maribank-device-spoof.zip
2. Reboot
3. Enable Shamiko (see below)
"@ -ForegroundColor Yellow
}
if ($NewIdentity) {
Write-Host "Removing saved identity (module will regenerate on next boot) ..."
Invoke-AdbShell "rm -f /data/adb/modules/maribank_device_spoof/serial.txt /data/adb/modules/maribank_device_spoof/android_id.txt"
}
if ($DeviceSerial -eq "") {
$DeviceSerial = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ForEach-Object { [char]$_ })
}
$AndroidId = -join ((48..57) + (97..102) | Get-Random -Count 16 | ForEach-Object { [char]$_ })
Write-Host "Applying one-shot resetprop (serial=$DeviceSerial android_id=$AndroidId) ..."
$props = @(
"resetprop ro.serialno $DeviceSerial",
"resetprop ro.boot.serialno $DeviceSerial",
"resetprop persist.sys.serialno $DeviceSerial",
"resetprop ro.debuggable 0",
"resetprop ro.secure 1",
"resetprop ro.build.tags release-keys",
"resetprop ro.boot.verifiedbootstate green",
"resetprop ro.boot.flash.locked 1",
"resetprop ro.boot.vbmeta.device_state locked",
"resetprop ro.boot.veritymode enforcing",
"resetprop init.svc.adbd stopped",
"resetprop persist.sys.adb_enable 0"
)
foreach ($p in $props) {
Invoke-AdbShell $p | Out-Null
}
Invoke-AdbShell "settings put secure android_id $AndroidId" | Out-Null
Write-Host "Verify:" -ForegroundColor Green
Invoke-AdbShell "getprop ro.serialno; getprop ro.boot.serialno; settings get secure android_id"
if ($ClearMariBank) {
Write-Host "Clearing MariBank app data ..."
Invoke-AdbShell "pm clear $Pkg"
Write-Host "MariBank data cleared. Cold start Sign up again." -ForegroundColor Green
}
Write-Host @"
--- Shamiko checklist (required for scheme B) ---
1. Magisk -> Settings -> Configure DenyList -> enable DenyList
2. DenyList -> add $Pkg (all sub-processes)
3. Install Shamiko module (Magisk repo / GitHub releases)
4. Magisk -> Settings -> hide Magisk app (optional)
5. LSPosed: keep module scoped to MariBank; soft reboot MariBank after spoof
6. Turn OFF USB debugging before testing register (or rely on Xposed adb bypass)
To install persistent module:
.\scripts\maribank-spoof-device.ps1 -InstallModule
To force new identity on next boot:
.\scripts\maribank-spoof-device.ps1 -NewIdentity -InstallModule
(then reboot)
Log on device: /cache/maribank_device_spoof.log
"@ -ForegroundColor Cyan

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."

4
scripts/pull-tng-log.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/system/bin/sh
logcat -d > /data/local/tmp/tnglog.txt
grep -E '7500|exited cleanly|Process my.com.tngdigital|TngRoot|UserLogin|FATAL|Abort message|tombstone|blocked syscall|has died' /data/local/tmp/tnglog.txt | tail -120 > /data/local/tmp/tnglog2.txt
wc -l /data/local/tmp/tnglog2.txt

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

@@ -0,0 +1,16 @@
#!/system/bin/sh
logcat -c
am force-stop my.com.tngdigital.ewallet
sleep 1
monkey -p my.com.tngdigital.ewallet -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1
sleep 2
echo "=== @2s ==="; ps -A | grep -i tng || echo NONE
sleep 5
echo "=== @7s ==="; ps -A | grep -i tng || echo NONE
sleep 8
echo "=== @15s ==="; ps -A | grep -i tng || echo NONE
logcat -d > /data/local/tmp/tngfull.txt
echo "=== guard ==="
grep TngExitGuard /data/local/tmp/tngfull.txt | tail -30
echo "=== outcome ==="
grep -E 'exited cleanly|exited due|has died|skipped SIGILL|blocked kill|blocked tgkill|UserLogin|Displayed|PLT blocked' /data/local/tmp/tngfull.txt | tail -30

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,74 @@
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.TngRootBypassHook;
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 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 (MariBankRootBypassHook.isTargetPackage(lpparam.packageName)) {
MariBankShpsNativeHook.install(lpparam);
MariBankRootBypassHook.install(lpparam);
return;
}
if (TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
TngRootBypassHook.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,232 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
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;
/**
* SHPSSDK attestation 生成链 Hook含 native JNI
* 目标:在 {@code rdVerifyInfo.data/dataKey} 组装前,让 native 采集层读到「干净环境」。
*/
public final class MariBankAttestationHook {
private static final String TAG = "notiMessageHook/MariBankAttest";
private static volatile boolean installed = false;
private MariBankAttestationHook() {
}
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
if (installed) {
return;
}
installed = true;
int n = 0;
n += hookAttestationClass(lpparam, "com.shopee.shpssdkbank.wvvvuwwu");
n += hookAttestationClass(lpparam, "com.shopee.shpssdk.wvvvuwwu");
n += hookAttestationClass(lpparam,
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu");
n += hookKnownAttestationMethods(lpparam);
hookEnvironmentProbes(lpparam);
XposedBridge.log(TAG + " attestation hooks=" + n);
}
/** 逆向确认的 attestation / requestDefense 桥接方法。 */
private static int hookKnownAttestationMethods(XC_LoadPackage.LoadPackageParam lpparam) {
int count = 0;
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vvuwuuvuu");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "wwvwvwuvv");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuuwv");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv", "wuvwuvwwu");
return count;
}
private static int hookMethodByName(
XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (!methodName.equals(method.getName())) {
continue;
}
if (hookAttestationMethod(className, method)) {
count++;
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
}
return count;
}
private static int hookAttestationClass(
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;
}
if (hookAttestationMethod(className, method)) {
count++;
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip class " + className + ": " + t.getMessage());
}
return count;
}
private static boolean hookAttestationMethod(String className, Method method) {
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;
String sanitized = sanitizeAttestationString(s);
if (!sanitized.equals(s)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " " + className + "#" + method.getName()
+ (Modifier.isNative(method.getModifiers()) ? " (native)" : "")
+ " sanitized len=" + s.length());
}
}
});
return true;
}
if (rt == byte[].class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (!(result instanceof byte[])) {
return;
}
byte[] bytes = (byte[]) result;
byte[] out = MariBankRegisterPayloadUtil.sanitizeRegistrationBytes(bytes);
if (out != bytes) {
param.setResult(out);
XposedBridge.log(TAG + " " + className + "#" + method.getName()
+ " byte[] sanitized");
} else {
byte[] tokenOut = MariBankRiskTokenUtil.sanitizeBytes(bytes, 0, bytes.length);
if (tokenOut != bytes) {
param.setResult(tokenOut);
XposedBridge.log(TAG + " " + className + "#" + method.getName()
+ " byte[] riskToken sanitized");
}
}
}
});
return true;
}
if (rt == boolean.class || rt == Boolean.class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(false);
}
});
return true;
}
if (rt == int.class || rt == Integer.class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(0);
}
});
return true;
}
return false;
}
private static String sanitizeAttestationString(String s) {
if (s == null || s.isEmpty()) {
return s;
}
if (MariBankRegisterPayloadUtil.isRegistrationPayload(s)) {
return MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s);
}
if (s.contains("|")) {
return MariBankRiskTokenUtil.sanitizeRiskToken(s);
}
return MariBankRiskTokenUtil.sanitizeAllInText(s);
}
private static void hookEnvironmentProbes(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"android.os.Debug",
lpparam.classLoader,
"isDebuggerConnected",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(false);
}
}
);
} catch (Throwable ignored) {
}
try {
XposedHelpers.findAndHookMethod(
"android.os.Debug",
lpparam.classLoader,
"waitingForDebugger",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(false);
}
}
);
} catch (Throwable ignored) {
}
RootBypassHelper.hookFileExists(lpparam);
RootBypassHelper.hookRuntimeExec(lpparam);
RootBypassHelper.hookSystemGetProperty(lpparam);
hookProcessBuilder(lpparam);
}
private static void hookProcessBuilder(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
ProcessBuilder.class,
"start",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
ProcessBuilder pb = (ProcessBuilder) param.thisObject;
if (pb == null || pb.command() == null) {
return;
}
String joined = String.join(" ", pb.command()).toLowerCase();
if (joined.contains(" su") || joined.startsWith("su")
|| joined.contains("magisk") || joined.contains("which su")
|| joined.contains("getprop ro.debuggable")) {
XposedBridge.log(TAG + " blocked ProcessBuilder: " + joined);
throw new SecurityException("blocked root probe");
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " ProcessBuilder hook failed: " + t.getMessage());
}
}
}

View File

@@ -0,0 +1,71 @@
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;
/**
* register 加密前 JSON 净化:{@code scene=REGISTRATION} 路径上的 deviceFingerprint / riskToken。
*/
final class MariBankRegisterPayloadUtil {
private static final String TAG = "notiMessageHook/MariBankRegister";
private static final Pattern DEVICE_FINGERPRINT = Pattern.compile(
"\"deviceFingerprint\"\\s*:\\s*\"([^\"]+)\"");
private MariBankRegisterPayloadUtil() {
}
static boolean isRegistrationPayload(String text) {
if (text == null || text.length() < 24) {
return false;
}
return text.contains("\"scene\":\"REGISTRATION\"")
|| text.contains("\"scene\": \"REGISTRATION\"")
|| (text.contains("rdVerifyInfo") && text.contains("\"step\":\"BE\""));
}
static byte[] sanitizeRegistrationBytes(byte[] data) {
if (data == null || data.length == 0) {
return data;
}
String text = new String(data, StandardCharsets.UTF_8);
if (!isRegistrationPayload(text)) {
return MariBankRiskTokenUtil.sanitizeBytes(data, 0, data.length);
}
String out = sanitizeRegistrationJson(text);
if (out.equals(text)) {
return data;
}
XposedBridge.log(TAG + " sanitized register payload len=" + data.length + " -> " + out.length());
return out.getBytes(StandardCharsets.UTF_8);
}
static String sanitizeRegistrationJson(String json) {
if (json == null) {
return null;
}
String out = MariBankRiskTokenUtil.sanitizeAllInText(json);
Matcher m = DEVICE_FINGERPRINT.matcher(out);
StringBuffer sb = new StringBuffer();
boolean changed = false;
while (m.find()) {
String old = m.group(1);
String neu = MariBankRiskTokenUtil.sanitizeRiskToken(old);
if (!neu.equals(old)) {
changed = true;
}
m.appendReplacement(sb, Matcher.quoteReplacement(
"\"deviceFingerprint\":\"" + neu + "\""));
}
if (changed) {
m.appendTail(sb);
out = sb.toString();
XposedBridge.log(TAG + " deviceFingerprint sanitized in register JSON");
}
return out;
}
}

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,1943 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.app.Activity;
import android.app.Dialog;
import android.content.ContentResolver;
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 Root 检测绕过PH + SG
* 逆向SafeMode SDK + SHPSSDKSG 额外有 USB/无线 ADB 检测RISK_USB_ADB / RISK_WIFI_ADB
*/
public final class MariBankRootBypassHook {
private static final String TAG = "notiMessageHook/MariBankRoot";
/** 菲律宾 MariBank / SeaBank PH */
public static final String PACKAGE = "ph.seabank.seabank";
/** 新加坡 MariBank */
public static final String PACKAGE_SG = "sg.com.maribankmobile.digitalbank";
private static final String[] TARGET_PACKAGES = {PACKAGE, PACKAGE_SG};
public static boolean isTargetPackage(String packageName) {
if (packageName == null) {
return false;
}
for (String pkg : TARGET_PACKAGES) {
if (pkg.equals(packageName)) {
return true;
}
}
return false;
}
/** 服务端注册被拒错误码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",
"com.shopee.bke.biz.user.viewmodel.RegisterViewModel",
};
private MariBankRootBypassHook() {
}
private static volatile boolean deferredHooksInstalled = false;
private static volatile boolean lateAppHooksInstalled = false;
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!isTargetPackage(lpparam.packageName)) {
return;
}
XposedBridge.log(TAG + " install for " + lpparam.packageName);
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);
}
};
XC_MethodHook afterOnCreate = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
MariBankShpsNativeHook.installLateNativeHooks(lpparam);
installLateAppHooks(lpparam);
}
};
try {
XposedHelpers.findAndHookMethod(
"com.shopee.bke.digitalbank.BkeApplication",
lpparam.classLoader,
"attachBaseContext",
"android.content.Context",
afterAttach);
XposedHelpers.findAndHookMethod(
"com.shopee.bke.digitalbank.BkeApplication",
lpparam.classLoader,
"onCreate",
afterOnCreate);
XposedBridge.log(TAG + " waiting attachBaseContext + onCreate for app hooks");
} catch (Throwable t) {
XposedBridge.log(TAG + " BkeApplication hook failed, install now: " + t.getMessage());
installDeferredHooks(lpparam);
MariBankShpsNativeHook.installDeferred(lpparam);
MariBankShpsNativeHook.installLateNativeHooks(lpparam);
installLateAppHooks(lpparam);
}
}
/** onCreate 之后补装:此时 classes11 / SHPSSDK 与 RN SO 均已就绪。 */
private static void installLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
if (lateAppHooksInstalled) {
return;
}
lateAppHooksInstalled = true;
int hooked = 0;
for (String className : SAFE_MODE_CLASSES) {
hooked += hookAllBooleanChecks(lpparam, className);
}
hooked += hookShpsRisk(lpparam);
hookShpsToken(lpparam);
hookErrorFlowLogging(lpparam);
MariBankSdkUtilsHook.installLate(lpparam);
MariBankAttestationHook.installLate(lpparam);
XposedBridge.log(TAG + " late app hooks installed, booleanHooks=" + hooked);
}
private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) {
if (deferredHooksInstalled) {
return;
}
deferredHooksInstalled = true;
hookSafeModeDialog(lpparam);
hookRootDialogBlock(lpparam);
hookAdbBypass(lpparam);
hookNetworkLogging(lpparam);
XposedBridge.log(TAG + " early app hooks installed");
}
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.contains("406")
|| args.toLowerCase().contains("error")
|| args.toLowerCase().contains("unavailable")) {
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();
String url = CURRENT_REQUEST_URL.get();
boolean maribankApi = url != null
&& (url.contains("maribank.com") || url.contains("seabank.ph"));
boolean interesting = 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\"")
|| (maribankApi && (lower.contains("register")
|| lower.contains("error") || lower.contains("unavailable")));
if (interesting) {
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);
}
try {
Object req = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
if (req != null) {
Object url = XposedHelpers.callMethod(req, "url");
if (url != null) {
String urlStr = String.valueOf(url);
if (urlStr.contains("maribank.com") || urlStr.contains("seabank.ph")
|| urlStr.contains("/register")) {
XposedBridge.log(TAG + " outbound " + urlStr);
}
}
}
} catch (Throwable ignored) {
}
} 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());
}
}
/** SGADB / 无线调试检测 + Root 弹窗/Toast/全屏页拦截。 */
private static void hookAdbBypass(XC_LoadPackage.LoadPackageParam lpparam) {
hookAdbSettings(lpparam);
hookAdbActivityEscape(lpparam);
}
private static void hookAdbSettings(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook fakeDisabled = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args.length < 2 || !(param.args[1] instanceof String)) {
return;
}
String key = (String) param.args[1];
if (!isAdbSettingKey(key)) {
return;
}
Class<?> ret = ((Method) param.method).getReturnType();
if (ret == int.class || ret == Integer.class) {
param.setResult(0);
} else if (ret == long.class || ret == Long.class) {
param.setResult(0L);
} else if (ret == String.class) {
param.setResult("0");
}
XposedBridge.log(TAG + " faked Settings key=" + key);
}
};
String[][] targets = {
{"android.provider.Settings$Global", "getInt"},
{"android.provider.Settings$Global", "getLong"},
{"android.provider.Settings$Global", "getString"},
{"android.provider.Settings$Secure", "getInt"},
{"android.provider.Settings$Secure", "getString"},
{"android.provider.Settings$System", "getInt"},
};
for (String[] target : targets) {
try {
Class<?> clazz = XposedHelpers.findClass(target[0], lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (!target[1].equals(method.getName())) {
continue;
}
Class<?>[] params = method.getParameterTypes();
if (params.length >= 2 && ContentResolver.class.isAssignableFrom(params[0])
&& params[1] == String.class) {
XposedBridge.hookMethod(method, fakeDisabled);
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip Settings hook " + target[0] + ": " + t.getMessage());
}
}
}
/** 若全屏/RN 页已展示 ADB 拦截文案,直接 finish 退出该 Activity。 */
private static void hookAdbActivityEscape(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
Activity.class,
"onResume",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
if (activity == null || activity.isFinishing()) {
return;
}
String name = activity.getClass().getName();
if (name.contains("SafeModeRecoverActivity")) {
return;
}
String text = extractActivityText(activity);
if (isAdbBlockText(text)) {
XposedBridge.log(TAG + " finish adb block activity: " + name);
activity.finish();
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " adb activity hook failed: " + t.getMessage());
}
}
private static String extractActivityText(Activity activity) {
StringBuilder sb = new StringBuilder();
try {
CharSequence title = activity.getTitle();
if (title != null) {
sb.append(title);
}
} catch (Throwable ignored) {
}
try {
if (activity.getWindow() != null) {
collectTextViews(activity.getWindow().getDecorView(), sb);
}
} catch (Throwable ignored) {
}
return sb.toString();
}
private static boolean isAdbSettingKey(String key) {
if (key == null) {
return false;
}
String lower = key.toLowerCase();
return lower.contains("adb")
|| "development_settings_enabled".equals(lower)
|| lower.contains("wireless_debug");
}
private static boolean isAdbBlockText(String text) {
if (text == null || text.isEmpty()) {
return false;
}
String lower = text.toLowerCase();
return lower.contains("adb/wireless adb")
|| lower.contains("wireless adb detected")
|| lower.contains("usb debugging")
|| lower.contains("wireless debugging")
|| lower.contains("turn off adb")
|| lower.contains("third parties to access")
|| lower.contains("safeguard your banking")
|| (lower.contains("adb") && lower.contains("detect"));
}
private static boolean isEnvironmentBlockText(String text) {
return isRootBlockText(text) || isAdbBlockText(text);
}
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 && isEnvironmentBlockText(String.valueOf(param.args[0]))) {
param.args[0] = " ";
XposedBridge.log(TAG + " blanked env block 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 (isEnvironmentBlockText(s)) {
param.setResult(" ");
XposedBridge.log(TAG + " blanked env block 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 (isEnvironmentBlockText(extractDialogText(dialog))) {
XposedBridge.log(TAG + " blocked env 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);
} else if (isAdbBlockText(text)) {
XposedBridge.log(TAG + " blocked adb 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")
|| lower.contains("does not support root")
|| lower.contains("support root device");
}
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,225 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
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;
/**
* register body 加密前 Hook{@code com.shopee.bke.lib.jni.utils.d} / {@code uvwuvwuv}。
* 勿 Hook {@code utils.f}SoUtils否则会 libsdkutils 白屏。
*/
public final class MariBankSdkUtilsHook {
private static final String TAG = "notiMessageHook/MariBankEncrypt";
private static final int MAX_LOG = 2000;
private static final String WRAPPER = "com.shopee.bke.lib.jni.utils.d";
private static final String NATIVE_ENCRYPT = "com.shopee.bke.lib.jni.utils.uvwuvwuv";
private static volatile boolean installed = false;
private MariBankSdkUtilsHook() {
}
/** BkeApplication.onCreate 之后安装libsdkutils 已加载)。 */
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
if (installed) {
return;
}
installed = true;
int n = hookEncryptWrapper(lpparam);
n += hookNativeEncryptUtils(lpparam);
n += hookGsonRegister(lpparam);
XposedBridge.log(TAG + " late encrypt hooks=" + n);
}
private static int hookEncryptWrapper(XC_LoadPackage.LoadPackageParam lpparam) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(WRAPPER, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String name = method.getName();
XposedBridge.log(TAG + " >> utils.d." + name);
for (int i = 0; i < param.args.length; i++) {
Object sanitized = sanitizeArg(param.args[i]);
if (sanitized != param.args[i]) {
param.args[i] = sanitized;
}
logArg(" in" + i, param.args[i]);
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result == null) {
return;
}
if (result instanceof String[]) {
for (int i = 0; i < ((String[]) result).length; i++) {
logArg(" out" + i, ((String[]) result)[i]);
}
} else if (result instanceof byte[]) {
logArg(" out", result);
}
}
});
count++;
}
if (count > 0) {
XposedBridge.log(TAG + " hooked utils.d methods=" + count);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip utils.d: " + t.getMessage());
}
return count;
}
private static int hookNativeEncryptUtils(XC_LoadPackage.LoadPackageParam lpparam) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(NATIVE_ENCRYPT, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (!Modifier.isStatic(method.getModifiers())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " >> uvwuvwuv." + method.getName());
for (int i = 0; i < param.args.length; i++) {
Object sanitized = sanitizeArg(param.args[i]);
if (sanitized != param.args[i]) {
param.args[i] = sanitized;
}
logArg(" in" + i, param.args[i]);
}
}
});
count++;
}
if (count > 0) {
XposedBridge.log(TAG + " hooked uvwuvwuv methods=" + count);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip uvwuvwuv: " + t.getMessage());
}
return count;
}
private static int hookGsonRegister(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"toJson",
Object.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!(param.getResult() instanceof String)) {
return;
}
String json = (String) param.getResult();
if (!looksLikeRegisterJson(json)) {
return;
}
String sanitized = MariBankRegisterPayloadUtil.isRegistrationPayload(json)
? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(json)
: MariBankRiskTokenUtil.sanitizeAllInText(json);
if (!sanitized.equals(json)) {
param.setResult(sanitized);
json = sanitized;
}
if (MariBankRegisterPayloadUtil.isRegistrationPayload(json)) {
XposedBridge.log(TAG + " Gson REGISTRATION: " + truncate(json));
}
}
}
);
return 1;
} catch (Throwable t) {
XposedBridge.log(TAG + " skip Gson.toJson: " + t.getMessage());
return 0;
}
}
private static boolean looksLikeRegisterJson(String text) {
if (text == null || text.length() < 8) {
return false;
}
if (MariBankRegisterPayloadUtil.isRegistrationPayload(text)) {
return true;
}
String lower = text.toLowerCase();
return lower.contains("rdverifyinfo")
|| lower.contains("devicefingerprint")
|| (lower.contains("encphone") && lower.contains("scene"));
}
private static Object sanitizeArg(Object arg) {
if (arg instanceof String) {
String s = (String) arg;
if (!s.contains("|") && !looksLikeRegisterJson(s)) {
return arg;
}
String out = MariBankRegisterPayloadUtil.isRegistrationPayload(s)
? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s)
: MariBankRiskTokenUtil.sanitizeAllInText(s);
return out.equals(s) ? arg : out;
}
if (arg instanceof byte[]) {
byte[] bytes = (byte[]) arg;
byte[] out = MariBankRegisterPayloadUtil.sanitizeRegistrationBytes(bytes);
return out == bytes ? arg : out;
}
return arg;
}
private static void logArg(String label, Object arg) {
if (arg == null) {
XposedBridge.log(TAG + label + " null");
return;
}
if (arg instanceof String) {
String s = (String) arg;
if (s.length() > 4 || s.contains("|") || looksLikeRegisterJson(s)) {
XposedBridge.log(TAG + label + " String(" + s.length() + ") " + truncate(s));
}
return;
}
if (arg instanceof byte[]) {
byte[] bytes = (byte[]) arg;
String text;
try {
text = new String(bytes, StandardCharsets.UTF_8);
} catch (Throwable t) {
text = "<bin>";
}
if (text.contains("|") || looksLikeRegisterJson(text) || bytes.length < 512) {
XposedBridge.log(TAG + label + " byte[" + bytes.length + "] " + truncate(text));
}
}
}
private static String truncate(String s) {
if (s == null) {
return "";
}
if (s.length() <= MAX_LOG) {
return s;
}
return s.substring(0, MAX_LOG) + "...";
}
}

View File

@@ -0,0 +1,689 @@
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;
private static volatile boolean lateNativeInstalled = false;
/**
* 勿 Hook负责 SoUtils.loadSoLibrary / libshpssdk_bank.so 加载Hook 会导致 SO 找不到。
*/
private static final Set<String> NATIVE_BRIDGE_EXCLUDED = new HashSet<>(Arrays.asList(
"com.shopee.shpssdkbank.vuvuwwwuw",
"com.shopee.shpssdkbank.vwuuwwvwv"
));
/** 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 之后:仅装不干扰 SO/RN 加载的 Hook。 */
public static void installDeferred(XC_LoadPackage.LoadPackageParam lpparam) {
if (deferredInstalled) {
return;
}
deferredInstalled = true;
hookRequestDefense(lpparam);
hookBuildFields(lpparam);
XposedBridge.log(TAG + " deferred hooks installed for " + lpparam.packageName);
}
/** BkeApplication.onCreate 之后RN / shpssdk SO 已加载,再装 native 桥接 Hook。 */
public static void installLateNativeHooks(XC_LoadPackage.LoadPackageParam lpparam) {
if (lateNativeInstalled) {
return;
}
lateNativeInstalled = true;
hookShpsNativeBridge(lpparam);
hookShpsNativeCore(lpparam);
XposedBridge.log(TAG + " late native 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 "";
}
/** 保留 requestDefense 执行(生成 x-sap-fixme仅净化返回值中的 risk 字段。 */
private static void hookRequestDefense(XC_LoadPackage.LoadPackageParam lpparam) {
for (String className : new String[]{
"com.shopee.shpssdkbank.SHPSSDK",
"com.shopee.shpssdk.SHPSSDK",
}) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"requestDefense",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result instanceof String) {
String s = (String) result;
if (s.contains("|")) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
if (!sanitized.equals(s)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " requestDefense String sanitized");
}
}
} else if (result instanceof byte[]) {
byte[] bytes = (byte[]) result;
byte[] out = MariBankRiskTokenUtil.sanitizeBytes(
bytes, 0, bytes.length);
if (out != bytes) {
param.setResult(out);
XposedBridge.log(TAG + " requestDefense byte[] sanitized");
}
}
}
}
);
} 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) {
if (NATIVE_BRIDGE_EXCLUDED.contains(className)) {
XposedBridge.log(TAG + " skip native-bridge (so loader): " + className);
continue;
}
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 (Modifier.isNative(method.getModifiers()) && 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;
}
if (Modifier.isNative(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 ("init.svc.adbd".equals(key) || "init.svc.adb".equals(key)) {
return "stopped";
}
if ("service.adb.root".equals(key)) {
return "0";
}
if ("persist.sys.adb_enable".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,370 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.os.Build;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.WeakHashMap;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* 过滤 /proc/self/maps 等敏感路径,隐藏 Xposed / Magisk / Zygisk 库名。
*/
public final class ProcMapsFilterHook {
private static final String TAG = "notiMessageHook/ProcMaps";
private static final Set<String> PROC_SENSITIVE = new HashSet<>(Arrays.asList(
"/proc/self/maps",
"/proc/version",
"/proc/self/status",
"/proc/mounts",
"/proc/self/attr/current",
"/proc/self/mountinfo"
));
private static final String[] MAPS_HIDE_MARKERS = {
"xposed", "lsposed", "edxposed", "magisk", "frida", "substrate",
"libpine", "pine.so", "zygisk", "riru", "shamiko", "notimessage",
"miraclegarden", "libbytehook", "libgadget", "libfrida", "libriru",
"liblspd", "libzygisk", "libvector", "zygisk_vector", "vector",
};
private static final String FAKE_SELINUX_CTX =
"u:r:untrusted_app:s0:c512,c768";
private static final WeakHashMap<Object, String> TRACKED_INPUTS = new WeakHashMap<>();
private static volatile boolean installed = false;
private ProcMapsFilterHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (installed) {
return;
}
installed = true;
hookProcAccess(lpparam);
hookProcViaRandomAccessFile(lpparam);
hookBufferedReader(lpparam);
hookSystemProperties(lpparam);
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
}
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 norm = normalizeProcPath(String.valueOf(param.args[0]));
if (norm != null) {
param.setResult(filterProcBytesAll(
norm, (byte[]) param.getResult()));
}
}
}
);
} catch (Throwable ignored) {
}
}
}
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 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 hookSystemProperties(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> sp = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader);
for (java.lang.reflect.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 ("init.svc.adbd".equals(key) || "init.svc.adb".equals(key)) {
return "stopped";
}
if ("service.adb.root".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.vbmeta.device_state".equals(key)
|| "vendor.boot.vbmeta.device_state".equals(key)) {
return "locked";
}
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,182 @@
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) {
XC_MethodHook blockExec = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String cmd = null;
if (param.args.length > 0 && param.args[0] instanceof String) {
cmd = (String) param.args[0];
} else if (param.args.length > 0 && param.args[0] instanceof String[]) {
cmd = String.join(" ", (String[]) param.args[0]);
}
if (ProbeGuard.isBlockedCommand(cmd)) {
param.setResult(ProbeGuard.fakeFailedProcess(cmd));
}
}
};
try {
XposedHelpers.findAndHookMethod(Runtime.class, "exec", String.class, blockExec);
XposedHelpers.findAndHookMethod(Runtime.class, "exec", String[].class, blockExec);
XposedHelpers.findAndHookMethod(Runtime.class, "exec", String.class, String[].class, blockExec);
XposedHelpers.findAndHookMethod(
Runtime.class, "exec", String[].class, String[].class, blockExec);
} 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,2589 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.app.Application;
import android.app.Dialog;
import android.app.ActivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.Message;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Locale;
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;
/**
* TNG eWallet Root / Promon / JailBroken 检测绕过。
* 包名my.com.tngdigital.ewalletv1.9.9+
*/
public final class TngRootBypassHook {
public static final String PACKAGE = "my.com.tngdigital.ewallet";
private static final String TAG = "notiMessageHook/TngRoot";
/** Promon 混淆包名1.9.10 为 vhvlnqgy旧版为 xwwqazamx。 */
private static final String[] PROMON_PKG_PREFIXES = {"vhvlnqgy", "xwwqazamx"};
private static final String SECURITY_ERROR_ACTIVITY =
"my.com.tngdigital.common.security.ui.SecurityErrorActivity";
private static final String[] BOOLEAN_HOOK_CLASSES = {
"my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
};
private static final String[] BLOCKED_SUPPORT_MARKERS = {
"36616543382169-rooting",
"36616508159769-emulator",
"36616480108697-malicious",
"36616444815257-multiboxing",
"45485757513113-private-space",
"48493749288857-malware",
"/articles/",
"support.tngdigital.com.my/hc/",
};
private static volatile long lastBlockedSuicideAt = 0L;
private static final long SOFT_CRASH_GUARD_MS = 10000L;
/** 用户已进入注册/登录后续页时,禁止 Splash 强拉回 Login。 */
private static volatile boolean registrationFlowActive = false;
private static final String[] REGISTRATION_FLOW_MARKERS = {
"GuideActivity",
"UserRegistrationMobileActivity",
"UserOtpVerificationActivity",
"UserRegistrationIdentityActivity",
"UserRegistrationSixPinActivity",
"UserRegistrationStrengthenPinActivity",
"UserRegistrationSecurityQuestionActivity",
"UserRegistrationSuccessActivity",
"UserSearchCallingCodeActivity",
"UserPinActivity",
"UserSecurePinActivity",
"UserPinVerifyActivity",
"EmailOtpVerificationActivity",
};
private static final String[] REGISTRATION_RPC_MARKERS = {
"phonecheck", "com.abl.wallet.phone", "com.abl.wallet.otp",
"customer.registration", "customer.verify", "customer.login",
"login.options", "callingcode", "pin.token", "module.whitelist",
};
private TngRootBypassHook() {
}
public static boolean isTargetPackage(String packageName) {
return PACKAGE.equals(packageName);
}
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
/** 进程启动最早打点,便于确认 LSPosed 是否注入(注册闪退常因 hook 未生效)。 */
private static void hookEarlyAttachLog(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(Application.class, "attachBaseContext", Context.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Context ctx = (Context) param.args[0];
if (ctx != null && PACKAGE.equals(ctx.getPackageName())) {
XposedBridge.log(TAG + " attachBaseContext pid="
+ Process.myPid());
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " early attach log failed: " + t.getMessage());
}
}
/** Splash 优先生效;新 schedule 会取消旧 RunnableApplication 兜底 vs Splash 2000ms。 */
private static final Handler FORCE_LOGIN_HANDLER = new Handler(Looper.getMainLooper());
private static Runnable pendingForceLoginRunnable;
/** Promon native-bridge short-circuit 重入保护,避免 __cxa_guard_acquire 递归 abort。 */
private static final ThreadLocal<Integer> PROMON_BRIDGE_DEPTH = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
private static void scheduleForceLoginToUserLogin(
final Context appCtx, final Activity splashAct, final String reason, final long delayMs) {
if (appCtx == null) {
return;
}
if (pendingForceLoginRunnable != null) {
FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable);
}
final String login = "my.com.tngdigital.user.view.UserLoginActivity";
pendingForceLoginRunnable = new Runnable() {
@Override
public void run() {
pendingForceLoginRunnable = null;
try {
if (registrationFlowActive || isTopActivityRegistrationFlow(appCtx)) {
XposedBridge.log(TAG + " skip force login (" + reason + ", registration flow)");
return;
}
ActivityManager am =
(ActivityManager) appCtx.getSystemService(Context.ACTIVITY_SERVICE);
if (am != null) {
for (ActivityManager.AppTask task : am.getAppTasks()) {
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
if (info == null || info.topActivity == null) {
continue;
}
String top = info.topActivity.getClassName();
if (top.endsWith(".UserLoginActivity")
|| top.endsWith(".UserPinActivity")
|| isRegistrationFlowActivity(top)) {
XposedBridge.log(TAG + " skip force login (" + reason + ", on " + top + ")");
return;
}
}
}
if (!isUiVisibleForForceLogin(appCtx)) {
XposedBridge.log(TAG + " skip force login (" + reason + ", no visible UI / BAL)");
return;
}
Intent intent = new Intent();
intent.setClassName(PACKAGE, login);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (splashAct != null && !splashAct.isFinishing()) {
splashAct.startActivity(intent);
try {
splashAct.finish();
} catch (Throwable ignored) {
}
XposedBridge.log(TAG + " forced → UserLogin (" + reason + ", from Splash)");
} else {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
appCtx.startActivity(intent);
XposedBridge.log(TAG + " forced → UserLogin (" + reason + ", from AppCtx)");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " force login failed (" + reason + "): " + t.getMessage());
}
}
};
FORCE_LOGIN_HANDLER.postDelayed(pendingForceLoginRunnable, delayMs);
}
/** 后台 Service 重启(如 Firebase SessionLifecycle无可见 Activity强拉会被 BAL 拦截。 */
private static boolean isUiVisibleForForceLogin(Context ctx) {
ActivityManager.RunningAppProcessInfo state = new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(state);
if (state.importance > ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
return false;
}
try {
ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
if (am != null) {
for (ActivityManager.AppTask task : am.getAppTasks()) {
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
if (info != null && info.topActivity != null
&& PACKAGE.equals(info.topActivity.getPackageName())) {
return true;
}
}
}
} catch (Throwable ignored) {
}
return state.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
}
private static final String[] HTTP_LOG_MARKERS = {
"otp", "verify", "pin", "register", "auth", "login", "sms", "mobile",
"risk", "token", "error", "code", "unexpected", "reference",
};
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!isTargetPackage(lpparam.packageName)) {
return;
}
XposedBridge.log(TAG + " install for " + lpparam.packageName
+ " pid=" + Process.myPid()
+ " proc=" + getProcessName());
// Splash 常卡死Instrumentation 强拉即可。双通道会抢跑导致 Login 重载/卡死。
hookEarlyAttachLog(lpparam);
hookRegistrationFlowGuard(lpparam);
hookSplashForceLogin(lpparam);
hookLoginDismissSplash(lpparam);
hookHardwareRendererSetName();
hookBottomSelectDialogDiag(lpparam);
hookActivityLifecycleDiag(lpparam);
hookPromonApService(lpparam);
hookPromonBroadcastReceiver(lpparam);
hookAppAttachForceLogin(lpparam);
RootBypassHelper.hookFileExists(lpparam);
RootBypassHelper.hookRuntimeExec(lpparam);
RootBypassHelper.hookSystemGetProperty(lpparam);
ProcMapsFilterHook.install(lpparam);
hookAntiSuicide();
hookUncaughtPromonException(lpparam);
hookKillApplicationHandler(lpparam);
hookBlockSecurityErrorLaunch(lpparam);
hookPromonNativeGuard(lpparam);
hookPromonLifecycle(lpparam);
hookJnicLibrary(lpparam);
hookTigerTally(lpparam);
hookActivityThreadExit(lpparam);
hookForceExitFlow(lpparam);
hookFinishAllActivityAndKillApp(lpparam);
hookShowSecurityScreenForState(lpparam);
hookSecurityUrlOpeners(lpparam);
hookJailBroken(lpparam);
hookJailBrokenRpc(lpparam);
hookAppSecurityManager(lpparam);
hookAppSecurityCallbacks(lpparam);
hookPromonNativeBridge(lpparam);
hookSecurityBooleanChecks(lpparam);
hookSecurityErrorActivity(lpparam);
hookNetworkDiag(lpparam);
hookWebViewErrorDiag(lpparam);
}
private static void logActivityDiag(String phase, Activity activity) {
if (activity == null) {
return;
}
String name = activity.getClass().getName();
String lower = name.toLowerCase(Locale.US);
if (lower.contains("userpin")
|| lower.contains("userlogin")
|| lower.contains("registration")
|| lower.contains("otp")
|| lower.contains("verify")
|| lower.contains("sms")
|| lower.contains("webview")
|| lower.contains("issue")
|| lower.contains("guide")
|| lower.contains("error")
|| lower.contains("dialog")) {
XposedBridge.log(TAG + " ACT " + phase + " " + name);
}
}
/** 记录 OTP/登录相关 HTTP 请求与响应体,定位验证码提交失败原因。 */
private static void hookNetworkDiag(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) {
CURRENT_REQUEST_URL.set(String.valueOf(url));
}
} catch (Throwable ignored) {
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " diag Request.Builder.build failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
"okhttp3.ResponseBody",
lpparam.classLoader,
"string",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String body = (String) param.getResult();
String url = CURRENT_REQUEST_URL.get();
CURRENT_REQUEST_URL.remove();
if (body == null) {
return;
}
if (!shouldLogHttp(url, body)) {
return;
}
String snippet = body.length() > 800
? body.substring(0, 800) + "..." : body;
XposedBridge.log(TAG + " HTTP rsp"
+ (url != null ? " " + url : "")
+ " body=" + snippet);
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " diag ResponseBody.string failed: " + t.getMessage());
}
XC_MethodHook callRequestHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object req = XposedHelpers.callMethod(param.thisObject, "request");
logHttpRequest(lpparam.classLoader, req);
} catch (Throwable ignored) {
}
}
};
XC_MethodHook enqueueHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object req = XposedHelpers.callMethod(param.thisObject, "request");
logHttpRequest(lpparam.classLoader, req);
} catch (Throwable ignored) {
}
}
};
for (String className : new String[]{
"okhttp3.RealCall", "okhttp3.internal.connection.RealCall"}) {
try {
XposedHelpers.findAndHookMethod(
className, lpparam.classLoader, "execute", callRequestHook);
XposedHelpers.findAndHookMethod(
className, lpparam.classLoader, "enqueue",
"okhttp3.Callback", enqueueHook);
} catch (Throwable ignored) {
}
}
XposedBridge.log(TAG + " hooked network diag (okhttp)");
}
private static void logHttpRequest(ClassLoader loader, Object req) {
if (req == null) {
return;
}
Object url = XposedHelpers.callMethod(req, "url");
if (url == null) {
return;
}
String urlStr = String.valueOf(url);
if (!shouldLogHttpUrl(urlStr)) {
return;
}
XposedBridge.log(TAG + " HTTP req " + urlStr);
Object body = XposedHelpers.callMethod(req, "body");
if (body != null) {
logRequestBodySnippet(loader, body);
}
}
private static boolean shouldLogHttpUrl(String url) {
if (url == null) {
return false;
}
String lower = url.toLowerCase(Locale.US);
return lower.contains("tngdigital")
|| lower.contains("alipay")
|| lower.contains("aliyun")
|| lower.contains("otp")
|| lower.contains("verify")
|| lower.contains("register")
|| lower.contains("auth")
|| lower.contains("login")
|| lower.contains("pin")
|| lower.contains("sms");
}
private static boolean shouldLogHttp(String url, String body) {
if (shouldLogHttpUrl(url)) {
return true;
}
String lower = body.toLowerCase(Locale.US);
for (String marker : HTTP_LOG_MARKERS) {
if (lower.contains(marker)) {
return true;
}
}
return lower.contains("\"code\"") || lower.contains("reference");
}
private static void logRequestBodySnippet(ClassLoader loader, Object body) {
try {
Class<?> bufferClass = XposedHelpers.findClass("okio.Buffer", loader);
Object buffer = XposedHelpers.newInstance(bufferClass);
XposedHelpers.callMethod(body, "writeTo", buffer);
String text = (String) XposedHelpers.callMethod(buffer, "readUtf8");
if (text == null || text.isEmpty()) {
return;
}
String snippet = text.length() > 500 ? text.substring(0, 500) + "..." : text;
XposedBridge.log(TAG + " HTTP req body=" + snippet);
} catch (Throwable ignored) {
}
}
private static void hookWebViewErrorDiag(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"android.webkit.WebViewClient",
lpparam.classLoader,
"onReceivedError",
"android.webkit.WebView",
"android.webkit.WebResourceRequest",
"android.webkit.WebResourceError",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
try {
Object error = param.args[2];
Object code = XposedHelpers.callMethod(error, "getDescription");
Object url = XposedHelpers.callMethod(param.args[1], "getUrl");
XposedBridge.log(TAG + " WebView error url=" + url + " desc=" + code);
} catch (Throwable t) {
XposedBridge.log(TAG + " WebView error: " + t.getMessage());
}
}
});
XposedBridge.log(TAG + " hooked WebViewClient.onReceivedError");
} catch (Throwable t) {
XposedBridge.log(TAG + " WebView error hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Dialog.class,
"show",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String owner = param.thisObject.getClass().getName();
if (owner.contains("TNG") || owner.contains("Dialog")
|| owner.contains("Error") || owner.contains("i7.")) {
XposedBridge.log(TAG + " Dialog.show " + owner);
}
}
});
} catch (Throwable ignored) {
}
}
/** 仅记录注册/登录链 Activity 生命周期,便于 logcat 定位卡点。 */
private static void hookActivityLifecycleDiag(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(Application.class, "onCreate", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Context ctx = (Context) param.thisObject;
if (ctx == null || !PACKAGE.equals(ctx.getPackageName())) {
return;
}
Application app = (Application) param.thisObject;
app.registerActivityLifecycleCallbacks(
new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
logActivityDiag("onCreate", activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
logActivityDiag("onResume", activity);
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(
Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
XposedBridge.log(TAG + " registered activity lifecycle diag");
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " activity lifecycle diag failed: " + t.getMessage());
}
}
/**
* 区号选择弹窗诊断。勿关 HW 加速——Android 16 上会导致列表黑屏。
* ANR 由 HardwareRenderer.setName 拦截兜底。
*/
private static void hookBottomSelectDialogDiag(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.widget.BottomSelectDialogFragment",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart");
}
});
XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (diag only)");
} catch (Throwable t) {
XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage());
}
}
/**
* ANR 栈Dialog.show → enableHardwareAcceleration → HardwareRenderer.setName → future.get 卡死。
* 仅拦截 setName勿全局关 Dialog HW区号选择会黑屏
*/
private static void hookHardwareRendererSetName() {
try {
XposedHelpers.findAndHookMethod(
"android.graphics.HardwareRenderer", null, "setName", String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(null);
}
});
XposedBridge.log(TAG + " hooked HardwareRenderer.setName (blocked)");
} catch (Throwable t) {
XposedBridge.log(TAG + " HardwareRenderer.setName hook failed: " + t.getMessage());
}
}
/** Promon 隔离 Service打点确认 :goacqowmmt 进程 hook 已注入。 */
private static void hookPromonApService(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> svc = findPromonClass(lpparam.classLoader, "ap");
if (svc == null) {
XposedBridge.log(TAG + " Promon ap Service not found");
return;
}
try {
final String svcName = svc.getName();
XposedHelpers.findAndHookMethod(svc, "onCreate", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " " + svcName + " onCreate pid=" + Process.myPid()
+ " proc=" + getProcessName());
}
});
for (Method method : svc.getDeclaredMethods()) {
if (!"onStartCommand".equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " " + svcName + " onStartCommand pid="
+ Process.myPid());
}
});
}
XposedBridge.log(TAG + " hooked " + svcName + " Service");
} catch (Throwable t) {
XposedBridge.log(TAG + " Promon ap Service hook failed: " + t.getMessage());
}
}
/** 标记注册链 Activity 活跃,防止 Splash 强拉 Login 清栈。 */
private static void hookRegistrationFlowGuard(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XC_MethodHook flowGuardHook = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
String name = activity.getClass().getName();
if (isRegistrationFlowActivity(name)) {
registrationFlowActive = true;
if (pendingForceLoginRunnable != null) {
FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable);
pendingForceLoginRunnable = null;
}
XposedBridge.log(TAG + " registration flow active: " + name);
} else if (name.endsWith(".SplashActivity")) {
registrationFlowActive = false;
}
}
};
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnCreate",
Activity.class,
Bundle.class,
flowGuardHook);
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnResume",
Activity.class,
flowGuardHook);
hookLoginOptionsDiag(lpparam);
XposedBridge.log(TAG + " hooked registration flow guard");
} catch (Throwable t) {
XposedBridge.log(TAG + " registration flow guard failed: " + t.getMessage());
}
}
/** Login 页 startLoginOptions 是进入注册/登录选项的网关 RPC 入口。 */
private static void hookLoginOptionsDiag(XC_LoadPackage.LoadPackageParam lpparam) {
final String login = "my.com.tngdigital.user.view.UserLoginActivity";
try {
Class<?> clazz = XposedHelpers.findClass(login, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
if (!name.contains("LoginOptions") && !name.contains("loginOptions")
&& !name.contains("startLogin")) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " UserLoginActivity#" + name + " enter");
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " UserLoginActivity#" + name + " done");
}
});
}
XposedBridge.log(TAG + " hooked UserLoginActivity login-options diag");
} catch (Throwable t) {
XposedBridge.log(TAG + " login-options diag failed: " + t.getMessage());
}
}
private static boolean isRegistrationFlowActivity(String className) {
if (className == null) {
return false;
}
for (String marker : REGISTRATION_FLOW_MARKERS) {
if (className.endsWith("." + marker) || className.contains(marker)) {
return true;
}
}
return false;
}
private static String getProcessName() {
try {
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Method current = activityThread.getDeclaredMethod("currentProcessName");
current.setAccessible(true);
Object name = current.invoke(null);
return name != null ? String.valueOf(name) : "?";
} catch (Throwable t) {
return "?";
}
}
private static boolean isTopActivityRegistrationFlow(Context ctx) {
try {
android.app.ActivityManager am =
(android.app.ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return false;
}
for (android.app.ActivityManager.AppTask task : am.getAppTasks()) {
android.app.ActivityManager.RecentTaskInfo info = task.getTaskInfo();
if (info == null || info.topActivity == null) {
continue;
}
if (isRegistrationFlowActivity(info.topActivity.getClassName())) {
return true;
}
}
} catch (Throwable ignored) {
}
return false;
}
/** Promon USB 广播 N 跑在主线程,复进时拖死 Looper。 */
private static void hookPromonBroadcastReceiver(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> clazz = findPromonClass(lpparam.classLoader, "N");
if (clazz == null) {
XposedBridge.log(TAG + " Promon N receiver not found");
return;
}
try {
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!"onReceive".equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(null);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + clazz.getName() + " onReceive x" + hooked);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " Promon N hook failed: " + t.getMessage());
}
}
/**
* 强拉 Login 后系统 Splash 遮罩常挂在 UserLogin 上windows=Splash Screen
* 导致复进「未响应」。onCreate/onResume 强制 dismiss。
*/
private static void hookLoginDismissSplash(XC_LoadPackage.LoadPackageParam lpparam) {
final String login = "my.com.tngdigital.user.view.UserLoginActivity";
try {
XC_MethodHook dismissHook = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
String name = activity.getClass().getName();
if (!login.equals(name) && !name.endsWith(".UserLoginActivity")) {
return;
}
dismissSplashScreen(activity);
}
};
XposedHelpers.findAndHookMethod(
Instrumentation.class, "callActivityOnCreate",
Activity.class, Bundle.class, dismissHook);
XposedHelpers.findAndHookMethod(
Instrumentation.class, "callActivityOnResume",
Activity.class, dismissHook);
XposedBridge.log(TAG + " hooked UserLogin splash dismiss (onCreate+onResume)");
} catch (Throwable t) {
XposedBridge.log(TAG + " login splash dismiss hook failed: " + t.getMessage());
}
}
/** 移除 Android 12+ / androidx 启动页遮罩,避免挡在 Login 前导致 ANR。 */
private static void dismissSplashScreen(Activity activity) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
activity.reportFullyDrawn();
} catch (Throwable ignored) {
}
if (Build.VERSION.SDK_INT >= 31) {
try {
android.window.SplashScreen ss = activity.getSplashScreen();
if (ss != null) {
ss.setOnExitAnimationListener(
splashScreenView -> splashScreenView.remove());
}
} catch (Throwable ignored) {
}
}
try {
ClassLoader cl = activity.getClassLoader();
Class<?> splashCl = XposedHelpers.findClass(
"androidx.core.splashscreen.SplashScreen", cl);
Object splash = XposedHelpers.callStaticMethod(
splashCl, "installSplashScreen", activity);
Class<?> condCl = XposedHelpers.findClass(
"androidx.core.splashscreen.SplashScreen$KeepOnScreenCondition", cl);
Object keepOff = Proxy.newProxyInstance(
cl, new Class<?>[] { condCl },
(proxy, method, args) -> false);
XposedHelpers.callMethod(splash, "setKeepOnScreenCondition", keepOff);
Class<?> exitCl = XposedHelpers.findClass(
"androidx.core.splashscreen.SplashScreen$OnExitAnimationListener", cl);
Object exitListener = Proxy.newProxyInstance(
cl, new Class<?>[] { exitCl },
(proxy, method, args) -> {
if (args != null && args.length > 0 && args[0] != null) {
XposedHelpers.callMethod(args[0], "remove");
}
return null;
});
XposedHelpers.callMethod(splash, "setOnExitAnimationListener", exitListener);
} catch (Throwable t) {
XposedBridge.log(TAG + " dismissSplashScreen compat: " + t.getMessage());
}
}
});
}
/**
* Splash.onCreate 常被 Promon 堵死永远不返回;必须在 onCreate 入口before就调度强拉。
*/
private static void hookSplashForceLogin(XC_LoadPackage.LoadPackageParam lpparam) {
final String splash = "my.com.tngdigital.ewallet.ui.SplashActivity";
try {
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnCreate",
Activity.class,
Bundle.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
final Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
String name = activity.getClass().getName();
if (!splash.equals(name) && !name.endsWith(".SplashActivity")) {
return;
}
XposedBridge.log(TAG + " Splash.onCreate enter — schedule force login");
scheduleForceLoginToUserLogin(
activity.getApplicationContext(),
activity,
"Splash/beforeOnCreate",
2000L);
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
final Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
String name = activity.getClass().getName();
if (!splash.equals(name) && !name.endsWith(".SplashActivity")) {
return;
}
try {
activity.reportFullyDrawn();
} catch (Throwable ignored) {
}
dismissSplashScreen(activity);
}
});
XposedBridge.log(TAG + " hooked Instrumentation Splash force→UserLogin (before+after)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Splash force hook failed: " + t.getMessage());
}
}
/** Application.onCreate 兜底Splash beforeHook 未触发时仍强拉 Login跳过纯 Service 进程)。 */
private static void hookAppAttachForceLogin(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
android.app.Application.class,
"onCreate",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
final Context appCtx = (Context) param.thisObject;
if (appCtx == null || !PACKAGE.equals(appCtx.getPackageName())) {
return;
}
String proc = getProcessName();
if (proc != null && proc.contains(":")) {
return;
}
XposedBridge.log(TAG + " Application.onCreate — schedule force login fallback");
scheduleForceLoginToUserLogin(appCtx, null, "Application/onCreate", 3500L);
}
});
XposedBridge.log(TAG + " hooked Application.onCreate force→UserLogin fallback");
} catch (Throwable t) {
XposedBridge.log(TAG + " Application force hook failed: " + t.getMessage());
}
}
private static void hookAntiSuicide() {
try {
XposedHelpers.findAndHookMethod(
Process.class,
"killProcess",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (((Integer) param.args[0]) == Process.myPid()) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked killProcess(self)");
param.setResult(null);
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
System.class,
"exit",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked System.exit(" + param.args[0] + ")");
param.setResult(null);
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Runtime.class,
"exit",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked Runtime.exit(" + param.args[0] + ")");
param.setResult(null);
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Runtime.class,
"halt",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
lastBlockedSuicideAt = System.currentTimeMillis();
XposedBridge.log(TAG + " blocked Runtime.halt(" + param.args[0] + ")");
param.setResult(null);
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " Runtime.halt 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());
}
XC_MethodHook blockFinish = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
if (!isSecurityRelatedActivity(activity)) {
return;
}
if (System.currentTimeMillis() - lastBlockedSuicideAt > SOFT_CRASH_GUARD_MS) {
return;
}
XposedBridge.log(TAG + " blocked " + param.method.getName()
+ " on " + activity.getClass().getSimpleName());
param.setResult(null);
}
};
try {
XposedHelpers.findAndHookMethod(Activity.class, "finish", blockFinish);
XposedHelpers.findAndHookMethod(Activity.class, "finishAffinity", blockFinish);
XposedHelpers.findAndHookMethod(Activity.class, "finishAndRemoveTask", blockFinish);
} catch (Throwable t) {
XposedBridge.log(TAG + " finish hooks failed: " + t.getMessage());
}
}
private static boolean isSecurityRelatedActivity(Activity activity) {
String name = activity.getClass().getName();
return name.contains("SecurityError")
|| name.contains("security.ui")
|| name.contains("Promon");
}
private static void hookBlockSecurityErrorLaunch(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook blockLaunch = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Intent intent = extractIntent(param.args);
if (intent != null && shouldBlockIntent(intent)) {
XposedBridge.log(TAG + " blocked intent via "
+ param.method.getDeclaringClass().getSimpleName()
+ "#" + param.method.getName()
+ " data=" + intent.getDataString());
param.setResult(null);
}
}
};
hookStartActivityOverloads("android.app.Activity", null, blockLaunch);
hookStartActivityOverloads("android.content.ContextWrapper", null, blockLaunch);
hookStartActivityOverloads("android.app.ContextImpl", null, blockLaunch);
try {
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"execStartActivity",
Context.class,
android.os.IBinder.class,
android.os.IBinder.class,
Activity.class,
Intent.class,
int.class,
Bundle.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Intent intent = (Intent) param.args[4];
if (shouldBlockIntent(intent)) {
XposedBridge.log(TAG + " blocked execStartActivity "
+ intent.getDataString());
param.setResult(-1);
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " execStartActivity hook failed: " + t.getMessage());
}
}
/** Promon Shield 安全回调由 handle*Callback / bl short-circuit 处理lifecycle 回调勿拦。 */
private static void hookStartActivityOverloads(
String className, ClassLoader classLoader, XC_MethodHook blockLaunch) {
try {
if ("android.app.Activity".equals(className)) {
XposedHelpers.findAndHookMethod(
Activity.class, "startActivity", Intent.class, blockLaunch);
XposedHelpers.findAndHookMethod(
Activity.class, "startActivity", Intent.class, Bundle.class, blockLaunch);
return;
}
if (classLoader == null) {
XposedHelpers.findAndHookMethod(
className, null, "startActivity", Intent.class, blockLaunch);
XposedHelpers.findAndHookMethod(
className, null, "startActivity", Intent.class, Bundle.class, blockLaunch);
return;
}
XposedHelpers.findAndHookMethod(
className, classLoader, "startActivity", Intent.class, blockLaunch);
XposedHelpers.findAndHookMethod(
className, classLoader, "startActivity", Intent.class, Bundle.class, blockLaunch);
} catch (Throwable t) {
XposedBridge.log(TAG + " startActivity hooks failed for " + className + ": "
+ t.getMessage());
}
}
private static Intent extractIntent(Object[] args) {
if (args == null) {
return null;
}
for (Object arg : args) {
if (arg instanceof Intent) {
return (Intent) arg;
}
}
return null;
}
private static boolean shouldBlockIntent(Intent intent) {
if (intent == null) {
return false;
}
if (isSecurityErrorIntent(intent)) {
return true;
}
Uri data = intent.getData();
if (data != null && isBlockedSupportUrl(data.toString())) {
return true;
}
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action) && data != null) {
return isBlockedSupportUrl(data.toString());
}
return false;
}
private static boolean isBlockedSupportUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
String lower = url.toLowerCase(Locale.US);
if (!lower.contains("support.tngdigital.com.my")) {
return false;
}
for (String marker : BLOCKED_SUPPORT_MARKERS) {
if (lower.contains(marker)) {
return true;
}
}
return lower.contains("rooting") || lower.contains("jailbroken")
|| lower.contains("emulator") || lower.contains("malware");
}
/** 阻止 Promon 混淆层抛出 W/bd:16 并触发浏览器 fallback。 */
private static void hookPromonNativeGuard(XC_LoadPackage.LoadPackageParam lpparam) {
hookPromonBlSwallowExceptions(lpparam);
for (String simple : new String[]{"W", "A", "bd"}) {
hookPromonExceptionClass(lpparam, simple);
}
hookPromonRunnable(lpparam);
}
/**
* Promon lifecycle1.9.10 用 vhvlnqgy.u旧版用 w。只吞异常不全拦。
*/
private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> promonExc = findPromonClass(lpparam.classLoader, "W");
if (promonExc == null) {
promonExc = findPromonClass(lpparam.classLoader, "bd");
}
final Class<?> promonExcFinal = promonExc;
for (String simple : new String[]{"w", "u"}) {
hookPromonLifecycleClass(lpparam, simple, promonExcFinal);
}
}
private static void hookPromonLifecycleClass(
XC_LoadPackage.LoadPackageParam lpparam,
String simpleName,
Class<?> promonExc) {
Class<?> lifecycleClass = findPromonClass(lpparam.classLoader, simpleName);
if (lifecycleClass == null) {
return;
}
final String lifecycleName = lifecycleClass.getName();
try {
int hooked = 0;
for (Method method : lifecycleClass.getDeclaredMethods()) {
String name = method.getName();
if (!name.startsWith("onActivity") && !name.startsWith("onApplication")) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!param.hasThrowable()) {
return;
}
Throwable t = param.getThrowable();
if (isPromonThrowable(t, promonExc)) {
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
+ " in " + lifecycleName + "#" + method.getName());
param.setThrowable(null);
}
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked
+ " " + lifecycleName + " lifecycle method(s) (afterHook only)");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " " + lifecycleName + " lifecycle hook failed: "
+ t.getMessage());
}
}
/** 吞掉 :tools 进程里 Promon 抛出的未捕获 W:16。 */
private static void hookUncaughtPromonException(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
Thread.class,
"dispatchUncaughtException",
Throwable.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Throwable t = (Throwable) param.args[0];
if (t == null || !isPromonThrowableName(t.getClass().getName())) {
return;
}
XposedBridge.log(TAG + " swallowed uncaught " + t.getClass().getSimpleName()
+ " in " + lpparam.processName);
param.setResult(null);
}
});
XposedBridge.log(TAG + " hooked Thread.dispatchUncaughtException");
} catch (Throwable t) {
XposedBridge.log(TAG + " dispatchUncaughtException hook failed: " + t.getMessage());
}
}
/**
* bl/R 全方法 short-circuita/b 之外的方法仍会跑 native~40s 后 stack_chk/SEGV。
* a.run 是 bl#b 后台 Runnable必须 beforeHook 直接 return。
*/
private static void hookPromonBlSwallowExceptions(XC_LoadPackage.LoadPackageParam lpparam) {
for (String simple : new String[]{"bl", "R"}) {
hookPromonClassShortCircuit(lpparam, simple);
}
}
private static void hookPromonClassShortCircuit(
XC_LoadPackage.LoadPackageParam lpparam, String simpleName) {
Class<?> clazz = findPromonClass(lpparam.classLoader, simpleName);
if (clazz == null) {
return;
}
try {
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
setSafeHookResult(param, method);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " " + clazz.getName()
+ " method(s), all short-circuit");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " " + clazz.getName() + " short-circuit failed: "
+ t.getMessage());
}
}
private static void fixNullPromonResult(XC_MethodHook.MethodHookParam param, Method method) {
if (param.getResult() != null) {
return;
}
Class<?> returnType = method.getReturnType();
if (returnType == Integer.class || returnType == int.class) {
XposedBridge.log(TAG + " fixed null bl#" + method.getName() + " -> 0");
setSafeHookResult(param, method);
}
}
/** Promon 后台 Runnablebl#b 检测线程beforeHook 直接 noop禁止跑 native。 */
private static void hookPromonRunnable(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> runnableClass = findPromonClass(lpparam.classLoader, "a");
if (runnableClass == null) {
XposedBridge.log(TAG + " Promon a runnable not found");
return;
}
try {
XposedHelpers.findAndHookMethod(runnableClass, "run", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(null);
}
});
XposedBridge.log(TAG + " hooked " + runnableClass.getName() + ".run (short-circuit)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Promon a.run hook failed: " + t.getMessage());
}
}
private static void hookPromonExceptionClass(
XC_LoadPackage.LoadPackageParam lpparam, String simpleName) {
Class<?> promonExc = findPromonClass(lpparam.classLoader, simpleName);
if (promonExc == null) {
return;
}
final String className = promonExc.getName();
try {
for (Method method : promonExc.getDeclaredMethods()) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " swallowed " + className + "#" + method.getName());
setSafeHookResult(param, method);
}
});
}
try {
XposedHelpers.findAndHookConstructor(promonExc, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " swallowed " + className + "<init>(int)");
param.setResult(null);
}
});
} catch (Throwable ignored) {
// constructor overload may differ
}
XposedBridge.log(TAG + " hooked Promon exception " + className);
} catch (Throwable t) {
XposedBridge.log(TAG + " " + className + " hook failed: " + t.getMessage());
}
}
private static boolean isPromonException(Throwable t, Class<?> promonExc) {
return isPromonThrowable(t, promonExc);
}
private static boolean isPromonThrowable(Throwable t, Class<?> promonExc) {
if (t == null) {
return false;
}
if (promonExc != null && promonExc.isInstance(t)) {
return true;
}
return isPromonThrowableName(t.getClass().getName());
}
private static boolean isPromonThrowableName(String className) {
if (className == null) {
return false;
}
if (!isPromonPackageClass(className)) {
return false;
}
// W/A/bd 等 Promon 异常;排除 bl/w/bg/R 等功能类
int dot = className.lastIndexOf('.');
if (dot < 0) {
return false;
}
String simple = className.substring(dot + 1);
if ("bd".equals(simple)) {
return true;
}
return simple.length() == 1;
}
private static boolean isPromonPackageClass(String className) {
for (String prefix : PROMON_PKG_PREFIXES) {
if (className.startsWith(prefix + ".")) {
return true;
}
}
return false;
}
private static Class<?> findPromonClass(ClassLoader loader, String simpleName) {
for (String prefix : PROMON_PKG_PREFIXES) {
try {
return XposedHelpers.findClass(prefix + "." + simpleName, loader);
} catch (Throwable ignored) {
}
}
return null;
}
/** SecurityGuard探测命令 stub10101 init + 104xx/105xx sign/verify 走真实 native。 */
private static void hookJnicLibrary(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"com.hzchengdun.securityguard.adapter.JNICLibrary",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(
clazz,
"doCommand",
int.class,
Object[].class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
int cmd = (Integer) param.args[0];
if (shouldStubJnicCmd(cmd)) {
Object[] payload = (Object[]) param.args[1];
Object stub = safeJnicReturn(cmd, payload);
XposedBridge.log(TAG + " stub JNICLibrary.doCommand cmd=" + cmd
+ " -> " + describeJnicResult(stub));
param.setResult(stub);
return;
}
if (shouldLogJnicCmd(cmd)) {
XposedBridge.log(TAG + " JNIC passthrough call cmd=" + cmd
+ " args=" + describeJnicArgs((Object[]) param.args[1]));
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
int cmd = (Integer) param.args[0];
if (shouldStubJnicCmd(cmd)) {
return;
}
if (!shouldLogJnicCmd(cmd)) {
return;
}
if (param.hasThrowable()) {
logJnicThrowable(cmd, param.getThrowable());
return;
}
XposedBridge.log(TAG + " JNIC passthrough cmd=" + cmd
+ " -> " + describeJnicResult(param.getResult()));
}
});
XposedBridge.log(TAG + " hooked JNICLibrary.doCommand (probe stub + init/verify passthrough)");
} catch (Throwable t) {
XposedBridge.log(TAG + " JNICLibrary hook failed: " + t.getMessage());
}
}
/** 仅 stub 低号 env/root 探测10101 init 与 104xx/105xx 必须 passthrough。 */
private static boolean shouldStubJnicCmd(int cmd) {
if (cmd == 10101 || cmd == 10102 || cmd == 10103 || cmd == 10104) {
return false;
}
if (cmd >= 10000) {
return false;
}
return true;
}
private static boolean shouldLogJnicCmd(int cmd) {
return cmd == 10101 || cmd == 10102 || cmd == 10103 || cmd == 10104
|| cmd == 10401 || cmd == 10501 || cmd == 10603
|| (cmd >= 10400 && cmd < 10700);
}
private static void logJnicThrowable(int cmd, Throwable t) {
StringBuilder sb = new StringBuilder();
sb.append(TAG).append(" JNIC passthrough cmd=").append(cmd)
.append(" err ").append(t.getClass().getName());
String msg = t.getMessage();
if (msg != null && !msg.isEmpty()) {
sb.append(" msg=").append(msg);
}
try {
Object code = XposedHelpers.callMethod(t, "getErrorCode");
if (code != null) {
sb.append(" errorCode=").append(code);
}
} catch (Throwable ignored) {
}
StackTraceElement[] stack = t.getStackTrace();
if (stack != null && stack.length > 0) {
sb.append(" at ").append(stack[0]);
}
XposedBridge.log(sb.toString());
}
private static String describeJnicArgs(Object[] args) {
if (args == null) {
return "null";
}
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < args.length; i++) {
if (i > 0) {
sb.append(", ");
}
Object arg = args[i];
if (arg == null) {
sb.append("null");
} else if (arg instanceof byte[]) {
sb.append("byte[").append(((byte[]) arg).length).append("]");
} else if (arg instanceof String) {
String s = (String) arg;
sb.append("String(").append(s.length() > 40 ? s.substring(0, 40) + "..." : s).append(")");
} else {
sb.append(arg.getClass().getSimpleName()).append("=").append(arg);
}
}
sb.append("]");
return sb.toString();
}
/**
* Aliyun TigerTally与 Promon 并行的设备指纹/风控 SDKlibtiger_tally.so
*
* ANR 根因2026-07-31init 链TigerTallyAPI.init → initCommon → t.B.genericNt1
* native会 fork 子进程跑 `getprop ro.build.version.sdk` 并用 pipe 等其 stdout。
* Zygisk tng_exit_guard 的 exit_group seccomp 被 fork 继承 → getprop 永不退出 →
* fread 永久阻塞 → StartupManager latch 卡死 → Application.onCreate ANR。
*
* 对策:对 init 链做 beforeHook 短路(跳过 native启动不再 fork 等待。
* 其它方法保留 afterHook 清异常软化。
*/
private static void hookTigerTally(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"com.aliyun.TigerTally.TigerTallyAPI",
"com.aliyun.TigerTally.t.B",
"com.aliyun.TigerTally.t.C",
"com.aliyun.TigerTally.s.A",
};
for (String className : classes) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
int shorted = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (Modifier.isAbstract(method.getModifiers())) {
continue;
}
// 不拦 Object 基础方法
String name = method.getName();
if ("equals".equals(name) || "hashCode".equals(name)
|| "toString".equals(name) || "getClass".equals(name)) {
continue;
}
final boolean sc = isTigerShortCircuit(className, name);
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (sc) {
XposedBridge.log(TAG + " TigerTally SC "
+ className + "#" + method.getName());
setSafeHookResult(param, method);
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (sc) {
return;
}
if (param.hasThrowable()) {
XposedBridge.log(TAG + " TigerTally err "
+ className + "#" + method.getName()
+ " " + param.getThrowable().getClass().getSimpleName()
+ ": " + param.getThrowable().getMessage());
param.setThrowable(null);
setSafeHookResult(param, method);
}
}
});
if (sc) {
shorted++;
}
hooked++;
}
XposedBridge.log(TAG + " hooked TigerTally " + className
+ " methods=" + hooked + " sc=" + shorted);
} catch (Throwable t) {
XposedBridge.log(TAG + " TigerTally " + className
+ " hook skipped: " + t.getMessage());
}
}
}
/** 短路 TigerTally 启动初始化native 会 fork getprop 等待 → seccomp 卡死 ANR。 */
private static boolean isTigerShortCircuit(String className, String methodName) {
if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className)
&& ("init".equals(methodName) || "initCommon".equals(methodName))) {
return true;
}
return "com.aliyun.TigerTally.t.B".equals(className)
&& "genericNt1".equals(methodName);
}
private static Object safeJnicReturn(int cmd, Object[] args) {
if (args != null) {
for (Object arg : args) {
if (arg instanceof byte[]) {
return new byte[0];
}
}
}
return Integer.valueOf(0);
}
private static String describeJnicResult(Object result) {
if (result == null) {
return "null";
}
if (result instanceof byte[]) {
return "byte[" + ((byte[]) result).length + "]";
}
return result.getClass().getSimpleName() + "=" + result;
}
/** 拦截 ActivityThread / Handler 触发的应用退出Promon 常走 native→H.exit。 */
private static void hookActivityThreadExit(XC_LoadPackage.LoadPackageParam lpparam) {
hookActivityThreadExitMethods(null);
hookActivityThreadExitMethods(lpparam.classLoader);
hookActivityThreadHandlerExit(lpparam);
hookShutdownExit();
}
private static void hookActivityThreadExitMethods(ClassLoader classLoader) {
try {
Class<?> atClass = XposedHelpers.findClass("android.app.ActivityThread", classLoader);
int hooked = 0;
for (Method method : atClass.getDeclaredMethods()) {
String name = method.getName();
if (!isActivityThreadExitMethod(name)) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked ActivityThread#" + name);
setSafeHookResult(param, method);
}
});
hooked++;
XposedBridge.log(TAG + " registered ActivityThread exit hook: " + name);
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " ActivityThread exit method(s)"
+ (classLoader == null ? " [boot]" : " [app]"));
}
} catch (Throwable t) {
XposedBridge.log(TAG + " ActivityThread exit hook failed: " + t.getMessage());
}
}
/** 仅拦截真正执行退出的方法,勿匹配 isHandleSplashScreenExit 等查询方法。 */
private static boolean isActivityThreadExitMethod(String name) {
if (name.startsWith("is") || name.startsWith("get") || name.startsWith("has")) {
return false;
}
String lower = name.toLowerCase(Locale.US);
return lower.contains("exitapplication")
|| lower.equals("exit")
|| lower.contains("handleexit")
|| lower.contains("appexit");
}
/** 拦截 EXIT_APPLICATION 等 Handler 消息Android 16 上 handleExitApplication 签名已变)。 */
private static void hookActivityThreadHandlerExit(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> handlerClass = XposedHelpers.findClass("android.app.ActivityThread$H", lpparam.classLoader);
XposedHelpers.findAndHookMethod(
handlerClass,
"handleMessage",
Message.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Message msg = (Message) param.args[0];
if (msg == null) {
return;
}
int what = msg.what;
// AOSP: KILL_APPLICATION=109, EXIT_APPLICATION=111
if (what == 109 || what == 111) {
XposedBridge.log(TAG + " blocked ActivityThread$H msg.what=" + what);
param.setResult(null);
}
}
});
XposedBridge.log(TAG + " hooked ActivityThread$H.handleMessage");
} catch (Throwable t) {
XposedBridge.log(TAG + " ActivityThread$H hook failed: " + t.getMessage());
}
}
private static void hookShutdownExit() {
try {
Class<?> shutdownClass = Class.forName("java.lang.Shutdown");
XposedHelpers.findAndHookMethod(
shutdownClass,
"exit",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked Shutdown.exit(" + param.args[0] + ")");
param.setResult(null);
}
});
XposedBridge.log(TAG + " hooked Shutdown.exit");
} catch (Throwable t) {
XposedBridge.log(TAG + " Shutdown.exit hook failed: " + t.getMessage());
}
}
/** 阻止 RuntimeInit 默认 handler 因 Promon W 异常杀进程。 */
private static void hookKillApplicationHandler(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> handlerClass = XposedHelpers.findClass(
"com.android.internal.os.RuntimeInit$KillApplicationHandler",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(
handlerClass,
"uncaughtException",
Thread.class,
Throwable.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Throwable t = (Throwable) param.args[1];
if (t == null || !isPromonThrowableName(t.getClass().getName())) {
return;
}
XposedBridge.log(TAG + " blocked KillApplicationHandler for "
+ t.getClass().getSimpleName()
+ " proc=" + lpparam.processName);
param.setResult(null);
}
});
XposedBridge.log(TAG + " hooked RuntimeInit$KillApplicationHandler");
} catch (Throwable t) {
XposedBridge.log(TAG + " KillApplicationHandler hook failed: " + t.getMessage());
}
}
/** Promon / SecurityError 强制退出倒计时。 */
/**
* dexdump 定位UnhandledEvent 走
* AppSecurityManager.showSecurityScreenForState → _ContextKt.finishAllActivityAndKillApp。
* 这是 UserLogin 之后 Java 层杀进程的主路径之一。
*/
private static void hookFinishAllActivityAndKillApp(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"my.com.tngdigital.common.internal._ContextKt",
"my.com.tngdigital.common.internal.ContextKt",
};
for (String className : classes) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
if (!name.toLowerCase(Locale.US).contains("finishall")
&& !name.toLowerCase(Locale.US).contains("killapp")
&& !name.equals("finishAllActivityAndKillApp")) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked " + className + "#" + name);
param.setResult(null);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " kill-app method(s) in " + className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
}
}
}
/** 拦截把 UnhandledEvent 导航成杀进程的入口。 */
private static void hookShowSecurityScreenForState(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.security.shielding.AppSecurityManager",
lpparam.classLoader);
Class<?> unhandled = null;
try {
unhandled = XposedHelpers.findClass(
"my.com.tngdigital.common.security.model.UnhandledEvent",
lpparam.classLoader);
} catch (Throwable ignored) {
// optional
}
final Class<?> unhandledFinal = unhandled;
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!"showSecurityScreenForState".equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Object state = param.args != null && param.args.length > 1
? param.args[1] : null;
if (state != null && unhandledFinal != null) {
try {
Object eventInfo = XposedHelpers.callMethod(state, "getEventInfo");
if (unhandledFinal.isInstance(eventInfo)) {
XposedBridge.log(TAG + " blocked showSecurityScreenForState UnhandledEvent");
param.setResult(null);
return;
}
} catch (Throwable ignored) {
// fall through to blanket block
}
}
XposedBridge.log(TAG + " blocked showSecurityScreenForState");
param.setResult(null);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " showSecurityScreenForState");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " showSecurityScreenForState hook failed: " + t.getMessage());
}
}
private static void hookForceExitFlow(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity",
"my.com.tngdigital.common.security.ui.SecurityErrorActivity",
"my.com.tngdigital.common.security.SecurityForceExitCountdownPolicyKt",
"my.com.tngdigital.common.security.shielding.AppSecurityManager",
};
for (String className : classes) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
String lower = name.toLowerCase(Locale.US);
if (!lower.contains("forceexit")
&& !lower.contains("exitcountdown")) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked " + className + "#" + name);
setSafeHookResult(param, method);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " force-exit/queue method(s) in " + className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
}
}
}
/** 消费 Promon Java 层安全回调,避免检测后走 SecurityError / native fallback 退出链。 */
private static void hookAppSecurityCallbacks(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.security.shielding.AppSecurityManager",
lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
if (!name.startsWith("handle") || !name.endsWith("Callback")) {
continue;
}
hookMethodNoopSilent(method);
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " AppSecurityManager callback(s) (silent)");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " AppSecurityManager callbacks hook failed: " + t.getMessage());
}
}
/** Promon native 桥接类:强制 int/boolean 检测返回安全值。 */
private static void hookPromonNativeBridge(XC_LoadPackage.LoadPackageParam lpparam) {
String[] simpleNames = {
"F", "bg", "b", "c", "d", "h", "k", "l", "m", "o", "s", "t", "z",
};
int total = 0;
for (String simpleName : simpleNames) {
Class<?> clazz = findPromonClass(lpparam.classLoader, simpleName);
if (clazz != null) {
total += hookPromonIntBooleanMethods(clazz);
}
}
if (total > 0) {
XposedBridge.log(TAG + " Promon native-bridge total hooks=" + total);
}
}
private static int hookPromonIntBooleanMethods(Class<?> clazz) {
int count = 0;
try {
for (Method method : clazz.getDeclaredMethods()) {
Class<?> returnType = method.getReturnType();
if (returnType != boolean.class && returnType != Boolean.class
&& returnType != int.class && returnType != Integer.class) {
continue;
}
if (method.getParameterTypes().length > 6) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
int depth = PROMON_BRIDGE_DEPTH.get();
if (depth > 0) {
return;
}
PROMON_BRIDGE_DEPTH.set(depth + 1);
try {
if (returnType == boolean.class || returnType == Boolean.class) {
param.setResult(false);
} else {
param.setResult(0);
}
} finally {
PROMON_BRIDGE_DEPTH.set(depth);
}
}
});
count++;
}
} catch (Throwable ignored) {
// class may be absent in this process
}
return count;
}
private static void hookSecurityUrlOpeners(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"my.com.tngdigital.common.security.shielding.AppSecurityManager",
"my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity",
"my.com.tngdigital.common.security.ui.SecurityErrorActivity",
"my.com.tngdigital.common.security.shielding.utils.HelperKt",
"my.com.tngdigital.app.launcher.initializer.AppSecurityInitializer",
};
String[] methods = {
"openSecurityUrl", "openUrlByBrowser", "openBrowser", "openUrl",
"openWebUrl",
};
for (String className : classes) {
for (String methodName : methods) {
hookUrlOrCallbackMethod(lpparam, className, methodName);
}
}
}
private static void hookUrlOrCallbackMethod(
XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!methodName.equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args != null && param.args.length > 0
&& param.args[0] instanceof String) {
String url = (String) param.args[0];
if (isBlockedSupportUrl(url)) {
XposedBridge.log(TAG + " blocked " + className
+ "#" + methodName + " url=" + url);
setSafeHookResult(param, method);
return;
}
}
if ("openSecurityUrl".equals(methodName)
|| "openUrlByBrowser".equals(methodName)
|| "openBrowser".equals(methodName)) {
XposedBridge.log(TAG + " blocked " + className + "#" + methodName);
setSafeHookResult(param, method);
}
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " " + className + "#" + methodName);
}
} catch (Throwable ignored) {
// class/method may not exist in this APK split
}
}
private static void hookMethodNoop(Method method, String label) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked " + label);
setSafeHookResult(param, method);
}
});
XposedBridge.log(TAG + " hooked " + label);
}
/** 高频回调(如 handleTapjackingCallback禁止逐次打 log避免复进主线程 ANR。 */
private static void hookMethodNoopSilent(Method method) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
setSafeHookResult(param, method);
}
});
}
/** methodName 为 null 时 hook 类内全部方法(用于 Promon 混淆类)。 */
private static void hookAllMethodsNoop(
XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (methodName != null && !methodName.equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " blocked " + className + "#" + method.getName());
setSafeHookResult(param, method);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " method(s) in " + className
+ (methodName != null ? "#" + methodName : ""));
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
}
}
private static boolean isSecurityErrorIntent(Intent intent) {
if (intent == null) {
return false;
}
if (intent.getComponent() != null) {
String cls = intent.getComponent().getClassName();
if (cls != null && cls.contains("SecurityError")) {
return true;
}
}
String target = intent.getStringExtra("targetActivity");
return target != null && target.contains("SecurityError");
}
private static void hookJailBroken(XC_LoadPackage.LoadPackageParam lpparam) {
hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
"showJailBrokenAlert");
hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
"showJailBrokenAlert$lambda$15");
hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"isJailBroken");
hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"detectJailBroken");
hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
"isJailBroken");
hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
"detectJailBroken");
// 以下为 void 回调(非 boolean触发时弹窗/杀进程,必须 noop。
hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"onBlockStaticCheck");
hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"onShowPopupDisable");
hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"onEmptyToken");
hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
"onRpcCheckValid");
hookJailBrokenResult(lpparam);
}
/** 拦截 jail.broken.detect RPC本地直接回调「未越狱」结果不发网关。 */
private static void hookJailBrokenRpc(XC_LoadPackage.LoadPackageParam lpparam) {
String implClass = "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl";
String[] shortCircuitMethods = {
"rpcCheckJailBroken",
"jailBrokenDetect",
"rpcCheck",
};
try {
Class<?> clazz = XposedHelpers.findClass(implClass, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
boolean match = false;
for (String target : shortCircuitMethods) {
if (target.equals(name)) {
match = true;
break;
}
}
if (!match) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " skip jail RPC " + implClass + "#" + name);
Object clean = buildCleanJailBrokenResult(lpparam.classLoader);
invokeJailBrokenCallbacks(param.args, clean);
setSafeHookResult(param, method);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked jail RPC short-circuit methods=" + hooked);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " jail RPC short-circuit failed: " + t.getMessage());
}
hookRpcInvocationJailBypass(lpparam);
}
private static void hookRpcInvocationJailBypass(XC_LoadPackage.LoadPackageParam lpparam) {
String[] rpcClasses = {
"my.com.tngdigital.common.aliservice.quake.TngdRpcInvocationHandlerHost",
"com.alipay.imobile.network.quake.rpc.RpcInvocationHandler",
};
XC_MethodHook rpcHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String op = extractRpcOperation(param.args);
if (op != null && isRegistrationRpc(op)) {
XposedBridge.log(TAG + " RPC reg/login op=" + op + " "
+ describeRpcInvocation(param.args));
}
if (!isJailBrokenRpcInvocation(param.args)) {
return;
}
XposedBridge.log(TAG + " block jail.broken RPC invoke "
+ describeRpcInvocation(param.args));
Object clean = buildCleanJailBrokenResult(lpparam.classLoader);
if (clean != null) {
param.setResult(clean);
return;
}
param.setResult(null);
}
};
for (String className : rpcClasses) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!"invoke".equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, rpcHook);
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked RPC guard+log " + className
+ " invoke=" + hooked);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip RPC guard " + className
+ ": " + t.getMessage());
}
}
}
private static String extractRpcOperation(Object[] args) {
if (args == null) {
return null;
}
for (Object arg : args) {
if (!(arg instanceof String)) {
continue;
}
String text = (String) arg;
if (text.startsWith("com.abl.")
|| text.startsWith("ap.tngd")
|| text.startsWith("alipayplus.")) {
return text;
}
}
return null;
}
private static boolean isRegistrationRpc(String op) {
if (op == null) {
return false;
}
String lower = op.toLowerCase(Locale.US);
for (String marker : REGISTRATION_RPC_MARKERS) {
if (lower.contains(marker)) {
return true;
}
}
return false;
}
private static boolean isJailBrokenRpcInvocation(Object[] args) {
if (args == null) {
return false;
}
for (Object arg : args) {
if (arg == null) {
continue;
}
if (arg instanceof Method) {
String name = ((Method) arg).getName().toLowerCase(Locale.US);
if (name.contains("jail")) {
return true;
}
continue;
}
if (arg instanceof String) {
String text = ((String) arg).toLowerCase(Locale.US);
if (text.contains("jail.broken")
|| text.contains("jailbroken")
|| text.contains("jail_broken")) {
return true;
}
continue;
}
// 勿对 RPC 动态代理 toString会再次进入 invoke 导致栈溢出。
if (Proxy.isProxyClass(arg.getClass())) {
continue;
}
Class<?> clazz = arg.getClass();
if (clazz.isArray()) {
continue;
}
String simple = clazz.getSimpleName().toLowerCase(Locale.US);
if (simple.contains("jailbroken") || simple.contains("jailbrokenrequest")) {
return true;
}
}
return false;
}
private static String describeRpcInvocation(Object[] args) {
if (args == null || args.length == 0) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < args.length; i++) {
if (i > 0) {
sb.append(", ");
}
Object arg = args[i];
if (arg instanceof Method) {
sb.append("Method=").append(((Method) arg).getName());
} else if (arg instanceof String) {
String s = (String) arg;
sb.append(s.length() > 120 ? s.substring(0, 120) + "..." : s);
} else if (Proxy.isProxyClass(arg.getClass())) {
sb.append("Proxy=").append(arg.getClass().getInterfaces()[0].getSimpleName());
} else {
sb.append(arg.getClass().getSimpleName());
}
}
sb.append("]");
return sb.toString();
}
private static Object buildCleanJailBrokenResult(ClassLoader loader) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.jailbrkendetect.JailBrokenResult", loader);
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
Class<?>[] types = constructor.getParameterTypes();
Object[] args = new Object[types.length];
boolean ok = true;
for (int i = 0; i < types.length; i++) {
Class<?> type = types[i];
if (type == boolean.class || type == Boolean.class) {
args[i] = Boolean.FALSE;
} else if (type == String.class) {
args[i] = "";
} else if (type == int.class) {
args[i] = 0;
} else if (type == Integer.class) {
args[i] = Integer.valueOf(0);
} else if (!type.isPrimitive()) {
args[i] = null;
} else {
ok = false;
break;
}
}
if (!ok) {
continue;
}
try {
constructor.setAccessible(true);
Object result = constructor.newInstance(args);
sanitizeJailBrokenResult(result);
return result;
} catch (Throwable ignored) {
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " buildCleanJailBrokenResult failed: " + t.getMessage());
}
return null;
}
private static void sanitizeJailBrokenResult(Object result) {
if (result == null) {
return;
}
try {
for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) {
field.setAccessible(true);
String name = field.getName().toLowerCase(Locale.US);
Class<?> type = field.getType();
if ((type == boolean.class || type == Boolean.class)
&& (name.contains("jail") || name.contains("root"))) {
field.set(result, Boolean.FALSE);
} else if (type == String.class && name.contains("msg")) {
field.set(result, "");
}
}
} catch (Throwable ignored) {
}
}
private static void invokeJailBrokenCallbacks(Object[] args, Object cleanResult) {
if (args == null || cleanResult == null) {
return;
}
for (Object arg : args) {
if (arg == null) {
continue;
}
try {
for (Method method : arg.getClass().getDeclaredMethods()) {
if (method.getParameterTypes().length != 1) {
continue;
}
Class<?> paramType = method.getParameterTypes()[0];
if (!paramType.getName().contains("JailBrokenResult")) {
continue;
}
method.setAccessible(true);
method.invoke(arg, cleanResult);
XposedBridge.log(TAG + " delivered clean JailBrokenResult via "
+ arg.getClass().getSimpleName() + "#" + method.getName());
}
} catch (Throwable ignored) {
}
try {
XposedHelpers.callMethod(arg, "invoke", cleanResult);
} catch (Throwable ignored) {
}
}
}
private static void hookJailBrokenResult(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> resultClass = XposedHelpers.findClass(
"my.com.tngdigital.common.jailbrkendetect.JailBrokenResult",
lpparam.classLoader);
for (Method method : resultClass.getDeclaredMethods()) {
String name = method.getName();
Class<?> returnType = method.getReturnType();
if (returnType == boolean.class || returnType == Boolean.class) {
if (name.toLowerCase(Locale.US).contains("jail")
|| name.toLowerCase(Locale.US).contains("root")) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(false);
}
});
XposedBridge.log(TAG + " hooked JailBrokenResult#" + name);
}
} else if (returnType == String.class
&& name.toLowerCase(Locale.US).contains("msg")) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult("");
}
});
XposedBridge.log(TAG + " hooked JailBrokenResult#" + name);
}
}
XposedHelpers.findAndHookConstructor(resultClass, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
sanitizeJailBrokenResult(param.getResult());
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " JailBrokenResult hooks failed: " + t.getMessage());
}
}
private static void hookAppSecurityManager(XC_LoadPackage.LoadPackageParam lpparam) {
// addIntoQueue 勿拦:拦截后 Promon 会走 native _exit fallback
hookReturnFalse(lpparam,
"my.com.tngdigital.common.security.malwarescan.MalwareScanUtils",
"isShowErrorScreen");
}
private static void hookSecurityBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam) {
for (String className : BOOLEAN_HOOK_CLASSES) {
RootBypassHelper.hookSecurityClass(lpparam, className);
}
}
private static void hookSecurityErrorActivity(XC_LoadPackage.LoadPackageParam lpparam) {
hookSecurityErrorLaunch(lpparam);
hookGenericSecurityErrorFinish();
XC_MethodHook closeSecurityScreen = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
XposedBridge.log(TAG + " closing " + activity.getClass().getSimpleName());
activity.finish();
}
};
for (String activityClass : new String[]{
SECURITY_ERROR_ACTIVITY,
"my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity",
}) {
hookAllOnCreateMethods(lpparam, activityClass, closeSecurityScreen);
}
}
private static void hookAllOnCreateMethods(
XC_LoadPackage.LoadPackageParam lpparam,
String className,
XC_MethodHook hook) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!"onCreate".equals(method.getName())) {
continue;
}
XposedBridge.hookMethod(method, hook);
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " onCreate overload(s) in " + className);
} else {
XposedBridge.log(TAG + " no onCreate in " + className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " " + className + " onCreate hook failed: " + t.getMessage());
}
}
/** SecurityErrorActivity 为 Compose无独立 onCreate在 Activity 基类统一 finish。 */
private static void hookGenericSecurityErrorFinish() {
try {
XposedHelpers.findAndHookMethod(
Activity.class,
"onCreate",
Bundle.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
String name = activity.getClass().getName();
if (name.contains("SecurityError")) {
XposedBridge.log(TAG + " finishing " + name);
activity.finish();
}
}
});
XposedBridge.log(TAG + " hooked Activity.onCreate SecurityError finish");
} catch (Throwable t) {
XposedBridge.log(TAG + " generic SecurityError finish failed: " + t.getMessage());
}
}
/** 拦截 Security 状态机启动 SecurityErrorActivity。 */
private static void hookSecurityErrorLaunch(XC_LoadPackage.LoadPackageParam lpparam) {
// launchProcessNextSecurityStateIfIdle / addIntoQueue 勿拦:
// 拦截后 Promon 会立刻走 native exit_group(1)。
// 杀进程改由 finishAllActivityAndKillApp / showSecurityScreenForState 兜底。
XposedBridge.log(TAG + " skip launchProcessNext noop (avoid native exit fallback)");
}
private static void hookReturnFalse(
XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!methodName.equals(method.getName())) {
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);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " overload(s) "
+ className + "#" + methodName + " -> false");
} else {
XposedBridge.log(TAG + " no boolean method " + className + "#" + methodName);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + "#" + methodName + ": " + t.getMessage());
}
}
private static void hookNoArgVoid(
XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
hookAllMethodsNoop(lpparam, className, methodName);
}
private static void setSafeHookResult(XC_MethodHook.MethodHookParam param, Method method) {
Class<?> returnType = method.getReturnType();
if (returnType == void.class) {
param.setResult(null);
} else if (returnType == boolean.class) {
param.setResult(false);
} else if (returnType == int.class) {
param.setResult(0);
} else if (returnType == long.class) {
param.setResult(0L);
} else if (returnType == float.class) {
param.setResult(0f);
} else if (returnType == double.class) {
param.setResult(0d);
} else if (returnType == byte.class) {
param.setResult((byte) 0);
} else if (returnType == short.class) {
param.setResult((short) 0);
} else if (returnType == char.class) {
param.setResult((char) 0);
} else if (returnType == Boolean.class) {
param.setResult(Boolean.FALSE);
} else if (returnType == Integer.class) {
param.setResult(Integer.valueOf(0));
} else if (returnType == Long.class) {
param.setResult(Long.valueOf(0L));
} else if (returnType == Float.class) {
param.setResult(Float.valueOf(0f));
} else if (returnType == Double.class) {
param.setResult(Double.valueOf(0d));
} else if (returnType == String.class) {
param.setResult("");
} else if (returnType == byte[].class) {
param.setResult(new byte[0]);
} else {
param.setResult(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,14 @@
<?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>
<item>sg.com.maribankmobile.digitalbank</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>