Compare commits

..

15 Commits

Author SHA1 Message Date
mars
e80f7f908c fix(tng): Splash 卡住才救援,并跳过首页强制 eKYC
避免已登录冷启动被强拉回登录页;HomeEkycVerify 测试期 finish + canBypass/enforce stub。
2026-08-04 14:10:24 +08:00
mars
f9426d6b68 chore: 清理临时逆向脚本,保留 TNG 安装工具与 captcha 文档
删除未跟踪的一次性 _*.py 扫描脚本,并从仓库移除已过时辅助脚本。
2026-08-04 13:48:47 +08:00
mars
bc51fb35cc chore(tng): 清理死代码并停止跟踪 reverse/dumps
精简 login-first 不可达 Hook,忽略调试产物目录,从仓库移除已入库的 dumps 文件。
2026-08-04 13:32:57 +08:00
mars
5378a34f58 fix(tng): 修复区号页 HWUI 闪退并放行 Compose HW 绘制
拦截 HardwareRenderer.setName,校验/缓存 libandroid.so,避免软件绘制撞 hardware bitmap;附带 Money Packet hook 与 mitm 脚本。
2026-08-04 12:26:27 +08:00
mars
609635aba1 chore: 备份 TNG 注册/captcha 逆向与 MariBank SG bypass 进展
TngRootBypassHook 增强 captcha 诊断、TigerTally/JNIC 分层与 HWUI 策略;新增逆向脚本、Frida 工具与 UI dump;同步 MariBank SG hook 与 tng_exit_guard 更新。
2026-08-03 15:23:02 +08:00
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
286 changed files with 26195 additions and 134 deletions

20
.gitignore vendored
View File

@@ -13,3 +13,23 @@
.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
debug-server/__pycache__/
magisk-modules/tng_exit_guard/obj/
magisk-modules/tng_exit_guard/libs/
# 调试抓包/截图/ANR/APK 产物,不入库
reverse/dumps/
reverse/scripts/__pycache__/

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()

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
mitmproxy 插件:自动保存 TNG Money Packet 含 receiverList 的 API 响应。
用法:
mitmdump -s debug-server/tng_mmp_mitm_addon.py -p 8888
mitmweb -s debug-server/tng_mmp_mitm_addon.py -p 8888
手机 WiFi 代理 -> PC_IP:8888安装 mitmproxy CA 后打开 TNG 红包 Leaderboard。
命中响应会打印到终端,并写入 reverse/dumps/mitm_mmp/
"""
from __future__ import annotations
import json
import os
import re
from datetime import datetime
from mitmproxy import ctx, http
OUTPUT_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"reverse",
"dumps",
"mitm_mmp",
)
MMP_HINTS = (
"receiverlist",
"claimedamount",
"mmpreceiver",
"moneypacket",
"merchantmoneypacket",
)
HOST_HINTS = (
"ebuckler.com",
"tngdigital.com",
"alipaydev.com",
)
def _ensure_dir() -> None:
os.makedirs(OUTPUT_DIR, exist_ok=True)
def _looks_like_mmp(body: str) -> bool:
lower = body.lower()
if any(h in lower for h in MMP_HINTS):
return True
return "mmp" in lower and "amount" in lower
def _extract_receiver_list(obj):
"""递归找 receiverList 并格式化为 [(nickname, amount), ...]"""
rows = []
def walk(node):
if isinstance(node, dict):
if "receiverList" in node and isinstance(node["receiverList"], list):
for item in node["receiverList"]:
if not isinstance(item, dict):
continue
name = (
item.get("nickName")
or item.get("displayName")
or item.get("userName")
or item.get("receiverName")
or item.get("name")
)
amount = (
item.get("claimedAmount")
or item.get("receiveAmount")
or item.get("amount")
)
if name and amount is not None:
rows.append((str(name), str(amount)))
for v in node.values():
walk(v)
elif isinstance(node, list):
for v in node:
walk(v)
walk(obj)
return rows
class TngMmpCapture:
def __init__(self) -> None:
_ensure_dir()
self.count = 0
ctx.log.info(f"TNG MMP capture -> {OUTPUT_DIR}")
def response(self, flow: http.HTTPFlow) -> None:
if flow.response is None or not flow.response.content:
return
host = (flow.request.host or "").lower()
if not any(h in host for h in HOST_HINTS):
return
try:
text = flow.response.get_text(strict=False)
except Exception:
return
if not text or not _looks_like_mmp(text):
return
self.count += 1
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_host = re.sub(r"[^\w.-]", "_", host)[:40]
path = os.path.join(OUTPUT_DIR, f"mmp_{ts}_{self.count}_{safe_host}.json")
summary_lines = [
f"[TNG-MMP #{self.count}] {flow.request.method} {flow.request.url}",
]
try:
data = json.loads(text)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
rows = _extract_receiver_list(data)
if rows:
summary_lines.append(f" receiverList ({len(rows)} 条):")
for name, amount in rows:
summary_lines.append(f" {name} -> {amount}")
else:
summary_lines.append(" (JSON 已保存,未解析到 receiverList)")
except json.JSONDecodeError:
with open(path, "w", encoding="utf-8") as f:
f.write(text)
summary_lines.append(" (非 JSON已保存原文)")
summary_lines.append(f" saved: {path}")
ctx.log.info("\n".join(summary_lines))
addons = [TngMmpCapture()]

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,590 @@
# MariBank SG `3100012` 根因分析与突破方案
> 基于 `register_20260706_1636.txt`675 行)+ 全部 Hook 源码 + 逆向文档综合分析
---
## 一、关键日志发现
### 1.1 加密前明文已被完整捕获
日志行 **#400**`uvwuvwuv.vvuuvvv``uvwvuww` 加密入口前):
```json
{
"cyCode": "65",
"paramInfo": {"publicKey": "MIIBIjAN...(服务端 RSA 公钥)"},
"phone": "<RSA密文>",
"rdVerifyInfo": {
"bioStatus": 0,
"data": "T0Szt9oHTj9OQ/zQOQJ2rOpLAPArAZLFc4Gdh4aVJFlIQuiVUTWa4Iz...",
"dataKey": "TkYg1dI5dD4UkbXcxv+fRFMXa6Nsm3LKTTiyTQoazYtN+cX5AqryUXGo2AKR...",
"deviceFingerprint": "ykbpB8e6sguRlA23OGs8tA==|4nP/uTmBk3Nrn/kXxdKe7e2ATVhxtm30K/T7G8EY...|8+hvSUQahER+Tpwd|00|0",
"random": "1783326963701_-4760471421264355822",
"softTokenActivated": false,
"afExtInfo": {"modeInCall":"N","modeInCommunication":"N","modeCallScreening":"N"}
},
"scene": "REGISTRATION",
"step": "BE"
}
```
**关键结论**`data`/`dataKey`**native 生成后「已是密文」** 地塞进这个 JSON 的,不是在这个 JSON 组装后再加密的。Java 层改此 JSON 不影响 `data`/`dataKey` 内容本身,因为此时内容已经是 native 加密过的密文。
### 1.2 `data`/`dataKey` 尺寸分析
- `dataLen=154`Base64字符数→ 原始 **~115 字节**
- `dataKeyLen=351` → 原始 **~263 字节**
RSA-2048 密文 = 256 bytes → base64 = 344 chars351 比 344 多 7可能含头部或为 RSA-2048+padding
**推断加密结构**
```
dataKey = Base64( RSA_OAEP_encrypt( AES_session_key_32bytes, server_RSA_pubkey ) )
data = Base64( AES_GCM_encrypt( env_attestation_json, AES_session_key ) )
```
### 1.3 register body 的加密密钥链路(行 359-368
```
uvwuvwuv.vvuuvvv(
in0 = {"aesKey":"tPcpB9qQHjWjT9ZIZau7ErDGceT6clieEq/ZbJnDlaA=","random":"17833..."},
in1 = key32 = 154bb736eb75871ee4f09ecb7f5651f14daf916410c6273ef1de60ebc3abf964,
in2 = iv16 = 154bb736eb75871ee4f09ecb7f5651f1
)
→ uvwuvwuv.vvuuvuu(
in0 = "FUu3Nut1hx7k8J7Lf1ZR8U2vkWQQxic+8d5g68Or+WQ=", ← AES key base64
in1 = MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ← RSA公钥2048-bit
)
```
这条链路加密的是 **register body 外层(`encryptData`字段)**,不是 `data`/`dataKey`
### 1.4 🔑 `proc_version` AVC Denied行 284— 关键缺口
```
07-06 16:36:01.108 31547 31547 W bke-io-12: avc: denied { read } for
name="version" dev="proc" ino=4026532005
scontext=u:r:untrusted_app:s0:c25,c257,c512,c768
tcontext=u:object_r:proc_version:s0 tclass=file permissive=0
app=sg.com.maribankmobile.digitalbank
```
**这是最重要的发现**`libshpssdk_bank.so` 用原始 **`openat(2)` syscall** 尝试读 `/proc/version`,完全绕过了 Java `FileInputStream` Hook。虽然这次被 SELinux 拒绝了permissive=0但说明 native 在走独立的系统调用路径探测内核版本信息。
---
## 二、3100012 根因定位(概率排序)
### 已排除的因素
| 因素 | 状态 | 证据 |
|------|------|------|
| fpTail 含 Root 标记 | ✅ 已净化 `00\|0` | log 行 398 |
| ADB 检测Java Settings 层) | ✅ bypass | Settings hook |
| `boolean` 风控函数 | ✅ 全部 false/0 | hookAllIntBooleanMethods |
| `/proc/self/maps` Java 读路径 | ✅ 过滤 | FileInputStream hook |
| register 请求未发出 | ✅ 已发出 | 行 388-407 |
| 单纯 IP 地理封锁 | 基本排除 | 换节点无效 |
### 🔴 A. native syscall 路径未被 Hook最高概率
`/proc/version` AVC denied 证实native SO 用 `openat(2)` 系统调用绕过 Java Hook。
**可能被 native 用 syscall 探测的路径**
- `/proc/self/maps` → 直接 mmap 或 read 系统调用,发现 `liblspd.so`/`libzygisk.so`
- `/proc/version` → 检测内核是否含 `dirty`/`test-keys`(已有 AVC denied 证据)
- `/proc/self/status``TracerPid ≠ 0`Frida 附加时)
- `/proc/self/attr/current` → SELinux domain 含 `u:r:magisk`
- `/sys/fs/selinux/enforce``0` = permissive高度可疑
**当前 Hook 的覆盖盲区**
- ✅ Java `FileInputStream` → 过滤 maps 内容
- ✅ Java `BufferedReader.readLine()` → 过滤 maps 行
- ❌ native `openat()` syscall → **未拦截**
- ❌ native `mmap()` 直读 /proc → **未拦截**
-`dl_iterate_phdr()` 枚举所有 .so → **未拦截**
### 🔴 B. Play Integrity 级别不足(高概率)
Pixel 6 解锁 bootloader 后 Play Integrity 状态:
- `MEETS_BASIC_INTEGRITY`
- `MEETS_DEVICE_INTEGRITY` ❌(需要 locked bootloader + certified device
- `MEETS_STRONG_INTEGRITY` ❌(需要 hardware-backed attestation
**SG vs PH 的差异**SG MariBank v3.2.2 服务端策略很可能要求 `MEETS_DEVICE_INTEGRITY`,而 PH SeaBank 3.22.0 可能仅要求 `MEETS_BASIC_INTEGRITY`。当前 Hook 无任何 Play Integrity API 覆盖。
### 🟡 C. `data` 内部含 Hook/Magisk 特征(中概率)
`libshpssdk_bank.so` 生成 `data` 时在 native 层可能检测:
- `dl_iterate_phdr()` → 遍历到 `liblspd.so` / `libgadget.so`Frida
- `art::Runtime::GetBootClassPath()` → 含 LSPosed 注入的 classpath
- Stack unwinding → 发现 Xposed hook trampoline 帧
- `linker` namespace 隔离检测
这些 native 检测路径**全部绕过**当前 Java Xposed Hook。
### 🟡 D. 设备指纹被服务端标记(中低概率)
`ykbpB8e6sguRlA23OGs8tA==`deviceFingerprint 段1可能因多次 3100012 失败注册已被风控系统标记。但可通过**换 serial/android_id已做**后 fingerprint 值是否变化来验证。
---
## 三、7个核心问题的逆向答案
### Q1: `rdVerifyInfo.data` 明文结构推断
基于 Shopee SHPSSDK 体系SeaBank PH 同源 SDK 已知结构):
```json
{
"appId": "sg.com.maribankmobile.digitalbank",
"appVersion": "3.2.2",
"deviceId": "<ANDROID_ID or serial hash>",
"isRoot": false, Hook native
"isEmulator": false, OK
"isHooked": false, native dl_iterate liblspd
"bootloaderLocked": false, Pixel 6
"integrityResult": "BASIC", SG DEVICE
"selinuxEnforcing": true, OKpermissive=0
"timestamp": 1783326963701,
"random": "1783326963701_-4760471421264355822",
"nonce": "<random bytes>"
}
```
`isHooked`native 检测到 liblspd.so`integrityResult`(非 DEVICE 级别)是最可能触发 3100012 的字段。
### Q2: 哪条 native 函数生成 `data`
根据 RegisterNatives 输出应能找到(需 Frida spawn 验证):
```
com.shopee.shpssdkbank.wvvvuwwu.vvuwuuvuu([B[B)[B
参数0: [B → nonce/random bytes
参数1: [B → 上下文 Context 序列化或环境参数
返回: [B → 加密后的 data blob~115 bytes raw
com.shopee.shpssdkbank.wvvvuwwu.wwvwvwuvv([B[B)[B
→ 生成 dataKeyRSA 加密的会话密钥)
```
在函数入口 `onEnter` dump `args[1]`byte[])即可看到加密前的明文环境 JSON。
### Q3: SG vs PH attestation 差异
| 项目 | PH 3.22.0 | SG 3.2.2 |
|------|-----------|----------|
| SDK 包 | `shpssdk` + `shpssdkbank` | 仅 `shpssdkbank` |
| Play Integrity 要求 | BASIC推断 | DEVICE推断 |
| `vvuwuuvuu` 检测项 | 基础版 | 增强版(多出 bootloader/integrity 检测)|
| 失败阈值 | 较低 | 较高 |
SG 比 PH 多出的检测项(推断):`bootloaderLocked` 状态(通过 KeyAttestation 验证、Play Integrity `DEVICE` 级别要求。
### Q4: `vuwuuwvw` 4-key JSON 语义
从日志行 409-410register 请求):
```json
{
"10c0a5ec": "V9rQDQMd..." (20B = IV/nonce A),
"1ca96197": "DXK5vhoi..." (20B = IV/nonce B HMAC tag),
"b4a937c8": "uK92+EOS..." (~1220B = SAP blob),
"dddcab8a": "7RWp0fXi..." (20B = MAC ),
"x-sap-ri": "f3684b6a..." (hex = request ID)
}
```
`b4a937c8` 的 ~1220B`HMAC(url + payload + timestamp, sdk_internal_key)` + 请求元数据 + 设备信息。密钥硬编码在 `libshpssdk_bank.so`SDK 版本级别,非设备绑定)。
**重要**:服务端对 SAP 签名的验证独立于 `rdVerifyInfo` 的验证。即使 SAP 签名通过,`data` 内容不干净仍返回 3100012。两者是串联校验不是并联。
### Q5: Play Integrity / TEE / KeyStore 参与情况
**高概率参与**`libshpssdk_bank.so` 内部推断调用链:
```
vvuwuuvuu()
→ collectEnvInfo()
→ android.security.keystore.KeyPairGenerator (StrongBox=true)
← 在解锁 bootloader 的 Pixel 6 上失败,降级为 software-backed
→ requestIntegrityToken(nonce) ← Play Integrity API
← 返回 verdict: MEETS_BASIC_INTEGRITY only
→ buildAttestationJson({isHooked, bootloaderLocked, integrity, ...})
→ AES_GCM_encrypt(attestation_json) → data
```
### Q6: 干净机 data/dataKey 重放可行性
**理论可行,有时效限制**
- `data`/`dataKey``random`(时间戳+随机数),服务端可能设 5 分钟有效窗口
-`deviceFingerprint` 段 1/2 是设备哈希,服务端**可能不 bind session**(仅风控评分)
- **最小实验**3 分钟内,干净机 data → Root 机重放,看是否 code=0
若重放成功 → 确认是 attestation 内容导致(而非设备黑名单)
若重放失败且错误码不同 → session 绑定问题,需另寻路径
### Q7: 3100012 精确触发条件
**多层评分系统(推断)**
```
score = 0
if isHooked: score += 40 ← native dl_iterate 检测到 liblspd
if bootloaderUnlocked: score += 30 ← KeyAttestation 无法通过
if integrityNotDevice: score += 20 ← Play Integrity 不是 DEVICE 级
if deviceBlacklisted: score += 100 ← 直接 ban
if score > SG_THRESHOLD:
return 3100012
else:
return code=0, step=BSO
```
SG_THRESHOLD 比 PH 低很多PH 容许更高 score
---
## 四、可执行突破方案
### ⚡ 方案 1PlayIntegrityFix今天30 分钟)
安装 Magisk 模块,伪造 Pixel 6 的 Play Integrity 为 DEVICE 级别:
```bash
# Magisk Manager → Modules → 安装以下模块之一:
# 1. PlayIntegrityFix (chiteroman) - 最主流,含 custom keybox 注入
# 2. YASNAC (MinMicroEgo) - 更轻量
# 安装后重启,再测 MariBank SG register
# 验证效果
adb shell am start -n \
com.google.android.gms/.phenotype.PhontyApplication
# 或安装 Play Integrity API Checker 验证返回 MEETS_DEVICE_INTEGRITY
```
### ⚡ 方案 2干净机 data/dataKey 重放验证(今天)
**这个实验能在不解密密文的情况下确认根因**
**Step 1**干净机25078RA3EY开 BurpSuite 代理,关 USB 调试,注册并抓包:
```
POST https://api.maribank.com.sg/uapi/v2/register
→ 保存 rdVerifyInfo.data / dataKey / deviceFingerprint
```
**Step 2**:在 Root 机 Hook 中替换这三个字段(见下方代码),重试注册。
**Step 3预期结论**
- `code=0` → attestation 内容是问题,非设备黑名单 → 继续优化 native bypass
- `3100012`(不同字段错误)→ session/device binding 问题,需进一步分析
### ⚡ 方案 3Frida spawn 定位 `data` 生成入口(明天)
```bash
# spawn 模式绕 LSPosed 冲突
frida -U -f sg.com.maribankmobile.digitalbank \
-l reverse/frida/trace_maribank_sg_native.js \
--no-pause 2>&1 | tee reverse/logs/frida_spawn_$(date +%H%M).txt
# 关注:
# RegisterNatives class=com.shopee.shpssdkbank.wvvvuwwu
# JNI vvuwuuvuu([B[B)[B -> libshpssdk_bank.so+0x????
# 拿到偏移后 → Ghidra 分析 → 找环境 JSON 组装点
```
### ⚡ 方案 4Native `openat` hook修补已知缺口
`trace_maribank_sg_native.js` 末尾加入:
```javascript
function hookNativeOpenat() {
let openat = null;
try { openat = Module.getExportByName(null, 'openat'); } catch(e) {}
if (!openat) { console.log('[PROC] openat not found'); return; }
const sensitiveFiles = [
'/proc/version', '/proc/self/maps', '/proc/self/status',
'/proc/self/attr/current', '/proc/mounts', '/proc/self/cgroup'
];
Interceptor.attach(openat, {
onEnter(args) {
try {
this.path = args[1].readCString();
} catch(e) { this.path = ''; }
},
onLeave(retval) {
if (!this.path) return;
for (const p of sensitiveFiles) {
if (this.path.endsWith(p)) {
console.log('[PROC] native openat(' + this.path + ') fd=' + retval);
// 如需 block返回 ENOENT=-1retval.replace(ptr(-1));
}
}
}
});
console.log('[PROC] hooked native openat');
}
hookNativeOpenat();
```
---
## 五、代码实现
### 5.1 MariBankDataReplayHook.java干净机重放验证
```java
package com.miraclegarden.smsmessage.xposed.hook;
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;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 干净机 rdVerifyInfo.data/dataKey/deviceFingerprint 重放钩子。
* 用于验证 3100012 是「attestation内容」还是「设备黑名单」导致的。
*
* 使用方法:
* 1. 干净机 BurpSuite 抓 /uapi/v2/register 明文uvwvuww 入口前 in0
* 2. 复制 data/dataKey/deviceFingerprint 三个值填入下方常量
* 3. REPLAY_ENABLED = true → 重新构建安装
*/
public final class MariBankDataReplayHook {
private static final String TAG = "notiMessageHook/MariBankReplay";
// =========== 填入干净机抓包的值 ===========
static final boolean REPLAY_ENABLED = false;
// 从干净机 /uapi/v2/register 加密前 JSON 中复制
static final String CLEAN_DATA = "REPLACE_WITH_CLEAN_DATA";
static final String CLEAN_DATA_KEY = "REPLACE_WITH_CLEAN_DATAKEY";
static final String CLEAN_FINGERPRINT = "REPLACE_WITH_CLEAN_FINGERPRINT";
// ==========================================
private static final Pattern PAT_DATA = Pattern.compile(
"\"data\"\\s*:\\s*\"([^\"]+)\"");
private static final Pattern PAT_DATAKEY = Pattern.compile(
"\"dataKey\"\\s*:\\s*\"([^\"]+)\"");
private static final Pattern PAT_FP = Pattern.compile(
"\"deviceFingerprint\"\\s*:\\s*\"([^\"]+)\"");
private MariBankDataReplayHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!REPLAY_ENABLED) {
XposedBridge.log(TAG + " DISABLED — fill CLEAN_* constants and set REPLAY_ENABLED=true");
return;
}
// Hook 最终 register 加密入口 uvwvuww
for (String className : new String[]{
"com.shopee.bke.lib.jni.utils.uvwuvwuv",
"com.shopee.bke.lib.jni.utils.uvwwwwuv",
}) {
hookClass(lpparam, className);
}
XposedBridge.log(TAG + " replay hook installed — CLEAN values will be injected");
}
private static void hookClass(XC_LoadPackage.LoadPackageParam lpparam, String className) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method m : clazz.getDeclaredMethods()) {
if (!"uvwvuww".equals(m.getName())) continue;
if (m.getParameterCount() < 1) continue;
Class<?> firstParam = m.getParameterTypes()[0];
if (firstParam != byte[].class && firstParam != String.class) continue;
XposedBridge.hookMethod(m, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object arg0 = param.args[0];
boolean isBytes = arg0 instanceof byte[];
String json = isBytes
? new String((byte[]) arg0, StandardCharsets.UTF_8)
: (String) arg0;
if (!json.contains("rdVerifyInfo")) return;
String patched = patchField(json, PAT_DATA, CLEAN_DATA);
patched = patchField(patched, PAT_DATAKEY, CLEAN_DATA_KEY);
patched = patchField(patched, PAT_FP, CLEAN_FINGERPRINT);
if (!patched.equals(json)) {
XposedBridge.log(TAG + " injected clean data/dataKey/fp into register JSON");
param.args[0] = isBytes
? patched.getBytes(StandardCharsets.UTF_8)
: patched;
}
} catch (Throwable t) {
XposedBridge.log(TAG + " inject err: " + t.getMessage());
}
}
});
XposedBridge.log(TAG + " hooked " + className + "#uvwvuww");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
}
}
private static String patchField(String json, Pattern p, String newValue) {
Matcher m = p.matcher(json);
if (!m.find()) return json;
StringBuffer sb = new StringBuffer();
m.appendReplacement(sb, Matcher.quoteReplacement(
m.group(0).replaceFirst("\"[^\"]+\"$", "\"" + newValue + "\"")));
m.appendTail(sb);
return sb.toString();
}
}
```
### 5.2 dump_rdverify_data.jsFrida 明文截获脚本)
```javascript
'use strict';
/**
* MariBank SG — rdVerifyInfo.data 生成前明文截获
* 运行frida -U -f sg.com.maribankmobile.digitalbank \
* -l reverse/frida/dump_rdverify_data.js --no-pause
* 目标:找到 vvuwuuvuu 的 native 参数(加密前的环境 JSON
*/
Java.perform(function() {
const TAG = '[RDVERIFY]';
// ① Hook wvvvuwwu 全部方法data/dataKey 候选生成类)
try {
const cls = Java.use('com.shopee.shpssdkbank.wvvvuwwu');
['vvuwuuvuu', 'wwvwvwuvv', 'vuwuuuwv', 'vuwuuwvw', 'vuwuuwvu'].forEach(function(mName) {
try {
cls[mName].overloads.forEach(function(ovl) {
const sig = ovl.argumentTypes.map(t => t.className).join(',');
ovl.implementation = function() {
console.log(TAG + ' wvvvuwwu.' + mName + '(' + sig + ') CALLED');
for (let i = 0; i < arguments.length; i++) {
const a = arguments[i];
if (a === null || a === undefined) {
console.log(' arg[' + i + '] = null');
continue;
}
try {
// byte[] → try UTF-8, fallback hex
if (Java.array('byte', []).getClass && a.getClass && a.getClass().getName() === '[B') {
const s = Java.use('java.lang.String').$new(a, 'UTF-8').toString();
const isPrintable = /^[\x20-\x7e\u4e00-\u9fff\r\n\t]+$/.test(s.substring(0,100));
if (isPrintable) {
console.log(' arg[' + i + '] byte[' + a.length + '] utf8=' + s.substring(0, 2000));
} else {
const hex = Array.from(a).slice(0,32).map(b => (b & 0xff).toString(16).padStart(2,'0')).join('');
console.log(' arg[' + i + '] byte[' + a.length + '] hex=' + hex + '...');
}
} else {
console.log(' arg[' + i + '] = ' + a.toString().substring(0, 500));
}
} catch(e) {
console.log(' arg[' + i + '] err=' + e);
}
}
const ret = ovl.apply(this, arguments);
if (ret !== null && ret !== undefined) {
try {
console.log(TAG + ' ret byte[' + ret.length + '] ← 这是 data/dataKey 候选!');
} catch(e) {
console.log(TAG + ' ret = ' + ret);
}
}
return ret;
};
console.log(TAG + ' hooked wvvvuwwu.' + mName);
});
} catch(e) {
console.log(TAG + ' skip ' + mName + ': ' + e.message);
}
});
} catch(e) {
console.log(TAG + ' wvvvuwwu not found: ' + e.message);
}
// ② Hook vvuuuuvvv.wwvuwuwvu — DFP/riskToken
try {
const dfpCls = Java.use('com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv');
dfpCls.wwvuwuwvu.overloads.forEach(function(ovl) {
ovl.implementation = function() {
const ret = ovl.apply(this, arguments);
console.log(TAG + ' DFP.wwvuwuwvu = ' + ret);
return ret;
};
});
} catch(e) {}
// ③ Hook uvwvuww — 最终 register 加密入口(可确认明文注入点)
['com.shopee.bke.lib.jni.utils.uvwuvwuv',
'com.shopee.bke.lib.jni.utils.uvwwwwuv'].forEach(function(className) {
try {
const encCls = Java.use(className);
if (encCls['uvwvuww']) {
encCls['uvwvuww'].overloads.forEach(function(ovl) {
ovl.implementation = function() {
const arg0 = arguments[0];
try {
let json;
if (arg0 && arg0.getClass && arg0.getClass().getName() === '[B') {
json = Java.use('java.lang.String').$new(arg0, 'UTF-8').toString();
} else {
json = '' + arg0;
}
if (json.includes('rdVerifyInfo')) {
console.log(TAG + ' uvwvuww register plaintext (len=' + json.length + '):\n' + json.substring(0, 3000));
}
} catch(e) {}
return ovl.apply(this, arguments);
};
});
console.log(TAG + ' hooked ' + className + '#uvwvuww');
}
} catch(e) {}
});
console.log(TAG + ' all hooks installed — trigger MariBank registration now');
});
```
---
## 六、结论
**3100012 最可能的触发链**
```
libshpssdk_bank.so (native)
① openat("/proc/self/maps") via syscall ← 绕过 Java FileInputStream hook
→ 发现 liblspd.so / libzygisk.so / libgadget.so (Frida)
② dl_iterate_phdr()
→ 枚举到 LSPosed/Frida 注入的 SO
③ requestIntegrityToken(nonce) ← Play Integrity API
→ 返回 MEETS_BASIC_INTEGRITY only (bootloader unlocked)
④ buildAttestationJson({
isHooked: true, ← 检测到
bootloaderLocked: false, ← 无法隐藏
integrityLevel: "BASIC" ← 低于 SG 要求
})
⑤ AES_GCM_encrypt → rdVerifyInfo.data
⑥ 服务端解密 → risk_score > SG_THRESHOLD → 3100012
```
**优先级最高的三步**
1. **PlayIntegrityFix** → 提升 Integrity 级别至 DEVICE30 分钟)
2. **干净机重放实验** → 验证根因(需干净机配合)
3. **Frida spawn + RegisterNatives** → 定位 `vvuwuuvuu` 偏移 → Ghidra 分析明文结构
*2026-07-06 17:05 SGT*

View File

@@ -0,0 +1,256 @@
# 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 检测 |
| [`MariBank新加坡突破.md`](MariBank新加坡突破.md) | **SG 3100012 突破计划与测试流程** |
| [`工作日志_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,139 @@
# MariBank 新加坡注册突破计划2026-07-06
> **现状**PH ✅ OTP`code=0`SG ❌ **3100012**`api.maribank.com.sg`
> **关联**[`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md) · [`MariBank新加坡逆向.md`](MariBank新加坡逆向.md) · [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md)
---
## 1. 问题定位
| 层级 | SG 状态 | 说明 |
|------|---------|------|
| 本地 Root / ADB 页 | ✅ 可 bypass | 能进注册页、点 Next |
| DFP 上报 | ✅ 通常 `code=0` | 只表示收到,不等于注册过 |
| `deviceFingerprint` 尾部 | ✅ 已 `\|00\|0` | Java 层净化生效 |
| **`rdVerifyInfo.data` / `dataKey`** | ❌ 服务端拒 | **3100012 主因** |
| 干净机 `25078RA3EY` | ✅ 关 USB 调试可 OTP | 无 LSPosed |
**结论**SG 卡在 **native attestation 密文 + 区域风控**,不是再堆本地弹窗拦截。
---
## 2. 与 PH 的差异(实测 + 逆向)
| 维度 | PH | SG |
|------|----|----|
| API | `api.seabank.ph` | `api.maribank.com.sg` |
| 错误码 | 4067012 | **3100012** |
| App 版本 | 3.22.0 | 3.2.2 |
| 国家码 `cyCode` | 63 | **65** |
| 本地 ADB 检测 | 较弱 | **RISK_USB_ADB / RISK_WIFI_ADB** |
| 服务端 strictness | 7/6 已通过 | **更严** |
| 设备黑名单 | 较少 | Pixel 6 多次测 SG 可能已标记 |
---
## 3. 本轮代码改动2026-07-06
| 改动 | 目的 |
|------|------|
| **`ProbeGuard`** | `ProcessBuilder` / `Runtime.exec` 返回假进程,**不再抛 SecurityException**(避免 SHPSSDK 记 tamper |
| **`MariBankAttestationHook`** | 增加 `vvuuuuvvv``uvuwwuvwv.uvwwuuvvw` attestation 类 Hook |
| **`MariBankShpsNativeHook`** | native-core 增加 `vvuuuuvvv` |
| **`MariBankRegisterPayloadUtil`** | 加密前 log`cyCode``dataLen``dataKeyLen``fpTail` |
---
## 4. 推荐测试流程Root 机)
### 4.1 每次测 SG 前(降低黑名单概率)
```powershell
cd C:\Users\Administrator\Desktop\notiMessage
# 1. 编译安装最新 Hook
.\scripts\build-debug.ps1
.\scripts\install-debug.ps1
# 2. 新设备 ID保持 USB 调试,便于继续 adb
.\scripts\maribank-sg-register.ps1 -NewIdentity -KeepAdb
# 3. 上机测 SG 前再关 USB 调试(会断开 PC adb属预期
.\scripts\maribank-sg-register.ps1 -DisableUsbDebug
```
### 4.2 手机侧
> **LSPosed 里两个都叫「MariBank」**:菲律宾 `ph.seabank.seabank`v3.22.0)与新加坡 `sg.com.maribankmobile.digitalbank`v3.2.2)桌面名相同,从 LSPosed 作用域点开会容易进错。**请认包名**,或 PC 执行 `.\scripts\launch-maribank-sg.ps1` 直接打开新加坡版。log 里应出现 `api.maribank.com.sg`,若全是 `api.seabank.ph` 说明开的是菲律宾 App。
1. LSPosed模块启用作用域含 **`sg.com.maribankmobile.digitalbank`**
2. **软重启** MariBank SG不是只杀进程
3. Shamiko + DenyList 勾选 SG 全部进程,**Enforce=OFF**
4. (建议)**关闭 VPN**
5. Sign up → 新加坡手机号 → **Next**
### 4.3 抓 log
```powershell
.\scripts\maribank-sg-register.ps1 -CaptureLog -KeepAdb
```
> **注意**:旧版脚本默认会关 USB 调试,导致 PC 立刻 `no devices`。抓 log 必须加 **`-KeepAdb`**,或先在手机上重新打开 USB 调试。
关注:
```text
MariBankRegister: register summary cy=65 scene=REGISTRATION step=BE fpTail=00|0 dataLen=... dataKeyLen=...
MariBankRoot HTTP .../uapi/v2/register ... code=0 ← 成功
MariBankRoot HTTP .../uapi/v2/register ... code=3100012 ← 仍失败
ProbeGuard: fake probe process ← 探针已静默拦截
```
---
## 5. 若仍 3100012 — 按优先级排查
| 优先级 | 动作 | 原因 |
|--------|------|------|
| P0 | **换新 serial + android_id**`-NewIdentity` | SG 可能设备级黑名单 |
| P0 | **关 USB 调试 + 无线调试** | 干净机对照:开调试本地 ADB 页;服务端 SG 更严 |
| P0 | **关 VPN** | 截图曾见 VPN 图标,可能进 risk |
| P1 | diff PH vs SG 同机 `register summary` | 对比 `dataLen`、fingerprint 段 |
| P1 | 用 **未测过 SG 的干净机** 注册一次 | 分离「设备黑」vs「Root 载荷不可过」 |
| P2 | Play Integrity / PIF 模块是否生效 | SG 可能校验 attestation 内 integrity |
| P3 | native hook `libshpssdk_bank.so` 生成链 | Java 层改不了 `data` 密文内容 |
---
## 6. 成功标准
```json
POST https://api.maribank.com.sg/uapi/v2/register
{"code":0,"msg":"success","data":{"scene":"REGISTRATION","step":"BSO",...}}
```
随后 App 进入 **OTP 短信** 步骤(与 PH 相同 step 流转)。
---
## 7. 禁止操作
与 PH 相同,会导致白屏 / RN 崩溃:
- Hook `System.loadLibrary`
- Hook `RealInterceptorChain.proceed`
- 过早 Hook `ShpssInstall` / `vuvuwwwuw`
---
## 8. 相关脚本
| 脚本 | 用途 |
|------|------|
| `scripts/maribank-sg-register.ps1` | SG 测前准备 + 抓 log |
| `scripts/maribank-spoof-device.ps1 -NewIdentity` | 换 serial / android_id |
| `scripts/maribank-scheme-b-finish.ps1` | LSPosed 作用域 + pm clear |
---
*最后更新2026-07-06*

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*

64
docs/TNG_captcha逆向.md Normal file
View File

@@ -0,0 +1,64 @@
# TNG eWallet 滑动验证码(阿里云 Captcha逆向笔记2026-08-03
## 现象
注册/登录手机号页点「继续」→ 弹阿里云滑动拼图验证 → 滑块拖对通过 →
**不返回**TTCaptcha 未回调 TNG 业务层)→ 手动点「继续」→ 再次弹验证 → 循环。
## 回调链1.9.10 dex 逆向)
```
用户滑动成功
→ JS postMessage → CaptchaWebViewDialog$2.a (action=sendAliyunCaptchaVerifyData, data 含 success/message)
→ Captcha.generateResult(char, zzbfs) → JSON {code, retCode, message, certifyId}
→ setVerifyResult(true) → Captcha$1(Handler) → VerificationCallback.onSuccess(result)
→ TTCaptcha 反射 Proxy → TTCaptchaCallback.callBack(result)
→ parseJson → TTCaptchaResponse{code, certifyId}
code==0 且 certifyId 非空 ?
→ notifySuccess(certifyId) → TTInListener.success → TNG 业务层提交 RPC
→ notifyFailure(code) / handleFailure("Result is null"|"Invalid code or certifyId")
→ 服务端二次校验 certifyId 失败
→ quake 抛 CaptchaNeededException("Captcha needed")
/ CaptchaNotPassedException("Captcha not passed")
→ 验证拦截器再弹滑块 → 循环
```
## 关键类dex 定位)
| 类 | 作用 |
|----|------|
| `com.aliyun.captcha.Captcha`classes10 | 单例verify/generateResult/showDialog |
| `com.aliyun.captcha.CaptchaWebViewDialog` + `$2` | 滑块 WebView + JS postMessage 桥 |
| `com.aliyun.TigerTally.captcha.api.TTCaptcha` | TigerTally 封装,**反射**调 aliyun Captcha |
| `com.aliyun.TigerTally.captcha.core.TTCaptchaCallback` | 解析 resultcode==0 且 certifyId 非空才 success |
| `my.com.tngdigital.captcha.TigerTallyApiWrapper` | TNG 业务侧封装Kotlin 协程 showCaptcha |
| `...aliservice.quake.CaptchaNeededException` / `CaptchaNotPassedException` | 服务端要验证 / 验证未通过 |
| `...amcs.CaptchaConfigCenter` / `CaptchaInitializer` | 远程下发 wafCaptchaKey / captcha_switch |
| `...opmpaasexpress.interceptor.OpMpVerifyInterceptor` 等 | RPC 验证拦截器,触发滑块 |
## 判定点(本次 hook 已打点)
1. `Captcha.generateResult` 返回的 JSON —— **certifyId 是否为空**(滑块是否真正拿到服务端签发)
2. `CaptchaWebViewDialog$2.a` —— JS postMessage 的 data 内容
3. `TTCaptchaCallback.callBack/notifySuccess/notifyFailure` —— TNG 是否拿到 certifyId
4. `CaptchaNeeded/NotPassedException` 构造 message —— 服务端二次校验失败原因
5. `TTCaptcha.verifyByReflect/buildParams` —— captcha 参数region/appKey 等)
## 根因候选
- **TigerTally 设备指纹umidToken异常**`hookTigerTally` 短路了
`TigerTallyAPI.init/initCommon``t.B.genericNt1`(防 ANR fork 卡死)。
若 captcha 服务端用 umidToken 校验设备,缺失/变化会导致 certifyId 校验失败。
- **captcha 全量方法打点拖慢 JS 桥回调**(已修复:改为精准打点)。
- **region 错误**`TTCaptcha.buildParams``t.B.genericNt14()` 取 region。
## 抓 logcat 判定
```powershell
powershell -File scripts/logcat-tng.ps1
```
复现滑块 → 观察:
- `captcha JS postMessage` 后是否有 `captcha RET generateResult`(含 certifyId
- `captchaCb CALL notifySuccess` 是否出现(成功)还是 `notifyFailure`
- `captcha EXC ...CaptchaNotPassedException` 的 msg

View File

@@ -0,0 +1,52 @@
# TNG 开放问题与抓包能力2026-08-04
## 待办:冷启动假掉登录(先不改)
**现象:** 登录成功后退出再进,界面像未登录,需重新登录。
**根因判断(已基本确认):**
旧逻辑:`SplashActivity.onCreate` **无条件** 2s 强拉 `UserLoginActivity`
**已修复2026-08-04** 改为 4s **卡住救援**——已自行跳到 PIN/首页则取消;仅仍停在 Splash 时救援(有本地会话优先 `UserPinActivity`,否则 `UserLoginActivity`)。
---
## Money Packet 领取统计 — 网络/明文获取能力
目标:群红包 Leaderboard 的 **昵称 + 已领金额**(字段预期 `receiverList` / `claimedAmount` 等)。
### 路线对比
| 路线 | 状态 | 说明 |
|------|------|------|
| 外部 mitmCharles / mitmproxy | **不可行(当前)** | TNG API`mpaasgw.tngdigital.com.my`)有 **证书 pinning**,只能看到域名,解不开 HTTPS 正文。`reverse/dumps/mitm_mmp/` 里曾落盘的多为官网 HTML/新闻,**不是** 红包 API。 |
| 进程内 Xposed`TngMoneyPacketHook` | **代码已接,运行时未实证** | 在 TLS 之后读明文Hook `OkHttp ResponseBody.string` + `Gson.fromJson`,匹配 `receiverList` / `claimedAmount` / `Mmp*` 模型,经 `HookForwarder` 转发。`MainHook``install`。 |
| 纯日志/UI 自动化 | 兜底 | 无 API 时可读界面,不稳定,不作主路径。 |
### 当前缺口
1. **尚未在真机打开 Money Packet Leaderboard 做过一次捕获验证** → logcat 里暂无 `TngMmp captured …`
2. TNG 主业务多为 **mPaaS / Quake RPC**,若响应不走 `ResponseBody.string()` / 目标 Gson 类名不符,现有 Hook 会漏;需补 **RPC invoke 返回值 / 其它 JSON 入口**
3. Splash 强拉登录不影响「登录后进群点红包」时的抓包,但影响复测效率。
### 建议验证步骤(下次动手)
1. 保持登录态,进群 → 打开红包详情 / Leaderboard。
2. 看 LSPosed`notiMessageHook/TngMmp installed``captured packet=… claims=N`
3. 若无:对同一次操作抓 logcat 里 URL / 类名,补 Hook 点Quake `RpcInvocationHandler` 等)。
4. 确认 notiMessage / debug-server 是否收到转发内容。
### eKYC「验证您的帐户」强制页2026-08-04
`HomeEkycVerifyActivity` 挡首页。测试期:`hookHomeEkycVerifySkip` — finish 该页 + 拦 Intent + `canBypassEkyc`/`enforceEkyc` stub。
**注意:** 服务端仍可能在部分功能(转账/红包)二次校验 eKYC首页跳过不等于全功能可用。
```text
packetId / title
receiverList[]:
- nickname / displayName
- claimedAmount或 amount
```
按昵称聚合 `claimedAmount` 即可做领取排行。

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`。详见 **[`MariBank新加坡突破.md`](MariBank新加坡突破.md)**3100012 排查与 `maribank-sg-register.ps1`)。
### 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*

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

@@ -0,0 +1,126 @@
# 更新说明
## TNG eWallet 登录/注册区号2026-08-03
- **包名** `my.com.tngdigital.ewallet` v1.9.10Android 16Pixel 6Root + LSPosed
- **问题**:登录/注册点区号时 `i7.l` loading 弹窗触发 HWUI gralloc ABRTsignal 6 黑屏)
- **修复**`TngRootBypassHook`
- Login / Register 统一 **skip `i7.l` Dialog.show**`Dialog.isShowing()` 返回 true 防卡死
- `UserSearchCallingCodeActivity` 正常打开,国家列表可见
- **自动化**`reverse/scripts/test_tng_full_flow.py`(注册区号 + 登录 PIN 区号,两次冷启动)
- **一键**`powershell -File scripts/test-tng-full-flow.ps1`
---
## 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,916 @@
/*
* 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 <dlfcn.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;
/* 主线程abort 后 _exit 被 seccomp 拦 → abort 内部死循环(还复位 handler
* 主线程永久卡死 → 黑屏/ANR。主线程 ABRT 时跳回 lrLooper pollOnce恢复
* 仅当 lr 距 pc 远(不在 abort 内部)才跳,否则仍在 abort epilogue 内跳 LR 会再 abort。 */
uintptr_t target = pc + 4;
if (tid == g_main_tid.load() && lr != 0 && pc != 0
&& (lr < pc - 0x1000 || lr > pc + 0x1000)) {
target = lr;
}
if (n <= 5 || n % 50 == 0) {
LOGI("ABRT tid=%d pc=%lx lr=%lx streak=%d -> %lx", (int)tid,
(unsigned long)pc, (unsigned long)lr, streak,
(unsigned long)target);
}
uc->uc_mcontext.pc = target;
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() {
/* exit_group 拦 + 精准拦 Promon 自杀 kill(SIGABRT)SI_USER
* 只拦 kill()nr=129Promon 自杀是 kill(pid,SIGABRT) → si_code=SI_USER。
* 不拦 tgkill/tkillART 的 abort() 用 tgkill(self) → si_code=SI_TKILL
* 拦它会让 libart 状态错乱 → 进程崩(之前验证)。 */
struct sock_filter filter[] = {
// 0: arch
_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), // 2: not aarch64
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, nr)),
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, __NR_exit_group), // 4: exit_group? true→5
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM), // 5: EPERM exit_group
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 3, __NR_kill), // 6: kill? true→7
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, args) + 8), // 7: args[1]=sig
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, SIGABRT), // 8: sig==SIGABRT? true→9
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM), // 9: EPERM kill ABRT
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW), // 10: 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+kill-ABRT via prctl");
} else {
LOGI("seccomp exit_group+kill-ABRT 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);
using android_dlopen_ext_fn = void *(*)(const char *, int, const void *);
using sphal_load_fn = void *(*)(const char *, int);
using open_passthrough_hal_fn = void *(*)(const char *, 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 android_dlopen_ext_fn orig_android_dlopen_ext = nullptr;
static sphal_load_fn orig_sphal_load = nullptr;
static open_passthrough_hal_fn orig_open_passthrough_hal = nullptr;
static void *g_libandroid_handle = nullptr;
static void *g_mapper_pixel_handle = nullptr;
#ifndef RTLD_NOW
#define RTLD_NOW 2
#endif
#ifndef RTLD_GLOBAL
#define RTLD_GLOBAL 0x100
#endif
#ifndef RTLD_NOLOAD
#define RTLD_NOLOAD 0x4
#endif
#ifndef RTLD_DEFAULT
#define RTLD_DEFAULT reinterpret_cast<void *>(static_cast<uintptr_t>(-1))
#endif
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;
}
/** 仅打断递归初始化;系统 libc++ 静态 ctor含 gralloc必须真实执行。 */
static thread_local void *tl_cxa_guard = nullptr;
static thread_local int tl_cxa_depth = 0;
static int hooked_cxa_guard_acquire(void *guard) {
if (guard != nullptr && tl_cxa_guard == guard) {
LOGI("cxa_guard recursive skip tid=%d", (int)gettid());
return 1;
}
if (!orig_cxa_guard_acquire) {
return 1;
}
void *prev = tl_cxa_guard;
tl_cxa_guard = guard;
++tl_cxa_depth;
int r = orig_cxa_guard_acquire(guard);
--tl_cxa_depth;
tl_cxa_guard = prev;
return r;
}
static void hooked_cxa_guard_abort() {
LOGI("blocked __cxa_guard_abort tid=%d depth=%d", (int)gettid(), tl_cxa_depth);
}
static void *try_dlopen_noload(const char *name) {
if (!orig_dlopen || name == nullptr) {
return nullptr;
}
void *h = orig_dlopen(name, RTLD_NOW | RTLD_NOLOAD);
if (h != nullptr) {
return h;
}
const char *base = strrchr(name, '/');
if (base != nullptr && base[1] != '\0') {
h = orig_dlopen(base + 1, RTLD_NOW | RTLD_NOLOAD);
}
return h;
}
/** 校验 dlopen 句柄:拒绝空/明显毒化指针,并用 dlsym 探活。 */
static bool libandroid_handle_ok(void *h) {
if (h == nullptr) return false;
uintptr_t p = reinterpret_cast<uintptr_t>(h);
// 用户态典型映射;排除明显垃圾(如 0x...c5c5 / 高熵毒化)
if (p < 0x10000UL) return false;
if ((p & 0xffffUL) == 0xc5c5UL) return false;
void *sym = dlsym(h, "ANativeWindow_fromSurface");
if (sym == nullptr) {
sym = dlsym(h, "AAssetManager_fromJava");
}
return sym != nullptr;
}
static void preload_hwui_libs() {
if (!orig_dlopen) {
// PLT 尚未拿到 orig 时,用 libc 直调
orig_dlopen = reinterpret_cast<dlopen_fn>(dlsym(RTLD_DEFAULT, "dlopen"));
}
if (!orig_sphal_load) {
orig_sphal_load = reinterpret_cast<sphal_load_fn>(
dlsym(RTLD_DEFAULT, "android_load_sphal_library"));
}
if (g_libandroid_handle != nullptr && !libandroid_handle_ok(g_libandroid_handle)) {
LOGI("drop invalid cached libandroid %p", g_libandroid_handle);
g_libandroid_handle = nullptr;
}
if (g_libandroid_handle == nullptr && orig_dlopen) {
static const char *kPaths[] = {
"libandroid.so",
"/system/lib64/libandroid.so",
"/apex/com.android.runtime/lib64/libandroid.so",
};
for (const char *path : kPaths) {
void *h = orig_dlopen(path, RTLD_NOW | RTLD_GLOBAL);
if (h == nullptr) {
h = try_dlopen_noload(path);
}
if (libandroid_handle_ok(h)) {
g_libandroid_handle = h;
LOGI("preload libandroid ok path=%s -> %p", path, h);
break;
}
if (h != nullptr) {
LOGI("preload libandroid reject path=%s -> %p", path, h);
}
}
if (g_libandroid_handle == nullptr) {
LOGI("preload libandroid FAILED");
}
}
if (g_mapper_pixel_handle == nullptr) {
if (orig_sphal_load) {
g_mapper_pixel_handle = orig_sphal_load("mapper.pixel.so", RTLD_NOW);
LOGI("preload mapper via sphal -> %p", g_mapper_pixel_handle);
}
if (g_mapper_pixel_handle == nullptr && orig_dlopen) {
g_mapper_pixel_handle = orig_dlopen(
"/vendor/lib64/hw/mapper.pixel.so", RTLD_NOW | RTLD_GLOBAL);
LOGI("preload mapper via path -> %p", g_mapper_pixel_handle);
}
}
}
static void *hooked_dlopen(const char *name, int flags) {
void *handle = orig_dlopen ? orig_dlopen(name, flags) : nullptr;
if (name == nullptr) {
return handle;
}
if (handle != nullptr) {
// 命名空间下偶发返回毒化非空句柄HWUI 随后 FATAL
if (strstr(name, "libandroid.so") != nullptr && !libandroid_handle_ok(handle)) {
LOGI("dlopen got bad handle %p for %s — recover", handle, name);
handle = nullptr;
} else {
if (strstr(name, "libandroid.so") != nullptr) {
g_libandroid_handle = handle;
}
return handle;
}
}
// 已映射库:命名空间下按名 dlopen 会失败RTLD_NOLOAD 可取回句柄
handle = try_dlopen_noload(name);
if (handle != nullptr) {
if (strstr(name, "libandroid.so") == nullptr || libandroid_handle_ok(handle)) {
LOGI("dlopen NOLOAD hit name=%s -> %p", name, handle);
if (strstr(name, "libandroid.so") != nullptr) {
g_libandroid_handle = handle;
}
return handle;
}
}
if (strstr(name, "libandroid.so") != nullptr) {
if (g_libandroid_handle != nullptr && !libandroid_handle_ok(g_libandroid_handle)) {
LOGI("drop bad cached libandroid %p", g_libandroid_handle);
g_libandroid_handle = nullptr;
}
if (g_libandroid_handle != nullptr) {
LOGI("dlopen return cached libandroid %p (from %s)",
g_libandroid_handle, name);
return g_libandroid_handle;
}
static const char *kAndroidPaths[] = {
"/system/lib64/libandroid.so",
"libandroid.so",
};
LOGI("dlopen miss name=%s flags=0x%x tid=%d — try fallback",
name, flags, (int)gettid());
for (const char *path : kAndroidPaths) {
handle = try_dlopen_noload(path);
if (handle == nullptr && orig_dlopen) {
handle = orig_dlopen(path, flags | RTLD_GLOBAL);
}
if (libandroid_handle_ok(handle)) {
g_libandroid_handle = handle;
LOGI("dlopen fallback %s -> %p (from %s)", path, handle, name);
return handle;
}
if (handle != nullptr) {
LOGI("dlopen fallback reject %s -> %p", path, handle);
}
}
LOGI("dlopen fallback failed name=%s tid=%d", name, (int)gettid());
return nullptr;
}
if (strstr(name, "mapper.pixel") != nullptr
|| strstr(name, "mapper.") != nullptr) {
if (g_mapper_pixel_handle != nullptr) {
LOGI("dlopen return cached mapper %p (from %s)",
g_mapper_pixel_handle, name);
return g_mapper_pixel_handle;
}
if (orig_sphal_load) {
handle = orig_sphal_load("mapper.pixel.so", RTLD_NOW);
if (handle != nullptr) {
g_mapper_pixel_handle = handle;
LOGI("dlopen sphal mapper -> %p (from %s)", handle, name);
return handle;
}
}
if (orig_dlopen) {
handle = orig_dlopen("/vendor/lib64/hw/mapper.pixel.so",
flags | RTLD_GLOBAL);
if (handle != nullptr) {
g_mapper_pixel_handle = handle;
LOGI("dlopen path mapper -> %p (from %s)", handle, name);
return handle;
}
}
LOGI("dlopen mapper failed name=%s tid=%d", name, (int)gettid());
}
return handle;
}
static void *hooked_android_dlopen_ext(const char *name, int flags, const void *extinfo) {
void *handle = orig_android_dlopen_ext
? orig_android_dlopen_ext(name, flags, extinfo)
: nullptr;
if (handle != nullptr || name == nullptr) {
return handle;
}
if (strstr(name, "libandroid.so") != nullptr
|| strstr(name, "mapper") != nullptr) {
LOGI("android_dlopen_ext miss name=%s — try dlopen fallback", name);
return hooked_dlopen(name, flags);
}
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_libc(dev_t *dev, ino_t *ino) {
return find_lib_by_suffix("libc.so", dev, ino);
}
/** 收集 maps 里所有匹配后缀的已加载库去重。Zygisk commit 前必须覆盖全部副本,
* 否则 libc++ 多副本(/system、/vendor、/apex时只 hook 一份,调用点仍走原生实现。 */
struct lib_devino {
dev_t dev;
ino_t ino;
};
static int find_all_lib_by_suffix(const char *suffix, lib_devino *out, int max) {
FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) return 0;
char line[1024];
int n = 0;
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 got = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]",
&start, &end, perms, &offset, deststr, &inode, path);
if (got < 7 || inode == 0) continue;
char *p = path;
while (*p == ' ') ++p;
size_t plen = strlen(p);
size_t slen = strlen(suffix);
if (plen < slen || strcmp(p + plen - slen, suffix) != 0) continue;
unsigned maj = 0, min = 0;
if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue;
dev_t d = makedev(maj, min);
ino_t in = inode;
bool dup = false;
for (int i = 0; i < n; i++) {
if (out[i].dev == d && out[i].ino == in) {
dup = true;
break;
}
}
if (dup) continue;
if (n < max) {
out[n].dev = d;
out[n].ino = in;
n++;
}
}
fclose(fp);
return n;
}
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 *hooked_sphal_load(const char *name, int flags) {
void *handle = orig_sphal_load ? orig_sphal_load(name, flags) : nullptr;
if (handle != nullptr || name == nullptr) {
return handle;
}
LOGI("sphal miss name=%s flags=0x%x — try cache/path", name, flags);
if (strstr(name, "mapper") != nullptr) {
if (g_mapper_pixel_handle != nullptr) {
LOGI("sphal return cached mapper %p", g_mapper_pixel_handle);
return g_mapper_pixel_handle;
}
if (orig_dlopen) {
handle = orig_dlopen("/vendor/lib64/hw/mapper.pixel.so",
RTLD_NOW | RTLD_GLOBAL);
if (handle != nullptr) {
g_mapper_pixel_handle = handle;
LOGI("sphal path mapper -> %p", handle);
return handle;
}
handle = orig_dlopen("mapper.pixel.so", RTLD_NOW | RTLD_GLOBAL);
if (handle != nullptr) {
g_mapper_pixel_handle = handle;
LOGI("sphal name mapper -> %p", handle);
return handle;
}
}
}
return nullptr;
}
static void *hooked_open_passthrough_hal(const char *interface, const char *instance,
int dlopen_flags) {
void *handle = orig_open_passthrough_hal
? orig_open_passthrough_hal(interface, instance, dlopen_flags)
: nullptr;
if (handle != nullptr) {
return handle;
}
LOGI("passthroughHal miss iface=%s inst=%s — try mapper path",
interface ? interface : "?", instance ? instance : "?");
if ((interface && strstr(interface, "mapper") != nullptr)
|| (instance && strstr(instance, "pixel") != nullptr)) {
if (g_mapper_pixel_handle != nullptr) {
return g_mapper_pixel_handle;
}
if (orig_sphal_load) {
handle = orig_sphal_load("mapper.pixel.so", RTLD_NOW);
}
if (handle == nullptr && orig_dlopen) {
handle = orig_dlopen("/vendor/lib64/hw/mapper.pixel.so",
RTLD_NOW | RTLD_GLOBAL);
}
if (handle != nullptr) {
g_mapper_pixel_handle = handle;
LOGI("passthroughHal mapper recovered -> %p", handle);
}
}
return handle;
}
static void register_dlopen_on_lib(zygisk::Api *api, const char *suffix) {
dev_t dev = 0;
ino_t ino = 0;
if (!find_lib_by_suffix(suffix, &dev, &ino)) {
return;
}
// 必须 hook 调用方 PLTlibhwui/libui只 hook libc 拦不到 HWUI 的 dlopen
register_plt(api, dev, ino, "dlopen",
(void *)hooked_dlopen, (void **)&orig_dlopen);
register_plt(api, dev, ino, "android_dlopen_ext",
(void *)hooked_android_dlopen_ext, (void **)&orig_android_dlopen_ext);
register_plt(api, dev, ino, "android_load_sphal_library",
(void *)hooked_sphal_load, (void **)&orig_sphal_load);
register_plt(api, dev, ino, "AServiceManager_openDeclaredPassthroughHal",
(void *)hooked_open_passthrough_hal,
(void **)&orig_open_passthrough_hal);
LOGI("dlopen PLT on %s", suffix);
}
/** 注册所有已加载 libc++ 副本的 __cxa_guard_acquire/abort。返回注册的副本数。 */
static int register_cxx_guard_hooks(zygisk::Api *api) {
if (!api) return 0;
lib_devino libs[8];
int n = find_all_lib_by_suffix("libc++.so", libs, 8);
if (n == 0) {
n = find_all_lib_by_suffix("libc++_shared.so", libs, 8);
}
for (int i = 0; i < n; i++) {
register_plt(api, libs[i].dev, libs[i].ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(api, libs[i].dev, libs[i].ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
}
if (n > 0) {
LOGI("cxx guard target libc++ copies=%d", n);
}
return n;
}
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);
register_plt(api, dev, ino, "android_dlopen_ext",
(void *)hooked_android_dlopen_ext, (void **)&orig_android_dlopen_ext);
}
// HWUI / libui 直接 PLT→linker必须单独挂
register_dlopen_on_lib(api, "libhwui.so");
register_dlopen_on_lib(api, "libui.so");
register_dlopen_on_lib(api, "libandroid_runtime.so");
register_dlopen_on_lib(api, "libbinder_ndk.so");
register_dlopen_on_lib(api, "libvndksupport.so");
// libc++ cxa guard 必须在首次 commit 前注册Zygisk pltHookCommit 二次调用会失败,
// 导致 __cxa_guard_acquire 递归 abort 保护从未生效(主线程反复 SIGABRT → 黑屏)。
int cxx = register_cxx_guard_hooks(api);
bool ok = api->pltHookCommit();
LOGI("PLT commit=%d cxx_guard_copies=%d", ok ? 1 : 0, cxx);
if (ok && cxx > 0) {
g_cxx_plt.store(1);
}
// commit 后立刻预加载,抢在 Promon/命名空间收紧之前拿到句柄
preload_hwui_libs();
}
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;
// libc++ 副本已由首次 PLT commit 覆盖;此处仅补 libtngdigital_ewallet.so 自身 PLT。
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;
LOGI("cxx guard target libtngdigital_ewallet");
}
if (!any) return;
bool ok = g_api->pltHookCommit();
LOGI("cxx guard commit=%d", ok ? 1 : 0);
if (ok) {
g_cxx_plt.store(1);
LOGI("PLT cxx guards committed (orig acquire + recursive skip)");
}
}
static void *phase_thread(void *) {
install_promon_segv_handler();
/* Promon 用 SVC exit_group 绕过 PLT必须 seccomp。延迟 400ms 避开最早的 fork/getprop。 */
usleep(400 * 1000);
if (install_seccomp_exit_group_only() == 0) {
LOGI("seccomp exit_group armed @400ms");
} else {
LOGE("seccomp install failed");
}
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+seccomp@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,43 @@
# 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
# SG 专用 native trace推荐
.\scripts\run-frida-sg-native.ps1
# 或
cd reverse\frida
C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe run_frida_sg_native.py attach
# PH 旧脚本(勿用于 SG
.\run-frida-trace.ps1 -Mode spawn
```
建议测试时**暂时关闭 LSPosed 对 MariBank 的作用域**,避免与 Frida 冲突。
## 预期输出
- `RegisterNatives libsdkutils.so ...` — JNI 符号
- `NativeEncryptWrapper.*` — 加密前明文(若走 Java 包装)
- `HTTP POST .../uapi/v2/register` — 请求/响应 body
- `vvuuuuvvv.wwvuwuwvu ret` — riskToken 全文

25
reverse/frida/mini_run.py Normal file
View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
import subprocess, time
import frida
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
def adb(*a): return subprocess.run([ADB, *a], capture_output=True, text=True)
def on_msg(m, d):
line = m.get("payload") if m.get("type") in ("send", "log") else str(m)
print(line, flush=True)
d = frida.get_usb_device(10)
adb("shell", "am", "force-stop", PKG); time.sleep(1)
pid = d.spawn([PKG])
s = d.attach(pid)
sc = s.create_script(open("mini_timer.js", encoding="utf-8").read())
sc.on("message", on_msg)
sc.load()
print("resume", pid, flush=True)
d.resume(pid)
for i in range(20):
time.sleep(1)
p = adb("shell", "pidof", PKG).stdout.strip()
print("t=%d pid=%s" % (i+1, p or "DEAD"), flush=True)
if not p: break
try: s.detach()
except: pass

View File

@@ -0,0 +1,9 @@
"use strict";
console.log("[MINI] start");
let i = 0;
const t = setInterval(() => {
i++;
const m = Process.findModuleByName("libtiger_tally.so");
console.log(`[MINI] tick=${i} tiger_loaded=${!!m}`);
if (i > 15) clearInterval(t);
}, 1000);

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,143 @@
# -*- coding: utf-8 -*-
"""Frida SG native trace — attach to sg.com.maribankmobile.digitalbank."""
import sys
import time
from datetime import datetime
from pathlib import Path
import frida
PKG = "sg.com.maribankmobile.digitalbank"
HERE = Path(__file__).resolve().parent
LOGS_DIR = HERE.parent / "logs" / "frida"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
SCRIPT = HERE / "trace_maribank_sg_native.js"
LOG = LOGS_DIR / ("sg_native_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
def on_message(message, data):
mtype = message.get("type")
if mtype == "send":
line = message.get("payload")
elif mtype == "log":
line = message.get("payload", message.get("description", ""))
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 mtype == "error":
with open(str(LOG) + ".err", "a", encoding="utf-8") as f:
f.write(text + "\n")
def wait_for_process(device, pkg, timeout_sec=60):
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
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",
"am",
"start",
"-n",
pkg + "/com.shopee.bke.digitalbank.ui.MainActivity",
],
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]
if not SCRIPT.is_file():
raise SystemExit("missing script: %s" % SCRIPT)
ensure_frida_server()
device = frida.get_usb_device(timeout=10)
source = SCRIPT.read_text(encoding="utf-8")
print("Package: %s" % PKG)
print("Script: %s" % SCRIPT)
print("Log: %s" % LOG)
print("")
print("IMPORTANT: keep LSPosed scope ENABLED for SG (bypasses ADB page while tracing)")
print("")
if mode == "spawn":
pid = device.spawn([PKG])
session = device.attach(pid)
else:
pid = wait_for_process(device, PKG, 3)
if pid is None:
print("Launching MariBank SG ...")
launch_app(PKG)
pid = wait_for_process(device, PKG, 90)
if pid is None:
raise SystemExit(
"SG MariBank not running — open app to Sign up page, then re-run"
)
print("Attach 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("Spawn resumed, wait JVM 15s ...")
time.sleep(15)
else:
time.sleep(3)
print("Trace running. Sign up -> +65 -> Next. Ctrl+C to stop.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Stopping...")
session.detach()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""Spawn TNG eWallet with Frida native exit blockers."""
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
import frida
PKG = "my.com.tngdigital.ewallet"
HERE = Path(__file__).resolve().parent
LOGS_DIR = HERE.parent / "logs" / "frida"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
SCRIPT = HERE / "trace_tng_native_exit.js"
LOG = LOGS_DIR / ("tng_native_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
HOLD_SEC = 45
def on_message(message, data):
mtype = message.get("type")
if mtype == "send":
line = message.get("payload")
elif mtype == "log":
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 adb(*args):
return subprocess.run([ADB, *args], capture_output=True, text=True)
def ensure_frida_server():
out = adb("shell", "su", "-c", "pgrep -x frida-server")
if out.stdout.strip():
print("frida-server already running pid=%s" % out.stdout.strip())
return
print("starting frida-server ...")
adb("shell", "su", "-c", "pkill -9 frida-server; true")
# run in background via nohup-like
subprocess.Popen(
[ADB, "shell", "su", "-c", "/data/local/tmp/frida-server -D"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(2)
out = adb("shell", "su", "-c", "pgrep -x frida-server")
if not out.stdout.strip():
raise RuntimeError("frida-server failed to start")
print("frida-server pid=%s" % out.stdout.strip())
def main():
ensure_frida_server()
adb("shell", "am", "force-stop", PKG)
time.sleep(1)
device = frida.get_usb_device(10)
source = SCRIPT.read_text(encoding="utf-8")
print("Spawning %s ..." % PKG)
print("log=%s" % LOG)
pid = device.spawn([PKG])
session = device.attach(pid)
script = session.create_script(source)
script.on("message", on_message)
script.load()
print("script loaded, resume pid=%s" % pid)
device.resume(pid)
alive = 0
for i in range(HOLD_SEC):
time.sleep(1)
# check process still alive
out = adb("shell", "pidof", PKG)
pids = out.stdout.strip()
if not pids:
print("DEAD after %ss" % (i + 1))
break
alive = i + 1
if (i + 1) % 5 == 0:
print("alive %ss pid=%s" % (alive, pids))
else:
print("STABLE %ss pid=%s" % (HOLD_SEC, adb("shell", "pidof", PKG).stdout.strip()))
# dump activity focus
focus = adb("shell", "dumpsys", "activity", "activities")
for line in focus.stdout.splitlines():
if "tngdigital" in line.lower() and (
"mResumedActivity" in line
or "topResumedActivity" in line
or "UserLogin" in line
or "SecurityError" in line
or "Splash" in line
):
print("ACT: " + line.strip())
try:
session.detach()
except Exception:
pass
print("done alive=%ss log=%s" % (alive, LOG))
return 0 if alive >= 15 else 1
if __name__ == "__main__":
sys.exit(main())

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,178 @@
# -*- coding: utf-8 -*-
"""Compare: Xposed-only vs Frida-stealth-spawn."""
import subprocess
import time
import frida
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
STEALTH = r"""
"use strict";
function log(m){ send("[stealth] "+m); }
function findExport(mod, name) {
try {
var m = Process.getModuleByName(mod);
var a = m.findExportByName(name);
if (a) return a;
} catch (e) {}
try { return Module.getGlobalExportByName(name); } catch (e2) { return null; }
}
// Rename frida threads
try {
var pthread_setname = findExport("libc.so", "pthread_setname_np");
// also patch existing: best-effort via Java later
} catch (e) {}
// Hide maps
var markers = ["frida","gadget","linjector","gum-js","gmain","pool-frida","hluda"];
var tracked = {};
function hideLine(line) {
var l = (line||"").toLowerCase();
for (var i=0;i<markers.length;i++) if (l.indexOf(markers[i])>=0) return true;
return false;
}
function filterBuf(buf, len) {
try {
var t = buf.readUtf8String(len);
if (!t) return len;
var out = t.split("\n").filter(function(x){return !hideLine(x);}).join("\n");
var b = Memory.allocUtf8String(out);
var n = Math.min(len, out.length);
Memory.copy(buf, b, n);
return n;
} catch (e) { return len; }
}
var openat = findExport("libc.so","openat");
var readFn = findExport("libc.so","read");
if (openat) {
Interceptor.attach(openat, {
onEnter: function(args){ this.path = args[1].isNull()?null:args[1].readCString(); },
onLeave: function(retval){
var fd=retval.toInt32();
if (fd>=0 && this.path && (this.path.indexOf("maps")>=0 || this.path.indexOf("status")>=0 || this.path.indexOf("task")>=0))
tracked[fd]=this.path;
}
});
}
if (readFn) {
Interceptor.attach(readFn, {
onEnter: function(args){ this.fd=args[0].toInt32(); this.buf=args[1]; },
onLeave: function(retval){
var n=retval.toInt32();
if (n>0 && tracked[this.fd]) retval.replace(ptr(filterBuf(this.buf, n)));
}
});
}
// Patch libc exit_group SVC
var libc = Process.getModuleByName("libc.so");
libc.enumerateRanges("r-x").forEach(function(r){
for (var off=0; off+8<r.size; off+=4) {
var p=r.base.add(off);
var w;
try { w=p.readU32(); } catch(e){ return; }
if (w!==0xd2800bc8 && w!==0xd2800ba8 && w!==0x52800bc8 && w!==0x52800ba8) continue;
for (var j=4;j<=24;j+=4) {
var s=p.add(j);
try {
if (s.readU32()===0xd4000001) {
Memory.protect(s,4,"rwx");
s.writeU32(0xd65f03c0);
log("SVC->RET "+s);
}
} catch(e2){}
}
}
});
// Replace exit APIs
["_exit","exit","abort"].forEach(function(n){
var a=findExport("libc.so",n);
if(!a) return;
try {
Interceptor.replace(a, new NativeCallback(function(c){ log("block "+n+"("+c+")"); }, "void", ["int"]));
} catch(e){}
});
log("stealth ready");
"""
def adb(*a):
return subprocess.run([ADB, *a], capture_output=True, text=True)
def on_msg(m, _d):
print(m.get("payload", m), flush=True)
def wait_alive(sec, label):
for i in range(sec):
time.sleep(1)
p = adb("shell", "pidof", PKG).stdout.strip()
act = ""
dump = adb("shell", "dumpsys", "activity", "activities").stdout
for line in dump.splitlines():
if "tngdigital" in line.lower() and any(
x in line for x in ("UserLogin", "SecurityError", "Splash", "topResumedActivity")
):
act = line.strip()[:120]
break
print("%s t=%ds pid=%s act=%s" % (label, i + 1, p or "DEAD", act), flush=True)
if not p:
return i + 1
return sec
def test_xposed_only():
print("=== Xposed-only (monkey) ===", flush=True)
adb("shell", "am", "force-stop", PKG)
adb("logcat", "-c")
time.sleep(0.3)
adb(
"shell",
"monkey",
"-p",
PKG,
"-c",
"android.intent.category.LAUNCHER",
"1",
)
alive = wait_alive(15, "XPOSED")
print("xposed_alive_sec", alive, flush=True)
for line in adb("logcat", "-d").stdout.splitlines():
if "TngRoot" in line and any(
x in line for x in ("install", "UserLogin", "short-circuit", "finishing", "blocked intent")
):
print(line[line.find("TngRoot") :][:180], flush=True)
if "Displayed" in line and "tngdigital" in line:
print(line.strip()[:200], flush=True)
if "exited cleanly" in line:
print(line.strip()[:160], flush=True)
def test_frida_stealth():
print("=== Frida stealth spawn ===", flush=True)
adb("shell", "am", "force-stop", PKG)
time.sleep(0.3)
d = frida.get_usb_device(10)
pid = d.spawn([PKG])
s = d.attach(pid)
sc = s.create_script(STEALTH)
sc.on("message", on_msg)
sc.load()
d.resume(pid)
alive = wait_alive(15, "FRIDA")
print("frida_alive_sec", alive, flush=True)
def main():
test_xposed_only()
test_frida_stealth()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
import subprocess
import time
from pathlib import Path
import frida
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
SCRIPT = Path(__file__).with_name("trace_tng_diag_exit.js").read_text(encoding="utf-8")
def adb(*a):
return subprocess.run([ADB, *a], capture_output=True, text=True)
def on_msg(m, d):
if m.get("type") == "send":
print(m["payload"], flush=True)
else:
print(m, flush=True)
def main():
adb("shell", "am", "force-stop", PKG)
time.sleep(0.5)
d = frida.get_usb_device(10)
pid = d.spawn([PKG])
s = d.attach(pid)
sc = s.create_script(SCRIPT)
sc.on("message", on_msg)
sc.load()
print("resume", pid, flush=True)
d.resume(pid)
for i in range(15):
time.sleep(1)
p = adb("shell", "pidof", PKG).stdout.strip()
print("t=%ds pid=%s" % (i + 1, p or "DEAD"), flush=True)
if not p:
break
try:
s.detach()
except Exception:
pass
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import subprocess
import time
import frida
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
def adb(*a):
return subprocess.run([ADB, *a], capture_output=True, text=True)
def main():
adb("shell", "am", "force-stop", PKG)
adb("logcat", "-c")
time.sleep(0.3)
d = frida.get_usb_device(10)
pid = d.spawn([PKG])
s = d.attach(pid)
sc = s.create_script('send("empty ok " + String(Process.id));')
sc.on("message", lambda m, _d: print(m, flush=True))
sc.load()
print("resume", pid, flush=True)
d.resume(pid)
for i in range(10):
time.sleep(1)
p = adb("shell", "pidof", PKG).stdout.strip()
print("t=%d %s" % (i + 1, p or "DEAD"), flush=True)
if not p:
break
print("--- death lines ---", flush=True)
for line in adb("logcat", "-d").stdout.splitlines():
if "exited cleanly" in line or "has died" in line and "tngdigital" in line:
print(line[:240], flush=True)
if "TngRoot" in line and ("install" in line or "short-circuit" in line or "blocked" in line):
print(line[:240], flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
"""Resolve libc _exit real target and patch its SVC."""
import subprocess
import time
import frida
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
SCRIPT = r"""
"use strict";
function log(m){ send("[TNG-native] "+m); }
function findExport(mod, name) {
try {
var m = Process.getModuleByName(mod);
var a = m.findExportByName(name);
if (a) return a;
} catch (e) {}
try { return Module.getGlobalExportByName(name); } catch (e2) { return null; }
}
function resolveTrampoline(addr, name) {
// Follow simple ADRP+ADD+BR / LDR+BR patterns a few times
var cur = addr;
for (var depth = 0; depth < 5; depth++) {
var w0 = cur.readU32();
var w1 = cur.add(4).readU32();
// BR Xn: 0xD61F0000 | (Rn<<5)
if ((w1 & 0xfffffc1f) === 0xd61f0000) {
var rn = (w1 >> 5) & 0x1f;
// LDR Xn, [PC, #imm] : 0x58000000
if ((w0 & 0xff000000) === 0x58000000) {
var imm19 = (w0 >> 5) & 0x7ffff;
if (imm19 & 0x40000) imm19 -= 0x80000;
var targetPtr = cur.add(imm19 * 4);
var target = targetPtr.readPointer();
log(name + " trampoline LDR+BR -> " + target);
cur = target;
continue;
}
// ADRP Xn, page
if ((w0 & 0x9f000000) === 0x90000000) {
var rd = w0 & 0x1f;
var immhi = (w0 >> 5) & 0x7ffff;
var immlo = (w0 >> 29) & 0x3;
var imm = ((immhi << 2) | immlo) << 12;
if (imm & 0x100000000) imm = imm - 0x200000000;
var page = cur.and(ptr("0xfffffffffffff000")).add(imm);
// next might be LDR/ADD
var w2 = cur.add(8).readU32();
log(name + " ADRP page="+page+" rn="+rn+" rd="+rd+" w2="+w2.toString(16));
}
log(name + " BR X" + rn + " at " + cur + " (stop follow)");
break;
}
break;
}
return cur;
}
function patchSvcNear(addr, name, windowSize) {
var nop = [0x1f, 0x20, 0x03, 0xd5];
var n = 0;
for (var i = 0; i < windowSize; i += 4) {
try {
var p = addr.add(i);
if (p.readU32() !== 0xd4000001) continue;
Memory.protect(p, 4, "rwx");
// replace svc with: mov x0, x0; ret — or just nop and hope
// Better: movz x0, #0; ret so "exit" becomes return 0
// movz x0,#0 = 0xD2800000; ret = 0xD65F03C0
p.writeU32(0xd2800000); // movz x0, #0
if (i + 4 < windowSize) {
var p2 = addr.add(i + 4);
// only overwrite next if also svc/brk or nop pad — safer: write ret at svc place only via branch
}
// Just NOP the svc — caller may hang; use ret instead by overwriting svc with ret
p.writeU32(0xd65f03c0); // RET
n++;
log("patched SVC->RET @ " + p + " (" + name + "+" + i + ")");
} catch (e) {
log("patch fail: " + e);
}
}
return n;
}
function hookByPatchingLibcExit() {
var libc = Process.getModuleByName("libc.so");
// Scan entire libc for the classic exit_group sequence:
// mov x8, #94; mov x0, ...; svc #0 OR svc inside _exit impl
var count = 0;
var svcAddrs = [];
// Focused: exports that must lead to exit
["_exit", "exit"].forEach(function (n) {
var a = libc.findExportByName(n);
if (!a) return;
log(n + " export " + a);
// DebugSymbol / Instruction parse: find first BL/B to real impl
});
// Brute: scan libc executable for movz x8,#94 followed within 16 bytes by svc
var ranges = libc.enumerateRanges("r-x");
ranges.forEach(function (r) {
for (var off = 0; off + 8 < r.size; off += 4) {
var p = r.base.add(off);
var w;
try { w = p.readU32(); } catch (e) { return; }
// movz x8, #94 = 0xD2800BC8 ; movz w8,#94 = 0x52800BC8
// movz x8, #93 = 0xD2800BA8
if (w !== 0xd2800bc8 && w !== 0x52800bc8 && w !== 0xd2800ba8 && w !== 0x52800ba8) continue;
// look ahead for svc
for (var j = 4; j <= 24; j += 4) {
try {
if (p.add(j).readU32() === 0xd4000001) {
svcAddrs.push(p.add(j));
log("exit-seq movz@ " + p + " svc@ " + p.add(j));
}
} catch (e2) {}
}
}
});
svcAddrs.forEach(function (svc) {
try {
Memory.protect(svc, 4, "rwx");
// Replace svc with ret — turns exit into function return
svc.writeU32(0xd65f03c0);
count++;
log("SVC->RET " + svc);
} catch (e) {
log("SVC patch fail " + svc + ": " + e);
}
});
log("libc exit SVC patches=" + count);
}
function installEntryLog() {
["_exit", "exit", "abort"].forEach(function (n) {
var a = findExport("libc.so", n);
if (!a) return;
try {
Interceptor.attach(a, {
onEnter: function (args) {
log("ENTER " + n + "(" + args[0] + ")");
}
});
} catch (e) {}
});
}
// Also Interceptor.replace as backup
function installReplace() {
["_exit", "exit", "abort"].forEach(function (n) {
var a = findExport("libc.so", n);
if (!a) return;
try {
Interceptor.replace(a, new NativeCallback(function (code) {
log("REPLACED-HIT " + n + "(" + (code|0) + ")");
}, "void", ["int"]));
log("replaced " + n);
} catch (e) {
log("replace " + n + " fail: " + e);
}
});
}
log("pid=" + Process.id);
hookByPatchingLibcExit();
installReplace();
installEntryLog();
log("ready");
"""
def adb(*a):
return subprocess.run([ADB, *a], capture_output=True, text=True)
def on_msg(m, d):
print(m.get("payload", m), flush=True)
def main():
adb("shell", "am", "force-stop", PKG)
time.sleep(0.4)
device = frida.get_usb_device(10)
pid = device.spawn([PKG])
session = device.attach(pid)
script = session.create_script(SCRIPT)
script.on("message", on_msg)
script.load()
print("resume", pid, flush=True)
device.resume(pid)
for i in range(25):
time.sleep(1)
p = adb("shell", "pidof", PKG).stdout.strip()
print("t=%ds %s" % (i + 1, p or "DEAD"), flush=True)
if not p:
break
else:
print("STABLE", flush=True)
focus = adb("shell", "dumpsys", "activity", "activities").stdout
for line in focus.splitlines():
if "tngdigital" in line.lower() and any(
x in line for x in ("UserLogin", "SecurityError", "Splash", "mResumed", "topResumed")
):
print("ACT", line.strip()[:200], flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
"""Spawn TNG, observe TigerTally fread blocking (observe-only, no behavior change)."""
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
import frida
PKG = "my.com.tngdigital.ewallet"
HERE = Path(__file__).resolve().parent
LOGS_DIR = HERE.parent / "logs" / "frida"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
SCRIPT = HERE / "trace_tng_tiger_fread.js"
LOG = LOGS_DIR / ("tng_tiger_fread_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
HOLD_SEC = 45
def on_message(message, data):
mtype = message.get("type")
if mtype == "send":
line = message.get("payload")
elif mtype == "log":
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 adb(*args):
return subprocess.run([ADB, *args], capture_output=True, text=True)
def ensure_frida_server():
out = adb("shell", "su", "-c", "pgrep -x frida-server")
if out.stdout.strip():
print("frida-server running pid=%s" % out.stdout.strip())
return
adb("shell", "su", "-c", "pkill -9 frida-server; true")
subprocess.Popen(
[ADB, "shell", "su", "-c", "/data/local/tmp/frida-server -D"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(2)
out = adb("shell", "su", "-c", "pgrep -x frida-server")
if not out.stdout.strip():
raise RuntimeError("frida-server failed to start")
print("frida-server pid=%s" % out.stdout.strip())
def main():
ensure_frida_server()
adb("shell", "am", "force-stop", PKG)
time.sleep(1)
device = frida.get_usb_device(10)
source = SCRIPT.read_text(encoding="utf-8")
print("spawn %s ..." % PKG)
print("log=%s" % LOG)
pid = device.spawn([PKG])
session = device.attach(pid)
script = session.create_script(source)
script.on("message", on_message)
script.load()
print("script loaded, resume pid=%s" % pid)
device.resume(pid)
alive = 0
for i in range(HOLD_SEC):
time.sleep(1)
out = adb("shell", "pidof", PKG)
pids = out.stdout.strip()
if not pids:
print("DEAD after %ss" % (i + 1))
break
alive = i + 1
if (i + 1) % 5 == 0:
print("alive %ss pid=%s" % (alive, pids))
else:
print("STABLE %ss pid=%s" % (HOLD_SEC, adb("shell", "pidof", PKG).stdout.strip()))
# 焦点 Activity
focus = adb("shell", "dumpsys", "activity", "activities")
for line in focus.stdout.splitlines():
if "tngdigital" in line.lower() and (
"mResumedActivity" in line or "topResumedActivity" in line
or "UserLogin" in line or "SecurityError" in line or "Splash" in line
):
print("ACT: " + line.strip())
try:
session.detach()
except Exception:
pass
print("done alive=%ss log=%s" % (alive, LOG))
return 0 if alive >= 15 else 1
if __name__ == "__main__":
sys.exit(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,60 @@
# -*- coding: utf-8 -*-
"""Spawn TNG and strace for exit syscalls (needs root)."""
import subprocess
import time
import sys
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
def adb(*args, timeout=30):
return subprocess.run([ADB, *args], capture_output=True, text=True, timeout=timeout)
def main():
adb("shell", "am", "force-stop", PKG)
time.sleep(0.5)
adb("logcat", "-c")
# start app
adb(
"shell",
"monkey",
"-p",
PKG,
"-c",
"android.intent.category.LAUNCHER",
"1",
)
time.sleep(0.4)
out = adb("shell", "pidof", PKG)
pid = out.stdout.strip().split()[0] if out.stdout.strip() else ""
if not pid:
print("no pid")
return 1
print("pid", pid)
# strace briefly
p = subprocess.Popen(
[
ADB,
"shell",
"su",
"-c",
f"timeout 8 strace -f -e trace=exit,exit_group,kill,tkill,tgkill,write -p {pid} 2>&1 | head -80",
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
try:
stdout, _ = p.communicate(timeout=15)
print(stdout)
except subprocess.TimeoutExpired:
p.kill()
print(p.stdout.read() if p.stdout else "timeout")
print("alive?", adb("shell", "pidof", PKG).stdout.strip())
return 0
if __name__ == "__main__":
sys.exit(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

@@ -0,0 +1,634 @@
'use strict';
/**
* MariBank SG 3.2.2 — Java + native attestation / encrypt trace
* Package: sg.com.maribankmobile.digitalbank
*
* SG 差异: 无 utils.d / com.shopee.shpssdk.*,仅 shpssdkbank + uvwuvwuv
*/
const TAG = '[MB-NATIVE]';
const MAX_STR = 4000;
const MAX_BYTES_LOG = 8192;
const HOOKED_NATIVE_PTRS = {};
const JAVA_TARGETS = [
'com.shopee.shpssdkbank.wvvvuwwu',
'com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv',
'com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu',
'com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu',
'com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw',
'com.shopee.bke.lib.jni.utils.uvwuvwuv',
'com.shopee.bke.lib.jni.utils.uvwwwwuv',
'com.shopee.shpssdkbank.SHPSSDK',
];
const SO_WATCH = [
'libshpssdk_bank.so',
'libshpssdk.so',
'libsdkutils.so',
'libbkutils.so',
];
function log(msg) {
send(TAG + ' ' + msg);
}
function jniFn(envPtr, index, ret, args) {
const funcs = envPtr.readPointer();
const addr = funcs.add(index * Process.pointerSize).readPointer();
if (!addr || addr.isNull()) return null;
return new NativeFunction(addr, ret, args);
}
function jniReadByteArray(envPtr, jarrayPtr) {
if (!jarrayPtr || jarrayPtr.isNull()) return null;
try {
const GetArrayLength = jniFn(envPtr, 171, 'int', ['pointer', 'pointer']);
const GetByteArrayElements = jniFn(envPtr, 184, 'pointer', ['pointer', 'pointer', 'pointer']);
const ReleaseByteArrayElements = jniFn(envPtr, 187, 'void', ['pointer', 'pointer', 'pointer', 'int']);
if (!GetArrayLength || !GetByteArrayElements || !ReleaseByteArrayElements) {
return jniReadByteArrayArt(envPtr, jarrayPtr);
}
const len = GetArrayLength(envPtr, jarrayPtr);
if (len <= 0 || len > MAX_BYTES_LOG) return { len: len, hex: '', text: '' };
const elems = GetByteArrayElements(envPtr, jarrayPtr, ptr(0));
if (!elems || elems.isNull()) return { len: len, hex: '', text: '' };
const raw = elems.readByteArray(Math.min(len, MAX_BYTES_LOG));
ReleaseByteArrayElements(envPtr, jarrayPtr, elems, 0);
return bytesToPreview(raw, len);
} catch (e) {
return { len: -1, hex: 'err:' + e, text: '' };
}
}
function bytesToPreview(raw, len) {
const arr = new Uint8Array(raw);
let text = '';
try {
text = String.fromCharCode.apply(null, arr);
if (text.indexOf('\u0000') >= 0 || !/^[\x20-\x7e\r\n\t\u4e00-\u9fff\u0100-\u024f]+$/.test(text.substring(0, Math.min(text.length, 200)))) {
text = '';
}
} catch (e) {
text = '';
}
if (text.length > MAX_STR) text = text.substring(0, MAX_STR) + '...';
return { len: len, hex: hexPreview(arr, 64), text: text, arr: arr };
}
function jniReadByteArrayArt(envPtr, jarrayPtr) {
const art = moduleByName('libart.so');
if (!art) return { len: -1, hex: 'err:no-art', text: '' };
let sym = null;
art.enumerateSymbols().forEach(function (s) {
if (sym) return;
if (s.name.indexOf('GetByteArrayRegion') >= 0 && s.name.indexOf('JNI') >= 0) {
sym = s.address;
}
});
if (!sym) return { len: -1, hex: 'err:no-GetByteArrayRegion', text: '' };
const GetLen = jniFn(envPtr, 171, 'int', ['pointer', 'pointer']);
const len = GetLen ? GetLen(envPtr, jarrayPtr) : 0;
if (len <= 0 || len > MAX_BYTES_LOG) return { len: len, hex: '', text: '' };
const buf = Memory.alloc(len);
const GetRegion = new NativeFunction(sym, 'void', ['pointer', 'pointer', 'int', 'int', 'pointer']);
GetRegion(envPtr, jarrayPtr, 0, len, buf);
return bytesToPreview(buf.readByteArray(len), len);
}
function jniReadJstring(envPtr, jstrPtr) {
if (!jstrPtr || jstrPtr.isNull()) return '';
try {
const GetStringUTFChars = jniFn(envPtr, 169, 'pointer', ['pointer', 'pointer', 'pointer']);
const ReleaseStringUTFChars = jniFn(envPtr, 170, 'void', ['pointer', 'pointer', 'pointer']);
if (!GetStringUTFChars || !ReleaseStringUTFChars) return '';
const chars = GetStringUTFChars(envPtr, jstrPtr, ptr(0));
if (!chars || chars.isNull()) return '';
const s = chars.readCString();
ReleaseStringUTFChars(envPtr, jstrPtr, chars);
return s || '';
} catch (e) {
return '';
}
}
function dumpNativeArgs(methodName, sig, envPtr, args) {
if (methodName === 'vuwuuwvw' && sig.indexOf('[B[B') >= 0) {
const a0 = jniReadByteArray(envPtr, args[2]);
const a1 = jniReadByteArray(envPtr, args[3]);
if (a0) log(' nat in0 len=' + a0.len + ' hex=' + a0.hex);
if (a1) log(' nat in1 len=' + a1.len + ' hex=' + a1.hex);
return;
}
if (methodName === 'uvwuuww') {
const plain = jniReadByteArray(envPtr, args[2]);
const key = jniReadJstring(envPtr, args[3]);
const flag = args[4] ? args[4].toInt32() : 0;
if (plain) {
log(' nat plain len=' + plain.len + ' hex=' + plain.hex);
if (plain.text) log(' nat plain utf8=' + plain.text);
}
if (key) log(' nat key=' + key + ' flag=' + flag);
return;
}
if (methodName === 'vuwuuuwv' && sig.indexOf('[B[B') >= 0) {
const a0 = jniReadByteArray(envPtr, args[2]);
const a1 = jniReadByteArray(envPtr, args[3]);
if (a0) log(' nat defense in0 len=' + a0.len + ' hex=' + a0.hex);
if (a1) log(' nat defense in1 len=' + a1.len + ' hex=' + a1.hex);
}
}
function hexPreview(arr, limit) {
if (!arr) return '';
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 += '...(' + arr.length + ')';
return hex;
}
function dumpBytes(label, jobj) {
if (jobj === null || jobj === undefined) {
log(label + ' = null');
return;
}
try {
const arr = Java.cast(jobj, Java.use('[B'));
let text = '';
try {
text = Java.use('java.lang.String').$new(arr, 'UTF-8').toString();
} catch (e) {
text = '';
}
const printable = text.length > 0 && text.indexOf('\u0000') < 0;
if (printable && (text.indexOf('rdVerifyInfo') >= 0 || text.indexOf('REGISTRATION') >= 0
|| text.indexOf('deviceFingerprint') >= 0 || text.length < MAX_STR)) {
const show = text.length > MAX_STR ? text.substring(0, MAX_STR) + '...' : text;
log(label + ' byte[' + arr.length + '] utf8=' + show);
} else {
log(label + ' byte[' + arr.length + '] hex=' + hexPreview(arr, 48));
}
} catch (e) {
log(label + ' dump err=' + e);
}
}
function dumpJava(label, obj) {
if (obj === null || obj === undefined) {
log(label + ' = null');
return;
}
try {
const cls = obj.getClass().getName();
if (cls === '[B') {
dumpBytes(label, obj);
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(label + ' String(' + s.length + ') ' + show);
return;
}
if (cls === '[Ljava.lang.String;') {
const arr = Java.cast(obj, Java.use('[Ljava.lang.String;'));
log(label + ' String[' + arr.length + ']');
for (let i = 0; i < arr.length; i++) dumpJava(label + '[' + i + ']', arr[i]);
return;
}
if (cls === '[[B') {
const outer = Java.cast(obj, Java.use('[[B'));
log(label + ' byte[][] len=' + outer.length);
for (let i = 0; i < outer.length; i++) dumpBytes(label + '[' + i + ']', outer[i]);
return;
}
log(label + ' ' + cls + ' = ' + obj.toString());
} catch (e) {
log(label + ' err=' + e);
}
}
function shouldLogRegisterText(s) {
if (!s) return false;
const low = s.toLowerCase();
return low.indexOf('register') >= 0 || low.indexOf('rdverifyinfo') >= 0
|| low.indexOf('datakey') >= 0 || low.indexOf('fingerprint') >= 0
|| low.indexOf('3100012') >= 0 || s.indexOf('|') >= 0;
}
/* ---------- native: dlopen + RegisterNatives ---------- */
function moduleExport(moduleName, symbol) {
if (typeof Module.getExportByName === 'function') {
try {
return Module.getExportByName(moduleName, symbol);
} catch (e) {
return null;
}
}
if (typeof Module.findExportByName === 'function') {
return Module.findExportByName(moduleName, symbol);
}
return null;
}
function moduleByName(name) {
if (typeof Process.getModuleByName === 'function') {
try {
return Process.getModuleByName(name);
} catch (e) {
return null;
}
}
if (typeof Process.findModuleByName === 'function') {
return Process.findModuleByName(name);
}
return null;
}
function moduleByAddress(addr) {
if (typeof Process.getModuleByAddress === 'function') {
try {
return Process.getModuleByAddress(addr);
} catch (e) {
return null;
}
}
if (typeof Process.findModuleByAddress === 'function') {
return Process.findModuleByAddress(addr);
}
return null;
}
function hookDlopen() {
const names = ['android_dlopen_ext', '__loader_android_dlopen_ext', 'dlopen'];
names.forEach(function (sym) {
const addr = moduleExport(null, sym);
if (!addr) return;
Interceptor.attach(addr, {
onEnter(args) {
try {
this.path = args[0].readCString();
} catch (e) {
this.path = '';
}
},
onLeave() {
if (!this.path) return;
SO_WATCH.forEach(function (so) {
if (this.path.indexOf(so) >= 0) log('dlopen ' + this.path);
}, this);
},
});
log('hooked ' + sym);
});
}
function findRegisterNatives() {
const art = moduleByName('libart.so');
if (!art) return null;
let found = null;
art.enumerateSymbols().forEach(function (sym) {
if (found) return;
const n = sym.name;
if (n.indexOf('RegisterNatives') >= 0
&& n.indexOf('CheckJNI') < 0
&& n.indexOf('art') >= 0) {
found = sym.address;
}
});
return found;
}
function hookNativePtr(className, methodName, sig, fnPtr) {
const key = fnPtr.toString();
if (HOOKED_NATIVE_PTRS[key]) return;
HOOKED_NATIVE_PTRS[key] = true;
const mod = moduleByAddress(fnPtr);
const modName = mod ? mod.name : '?';
const off = mod ? fnPtr.sub(mod.base) : fnPtr;
log('RegisterNatives HOOK ' + className + '.' + methodName + sig
+ ' @ ' + modName + '+0x' + off.toString(16));
try {
Interceptor.attach(fnPtr, {
onEnter(args) {
this.mname = methodName;
this.msig = sig;
this.env = args[0];
log('native>> ' + className + '.' + methodName + sig);
dumpNativeArgs(methodName, sig, this.env, args);
},
onLeave(retval) {
log('native<< ' + className + '.' + methodName + ' ret=' + retval);
},
});
} catch (e) {
log('Interceptor.attach fail ' + methodName + ': ' + e);
}
}
function resolveJClassName(jclassPtr) {
if (typeof Java === 'undefined' || !Java.available) {
return '';
}
let className = '';
const run = (typeof Java.performNow === 'function') ? Java.performNow : Java.perform;
try {
run(function () {
className = Java.cast(jclassPtr, Java.use('java.lang.Class')).getName();
});
} catch (e) {
className = '';
}
return className;
}
function isInterestingSo(modName) {
if (!modName) return false;
return modName.indexOf('shpssdk') >= 0
|| modName.indexOf('sdkutils') >= 0
|| modName.indexOf('bkutils') >= 0;
}
function hookRegisterNatives() {
const addr = findRegisterNatives();
if (!addr) {
log('RegisterNatives symbol not found');
return;
}
Interceptor.attach(addr, {
onEnter(args) {
const count = args[3].toInt32();
const methods = args[2];
const clazz = args[1];
const className = resolveJClassName(clazz) || '<unknown>';
const classHit = className.indexOf('shpssdk') >= 0
|| className.indexOf('jni.utils') >= 0
|| className.indexOf('bke.lib.jni') >= 0;
const ptrSize = Process.pointerSize;
let loggedClass = false;
for (let i = 0; i < count; i++) {
const base = methods.add(i * ptrSize * 3);
const name = base.readPointer().readCString();
const sig = base.add(ptrSize).readPointer().readCString();
const fnPtr = base.add(ptrSize * 2).readPointer();
const mod = moduleByAddress(fnPtr);
const modName = mod ? mod.name : '';
if (!classHit && !isInterestingSo(modName)) continue;
if (!loggedClass) {
log('RegisterNatives class=' + className + ' count=' + count);
loggedClass = true;
}
log(' JNI ' + name + sig + ' -> ' + fnPtr + ' (' + modName + ')');
hookNativePtr(className, name, sig, fnPtr);
}
},
});
log('hooked RegisterNatives @ ' + addr);
}
/* ---------- Java: hook static native + key methods ---------- */
function hookJavaMethod(className, clazz, methodName, isNative, retName, isStatic) {
try {
const overloads = clazz[methodName].overloads;
overloads.forEach(function (ovl) {
ovl.implementation = function () {
const args = [].slice.call(arguments);
log('Java>> ' + className + '.' + methodName
+ (isStatic ? ' static' : '')
+ (isNative ? ' native' : '') + ' args=' + args.length);
args.forEach(function (a, idx) { dumpJava(' in' + idx, a); });
const ret = ovl.apply(this, args);
if (retName === 'void') {
log('Java<< ' + methodName + ' void');
} else if (retName === '[B') {
dumpBytes(' out', ret);
} else if (retName === 'java.lang.String') {
dumpJava(' out', ret);
} else if (retName === '[[B') {
dumpJava(' out', ret);
} else if (retName === 'boolean' || retName === 'int' || retName === 'long') {
log(' out=' + ret);
} else {
dumpJava(' out', ret);
}
return ret;
};
});
log('hooked ' + className + '.' + methodName + ' overloads=' + overloads.length
+ (isNative ? ' native' : '') + (isStatic ? ' static' : ''));
return 1;
} catch (e) {
return 0;
}
}
function hookClassMethods(className, staticOnly, instanceOnly) {
let clazz;
try {
clazz = Java.use(className);
} catch (e) {
log('skip Java class ' + className + ': ' + e);
return 0;
}
const Modifier = Java.use('java.lang.reflect.Modifier');
const declared = clazz.class.getDeclaredMethods();
let hooked = 0;
for (let i = 0; i < declared.length; i++) {
const m = declared[i];
const isStatic = Modifier.isStatic(m.getModifiers());
if (staticOnly && !isStatic) continue;
if (instanceOnly && isStatic) continue;
const methodName = m.getName();
const isNative = Modifier.isNative(m.getModifiers());
const retName = m.getReturnType().getName();
hooked += hookJavaMethod(className, clazz, methodName, isNative, retName, isStatic);
}
return hooked;
}
function hookShpsSdkFacade() {
try {
const SHPSSDK = Java.use('com.shopee.shpssdkbank.SHPSSDK');
['getRiskToken', 'getRiskSync', 'requestDefense', 'assessRisk'].forEach(function (name) {
if (!SHPSSDK[name]) return;
SHPSSDK[name].overloads.forEach(function (ovl) {
ovl.implementation = function () {
const args = [].slice.call(arguments);
log('Java>> SHPSSDK.' + name);
args.forEach(function (a, idx) { dumpJava(' in' + idx, a); });
const ret = ovl.apply(this, args);
dumpJava(' out', ret);
return ret;
};
});
log('hooked SHPSSDK.' + name);
});
} catch (e) {
log('SHPSSDK facade skip: ' + e);
}
}
function hookOkHttp() {
try {
const RealCall = Java.use('okhttp3.RealCall');
RealCall.execute.implementation = function () {
const req = this.request();
const url = req.url().toString();
if (url.indexOf('register') >= 0 || url.indexOf('dfp') >= 0 || url.indexOf('uapi') >= 0) {
log('HTTP>> ' + req.method() + ' ' + url);
}
const resp = this.execute.call(this);
if (url.indexOf('register') >= 0) {
try {
const peek = resp.peekBody(Java.use('java.lang.Long').parseLong('1048576'));
log('HTTP<< register ' + peek.string());
} catch (e) {
log('HTTP<< register peek err=' + e);
}
}
return resp;
};
log('hooked OkHttp RealCall.execute');
} catch (e) {
log('OkHttp skip: ' + e);
}
}
function hookGsonRegister() {
try {
const Gson = Java.use('com.google.gson.Gson');
Gson.toJson.overload('java.lang.Object').implementation = function (obj) {
const ret = this.toJson(obj);
if (shouldLogRegisterText(ret)) {
const show = ret.length > MAX_STR ? ret.substring(0, MAX_STR) + '...' : ret;
log('Gson.toJson REGISTRATION len=' + ret.length + ' ' + show);
}
return ret;
};
log('hooked Gson.toJson');
} catch (e) {
log('Gson skip: ' + e);
}
}
function hookRiskTokenEntry() {
try {
const V = Java.use('com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv');
hookJavaMethod(
'com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv',
V, 'wwvuwuwvu', false, 'java.lang.String', true);
} catch (e) {
log('vvuuuuvvv skip: ' + e);
}
}
function hookEncryptHelper() {
hookClassMethods('com.shopee.bke.lib.jni.utils.uvwwwwuv', false, true);
}
function isAdbSettingKey(key) {
if (!key) return false;
const lower = key.toLowerCase();
return lower.indexOf('adb') >= 0
|| lower === 'development_settings_enabled'
|| lower.indexOf('wireless_debug') >= 0;
}
function hookAdbBypassJava() {
try {
const fakeInt = function (key) {
if (isAdbSettingKey(key)) {
log('fake Settings int ' + key + ' -> 0');
return 0;
}
return null;
};
const fakeStr = function (key) {
if (isAdbSettingKey(key)) {
log('fake Settings str ' + key + ' -> 0');
return '0';
}
return null;
};
['Global', 'Secure', 'System'].forEach(function (bucket) {
const Cls = Java.use('android.provider.Settings$' + bucket);
Cls.getInt.overloads.forEach(function (ovl) {
ovl.implementation = function () {
const key = arguments[1];
const f = fakeInt(String(key));
if (f !== null) return f;
return ovl.apply(this, arguments);
};
});
if (Cls.getString) {
Cls.getString.overloads.forEach(function (ovl) {
ovl.implementation = function () {
const key = arguments[1];
const f = fakeStr(String(key));
if (f !== null) return f;
return ovl.apply(this, arguments);
};
});
}
});
const SysProp = Java.use('android.os.SystemProperties');
SysProp.get.overload('java.lang.String').implementation = function (key) {
if (key === 'init.svc.adbd' || key === 'init.svc.adb_wifi') {
log('fake SystemProperties ' + key + ' -> stopped');
return 'stopped';
}
return this.get(key);
};
log('hooked ADB Settings/SystemProperties bypass');
} catch (e) {
log('ADB Java bypass skip: ' + e);
}
}
function installJavaHooks() {
hookAdbBypassJava();
let total = 0;
JAVA_TARGETS.forEach(function (cn) {
total += hookClassMethods(cn, true, false);
});
hookRiskTokenEntry();
hookEncryptHelper();
hookShpsSdkFacade();
hookOkHttp();
hookGsonRegister();
log('Java hooks installed methods=' + total + ' pid=' + Process.id);
log('READY SG — Sign up -> +65 -> Next (watch native>> / Gson / HTTP)');
}
function waitForJava(n) {
n = n || 0;
if (typeof Java === 'undefined' || !Java.available) {
if (n % 10 === 0) log('waiting Java.available attempt=' + n);
setTimeout(function () { waitForJava(n + 1); }, 500);
return;
}
Java.perform(function () {
installJavaHooks();
});
}
setImmediate(function () {
log('SG native trace loaded pid=' + Process.id);
hookDlopen();
hookRegisterNatives();
waitForJava(0);
});

View File

@@ -0,0 +1,74 @@
/**
* Diagnostic: only LOG exit-related calls, do not block.
*/
"use strict";
function log(msg) { send("[TNG-diag] " + msg); }
function findExport(mod, name) {
try {
var m = Process.findModuleByName(mod);
if (m) { var a = m.findExportByName(name); if (a) return a; }
} catch (e) {}
try { return Module.getGlobalExportByName(name); } catch (e2) { return null; }
}
function bt(ctx) {
try {
return Thread.backtrace(ctx, Backtracer.FUZZY).map(DebugSymbol.fromAddress).slice(0, 8).join(" <- ");
} catch (e) { return "?"; }
}
["_exit", "exit", "abort", "quick_exit"].forEach(function (n) {
var a = findExport("libc.so", n);
if (!a) return;
Interceptor.attach(a, {
onEnter: function (args) {
log("CALL " + n + "(" + args[0] + ") " + bt(this.context));
}
});
log("watch " + n + " @ " + a);
});
["kill", "tgkill", "raise"].forEach(function (n) {
var a = findExport("libc.so", n);
if (!a) return;
Interceptor.attach(a, {
onEnter: function (args) {
log("CALL " + n + "(" + args[0] + "," + args[1] + ") " + bt(this.context));
}
});
});
var sys = findExport("libc.so", "syscall");
if (sys) {
Interceptor.attach(sys, {
onEnter: function (args) {
var nr = args[0].toInt32();
if (nr === 93 || nr === 94 || nr === 129 || nr === 131) {
log("CALL syscall(" + nr + ") " + bt(this.context));
}
}
});
}
// count mprotect EXEC
var mp = findExport("libc.so", "mprotect");
if (mp) {
Interceptor.attach(mp, {
onEnter: function (args) {
if (args[2].toInt32() & 4) {
log("mprotect EXEC " + args[0] + " len=" + args[1]);
}
}
});
}
log("diag ready pid=" + Process.id);
setTimeout(function () {
var n = 0;
Process.enumerateRanges("r-x").forEach(function (r) {
var file = r.file ? r.file.path : "anon";
if (file.indexOf("/system") === 0 || file.indexOf("/apex") === 0) return;
n++;
log("RX " + file + " " + r.base + " +" + r.size);
});
log("app RX ranges=" + n);
}, 800);

View File

@@ -0,0 +1,282 @@
/**
* TNG — catch Promon exit after runtime code decrypt (mmap/mprotect RX).
* Frida 17 compatible.
*/
"use strict";
function log(msg) {
send("[TNG-native] " + msg);
}
function findExport(moduleName, name) {
try {
if (moduleName) {
var m = Process.findModuleByName(moduleName);
if (m) {
var a = m.findExportByName(name);
if (a) return a;
}
}
} catch (e) {}
try {
return Module.getGlobalExportByName(name);
} catch (e2) {
return null;
}
}
var patched = {};
function looksLikeExitSetup(addr) {
for (var i = 1; i <= 12; i++) {
try {
var w = addr.sub(i * 4).readU32();
var opc = w & 0xff800000;
if (opc === 0x52800000 || opc === 0xd2800000) {
var rd = w & 0x1f;
var imm = (w >> 5) & 0xffff;
if (rd === 8 && (imm === 93 || imm === 94)) return imm;
}
// mov x8, xN then earlier load — also catch svc after mov x0, #imm (exit code)
if ((w & 0xffe0ffff) === 0xaa0003e8) return 8; // mov x8, x0.. pattern loose
} catch (e) {}
}
return 0;
}
function patchRegion(base, size, tag) {
if (size <= 0 || size > 64 * 1024 * 1024) return;
var key = base + ":" + size;
if (patched[key]) return;
patched[key] = true;
var nop = [0x1f, 0x20, 0x03, 0xd5];
var n = 0;
var totalSvc = 0;
try {
// only scan 4-byte aligned by walking manually for reliability
var end = base.add(size - 4);
for (var p = base; p.compare(end) <= 0; p = p.add(4)) {
var w;
try {
w = p.readU32();
} catch (e) {
break;
}
if (w !== 0xd4000001) continue; // svc #0
totalSvc++;
var kind = looksLikeExitSetup(p);
if (!kind) continue;
try {
Memory.protect(p, 4, "rwx");
p.writeByteArray(nop);
n++;
log("patched exit SVC#" + kind + " @ " + p + " [" + tag + "]");
} catch (e2) {
log("patch err " + p + ": " + e2);
}
}
if (totalSvc > 0) {
log("region " + tag + " svc#0=" + totalSvc + " patched=" + n + " size=" + size);
}
} catch (e) {
log("scan err " + tag + ": " + e);
}
}
function scanAllExecutable(tag) {
Process.enumerateRanges("r-x").forEach(function (r) {
var file = r.file ? r.file.path : "anon";
// skip system libs except if anonymous / app
if (file.indexOf("/system/") === 0 || file.indexOf("/apex/") === 0) return;
if (file.indexOf("frida") >= 0) return;
patchRegion(r.base, r.size, tag + ":" + file);
});
}
function installLibcExitHooks() {
function blockExit(name, address) {
try {
Interceptor.replace(
address,
new NativeCallback(
function (code) {
log("BLOCKED " + name + "(" + (code | 0) + ")");
},
"void",
["int"]
)
);
log("replaced " + name);
} catch (e) {
Interceptor.attach(address, {
onEnter: function (args) {
log("BLOCKED(attach) " + name + "(" + args[0].toInt32() + ")");
while (true) Thread.sleep(60);
},
});
log("attached " + name);
}
}
["_exit", "exit", "abort", "quick_exit"].forEach(function (n) {
var a = findExport("libc.so", n);
if (a) blockExit(n, a);
});
["kill", "tgkill", "pthread_kill", "raise"].forEach(function (n) {
var a = findExport("libc.so", n);
if (!a) return;
Interceptor.attach(a, {
onEnter: function (args) {
var pid = args[0].toInt32();
var sig = args[1].toInt32();
if ((pid === Process.id || pid === 0 || pid === -1) &&
(sig === 9 || sig === 15 || sig === 6 || sig === 5)) {
log("BLOCKED " + n + " sig=" + sig);
args[1] = ptr(0);
}
},
});
});
var sys = findExport("libc.so", "syscall");
if (sys) {
Interceptor.attach(sys, {
onEnter: function (args) {
var nr = args[0].toInt32();
if (nr === 93 || nr === 94) {
log("BLOCKED syscall exit " + nr);
args[0] = ptr(-1);
}
},
});
}
log("libc hooks OK");
}
function installMprotectWatcher() {
var mprotect = findExport("libc.so", "mprotect");
var mmap = findExport("libc.so", "mmap");
if (mprotect) {
Interceptor.attach(mprotect, {
onEnter: function (args) {
this.addr = args[0];
this.len = args[1].toInt32();
this.prot = args[2].toInt32();
},
onLeave: function () {
// PROT_EXEC = 4
if (this.prot & 4) {
log("mprotect+EXEC " + this.addr + " len=" + this.len);
patchRegion(this.addr, this.len, "mprotect");
}
},
});
}
if (mmap) {
Interceptor.attach(mmap, {
onEnter: function (args) {
this.len = args[1].toInt32();
this.prot = args[2].toInt32();
},
onLeave: function (retval) {
if ((this.prot & 4) && !retval.isNull()) {
log("mmap+EXEC " + retval + " len=" + this.len);
patchRegion(retval, this.len, "mmap");
}
},
});
}
log("mprotect/mmap watchers OK");
}
function installMapsHide() {
var markers = ["frida", "gadget", "xposed", "lsposed", "vector", "zygisk", "magisk", "liblspd"];
var tracked = {};
function hide(line) {
var l = (line || "").toLowerCase();
for (var i = 0; i < markers.length; i++) if (l.indexOf(markers[i]) >= 0) return true;
return false;
}
function filter(buf, len) {
try {
var text = buf.readUtf8String(len);
if (!text) return len;
var out = text.split("\n").filter(function (x) { return !hide(x); }).join("\n");
var bytes = Memory.allocUtf8String(out);
var n = Math.min(len, out.length);
Memory.copy(buf, bytes, n);
return n;
} catch (e) {
return len;
}
}
var openat = findExport("libc.so", "openat");
var readFn = findExport("libc.so", "read");
if (openat) {
Interceptor.attach(openat, {
onEnter: function (args) {
this.path = args[1].isNull() ? null : args[1].readCString();
},
onLeave: function (retval) {
var fd = retval.toInt32();
if (fd >= 0 && this.path && this.path.indexOf("maps") >= 0) tracked[fd] = 1;
},
});
}
if (readFn) {
Interceptor.attach(readFn, {
onEnter: function (args) {
this.fd = args[0].toInt32();
this.buf = args[1];
},
onLeave: function (retval) {
var n = retval.toInt32();
if (n > 0 && tracked[this.fd]) retval.replace(ptr(filter(this.buf, n)));
},
});
}
log("maps hide OK");
}
function installJavaGuards() {
if (typeof Java === "undefined") {
setTimeout(installJavaGuards, 500);
return;
}
Java.perform(function () {
try {
Java.use("java.lang.System").exit.implementation = function (c) {
log("Java System.exit(" + c + ") blocked");
};
} catch (e) {}
try {
var R = Java.use("java.lang.Runtime");
R.exit.overload("int").implementation = function (c) {
log("Java Runtime.exit(" + c + ") blocked");
};
} catch (e) {}
try {
var P = Java.use("android.os.Process");
P.killProcess.implementation = function (pid) {
if (pid === P.myPid()) {
log("Java killProcess(self) blocked");
return;
}
return this.killProcess(pid);
};
} catch (e) {}
log("Java guards OK");
});
}
log("load pid=" + Process.id);
installLibcExitHooks();
installMapsHide();
installMprotectWatcher();
scanAllExecutable("boot");
setInterval(function () {
scanAllExecutable("tick");
}, 1000);
installJavaGuards();
log("ready");

View File

@@ -0,0 +1,147 @@
"use strict";
/*
* TNG eWallet — 定位 TigerTally(libtiger_tally.so) 启动期 fread 阻塞。观察不改行为。
*
* 背景(ANR 栈): CaptchaInitializer → TigerTallyAPI.init → t.B.genericNt1(native)
* → libtiger_tally.so (mNYjyzyN23) → fread → __sread → read 永远读不到数据
*/
const TIGER_SO = "libtiger_tally.so";
const LIBC = Process.getModuleByName("libc.so");
const readlink = new NativeFunction(
LIBC.findExportByName("readlink"), "long", ["pointer", "pointer", "ulong"]);
/* ---- Tiger 模块范围(热路径缓存,每 2s 刷新一次) ---- */
let tigerMod = null;
function refreshTiger() {
const m = Process.findModuleByName(TIGER_SO);
if (m) tigerMod = m;
return !!tigerMod;
}
function inTiger(addr) {
if (!addr) return false;
if (!tigerMod) return false;
return addr.compare(tigerMod.base) >= 0 && addr.compare(tigerMod.base.add(tigerMod.size)) < 0;
}
setInterval(() => { refreshTiger(); }, 2000);
function resolveFd(fd) {
try {
const link = Memory.allocUtf8String(`/proc/self/fd/${fd}`);
const out = Memory.alloc(256);
const n = readlink(link, out, 256);
if (n > 0) return out.readUtf8String(Math.min(n, 255));
} catch (e) { /* ignore */ }
return "?";
}
function fdKind(fd) {
const p = resolveFd(fd);
if (p.indexOf("socket:") === 0) return "SOCKET " + p;
if (p.indexOf("pipe:") === 0) return "PIPE " + p;
if (p.indexOf("anon_inode:") === 0) return "ANON " + p;
return p;
}
function threadName() {
try { return Process.getCurrentThreadName(); } catch (e) { return "?"; }
}
function fmtAddr(a) { return a ? a.toString(16) : "?"; }
const stats = {}; // tid -> info
function bump(fd, kind, ret) {
const tid = Process.getCurrentThreadId();
let s = stats[tid];
if (!s) { s = { name: threadName(), reads: 0, lastFd: fd, lastFdKind: kind, lastRet: ret }; stats[tid] = s; }
s.name = threadName();
s.reads++;
s.lastFd = fd;
s.lastFdKind = kind;
s.lastRet = ret;
}
/* ---- fread: FILE* 第4参数; bionic __sFILE._file 偏移约 18 ---- */
Interceptor.attach(LIBC.findExportByName("fread"), {
onEnter(args) {
const caller = this.returnAddress;
if (!inTiger(caller)) return;
const fp = args[3];
let fd = -1;
for (const off of [18, 16, 24, 20]) {
try { const v = fp.add(off).readU16(); if (v > 0 && v < 4096) { fd = v; break; } }
catch (e) { /* try next */ }
}
const kind = fd > 0 ? fdKind(fd) : "?";
bump(fd, kind, "pending");
console.log(`[FREAD] tid=${Process.getCurrentThreadId()} "${threadName()}" caller=${fmtAddr(caller)} fd=${fd} kind=${kind}`);
},
onLeave(ret) {
if (!inTiger(this.returnAddress)) return;
bump(-1, "", ret.toInt32());
console.log(`[FREAD-LEAVE] tid=${Process.getCurrentThreadId()} ret=${ret.toInt32()}`);
}
});
/* ---- read: 只观察调用者位于 libtiger_tally.so 的 ---- */
Interceptor.attach(LIBC.findExportByName("read"), {
onEnter(args) {
const caller = this.returnAddress;
if (!inTiger(caller)) return;
const fd = args[0].toInt32();
const kind = fdKind(fd);
bump(fd, kind, "?");
console.log(`[READ] tid=${Process.getCurrentThreadId()} "${threadName()}" caller=${fmtAddr(caller)} fd=${fd} kind=${kind}`);
},
onLeave(ret) {
if (!inTiger(this.returnAddress)) return;
const tid = Process.getCurrentThreadId();
const s = stats[tid];
const fd = s ? s.lastFd : -1;
const r = ret.toInt32();
if (s) s.lastRet = r;
if (r < 0) console.log(`[READ-RET] tid=${tid} fd=${fd} ret=${r} (blocked/error)`);
else if (r === 0) console.log(`[READ-EOF] tid=${tid} fd=${fd} ret=0 (EOF/closed)`);
else console.log(`[READ-RET] tid=${tid} fd=${fd} ret=${r}`);
}
});
/* ---- 周期 dump ---- */
function dumpThreads() {
try {
const dir = new File("/proc/self/task", "r");
const entries = dir.list();
dir.close();
let relevant = [];
for (const e of entries) {
let nm = "?";
try { const nf = new File(`/proc/self/task/${e}/comm`, "r"); nm = nf.readString().trim(); nf.close(); } catch (err) {}
const s = stats[e] || null;
const lower = nm.toLowerCase();
if (lower.indexOf("location") >= 0 || lower.indexOf("tally") >= 0 || s) {
let info = `tid=${e} "${nm}"`;
if (s) info += ` tigerReads=${s.reads} lastFd=${s.lastFd} kind=${s.lastFdKind} lastRet=${s.lastRet}`;
relevant.push(info);
}
}
console.log(`[DUMP] tiger-fread threads: ${relevant.length ? relevant.join(" | ") : "(none)"}`);
} catch (e) {
console.log(`[DUMP] failed: ${e}`);
}
}
setInterval(() => {
dumpThreads();
try {
const dir = new File("/proc/self/fd", "r");
const fds = dir.list();
dir.close();
let pipes = [], socks = [];
for (const f of fds) {
const kind = fdKind(parseInt(f, 10));
if (kind.indexOf("PIPE") === 0) pipes.push(f + ":" + kind.split(" ").slice(1).join(" "));
if (kind.indexOf("SOCKET") === 0) socks.push(f + ":" + kind.split(" ").slice(1).join(" "));
}
if (pipes.length) console.log(`[DUMP-FD] pipes: ${pipes.join(" | ")}`);
if (socks.length) console.log(`[DUMP-FD] sockets: ${socks.join(" | ")}`);
} catch (e) { /* ignore */ }
}, 3000);
console.log("[TIGER-FREAD] armed (observe-only)");

View File

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Dump aliyun captcha method signatures from TNG APK."""
import zipfile
import re
import sys
apk = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_1.9.10_base.apk"
targets = [
b"Lcom/aliyun/captcha/Captcha;",
b"Lcom/aliyun/captcha/CaptchaWebViewDialog;",
b"Lcom/aliyun/captcha/Captcha$VerificationCallback;",
b"Lcom/aliyun/captcha/CaptchaWebViewDialog$CaptchaCompletionListener;",
b"Lcom/aliyun/captcha/a;",
b"Lcom/aliyun/captcha/b;",
b"Lcom/aliyun/captcha/c;",
]
# Method refs: Lclass;->name(args)ret
pat = re.compile(
rb"(Lcom/aliyun/captcha/[A-Za-z0-9_/$]+;)->([A-Za-z0-9_<>$]+)\(([^)]*)\)([A-Za-z0-9_/;$[\]-]+)"
)
found = {t.decode(): set() for t in targets}
with zipfile.ZipFile(apk) as z:
for n in z.namelist():
if not n.endswith(".dex"):
continue
data = z.read(n)
for m in pat.finditer(data):
clazz = m.group(1).decode()
if clazz not in found:
continue
name = m.group(2).decode()
args = m.group(3).decode()
ret = m.group(4).decode()
found[clazz].add(f"{name}({args}){ret}")
for clazz, methods in found.items():
print("====", clazz, "n=", len(methods))
for s in sorted(methods):
print(" ", s)

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,81 @@
# -*- coding: utf-8 -*-
"""Scan TNG APK for CallingCode / country UI / HW-related classes."""
import re
import sys
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_base_scan.apk"
NEEDLES = [
b"UserSearchCallingCodeActivity",
b"CallingCode",
b"ll_country",
b"BottomSelect",
b"BottomSelectDialogFragment",
b"ftv_title",
b"i7.l",
b"enableHardwareAcceleration",
b"FLAG_HARDWARE_ACCELERATED",
]
def main():
z = zipfile.ZipFile(APK)
print("=== DEX STRING HITS ===")
for name in sorted(z.namelist()):
if not name.endswith(".dex"):
continue
data = z.read(name)
hits = [s.decode() for s in NEEDLES if s in data]
if hits:
print("%s -> %s" % (name, hits))
print("\n=== CLASS NAMES (CallingCode / BottomSelect / country) ===")
pat = re.compile(
rb"L[a-zA-Z0-9_$/]*(?:CallingCode|BottomSelect|Country|country)[a-zA-Z0-9_$/]*;"
)
found = set()
for name in sorted(z.namelist()):
if not name.endswith(".dex"):
continue
data = z.read(name)
for m in pat.findall(data):
found.add(m.decode("ascii", "ignore")[1:-1].replace("/", "."))
for c in sorted(found):
print(" ", c)
print("\n=== CONTEXT AROUND UserSearchCallingCodeActivity ===")
target = b"UserSearchCallingCodeActivity"
for name in sorted(z.namelist()):
if not name.endswith(".dex"):
continue
data = z.read(name)
start = 0
n = 0
while True:
idx = data.find(target, start)
if idx < 0:
break
s = max(0, idx - 80)
e = min(len(data), idx + len(target) + 120)
chunk = re.sub(rb"[^\x20-\x7e]+", b".", data[s:e])
print("[%s @%d] %s" % (name, idx, chunk.decode("ascii", "ignore")))
start = idx + 1
n += 1
if n >= 8:
break
# Manifest component
print("\n=== ANDROIDMANIFEST snippets ===")
try:
# binary manifest — just search utf16/utf8 remnants in apk
data = z.read("AndroidManifest.xml")
for key in (b"CallingCode", b"hardwareAccelerated", b"user.view"):
if key in data or key.decode().encode("utf-16le") in data:
print(" manifest contains", key)
except KeyError:
pass
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""Deeper scan: UserSearchCallingCodeActivity methods / Compose / launch."""
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_base_scan.apk"
def strings_near(data, needle, radius=200, limit=15):
out = []
start = 0
while len(out) < limit:
idx = data.find(needle, start)
if idx < 0:
break
s = max(0, idx - radius)
e = min(len(data), idx + len(needle) + radius)
chunk = re.sub(rb"[^\x20-\x7e]+", b".", data[s:e])
out.append(chunk.decode("ascii", "ignore"))
start = idx + 1
return out
def main():
z = zipfile.ZipFile(APK)
# Collect interesting strings from classes that have CallingCode
keys = [
b"CallingListScreen",
b"setContent",
b"ComposeView",
b"AbstractComposeView",
b"ComponentActivity",
b"getCallingCodeList",
b"CountryListRepository",
b"startActivity",
b"UserSearchCallingCodeActivity",
b"ll_country",
b"hardwareAccelerated",
b"RecyclerView",
b"LazyColumn",
b"androidx/compose",
]
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
data = z.read(dex)
if b"UserSearchCallingCodeActivity" not in data and b"CallingListScreen" not in data and b"ll_country" not in data:
continue
print("\n========", dex, "========")
for k in keys:
if k in data:
print("HAS", k.decode())
if b"CallingListScreen" in data:
print("--- CallingListScreen ctx ---")
for c in strings_near(data, b"CallingListScreen", 120, 6):
print(" ", c[:240])
if b"ll_country" in data:
print("--- ll_country ctx ---")
for c in strings_near(data, b"ll_country", 100, 8):
print(" ", c[:240])
# Who references UserSearchCallingCodeActivity (launchers)
print("\n=== who references UserSearchCallingCodeActivity class desc ===")
desc = b"Lmy/com/tngdigital/user/view/UserSearchCallingCodeActivity;"
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
data = z.read(dex)
count = data.count(desc)
if count:
print(dex, "count=", count)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,16 @@
import re
import zipfile
apk = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_1.9.10_base.apk"
z = zipfile.ZipFile(apk)
pat = re.compile(rb"Lcom/aliyun/captcha/[A-Za-z0-9_/$]+;")
found = set()
for n in z.namelist():
if not n.endswith(".dex"):
continue
data = z.read(n)
for m in pat.findall(data):
found.add(m.decode())
for c in sorted(found):
print(c)
print("total", len(found))

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""Compare CallingCode vs CountrySelect activities / intent extras."""
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_base_scan.apk"
def near(data, needle, r=180, lim=12):
out = []
start = 0
while len(out) < lim:
i = data.find(needle, start)
if i < 0:
break
chunk = re.sub(rb"[^\x20-\x7e]+", b".", data[max(0, i - r): i + len(needle) + r])
out.append(chunk.decode("ascii", "ignore"))
start = i + 1
return out
def main():
z = zipfile.ZipFile(APK)
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
data = z.read(dex)
if b"UserCountrySelectActivity" not in data and b"UserSearchCallingCodeActivity" not in data:
continue
print("\n====", dex, "====")
for n in (b"UserCountrySelectActivity", b"AbsCountrySelectActivity",
b"newIntent", b"CallingListScreen"):
if n in data:
print("HAS", n.decode())
for n in (b"UserSearchCallingCodeActivity", b"UserCountrySelectActivity"):
if n not in data:
continue
print("--", n.decode(), "--")
for c in near(data, n, 100, 6):
if "Hilt_" in c and ".java" in c:
continue
print(" ", c[:220])
print("\n=== calling/country intent-like strings ===")
seen = set()
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
data = z.read(dex)
if b"CallingCode" not in data and b"CountrySelect" not in data:
continue
for m in re.findall(
rb"(?:EXTRA_|KEY_|arg_|ARG_)[A-Za-z0-9_]{2,40}|"
rb"[A-Za-z0-9_]{0,15}(?:calling_code|CallingCode|country_code|CountryCode|countryList)[A-Za-z0-9_]{0,20}",
data):
s = m.decode("ascii", "ignore")
if s not in seen and len(s) > 6:
seen.add(s)
print(" ", s)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,41 @@
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")]
# Find packages that look like Promon (short random package + few classes)
# Also search string markers
markers = [
b"promon",
b"Promon",
b"PROMON",
b"xwwqazamx",
b"Rooting",
b"jailbroken",
b"JailBroken",
b"W:16",
b"libtngdigital_ewallet",
]
for m in markers:
hits = 0
for n in names:
hits += z.read(n).count(m)
print(f"marker {m!r}: {hits}")
# Extract L.../...; type descriptors that contain 'promon' case-insensitive or weird short pkgs
pkg_re = re.compile(rb"L([a-z]{6,12})/([A-Za-z0-9_$]{1,20});")
pkg_counts = {}
for n in names:
data = z.read(n)
for m in pkg_re.findall(data):
pkg = m[0].decode("ascii", errors="ignore")
pkg_counts[pkg] = pkg_counts.get(pkg, 0) + 1
# Show rare short packages (likely obfuscated)
cands = [(p, c) for p, c in pkg_counts.items() if 5 <= c <= 500 and p.isalpha() and len(p) <= 12]
cands.sort(key=lambda x: -x[1])
print("\ncandidate obfuscated packages:")
for p, c in cands[:40]:
print(f" {p}: {c}")

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "tng" / "base.apk"
if not APK.exists():
APK = Path(__file__).resolve().parent.parent / "apks" / "tng" / "tng.apk"
acts = set()
ops = set()
xww = set()
tng = set()
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith(".dex"):
continue
data = zf.read(name)
for m in re.finditer(rb"my/com/tngdigital/[a-zA-Z0-9_/]+Activity", data):
acts.add(m.group().decode().replace("/", "."))
for m in re.finditer(rb"com\.(?:abl|tngd|zoloz|alipayplus)\.[a-z0-9.]+", data):
s = m.group().decode("ascii", "ignore")
if any(k in s for k in ("wallet", "otp", "login", "register", "phone", "member", "jail", "customer", "pin", "mobile")):
ops.add(s)
for m in re.finditer(rb"xwwqazamx/[a-zA-Z0-9_]+", data):
xww.add(m.group().decode().replace("/", "."))
print("=== User flow Activities ===")
for a in sorted(acts):
if any(k in a for k in ("User", "Splash", "Guide", "Registration", "Login", "Otp", "Pin", "WebView", "Security")):
print(a)
print("\n=== Promon xwwqazamx classes (sample) ===")
for c in sorted(xww)[:40]:
print(c)
print(f"... total {len(xww)}")
print("\n=== RPC operationTypes (sample) ===")
for o in sorted(ops)[:50]:
print(o)

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,68 @@
#!/usr/bin/env python3
import json
import time
import urllib.parse
import urllib.request
BASE = "http://127.0.0.1:9090"
def wait_api(retries=15):
for _ in range(retries):
try:
with urllib.request.urlopen(BASE + "/proxies", timeout=3) as r:
return json.loads(r.read())
except Exception:
time.sleep(1)
raise SystemExit("clash api not ready")
def put_proxy(group: str, target: str):
enc_g = urllib.parse.quote(group, safe="")
req = urllib.request.Request(
BASE + "/proxies/" + enc_g,
data=json.dumps({"name": target}).encode(),
method="PUT",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=5) as r:
print("switched", group, "->", target, "status", r.status)
def main():
data = wait_api()
proxies = data.get("proxies", {})
group = None
for gname in ["🚀节点选择", "GLOBAL"]:
if gname in proxies:
group = gname
break
if not group:
raise SystemExit("selector group not found")
print("group=", group, "now=", proxies.get(group, {}).get("now"))
candidates = [
"🇸🇬狮城节点",
"🇸🇬AWS新加坡01 | 电信移动联通推荐",
"🇸🇬新加坡01 | 电信联通推荐",
"🇸🇬新加坡 | 高速专线-hy2",
]
target = next((c for c in candidates if c in proxies), None)
if not target:
for k in proxies:
if "新加坡" in k or "AWS新加坡" in k:
target = k
break
if not target:
raise SystemExit("no SG proxy found")
put_proxy(group, target)
enc_g = urllib.parse.quote(group, safe="")
with urllib.request.urlopen(BASE + "/proxies/" + enc_g, timeout=3) as r:
print("verify now=", json.loads(r.read()).get("now"))
if __name__ == "__main__":
main()

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,114 @@
#!/usr/bin/env python3
"""Download TNG eWallet XAPK from Uptodown eAPI (arm64-v8a)."""
from __future__ import annotations
import json
import re
import subprocess
import sys
import urllib.request
import zipfile
from pathlib import Path
UA = "Mozilla/5.0 (Linux; Android 13) Chrome/120.0.0.0 Mobile Safari/537.36"
APP_CODE = "1000382462"
VERSION = "1.9.10"
ARCH = "arm64-v8a, armeabi-v7a, x86_64"
BASE = "https://touch-n-go-ewallet.en.uptodown.com"
OUT_XAPK = Path("reverse/dumps/tng_1.9.10.xapk")
OUT_DIR = Path("reverse/dumps/tng_xapk_extracted")
def fetch(url: str) -> bytes:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=120) as resp:
return resp.read()
def fetch_text(url: str) -> str:
return fetch(url).decode("utf-8", "ignore")
def main() -> int:
versions = json.loads(fetch_text(f"{BASE}/android/apps/{APP_CODE}/versions/1"))
entry = next(x for x in versions["data"] if x["version"] == VERSION)
version_id = entry["versionURL"]["versionID"]
print(f"version {VERSION} fileID={entry['fileID']} kind={entry['kindFile']}")
dl_page = fetch_text(f"{BASE}/android/download/{version_id}")
m = re.search(r'class="button variants" data-version="(\d+)"', dl_page)
if not m:
print("variants data-version not found", file=sys.stderr)
return 1
data_version = m.group(1)
print("data_version", data_version)
files_json = json.loads(fetch_text(f"{BASE}/app/{APP_CODE}/version/{data_version}/files"))
content = files_json.get("content", "")
# parse variant rows from HTML fragment
rows = re.findall(
r'class="variant".*?data-file-id="(\d+)".*?<span>([^<]+)</span>',
content,
flags=re.S,
)
if not rows:
# fallback: any data-file-id near xapk
rows = re.findall(r'data-file-id="(\d+)"', content)
rows = [(rid, "?") for rid in rows]
print("variants", rows)
target_file_id = None
for fid, arch in rows:
if ARCH in arch or "arm64-v8a" in arch:
target_file_id = fid
print("pick", fid, arch)
break
if not target_file_id and rows:
target_file_id = rows[0][0]
print("fallback file_id", target_file_id)
if not target_file_id:
print("no file id", file=sys.stderr)
return 1
variant_page = fetch_text(f"{BASE}/android/download/{target_file_id}-x")
token_m = re.search(r'id="detail-download-button"[^>]*data-url="(-[^"]+)"', variant_page)
if not token_m:
token_m = re.search(r'data-url="(-[^"]+)"', variant_page)
if not token_m:
print("download token not found", file=sys.stderr)
return 1
token = token_m.group(1)
print("downloading XAPK...")
data = fetch(f"https://dw.uptodown.com/dwn/{token}")
OUT_XAPK.write_bytes(data)
print("saved", OUT_XAPK, "bytes", len(data))
with zipfile.ZipFile(OUT_XAPK) as z:
apks = [n for n in z.namelist() if n.endswith(".apk")]
print("apk splits", apks)
if not apks:
print("no apk inside xapk", file=sys.stderr)
return 1
if OUT_DIR.exists():
import shutil
shutil.rmtree(OUT_DIR)
OUT_DIR.mkdir(parents=True)
import zipfile as zf
with zf.ZipFile(OUT_XAPK) as z:
z.extractall(OUT_DIR)
apk_files = sorted(OUT_DIR.rglob("*.apk"))
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
cmd = [adb, "install-multiple", "-r"] + [str(p) for p in apk_files]
print("install:", " ".join(cmd))
r = subprocess.run(cmd, capture_output=True, text=True)
print(r.stdout)
print(r.stderr)
return 0 if r.returncode == 0 else r.returncode
if __name__ == "__main__":
raise SystemExit(main())

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,110 @@
"""Minimal DEX parser: dump methods for target classes."""
import struct
import zipfile
from pathlib import Path
TARGET = {
"Lcom/tngd/networksdk/common/NativeLib;",
"Lcom/tngd/networksdk/common/ApiSixSecretKeys;",
"Lmy/com/tngdigital/common/internal/libs/RetrieveFromNativeLibs;",
}
def uleb(data, i):
result = 0
shift = 0
while True:
b = data[i]
i += 1
result |= (b & 0x7F) << shift
if (b & 0x80) == 0:
break
shift += 7
return result, i
def parse_dex(data: bytes, label: str):
if data[:4] != b"dex\n":
return
string_ids_size, string_ids_off = struct.unpack_from("<II", data, 56)
type_ids_size, type_ids_off = struct.unpack_from("<II", data, 64)
proto_ids_size, proto_ids_off = struct.unpack_from("<II", data, 72)
field_ids_size, field_ids_off = struct.unpack_from("<II", data, 80)
method_ids_size, method_ids_off = struct.unpack_from("<II", data, 88)
class_defs_size, class_defs_off = struct.unpack_from("<II", data, 96)
def string_at(idx):
off = struct.unpack_from("<I", data, string_ids_off + idx * 4)[0]
size, p = uleb(data, off)
return data[p : p + size].decode("utf-8", "replace")
def type_at(idx):
return string_at(struct.unpack_from("<I", data, type_ids_off + idx * 4)[0])
def proto_at(idx):
shorty_idx, return_type_idx, parameters_off = struct.unpack_from(
"<III", data, proto_ids_off + idx * 12
)
ret = type_at(return_type_idx)
params = []
if parameters_off:
size = struct.unpack_from("<I", data, parameters_off)[0]
for i in range(size):
tidx = struct.unpack_from("<H", data, parameters_off + 4 + i * 2)[0]
params.append(type_at(tidx))
return ret, params
def method_at(idx):
class_idx, proto_idx, name_idx = struct.unpack_from(
"<HHI", data, method_ids_off + idx * 8
)
ret, params = proto_at(proto_idx)
return type_at(class_idx), string_at(name_idx), ret, params
print(f"\n===== {label} =====")
for c in range(class_defs_size):
class_idx, access_flags, superclass_idx, interfaces_off, source_file_idx, annotations_off, class_data_off, static_values_off = struct.unpack_from(
"<IIIIIIII", data, class_defs_off + c * 32
)
cname = type_at(class_idx)
if cname not in TARGET:
continue
print(f"\nCLASS {cname} access=0x{access_flags:x}")
if not class_data_off:
print(" (no class_data)")
continue
p = class_data_off
static_fields_size, p = uleb(data, p)
instance_fields_size, p = uleb(data, p)
direct_methods_size, p = uleb(data, p)
virtual_methods_size, p = uleb(data, p)
# skip fields
for _ in range(static_fields_size + instance_fields_size):
_, p = uleb(data, p)
_, p = uleb(data, p)
mid = 0
for kind, count in (("direct", direct_methods_size), ("virtual", virtual_methods_size)):
mid = 0
for _ in range(count):
diff, p = uleb(data, p)
access, p = uleb(data, p)
code_off, p = uleb(data, p)
mid += diff
cls, name, ret, params = method_at(mid)
flags = []
if access & 0x100:
flags.append("native")
if access & 0x8:
flags.append("static")
if access & 0x10000:
flags.append("constructor")
print(f" [{kind}] {' '.join(flags)} {name}({', '.join(params)}){ret} code=0x{code_off:x}")
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
with zipfile.ZipFile(apk) as z:
for n in z.namelist():
if n.endswith(".dex"):
data = z.read(n)
if any(t.encode() in data for t in ("NativeLib;", "ApiSixSecretKeys;", "RetrieveFromNativeLibs;")):
parse_dex(data, n)

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Race-window: dump TNG executable maps and hunt kill+SVC after launch."""
import re
import subprocess
import sys
import time
from pathlib import Path
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
PKG = "my.com.tngdigital.ewallet"
OUT = Path(__file__).resolve().parents[1] / "dumps" / "tng_runtime"
OUT.mkdir(parents=True, exist_ok=True)
# aarch64 movz x8/w8,#129/130/131 + svc #0
KILL_IMMS = [
bytes.fromhex("281080d2"),
bytes.fromhex("28108052"),
bytes.fromhex("481080d2"),
bytes.fromhex("48108052"),
bytes.fromhex("681080d2"),
bytes.fromhex("68108052"),
]
SVC0 = bytes.fromhex("010000d4")
EXIT_IMM = bytes.fromhex("c80b80d2") # movz x8,#94 exit_group
EXIT2 = bytes.fromhex("ba0b80d2") # movz x8,#93 exit
def adb(*args, check=False):
r = subprocess.run([ADB, *args], capture_output=True)
out = (r.stdout or b"") + (r.stderr or b"")
if check and r.returncode != 0:
raise RuntimeError(out.decode("utf-8", "ignore"))
return r.returncode, out.decode("utf-8", "replace")
def su(cmd):
return adb("shell", f"su -c '{cmd}'")
def main():
adb("shell", "am", "force-stop", PKG)
time.sleep(0.5)
adb("shell", "monkey", "-p", PKG, "-c", "android.intent.category.LAUNCHER", "1")
pid = None
for _ in range(40):
time.sleep(0.25)
_, out = adb("shell", "pidof", PKG)
toks = out.strip().split()
if toks:
pid = toks[0]
break
if not pid:
print("FAIL: no pid")
return 1
print(f"pid={pid}")
# pull maps
_, maps = su(f"cat /proc/{pid}/maps")
maps_path = OUT / f"maps_{pid}.txt"
maps_path.write_text(maps, encoding="utf-8", errors="replace")
print(f"maps -> {maps_path} lines={len(maps.splitlines())}")
targets = []
for line in maps.splitlines():
if "r-xp" not in line and "r-x" not in line:
# also rw-p with execute rarely; keep x
if "x" not in line.split()[1] if len(line.split()) > 1 else "":
continue
if "tngdigital" in line or "libnative" in line or "ewallet" in line.lower():
parts = line.split()
rng = parts[0]
start_s, end_s = rng.split("-")
start, end = int(start_s, 16), int(end_s, 16)
path = parts[-1] if len(parts) >= 6 else ""
targets.append((start, end, path, line))
print(f"target segments={len(targets)}")
for start, end, path, line in targets[:12]:
print(f" {hex(start)}-{hex(end)} {path}")
# dump via dd from /proc/pid/mem
remote = f"/data/local/tmp/tng_rt_{pid}.bin"
su(f"rm -f {remote}")
total = 0
for i, (start, end, path, _) in enumerate(targets):
size = end - start
if size <= 0 or size > 32 * 1024 * 1024:
continue
# append dump
cmd = (
f"dd if=/proc/{pid}/mem bs=4096 skip={start // 4096} "
f"count={(size + 4095) // 4096} 2>/dev/null >> {remote}"
)
# dd skip is in blocks from file start — wrong for /proc/pid/mem!
# Use busybox dd with seek on output and skip via python on device instead.
cmd = (
f"toybox dd if=/proc/{pid}/mem of={remote}.p{i} "
f"bs=1 skip={start} count={size} 2>/dev/null"
)
code, _ = su(cmd)
if code == 0:
total += size
print(f" dumped p{i} size={size} from {path}")
else:
# fallback: python on device
py = (
f"python3 -c \"import sys;f=open('/proc/{pid}/mem','rb');"
f"f.seek({start});d=f.read({size});open('{remote}.p{i}','wb').write(d)\""
)
code2, out2 = su(py)
if code2 == 0:
total += size
print(f" dumped p{i} via python size={size}")
else:
print(f" FAIL dump p{i}: {out2[:120]}")
# pull pieces and scan
local_dir = OUT / f"mem_{pid}"
local_dir.mkdir(exist_ok=True)
kill_hits = 0
exit_hits = 0
for i, (start, end, path, _) in enumerate(targets):
rem = f"{remote}.p{i}"
loc = local_dir / f"seg_{i}_{start:x}.bin"
code, _ = adb("shell", f"su -c 'test -f {rem} && echo OK'")
if "OK" not in _:
continue
adb("pull", rem, str(loc))
if not loc.exists():
continue
data = loc.read_bytes()
for imm in KILL_IMMS:
pos = 0
while True:
j = data.find(imm, pos)
if j < 0:
break
win = data[j : j + 36]
if SVC0 in win:
kill_hits += 1
delta = win.find(SVC0)
print(f"KILL+SVC seg{i} file+0x{j:x} va=0x{start+j:x} delta={delta} path={path}")
pos = j + 4
for imm in (EXIT_IMM, EXIT2):
pos = 0
while True:
j = data.find(imm, pos)
if j < 0:
break
win = data[j : j + 36]
if SVC0 in win:
exit_hits += 1
if exit_hits <= 15:
print(f"EXIT+SVC seg{i} file+0x{j:x} va=0x{start+j:x} path={path}")
pos = j + 4
# also count raw svc
print(f" seg{i} svc0={data.count(SVC0)} size={len(data)}")
print(f"DONE kill+svc={kill_hits} exit+svc={exit_hits} dumped_bytes~={total}")
_, alive = adb("shell", "pidof", PKG)
print(f"still alive? {alive.strip() or 'NO'}")
return 0
if __name__ == "__main__":
sys.exit(main())

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,22 @@
# -*- 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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for m in re.finditer(rb"Lcom/[^;]{0,160}CharacterCrypto[^;]{0,40};", d):
print(m.group().decode()[1:-1].replace("/", "."))
for m in re.finditer(rb"Lcom/[^;]{0,160}NativeEncrypt[^;]{0,40};", d):
print(m.group().decode()[1:-1].replace("/", "."))
# find class with getDfpByMMKV - search all Lcom paths and check if followed by getDfp in same method table is hard
# instead search for MMKV + dfp strings proximity
idx = d.find(b"getDfpByMMKV:")
if idx >= 0:
chunk = d[max(0, idx - 500) : idx + 500]
for m in re.finditer(rb"Lcom/[^;\x00]{5,200};", chunk):
print("near getDfpByMMKV:", m.group().decode()[1:-1].replace("/", "."))

View File

@@ -0,0 +1,20 @@
# -*- 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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for m in re.finditer(rb"CharacterCrypto[\w$]{0,40}", d):
print(m.group().decode())
for m in re.finditer(rb"getDfp[\w$]{0,20}", d):
s = m.group().decode()
if s not in ("getDfp",):
print("method:", s)
# utils.d wrapper
for m in re.finditer(rb"Lcom/shopee/bke/lib/jni/utils/[\w$]{1,20};", d):
print(m.group().decode()[1:-1].replace("/", "."))

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,21 @@
# -*- 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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for needle in [b"CharacterCryptoManager", b"CharacterCryptoManagerWrapper", b"NativeEncryptUtilsWrapper"]:
print("\n===", needle.decode(), "===")
for m in re.finditer(re.escape(needle) + rb"[\w$]{0,30}", d):
name = m.group().decode()
if "$" in name or name.endswith("Wrapper") or name.endswith("Manager"):
pass
for m in re.finditer(rb"Lcom/[^;]{0,200}" + re.escape(needle) + rb"[^;]{0,20};", d):
print(m.group().decode()[1:-1].replace("/", "."))
# also search for class ending with .d
for m in re.finditer(rb"Lcom/shopee/bke/lib/jni/utils/[a-z];", d):
print("short utils:", m.group().decode()[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,23 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
needle = b"The dfp is empty in register scene"
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)
ctx = data[max(0, idx - 2000) : idx + 2000]
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/[^;\x00]{5,200};", ctx)))
print("classes near register dfp empty:")
for c in classes:
print(c)
print("\nstrings:")
for m in re.finditer(rb"[\x20-\x7e]{6,100}", ctx):
s = m.group().decode()
if any(k in s.lower() for k in ("dfp", "register", "fingerprint", "empty", "scene", "monitor", "iv_")):
print(s)

View File

@@ -0,0 +1,27 @@
# -*- 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 needle in [b"getDfp", b"DeviceFingerprintResp", b"deviceFingerprint", b"/dfp/", b"dfp/v1"]:
print("\n===", needle.decode(), "===")
for m in re.finditer(re.escape(needle) + rb"[\x00-\xff]{0,80}", data):
chunk = data[m.start() : m.start() + 120]
s = re.sub(rb"[^\x20-\x7e]+", b"|", chunk).decode("ascii", "ignore")
print(s[:140])
break
# classes with fingerprint in name
fps = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/shopee/bke/[^;\x00]{0,120}[Ff]ingerprint[^;\x00]{0,40};", data)))
print("\n=== fingerprint classes ===")
for c in fps[:30]:
print(c)
# RegisterViewModel methods - search string RegisterViewModel in dex
for m in re.finditer(rb"Lcom/shopee/bke/biz/user/viewmodel/RegisterViewModel[^;\x00]*;", data):
print("\nRegisterViewModel:", m.group().decode())

View File

@@ -0,0 +1,19 @@
# -*- 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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
idx = d.find(b"getDfpByMMKV")
print("idx", idx)
window = d[max(0, idx - 20000) : idx + 20000]
classes = re.findall(rb"Lcom/[a-zA-Z0-9_$/]{5,200};", window)
unique = sorted(set(x.decode()[1:-1].replace("/", ".") for x in classes))
print("classes in 40k window:", len(unique))
for c in unique:
cl = c.lower()
if any(k in cl for k in ("crypto", "dfp", "finger", "device", "user", "jni", "utils", "manager", "wrapper", "register", "login")):
print(c)

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
needles = [b"getDfp empty!", b"getDfp onError:", b"getDfpByMMKV:", b"The dfp is empty in register scene"]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
all_classes = [m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/shopee/bke/[\w$/]{5,160};", data)]
for needle in needles:
print("\n===", needle.decode(), "===")
idx = data.find(needle)
if idx < 0:
print("not found")
continue
window = data[max(0, idx - 8000) : idx + 8000]
nearby = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/shopee/bke/[\w$/]{5,160};", window)))
for c in nearby:
if any(k in c.lower() for k in ("dfp", "finger", "device", "register", "user", "util", "manager", "helper", "repo", "data", "rn")):
print(" ", c)
dfp_classes = sorted(set(c for c in all_classes if "dfp" in c.lower() or "fingerprint" in c.lower()))
print("\n=== dfp/fingerprint class names ===")
for c in dfp_classes:
print(c)

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
needle = b"getDfp empty!"
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)
window = data[max(0, idx - 12000) : idx + 12000]
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/[^;\x00]{5,200};", window)))
print("all classes near getDfp empty (filtered):")
for c in classes:
cl = c.lower()
if any(k in cl for k in ("dfp", "finger", "shps", "bke", "jni", "utils", "sdk", "device", "monitor", "crypto", "register")):
print(c)
print("\nall com classes count:", len(classes))

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
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
with zipfile.ZipFile(str(APK)) as zf:
for name in zf.namelist():
if not name.endswith((".json", ".jsbundle")):
continue
data = zf.read(name)
if b"currently unavailable" not in data and b"system is currently" not in data:
continue
text = data.decode("utf-8", errors="replace")
for m in re.finditer(r".{0,40}currently unavailable.{0,60}", text):
print(f"\n[{name}]")
print(m.group().replace("\n", " ")[:200])

View File

@@ -0,0 +1,24 @@
# -*- 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:
for name in sorted(zf.namelist()):
low = name.lower()
if not name.endswith(".json"):
continue
if "user" not in low and "auth" not in low and "register" not in low and "ekyc" not in low:
continue
data = zf.read(name).decode("utf-8", errors="replace")
hits = []
for m in re.finditer(r'"[^"]+"\s*:\s*"[^"]{8,200}"', data):
s = m.group()
sl = s.lower()
if any(k in sl for k in ("unavailable", "register", "dfp", "phone", "otp", "system is")):
hits.append(s[:220])
if hits:
print("\n===", name, "===")
for h in hits[:40]:
print(h)

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,13 @@
#!/usr/bin/env python3
import sqlite3
import sys
p = sys.argv[1] if len(sys.argv) > 1 else "clash_cache.db"
con = sqlite3.connect(p)
for (name,) in con.execute("SELECT name FROM sqlite_master WHERE type='table'"):
print("TABLE", name)
cols = [c[1] for c in con.execute(f"PRAGMA table_info({name})")]
print(" cols", cols)
for row in con.execute(f"SELECT * FROM {name} LIMIT 8"):
s = str(row)
print(" ", s[:300] + ("..." if len(s) > 300 else ""))

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,18 @@
# -*- 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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/shopee/bke/lib/safemode/[^;]{1,80};", d)))
print("safemode classes:", len(classes))
for c in classes[:30]:
print(c)
print("\nrisk classes:")
for m in re.finditer(rb"Lcom/shopee/bke/biz/base/risk/[^;]{1,40};", d):
print(m.group().decode()[1:-1].replace("/", "."))

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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
patterns = [
rb"Lcom/shopee/bke/lib/safemode/[a-zA-Z$][\w$]{0,30};",
rb"Lcom/shopee/bke/lib/safemode/[a-z]+/[a-zA-Z$][\w$]{0,40};",
]
seen = set()
for pat in patterns:
for m in re.finditer(pat, d):
c = m.group().decode()[1:-1].replace("/", ".")
if c.startswith("com.shopee.bke.lib.safemode.R"):
continue
if c not in seen:
seen.add(c)
print(c)
print("\n--- short obfuscated bke classes (root/adb) ---")
for m in re.finditer(rb"Lcom/shopee/bke/[a-z]+/[a-z]{1,2};", d):
c = m.group().decode()[1:-1].replace("/", ".")
if "lib" in c or "safemode" in c or "risk" in c:
print(c)

View File

@@ -0,0 +1,21 @@
# -*- 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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for pat in [
rb"Lcom/shopee/shpssdk/[\w$/]{3,100};",
rb"Lcom/shopee/shpssdkbank/[\w$/]{3,100};",
]:
cs = sorted(set(m.group().decode()[1:-1].replace("/", ".") for m in re.finditer(pat, d)))
print("\n", pat.decode(), len(cs))
for c in cs:
if "R" != c.split(".")[-1] or "$" in c:
print(" ", c)
for needle in [b"RISK_USB", b"RISK_WIFI", b"RISK_ROOT", b"RISK_HOOK", b"requestDefense"]:
print(needle.decode(), d.count(needle))

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
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])
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for pkg in ("shpssdkbank", "shpssdk"):
pat = re.compile(rf"Lcom/shopee/{pkg}/[\w$]+;".encode())
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".") for m in pat.finditer(data)))
print(f"\n=== {pkg} ({len(classes)} classes) ===")
short = [c for c in classes if len(c.split(".")[-1]) <= 12 and "shpssdk" in c]
for c in short[:40]:
print(" ", c)

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,156 @@
# -*- coding: utf-8 -*-
"""Parse vuwuuwvw attestation JSON from logcat or raw JSON file."""
import base64
import hashlib
import json
import re
import sys
from pathlib import Path
SUSPICIOUS = re.compile(
rb"(root|hook|xposed|lsposed|magisk|frida|substrate|emulator|debug|adb|"
rb"selinux|\bsu\b|/proc/|zygisk|riru|shamiko|tamper|integrity|"
rb"jailbreak|virtual|mock|proxy|vpn|developer)",
re.I,
)
KNOWN_FIELDS = [
"root", "hook", "xposed", "lsposed", "magisk", "frida", "adb", "debug",
"debuggable", "emulator", "simulator", "vpn", "proxy", "mock",
"selinux", "su", "supersu", "zygisk", "riru", "shamiko", "substrate",
"integrity", "safetynet", "playIntegrity", "deviceId", "androidId",
"serial", "fingerprint", "model", "brand", "manufacturer", "board",
"host", "tags", "type", "user", "display", "product", "hardware",
"usb", "wifi", "adb_enabled", "development_settings_enabled",
"RISK_ROOT", "RISK_HOOK", "RISK_USB_ADB", "RISK_WIFI_ADB", "RISK_ADB",
"RISK_EMULATOR", "RISK_DEBUG", "RISK_VPN", "RISK_PROXY", "RISK_MOCK",
"rdVerifyInfo", "deviceFingerprint", "data", "dataKey", "riskToken",
"isRoot", "isHook", "isDebug", "isAdb", "isEmulator", "isVirtual",
"tamper", "jailbreak", "bootloader", "verifiedbootstate", "vbmeta",
"init.svc.adbd", "/proc/self/maps", "RealInterceptorChain",
]
# keys seen in 16:29-16:30 Pixel6 logs (from vuwuuwvw head=...)
SAMPLE_KEYS = """
2535994b 3923d741 68e69650 37132b99 1c5681ce 324f4370 4ea521fa
2236b022 5a5532da 1309e885 1bb219c0 3ade7f65 3ade7f66 1c560a56 1c560a55
3d33c1b1 1854d9b1 21e5cca2 23a20fae 36f30e66 29e2320e 2652ab1c 122c5826
269b494b 22b1f08d 169b85f 1610b055 2baf3770 5bc1a01a 37132b99
""".split()
def md5_key(name: str) -> str:
return hashlib.md5(name.encode()).hexdigest()[:8]
def guess_keys(keys):
table = {md5_key(n): n for n in KNOWN_FIELDS}
out = []
for k in keys:
if k.lower() in table:
out.append((k, table[k.lower()]))
return out
def scan_value(path, val, hits):
if isinstance(val, str):
b = val.encode("utf-8", "replace")
m = SUSPICIOUS.search(b)
if m:
hits.append(f"{path} str hit={m.group().decode()} val={val[:120]}")
if re.fullmatch(r"[A-Za-z0-9+/=]+", val) and 8 <= len(val) <= 512:
try:
raw = base64.b64decode(val + "==="[: (4 - len(val) % 4) % 4])
if sum(32 <= c < 127 for c in raw) * 100 // max(len(raw), 1) >= 85:
inner = raw.decode("utf-8", "replace")
m2 = SUSPICIOUS.search(inner.encode())
if m2:
hits.append(f"{path} b64utf8 hit={m2.group().decode()} val={inner[:120]}")
else:
hx = raw[:32].hex()
hits.append(f"{path} b64 bin len={len(raw)} hex={hx}")
except Exception:
pass
elif isinstance(val, (int, float, bool)):
if val in (1, True):
hits.append(f"{path} ={val} (flag?)")
def parse_json(text, label=""):
obj = json.loads(text)
keys = sorted(obj.keys())
print(f"\n=== {label} keys={len(keys)} ===")
print("first keys:", keys[:12])
hits = []
for k in keys:
scan_value(k, obj[k], hits)
if hits:
print("SUSPICIOUS:")
for h in hits[:30]:
print(" ", h)
else:
print("no plain suspicious strings")
matched = guess_keys(keys)
if matched:
print("MD5 key guesses:")
for k, n in matched:
print(f" {k} => {n}")
return obj
def extract_from_log(path):
text = Path(path).read_text(encoding="utf-8", errors="replace")
# MariBankCapture chunked: [vuwuuwvw.out REGISTER] 1/N ...
chunks = {}
current = None
for line in text.splitlines():
if "vuwuuwvw.out REGISTER" in line or "vuwuuwvw.out]" in line:
m = re.search(r"\] (\d+)/(\d+) (.+)$", line)
if m:
idx, total, part = int(m.group(1)), int(m.group(2)), m.group(3)
key = (total, line.split("REGISTER")[0])
chunks.setdefault(key, {})[idx] = part
elif " len=" in line and " parts=" not in line:
m2 = re.search(r"\] len=\d+ (.+)$", line)
if m2:
current = m2.group(1)
elif "vuwuuwvw.out REGISTER] len=" in line and " parts=" not in line:
m2 = re.search(r"len=\d+ (.+)$", line)
if m2:
current = m2.group(1)
if current and current.startswith("{"):
return [current]
out = []
for parts in chunks.values():
if parts:
joined = "".join(parts[i] for i in sorted(parts))
if joined.startswith("{"):
out.append(joined)
# fallback: head= lines won't work for full JSON
return out
def main():
print("=== MD5 key table (known fields -> 8 hex) ===")
for name in KNOWN_FIELDS[:20]:
print(f" {md5_key(name):8s} {name}")
print(" ...")
print("\n=== sample keys from device logs ===")
matched = guess_keys(SAMPLE_KEYS)
if matched:
for k, n in matched:
print(f" {k} => {n}")
else:
print(" (no MD5 match — keys may use different hash algo)")
if len(sys.argv) > 1:
p = Path(sys.argv[1])
if p.suffix == ".json":
parse_json(p.read_text(encoding="utf-8"), p.name)
else:
for i, blob in enumerate(extract_from_log(p)):
parse_json(blob, f"log#{i+1}")
if __name__ == "__main__":
main()

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,28 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
needle = b"dfp is empty"
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 - 400) : idx + 400]
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/[^;\x00]{5,160};", ctx)))
print("\n--- hit at", idx, "---")
for c in classes:
if any(k in c.lower() for k in ("user", "register", "dfp", "fingerprint", "viewmodel", "rn", "helper")):
print(" ", c)
# printable strings nearby
for m in re.finditer(rb"[\x20-\x7e]{4,80}", ctx):
s = m.group().decode()
if any(k in s.lower() for k in ("dfp", "register", "empty", "error", "unavailable", "fingerprint")):
print(" str:", s)
idx += len(needle)

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,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:
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for kw in [
b"CharacterCrypto",
b"IV_Monitor",
b"getDfpByMMKV",
b"NativeEncryptUtilsWrapper",
b"dfp/v1/data/report",
b"com/shopee/bke/lib/jni/utils/d",
]:
print(kw.decode(), d.count(kw))
print("\ndfp-related classes:")
for m in re.finditer(rb"Lcom/[^;]{0,120}[Dd][Ff][Pp][^;]{0,40};", d):
print(m.group().decode()[1:-1].replace("/", "."))
print("\nMonitor classes:")
for m in re.finditer(rb"Lcom/[^;]{0,120}Monitor[^;]{0,40};", d):
s = m.group().decode()[1:-1].replace("/", ".")
if "bke" in s or "shps" in s:
print(s)

View File

@@ -0,0 +1,20 @@
# -*- 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 needle in [b"dfp is empty", b"getDfp", b"DfpManager", b"DeviceFingerprint"]:
print("\n===", needle.decode(), "count=", data.count(needle))
idx = 0
for _ in range(5):
idx = data.find(needle, idx)
if idx < 0:
break
ctx = data[max(0, idx - 120) : idx + 200]
for m in re.finditer(rb"Lcom/[^;\x00]{5,140};", ctx):
print(" ", m.group().decode()[1:-1].replace("/", "."))
idx += len(needle)

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""Find jni/utils and safemode classes across SG split dex files."""
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:
for dex in sorted(n for n in zf.namelist() if n.endswith(".dex")):
d = zf.read(dex)
needles = [
b"Lcom/shopee/bke/lib/jni/utils/",
b"Lcom/shopee/bke/lib/safemode/",
b"CharacterCrypto",
b"rdVerifyInfo",
]
if not any(n in d for n in needles):
continue
print("\n===", dex, "===")
for pat in [
rb"Lcom/shopee/bke/lib/jni/utils/[^;]{1,40};",
rb"Lcom/shopee/bke/lib/safemode/[^;]{1,60};",
rb"Lcom/shopee/bke/biz/base/risk/[^;]{1,40};",
]:
cs = sorted(
set(
m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(pat, d)
)
)
for c in cs:
if ".R" in c and c.endswith(".R"):
continue
if "$" in c or not c.endswith(".R"):
print(" ", c)

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import re
import zipfile
from pathlib import Path
APKS = Path(__file__).resolve().parent.parent / "apks"
for name in ["maribank_sg_base.apk", "maribank_sg_arm64.apk"]:
p = APKS / name
if not p.exists():
print(name, "missing")
continue
with zipfile.ZipFile(p) as z:
dex = [n for n in z.namelist() if n.endswith(".dex")]
print("\n", name, "dex:", dex)
if not dex:
so = [n for n in z.namelist() if n.endswith(".so")][:5]
print(" native:", so)
continue
d = b"".join(z.read(n) for n in dex)
for needle in [
b"safemode.b",
b"safemode/catchs",
b"safemode/util",
b"USB_ADB",
b"RISK_USB",
b"lib/safemode/",
]:
print(" ", needle.decode(), d.count(needle))
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
for m in re.finditer(rb"Lcom/shopee/bke/lib/safemode/[^;]{1,80};", d)))
for c in classes:
if not c.endswith(".R") and ".R$" not in c:
print(" ", c)

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
needles = [
b"system is currently unavailable",
b"currently unavailable",
b"Unexpected error occurred",
b"Please try again later",
b"3100012",
b"deviceFingerprint",
b"preCheck",
b"preRegister",
b"getDfp",
b"dfp is empty",
b"dfpReady",
b"isDfpReady",
b"IV_Monitor",
b"register scene",
b"GlobalAuthError",
b"ErrorFlowHelper",
]
with zipfile.ZipFile(str(APK)) as zf:
data = b"".join(
zf.read(n)
for n in zf.namelist()
if n.endswith((".dex", ".jsbundle", ".json"))
)
for n in needles:
print(n.decode(), data.count(n))
idx = data.find(b"currently unavailable")
if idx >= 0:
print("\ncontext:", data[max(0, idx - 100) : idx + 150])

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,94 @@
# -*- coding: utf-8 -*-
"""Scan TNG eWallet APK for AppProtect / root detection artifacts."""
import re
import sys
import zipfile
KEYS = [
b"AppProtect", b"appprotect", b"vkey", b"V-Key", b"VKey", b"VGuard",
b"jailbroken", b"Jailbroken", b"Rooted Device", b"rooted device",
b"Close app", b"tngdigital", b"TNG eWallet", b"How to keep device safe",
b"enhanced our security", b"RootBeer", b"SafetyNet", b"PlayIntegrity",
b"detectRoot", b"isRooted", b"checkRoot", b"magisk", b"xposed", b"lsposed",
b"frida", b"emulator", b"su binary", b"/system/xbin/su",
]
CLASS_KEYS = [
b"safemode", b"SafeMode", b"appprotect", b"AppProtect", b"vkey", b"VKey",
b"vguard", b"VGuard", b"rooted", b"RootBeer", b"integrity", b"jailbreak",
b"security", b"RiskDevice", b"tamper", b"hook", b"frida",
]
def scan_strings(apk_path):
print("=== STRING SCAN: %s ===" % 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)
hits = []
for key in KEYS:
start = 0
while True:
idx = data.find(key, start)
if idx < 0:
break
s = max(0, idx - 60)
e = min(len(data), idx + len(key) + 100)
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e])
chunk = chunk.decode("ascii", "ignore").strip()
if chunk and chunk not in hits:
hits.append(chunk)
start = idx + 1
if hits:
print("\n--- %s (%d hits) ---" % (name, len(hits)))
for h in sorted(set(hits))[:40]:
print(" ", h)
if len(hits) > 40:
print(" ... +%d more" % (len(hits) - 40))
def scan_classes(apk_path):
print("\n=== CLASS SCAN: %s ===" % 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:
low = raw.lower()
if any(k.lower() in low for k in CLASS_KEYS):
s = raw.decode("ascii", "ignore")[1:-1].replace("/", ".")
hits.append(s)
if hits:
print("\n--- %s (%d classes) ---" % (name, len(hits)))
for h in sorted(set(hits))[:60]:
print(" ", h)
if len(hits) > 60:
print(" ... +%d more" % (len(hits) - 60))
def scan_native(apk_path):
print("\n=== NATIVE LIB SCAN: %s ===" % apk_path)
with zipfile.ZipFile(apk_path) as zf:
for name in sorted(zf.namelist()):
if not name.endswith(".so"):
continue
data = zf.read(name)
lib = name.split("/")[-1]
found = []
for key in KEYS + [b"libvos", b"libvkey", b"libvguard", b"libappprotect"]:
if key.lower() in data.lower():
found.append(key.decode("ascii", "ignore"))
if found:
print(" %s: %s" % (lib, ", ".join(sorted(set(found)))))
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "reverse/apks/tng/base.apk"
scan_strings(path)
scan_classes(path)
scan_native(path)

View File

@@ -0,0 +1,18 @@
"""Dump xwwqazamx bl/w/A method refs from TNG dex."""
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\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 ["Lxwwqazamx/bl;", "Lxwwqazamx/w;", "Lxwwqazamx/W;", "Lxwwqazamx/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("=== lifecycle on xwwqazamx/w ===")
for m in sorted(set(re.findall(rb"Lxwwqazamx/w;->on[A-Za-z]+", data))):
print(m.decode())

View File

@@ -0,0 +1,25 @@
"""Scan TNG AppSecurityManager callbacks and bl methods."""
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk"
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
keys = [
b"handleRootingCallback", b"handleEmulatorCallback", b"handleHookingCallback",
b"handleMalwareCallback", b"onBlockStaticCheck", b"addIntoQueue",
b"ForceExit", b"exitApplication", b"startForceExit", b"Lxwwqazamx/bl;",
]
for k in keys:
i = data.find(k)
if i < 0:
continue
print("---", k.decode(), "---")
s = re.sub(rb"[^\x20-\x7e]+", b" ", data[max(0, i - 150) : i + len(k) + 200])
print(s.decode("ascii", "ignore")[:400])
print()
print("=== bl method refs ===")
for m in sorted(set(re.findall(rb"Lxwwqazamx/bl;->[a-zA-Z0-9_$<>\[\]]+", data))):
print(m.decode())

View File

@@ -0,0 +1,37 @@
"""Scan TNG dex for Promon exit paths and ActivityThread refs."""
import re
import sys
import zipfile
apk = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\Administrator\AppData\Local\Temp\tng_base.apk"
with zipfile.ZipFile(apk) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
patterns = [
rb"handleExitApplication",
rb"System;->exit",
rb"Runtime;->exit",
rb"Process;->killProcess",
rb"Runtime;->halt",
rb"xwwqazamx/bl",
rb"xwwqazamx/w",
rb"xwwqazamx/W",
rb"addIntoQueue",
rb"handleRootingCallback",
rb"startForceExit",
]
for pat in patterns:
hits = len(re.findall(pat, data))
print(f"{pat.decode('utf-8', 'ignore')}: {hits}")
print("\n=== xwwqazamx class names (sample) ===")
classes = sorted(set(m.group().decode("utf-8", "ignore") for m in re.finditer(rb"Lxwwqazamx/[A-Za-z0-9_$]+;", data)))
for c in classes[:60]:
print(c)
print(f"... total {len(classes)}")
for pat in [b"startForceExit", b"ForceExit", b"openSecurityUrl", b"Lxwwqazamx/w;", b"Lxwwqazamx/bl;->"]:
print("---", pat.decode())
hits = sorted(set(m.group().decode("utf-8", "ignore") for m in re.finditer(pat + rb"[^\x00]{0,100}", data)))
for h in hits[:20]:
print(h)

View File

@@ -0,0 +1,72 @@
"""Find System.exit(10) / killProcess callers and nearby strings in TNG DEX."""
import re
import zipfile
from pathlib import Path
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
with zipfile.ZipFile(apk) as z:
dex_blobs = [(n, z.read(n)) for n in z.namelist() if n.endswith(".dex")]
needles = [
b"finishAllActivityAndKillApp",
b"UnhandledEvent detected",
b"AppSecurityManager: UnhandledEvent",
b"openSecurityUrl",
b"startForceExitCountdown",
b"ForceExitCountdown",
b"killProcess",
b"SecurityForceExit",
b"handleExitApplication",
b"exitApplication",
b"Jailbroken/Rooted",
b"Detected by AppProtect",
]
print("=== string hits ===")
for name, data in dex_blobs:
for n in needles:
c = data.count(n)
if c:
print(f"{name}: {n.decode(errors='ignore')} x{c}")
# Find UTF-16 / UTF-8 contexts around exit-related
print("\n=== contexts near 'exit' security strings ===")
for name, data in dex_blobs:
for m in re.finditer(rb"[\x20-\x7e]{0,30}(exit|KillApp|killApp|ForceExit|Unhandled)[\x20-\x7e]{0,80}", data):
s = m.group().decode("ascii", "ignore")
if any(k in s.lower() for k in ("force", "kill", "unhandled", "security", "promon", "root")):
print(f"{name}: {s}")
# Smali-ish type refs
print("\n=== type refs ===")
patterns = [
rb"Lmy/com/tngdigital/common/internal/_ContextKt;",
rb"Lmy/com/tngdigital/common/security/model/UnhandledEvent;",
rb"Lxwwqazamx/W;",
rb"Lxwwqazamx/bl;",
rb"Landroid/os/Process;->killProcess",
rb"Ljava/lang/System;->exit",
rb"Ljava/lang/Runtime;->exit",
]
for name, data in dex_blobs:
for pat in patterns:
hits = len(re.findall(pat, data))
if hits:
print(f"{name}: {pat.decode(errors='ignore')} x{hits}")
# Look for const/16 near exit - hard in raw dex; instead find methods that mention exit code strings
print("\n=== classes near ForceExit / KillApp strings ===")
for name, data in dex_blobs:
for pat in [b"finishAllActivityAndKillApp", b"UnhandledEvent detected", b"startForceExitCountdownIfNeeded"]:
i = 0
while True:
j = data.find(pat, i)
if j < 0:
break
# scan backwards for L...; class descriptor within 2KB
window = data[max(0, j - 2048):j]
classes = re.findall(rb"L[\w/$]+;", window)
if classes:
print(f"{name} @{j} near {pat.decode()}: ...{classes[-5:]}")
i = j + 1

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Deeper TNG reverse: Exiting/Report, TigerTally API, kill-SVC in SO."""
from pathlib import Path
import re
import struct
ROOT = Path(__file__).resolve().parents[2]
APK_DIR = ROOT / "reverse" / "apks" / "tng"
SO = APK_DIR / "libtngdigital_ewallet.so"
def load_dexes():
files = sorted(APK_DIR.glob("classes*.dex"))
if not files:
# try extracted under other layouts
files = sorted((ROOT / "reverse" / "apks").rglob("tng*/classes*.dex"))
return files
def near(data, needle, before=40, after=80):
out = []
for m in re.finditer(re.escape(needle), data):
s = max(0, m.start() - before)
e = min(len(data), m.end() + after)
chunk = data[s:e]
printable = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
out.append(printable)
return out
def main():
dexes = load_dexes()
print(f"dex count={len(dexes)}")
all_data = b""
for d in dexes:
data = d.read_bytes()
all_data += data
hits = []
for k in [b"Exiting:", b"Exiting", b"Report", b"W: 16", b"W:16",
b"TigerTallyAPI", b"ttInit", b"collect", b"killProcess",
b"SIGABRT", b"abort(", b"tgkill"]:
c = data.count(k)
if c:
hits.append(f"{k.decode('latin1')}x{c}")
if hits:
print(f"{d.name}: {', '.join(hits)}")
print("\n=== near Exiting ===")
for s in near(all_data, b"Exiting")[:15]:
print(" ", s)
print("\n=== TigerTallyAPI method refs ===")
for m in sorted(set(re.findall(rb"Lcom/aliyun/TigerTally/TigerTallyAPI;->[A-Za-z0-9_<>$]+", all_data))):
print(" ", m.decode())
print("\n=== TigerTally t/ classes ===")
for m in sorted(set(re.findall(rb"Lcom/aliyun/TigerTally/[a-z]/[A-Za-z0-9_/$]*;", all_data))):
print(" ", m.decode())
print("\n=== xwwqazamx/bl method refs ===")
for m in sorted(set(re.findall(rb"Lxwwqazamx/bl;->[A-Za-z0-9_<>$]+", all_data)))[:40]:
print(" ", m.decode())
print("\n=== Process.killProcess / Runtime.exit refs near Promon ===")
for pat in [rb"Landroid/os/Process;->killProcess", rb"Ljava/lang/System;->exit",
rb"Ljava/lang/Runtime;->exit", rb"Ljava/lang/Runtime;->halt"]:
print(pat.decode(), "count=", len(re.findall(pat, all_data)))
if SO.exists():
so = SO.read_bytes()
print(f"\n=== SO {SO.name} size={len(so)} ===")
# movz x8,#129 = D2801028 LE
patterns = {
"movz_x8_129": bytes.fromhex("281080d2"),
"movz_w8_129": bytes.fromhex("28108052"),
"movz_x8_130": bytes.fromhex("481080d2"),
"movz_x8_131": bytes.fromhex("681080d2"),
"svc0": bytes.fromhex("010000d4"),
"brk0": bytes.fromhex("000020d4"),
}
for name, pat in patterns.items():
print(f" {name}: {so.count(pat)}")
# find movz kill + nearby svc within 32 bytes
kill_imm = [bytes.fromhex(x) for x in ("281080d2", "28108052", "481080d2", "681080d2")]
svc = bytes.fromhex("010000d4")
found = 0
for imm in kill_imm:
start = 0
while True:
i = so.find(imm, start)
if i < 0:
break
window = so[i:i+36]
if svc in window:
found += 1
if found <= 20:
off = window.find(svc)
print(f" kill+svc @ file+0x{i:x} svc_delta={off}")
start = i + 4
print(f" kill+svc pairs (packed): {found}")
else:
print(f"\nSO missing: {SO}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = "reverse/apks/tng/base.apk"
KEYS = [
b"AppProtect", b"JailBroken", b"jailbroken", b"isRooted", b"isJailbroken",
b"VKey", b"Promon", b"promon", b"APSE", b"Close app", b"Rooted Device",
b"enhanced our security", b"How to keep device safe", b"Detected by",
]
CLASS_NEEDLES = [
b"JailBroken", b"AppProtect", b"ApSecurity", b"Promon", b"VKey", b"VGuard",
b"RootDetect", b"DeviceRisk", b"SecurityInitializer", b"libAPSE",
]
def main():
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
print("=== KEY STRINGS ===")
for k in KEYS:
idx = 0
while True:
idx = data.find(k, idx)
if idx < 0:
break
s = max(0, idx - 80)
e = min(len(data), idx + len(k) + 120)
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
print(f"[{k.decode()}] {chunk.strip()}")
idx += 1
print("\n=== KEY CLASSES ===")
classes = set(re.findall(rb"L[a-zA-Z0-9_$/]+;", data))
hits = []
for raw in classes:
if any(n in raw for n in CLASS_NEEDLES):
hits.append(raw.decode()[1:-1].replace("/", "."))
for h in sorted(set(hits)):
print(h)
FLOW_KEYS = [
b"Detected by", b"Close app", b"AppSecurityManager", b"RootEvent", b"RootI18n",
b"PromonError", b"showJailBrokenAlert", b"onShowPopupDisable", b"isJailBroken",
b"detectJailBroken", b"APSecuritySdk", b"no/promon/shield", b"HookingFrameworks",
b"How to keep device safe", b"onBlockStaticCheck",
]
def scan_flow():
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
print("\n=== FLOW STRINGS ===")
for k in FLOW_KEYS:
idx = data.find(k)
if idx < 0:
continue
s = max(0, idx - 80)
e = min(len(data), idx + len(k) + 120)
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
print(f"[{k.decode()}] {chunk.strip()}")
if __name__ == "__main__":
main()
scan_flow()

View File

@@ -0,0 +1,53 @@
"""Find NativeLib / loadLibrary targets in TNG APK."""
import re
import zipfile
from pathlib import Path
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
split = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\split_config.arm64_v8a.apk")
with zipfile.ZipFile(apk) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
print("base dex files:", [n for n in z.namelist() if n.endswith(".dex")])
print("base lib entries:", [n for n in z.namelist() if "lib/" in n][:40])
print("\nsplit libs:")
with zipfile.ZipFile(split) as z:
libs = [n for n in z.namelist() if n.endswith(".so")]
for n in libs:
print(" ", n)
# strings related to NativeLib
needles = [
b"NativeLib",
b"tngd.networksdk",
b"RetrieveFromNativeLibs",
b"getApiSixSecretKeys",
b"networksdk",
b"libtng",
b"loadLibrary",
]
print("\n=== string hits ===")
for n in needles:
hits = list(re.finditer(n, data))
print(f"{n!r}: {len(hits)}")
for h in hits[:5]:
ctx = data[max(0, h.start()-30):h.end()+80]
ctx = bytes(c if 32 <= c < 127 else 46 for c in ctx)
print(" ", ctx)
# library name candidates near NativeLib
print("\n=== lib name-like strings near 'NativeLib' / networksdk ===")
for m in re.finditer(rb"[\x20-\x7e]{4,60}", data):
s = m.group().decode()
if "network" in s.lower() or "tngd" in s.lower() or s.startswith("lib") and "tng" in s.lower():
if len(s) < 80:
print(" ", s)
# specific: System.loadLibrary argument often stored as short string without lib/ prefix
print("\n=== candidate loadLibrary short names ===")
cands = set(re.findall(rb"[\x00]([A-Za-z0-9_]{3,40})[\x00]", data))
for c in sorted(cands):
s = c.decode()
if any(k in s.lower() for k in ("tng", "network", "native", "promon", "shield", "apse")):
print(" ", s)

View File

@@ -0,0 +1,19 @@
"""Find NativeLib method signatures / loadLibrary name via dex string proximity."""
import re
import zipfile
from pathlib import Path
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
with zipfile.ZipFile(apk) as z:
for name in z.namelist():
if not name.endswith(".dex"):
continue
data = z.read(name)
if b"NativeLib" not in data and b"native-lib" not in data:
continue
print("===", name, "===")
for pat in [b"NativeLib", b"native-lib", b"getApiSixSecretKeys", b"RetrieveFromNativeLibs", b"Lcom/tngd/networksdk"]:
for m in re.finditer(pat, data):
ctx = data[max(0, m.start()-60):m.end()+100]
ctx = bytes(c if 32 <= c < 127 else 46 for c in ctx)
print(pat.decode(), "@", m.start(), ":", ctx.decode())

View File

@@ -0,0 +1,16 @@
import re
import sys
import zipfile
apk = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\Administrator\AppData\Local\Temp\tng_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 ["xwwqazamx/bl", "xwwqazamx/W", "xwwqazamx/a", "JNICLibrary", "hzchengdun"]:
pattern = cls.encode("utf-8") + rb"[^\x00]{0,120}"
hits = sorted(set(
m.group().decode("utf-8", "ignore") for m in re.finditer(pattern, data)
))
print(f"\n=== {cls} ({len(hits)} strings) ===")
for h in hits[:40]:
print(h)

View File

@@ -0,0 +1,18 @@
"""Find SecurityErrorActivity onCreate signature."""
import re
import zipfile
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk"
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
for pat in [
b"SecurityErrorActivity",
b"launchProcessNextSecurityState",
b"addIntoQueueAndLaunch",
b"SecurityErrorBaseActivity;->onCreate",
]:
print("===", pat.decode(), "===")
for m in sorted(set(re.findall(pat + rb"[^\x00]{0,120}", data))):
print(m.decode("ascii", "ignore")[:150])
print()

View File

@@ -0,0 +1,61 @@
"""Scan libtngdigital_ewallet.so for SVC / exit patterns."""
from pathlib import Path
import struct
SO = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\libtngdigital_ewallet.so")
if not SO.exists():
# try from split apk
import zipfile
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\split_config.arm64_v8a.apk")
if apk.exists():
with zipfile.ZipFile(apk) as z:
for n in z.namelist():
if n.endswith("libtngdigital_ewallet.so"):
SO.write_bytes(z.read(n))
print("extracted from", apk, "->", SO)
break
data = SO.read_bytes()
print("size", len(data), SO)
svc = b"\x01\x00\x00\xd4"
idxs = []
start = 0
while True:
i = data.find(svc, start)
if i < 0:
break
idxs.append(i)
start = i + 4
print("total svc#0:", len(idxs))
# movz x8,#93 = d2 80 0b a8 ; movz x8,#94 = d2 80 0b c8 (LE)
# bytes LE: A8 0B 80 D2 / C8 0B 80 D2
exit_setups = [
(b"\xa8\x0b\x80\xd2", 93), # movz x8, #93
(b"\xc8\x0b\x80\xd2", 94), # movz x8, #94
(b"\xa8\x0b\x80\x52", 93), # movz w8, #93
(b"\xc8\x0b\x80\x52", 94), # movz w8, #94
]
for pat, nr in exit_setups:
c = data.count(pat)
print(f"movz *8,#{nr} pattern count={c}")
print("\nSVC with nearby exit setup (lookback 32 bytes):")
hits = 0
for i in idxs[:2000]:
window = data[max(0, i - 32) : i]
for pat, nr in exit_setups:
if pat in window:
print(f" off=0x{i:x} exit_group/exit via #{nr}")
hits += 1
break
print("hits", hits)
# also search brk
brk = b"\x00\x00\x20\xd4" # brk #0
print("brk#0 count", data.count(brk))
# string refs
for s in [b"_exit", b"exit_group", b"abort", b"frida", b"/proc/self/maps", b"xposed"]:
print(s, "->", data.find(s))

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
from pathlib import Path
DIR = Path(__file__).resolve().parents[1] / "apks" / "tng"
KILL = [bytes.fromhex(x) for x in (
"281080d2", "28108052", "481080d2", "48108052", "681080d2", "68108052",
)]
SVC = bytes.fromhex("010000d4")
EXIT = [bytes.fromhex(x) for x in ("c80b80d2", "ba0b80d2")] # exit_group, exit
for so in sorted(DIR.glob("lib*.so")):
data = so.read_bytes()
pairs = 0
for imm in KILL:
i = 0
while True:
j = data.find(imm, i)
if j < 0:
break
if SVC in data[j:j + 36]:
pairs += 1
if pairs <= 10:
print(f"{so.name} KILL+SVC @0x{j:x} d={data[j:j+36].find(SVC)}")
i = j + 4
ep = 0
for imm in EXIT:
i = 0
while True:
j = data.find(imm, i)
if j < 0:
break
if SVC in data[j:j + 36]:
ep += 1
if ep <= 8:
print(f"{so.name} EXIT+SVC @0x{j:x}")
i = j + 4
print(
f"{so.name}: size={len(data)} svc0={data.count(SVC)} "
f"kill+svc={pairs} exit+svc={ep} "
f"abort={data.count(b'abort')} kill={data.count(b'kill')}"
)

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""TNG reverse notes helper — ForceExit / abort / Promon suicide map."""
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parents[1] / "apks" / "tng" / "base.apk"
SO = Path(__file__).resolve().parents[1] / "apks" / "tng" / "libtngdigital_ewallet.so"
def main():
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
print("=== suicide ladder (from runtime + static) ===")
print("1) Promon root hit -> openSecurityUrl Rooting FAQ (Xposed blocks)")
print("2) xwwqazamx.W -> KillApplicationHandler (Xposed blocks)")
print("3) native exit_group(1) OR SIGABRT SI_USER via libc abort/raise/tgkill")
print("4) AppSecurityManager.startForceExitCountdown* / addIntoQueueAndLaunch")
print()
print("=== ForceExit-related descriptors ===")
for m in sorted(set(re.findall(rb"L[A-Za-z0-9_/$]*ForceExit[A-Za-z0-9_/$]*;", data))):
print(m.decode())
print("\n=== AppSecurityManager log strings (detection events) ===")
for m in re.finditer(rb"AppSecurityManager: [A-Za-z][^\x00]{5,80}", data):
s = m.group().decode("utf-8", "ignore")
if any(k in s for k in ("Root", "Hook", "Emulator", "Force", "Unhandled", "Navigat")):
print(s)
if SO.exists():
raw = SO.read_bytes()
print("\n=== SO imports of interest ===")
for s in (b"abort", b"raise", b"tgkill", b"kill", b"exit"):
print(s.decode(), "at", hex(raw.find(s)) if raw.find(s) >= 0 else None)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
"""Scan TNG DEX for Aliyun TigerTally / abort / SI_USER suicide helpers."""
import re
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parents[1] / "apks" / "tng" / "base.apk"
KEYS = [
b"TigerTally",
b"aliyun",
b"Aliyun",
b"com/aliyun/TigerTally",
b"UnhandledEvent detected",
b"finishAllActivityAndKillApp",
b"trackUnhandledEvent",
b"SI_USER",
b"raise",
b"SIGABRT",
b"pthread_kill",
b"dispatchUncaughtException",
b"AppProtect",
b"promon",
b"xwwqazamx",
]
def main():
with zipfile.ZipFile(APK) as z:
entries = [(n, z.read(n)) for n in z.namelist() if n.endswith(".dex")]
print("=== key hits ===")
for name, data in entries:
for k in KEYS:
c = data.count(k)
if c:
print(f"{name}: {k.decode('utf-8','ignore')} x{c}")
print("\n=== TigerTally class descriptors ===")
all_data = b"".join(d for _, d in entries)
classes = sorted(set(re.findall(rb"Lcom/aliyun/TigerTally/[A-Za-z0-9_/$]*;", all_data)))
for c in classes[:80]:
print(c.decode())
print("total", len(classes))
print("\n=== nearby strings TigerTally ===")
for m in re.finditer(rb"TigerTally[\x20-\x7e]{0,60}", all_data):
print(m.group().decode("ascii", "ignore"))
print("\n=== finishAll / Unhandled contexts ===")
for pat in [
b"finishAllActivityAndKillApp",
b"UnhandledEvent detected",
b"trackUnhandledEvent",
b"dispatchUncaughtException",
]:
idx = all_data.find(pat)
if idx < 0:
continue
ctx = all_data[max(0, idx - 40) : idx + len(pat) + 80]
printable = "".join(chr(b) if 32 <= b < 127 else "." for b in ctx)
print(pat.decode(), "=>", printable)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import re
import zipfile
APK = "reverse/apks/tng/base.apk"
KEYS = [
b"36616543382169", b"support.tngdigital", b"Rooting", b"How to keep device safe",
b"showJailBroken", b"openUrl", b"openBrowser", b"launchUrl", b"ACTION_VIEW",
b"RootI18n", b"SecurityError", b"startChrome", b"IntentDispatcher",
]
def main():
with zipfile.ZipFile(APK) as zf:
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
for k in KEYS:
idx = 0
while True:
idx = data.find(k, idx)
if idx < 0:
break
s = max(0, idx - 80)
e = min(len(data), idx + len(k) + 120)
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
print(f"[{k.decode()}] {chunk.strip()}")
idx += 1
print("\n=== classes with Root/security ===")
classes = set(re.findall(rb"Lmy/com/tngdigital/common/security[^;]+;", data))
for c in sorted(classes):
s = c.decode()[1:-1].replace("/", ".")
if any(x in s.lower() for x in ["root", "error", "jail", "shield", "promon"]):
print(s)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
needles = [
b"The system is currently unavailable",
b"dfp is empty",
b"getDfp",
b"preCheck",
b"register",
b"Sign up with mobile",
b"msg_ekyc_singpass_service_error",
b"general_error",
b"GlobalAuth",
]
with zipfile.ZipFile(str(APK)) as zf:
bundles = [n for n in zf.namelist() if n.endswith(".jsbundle")]
print("bundles:", len(bundles))
for name in bundles:
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
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,466 @@
#!/usr/bin/env python3
"""TNG 全流程自动化:注册区号 + 登录 PIN/区号;两次冷启动,互不干扰。"""
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
PKG = "my.com.tngdigital.ewallet"
SPLASH = f"{PKG}/.ui.SplashActivity"
ADB = ["adb"]
TNG_LOG = re.compile(
r"TngRoot|F HWUI|GraphicBuffer|Runtime abort|signal 6|exited due to signal"
r"|UserSearchCallingCode|UserLogin|UserRegistration|SecurityError"
r"|Dialog\.show|skip loading|registration flow",
re.I,
)
CRASH_LOG = re.compile(
r"signal 6|Runtime aborting|F HWUI|F GraphicBuffer|gralloc-mapper is missing",
re.I,
)
def adb(*args, timeout=60):
r = subprocess.run(ADB + list(args), capture_output=True, text=True, timeout=timeout, errors="ignore")
return r.returncode, (r.stdout or "") + (r.stderr or "")
def is_tng_foreground():
_, out = adb("shell", "dumpsys", "activity", "activities")
for line in out.splitlines():
if "topResumedActivity=" in line:
return PKG in line
return False
def bring_tng_foreground():
adb("shell", "input", "keyevent", "KEYCODE_WAKEUP")
if is_tng_foreground():
return
adb("shell", "am", "start", "-n", SPLASH)
time.sleep(2)
def dump_ui(path="/sdcard/tng_ui.xml", retries=2):
adb("shell", "input", "keyevent", "KEYCODE_WAKEUP")
for _ in range(retries):
adb("shell", "uiautomator", "dump", path)
code, xml = adb("shell", "cat", path)
if code == 0 and xml.strip().startswith("<?xml"):
try:
return ET.fromstring(xml)
except ET.ParseError:
pass
time.sleep(1)
return None
def center(bounds_str):
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds_str or "")
if not m:
return None
x1, y1, x2, y2 = map(int, m.groups())
return (x1 + x2) // 2, (y1 + y2) // 2
def find_pin_login_row(root):
candidates = []
for node in root.iter("node"):
t = node.get("text") or ""
if "忘记" in t:
continue
pt = center(node.get("bounds"))
if pt is None:
continue
# 登录方式页 PIN 行 y≈600780
if 600 <= pt[1] <= 780 and (
"PIN" in t.upper() or "6位数" in t or "6位" in t or (len(t) >= 4 and "PIN" in t)
):
candidates.append((pt[1], node))
if not candidates:
return None
candidates.sort(key=lambda x: x[0])
return candidates[0][1]
def wait_phone_page(timeout=20):
return wait_for(lambda: is_phone_page(dump_ui()), timeout=timeout, interval=1.5, desc="phone page")
def find_nodes(root, **kwargs):
out = []
for node in root.iter("node"):
text = (node.get("text") or "").strip()
rid = node.get("resource-id") or ""
cls = node.get("class") or ""
clickable = node.get("clickable") == "true"
ok = True
if "text_contains" in kwargs and kwargs["text_contains"] not in text:
ok = False
if "text_excludes" in kwargs:
for ex in kwargs["text_excludes"]:
if ex in text:
ok = False
if "rid_contains" in kwargs and kwargs["rid_contains"] not in rid:
ok = False
if "class_contains" in kwargs and kwargs["class_contains"] not in cls:
ok = False
if kwargs.get("clickable") and not clickable:
ok = False
if ok:
out.append(node)
return out
def tap_node(node, label=""):
pt = center(node.get("bounds"))
if not pt:
print(f" skip tap {label}: bad bounds")
return False
x, y = pt
print(f" tap {label!r} at {x},{y}")
adb("shell", "input", "tap", str(x), str(y))
return True
def find_country_control(root, min_y=0, max_y=9999):
nodes = find_nodes(root, rid_contains="ll_country", clickable=True)
for node in nodes:
pt = center(node.get("bounds"))
if pt and min_y <= pt[1] <= max_y:
return node
nodes = find_nodes(root, rid_contains="tv_left", clickable=True)
for node in nodes:
pt = center(node.get("bounds"))
if pt and min_y <= pt[1] <= max_y:
return node
return None
def find_register_btn(root):
for node in root.iter("node"):
if node.get("clickable") != "true":
continue
t = node.get("text") or ""
if ("注册" in t or "Register" in t.lower()) and "已经" not in t and "已注" not in t:
return node
pt = center(node.get("bounds"))
if pt and 1050 < pt[1] < 1220 and 430 < pt[0] < 660:
return node
return None
def is_phone_page(root):
return bool(find_nodes(root, rid_contains="userContinueBtn"))
def is_register_page(root):
return bool(
find_nodes(root, rid_contains="ftv_register_title")
or find_nodes(root, rid_contains="userRegisterContinueBtn")
)
def has_country_list(root):
if root is None:
return False
for node in root.iter("node"):
t = node.get("text") or ""
if any(k in t for k in ("Malaysia", "Singapore", "Australia", "China", "+86", "+61")):
return True
return False
def pid():
code, out = adb("shell", "pidof", PKG)
return out.strip() if code == 0 and out.strip() else ""
def top_activity():
_, out = adb("shell", "dumpsys", "activity", "activities")
for line in out.splitlines():
if PKG not in line:
continue
if "topResumedActivity=" in line:
m = re.search(r"/([^/}\s]+)", line)
return m.group(1) if m else "?"
for line in out.splitlines():
if "ResumedActivity:" in line and PKG in line:
m = re.search(r"/([^/}\s]+)", line)
if m:
return m.group(1)
return "?"
def wait_for(fn, timeout=35, interval=1.5, desc=""):
deadline = time.time() + timeout
while time.time() < deadline:
if fn():
return True
time.sleep(interval)
print(f" timeout: {desc}")
return False
def wait_ui_widgets(timeout=30):
"""等待 Compose 控件出现在 dumpSplash WebView 退场后)。"""
def ready():
root = dump_ui()
if root is None:
return False
return bool(
find_pin_login_row(root)
or find_register_btn(root)
or is_phone_page(root)
or find_nodes(root, rid_contains="ftv_content")
or find_nodes(root, rid_contains="ftv_register_title")
)
return wait_for(ready, timeout=timeout, interval=2, desc="login/register widgets")
def wait_login_method(timeout=40):
def ready():
if not pid():
return False
if not is_tng_foreground():
bring_tng_foreground()
return "UserLoginActivity" in top_activity()
ok = wait_for(ready, timeout=timeout, interval=2, desc="UserLoginActivity ready")
if ok:
wait_ui_widgets(timeout=25)
return ok
def wait_login_method_register(timeout=40):
return wait_login_method(timeout=timeout)
def wait_country_list(timeout=30):
def activity_has_country_list():
act = top_activity()
if "UserSearchCallingCode" in act:
return True
_, out = adb("shell", "dumpsys", "activity", "activities")
return "UserSearchCallingCodeActivity" in out and PKG in out
def ready():
if activity_has_country_list():
return True
root = dump_ui()
return has_country_list(root)
return wait_for(ready, timeout=timeout, interval=1.5, desc="country list")
def cold_start(clear_log=False):
if clear_log:
adb("logcat", "-c")
adb("shell", "am", "force-stop", PKG)
time.sleep(3)
adb("shell", "input", "keyevent", "KEYCODE_WAKEUP")
adb("shell", "am", "start", "-W", "-n", SPLASH)
ok = wait_for(
lambda: bool(pid()) and "UserLoginActivity" in top_activity(),
timeout=60,
interval=2,
desc="UserLoginActivity after cold start",
)
if not ok:
return False
bring_tng_foreground()
time.sleep(3)
return True
def cold_start_with_retry(clear_log=False, attempts=3):
for i in range(attempts):
if cold_start(clear_log and i == 0):
return True
print(f" cold start retry {i + 1}/{attempts}")
adb("shell", "am", "force-stop", PKG)
time.sleep(5)
return bool(pid()) and "UserLoginActivity" in top_activity()
def tap_pin_row(root):
if root is None:
print(" PIN fallback (no dump) 561,677")
adb("shell", "input", "tap", "561", "677")
return
pin_text = find_pin_login_row(root)
if pin_text is None:
if find_nodes(root, rid_contains="ftv_content") or find_nodes(root, rid_contains="ftv_title"):
print(" PIN layout fallback 561,677")
adb("shell", "input", "tap", "561", "677")
return
print(" PIN fallback tap 540,677")
adb("shell", "input", "tap", "540", "677")
return
pt = center(pin_text.get("bounds"))
print(f" tap PIN at {pt[0]},{pt[1]}")
adb("shell", "input", "tap", str(pt[0]), str(pt[1]))
def collect_logs(n=3000):
_, out = adb("shell", "logcat", "-d", "-t", str(n))
return [ln for ln in out.splitlines() if TNG_LOG.search(ln)]
def collect_crashes(n=4000):
_, out = adb("shell", "logcat", "-d", "-t", str(n))
return [ln for ln in out.splitlines() if CRASH_LOG.search(ln) and "digital.ewallet" in ln]
def step(name, fn):
print(f"\n=== {name} ===")
bring_tng_foreground()
ok = fn()
print(f" pid={pid() or 'DEAD'} activity={top_activity()}")
return ok and bool(pid())
def wait_register_page(timeout=20):
def ready():
root = dump_ui()
if root is None or not is_register_page(root):
return False
ctrl = find_country_control(root, min_y=680)
if ctrl is None:
return False
pt = center(ctrl.get("bounds"))
# 注册页 ll_country 中心 y 通常 > 680
return pt is not None and pt[1] >= 680
ok = wait_for(ready, timeout=timeout, interval=1.5, desc="register page settled")
if ok:
time.sleep(1)
return ok
def flow_register():
time.sleep(5)
if not cold_start_with_retry():
return False
if not wait_login_method_register():
print(" register entry not visible")
return False
root = dump_ui()
reg = find_register_btn(root)
if reg is None:
print(" register fallback 540,1137")
adb("shell", "input", "tap", "540", "1137")
else:
tap_node(reg, "register")
if not wait_register_page():
return False
root2 = dump_ui()
target = find_country_control(root2, min_y=680)
if target is None:
print(" no country control on register page")
return False
tap_node(target, "register country")
if not wait_country_list(timeout=25):
return False
root3 = dump_ui()
if root3 is not None:
for node in root3.iter("node"):
t = node.get("text") or ""
if "Malaysia" in t or "Singapore" in t:
print(f" country UI: {t[:40]}")
break
logs = collect_logs(300)
for ln in logs:
if "skip loading" in ln or "UserSearchCallingCode" in ln:
print(f" log: {ln[:120]}")
break
return True
def flow_login():
if not cold_start_with_retry(clear_log=True):
return False
if not wait_login_method():
return False
root = dump_ui()
if is_phone_page(root):
print(" already on phone page")
else:
for attempt in range(2):
root = dump_ui()
if root is None:
time.sleep(2)
continue
if is_phone_page(root):
print(" phone page ready")
break
tap_pin_row(root)
if wait_phone_page(timeout=15):
break
print(f" phone page retry {attempt + 1}/2")
time.sleep(2)
else:
return False
root2 = dump_ui()
target = find_country_control(root2, min_y=450, max_y=650)
if target is None:
print(" no country control on phone page")
return False
tap_node(target, "login country")
if not wait_country_list():
return False
root3 = dump_ui()
if root3 is not None:
for node in root3.iter("node"):
t = node.get("text") or ""
if "Malaysia" in t or "Singapore" in t:
print(f" country UI: {t[:40]}")
break
logs = collect_logs(300)
for ln in logs:
if "skip loading" in ln:
print(f" log: {ln[:120]}")
break
return "UserSearchCallingCode" in top_activity() or has_country_list(root3)
def main():
adb("shell", "svc", "power", "stayon", "true")
adb("logcat", "-c")
adb("shell", "am", "force-stop", PKG)
time.sleep(5)
# 先登录后注册:首次冷启动最稳定
results = [
("login_country", step("A. login PIN → country list", flow_login)),
("register_country", step("B. register → country list", flow_register)),
]
crashes = collect_crashes()
p = pid()
print("\n=== SUMMARY ===")
for name, ok in results:
print(f" {'PASS' if ok else 'FAIL'} {name}")
print(f" pid={p or 'DEAD'} activity={top_activity()}")
print(f" crashes={len(crashes)}")
if crashes:
for ln in crashes[-5:]:
print(" ", ln[:150])
print("\n=== TngRoot (last 20) ===")
for ln in collect_logs(2000)[-20:]:
if "TngRoot" in ln:
print(ln[:180])
ok = bool(p) and all(r[1] for r in results)
if crashes and not ok:
print(" (crash lines may include prior process; see pid)")
return 0 if ok else 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())

10
scripts/_tng_anr_dump.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/system/bin/sh
ANR=/data/anr/anr_2026-07-31-13-30-08-628
echo "=== header ==="
su -c "head -40 $ANR"
echo "=== main tid ==="
su -c "grep -n '\"main\"' $ANR | head -5"
echo "=== main block ==="
su -c "awk '/\"main\" prio/{p=1} p{print} p&&/^$/{c++} c>=2{exit}' $ANR" | head -80
echo "=== cpu ==="
su -c "grep -A20 'CPU usage' $ANR | head -30"

View File

@@ -0,0 +1,6 @@
#!/system/bin/sh
ANR=/data/anr/anr_2026-07-31-13-30-08-628
echo "=== RenderThread ==="
su -c "awk '/\"RenderThread\"/{p=1} p{print} p&&/^\"/{if(!/RenderThread/){exit}}' $ANR" | head -60
echo "=== Quake / promon threads ==="
su -c "grep -E '^\"|libtng|Quake|xwwq|promon|tiger' $ANR | head -80"

View File

@@ -0,0 +1,22 @@
#!/system/bin/sh
# 抓到 getprop 子进程瞬间 dump 其 syscall/status验证 exit_group 被 seccomp 卡住)
PKG="my.com.tngdigital.ewallet"
am force-stop "$PKG" 2>/dev/null
sleep 1
am start -n "$PKG/.ui.SplashActivity" 2>/dev/null
i=0
while [ $i -lt 200 ]; do
i=$((i+1))
for gp in $(pidof getprop 2>/dev/null); do
ppid=$(awk '/^PPid/{print $2}' /proc/$gp/status 2>/dev/null)
pp=$(tr '\0' ' ' < /proc/$ppid/cmdline 2>/dev/null)
echo "GETPROP pid=$gp ppid=$ppid pp=[$pp]"
echo " seccomp: $(grep -i seccomp /proc/$gp/status 2>/dev/null | tr '\n' ' ')"
echo " syscall: $(cat /proc/$gp/syscall 2>/dev/null)"
echo " wchan: $(cat /proc/$gp/wchan 2>/dev/null) state=$(awk '/^State/{print $2}' /proc/$gp/status 2>/dev/null)"
done
sleep 0.05
done
echo "=== done ==="

24
scripts/_tng_pin_check.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/system/bin/sh
logcat -c
input keyevent KEYCODE_WAKEUP
settings put system screen_off_timeout 600000
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
i=0
while [ $i -lt 40 ]; do
sleep 1
i=$((i+1))
p=$(pidof my.com.tngdigital.ewallet)
if [ -z "$p" ]; then
echo "t=${i}s DEAD"
break
fi
if [ $((i % 5)) -eq 0 ]; then
echo "t=${i}s pid=$p"
fi
done
echo "=== focus ==="
dumpsys window 2>/dev/null | grep -E 'mCurrentFocus|mFocusedApp' | head -4
echo "=== key ==="
logcat -d 2>/dev/null | grep -E 'TngExitGuard|Displayed.*User|UserPin|UserLogin|seccomp|caught sig=|exited cleanly|Fatal signal|ANR in my.com.tng' | tail -40

View File

@@ -0,0 +1,39 @@
#!/system/bin/sh
# 抓 TigerTally fread 阻塞的 pipe 写端进程。自动启动 TNG 并连续扫描。
PKG="my.com.tngdigital.ewallet"
am force-stop "$PKG" 2>/dev/null
sleep 1
am start -n "$PKG/.ui.SplashActivity" 2>/dev/null
i=0
while [ $i -lt 40 ]; do
i=$((i+1))
pid=""
for p in $(pidof "$PKG"); do
if [ "$(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null | tr -d ' ')" = "$PKG" ]; then pid=$p; break; fi
done
if [ -z "$pid" ]; then sleep 0.5; continue; fi
for d in /proc/$pid/task/*/; do
t="${d%/}"
s=$(cat "$t/syscall" 2>/dev/null)
case "$s" in
63*)
c=$(cat "$t/comm" 2>/dev/null)
fd=$(echo "$s" | awk '{print $2}')
fd=$((fd))
tgt=$(readlink "/proc/$pid/fd/$fd" 2>/dev/null)
echo "[$i] pid=$pid TID=${t##*/} comm=$c wchan=$(cat "$t/wchan" 2>/dev/null) syscall=$s"
echo "[$i] fd=$fd -> $tgt"
ino=$(echo "$tgt" | sed 's/.*\[//; s/\]//')
if [ -n "$ino" ]; then
echo "[$i] writer-search inode=$ino:"
find /proc/[0-9]*/fd -lname "*$ino*" 2>/dev/null
fi
;;
esac
done
sleep 0.5
done
echo "=== scan done ==="

View File

@@ -0,0 +1,65 @@
#!/system/bin/sh
# 抓 TigerTally fread 阻塞的 pipe 写端进程,命中后立即 dump 该进程身份/冻结状态。
PKG="my.com.tngdigital.ewallet"
am force-stop "$PKG" 2>/dev/null
sleep 1
am start -n "$PKG/.ui.SplashActivity" 2>/dev/null
i=0
while [ $i -lt 60 ]; do
i=$((i+1))
pid=""
for p in $(pidof "$PKG"); do
if [ "$(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null | tr -d ' ')" = "$PKG" ]; then pid=$p; break; fi
done
if [ -z "$pid" ]; then sleep 0.5; continue; fi
for d in /proc/$pid/task/*/; do
t="${d%/}"
s=$(cat "$t/syscall" 2>/dev/null)
case "$s" in
63*)
c=$(cat "$t/comm" 2>/dev/null)
case "$c" in
*pool*|*Tiger*|*tiger*|*tally*|*Tally*)
fd=$(echo "$s" | awk '{print $2}')
fd=$((fd))
tgt=$(readlink "/proc/$pid/fd/$fd" 2>/dev/null)
echo "[$i] MAIN pid=$pid TID=${t##*/} comm=$c wchan=$(cat "$t/wchan" 2>/dev/null)"
echo "[$i] MAIN fd=$fd -> $tgt"
ino=$(echo "$tgt" | sed 's/.*\[//; s/\]//')
[ -z "$ino" ] && continue
echo "[$i] writer-search inode=$ino:"
for wp in $(find /proc/[0-9]*/fd -lname "*$ino*" 2>/dev/null); do
echo "[$i] $wp"
done
# dump 所有非主进程端点
for wp in $(find /proc/[0-9]*/fd -lname "*$ino*" 2>/dev/null); do
wproc=$(echo "$wp" | cut -d/ -f3)
[ "$wproc" = "$pid" ] && continue
wfd=$(echo "$wp" | cut -d/ -f5)
echo "[$i] WRITER proc=$wproc fd=$wfd"
echo "[$i] cmdline: $(tr '\0' ' ' < /proc/$wproc/cmdline 2>/dev/null)"
echo "[$i] comm: $(cat /proc/$wproc/comm 2>/dev/null) state=$(cat /proc/$wproc/stat 2>/dev/null | awk '{print $3}')"
cg=$(cat /proc/$wproc/cgroup 2>/dev/null | grep -v freezer | head -1)
echo "[$i] cgroup: $cg"
# cgroup v2 freezer
cgpath=$(echo "$cg" | sed 's/^[0-9]*://')
if [ -f "/sys/fs/cgroup${cgpath}/cgroup.freeze" ]; then
echo "[$i] cgroup.freeze=$(cat /sys/fs/cgroup${cgpath}/cgroup.freeze 2>/dev/null)"
fi
echo "[$i] threads(wchan):"
for td in /proc/$wproc/task/*/; do
ttn=${td%/}
echo "[$i] ${ttn##*/} $(cat $ttn/comm 2>/dev/null) $(cat $ttn/wchan 2>/dev/null)"
done
done
;;
esac
;;
esac
done
sleep 0.4
done
echo "=== scan done ==="

46
scripts/_tng_proc_scan.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/system/bin/sh
# TNG eWallet — 启动期 /proc 扫描:定位阻塞在 read 的 TigerTally 线程及其 fd 目标。
# 用法: adb shell su -c 'sh /data/local/tmp/_tng_proc_scan.sh [loop_seconds]'
PKG="my.com.tngdigital.ewallet"
LOOP="${1:-1}" # 持续扫描秒数(默认 1 秒抓一次快照)
# 选主进程cmdline 恰为包名,排除 :tools / :goacqowmmt 等子进程)
main_pid=""
for p in $(pidof "$PKG"); do
if [ "$(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null | tr -d ' ')" = "$PKG" ]; then
main_pid="$p"
break
fi
done
[ -z "$main_pid" ] && { echo "NO-MAIN-PROC pidof=$(pidof $PKG)"; exit 1; }
echo "=== main pid=$main_pid ==="
i=0
while [ $i -lt "$LOOP" ]; do
i=$((i+1))
echo "--- scan #$i ---"
# 1) 所有可疑线程的状态
for tid in $(ls /proc/$main_pid/task 2>/dev/null); do
comm=$(cat /proc/$main_pid/task/$tid/comm 2>/dev/null)
case "$comm" in
*pool*|*location*|*tally*|*Tiger*|*tiger*)
syscall=$(cat /proc/$main_pid/task/$tid/syscall 2>/dev/null)
wchan=$(cat /proc/$main_pid/task/$tid/wchan 2>/dev/null)
stat=$(cat /proc/$main_pid/task/$tid/stat 2>/dev/null | awk '{print $3}')
echo "TID=$tid comm=$comm state=$stat syscall=[$syscall] wchan=$wchan"
;;
esac
done
# 2) 所有管道/套接字 fdTigerTally 握手候选)
for fd in /proc/$main_pid/fd/*; do
tgt=$(readlink "$fd" 2>/dev/null)
case "$tgt" in
*pipe:*|*socket:*|*anon_inode:*)
echo "FD=$(basename $fd) -> $tgt"
;;
esac
done
[ $i -lt "$LOOP" ] && sleep 1
done
echo "=== done ==="

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,37 @@
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",
"my.com.tngdigital.ewallet",
]
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,35 @@
# 解压 XAPK 并通过 adb install-multiple 安装 TNG
param(
[Parameter(Mandatory = $true)]
[string]$XapkPath
)
$ErrorActionPreference = "Stop"
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
if (-not (Test-Path $XapkPath)) {
Write-Host "文件不存在: $XapkPath" -ForegroundColor Red
exit 1
}
$extractDir = Join-Path ([IO.Path]::GetDirectoryName($XapkPath)) "tng_xapk_extracted"
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
New-Item -ItemType Directory -Path $extractDir | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory((Resolve-Path $XapkPath), $extractDir)
$apks = Get-ChildItem $extractDir -Filter "*.apk" -Recurse | Sort-Object Name
if ($apks.Count -eq 0) {
Write-Host "XAPK 内未找到 apk 文件" -ForegroundColor Red
exit 1
}
Write-Host "找到 $($apks.Count) 个 APK开始安装..." -ForegroundColor Cyan
$apkArgs = @("install-multiple", "-r") + ($apks | ForEach-Object { $_.FullName })
& $adb @apkArgs
if ($LASTEXITCODE -eq 0) {
Write-Host "TNG 安装成功" -ForegroundColor Green
& $adb shell monkey -p my.com.tngdigital.ewallet -c android.intent.category.LAUNCHER 1
} else {
Write-Host "安装失败exit=$LASTEXITCODE" -ForegroundColor Red
}

View File

@@ -0,0 +1,87 @@
# Launch MariBank Singapore (not PH SeaBank)
param(
[switch]$ClearData,
[switch]$StopPh,
[switch]$ColdStart,
[int]$WaitSeconds = 0
)
$ErrorActionPreference = "Stop"
$PkgSg = "sg.com.maribankmobile.digitalbank"
$PkgPh = "ph.seabank.seabank"
$Activity = "com.shopee.bke.digitalbank.ui.MainActivity"
function Resolve-AdbPath {
$candidates = @(
(Join-Path $env:LOCALAPPDATA "Android\Sdk\platform-tools\adb.exe"),
"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
)
foreach ($path in $candidates) {
if (Test-Path $path) { return $path }
}
$cmd = Get-Command adb -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
return $null
}
$adb = Resolve-AdbPath
if (-not $adb) {
Write-Host "adb not found." -ForegroundColor Red
exit 1
}
$devices = & $adb devices 2>&1 | Where-Object { $_ -match "\tdevice$" }
if (-not $devices) {
Write-Host "No authorized adb device." -ForegroundColor Red
exit 1
}
Write-Host "=== Launch MariBank SINGAPORE ===" -ForegroundColor Cyan
Write-Host "Package: $PkgSg (v3.2.2)"
Write-Host ""
Write-Host "[Required] After Xposed module update: LSPosed -> scope SG pkg -> re-optimize / force-stop / launch" -ForegroundColor Yellow
Write-Host " Without soft reboot, old hooks may cause BLANK-PAGE white screen." -ForegroundColor Yellow
Write-Host ""
Write-Host "Package: sg.com.maribankmobile.digitalbank (NOT ph.seabank.seabank)" -ForegroundColor Yellow
Write-Host ""
if ($StopPh) {
& $adb shell am force-stop $PkgPh | Out-Null
}
if ($ClearData) {
Write-Host "Clearing SG app data..."
& $adb shell pm clear $PkgSg | Out-Null
}
if ($ColdStart) {
Write-Host "Cold start: force-stop SG (first screen may stay white 30-60s)" -ForegroundColor Cyan
& $adb shell am force-stop $PkgSg | Out-Null
Start-Sleep -Seconds 2
} else {
Write-Host "Warm start (recommended). Use -ColdStart for cold start." -ForegroundColor Cyan
}
& $adb shell am start -n "$PkgSg/$Activity"
Write-Host ""
Write-Host "MariBank SG launched." -ForegroundColor Green
Write-Host "RN may show BLANK-PAGE for ~25-45s before welcome screen."
Write-Host "If white screen > 1 min: LSPosed soft reboot SG, then retry -ColdStart"
Write-Host "Verify API: adb logcat -d | Select-String api.maribank.com.sg"
if ($WaitSeconds -gt 0) {
Write-Host ""
Write-Host "Waiting ${WaitSeconds}s for UI..." -ForegroundColor Cyan
Start-Sleep -Seconds $WaitSeconds
& $adb shell uiautomator dump /sdcard/ui_launch_check.xml 2>&1 | Out-Null
$prevEap = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& $adb pull /sdcard/ui_launch_check.xml "$env:TEMP\ui_launch_check.xml" 2>&1 | Out-Null
$ErrorActionPreference = $prevEap
if (Test-Path "$env:TEMP\ui_launch_check.xml") {
$xml = Get-Content "$env:TEMP\ui_launch_check.xml" -Raw
$blank = $xml -match "BLANK-PAGE"
Write-Host ("BLANK-PAGE=" + $blank)
}
}

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|MariBankDfp|MariBankEncrypt|MariBankCapture" |
Select-String -Pattern "HTTP|outbound|register|dfp/v1|dfp is empty|3100012|4067|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,58 @@
# MariBank SG 一键:编译 → 安装模块 → 换 ID → 清数据 → 启动 SG
# 用法:
# .\scripts\maribank-sg-all-in.ps1
# .\scripts\maribank-sg-all-in.ps1 -DisableUsbDebug # 测前关 USB 调试(会断 adb
# .\scripts\maribank-sg-all-in.ps1 -SkipBuild # 仅换 ID + 启动
param(
[switch]$SkipBuild,
[switch]$DisableUsbDebug,
[switch]$KeepAdb,
[switch]$ColdStart
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
Write-Host "=== MariBank SG All-In ===" -ForegroundColor Cyan
if (-not $SkipBuild) {
Write-Host "`n[1/5] Build debug APKs..." -ForegroundColor Cyan
& "$ProjectRoot\scripts\build-debug.ps1"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "`n[2/5] Install APKs..." -ForegroundColor Cyan
& "$ProjectRoot\scripts\install-debug.ps1"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else {
Write-Host "`n[1-2/5] Skip build/install (-SkipBuild)" -ForegroundColor Yellow
}
Write-Host "`n[3/5] New device identity + clear MariBank SG..." -ForegroundColor Cyan
& "$ProjectRoot\scripts\maribank-spoof-device.ps1" -NewIdentity -ClearMariBank
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "`n[4/5] LSPosed 必做PC 无法代劳)" -ForegroundColor Yellow
Write-Host " 1. LSPosed -> 模块 -> notiMessage 启用"
Write-Host " 2. 作用域勾选: sg.com.maribankmobile.digitalbank"
Write-Host " 3. 对该包: 重新优化 -> 强行停止 -> 启动(等价软重启)"
Write-Host " 4. Shamiko DenyList 含 SG 全部进程Enforce=OFF"
Write-Host ""
Read-Host "完成 LSPosed 软重启后按 Enter 继续" | Out-Null
$launchArgs = @("-StopPh")
if ($ColdStart) { $launchArgs += "-ColdStart" }
Write-Host "`n[5/5] Launch MariBank SG..." -ForegroundColor Cyan
& "$ProjectRoot\scripts\launch-maribank-sg.ps1" @launchArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if ($DisableUsbDebug -and -not $KeepAdb) {
Write-Host "`n=== Disable USB debugging (SG stricter) ===" -ForegroundColor Cyan
& "$ProjectRoot\scripts\maribank-sg-register.ps1" -DisableUsbDebug
} else {
Write-Host "`n=== Ready to test ===" -ForegroundColor Green
Write-Host "Phone: Sign up -> +65 -> Next"
Write-Host "Log: .\scripts\maribank-sg-register.ps1 -CaptureLog -KeepAdb"
Write-Host " .\scripts\logcat-maribank.ps1"
Write-Host "Strict test (no adb): .\scripts\maribank-sg-all-in.ps1 -SkipBuild -DisableUsbDebug"
}

View File

@@ -0,0 +1,153 @@
# MariBank SG register test — new identity, optional log capture
param(
[switch]$NewIdentity,
[switch]$CaptureLog,
[switch]$DumpLog,
[switch]$InstallModule,
[switch]$DisableUsbDebug,
[switch]$KeepAdb,
[switch]$All
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$Pkg = "sg.com.maribankmobile.digitalbank"
function Resolve-AdbPath {
$candidates = @(
(Join-Path $env:LOCALAPPDATA "Android\Sdk\platform-tools\adb.exe"),
"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
)
foreach ($path in $candidates) {
if (Test-Path $path) { return $path }
}
$cmd = Get-Command adb -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
return $null
}
$adb = Resolve-AdbPath
if (-not $adb) {
Write-Host "adb not found. Install Android SDK platform-tools or add adb to PATH." -ForegroundColor Red
exit 1
}
function Get-AdbDevicesText {
& $adb devices 2>&1 | Out-String
}
function Test-AdbAuthorized {
$lines = & $adb devices 2>&1 | Where-Object { $_ -match "\tdevice$" }
return [bool]$lines
}
function Invoke-AdbShell([string]$cmd) {
& $adb shell $cmd 2>&1
}
function Show-AdbHelp {
param([string]$DevicesText)
Write-Host "`nadb devices output:" -ForegroundColor Yellow
Write-Host $DevicesText
Write-Host @"
1. USB
USB + 线
2. 线 / USB / MTP
3. USB
4. PC adb kill-server && adb start-server && adb devices
5. log .\scripts\maribank-sg-register.ps1 -CaptureLog -KeepAdb
"@ -ForegroundColor Cyan
}
Write-Host "=== MariBank SG Register Test ===" -ForegroundColor Cyan
Write-Host "adb: $adb"
if ($All) {
$allArgs = @()
if ($DisableUsbDebug -and -not $KeepAdb) { $allArgs += "-DisableUsbDebug" }
if ($KeepAdb) { $allArgs += "-KeepAdb" }
& "$ProjectRoot\scripts\maribank-sg-all-in.ps1" @allArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if (-not $CaptureLog) { exit 0 }
}
& $adb start-server 2>&1 | Out-Null
$devicesText = Get-AdbDevicesText
if (-not (Test-AdbAuthorized)) {
Write-Host "No authorized adb device." -ForegroundColor Red
Show-AdbHelp -DevicesText $devicesText
exit 1
}
Write-Host $devicesText
if ($NewIdentity -or $InstallModule) {
$spoofArgs = @()
if ($NewIdentity) { $spoofArgs += "-NewIdentity" }
if ($InstallModule) { $spoofArgs += "-InstallModule" }
& "$ProjectRoot\scripts\maribank-spoof-device.ps1" @spoofArgs -ClearMariBank
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
$shouldDisableAdb = $DisableUsbDebug -and -not $KeepAdb
if ($shouldDisableAdb) {
Write-Host "`n=== Disable USB / wireless debugging (SG stricter) ===" -ForegroundColor Cyan
Write-Host "WARNING: PC adb will disconnect after this. Re-enable USB debug on phone to connect again." -ForegroundColor Yellow
Invoke-AdbShell "settings put global adb_enabled 0" | Out-Null
Invoke-AdbShell "settings put global development_settings_enabled 0" | Out-Null
Invoke-AdbShell "settings put secure adb_wifi_enabled 0" | Out-Null
Invoke-AdbShell "su -c 'resetprop init.svc.adbd stopped; resetprop persist.sys.adb_enable 0'" 2>$null | Out-Null
} elseif (-not $KeepAdb) {
Write-Host "`n=== Skip disabling USB debug (default) ===" -ForegroundColor Cyan
Write-Host "Use -DisableUsbDebug when ready to test SG without PC adb; use -KeepAdb with -CaptureLog."
}
if (-not $CaptureLog) {
Write-Host "`n=== Clear SG app + verify IDs ===" -ForegroundColor Cyan
Invoke-AdbShell "pm clear $Pkg" | Out-Null
Invoke-AdbShell "su -c 'getprop ro.serialno; settings get secure android_id'"
}
Write-Host ""
Write-Host "=== LSPosed: pick the correct MariBank package ===" -ForegroundColor Yellow
Write-Host " SG: sg.com.maribankmobile.digitalbank (v3.2.2, api.maribank.com.sg)"
Write-Host " PH: ph.seabank.seabank (v3.22.0, api.seabank.ph)"
Write-Host " Launch SG: .\scripts\launch-maribank-sg.ps1"
Write-Host ""
Write-Host "Phone: LSPosed -> soft reboot $Pkg"
Write-Host "Then: MariBank SG -> Sign up -> +65 phone -> Next"
if ($CaptureLog -or $DumpLog) {
if ($CaptureLog) {
Write-Host ""
Write-Host "=== logcat cleared; tap Next then press Enter ===" -ForegroundColor Cyan
& $adb logcat -c
Write-Host "Tap Next on phone, then press Enter..."
Read-Host | Out-Null
} else {
Write-Host ""
Write-Host "=== dump current logcat (no clear) ===" -ForegroundColor Cyan
}
Write-Host ""
Write-Host "--- register / dfp / attestation ---" -ForegroundColor Cyan
$lines = & $adb logcat -d | Select-String "MariBankCapture|MariBankRegister|MariBankEncrypt|MariBankAttest|MariBankDfp|MariBankNative|MariBankRoot HTTP|ProbeGuard|3100012|4067012|OTP_SMS|register summary|dfp/v1|code=0|Gson REGISTRATION|uapi/v2/register|attestation hooks|native-core"
if ($lines) {
$lines
} else {
Write-Host "(no matches)" -ForegroundColor Yellow
Write-Host "Likely: Enter pressed before Next, or register API not sent yet."
Write-Host "You are on phone screen? Tap Next, wait for loading, then run:"
Write-Host " .\scripts\maribank-sg-register.ps1 -DumpLog -KeepAdb"
}
} else {
Write-Host "`nLog after Next:" -ForegroundColor Cyan
Write-Host " .\scripts\maribank-sg-register.ps1 -CaptureLog -KeepAdb"
Write-Host " .\scripts\maribank-sg-register.ps1 -DumpLog -KeepAdb # no clear, dump now"
}
Write-Host ""
Write-Host "Doc: docs/MariBank新加坡突破.md" -ForegroundColor Green

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

32
scripts/pull-tng-apk.ps1 Normal file
View File

@@ -0,0 +1,32 @@
# 从已安装 TNG 的手机 pull 完整 split APK供另一台 adb install-multiple
$ErrorActionPreference = "Stop"
$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
$pkg = "my.com.tngdigital.ewallet"
$outDir = Join-Path (Split-Path -Parent $PSScriptRoot) "reverse\dumps\tng_splits"
$paths = & $adb shell pm path $pkg 2>$null
if (-not $paths) {
Write-Host "设备未安装 $pkg" -ForegroundColor Red
Write-Host "请先在已装 TNG 的手机(如 Pixel 6上 USB 调试连接。"
exit 1
}
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
Remove-Item "$outDir\*.apk" -Force -ErrorAction SilentlyContinue
$i = 0
foreach ($line in $paths) {
if ($line -match "package:(.+)") {
$remote = $Matches[1].Trim()
$name = Split-Path $remote -Leaf
if ($name -eq "base.apk") { $local = Join-Path $outDir "base.apk" }
else { $local = Join-Path $outDir $name }
Write-Host "Pull $remote -> $local"
& $adb pull $remote $local
$i++
}
}
Write-Host "`n已 pull $i 个 APK 到 $outDir" -ForegroundColor Green
Write-Host "安装到另一台手机:"
Write-Host " adb install-multiple -r $outDir\*.apk"

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,48 @@
# MariBank SG Frida native attestation trace
# Usage:
# .\scripts\run-frida-sg-native.ps1
# .\scripts\run-frida-sg-native.ps1 -Spawn
param(
[switch]$Attach,
[switch]$SkipLsposedHint
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$FridaDir = Join-Path $ProjectRoot "reverse\frida"
$Py312 = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
$Adb = Join-Path $env:LOCALAPPDATA "Android\Sdk\platform-tools\adb.exe"
if (-not (Test-Path $Adb)) {
$Adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
}
Write-Host "=== MariBank SG Frida Native Trace ===" -ForegroundColor Cyan
Write-Host "Package: sg.com.maribankmobile.digitalbank"
Write-Host "Script: reverse\frida\trace_maribank_sg_native.js"
Write-Host ""
if (-not $SkipLsposedHint) {
Write-Host "[Required before trace]" -ForegroundColor Yellow
Write-Host " 1. LSPosed -> KEEP scope ENABLED for sg.com.maribankmobile.digitalbank"
Write-Host " (module bypasses ADB page; disabling scope shows ADB Detected screen)"
Write-Host " 2. Soft reboot SG app (force-stop then reopen)"
Write-Host " 3. frida-server running: .\scripts\install-frida.ps1 -StartServer"
Write-Host ""
Read-Host "Done? Press Enter to continue" | Out-Null
}
& $Adb devices
& $Adb shell "su -c 'pgrep frida-server || /data/local/tmp/frida-server -D &'" 2>&1 | Out-Null
Start-Sleep -Seconds 1
if (-not (Test-Path $Py312)) {
Write-Host "Python 3.12 not found at $Py312" -ForegroundColor Red
Write-Host "Run: .\scripts\install-frida.ps1"
exit 1
}
$mode = if ($Attach) { "attach" } else { "spawn" }
Write-Host "Mode: $mode (default spawn — open Sign up after app starts)" -ForegroundColor Cyan
Write-Host ""
& $Py312 (Join-Path $FridaDir "run_frida_sg_native.py") $mode

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,41 @@
# 启动 mitmproxy 抓 TNG Money Packet API需先: pip install mitmproxy
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$Addon = Join-Path $ProjectRoot "debug-server\tng_mmp_mitm_addon.py"
function Get-LanIp {
$ip = Get-NetIPAddress -AddressFamily IPv4 |
Where-Object { $_.IPAddress -notlike "127.*" -and $_.InterfaceAlias -notlike "*Loopback*" } |
Select-Object -First 1 -ExpandProperty IPAddress
if ($ip) { return $ip }
return "127.0.0.1"
}
$mitm = Get-Command mitmweb -ErrorAction SilentlyContinue
if (-not $mitm) {
$mitm = Get-Command mitmdump -ErrorAction SilentlyContinue
}
if (-not $mitm) {
Write-Host "未找到 mitmproxy。请先安装:" -ForegroundColor Yellow
Write-Host " pip install mitmproxy" -ForegroundColor Cyan
exit 1
}
$pcIp = Get-LanIp
Write-Host ""
Write-Host "=== TNG Money Packet 抓包 ===" -ForegroundColor Cyan
Write-Host "1. 手机与 PC 同一 WiFi" -ForegroundColor White
Write-Host "2. 手机 WiFi 代理: 手动 $pcIp 端口 8888" -ForegroundColor Green
Write-Host "3. 手机浏览器打开 http://mitm.it 安装证书 (Android)" -ForegroundColor White
Write-Host "4. TNG 登录 -> 进群 -> 打开红包 Leaderboard" -ForegroundColor White
Write-Host "5. 命中响应保存到 reverse/dumps/mitm_mmp/" -ForegroundColor White
Write-Host ""
Write-Host "若 TNG 报网络错误,可能是证书 pinning见 docs 说明。" -ForegroundColor Yellow
Write-Host ""
if ($mitm.Name -eq "mitmweb") {
Write-Host "Web UI: http://127.0.0.1:8081" -ForegroundColor Cyan
& mitmweb -s $Addon -p 8888 --web-host 127.0.0.1
} else {
& mitmdump -s $Addon -p 8888
}

View File

@@ -0,0 +1,20 @@
# TNG eWallet 全流程自动化:注册/登录 → 点区号 → 国家列表
# 依赖adb、设备已 Root + LSPosed + notiMessage xposed-module 已勾选 TNG
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
Set-Location $Root
Write-Host "== build xposed-module ==" -ForegroundColor Cyan
& .\gradlew :xposed-module:assembleDebug
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "== install ==" -ForegroundColor Cyan
adb install -r xposed-module\build\outputs\apk\debug\xposed-module-debug.apk
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
adb shell am force-stop my.com.tngdigital.ewallet
Start-Sleep -Seconds 3
Write-Host "== full flow test ==" -ForegroundColor Cyan
python reverse/scripts/test_tng_full_flow.py
exit $LASTEXITCODE

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

@@ -0,0 +1,51 @@
# TNG eWallet bypass: install xposed + LSPosed scope + optional clear app data
param(
[switch]$ClearTng,
[switch]$SkipBuild
)
$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)) { $adb = "adb" }
if (-not $SkipBuild) {
& "$ProjectRoot\scripts\build-debug.ps1"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
$xposedApk = Join-Path $ProjectRoot "xposed-module\build\outputs\apk\debug\xposed-module-debug.apk"
Write-Host "=== Install Xposed module ===" -ForegroundColor Cyan
& $adb install -r -t -g $xposedApk
if ($LASTEXITCODE -ne 0) {
& $adb install -r -t -g --bypass-low-target-sdk-block $xposedApk
}
Write-Host "=== Configure LSPosed scope (incl. TNG) ===" -ForegroundColor Cyan
$apkPath = (& $adb shell pm path com.miraclegarden.smsmessage.xposed) -replace '^package:', ''
$apkPath = $apkPath.Trim()
& $adb shell "su -c 'cp /data/adb/lspd/config/modules_config.db /sdcard/Download/modules_config.db; rm -f /data/adb/lspd/config/modules_config.db-wal /data/adb/lspd/config/modules_config.db-shm'"
$db = Join-Path $env:TEMP "modules_config_tng_finish.db"
& $adb pull /sdcard/Download/modules_config.db $db
python "$ProjectRoot\scripts\configure-lsposed.py" $db $apkPath
& $adb push $db /sdcard/Download/modules_config.db
& $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 "=== Phone checklist ===" -ForegroundColor Yellow
Write-Host " 1. TNG must NOT be in Magisk DenyList (DenyList blocks Vector/LSPosed injection)"
Write-Host " 2. MariBank/Seabank may stay on DenyList; Shamiko + ProcMaps hook hide root for them"
Write-Host " 3. LSPosed: notiMessage Xposed enabled, scope includes my.com.tngdigital.ewallet"
Write-Host " 4. LSPosed soft reboot / zygote restart after scope update"
Write-Host " 5. Disable USB debugging before test (recommended)"
if ($ClearTng) {
Write-Host "=== Clear TNG data ===" -ForegroundColor Cyan
& $adb shell "su -c 'pm clear my.com.tngdigital.ewallet'"
}
& $adb shell "am force-stop my.com.tngdigital.ewallet"
Write-Host ""
Write-Host "Done. Launch TNG eWallet, then run: .\scripts\logcat-tng.ps1" -ForegroundColor Green

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,25 @@
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";
public static final String SOURCE_XPOSED_TNG_MMP = "xposed_tng_mmp";
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,76 @@
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.TngMoneyPacketHook;
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);
TngMoneyPacketHook.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,308 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
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 probesInstalled = false;
private static volatile boolean loadClassWatcherInstalled = false;
private static final Set<String> HOOKED_ATTESTATION_CLASSES = new HashSet<>();
private MariBankAttestationHook() {
}
public static int installLate(XC_LoadPackage.LoadPackageParam lpparam) {
if (!probesInstalled) {
hookEnvironmentProbes(lpparam);
probesInstalled = true;
}
installLoadClassWatcher(lpparam);
RootBypassHelper.hookSecurityClass(lpparam, "com.shopee.bke.biz.base.risk.a");
int n = 0;
for (String className : MariBankRegionProfile.shpsAttestationCoreClasses(lpparam.packageName)) {
n += hookAttestationClass(lpparam, className);
}
n += hookKnownAttestationMethods(lpparam);
if (n > 0) {
XposedBridge.log(TAG + " attestation hooks=" + n
+ " region=" + MariBankRegionProfile.label(lpparam.packageName)
+ " classes=" + HOOKED_ATTESTATION_CLASSES.size());
}
return n;
}
private static void installLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
if (loadClassWatcherInstalled) {
return;
}
loadClassWatcherInstalled = true;
try {
XposedHelpers.findAndHookMethod(
ClassLoader.class,
"loadClass",
String.class,
boolean.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getThrowable() != null) {
return;
}
String name = (String) param.args[0];
if (name == null) {
return;
}
if (name.contains("shpssdk")
|| name.contains("CharacterCrypto")
|| name.contains("bke.biz.base.risk")) {
if (MariBankRegionProfile.isSingapore(lpparam.packageName)
&& MariBankRegionProfile.isShpssdkLegacyPackage(name)) {
return;
}
installLate(lpparam);
}
}
});
XposedBridge.log(TAG + " ClassLoader attestation watcher OK");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip ClassLoader watcher: " + t.getMessage());
}
}
/** 逆向确认的 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.wvvvuwwu", "vuwuuwvw");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuwvvw");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuwvu");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv", "wuvwuvwwu");
count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv", "wwvuwuwvu");
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) {
if (HOOKED_ATTESTATION_CLASSES.contains(className)) {
return 0;
}
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++;
}
}
if (count > 0) {
HOOKED_ATTESTATION_CLASSES.add(className);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip class " + className + ": " + t.getMessage());
}
return count;
}
private static boolean hookAttestationMethod(String className, Method method) {
final String methodName = method.getName();
final boolean isVuwuuwvw = "vuwuuwvw".equals(methodName);
Class<?> rt = method.getReturnType();
if (rt == String.class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
private byte[][] vuwuInputs;
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (!isVuwuuwvw) {
return;
}
vuwuInputs = new byte[param.args.length][];
for (int i = 0; i < param.args.length; i++) {
if (param.args[i] instanceof byte[]) {
byte[] b = (byte[]) param.args[i];
vuwuInputs[i] = b;
XposedBridge.log(TAG + " >> vuwuuwvw in" + i + " len=" + b.length);
}
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (!(result instanceof String)) {
return;
}
String s = (String) result;
if (isVuwuuwvw) {
MariBankAttestationJsonUtil.logVuwuuwvwCall(vuwuInputs, s);
}
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;
}
if (ProbeGuard.isBlockedCommand(pb.command())) {
param.setResult(ProbeGuard.fakeFailedProcess(
String.join(" ", pb.command())));
}
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " ProcessBuilder hook failed: " + t.getMessage());
}
}
}

View File

@@ -0,0 +1,251 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.util.Base64;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.robv.android.xposed.XposedBridge;
/**
* 解析 {@code vuwuuwvw} 返回的 obfuscated-key JSONkey 多为 8 位 hex
*/
final class MariBankAttestationJsonUtil {
private static final String TAG = "notiMessageHook/MariBankAttestJson";
private static final Pattern HEX_KEY = Pattern.compile("^[0-9a-fA-F]{8}$");
private static final Pattern SUSPICIOUS = Pattern.compile(
"(root|hook|xposed|lsposed|magisk|frida|substrate|emulator|debug|adb|"
+ "selinux|su\\b|/proc/|zygisk|riru|shamiko|tamper|integrity|"
+ "jailbreak|virtual|mock|proxy|vpn|developer)",
Pattern.CASE_INSENSITIVE);
private MariBankAttestationJsonUtil() {
}
static void logVuwuuwvwCall(byte[][] inputs, String output) {
if (inputs != null) {
for (int i = 0; i < inputs.length; i++) {
if (inputs[i] != null && inputs[i].length > 0) {
MariBankCaptureUtil.logBytes("vuwuuwvw.in" + i, inputs[i]);
}
}
}
if (output == null || output.isEmpty()) {
return;
}
boolean registerLike = isRegisterAttestationCall(inputs, output);
String label = registerLike ? "vuwuuwvw.out REGISTER" : "vuwuuwvw.out";
MariBankCaptureUtil.logText(label, output);
parseAndSummarize(output, registerLike);
}
static boolean isRegisterAttestationCall(byte[][] inputs, String output) {
if (inputs != null) {
for (byte[] in : inputs) {
if (in != null && in.length >= 200 && in.length <= 400) {
return true;
}
}
}
return output != null && output.length() >= 2200 && output.length() <= 2500;
}
static void parseAndSummarize(String json, boolean highlight) {
try {
JSONObject obj = new JSONObject(json);
List<String> keys = new ArrayList<>();
Iterator<String> it = obj.keys();
while (it.hasNext()) {
keys.add(it.next());
}
Collections.sort(keys);
XposedBridge.log(TAG + " keys=" + keys.size()
+ (highlight ? " [REGISTER-LIKE]" : "")
+ " sample=" + keys.subList(0, Math.min(6, keys.size())));
List<String> hits = new ArrayList<>();
for (String key : keys) {
Object val = obj.get(key);
scanValue(key, val, hits);
}
if (!hits.isEmpty()) {
XposedBridge.log(TAG + " SUSPICIOUS count=" + hits.size());
for (int i = 0; i < Math.min(hits.size(), 24); i++) {
XposedBridge.log(TAG + " " + hits.get(i));
}
} else {
XposedBridge.log(TAG + " no plain suspicious strings in values");
}
logKeyGuesses(keys);
} catch (Throwable t) {
XposedBridge.log(TAG + " parse fail: " + t.getMessage()
+ " head=" + json.substring(0, Math.min(120, json.length())));
}
}
private static void scanValue(String key, Object val, List<String> hits) {
if (val instanceof String) {
String s = (String) val;
noteIfSuspicious(key, "str", s, hits);
if (s.length() >= 8 && s.length() <= 512 && looksBase64(s)) {
byte[] decoded = tryBase64(s);
if (decoded != null) {
String inner = new String(decoded, StandardCharsets.UTF_8);
if (isMostlyPrintable(inner)) {
noteIfSuspicious(key, "b64utf8", inner, hits);
} else {
noteIfSuspicious(key, "b64hex", hexPreview(decoded, 32), hits);
}
}
}
return;
}
if (val instanceof Number || val instanceof Boolean) {
String s = String.valueOf(val);
if ("1".equals(s) || "true".equalsIgnoreCase(s)) {
hits.add(key + " =" + s + " (flag?)");
}
return;
}
if (val instanceof JSONObject) {
JSONObject nested = (JSONObject) val;
Iterator<String> it = nested.keys();
while (it.hasNext()) {
String nk = it.next();
try {
scanValue(key + "." + nk, nested.get(nk), hits);
} catch (Throwable ignored) {
}
}
return;
}
if (val instanceof JSONArray) {
JSONArray arr = (JSONArray) val;
for (int i = 0; i < arr.length(); i++) {
try {
scanValue(key + "[" + i + "]", arr.get(i), hits);
} catch (Throwable ignored) {
}
}
}
}
private static void noteIfSuspicious(String key, String kind, String text, List<String> hits) {
if (text == null || text.isEmpty()) {
return;
}
Matcher m = SUSPICIOUS.matcher(text);
if (m.find()) {
String show = text.length() > 120 ? text.substring(0, 120) + "..." : text;
hits.add(key + " " + kind + " hit=" + m.group().toLowerCase(Locale.ROOT) + " val=" + show);
}
}
private static void logKeyGuesses(List<String> keys) {
Set<String> candidates = knownFieldNames();
List<String> matched = new ArrayList<>();
for (String key : keys) {
if (!HEX_KEY.matcher(key).matches()) {
continue;
}
String lower = key.toLowerCase(Locale.ROOT);
for (String name : candidates) {
if (hashKey(name).equals(lower)) {
matched.add(key + "=>" + name);
}
}
}
if (!matched.isEmpty()) {
XposedBridge.log(TAG + " key guesses: " + matched);
}
}
private static Set<String> knownFieldNames() {
Set<String> names = new LinkedHashSet<>();
String[] base = {
"root", "hook", "xposed", "lsposed", "magisk", "frida", "adb", "debug",
"debuggable", "emulator", "simulator", "vpn", "proxy", "mock",
"selinux", "su", "supersu", "zygisk", "riru", "shamiko", "substrate",
"integrity", "safetynet", "playIntegrity", "deviceId", "androidId",
"serial", "fingerprint", "model", "brand", "manufacturer", "board",
"host", "tags", "type", "user", "display", "product", "hardware",
"usb", "wifi", "adb_enabled", "development_settings_enabled",
"RISK_ROOT", "RISK_HOOK", "RISK_USB_ADB", "RISK_WIFI_ADB", "RISK_ADB",
"RISK_EMULATOR", "RISK_DEBUG", "RISK_VPN", "RISK_PROXY", "RISK_MOCK",
"rdVerifyInfo", "deviceFingerprint", "data", "dataKey", "riskToken",
"isRoot", "isHook", "isDebug", "isAdb", "isEmulator", "isVirtual",
"tamper", "jailbreak", "bootloader", "verifiedbootstate", "vbmeta",
"init.svc.adbd", "/proc/self/maps", "RealInterceptorChain",
};
Collections.addAll(names, base);
return names;
}
private static String hashKey(String name) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] dig = md.digest(name.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 4; i++) {
sb.append(String.format(Locale.ROOT, "%02x", dig[i] & 0xff));
}
return sb.toString();
} catch (Throwable t) {
return "";
}
}
private static boolean looksBase64(String s) {
return s.matches("^[A-Za-z0-9+/=]+$");
}
private static byte[] tryBase64(String s) {
try {
return Base64.decode(s, Base64.DEFAULT);
} catch (Throwable t) {
return null;
}
}
private static boolean isMostlyPrintable(String s) {
if (s == null || s.isEmpty()) {
return false;
}
int ok = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c >= 0x20 && c < 0x7f) {
ok++;
}
}
return ok * 100 / s.length() >= 85;
}
private static String hexPreview(byte[] b, int max) {
if (b == null) {
return "";
}
int n = Math.min(b.length, max);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(String.format(Locale.ROOT, "%02x", b[i] & 0xff));
}
if (b.length > n) {
sb.append("...");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,139 @@
package com.miraclegarden.smsmessage.xposed.hook;
import android.util.Base64;
import java.nio.charset.StandardCharsets;
import de.robv.android.xposed.XposedBridge;
/**
* 分段 logcat 输出,避免单条超过 ~4KB 被截断。
*/
final class MariBankCaptureUtil {
static final String TAG = "notiMessageHook/MariBankCapture";
private static final int CHUNK = 3500;
private static final int HEX_PREVIEW = 96;
private MariBankCaptureUtil() {
}
static void logText(String section, String text) {
if (text == null) {
XposedBridge.log(TAG + " [" + section + "] null");
return;
}
if (text.length() <= CHUNK) {
XposedBridge.log(TAG + " [" + section + "] len=" + text.length() + " " + text);
return;
}
int parts = (text.length() + CHUNK - 1) / CHUNK;
XposedBridge.log(TAG + " [" + section + "] len=" + text.length() + " parts=" + parts);
for (int i = 0; i < parts; i++) {
int start = i * CHUNK;
int end = Math.min(start + CHUNK, text.length());
XposedBridge.log(TAG + " [" + section + "] " + (i + 1) + "/" + parts + " "
+ text.substring(start, end));
}
}
static void logBytes(String section, byte[] bytes) {
if (bytes == null) {
XposedBridge.log(TAG + " [" + section + "] null");
return;
}
String utf8 = tryUtf8(bytes);
if (isMostlyPrintable(utf8)) {
logText(section + " utf8", utf8);
} else {
XposedBridge.log(TAG + " [" + section + "] byte[" + bytes.length + "] hex="
+ hexPreview(bytes) + " b64=" + base64(bytes));
}
}
static void logArg(String section, Object arg) {
if (arg == null) {
XposedBridge.log(TAG + " [" + section + "] null");
return;
}
if (arg instanceof String) {
logText(section, (String) arg);
return;
}
if (arg instanceof byte[]) {
logBytes(section, (byte[]) arg);
return;
}
if (arg instanceof String[]) {
String[] arr = (String[]) arg;
XposedBridge.log(TAG + " [" + section + "] String[" + arr.length + "]");
for (int i = 0; i < arr.length; i++) {
logText(section + "[" + i + "]", arr[i]);
}
return;
}
if (arg instanceof byte[][]) {
byte[][] arr = (byte[][]) arg;
XposedBridge.log(TAG + " [" + section + "] byte[][] len=" + arr.length);
for (int i = 0; i < arr.length; i++) {
logBytes(section + "[" + i + "]", arr[i]);
}
return;
}
XposedBridge.log(TAG + " [" + section + "] " + arg.getClass().getName()
+ " = " + String.valueOf(arg));
}
static boolean isBankUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
String lower = url.toLowerCase();
return lower.contains("maribank.com")
|| lower.contains("seabank.ph")
|| lower.contains("/uapi/");
}
private static String tryUtf8(byte[] bytes) {
try {
return new String(bytes, StandardCharsets.UTF_8);
} catch (Throwable t) {
return "";
}
}
static boolean isMostlyPrintable(String s) {
if (s == null || s.isEmpty()) {
return false;
}
int printable = 0;
int sample = Math.min(s.length(), 512);
for (int i = 0; i < sample; i++) {
char c = s.charAt(i);
if (c == '\n' || c == '\r' || c == '\t' || (c >= 32 && c < 127)) {
printable++;
}
}
return printable * 100 / sample >= 85;
}
private static String hexPreview(byte[] bytes) {
int n = Math.min(bytes.length, HEX_PREVIEW);
StringBuilder sb = new StringBuilder(n * 2);
for (int i = 0; i < n; i++) {
sb.append(String.format("%02x", bytes[i] & 0xff));
}
if (bytes.length > n) {
sb.append("...");
}
return sb.toString();
}
private static String base64(byte[] bytes) {
try {
return Base64.encodeToString(bytes, Base64.NO_WRAP);
} catch (Throwable t) {
return "<b64 err>";
}
}
}

View File

@@ -0,0 +1,209 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
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;
/**
* DFPdevice fingerprint初始化诊断register 前若 dfp 为空会弹
* "The system is currently unavailable",且不会发 /uapi/v2/register。
*/
public final class MariBankDfpHook {
private static final String TAG = "notiMessageHook/MariBankDfp";
private static final Set<String> HOOKED_CLASSES = new HashSet<>();
private static volatile boolean loadClassWatcherInstalled = false;
private static volatile boolean registerVmHooked = false;
private MariBankDfpHook() {
}
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
hookRegisterViewModel(lpparam);
hookKnownDfpClasses(lpparam);
installLoadClassWatcher(lpparam);
}
private static void hookRegisterViewModel(XC_LoadPackage.LoadPackageParam lpparam) {
if (registerVmHooked) {
return;
}
String className = "com.shopee.bke.biz.user.viewmodel.RegisterViewModel";
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
registerVmHooked = true;
for (Method method : clazz.getDeclaredMethods()) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String args = param.args == null ? "[]" : Arrays.toString(param.args);
if (args.length() > 800) {
args = args.substring(0, 800) + "...";
}
XposedBridge.log(TAG + " RegisterViewModel."
+ method.getName() + " args=" + args);
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result == null) {
return;
}
String rs = String.valueOf(result);
if (rs.length() > 600) {
rs = rs.substring(0, 600) + "...";
}
if (rs.isEmpty()
|| rs.toLowerCase().contains("dfp")
|| rs.toLowerCase().contains("error")
|| rs.toLowerCase().contains("unavailable")
|| rs.contains("3100012")
|| rs.contains("4067")) {
XposedBridge.log(TAG + " RegisterViewModel."
+ method.getName() + " result=" + rs);
}
}
});
}
XposedBridge.log(TAG + " RegisterViewModel hooked methods="
+ clazz.getDeclaredMethods().length);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip RegisterViewModel: " + t.getMessage());
}
}
/** 启动时类可能已加载,先尝试已知命名。 */
private static void hookKnownDfpClasses(XC_LoadPackage.LoadPackageParam lpparam) {
String[] candidates = {
"com.shopee.bke.lib.jni.security.CharacterCryptoManager",
"com.shopee.bke.lib.jni.security.CharacterCryptoManagerWrapper",
"com.shopee.bke.lib.jni.utils.NativeEncryptUtilsWrapper",
};
for (String cn : candidates) {
tryHookDfpClass(lpparam, cn);
}
}
private static void installLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
if (loadClassWatcherInstalled) {
return;
}
loadClassWatcherInstalled = true;
try {
XposedHelpers.findAndHookMethod(
ClassLoader.class,
"loadClass",
String.class,
boolean.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getThrowable() != null) {
return;
}
String name = (String) param.args[0];
if (name == null) {
return;
}
if (name.contains("CharacterCrypto")
|| name.contains("NativeEncryptUtils")
|| (name.contains("dfp") && name.contains("bke"))) {
tryHookDfpClass(lpparam, name);
}
}
});
XposedBridge.log(TAG + " ClassLoader dfp watcher OK");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip ClassLoader watcher: " + t.getMessage());
}
}
private static void tryHookDfpClass(XC_LoadPackage.LoadPackageParam lpparam, String className) {
if (HOOKED_CLASSES.contains(className)) {
return;
}
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int count = 0;
for (Method method : clazz.getDeclaredMethods()) {
String mn = method.getName();
if (!mn.startsWith("getDfp") && !mn.contains("Fingerprint")) {
continue;
}
if (method.getReturnType() != String.class) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
String s = result instanceof String ? (String) result : null;
if (s == null || s.isEmpty()) {
XposedBridge.log(TAG + " EMPTY " + className + "."
+ method.getName() + " → register may fail");
} else {
String tail = s.length() > 80
? s.substring(0, 40) + "..." + s.substring(s.length() - 20)
: s;
XposedBridge.log(TAG + " " + className + "."
+ method.getName() + " len=" + s.length() + " val=" + tail);
}
}
});
count++;
}
if (count > 0) {
HOOKED_CLASSES.add(className);
XposedBridge.log(TAG + " hooked " + count + " dfp getters in " + className);
}
} catch (Throwable ignored) {
}
}
/** 拦截 dfp 空检查日志源(含 register scene 文案的类方法)。 */
public static void hookDfpEmptyGuards(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"android.util.Log",
lpparam.classLoader,
"e",
String.class,
String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String msg = param.args[1] instanceof String ? (String) param.args[1] : null;
if (msg == null) {
return;
}
String lower = msg.toLowerCase();
if (lower.contains("dfp is empty")
|| lower.contains("getdfp empty")
|| lower.contains("getdfp onerror")) {
XposedBridge.log(TAG + " Log.e: " + msg);
logStack();
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " skip Log.e dfp guard: " + t.getMessage());
}
}
private static void logStack() {
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
String cn = frame.getClassName();
if (cn.startsWith("com.shopee.bke") || cn.contains("CharacterCrypto")) {
XposedBridge.log(TAG + " at " + cn + "." + frame.getMethodName());
}
}
}
}

View File

@@ -0,0 +1,153 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.util.concurrent.atomic.AtomicInteger;
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 HTTP 全量抓包(仅 OkHttp 头 + 响应体)。
* 加密明文 / attestation 由 {@link MariBankSdkUtilsHook} 负责,勿在此重复 Hook Gson/native。
*/
public final class MariBankFullCaptureHook {
private static final String TAG = MariBankCaptureUtil.TAG;
private static final AtomicInteger SEQ = new AtomicInteger();
private static volatile boolean installed = false;
private MariBankFullCaptureHook() {
}
public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) {
if (installed) {
return;
}
installed = true;
int n = hookOkHttpRealCall(lpparam);
XposedBridge.log(TAG + " http capture hooks=" + n);
}
private static int hookOkHttpRealCall(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook hook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object request = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
if (request == null) {
return;
}
captureRequest(request);
} catch (Throwable t) {
XposedBridge.log(TAG + " req capture err: " + t.getMessage());
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (!"execute".equals(param.method.getName())) {
return;
}
try {
Object response = param.getResult();
if (response != null) {
captureResponse(response);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " resp capture err: " + t.getMessage());
}
}
};
int count = 0;
for (String className : new String[]{
"okhttp3.RealCall", "okhttp3.internal.connection.RealCall"
}) {
try {
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "execute", hook);
XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "enqueue",
"okhttp3.Callback", hook);
count += 2;
} catch (Throwable ignored) {
}
}
return count;
}
private static void captureRequest(Object request) {
Object urlObj = XposedHelpers.callMethod(request, "url");
String url = urlObj != null ? String.valueOf(urlObj) : "?";
if (!MariBankCaptureUtil.isBankUrl(url)) {
return;
}
int seq = SEQ.incrementAndGet();
String method = (String) XposedHelpers.callMethod(request, "method");
XposedBridge.log(TAG + " >>> #" + seq + " " + method + " " + url);
logHeaders("reqHdr #" + seq, XposedHelpers.callMethod(request, "headers"));
}
private static void captureResponse(Object response) {
try {
Object request = XposedHelpers.callMethod(response, "request");
Object urlObj = request != null ? XposedHelpers.callMethod(request, "url") : null;
String url = urlObj != null ? String.valueOf(urlObj) : "?";
if (!MariBankCaptureUtil.isBankUrl(url)) {
return;
}
int code = (int) XposedHelpers.callMethod(response, "code");
XposedBridge.log(TAG + " <<< HTTP " + code + " " + url);
logHeaders("respHdr", XposedHelpers.callMethod(response, "headers"));
try {
Object peek = XposedHelpers.callMethod(response, "peekBody", 256L * 1024L);
if (peek != null) {
Object ctObj = XposedHelpers.callMethod(peek, "contentType");
String contentType = ctObj != null ? String.valueOf(ctObj).toLowerCase() : "";
if (contentType.contains("pdf") || contentType.contains("octet-stream")) {
long size = 0L;
try {
size = (long) XposedHelpers.callMethod(peek, "contentLength");
} catch (Throwable ignored) {
}
XposedBridge.log(TAG + " <<< respBody skipped binary ct="
+ contentType + " len=" + size + " " + url);
} else {
String body = (String) XposedHelpers.callMethod(peek, "string");
if (body != null && !body.isEmpty()) {
if (body.length() > 512
&& !MariBankCaptureUtil.isMostlyPrintable(body)) {
XposedBridge.log(TAG + " <<< respBody skipped non-text len="
+ body.length() + " " + url);
} else {
MariBankCaptureUtil.logText("respBody", body);
}
}
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " peekBody err: " + t.getMessage());
}
} catch (Throwable t) {
XposedBridge.log(TAG + " captureResponse err: " + t.getMessage());
}
}
private static void logHeaders(String section, Object headers) {
if (headers == null) {
return;
}
try {
int size = (int) XposedHelpers.callMethod(headers, "size");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
String name = (String) XposedHelpers.callMethod(headers, "name", i);
String value = (String) XposedHelpers.callMethod(headers, "value", i);
sb.append(name).append(": ").append(value).append("\n");
}
MariBankCaptureUtil.logText(section, sb.toString().trim());
} catch (Throwable t) {
XposedBridge.log(TAG + " [" + section + "] headers err: " + t.getMessage());
}
}
}

View File

@@ -0,0 +1,123 @@
package com.miraclegarden.smsmessage.xposed.hook;
/**
* PH vs SG MariBank 差异配置(基于 reverse/apks 扫描,非「换包名即相同」)。
* <p>
* SG 3.2.2dex 内无 {@code com.shopee.shpssdk.*}、无 {@code safemode.b/catchs.a/util.c}、
* 无 {@code jni.utils.d};风控仅 {@code shpssdkbank}。
*/
public final class MariBankRegionProfile {
private MariBankRegionProfile() {
}
public static boolean isSingapore(String packageName) {
return MariBankRootBypassHook.PACKAGE_SG.equals(packageName);
}
public static boolean isPhilippines(String packageName) {
return MariBankRootBypassHook.PACKAGE.equals(packageName);
}
public static String label(String packageName) {
if (isSingapore(packageName)) {
return "SG";
}
if (isPhilippines(packageName)) {
return "PH";
}
return "?";
}
/** PH SafeMode boolean 类SG base/split dex 均无,勿 findClass。 */
public static String[] safeModeBooleanClasses(String packageName) {
if (isPhilippines(packageName)) {
return new String[]{
"com.shopee.bke.lib.safemode.b",
"com.shopee.bke.lib.safemode.catchs.a",
"com.shopee.bke.lib.safemode.util.c",
};
}
return new String[0];
}
/** 两区共有或 SG 专用 risk 类classes3.dex。 */
public static String[] sharedRiskClasses() {
return new String[]{"com.shopee.bke.biz.base.risk.a"};
}
/** SHPSSDK Java 入口SG 仅 bank 包。 */
public static String[] shpsSdkClasses(String packageName) {
if (isSingapore(packageName)) {
return new String[]{"com.shopee.shpssdkbank.SHPSSDK"};
}
return new String[]{
"com.shopee.shpssdk.SHPSSDK",
"com.shopee.shpssdkbank.SHPSSDK",
};
}
public static String[] shpsAssessRiskClasses(String packageName) {
if (isSingapore(packageName)) {
return new String[]{"com.shopee.shpssdkbank.SPSAssessRisk"};
}
return new String[]{
"com.shopee.shpssdkbank.SPSAssessRisk",
"com.shopee.shpssdk.SPSAssessRisk",
};
}
public static String[] shpsCallbackAdapterClasses(String packageName) {
if (isSingapore(packageName)) {
return new String[]{"com.shopee.shpssdkbank.SPSCallbackAdapter"};
}
return new String[]{
"com.shopee.shpssdk.SPSCallbackAdapter",
"com.shopee.shpssdkbank.SPSCallbackAdapter",
};
}
/** attestation / native-coreSG 无 com.shopee.shpssdk.wvvvuwwu。 */
public static String[] shpsAttestationCoreClasses(String packageName) {
if (isSingapore(packageName)) {
return new String[]{
"com.shopee.shpssdkbank.wvvvuwwu",
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
"com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv",
"com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
"com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw",
};
}
return new String[]{
"com.shopee.shpssdkbank.wvvvuwwu",
"com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
"com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv",
"com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
"com.shopee.shpssdk.wvvvuwwu",
};
}
/** 加密 JNIPH 有 utils.d 包装SG 仅 uvwuvwuv + uvwwwwuvclasses6.dex。 */
public static boolean hasEncryptWrapperD(String packageName) {
return isPhilippines(packageName);
}
public static boolean hasEncryptHelperUvwwwwuv(String packageName) {
return isSingapore(packageName);
}
/** late hooks 完成条件SG 不依赖 PH safemode booleanHooks。 */
public static boolean isLateHooksReady(
String packageName, int booleanHooks, int encryptHooks, int attestHooks) {
if (isSingapore(packageName)) {
return encryptHooks > 0 || attestHooks > 0;
}
return booleanHooks > 0 || encryptHooks > 0 || attestHooks > 0;
}
public static boolean isShpssdkLegacyPackage(String className) {
return className != null
&& className.startsWith("com.shopee.shpssdk.")
&& !className.startsWith("com.shopee.shpssdkbank.");
}
}

View File

@@ -0,0 +1,110 @@
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");
}
logRegisterSummary(out);
return out;
}
private static void logRegisterSummary(String json) {
if (json == null) {
return;
}
String cy = extractJsonString(json, "cyCode");
String scene = extractJsonString(json, "scene");
String step = extractJsonString(json, "step");
Matcher fp = DEVICE_FINGERPRINT.matcher(json);
String fpTail = fp.find() ? MariBankRiskTokenUtil.tail(fp.group(1)) : "?";
int dataLen = lengthOfJsonString(json, "\"data\"");
int dataKeyLen = lengthOfJsonString(json, "\"dataKey\"");
XposedBridge.log(TAG + " register summary cy=" + cy + " scene=" + scene
+ " step=" + step + " fpTail=" + fpTail
+ " dataLen=" + dataLen + " dataKeyLen=" + dataKeyLen);
}
private static String extractJsonString(String json, String key) {
Pattern p = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*\"([^\"]*)\"");
Matcher m = p.matcher(json);
return m.find() ? m.group(1) : "?";
}
private static int lengthOfJsonString(String json, String key) {
int idx = json.indexOf(key);
if (idx < 0) {
return -1;
}
int start = json.indexOf('"', idx + key.length());
if (start < 0) {
return -1;
}
int end = json.indexOf('"', start + 1);
if (end < 0) {
return -1;
}
return end - start - 1;
}
}

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,2085 @@
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.Handler;
import android.os.Looper;
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.HashSet;
import java.util.List;
import java.util.Set;
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[] 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 static String[] booleanHookTargets(XC_LoadPackage.LoadPackageParam lpparam) {
String[] ph = MariBankRegionProfile.safeModeBooleanClasses(lpparam.packageName);
String[] shared = MariBankRegionProfile.sharedRiskClasses();
String[] out = new String[ph.length + shared.length];
System.arraycopy(ph, 0, out, 0, ph.length);
System.arraycopy(shared, 0, out, ph.length, shared.length);
return out;
}
private MariBankRootBypassHook() {
}
private static volatile boolean deferredHooksInstalled = false;
private static volatile boolean lateHooksComplete = false;
private static volatile boolean lateHooksCoreInstalled = false;
private static volatile boolean lateHandlerRetriesDone = false;
private static volatile int lateHookAttempts = 0;
private static final int MAX_LATE_HOOK_ATTEMPTS = 10;
private static final Set<String> HOOKED_SAFE_MODE_CLASSES = new HashSet<>();
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!isTargetPackage(lpparam.packageName)) {
return;
}
XposedBridge.log(TAG + " install for " + lpparam.packageName
+ " region=" + MariBankRegionProfile.label(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);
scheduleLateAppHooks(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);
hookMainActivityForLateHooks(lpparam);
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);
scheduleLateAppHooks(lpparam);
}
}
/** SG split-dex 在 onCreate 时类可能未加载MainActivity 再试。 */
private static void hookMainActivityForLateHooks(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
"com.shopee.bke.digitalbank.ui.MainActivity",
lpparam.classLoader,
"onResume",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
scheduleLateAppHooks(lpparam);
}
}
);
} catch (Throwable t) {
XposedBridge.log(TAG + " skip MainActivity.onResume retry: " + t.getMessage());
}
}
private static void scheduleLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
tryInstallLateAppHooks(lpparam, false);
}
/** onCreate 之后补装SG 3.2.2 split dex 常需延迟重试。 */
private static void tryInstallLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
tryInstallLateAppHooks(lpparam, false);
}
private static void tryInstallLateAppHooks(
XC_LoadPackage.LoadPackageParam lpparam, boolean fromSplitDex) {
if (lateHooksComplete) {
return;
}
if (!lateHooksCoreInstalled) {
lateHooksCoreInstalled = true;
hookShpsRisk(lpparam);
hookShpsToken(lpparam);
hookErrorFlowLogging(lpparam);
MariBankFullCaptureHook.installLate(lpparam);
}
int booleanHooks = 0;
for (String className : booleanHookTargets(lpparam)) {
booleanHooks += hookAllBooleanChecks(lpparam, className);
}
int encryptHooks = MariBankSdkUtilsHook.installLate(lpparam);
int attestHooks = MariBankAttestationHook.installLate(lpparam);
MariBankDfpHook.installLate(lpparam);
MariBankDfpHook.hookDfpEmptyGuards(lpparam);
if (MariBankRegionProfile.isLateHooksReady(
lpparam.packageName, booleanHooks, encryptHooks, attestHooks)) {
lateHooksComplete = true;
XposedBridge.log(TAG + " late app hooks ready region="
+ MariBankRegionProfile.label(lpparam.packageName)
+ " booleanHooks=" + booleanHooks
+ " encryptHooks=" + encryptHooks + " attestHooks=" + attestHooks
+ " attempts=" + lateHookAttempts
+ (fromSplitDex ? " viaSplitDex" : ""));
return;
}
if (fromSplitDex) {
XposedBridge.log(TAG + " split-dex class loaded, safemode still pending encryptHooks="
+ encryptHooks + " attestHooks=" + attestHooks);
return;
}
if (lateHandlerRetriesDone) {
return;
}
lateHookAttempts++;
if (lateHookAttempts >= MAX_LATE_HOOK_ATTEMPTS) {
lateHandlerRetriesDone = true;
XposedBridge.log(TAG + " late handler retries done (split-dex may load later) attempts="
+ lateHookAttempts + " encryptHooks=" + encryptHooks
+ " attestHooks=" + attestHooks);
return;
}
long delay = Math.min(400L * lateHookAttempts, 2500L);
XposedBridge.log(TAG + " late hooks retry #" + lateHookAttempts + " in " + delay + "ms");
new Handler(Looper.getMainLooper()).postDelayed(
() -> tryInstallLateAppHooks(lpparam, false), delay);
}
private static boolean isLateHookTargetClass(String name, String packageName) {
if (name == null) {
return false;
}
for (String cn : MariBankRegionProfile.safeModeBooleanClasses(packageName)) {
if (cn.equals(name)) {
return true;
}
}
for (String cn : MariBankRegionProfile.sharedRiskClasses()) {
if (cn.equals(name)) {
return true;
}
}
for (String prefix : ERROR_FLOW_CLASSES) {
if (name.equals(prefix) || name.startsWith(prefix.substring(0, prefix.lastIndexOf('.') + 1))) {
return true;
}
}
return name.startsWith("com.shopee.bke.lib.safemode.")
|| name.equals("com.shopee.bke.lib.jni.utils.uvwuvwuv")
|| name.equals("com.shopee.bke.lib.jni.utils.uvwwwwuv")
|| (MariBankRegionProfile.hasEncryptWrapperD(packageName)
&& name.equals("com.shopee.bke.lib.jni.utils.d"))
|| name.startsWith("com.shopee.shpssdkbank.")
|| (!MariBankRegionProfile.isSingapore(packageName)
&& name.startsWith("com.shopee.shpssdk."));
}
/** SG split-dexsafemode / utils.d 延迟加载时在 loadClass 补装 Hook。 */
private static void hookClassLoaderSplitDex(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
ClassLoader.class,
"loadClass",
String.class,
boolean.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getThrowable() != null) {
return;
}
String name = (String) param.args[0];
if (isLateHookTargetClass(name, lpparam.packageName)) {
tryInstallLateAppHooks(lpparam, true);
}
}
}
);
XposedBridge.log(TAG + " ClassLoader.loadClass split-dex watcher OK");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip ClassLoader.loadClass watcher: " + t.getMessage());
}
}
private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) {
if (deferredHooksInstalled) {
return;
}
deferredHooksInstalled = true;
hookSafeModeDialog(lpparam);
hookRootDialogBlock(lpparam);
hookAdbBypass(lpparam);
hookNetworkLogging(lpparam);
hookClassLoaderSplitDex(lpparam);
XposedBridge.log(TAG + " early app hooks installed");
}
private static int hookAllBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam, String className) {
if (HOOKED_SAFE_MODE_CLASSES.contains(className)) {
return 0;
}
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) {
HOOKED_SAFE_MODE_CLASSES.add(className);
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;
for (String className : MariBankRegionProfile.shpsAssessRiskClasses(lpparam.packageName)) {
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";
for (String className : MariBankRegionProfile.shpsSdkClasses(lpparam.packageName)) {
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) {
}
for (String className : MariBankRegionProfile.shpsAssessRiskClasses(lpparam.packageName)) {
try {
XposedHelpers.findAndHookMethod(
className,
lpparam.classLoader,
"getType",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(0);
}
}
);
} catch (Throwable ignored) {
}
}
for (String className : MariBankRegionProfile.shpsCallbackAdapterClasses(lpparam.packageName)) {
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 url = CURRENT_REQUEST_URL.get();
if (!content.isEmpty() && url != null && MariBankCaptureUtil.isBankUrl(url)) {
MariBankCaptureUtil.logText("reqBody wire " + url, content);
}
String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
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);
}
}
// getInt(cr, key, def) — 部分 SDK 走三参数重载
if ("getInt".equals(target[1])) {
try {
XposedHelpers.findAndHookMethod(
clazz,
"getInt",
ContentResolver.class,
String.class,
int.class,
fakeDisabled);
} catch (Throwable ignored) {
}
}
} 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,313 @@
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 String WRAPPER = "com.shopee.bke.lib.jni.utils.d";
private static final String NATIVE_ENCRYPT = "com.shopee.bke.lib.jni.utils.uvwuvwuv";
private static final String SG_ENCRYPT_HELPER = "com.shopee.bke.lib.jni.utils.uvwwwwuv";
private static volatile boolean encryptHooksInstalled = false;
private static volatile boolean gsonHookInstalled = false;
private static volatile boolean encryptLoadClassWatcherInstalled = false;
private static volatile String installedForPackage;
private MariBankSdkUtilsHook() {
}
/** BkeApplication.onCreate 之后安装libsdkutils 已加载)。 */
public static int installLate(XC_LoadPackage.LoadPackageParam lpparam) {
installEncryptLoadClassWatcher(lpparam);
if (encryptHooksInstalled && lpparam.packageName.equals(installedForPackage)) {
return 0;
}
int n = 0;
if (MariBankRegionProfile.hasEncryptWrapperD(lpparam.packageName)) {
n += hookEncryptWrapper(lpparam);
}
n += hookNativeEncryptUtils(lpparam);
if (MariBankRegionProfile.hasEncryptHelperUvwwwwuv(lpparam.packageName)) {
n += hookEncryptHelperClass(lpparam, SG_ENCRYPT_HELPER, "uvwwwwuv");
}
if (n > 0) {
encryptHooksInstalled = true;
installedForPackage = lpparam.packageName;
}
if (!gsonHookInstalled) {
int g = hookGsonRegister(lpparam);
if (g > 0) {
gsonHookInstalled = true;
n += g;
}
}
if (n > 0) {
XposedBridge.log(TAG + " late encrypt hooks=" + n + " region="
+ MariBankRegionProfile.label(lpparam.packageName));
}
return n;
}
private static void installEncryptLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
if (encryptLoadClassWatcherInstalled) {
return;
}
encryptLoadClassWatcherInstalled = true;
final String pkg = lpparam.packageName;
try {
XposedHelpers.findAndHookMethod(
ClassLoader.class,
"loadClass",
String.class,
boolean.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getThrowable() != null) {
return;
}
String name = (String) param.args[0];
if (NATIVE_ENCRYPT.equals(name)
|| SG_ENCRYPT_HELPER.equals(name)
|| (MariBankRegionProfile.hasEncryptWrapperD(pkg)
&& WRAPPER.equals(name))) {
installLate(lpparam);
}
}
});
XposedBridge.log(TAG + " ClassLoader encrypt watcher OK");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip encrypt ClassLoader watcher: " + t.getMessage());
}
}
private static int hookEncryptHelperClass(
XC_LoadPackage.LoadPackageParam lpparam, String className, String logLabel) {
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, 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 + " >> " + logLabel + "." + 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]);
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result instanceof byte[]) {
logArg(" out", result);
} else if (result instanceof String) {
logArg(" out", result);
}
}
});
count++;
}
if (count > 0) {
XposedBridge.log(TAG + " hooked " + logLabel + " methods=" + count);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " skip " + logLabel + ": " + t.getMessage());
}
return count;
}
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)) {
MariBankCaptureUtil.logText("Gson REGISTRATION plaintext", 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)) {
MariBankCaptureUtil.logText(label, 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 < 8192) {
MariBankCaptureUtil.logBytes(label, bytes);
}
}
}
}

View File

@@ -0,0 +1,745 @@
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 nativeBridgeInstalled = false;
private static volatile boolean nativeLoadClassWatcherInstalled = false;
private static final Set<String> HOOKED_NATIVE_CORE_CLASSES = new HashSet<>();
/**
* 勿 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 (!nativeBridgeInstalled) {
nativeBridgeInstalled = true;
hookShpsNativeBridge(lpparam);
installNativeLoadClassWatcher(lpparam);
}
hookShpsNativeCore(lpparam);
}
private static void installNativeLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
if (nativeLoadClassWatcherInstalled) {
return;
}
nativeLoadClassWatcherInstalled = true;
try {
XposedHelpers.findAndHookMethod(
ClassLoader.class,
"loadClass",
String.class,
boolean.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getThrowable() != null) {
return;
}
String name = (String) param.args[0];
if (name != null && name.contains("shpssdk")) {
if (MariBankRegionProfile.isSingapore(lpparam.packageName)
&& MariBankRegionProfile.isShpssdkLegacyPackage(name)) {
return;
}
hookShpsNativeCore(lpparam);
}
}
});
XposedBridge.log(TAG + " ClassLoader native-core watcher OK");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip native ClassLoader watcher: " + t.getMessage());
}
}
/** 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 : MariBankRegionProfile.shpsSdkClasses(lpparam.packageName)) {
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) {
int total = 0;
for (String className : MariBankRegionProfile.shpsAttestationCoreClasses(lpparam.packageName)) {
total += hookNativeCoreClass(lpparam, className);
}
XposedBridge.log(TAG + " native-core total hooks=" + total + " region="
+ MariBankRegionProfile.label(lpparam.packageName));
}
private static int hookNativeCoreClass(
XC_LoadPackage.LoadPackageParam lpparam, String className) {
if (HOOKED_NATIVE_CORE_CLASSES.contains(className)) {
return 0;
}
int count = 0;
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : clazz.getDeclaredMethods()) {
if (!Modifier.isStatic(method.getModifiers())) {
continue;
}
Class<?> rt = method.getReturnType();
if (rt == String.class) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (!(result instanceof String)) {
return;
}
String s = (String) result;
if (s.length() > 80 && s.contains("|")) {
String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
if (!sanitized.equals(s)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " core String sanitized "
+ className + "#" + method.getName()
+ (Modifier.isNative(method.getModifiers()) ? " (native)" : ""));
}
} else if (s.length() > 40) {
String sanitized = MariBankRegisterPayloadUtil.isRegistrationPayload(s)
? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s)
: MariBankRiskTokenUtil.sanitizeAllInText(s);
if (!sanitized.equals(s)) {
param.setResult(sanitized);
XposedBridge.log(TAG + " core register String sanitized "
+ className + "#" + method.getName());
}
}
}
});
count++;
} else 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 + " core byte[] sanitized "
+ className + "#" + method.getName()
+ (Modifier.isNative(method.getModifiers()) ? " (native)" : ""));
}
}
});
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) {
HOOKED_NATIVE_CORE_CLASSES.add(className);
XposedBridge.log(TAG + " hooked " + count + " core methods 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,77 @@
package com.miraclegarden.smsmessage.xposed.hook;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Locale;
import de.robv.android.xposed.XposedBridge;
/**
* 拦截 root 探测命令时返回「空 / 失败」假进程,避免抛异常被 SHPSSDK 记为 tamper。
*/
public final class ProbeGuard {
private static final String TAG = "notiMessageHook/ProbeGuard";
private ProbeGuard() {
}
public static boolean isBlockedCommand(String command) {
if (command == null || command.isEmpty()) {
return false;
}
String lower = command.toLowerCase(Locale.US);
return lower.contains(" magisk")
|| lower.contains("/su")
|| lower.startsWith("su")
|| lower.contains("which su")
|| lower.contains("getprop ro.debuggable")
|| lower.contains("busybox")
|| lower.equals("su");
}
public static boolean isBlockedCommand(List<String> commands) {
if (commands == null || commands.isEmpty()) {
return false;
}
return isBlockedCommand(String.join(" ", commands));
}
public static Process fakeFailedProcess(String reason) {
XposedBridge.log(TAG + " fake probe process: " + reason);
return new FakeProcess();
}
private static final class FakeProcess extends Process {
@Override
public OutputStream getOutputStream() {
return OutputStream.nullOutputStream();
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(new byte[0]);
}
@Override
public InputStream getErrorStream() {
return new ByteArrayInputStream(new byte[0]);
}
@Override
public int waitFor() {
return 1;
}
@Override
public int exitValue() {
return 1;
}
@Override
public void destroy() {
}
}
}

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,579 @@
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 org.json.JSONArray;
import org.json.JSONObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
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 Money Packet群红包领取统计。
* 从 HTTP 响应 JSON / Gson 反序列化结果中提取 receiverList昵称 + 金额),转发到 notiMessage。
*/
public final class TngMoneyPacketHook {
private static final String TAG = "notiMessageHook/TngMmp";
private static final String PACKAGE = TngRootBypassHook.PACKAGE;
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 static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
private TngMoneyPacketHook() {
}
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
if (!TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
return;
}
hookOkHttpUrl(lpparam);
hookResponseBody(lpparam);
hookGsonFromJson(lpparam);
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
}
private static void hookOkHttpUrl(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 + " Request.Builder.build hook failed: " + t.getMessage());
}
}
private static void hookResponseBody(XC_LoadPackage.LoadPackageParam lpparam) {
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 (!isMmpPayload(url, body)) {
return;
}
forwardParsedJson(body, url, "http");
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " ResponseBody.string hook failed: " + t.getMessage());
}
}
private static void hookGsonFromJson(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook hook = new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object result = param.getResult();
if (result == null) {
return;
}
String className = result.getClass().getName();
if (!isMmpModelClass(className)) {
return;
}
try {
String json = buildJsonFromObject(result);
if (TextUtils.isEmpty(json)) {
return;
}
forwardParsedJson(json, className, "gson");
} catch (Throwable t) {
XposedBridge.log(TAG + " gson capture failed: " + t.getMessage());
}
}
};
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"fromJson",
String.class,
Class.class,
hook);
} catch (Throwable t) {
XposedBridge.log(TAG + " Gson.fromJson(Class) hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
"com.google.gson.Gson",
lpparam.classLoader,
"fromJson",
String.class,
java.lang.reflect.Type.class,
hook);
} catch (Throwable ignored) {
}
}
private static boolean isMmpPayload(String url, String body) {
if (TextUtils.isEmpty(body) || body.length() < 24) {
return false;
}
String lower = body.toLowerCase(Locale.US);
boolean jsonHit = lower.contains("receiverlist")
|| lower.contains("\"claimedamount\"")
|| lower.contains("mmpreceiver")
|| (lower.contains("nick") && lower.contains("amount") && lower.contains("mmp"));
if (jsonHit) {
return true;
}
if (TextUtils.isEmpty(url)) {
return false;
}
String urlLower = url.toLowerCase(Locale.US);
return urlLower.contains("mmp") || urlLower.contains("moneypacket");
}
private static boolean isMmpModelClass(String className) {
if (TextUtils.isEmpty(className)) {
return false;
}
return className.contains("MmpDetailResult")
|| className.contains("MmpClaimQueryResult")
|| className.contains("MmpClaimResult")
|| className.contains("MmpReceiver")
|| className.contains("MmpDetailLeaderboard");
}
private static void forwardParsedJson(String json, String sourceHint, String channel) {
MmpSnapshot snapshot = parseSnapshot(json);
if (snapshot == null || snapshot.claims.isEmpty()) {
return;
}
String dedupKey = snapshot.dedupKey();
if (!remember(dedupKey)) {
return;
}
Context context = getContext();
if (context == null) {
return;
}
String title = TextUtils.isEmpty(snapshot.title) ? "TNG Money Packet" : snapshot.title;
String content = snapshot.formatForForward(channel, sourceHint);
HookForwarder.forward(context, PACKAGE, title, content, HookBridge.SOURCE_XPOSED_TNG_MMP);
XposedBridge.log(TAG + " captured packet=" + snapshot.packetId
+ " claims=" + snapshot.claims.size() + " via " + channel);
}
private static MmpSnapshot parseSnapshot(String json) {
try {
JSONObject root = new JSONObject(json.trim());
return parseSnapshot(root, null);
} catch (Throwable t) {
return null;
}
}
private static MmpSnapshot parseSnapshot(JSONObject root, MmpSnapshot base) {
MmpSnapshot snapshot = base != null ? base : new MmpSnapshot();
fillMeta(root, snapshot);
JSONArray receiverList = findReceiverList(root);
if (receiverList != null) {
for (int i = 0; i < receiverList.length(); i++) {
JSONObject item = receiverList.optJSONObject(i);
if (item == null) {
continue;
}
ClaimLine line = parseClaimLine(item);
if (line != null) {
snapshot.claims.add(line);
}
}
}
Iterator<String> keys = root.keys();
while (keys.hasNext()) {
String key = keys.next();
Object val = root.opt(key);
if (val instanceof JSONObject) {
parseSnapshot((JSONObject) val, snapshot);
} else if (val instanceof JSONArray) {
JSONArray arr = (JSONArray) val;
for (int i = 0; i < arr.length(); i++) {
Object elem = arr.opt(i);
if (elem instanceof JSONObject) {
ClaimLine line = parseClaimLine((JSONObject) elem);
if (line != null && looksLikeClaimRow((JSONObject) elem)) {
snapshot.claims.add(line);
}
parseSnapshot((JSONObject) elem, snapshot);
}
}
}
}
return snapshot;
}
private static JSONArray findReceiverList(JSONObject obj) {
Iterator<String> keys = obj.keys();
while (keys.hasNext()) {
String key = keys.next();
Object val = obj.opt(key);
if ("receiverList".equalsIgnoreCase(key) && val instanceof JSONArray) {
return (JSONArray) val;
}
if (val instanceof JSONObject) {
JSONArray nested = findReceiverList((JSONObject) val);
if (nested != null) {
return nested;
}
}
}
return null;
}
private static void fillMeta(JSONObject obj, MmpSnapshot snapshot) {
putIfPresent(obj, snapshot, "packetId", "mmpId", "moneyPacketId", "id");
putIfPresent(obj, snapshot, "groupId", "chatId", "conversationId");
putIfPresent(obj, snapshot, "groupName", "chatName", "conversationName");
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName");
putIfPresent(obj, snapshot, "totalAmount", "packetAmount", "amount");
if (TextUtils.isEmpty(snapshot.title)) {
String merchant = firstNonEmpty(
obj.optString("merchantName", null),
obj.optString("shopName", null));
if (!TextUtils.isEmpty(merchant)) {
snapshot.title = merchant;
}
}
}
private static void putIfPresent(JSONObject obj, MmpSnapshot snapshot, String... keys) {
for (String key : keys) {
if (!obj.has(key)) {
continue;
}
String val = obj.optString(key, null);
if (TextUtils.isEmpty(val) || "null".equalsIgnoreCase(val)) {
continue;
}
if (key.toLowerCase(Locale.US).contains("packet") || key.equals("mmpId") || key.equals("id")) {
if (TextUtils.isEmpty(snapshot.packetId)) {
snapshot.packetId = val;
}
} else if (key.toLowerCase(Locale.US).contains("group")) {
if (TextUtils.isEmpty(snapshot.groupId)) {
snapshot.groupId = val;
}
} else if (key.toLowerCase(Locale.US).contains("name") && key.toLowerCase(Locale.US).contains("group")) {
snapshot.title = val;
} else if (key.toLowerCase(Locale.US).contains("sender")
|| key.toLowerCase(Locale.US).contains("operator")) {
if (TextUtils.isEmpty(snapshot.senderName)) {
snapshot.senderName = val;
}
} else if (key.toLowerCase(Locale.US).contains("total") || key.equals("amount")) {
if (TextUtils.isEmpty(snapshot.totalAmount)) {
snapshot.totalAmount = val;
}
}
}
}
private static boolean looksLikeClaimRow(JSONObject obj) {
String name = firstNonEmpty(
obj.optString("nickName", null),
obj.optString("displayName", null),
obj.optString("userName", null),
obj.optString("receiverName", null));
String amount = firstNonEmpty(
obj.optString("claimedAmount", null),
obj.optString("receiveAmount", null),
obj.optString("amount", null));
return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(amount);
}
private static ClaimLine parseClaimLine(JSONObject obj) {
String name = firstNonEmpty(
obj.optString("nickName", null),
obj.optString("displayName", null),
obj.optString("userName", null),
obj.optString("receiverName", null),
obj.optString("name", null));
String amount = firstNonEmpty(
obj.optString("claimedAmount", null),
obj.optString("receiveAmount", null),
obj.optString("amount", null));
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(amount)) {
return null;
}
ClaimLine line = new ClaimLine();
line.nickname = name;
line.amount = amount;
line.claimTime = firstNonEmpty(
obj.optString("claimTime", null),
obj.optString("claimedTime", null),
obj.optString("receiveTime", null));
return line;
}
private static String buildJsonFromObject(Object root) {
JSONObject json = objectToJson(root, new HashSet<Integer>(), 0);
return json != null ? json.toString() : null;
}
private static JSONObject objectToJson(Object obj, Set<Integer> visited, int depth) {
if (obj == null || depth > 6) {
return null;
}
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
JSONObject wrap = new JSONObject();
try {
wrap.put("value", obj);
} catch (Throwable ignored) {
}
return wrap;
}
int identity = System.identityHashCode(obj);
if (visited.contains(identity)) {
return null;
}
visited.add(identity);
JSONObject out = new JSONObject();
Class<?> clazz = obj.getClass();
if (clazz.isArray()) {
JSONArray arr = new JSONArray();
int len = Array.getLength(obj);
for (int i = 0; i < len; i++) {
Object elem = Array.get(obj, i);
JSONObject child = objectToJson(elem, visited, depth + 1);
if (child != null) {
arr.put(child);
}
}
try {
out.put("array", arr);
} catch (Throwable ignored) {
}
return out;
}
if (obj instanceof Iterable) {
JSONArray arr = new JSONArray();
for (Object elem : (Iterable<?>) obj) {
JSONObject child = objectToJson(elem, visited, depth + 1);
if (child != null) {
arr.put(child);
}
}
try {
out.put("receiverList", arr);
} catch (Throwable ignored) {
}
return out;
}
for (Field field : clazz.getDeclaredFields()) {
String fname = field.getName();
if (fname.contains("$") || fname.startsWith("CREATOR")) {
continue;
}
try {
field.setAccessible(true);
Object val = field.get(obj);
if (val == null) {
continue;
}
if (isPrimitiveLike(val)) {
out.put(fname, String.valueOf(val));
} else if (val instanceof Iterable || val.getClass().isArray()) {
JSONArray arr = new JSONArray();
if (val instanceof Iterable) {
for (Object elem : (Iterable<?>) val) {
putJsonValue(arr, elem, visited, depth + 1);
}
} else {
int len = Array.getLength(val);
for (int i = 0; i < len; i++) {
putJsonValue(arr, Array.get(val, i), visited, depth + 1);
}
}
out.put(fname, arr);
} else if (val.getClass().getName().startsWith("my.com.tngdigital")
|| val.getClass().getName().contains("Mmp")) {
JSONObject child = objectToJson(val, visited, depth + 1);
if (child != null) {
out.put(fname, child);
}
} else if (isPrimitiveLikeViaGetter(obj, fname)) {
out.put(fname, String.valueOf(val));
}
} catch (Throwable ignored) {
}
}
return out.length() > 0 ? out : null;
}
private static void putJsonValue(JSONArray arr, Object elem, Set<Integer> visited, int depth) {
if (elem == null) {
return;
}
if (isPrimitiveLike(elem)) {
arr.put(String.valueOf(elem));
return;
}
JSONObject child = objectToJson(elem, visited, depth);
if (child != null) {
arr.put(child);
}
}
private static boolean isPrimitiveLike(Object val) {
return val instanceof String
|| val instanceof Number
|| val instanceof Boolean
|| val instanceof Character;
}
private static boolean isPrimitiveLikeViaGetter(Object obj, String fieldName) {
try {
String suffix = fieldName.substring(0, 1).toUpperCase(Locale.US) + fieldName.substring(1);
for (String prefix : new String[]{"get", "is"}) {
try {
Method m = obj.getClass().getMethod(prefix + suffix);
Class<?> rt = m.getReturnType();
return rt == String.class || Number.class.isAssignableFrom(rt)
|| rt == boolean.class || rt == Boolean.class;
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return false;
}
private static boolean remember(String key) {
if (TextUtils.isEmpty(key)) {
return false;
}
if (RECENT_SET.contains(key)) {
return false;
}
RECENT_SET.add(key);
RECENT_KEYS.addLast(key);
while (RECENT_KEYS.size() > DEDUP_SIZE) {
String old = RECENT_KEYS.removeFirst();
RECENT_SET.remove(old);
}
return true;
}
private static String firstNonEmpty(String... values) {
if (values == null) {
return null;
}
for (String v : values) {
if (!TextUtils.isEmpty(v) && !"null".equalsIgnoreCase(v)) {
return v;
}
}
return null;
}
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;
}
private static final class MmpSnapshot {
String packetId;
String groupId;
String title;
String senderName;
String totalAmount;
final List<ClaimLine> claims = new ArrayList<>();
String dedupKey() {
StringBuilder sb = new StringBuilder();
sb.append(TextUtils.isEmpty(packetId) ? "?" : packetId).append('|');
for (ClaimLine line : claims) {
sb.append(line.nickname).append('=').append(line.amount).append(';');
}
return sb.toString();
}
String formatForForward(String channel, String sourceHint) {
StringBuilder sb = new StringBuilder();
sb.append("[MMP统计] ");
if (!TextUtils.isEmpty(packetId)) {
sb.append("packet=").append(packetId).append(' ');
}
if (!TextUtils.isEmpty(groupId)) {
sb.append("group=").append(groupId).append(' ');
}
if (!TextUtils.isEmpty(senderName)) {
sb.append("sender=").append(senderName).append(' ');
}
if (!TextUtils.isEmpty(totalAmount)) {
sb.append("total=").append(totalAmount).append(' ');
}
sb.append("via=").append(channel);
if (!TextUtils.isEmpty(sourceHint)) {
sb.append(" src=").append(sourceHint.length() > 120
? sourceHint.substring(0, 120) + "..." : sourceHint);
}
sb.append("\n");
for (ClaimLine line : claims) {
sb.append(line.nickname).append(" -> ").append(line.amount);
if (!TextUtils.isEmpty(line.claimTime)) {
sb.append(" (").append(line.claimTime).append(')');
}
sb.append('\n');
}
return sb.toString().trim();
}
}
private static final class ClaimLine {
String nickname;
String amount;
String claimTime;
}
}

View File

@@ -0,0 +1,2537 @@
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.view.Window;
import android.view.WindowManager;
import android.os.Message;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Collections;
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;
/**
* 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[] 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;
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",
"secauth",
};
private TngRootBypassHook() {
}
public static boolean isTargetPackage(String packageName) {
return PACKAGE.equals(packageName);
}
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());
hookEarlyAttachLog(lpparam);
hookConscryptStatsLogGuard();
// 登录优先:藏 root + 拦自杀 Java 链 + bl#a/b不碰 native-bridge 探测短路。
hookSplashForceLogin(lpparam);
hookLoginDismissSplash(lpparam);
// 点区号会走 onCountryClick → i7.l loading真 show → HWUI setName/gralloc abort
hookLoadingDialogSkip();
// setName 一律 noopA16+Zygisk 下 native setName→dlopen libandroid 易 ART abort
// 区号 Compose 必须 HW软件绘制会 IllegalArgumentException(hardware bitmaps)。
hookHardwareRendererSetNameNoop();
hookCallingCodeAllowHwSurface(lpparam);
RootBypassHelper.hookFileExists(lpparam);
RootBypassHelper.hookRuntimeExec(lpparam);
RootBypassHelper.hookSystemGetProperty(lpparam);
ProcMapsFilterHook.install(lpparam);
hookAntiSuicide();
hookPromonSuicideUpstream(lpparam);
hookUncaughtPromonException(lpparam);
hookKillApplicationHandler(lpparam);
hookBlockSecurityErrorLaunch(lpparam);
hookPromonNativeGuard(lpparam);
hookPromonLifecycle(lpparam);
hookActivityThreadExit(lpparam);
hookFinishAllActivityAndKillApp(lpparam);
hookSecurityUrlOpeners(lpparam);
hookJailBroken(lpparam);
hookJailBrokenRpc(lpparam);
hookAppSecurityManager(lpparam);
hookAppSecurityCallbacks(lpparam);
hookSecurityErrorActivity(lpparam);
// 首页强制 eKYC「验证您的帐户」— 测试期直接跳过
hookHomeEkycVerifySkip(lpparam);
// seccomp 开着时必须 stub TigerTally init否则 fork getprop 永不退出 → App.onCreate ANR
hookTigerTally(lpparam);
hookTigerTallyAppWrappers(lpparam);
XposedBridge.log(TAG + " login-first hooks armed");
}
/** 进程启动最早打点,便于确认 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 会取消旧 Runnable。 */
private static final Handler FORCE_LOGIN_HANDLER = new Handler(Looper.getMainLooper());
private static Runnable pendingForceLoginRunnable;
/** Splash 已离开(进 PIN/首页等)则取消救援。 */
private static volatile boolean splashNavigationDone = false;
/**
* 仅当 Splash 超时仍停在自身时才救援,避免已登录冷启动被强拉回 UserLogin。
* 有本地会话痕迹 → 优先 UserPin否则 → UserLogin。
*/
private static void scheduleSplashStuckRescue(
final Context appCtx, final Activity splashAct, final String reason, final long delayMs) {
if (appCtx == null) {
return;
}
splashNavigationDone = false;
if (pendingForceLoginRunnable != null) {
FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable);
}
pendingForceLoginRunnable = new Runnable() {
@Override
public void run() {
pendingForceLoginRunnable = null;
try {
if (splashNavigationDone) {
XposedBridge.log(TAG + " skip splash rescue (" + reason + ", already left)");
return;
}
if (isTopActivityRegistrationFlow(appCtx)) {
XposedBridge.log(TAG + " skip splash rescue (" + reason + ", registration flow)");
return;
}
String top = getTopActivityClassName(appCtx);
if (top != null && !isSplashActivityName(top)) {
XposedBridge.log(TAG + " skip splash rescue (" + reason + ", on " + top + ")");
splashNavigationDone = true;
return;
}
if (!isUiVisibleForForceLogin(appCtx)) {
XposedBridge.log(TAG + " skip splash rescue (" + reason + ", no visible UI / BAL)");
return;
}
boolean hasSession = hasLocalLoginSession(appCtx);
String target = hasSession
? "my.com.tngdigital.user.view.UserPinActivity"
: "my.com.tngdigital.user.view.UserLoginActivity";
Intent intent = new Intent();
intent.setClassName(PACKAGE, target);
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 + " splash stuck → " + shortActivityName(target)
+ " (" + reason + ", session=" + hasSession + ", from Splash)");
} else {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
appCtx.startActivity(intent);
XposedBridge.log(TAG + " splash stuck → " + shortActivityName(target)
+ " (" + reason + ", session=" + hasSession + ", from AppCtx)");
}
splashNavigationDone = true;
} catch (Throwable t) {
XposedBridge.log(TAG + " splash rescue failed (" + reason + "): " + t.getMessage());
}
}
};
FORCE_LOGIN_HANDLER.postDelayed(pendingForceLoginRunnable, delayMs);
}
private static String shortActivityName(String className) {
if (className == null) {
return "?";
}
int dot = className.lastIndexOf('.');
return dot >= 0 ? className.substring(dot + 1) : className;
}
private static boolean isSplashActivityName(String className) {
return className != null
&& (className.endsWith(".SplashActivity") || className.contains(".SplashActivity"));
}
private static String getTopActivityClassName(Context ctx) {
try {
ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return null;
}
for (ActivityManager.AppTask task : am.getAppTasks()) {
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
if (info == null || info.topActivity == null) {
continue;
}
if (PACKAGE.equals(info.topActivity.getPackageName())) {
return info.topActivity.getClassName();
}
}
} catch (Throwable ignored) {
}
return null;
}
/**
* 粗判本地是否已有登录痕迹(有则冷启动应走 PIN而不是登录页
* SharedPreferences 文件名/键含 session、token、user、pin、login 等即视为已登录。
*/
private static boolean hasLocalLoginSession(Context ctx) {
try {
File prefsDir = new File(ctx.getApplicationInfo().dataDir, "shared_prefs");
if (!prefsDir.isDirectory()) {
return false;
}
File[] files = prefsDir.listFiles();
if (files == null) {
return false;
}
for (File f : files) {
String name = f.getName().toLowerCase(Locale.US);
if (!name.endsWith(".xml")) {
continue;
}
if (name.contains("session") || name.contains("token") || name.contains("user")
|| name.contains("login") || name.contains("account")
|| name.contains("auth") || name.contains("pin")
|| name.contains("credential") || name.contains("wallet")) {
if (f.length() > 64) {
return true;
}
}
// 扫键名:任意 prefs 里出现登录相关 key
if (prefsXmlLooksLikeLoggedIn(f)) {
return true;
}
}
} catch (Throwable t) {
XposedBridge.log(TAG + " hasLocalLoginSession failed: " + t.getMessage());
}
return false;
}
private static boolean prefsXmlLooksLikeLoggedIn(File xmlFile) {
java.io.BufferedReader reader = null;
try {
reader = new java.io.BufferedReader(new java.io.FileReader(xmlFile));
String line;
int lines = 0;
while ((line = reader.readLine()) != null && lines < 200) {
lines++;
String lower = line.toLowerCase(Locale.US);
if ((lower.contains("name=\"") || lower.contains("name='"))
&& (lower.contains("token") || lower.contains("session")
|| lower.contains("userid") || lower.contains("user_id")
|| lower.contains("loginid") || lower.contains("mobile")
|| lower.contains("phonenumber") || lower.contains("islogin")
|| lower.contains("logged") || lower.contains("access_token"))) {
// 排除空值
if (lower.contains(">true<") || lower.contains("value=\"true\"")
|| (lower.contains("value=\"") && !lower.contains("value=\"\"")
&& !lower.contains("value=\"0\"") && !lower.contains("value=\"false\""))
|| (lower.contains(">") && lower.contains("</string>")
&& !lower.contains("><"))) {
return true;
}
if (lower.contains("<string") && lower.contains("</string>")) {
int a = lower.indexOf('>');
int b = lower.lastIndexOf("</string>");
if (a >= 0 && b > a + 1 && (b - a) > 8) {
return true;
}
}
if (lower.contains("<boolean") && lower.contains("value=\"true\"")) {
return true;
}
}
}
} catch (Throwable ignored) {
} finally {
if (reader != null) {
try {
reader.close();
} catch (Throwable ignored) {
}
}
}
return false;
}
/** 后台 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;
}
/**
* Zygisk/命名空间下 android.util.StatsLog native 常 UnsatisfiedLinkError
* Conscrypt TLS 指标线程一写就炸 → ART fatal。直接 noop 指标写入。
*/
private static void hookConscryptStatsLogGuard() {
int hooked = 0;
for (String className : new String[]{
"com.android.org.conscrypt.metrics.StatsLogImpl",
"com.android.org.conscrypt.metrics.ConscryptStatsLog",
"com.google.android.gms.org.conscrypt.metrics.StatsLogImpl",
}) {
try {
Class<?> clazz = Class.forName(className);
for (Method method : clazz.getDeclaredMethods()) {
String n = method.getName();
if (!n.startsWith("write")
&& !n.startsWith("report")
&& !n.startsWith("count")
&& !"startWriterThread".equals(n)) {
continue;
}
hookMethodNoopSilent(method);
hooked++;
}
} catch (Throwable ignored) {
}
}
try {
Class<?> statsLog = Class.forName("android.util.StatsLog");
for (Method method : statsLog.getDeclaredMethods()) {
String n = method.getName();
if ("loadNativeLibrary".equals(n) || n.startsWith("write")) {
hookMethodNoopSilent(method);
hooked++;
}
}
} catch (Throwable ignored) {
}
XposedBridge.log(TAG + " conscrypt/StatsLog guard hooked n=" + hooked);
}
/**
* 区号 Compose Activity 生命周期标记。
* 必须在 execStart/onCreate 前设为 true供 enableHardwareAcceleration 放行。
*/
private static volatile boolean callingCodeSurfaceActive = false;
private static boolean isCaptchaDialog(String owner) {
return owner != null
&& (owner.contains("CaptchaWebViewDialog") || owner.contains("com.aliyun.captcha"));
}
private static boolean isLoadingDialog(String owner) {
return owner != null
&& (owner.contains("i7.") || owner.contains("Loading") || owner.contains("Progress"));
}
private static boolean isCallingCodeActivity(String className) {
return className != null && className.contains("UserSearchCallingCodeActivity");
}
private static boolean isCallingCodeIntent(Intent intent) {
if (intent == null) {
return false;
}
if (intent.getComponent() != null) {
String cn = intent.getComponent().getClassName();
if (isCallingCodeActivity(cn)) {
return true;
}
}
String action = intent.getAction();
return action != null && action.contains("CallingCode");
}
private static String shortStack(int maxFrames) {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder();
int n = 0;
for (StackTraceElement el : stack) {
String cn = el.getClassName();
if (cn.startsWith("java.") || cn.startsWith("dalvik.")
|| cn.startsWith("android.os.") || cn.startsWith("de.robv.android.xposed")
|| cn.contains("TngRootBypassHook") || cn.contains("LSPosed")
|| cn.contains("XposedBridge")) {
continue;
}
if (n > 0) {
sb.append(" <- ");
}
sb.append(el.getClassName()).append("#").append(el.getMethodName())
.append(":").append(el.getLineNumber());
if (++n >= maxFrames) {
break;
}
}
return sb.length() == 0 ? "(empty)" : sb.toString();
}
/** 已 skip show 的 loading Dialog让 isShowing=true 避免业务层卡死。 */
private static final Set<Dialog> skippedLoadingDialogs =
Collections.newSetFromMap(new WeakHashMap<>());
/**
* i7.l 等 loading一律 skip show + 伪装 isShowing。
* Captcha 弹窗清 HW flag避免 Android 16 gralloc abort。
*/
private static void hookLoadingDialogSkip() {
try {
XposedHelpers.findAndHookMethod(Dialog.class, "show", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Dialog dialog = (Dialog) param.thisObject;
String owner = dialog.getClass().getName();
if (isLoadingDialog(owner)) {
skippedLoadingDialogs.add(dialog);
XposedBridge.log(TAG + " Dialog.show skip loading " + owner
+ " callingCode=" + callingCodeSurfaceActive);
param.setResult(null);
return;
}
if (isCaptchaDialog(owner)) {
try {
Window cw = dialog.getWindow();
if (cw != null) {
cw.clearFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
} catch (Throwable ignored) {
}
XposedBridge.log(TAG + " Dialog.show captcha software " + owner);
}
}
});
XposedBridge.log(TAG + " hooked Dialog.show (skip loading only)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(Dialog.class, "isShowing", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (skippedLoadingDialogs.contains(param.thisObject)) {
param.setResult(true);
}
}
});
XposedBridge.log(TAG + " hooked Dialog.isShowing (skipped loading)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Dialog.isShowing hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(Dialog.class, "dismiss", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String owner = param.thisObject.getClass().getName();
if (isLoadingDialog(owner)) {
skippedLoadingDialogs.remove(param.thisObject);
}
}
});
XposedBridge.log(TAG + " hooked Dialog.dismiss (loading cleanup)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Dialog.dismiss hook failed: " + t.getMessage());
}
}
/**
* 一律跳过 HardwareRenderer.setName。
* Pixel/A16 + Zygisk 命名空间下 native setName → dlopen("libandroid.so") 失败会 ART abort。
*/
private static void hookHardwareRendererSetNameNoop() {
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 (always noop)");
} catch (Throwable t) {
XposedBridge.log(TAG + " HardwareRenderer.setName noop failed: " + t.getMessage());
}
}
/**
* 区号 Compose标记 surface + 保证 HW flag勿清、勿拦 enableHW
* 软件绘制会崩Software rendering doesn't support hardware bitmaps。
*/
private static void hookCallingCodeAllowHwSurface(XC_LoadPackage.LoadPackageParam lpparam) {
XC_MethodHook markOn = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Intent intent = extractIntent(param.args);
if (isCallingCodeIntent(intent)) {
callingCodeSurfaceActive = true;
XposedBridge.log(TAG + " callingCode surface ON (HW path)");
}
}
};
try {
XposedHelpers.findAndHookMethod(
Activity.class, "startActivity", Intent.class, markOn);
XposedHelpers.findAndHookMethod(
Activity.class, "startActivity", Intent.class, Bundle.class, markOn);
} catch (Throwable t) {
XposedBridge.log(TAG + " startActivity callingCode mark failed: " + t.getMessage());
}
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 (isCallingCodeIntent(intent)) {
callingCodeSurfaceActive = true;
XposedBridge.log(TAG
+ " callingCode surface ON (execStart HW)");
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " execStart callingCode mark failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnCreate",
Activity.class,
Bundle.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
if (isCallingCodeActivity(activity.getClass().getName())) {
callingCodeSurfaceActive = true;
try {
Window window = activity.getWindow();
if (window != null) {
window.addFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
} catch (Throwable ignored) {
}
XposedBridge.log(TAG
+ " callingCode onCreate — keep HW for Compose");
}
}
});
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnDestroy",
Activity.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.args[0];
if (activity != null
&& isCallingCodeActivity(activity.getClass().getName())) {
callingCodeSurfaceActive = false;
XposedBridge.log(TAG + " callingCode surface OFF");
}
}
});
} catch (Throwable t) {
XposedBridge.log(TAG + " callingCode lifecycle mark failed: " + t.getMessage());
}
XposedBridge.log(TAG + " callingCode allow-HW surface armed");
}
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;
}
/**
* 强拉 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 卡住救援:不立刻强拉 Login。
* onCreate 只调度延迟检查;若已自行跳到 PIN/首页则取消。
* 超时仍停在 SplashPromon 堵死)才救援。
*/
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 (isSplashActivityName(name)) {
XposedBridge.log(TAG
+ " Splash.onCreate enter — schedule stuck rescue @4s");
scheduleSplashStuckRescue(
activity.getApplicationContext(),
activity,
"Splash/beforeOnCreate",
4000L);
return;
}
// 任何非 Splash Activity 创建 → 取消 Splash 救援
if (PACKAGE.equals(activity.getPackageName())) {
markSplashNavigationDone(name);
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
final Activity activity = (Activity) param.args[0];
if (activity == null) {
return;
}
String name = activity.getClass().getName();
if (!isSplashActivityName(name)) {
return;
}
try {
activity.reportFullyDrawn();
} catch (Throwable ignored) {
}
dismissSplashScreen(activity);
}
});
XposedHelpers.findAndHookMethod(
Instrumentation.class,
"callActivityOnResume",
Activity.class,
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 (PACKAGE.equals(activity.getPackageName())
&& !isSplashActivityName(name)) {
markSplashNavigationDone(name);
}
}
});
XposedBridge.log(TAG + " hooked Splash stuck-rescue (not always→Login)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Splash force hook failed: " + t.getMessage());
}
}
private static void markSplashNavigationDone(String activityName) {
if (splashNavigationDone) {
return;
}
splashNavigationDone = true;
if (pendingForceLoginRunnable != null) {
FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable);
pendingForceLoginRunnable = null;
XposedBridge.log(TAG + " cancel splash rescue — now on " + activityName);
}
}
private static volatile long lastSuicideLogAt = 0L;
private static volatile int suicideBlockCount = 0;
private static void hookAntiSuicide() {
XC_MethodHook blockSelfKill = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (!shouldBlockThisSuicideCall(param)) {
return;
}
lastBlockedSuicideAt = System.currentTimeMillis();
suicideBlockCount++;
long now = lastBlockedSuicideAt;
if (now - lastSuicideLogAt > 3000L) {
lastSuicideLogAt = now;
XposedBridge.log(TAG + " blocked suicide #" + suicideBlockCount
+ " " + param.method.getDeclaringClass().getSimpleName()
+ "#" + param.method.getName()
+ argsSummary(param.args)
+ " thread=" + Thread.currentThread().getName()
+ " stack=" + shortStack(6));
}
param.setResult(null);
// 非主线程:打断自杀循环线程,避免只拦 kill 却卡死 UI
freezeSuicideCallerThread();
}
};
try {
XposedHelpers.findAndHookMethod(Process.class, "killProcess", int.class, blockSelfKill);
} catch (Throwable t) {
XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(System.class, "exit", int.class, blockSelfKill);
} catch (Throwable t) {
XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(Runtime.class, "exit", int.class, blockSelfKill);
} catch (Throwable t) {
XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
}
try {
XposedHelpers.findAndHookMethod(Runtime.class, "halt", int.class, blockSelfKill);
} 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()) {
return;
}
if (signal != 9 && signal != 15 && signal != 6) {
return;
}
lastBlockedSuicideAt = System.currentTimeMillis();
suicideBlockCount++;
if (lastBlockedSuicideAt - lastSuicideLogAt > 3000L) {
lastSuicideLogAt = lastBlockedSuicideAt;
XposedBridge.log(TAG + " blocked sendSignal(self," + signal
+ ") #" + suicideBlockCount);
}
param.setResult(null);
freezeSuicideCallerThread();
}
});
} 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());
}
XposedBridge.log(TAG + " anti-suicide armed (rate-limited + freeze caller)");
}
private static boolean shouldBlockThisSuicideCall(XC_MethodHook.MethodHookParam param) {
// 真崩溃KillApplicationHandler放行避免僵尸进程卡 Splash/黑屏
if (stackHasClass("com.android.internal.os.RuntimeInit$KillApplicationHandler")) {
return false;
}
String name = param.method.getName();
if ("killProcess".equals(name)) {
return ((Integer) param.args[0]) == Process.myPid();
}
// System/Runtime.exit/halt一律拦TNG 正常退出极少走这里)
return true;
}
private static boolean stackHasClass(String className) {
try {
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
if (className.equals(e.getClassName())) {
return true;
}
}
} catch (Throwable ignored) {
}
return false;
}
private static String argsSummary(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(",");
}
sb.append(args[i]);
}
return sb.append(")").toString();
}
private static void freezeSuicideCallerThread() {
Thread t = Thread.currentThread();
if (t.getId() == Looper.getMainLooper().getThread().getId()) {
return;
}
// 栈里有 Promon 包才打断,避免误伤业务线程
if (!stackHasPromon()) {
return;
}
try {
t.interrupt();
} catch (Throwable ignored) {
}
}
private static boolean stackHasPromon() {
try {
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
if (isPromonPackageClass(e.getClassName())) {
return true;
}
}
} catch (Throwable ignored) {
}
return false;
}
/**
* 自杀链上游:只补 AppSecurityManager 杀进程路径 noop。
* 勿拦 Handler.post / 勿 short-circuit G/H 等:会弄死 ContentProvider 初始化Splash 僵尸卡住。
* bl/R/a 仍由 hookPromonNativeGuard 处理。
*/
private static void hookPromonSuicideUpstream(XC_LoadPackage.LoadPackageParam lpparam) {
hookAppSecurityKillPaths(lpparam);
XposedBridge.log(TAG + " promon suicide upstream armed (kill-path only)");
}
/** AppSecurityManager 里明确杀进程/退出的路径直接 noop勿匹配 destroy/shutdown。 */
private static void hookAppSecurityKillPaths(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 lower = method.getName().toLowerCase(Locale.US);
if (!lower.contains("kill")
&& !lower.contains("exit")
&& !lower.contains("force")
&& !lower.contains("die")
&& !lower.contains("suicide")
&& !lower.contains("terminate")) {
continue;
}
hookMethodNoopSilent(method);
hooked++;
}
XposedBridge.log(TAG + " AppSecurityManager kill-path noop n=" + hooked);
} catch (Throwable t) {
XposedBridge.log(TAG + " AppSecurityManager kill-path 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) || isHomeEkycVerifyIntent(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 isHomeEkycVerifyIntent(Intent intent) {
if (intent == null) {
return false;
}
if (intent.getComponent() != null) {
String cls = intent.getComponent().getClassName();
if (cls != null && cls.contains("HomeEkycVerify")) {
return true;
}
}
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。
* 实测 afterHook 仍跑 native → DeleteLocalRef 损坏 → ART Runtime abort (signal 6)。
* 登录优先onActivity* / onApplication* 全部 noop不调原 native。
*/
private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
for (String simple : new String[]{"w", "u"}) {
hookPromonLifecycleClass(lpparam, simple);
}
}
private static void hookPromonLifecycleClass(
XC_LoadPackage.LoadPackageParam lpparam,
String simpleName) {
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;
}
// 不调原 native避免 JNI DeleteLocalRef → ART abort
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
setSafeHookResult(param, method);
}
});
hooked++;
}
XposedBridge.log(TAG + " hooked " + hooked + " " + lifecycleName
+ " lifecycle (noop)");
} catch (Throwable t) {
XposedBridge.log(TAG + " skip lifecycle " + lifecycleName + ": " + 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) {
return;
}
if (isPromonThrowableName(t.getClass().getName())
|| isStatsLogNoise(t)) {
XposedBridge.log(TAG + " swallowed uncaught " + t.getClass().getSimpleName()
+ " in " + lpparam.processName
+ " msg=" + t.getMessage());
param.setResult(null);
}
}
});
XposedBridge.log(TAG + " hooked Thread.dispatchUncaughtException");
} catch (Throwable t) {
XposedBridge.log(TAG + " dispatchUncaughtException hook failed: " + t.getMessage());
}
}
/**
* 登录优先:只 short-circuit bl#a / bl#b文档有效路径。全拦 bl/R 六个方法会破坏
* u.onActivityCreated JNI → ART aborta.run 仍单独 noop。
*/
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()) {
String methodName = method.getName();
if (!"a".equals(methodName) && !"b".equals(methodName)) {
continue;
}
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), a/b short-circuit only");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " " + clazz.getName() + " short-circuit failed: "
+ t.getMessage());
}
}
/** 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 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 isStatsLogNoise(Throwable t) {
for (Throwable cur = t; cur != null; cur = cur.getCause()) {
String name = cur.getClass().getName();
String msg = cur.getMessage();
if (name != null && name.contains("StatsLog")) {
return true;
}
if (msg != null && msg.contains("StatsLog")) {
return true;
}
if (cur instanceof UnsatisfiedLinkError
&& msg != null
&& (msg.contains("stats") || msg.contains("Stats"))) {
return true;
}
}
return false;
}
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;
}
/**
* 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());
}
}
}
/**
* seccomp 开着时:必须 stub init/initCommon/genericNt1否则 fork getprop 卡死。
* seccomp 关且非登录优先:只 stub genericNt1 探测。
*/
private static boolean isTigerShortCircuit(String className, String methodName) {
if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className)
|| "com.aliyun.TigerTally.t.B".equals(className)
|| "com.aliyun.TigerTally.t.C".equals(className)) {
if ("init".equals(methodName) || "initCommon".equals(methodName)
|| "genericNt1".equals(methodName) || "initialize".equals(methodName)) {
return true;
}
}
return "com.aliyun.TigerTally.t.B".equals(className)
&& "genericNt1".equals(methodName);
}
/** TNG 封装层CaptchaInitializer / TigerTallyApiWrapper 直接 noop避免 Startup latch 卡住。 */
private static void hookTigerTallyAppWrappers(XC_LoadPackage.LoadPackageParam lpparam) {
String[] classes = {
"my.com.tngdigital.captcha.TigerTallyApiWrapper",
"my.com.tngdigital.app.launcher.initializer.CaptchaInitializer",
};
for (String className : classes) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int n = 0;
for (Method method : clazz.getDeclaredMethods()) {
String name = method.getName();
if (!"initialize".equals(name) && !"init".equals(name)
&& !"create".equals(name)) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " TigerTally wrapper SC "
+ className + "#" + method.getName());
setSafeHookResult(param, method);
}
});
n++;
}
XposedBridge.log(TAG + " hooked TigerTally wrapper " + className + " n=" + n);
} catch (Throwable t) {
XposedBridge.log(TAG + " TigerTally wrapper " + className
+ " skipped: " + t.getMessage());
}
}
}
/** 拦截 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());
}
}
}
/** 消费 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());
}
}
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
}
}
/** 高频回调(如 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) || op.contains("phoneCheck"))) {
XposedBridge.log(TAG + " RPC req 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);
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
String op = extractRpcOperation(param.args);
if (op == null || (!isRegistrationRpc(op) && !op.contains("phoneCheck"))) {
return;
}
if (param.hasThrowable()) {
XposedBridge.log(TAG + " RPC rsp op=" + op + " err "
+ param.getThrowable().getClass().getSimpleName()
+ ": " + param.getThrowable().getMessage());
return;
}
Object result = param.getResult();
String snippet = result != null ? String.valueOf(result) : "null";
if (snippet.length() > 800) {
snippet = snippet.substring(0, 800) + "...";
}
XposedBridge.log(TAG + " RPC rsp op=" + op + " body=" + snippet);
}
};
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 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)");
}
/**
* 首页强制「验证您的帐户」(HomeEkycVerifyActivity) — 测试期跳过。
* 拦启动 + onCreate finishcanBypassEkyc=trueenforceEkyc=false。
*/
private static void hookHomeEkycVerifySkip(XC_LoadPackage.LoadPackageParam lpparam) {
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 != null && name.contains("HomeEkycVerify")) {
XposedBridge.log(TAG + " skip eKYC — finish " + name);
activity.finish();
}
}
});
XposedBridge.log(TAG + " hooked HomeEkycVerify Activity finish");
} catch (Throwable t) {
XposedBridge.log(TAG + " HomeEkycVerify finish hook failed: " + t.getMessage());
}
// 白名单绕过:返回 true
hookBooleanMethodsByName(lpparam,
"my.com.tngdigital.home.viewmodel.BypassEkycWhiteListChecker",
true, "canBypassEkyc", "bypassEkyc", "isWhitelist", "inWhitelist");
// 强制 eKYC 开关:返回 false
for (String className : new String[]{
"my.com.tngdigital.home.help.HomeEkycCheckHelper",
"my.com.tngdigital.home.ekyc.KycHomepagePopUpManager",
"my.com.tngdigital.home.ekyc.HomeEkycVerifyViewModel",
"my.com.tngdigital.home.viewmodel.HomeListActivityViewModel",
}) {
hookBooleanMethodsByName(lpparam, className, false,
"getEnforceEkyc", "enforceEkyc", "needShowEkyc", "needForceEkyc",
"isForceEkyc", "checkEkyc", "firstCheckEkyc", "needShowEkycCddAudit");
hookVoidMethodsByName(lpparam, className,
"checkEkyc", "checkEkycStatus", "checkEkycRequest",
"requestEkycStatus", "showEkyc", "launchEkyc", "openEkyc");
}
}
private static void hookBooleanMethodsByName(
XC_LoadPackage.LoadPackageParam lpparam,
String className,
boolean result,
String... nameHints) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (method.getReturnType() != boolean.class && method.getReturnType() != Boolean.class) {
continue;
}
String n = method.getName();
boolean match = false;
for (String hint : nameHints) {
if (n.equals(hint) || n.toLowerCase(Locale.US).contains(hint.toLowerCase(Locale.US))) {
match = true;
break;
}
}
if (!match) {
continue;
}
final boolean ret = result;
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(ret);
}
});
hooked++;
}
XposedBridge.log(TAG + " eKYC bool stub " + className + " n=" + hooked + " -> " + result);
} catch (Throwable t) {
XposedBridge.log(TAG + " eKYC bool stub skip " + className + ": " + t.getMessage());
}
}
private static void hookVoidMethodsByName(
XC_LoadPackage.LoadPackageParam lpparam,
String className,
String... nameHints) {
try {
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (method.getReturnType() != void.class) {
continue;
}
String n = method.getName();
boolean match = false;
for (String hint : nameHints) {
if (n.equals(hint) || n.startsWith(hint)) {
match = true;
break;
}
}
if (!match) {
continue;
}
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(null);
}
});
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " eKYC void noop " + className + " n=" + hooked);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " eKYC void noop skip " + className + ": " + t.getMessage());
}
}
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>