diff --git a/AGENTS.md b/AGENTS.md index a25d169..fa71244 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,9 @@ Android 应用,监听通知栏消息并通过 **双通道**(通知监听 + Xposed Hook)抓取内容,支持 PC 本地调试台转发。 -> **Hook 架构与扩展指南**:详见 [docs/HOOK_GUIDE.md](docs/HOOK_GUIDE.md)(Telegram 实现、接入新 App 步骤) +> **Hook 架构与扩展指南**:详见 [docs/Hook指南.md](docs/Hook指南.md)(含 **Xposed / LSPosed**、Telegram、接入新 App) +> **手机部署与银行 bypass 操作**:详见 [docs/手机操作手册.md](docs/手机操作手册.md) +> **MariBank 风控与 register 载荷**:[docs/MariBank风控与载荷说明.md](docs/MariBank风控与载荷说明.md) ## 项目结构 @@ -29,7 +31,9 @@ notiMessage/ ├── scripts/ # build-debug.ps1, install-full.ps1, start-debug-server.ps1 ├── library/ # 基础库模块 (com.miraclegarden.library) │ └── MiracleGardenActivity # ViewBinding 基类 -├── docs/HOOK_GUIDE.md # Hook 架构与扩展文档 +├── docs/Hook指南.md # Hook 架构、Xposed/LSPosed、扩展 +├── docs/手机操作手册.md # Root 机部署与日常操作 +├── docs/MariBank风控与载荷说明.md # register 字段与风控说明 ├── build.gradle # 根构建文件 ├── settings.gradle # 模块: app, library, xposed-module └── gradle.properties # Gradle 配置 diff --git a/docs/HOOK_GUIDE.md b/docs/Hook指南.md similarity index 87% rename from docs/HOOK_GUIDE.md rename to docs/Hook指南.md index dfc36ea..e9f1320 100644 --- a/docs/HOOK_GUIDE.md +++ b/docs/Hook指南.md @@ -1,6 +1,43 @@ # 消息抓取架构与 Hook 扩展指南 -本文说明 notiMessage 的双通道抓取机制、Telegram 的实现方式,以及日后接入其他 App 的步骤。 +本文说明 notiMessage 的双通道抓取机制、Telegram 的实现方式、**Xposed 与 LSPosed**,以及日后接入其他 App 的步骤。 + +> 手机部署步骤见 [`手机操作手册.md`](手机操作手册.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 | --- diff --git a/docs/MariBank_2026-07-06_菲律宾突破.md b/docs/MariBank_2026-07-06_菲律宾突破.md new file mode 100644 index 0000000..b4d1324 --- /dev/null +++ b/docs/MariBank_2026-07-06_菲律宾突破.md @@ -0,0 +1,255 @@ +# MariBank 菲律宾注册突破 & 新加坡现状(2026-07-06) + +> **设备**:Pixel 6(`1C081FDF600K5Q`)· Magisk 30.7 + Zygisk + LSPosed +> **结论**:**菲律宾 SeaBank 已能发 OTP**;**新加坡 MariBank 仍 3100012** +> **关联**:[`MariBank实现说明.md`](MariBank实现说明.md) · [`MariBank新加坡逆向.md`](MariBank新加坡逆向.md) + +--- + +## 1. 结果一览 + +| 地区 | 包名 | API | 注册结果 | 错误码 | +|------|------|-----|----------|--------| +| **菲律宾** | `ph.seabank.seabank` | `https://api.seabank.ph` | ✅ **成功 → OTP 短信** | `code=0` | +| **新加坡** | `sg.com.maribankmobile.digitalbank` | `https://api.maribank.com.sg` | ❌ 仍失败 | **3100012** | + +**关键意义**:同一台 Root 机、同一套 Xposed + Magisk 伪装下,**PH 服务端已放行注册**,说明 Hook 链与 attestation 载荷 **对 PH 有效**;SG 失败更可能是 **区域风控策略差异** 或 **SG 侧设备黑名单**,而非「Root bypass 完全无效」。 + +--- + +## 2. 测试环境 + +| 项 | 值 | +|----|-----| +| 手机 | Google Pixel 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": "", + "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 检测 | +| [`工作日志_2026-07-03.md`](工作日志_2026-07-03.md) | 7/3 工作记录 | + +--- + +*记录日期:2026-07-06 · 最后更新:文档体系整理(操作手册 + 风控载荷说明)* diff --git a/docs/MARIBANK_IMPLEMENTATION.md b/docs/MariBank实现说明.md similarity index 90% rename from docs/MARIBANK_IMPLEMENTATION.md rename to docs/MariBank实现说明.md index 9978d9c..7de4170 100644 --- a/docs/MARIBANK_IMPLEMENTATION.md +++ b/docs/MariBank实现说明.md @@ -1,11 +1,11 @@ # MariBank 实现说明与问题记录 -> **日期**:2026-07-03 +> **日期**: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 +> **设备**:Pixel 6 · Magisk + Zygisk + LSPosed + Shamiko > **源码**:`xposed-module/.../hook/MariBank*.java` -本文档专门说明当日对 MariBank **注册流程风控绕过** 的 Xposed 实现细节,以及开发/测试中遇到的全部主要问题。通用工作汇总见 [`WORKLOG_2026-07-03.md`](WORKLOG_2026-07-03.md)。 +本文档专门说明当日对 MariBank **注册流程风控绕过** 的 Xposed 实现细节,以及开发/测试中遇到的全部主要问题。通用工作汇总见 [`工作日志_2026-07-03.md`](工作日志_2026-07-03.md)。 --- @@ -21,7 +21,21 @@ → 进入 OTP / 下一步 ``` -### 1.2 当日最终结果 +### 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 突破) | 阶段 | 结果 | @@ -33,7 +47,7 @@ | 注册请求明文 | **未抓到**(body native 加密) | -**结论**:本地层 bypass 已推进到能正常发起注册请求,但 **服务端风控仍拒绝**;当前实现不足以完成注册。 +**结论(2026-07-03)**:本地层 bypass 已推进到能正常发起注册请求,但 **服务端风控仍拒绝**。 --- @@ -332,9 +346,12 @@ Hook 对含 `temporarily blocked` / `8424 8050` 的 Toast 会 **记录栈**(`l ## 5. 遇到的问题(完整清单) -### 5.1 服务端注册拒绝 — 4067012(核心未解问题) +### 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 页 - 请求发出后弹服务端封锁弹窗 @@ -560,7 +577,10 @@ Log in → 输入已开户手机号 → 继续 | `reverse/frida/trace_maribank_register.js` | Frida trace 脚本 | | `scripts/logcat-maribank.ps1` | logcat 过滤 | | `scripts/start-mari-trace.ps1` | Frida + logcat 联启 | -| `docs/WORKLOG_2026-07-03.md` | 当日全项目工作汇总 | +| `docs/工作日志_2026-07-03.md` | 当日全项目工作汇总 | +| `docs/MariBank_2026-07-06_菲律宾突破.md` | PH OTP 突破 + SG 现状 | +| `docs/MariBank风控与载荷说明.md` | register 字段、afExtInfo、USB 调试 | +| `docs/手机操作手册.md` | Root 机部署与日常操作 | --- diff --git a/docs/MariBank新加坡逆向.md b/docs/MariBank新加坡逆向.md new file mode 100644 index 0000000..bb9d16a --- /dev/null +++ b/docs/MariBank新加坡逆向.md @@ -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 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 突破* diff --git a/docs/MariBank风控与载荷说明.md b/docs/MariBank风控与载荷说明.md new file mode 100644 index 0000000..aa1ed1c --- /dev/null +++ b/docs/MariBank风控与载荷说明.md @@ -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": "", + "scene": "REGISTRATION", + "step": "BE", + "rdVerifyInfo": { + "bioStatus": 0, + "data": "", + "dataKey": "", + "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* diff --git a/docs/WORKLOG_2026-07-03.md b/docs/工作日志_2026-07-03.md similarity index 94% rename from docs/WORKLOG_2026-07-03.md rename to docs/工作日志_2026-07-03.md index 49d43e1..121c7ad 100644 --- a/docs/WORKLOG_2026-07-03.md +++ b/docs/工作日志_2026-07-03.md @@ -198,7 +198,7 @@ reverse/ - **44 个** 脚本内路径改为相对 `reverse/` 根(`apks/`、`extracted/`、`tmp/`、`output/`)。 - Frida 日志输出改到 `reverse/logs/frida/`。 -- 更新 `.gitignore`、`docs/BANK_REVERSE.md`、`pull_split_apk.ps1`、`start-mari-trace.ps1`。 +- 更新 `.gitignore`、`docs/银行逆向.md`、`pull_split_apk.ps1`、`start-mari-trace.ps1`。 - `apk_extract/` 迁入 `extracted/apk_extract/`;`seabank.zip` 迁入 `apks/`。 --- @@ -223,10 +223,13 @@ reverse/ | 文档 / 路径 | 说明 | | ---------------------------------- | ----------------------------------- | -| `docs/MARIBANK_IMPLEMENTATION.md` | **MariBank 实现细节与问题记录(专文)** | -| `docs/BANK_REVERSE.md` | 澳大利亚三家银行 Hook(Up / Suncorp / ubank) | -| `docs/HOOK_GUIDE.md` | 通用 Hook 架构与扩展 | -| `docs/CHANGELOG.md` | 主 App / TG Hook 版本历史 | +| `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 实现 | diff --git a/docs/手机操作手册.md b/docs/手机操作手册.md new file mode 100644 index 0000000..c802858 --- /dev/null +++ b/docs/手机操作手册.md @@ -0,0 +1,156 @@ +# 手机操作手册 + +> 在 Root 手机上部署 **notiMessage** 主 App + **Xposed 模块**,以及 **MariBank / SeaBank** bypass 的日常步骤。 +> 关联:[`Hook指南.md`](Hook指南.md) · [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md) · [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md) + +--- + +## 1. 手机前置条件(一次性) + +| 步骤 | 操作 | +|------|------| +| 1 | 刷 **Magisk**,Magisk 设置里开启 **Zygisk** | +| 2 | 安装 **LSPosed**(Magisk 模块或 Manager APK) | +| 3 | 安装 **Shamiko**(银行 App **强烈建议**) | +| 4 | Magisk → **配置排除列表** → 开启 | +| 5 | 排除列表勾选目标银行 **全部子进程**(PH / SG 包名见下) | +| 6 | **不要** 开启「强制启用排除列表」(Shamiko 要求 Enforce = OFF) | +| 7 | (可选)安装 Magisk 模块 `maribank-device-spoof` 持久换 serial / android_id | + +### 目标包名 + +| App | 包名 | +|-----|------| +| 菲律宾 SeaBank | `ph.seabank.seabank` | +| 新加坡 MariBank | `sg.com.maribankmobile.digitalbank` | +| notiMessage 主 App | `com.miraclegarden.smsmessage` | +| Xposed 模块 | `com.miraclegarden.smsmessage.xposed` | +| Telegram(抓消息) | `org.telegram.messenger` 或 `org.telegram.messenger.web` | + +--- + +## 2. PC 安装 / 更新(USB 连手机) + +### 2.1 一键全套(推荐) + +```powershell +cd C:\Users\Administrator\Desktop\notiMessage +.\scripts\install-full.ps1 +``` + +自动完成:编译 → 安装双 APK → 配置 LSPosed 作用域 → 电池白名单 → 通知监听授权等。 + +### 2.2 仅编译安装 APK + +```powershell +.\scripts\build-debug.ps1 +.\scripts\install-debug.ps1 +``` + +### 2.3 MariBank 测前收尾 + +```powershell +.\scripts\maribank-scheme-b-finish.ps1 +``` + +作用:确认 Magisk 模块、写入 LSPosed 作用域、关 USB 调试(可选)、`pm clear` 银行 App。 + +换设备 ID: + +```powershell +.\scripts\maribank-spoof-device.ps1 -InstallModule # 持久模块 +.\scripts\maribank-spoof-device.ps1 -ClearMariBank # 一次性 resetprop + 清数据 +``` + +--- + +## 3. 手机上 LSPosed 必做步骤 + +**每次新装或更新 Xposed 模块 APK 后:** + +1. 打开 **LSPosed** +2. 找到模块 **notiMessage Xposed** → **启用** +3. 进入 **作用域**,勾选需要的 App(见 §1 包名表) +4. 对 **每个已勾选 App** 执行 **软重启**(或整机重启) + +未软重启时 Hook **不会** 注入目标 App。 + +--- + +## 4. 日常使用 + +### 4.1 菲律宾 SeaBank 注册(当前可 OTP) + +1. 确认 LSPosed 已启用且作用域含 `ph.seabank.seabank` +2. (建议)`pm clear ph.seabank.seabank` 或跑 `maribank-scheme-b-finish.ps1` +3. 打开 App → **Sign up** → 输入菲律宾手机号 → **Next** +4. 等待 OTP 短信 + +> **说明**:实测 **USB 调试可保持开启** 仍能 OTP,因 Xposed 对 App 伪装 adb 状态(详见 [`MariBank风控与载荷说明.md` §4](MariBank风控与载荷说明.md))。测 SG 或求稳时仍建议关调试。 + +### 4.2 新加坡 MariBank + +流程同上,包名为 `sg.com.maribankmobile.digitalbank`。当前仍可能 **3100012**,见 [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)。 + +### 4.3 notiMessage 抓 Telegram + +1. 打开 **notiMessage** → **开始监听** → 授予通知使用权 +2. LSPosed 作用域勾选 Telegram + 主 App +3. PC 调试台(可选):`.\scripts\start-debug-server.ps1`(需 `adb reverse tcp:8765 tcp:8765`) + +--- + +## 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* diff --git a/docs/CHANGELOG.md b/docs/更新说明.md similarity index 80% rename from docs/CHANGELOG.md rename to docs/更新说明.md index 457987b..20aa9d7 100644 --- a/docs/CHANGELOG.md +++ b/docs/更新说明.md @@ -1,5 +1,19 @@ # 更新说明 +## 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 后台时丢失消息 @@ -75,7 +89,7 @@ adb logcat | findstr /i "HookMessageReceiver DebugForwarder NotificationService ### 文档 -- 架构与扩展:`docs/HOOK_GUIDE.md` +- 架构与扩展:`docs/Hook指南.md` - Agent 说明:`AGENTS.md` --- diff --git a/docs/BANK_REVERSE.md b/docs/银行逆向.md similarity index 100% rename from docs/BANK_REVERSE.md rename to docs/银行逆向.md diff --git a/reverse/maribank_sg_screen.png b/reverse/maribank_sg_screen.png new file mode 100644 index 0000000..e69de29 diff --git a/reverse/scripts/dump_sg_safemode.py b/reverse/scripts/dump_sg_safemode.py new file mode 100644 index 0000000..0bc4efa --- /dev/null +++ b/reverse/scripts/dump_sg_safemode.py @@ -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]) diff --git a/reverse/scripts/find_sg_adb_classes.py b/reverse/scripts/find_sg_adb_classes.py new file mode 100644 index 0000000..c21994f --- /dev/null +++ b/reverse/scripts/find_sg_adb_classes.py @@ -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()) diff --git a/reverse/scripts/find_sg_adb_strings.py b/reverse/scripts/find_sg_adb_strings.py new file mode 100644 index 0000000..d5b31ae --- /dev/null +++ b/reverse/scripts/find_sg_adb_strings.py @@ -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) diff --git a/reverse/scripts/scan_maribank_sg.py b/reverse/scripts/scan_maribank_sg.py new file mode 100644 index 0000000..3efc0ad --- /dev/null +++ b/reverse/scripts/scan_maribank_sg.py @@ -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 diff --git a/scripts/configure-lsposed.py b/scripts/configure-lsposed.py index 0e4ab2f..0bf7d6d 100644 --- a/scripts/configure-lsposed.py +++ b/scripts/configure-lsposed.py @@ -9,6 +9,8 @@ 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", diff --git a/scripts/logcat-maribank.ps1 b/scripts/logcat-maribank.ps1 index b517e90..2f271e2 100644 --- a/scripts/logcat-maribank.ps1 +++ b/scripts/logcat-maribank.ps1 @@ -1,29 +1,15 @@ -# MariBank / Hook 相关 logcat(adb 不在 PATH 时也可用) -param( - [switch]$Clear, - [switch]$Follow -) - -$ErrorActionPreference = "Stop" -$sdk = "C:\Users\Administrator\AppData\Local\Android\Sdk" -$adb = Join-Path $sdk "platform-tools\adb.exe" - +# 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 at $adb" -ForegroundColor Red + Write-Host "adb not found: $adb" -ForegroundColor Red exit 1 } - -$pattern = "MariBankRoot|1201|seabank|ClashMeta|LSPosed-Bridge.*notiMessage" - if ($Clear) { & $adb logcat -c - Write-Host "logcat cleared." -ForegroundColor Green + Write-Host "Logcat cleared. Now click Next in MariBank, then run without -Clear:" -ForegroundColor Yellow + Write-Host " .\scripts\logcat-maribank.ps1" exit 0 } - -if ($Follow) { - Write-Host "Following logcat (Ctrl+C to stop)..." -ForegroundColor Cyan - & $adb logcat | Select-String -Pattern $pattern -} else { - & $adb logcat -d | Select-String -Pattern $pattern -} +& $adb logcat -d 2>&1 | Select-String -Pattern "MariBankRoot|MariBankNative" | + Select-String -Pattern "HTTP|outbound|register|faked|finish adb|blocked|ErrorFlow|RegisterViewModel|skip error|late app|late native|assessRisk|risk callback" diff --git a/scripts/magisk/maribank-device-spoof/module.prop b/scripts/magisk/maribank-device-spoof/module.prop new file mode 100644 index 0000000..360d658 --- /dev/null +++ b/scripts/magisk/maribank-device-spoof/module.prop @@ -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. diff --git a/scripts/magisk/maribank-device-spoof/post-fs-data.sh b/scripts/magisk/maribank-device-spoof/post-fs-data.sh new file mode 100644 index 0000000..206b168 --- /dev/null +++ b/scripts/magisk/maribank-device-spoof/post-fs-data.sh @@ -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" diff --git a/scripts/magisk/maribank-device-spoof/service.sh b/scripts/magisk/maribank-device-spoof/service.sh new file mode 100644 index 0000000..feb119a --- /dev/null +++ b/scripts/magisk/maribank-device-spoof/service.sh @@ -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" diff --git a/scripts/maribank-scheme-b-finish.ps1 b/scripts/maribank-scheme-b-finish.ps1 new file mode 100644 index 0000000..84caa3e --- /dev/null +++ b/scripts/maribank-scheme-b-finish.ps1 @@ -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 +} diff --git a/scripts/maribank-spoof-device.ps1 b/scripts/maribank-spoof-device.ps1 new file mode 100644 index 0000000..de4f73b --- /dev/null +++ b/scripts/maribank-spoof-device.ps1 @@ -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 diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java index 768a984..4d3df2e 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java @@ -20,7 +20,6 @@ public class MainHook implements IXposedHookLoadPackage { private static final String UP_BANK_PACKAGE = "au.com.up.money"; private static final String SUNCORP_PACKAGE = "au.com.suncorp.marketplace"; private static final String UBANK_PACKAGE = "au.com.bank86400"; - private static final String MARIBANK_PACKAGE = MariBankRootBypassHook.PACKAGE; private static final String MAIN_APP_PACKAGE = "com.miraclegarden.smsmessage"; @Override @@ -58,7 +57,7 @@ public class MainHook implements IXposedHookLoadPackage { return; } - if (MARIBANK_PACKAGE.equals(lpparam.packageName)) { + if (MariBankRootBypassHook.isTargetPackage(lpparam.packageName)) { MariBankShpsNativeHook.install(lpparam); MariBankRootBypassHook.install(lpparam); return; diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankAttestationHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankAttestationHook.java new file mode 100644 index 0000000..ad1b202 --- /dev/null +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankAttestationHook.java @@ -0,0 +1,232 @@ +package com.miraclegarden.smsmessage.xposed.hook; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * SHPSSDK attestation 生成链 Hook(含 native JNI)。 + * 目标:在 {@code rdVerifyInfo.data/dataKey} 组装前,让 native 采集层读到「干净环境」。 + */ +public final class MariBankAttestationHook { + + private static final String TAG = "notiMessageHook/MariBankAttest"; + + private static volatile boolean installed = false; + + private MariBankAttestationHook() { + } + + public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) { + if (installed) { + return; + } + installed = true; + + int n = 0; + n += hookAttestationClass(lpparam, "com.shopee.shpssdkbank.wvvvuwwu"); + n += hookAttestationClass(lpparam, "com.shopee.shpssdk.wvvvuwwu"); + n += hookAttestationClass(lpparam, + "com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu"); + n += hookKnownAttestationMethods(lpparam); + hookEnvironmentProbes(lpparam); + XposedBridge.log(TAG + " attestation hooks=" + n); + } + + /** 逆向确认的 attestation / requestDefense 桥接方法。 */ + private static int hookKnownAttestationMethods(XC_LoadPackage.LoadPackageParam lpparam) { + int count = 0; + count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vvuwuuvuu"); + count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "wwvwvwuvv"); + count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.wvvvuwwu", "vuwuuuwv"); + count += hookMethodByName(lpparam, "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv", "wuvwuvwwu"); + return count; + } + + private static int hookMethodByName( + XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) { + int count = 0; + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + for (Method method : clazz.getDeclaredMethods()) { + if (!methodName.equals(method.getName())) { + continue; + } + if (hookAttestationMethod(className, method)) { + count++; + } + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage()); + } + return count; + } + + private static int hookAttestationClass( + XC_LoadPackage.LoadPackageParam lpparam, String className) { + int count = 0; + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + for (Method method : clazz.getDeclaredMethods()) { + if (!Modifier.isStatic(method.getModifiers())) { + continue; + } + if (hookAttestationMethod(className, method)) { + count++; + } + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip class " + className + ": " + t.getMessage()); + } + return count; + } + + private static boolean hookAttestationMethod(String className, Method method) { + Class rt = method.getReturnType(); + if (rt == String.class) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Object result = param.getResult(); + if (!(result instanceof String)) { + return; + } + String s = (String) result; + String sanitized = sanitizeAttestationString(s); + if (!sanitized.equals(s)) { + param.setResult(sanitized); + XposedBridge.log(TAG + " " + className + "#" + method.getName() + + (Modifier.isNative(method.getModifiers()) ? " (native)" : "") + + " sanitized len=" + s.length()); + } + } + }); + return true; + } + if (rt == byte[].class) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Object result = param.getResult(); + if (!(result instanceof byte[])) { + return; + } + byte[] bytes = (byte[]) result; + byte[] out = MariBankRegisterPayloadUtil.sanitizeRegistrationBytes(bytes); + if (out != bytes) { + param.setResult(out); + XposedBridge.log(TAG + " " + className + "#" + method.getName() + + " byte[] sanitized"); + } else { + byte[] tokenOut = MariBankRiskTokenUtil.sanitizeBytes(bytes, 0, bytes.length); + if (tokenOut != bytes) { + param.setResult(tokenOut); + XposedBridge.log(TAG + " " + className + "#" + method.getName() + + " byte[] riskToken sanitized"); + } + } + } + }); + return true; + } + if (rt == boolean.class || rt == Boolean.class) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(false); + } + }); + return true; + } + if (rt == int.class || rt == Integer.class) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(0); + } + }); + return true; + } + return false; + } + + private static String sanitizeAttestationString(String s) { + if (s == null || s.isEmpty()) { + return s; + } + if (MariBankRegisterPayloadUtil.isRegistrationPayload(s)) { + return MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s); + } + if (s.contains("|")) { + return MariBankRiskTokenUtil.sanitizeRiskToken(s); + } + return MariBankRiskTokenUtil.sanitizeAllInText(s); + } + + private static void hookEnvironmentProbes(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + "android.os.Debug", + lpparam.classLoader, + "isDebuggerConnected", + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(false); + } + } + ); + } catch (Throwable ignored) { + } + try { + XposedHelpers.findAndHookMethod( + "android.os.Debug", + lpparam.classLoader, + "waitingForDebugger", + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(false); + } + } + ); + } catch (Throwable ignored) { + } + RootBypassHelper.hookFileExists(lpparam); + RootBypassHelper.hookRuntimeExec(lpparam); + RootBypassHelper.hookSystemGetProperty(lpparam); + hookProcessBuilder(lpparam); + } + + private static void hookProcessBuilder(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + ProcessBuilder.class, + "start", + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + ProcessBuilder pb = (ProcessBuilder) param.thisObject; + if (pb == null || pb.command() == null) { + return; + } + String joined = String.join(" ", pb.command()).toLowerCase(); + if (joined.contains(" su") || joined.startsWith("su") + || joined.contains("magisk") || joined.contains("which su") + || joined.contains("getprop ro.debuggable")) { + XposedBridge.log(TAG + " blocked ProcessBuilder: " + joined); + throw new SecurityException("blocked root probe"); + } + } + } + ); + } catch (Throwable t) { + XposedBridge.log(TAG + " ProcessBuilder hook failed: " + t.getMessage()); + } + } +} diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRegisterPayloadUtil.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRegisterPayloadUtil.java new file mode 100644 index 0000000..cb4aa16 --- /dev/null +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRegisterPayloadUtil.java @@ -0,0 +1,71 @@ +package com.miraclegarden.smsmessage.xposed.hook; + +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.robv.android.xposed.XposedBridge; + +/** + * register 加密前 JSON 净化:{@code scene=REGISTRATION} 路径上的 deviceFingerprint / riskToken。 + */ +final class MariBankRegisterPayloadUtil { + + private static final String TAG = "notiMessageHook/MariBankRegister"; + + private static final Pattern DEVICE_FINGERPRINT = Pattern.compile( + "\"deviceFingerprint\"\\s*:\\s*\"([^\"]+)\""); + + private MariBankRegisterPayloadUtil() { + } + + static boolean isRegistrationPayload(String text) { + if (text == null || text.length() < 24) { + return false; + } + return text.contains("\"scene\":\"REGISTRATION\"") + || text.contains("\"scene\": \"REGISTRATION\"") + || (text.contains("rdVerifyInfo") && text.contains("\"step\":\"BE\"")); + } + + static byte[] sanitizeRegistrationBytes(byte[] data) { + if (data == null || data.length == 0) { + return data; + } + String text = new String(data, StandardCharsets.UTF_8); + if (!isRegistrationPayload(text)) { + return MariBankRiskTokenUtil.sanitizeBytes(data, 0, data.length); + } + String out = sanitizeRegistrationJson(text); + if (out.equals(text)) { + return data; + } + XposedBridge.log(TAG + " sanitized register payload len=" + data.length + " -> " + out.length()); + return out.getBytes(StandardCharsets.UTF_8); + } + + static String sanitizeRegistrationJson(String json) { + if (json == null) { + return null; + } + String out = MariBankRiskTokenUtil.sanitizeAllInText(json); + Matcher m = DEVICE_FINGERPRINT.matcher(out); + StringBuffer sb = new StringBuffer(); + boolean changed = false; + while (m.find()) { + String old = m.group(1); + String neu = MariBankRiskTokenUtil.sanitizeRiskToken(old); + if (!neu.equals(old)) { + changed = true; + } + m.appendReplacement(sb, Matcher.quoteReplacement( + "\"deviceFingerprint\":\"" + neu + "\"")); + } + if (changed) { + m.appendTail(sb); + out = sb.toString(); + XposedBridge.log(TAG + " deviceFingerprint sanitized in register JSON"); + } + return out; + } +} diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRootBypassHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRootBypassHook.java index a8f00ff..8403e84 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRootBypassHook.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankRootBypassHook.java @@ -2,6 +2,7 @@ package com.miraclegarden.smsmessage.xposed.hook; import android.app.Activity; import android.app.Dialog; +import android.content.ContentResolver; import android.content.res.Resources; import android.os.Process; import android.view.View; @@ -22,13 +23,30 @@ import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; /** - * MariBank / SeaBank PH(ph.seabank.seabank)Root 检测绕过。 - * 逆向:SafeMode SDK + SHPSSDK;检测到 Root 后会 Toast 并 Process.killProcess 自杀。 + * 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; @@ -56,18 +74,21 @@ public final class MariBankRootBypassHook { "com.shopee.bke.biz.user.errorcodehandler.a", "com.shopee.bke.biz.user.errorcodehandler.b", "com.shopee.bke.biz.user.rn.helper.ErrorFlowHelper", + "com.shopee.bke.biz.user.viewmodel.RegisterViewModel", }; private MariBankRootBypassHook() { } private static volatile boolean deferredHooksInstalled = false; + private static volatile boolean lateAppHooksInstalled = false; public static void install(XC_LoadPackage.LoadPackageParam lpparam) { - if (!PACKAGE.equals(lpparam.packageName)) { + if (!isTargetPackage(lpparam.packageName)) { return; } + XposedBridge.log(TAG + " install for " + lpparam.packageName); hookAntiSuicide(lpparam); hookAntiSoftCrash(lpparam); scheduleAppHooks(lpparam); @@ -85,6 +106,13 @@ public final class MariBankRootBypassHook { MariBankShpsNativeHook.installDeferred(lpparam); } }; + XC_MethodHook afterOnCreate = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + MariBankShpsNativeHook.installLateNativeHooks(lpparam); + installLateAppHooks(lpparam); + } + }; try { XposedHelpers.findAndHookMethod( "com.shopee.bke.digitalbank.BkeApplication", @@ -92,36 +120,54 @@ public final class MariBankRootBypassHook { "attachBaseContext", "android.content.Context", afterAttach); - XposedBridge.log(TAG + " waiting attachBaseContext for app hooks"); + XposedHelpers.findAndHookMethod( + "com.shopee.bke.digitalbank.BkeApplication", + lpparam.classLoader, + "onCreate", + afterOnCreate); + XposedBridge.log(TAG + " waiting attachBaseContext + onCreate for app hooks"); } catch (Throwable t) { - XposedBridge.log(TAG + " attachBaseContext hook failed, install now: " + t.getMessage()); + XposedBridge.log(TAG + " BkeApplication hook failed, install now: " + t.getMessage()); installDeferredHooks(lpparam); MariBankShpsNativeHook.installDeferred(lpparam); + MariBankShpsNativeHook.installLateNativeHooks(lpparam); + installLateAppHooks(lpparam); } } + /** onCreate 之后补装:此时 classes11 / SHPSSDK 与 RN SO 均已就绪。 */ + private static void installLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) { + if (lateAppHooksInstalled) { + return; + } + lateAppHooksInstalled = true; + + int hooked = 0; + for (String className : SAFE_MODE_CLASSES) { + hooked += hookAllBooleanChecks(lpparam, className); + } + hooked += hookShpsRisk(lpparam); + hookShpsToken(lpparam); + hookErrorFlowLogging(lpparam); + MariBankSdkUtilsHook.installLate(lpparam); + MariBankAttestationHook.installLate(lpparam); + + XposedBridge.log(TAG + " late app hooks installed, booleanHooks=" + hooked); + } + private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) { if (deferredHooksInstalled) { return; } deferredHooksInstalled = true; - int hooked = 0; - for (String className : SAFE_MODE_CLASSES) { - hooked += hookAllBooleanChecks(lpparam, className); - } - hookSafeModeDialog(lpparam); hookRootDialogBlock(lpparam); - hookErrorFlowLogging(lpparam); - hooked += hookShpsRisk(lpparam); - hookShpsToken(lpparam); + hookAdbBypass(lpparam); hookNetworkLogging(lpparam); - XposedBridge.log(TAG + " app hooks installed, booleanHooks=" + hooked); + XposedBridge.log(TAG + " early app hooks installed"); } - - /** SafeMode 类方法名被混淆,Hook 所有返回 boolean/int 的实例方法。 */ private static int hookAllBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam, String className) { int count = 0; try { @@ -416,7 +462,9 @@ public final class MariBankRootBypassHook { } String args = Arrays.toString(param.args); if (args.contains("1201") || args.contains("-1201") - || args.toLowerCase().contains("error")) { + || args.contains("406") + || args.toLowerCase().contains("error") + || args.toLowerCase().contains("unavailable")) { XposedBridge.log(TAG + " " + className + "." + method.getName() + " args=" + args); } @@ -903,12 +951,17 @@ public final class MariBankRootBypassHook { body = sanitized; } String lower = body.toLowerCase(); - if (lower.contains("blocked") + 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\"")) { - String url = CURRENT_REQUEST_URL.get(); + || 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" @@ -1035,6 +1088,20 @@ public final class MariBankRootBypassHook { 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) { } } @@ -1516,6 +1583,139 @@ public final class MariBankRootBypassHook { } } + /** 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); + } + } + } 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( @@ -1540,9 +1740,9 @@ public final class MariBankRootBypassHook { XC_MethodHook blankRootTextHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { - if (param.args.length > 0 && isRootBlockText(String.valueOf(param.args[0]))) { + if (param.args.length > 0 && isEnvironmentBlockText(String.valueOf(param.args[0]))) { param.args[0] = " "; - XposedBridge.log(TAG + " blanked root dialog message"); + XposedBridge.log(TAG + " blanked env block dialog message"); } } }; @@ -1574,9 +1774,9 @@ public final class MariBankRootBypassHook { @Override protected void afterHookedMethod(MethodHookParam param) { String s = (String) param.getResult(); - if (isRootBlockText(s)) { + if (isEnvironmentBlockText(s)) { param.setResult(" "); - XposedBridge.log(TAG + " blanked root string resource"); + XposedBridge.log(TAG + " blanked env block string resource"); } } } @@ -1593,8 +1793,8 @@ public final class MariBankRootBypassHook { @Override protected void beforeHookedMethod(MethodHookParam param) { Dialog dialog = (Dialog) param.thisObject; - if (isRootBlockText(extractDialogText(dialog))) { - XposedBridge.log(TAG + " blocked root Dialog.show: " + if (isEnvironmentBlockText(extractDialogText(dialog))) { + XposedBridge.log(TAG + " blocked env Dialog.show: " + dialog.getClass().getSimpleName()); param.setResult(null); } @@ -1620,6 +1820,9 @@ public final class MariBankRootBypassHook { 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); } } } @@ -1638,7 +1841,9 @@ public final class MariBankRootBypassHook { || 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("restore to factory settings") + || lower.contains("does not support root") + || lower.contains("support root device"); } private static boolean isSecurityBlockText(String text) { diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankSdkUtilsHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankSdkUtilsHook.java new file mode 100644 index 0000000..f881868 --- /dev/null +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankSdkUtilsHook.java @@ -0,0 +1,225 @@ +package com.miraclegarden.smsmessage.xposed.hook; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * register body 加密前 Hook:{@code com.shopee.bke.lib.jni.utils.d} / {@code uvwuvwuv}。 + * 勿 Hook {@code utils.f}(SoUtils),否则会 libsdkutils 白屏。 + */ +public final class MariBankSdkUtilsHook { + + private static final String TAG = "notiMessageHook/MariBankEncrypt"; + private static final int MAX_LOG = 2000; + + private static final String WRAPPER = "com.shopee.bke.lib.jni.utils.d"; + private static final String NATIVE_ENCRYPT = "com.shopee.bke.lib.jni.utils.uvwuvwuv"; + + private static volatile boolean installed = false; + + private MariBankSdkUtilsHook() { + } + + /** BkeApplication.onCreate 之后安装(libsdkutils 已加载)。 */ + public static void installLate(XC_LoadPackage.LoadPackageParam lpparam) { + if (installed) { + return; + } + installed = true; + int n = hookEncryptWrapper(lpparam); + n += hookNativeEncryptUtils(lpparam); + n += hookGsonRegister(lpparam); + XposedBridge.log(TAG + " late encrypt hooks=" + n); + } + + private static int hookEncryptWrapper(XC_LoadPackage.LoadPackageParam lpparam) { + int count = 0; + try { + Class clazz = XposedHelpers.findClass(WRAPPER, lpparam.classLoader); + for (Method method : clazz.getDeclaredMethods()) { + if (Modifier.isStatic(method.getModifiers())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String name = method.getName(); + XposedBridge.log(TAG + " >> utils.d." + name); + for (int i = 0; i < param.args.length; i++) { + Object sanitized = sanitizeArg(param.args[i]); + if (sanitized != param.args[i]) { + param.args[i] = sanitized; + } + logArg(" in" + i, param.args[i]); + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + Object result = param.getResult(); + if (result == null) { + return; + } + if (result instanceof String[]) { + for (int i = 0; i < ((String[]) result).length; i++) { + logArg(" out" + i, ((String[]) result)[i]); + } + } else if (result instanceof byte[]) { + logArg(" out", result); + } + } + }); + count++; + } + if (count > 0) { + XposedBridge.log(TAG + " hooked utils.d methods=" + count); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip utils.d: " + t.getMessage()); + } + return count; + } + + private static int hookNativeEncryptUtils(XC_LoadPackage.LoadPackageParam lpparam) { + int count = 0; + try { + Class clazz = XposedHelpers.findClass(NATIVE_ENCRYPT, lpparam.classLoader); + for (Method method : clazz.getDeclaredMethods()) { + if (!Modifier.isStatic(method.getModifiers())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " >> uvwuvwuv." + method.getName()); + for (int i = 0; i < param.args.length; i++) { + Object sanitized = sanitizeArg(param.args[i]); + if (sanitized != param.args[i]) { + param.args[i] = sanitized; + } + logArg(" in" + i, param.args[i]); + } + } + }); + count++; + } + if (count > 0) { + XposedBridge.log(TAG + " hooked uvwuvwuv methods=" + count); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip uvwuvwuv: " + t.getMessage()); + } + return count; + } + + private static int hookGsonRegister(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + "com.google.gson.Gson", + lpparam.classLoader, + "toJson", + Object.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (!(param.getResult() instanceof String)) { + return; + } + String json = (String) param.getResult(); + if (!looksLikeRegisterJson(json)) { + return; + } + String sanitized = MariBankRegisterPayloadUtil.isRegistrationPayload(json) + ? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(json) + : MariBankRiskTokenUtil.sanitizeAllInText(json); + if (!sanitized.equals(json)) { + param.setResult(sanitized); + json = sanitized; + } + if (MariBankRegisterPayloadUtil.isRegistrationPayload(json)) { + XposedBridge.log(TAG + " Gson REGISTRATION: " + truncate(json)); + } + } + } + ); + return 1; + } catch (Throwable t) { + XposedBridge.log(TAG + " skip Gson.toJson: " + t.getMessage()); + return 0; + } + } + + private static boolean looksLikeRegisterJson(String text) { + if (text == null || text.length() < 8) { + return false; + } + if (MariBankRegisterPayloadUtil.isRegistrationPayload(text)) { + return true; + } + String lower = text.toLowerCase(); + return lower.contains("rdverifyinfo") + || lower.contains("devicefingerprint") + || (lower.contains("encphone") && lower.contains("scene")); + } + + private static Object sanitizeArg(Object arg) { + if (arg instanceof String) { + String s = (String) arg; + if (!s.contains("|") && !looksLikeRegisterJson(s)) { + return arg; + } + String out = MariBankRegisterPayloadUtil.isRegistrationPayload(s) + ? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s) + : MariBankRiskTokenUtil.sanitizeAllInText(s); + return out.equals(s) ? arg : out; + } + if (arg instanceof byte[]) { + byte[] bytes = (byte[]) arg; + byte[] out = MariBankRegisterPayloadUtil.sanitizeRegistrationBytes(bytes); + return out == bytes ? arg : out; + } + return arg; + } + + private static void logArg(String label, Object arg) { + if (arg == null) { + XposedBridge.log(TAG + label + " null"); + return; + } + if (arg instanceof String) { + String s = (String) arg; + if (s.length() > 4 || s.contains("|") || looksLikeRegisterJson(s)) { + XposedBridge.log(TAG + label + " String(" + s.length() + ") " + truncate(s)); + } + return; + } + if (arg instanceof byte[]) { + byte[] bytes = (byte[]) arg; + String text; + try { + text = new String(bytes, StandardCharsets.UTF_8); + } catch (Throwable t) { + text = ""; + } + if (text.contains("|") || looksLikeRegisterJson(text) || bytes.length < 512) { + XposedBridge.log(TAG + label + " byte[" + bytes.length + "] " + truncate(text)); + } + } + } + + private static String truncate(String s) { + if (s == null) { + return ""; + } + if (s.length() <= MAX_LOG) { + return s; + } + return s.substring(0, MAX_LOG) + "..."; + } +} diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankShpsNativeHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankShpsNativeHook.java index 9734abc..693f548 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankShpsNativeHook.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/MariBankShpsNativeHook.java @@ -72,6 +72,15 @@ public final class MariBankShpsNativeHook { } private static volatile boolean deferredInstalled = false; + private static volatile boolean lateNativeInstalled = false; + + /** + * 勿 Hook:负责 SoUtils.loadSoLibrary / libshpssdk_bank.so 加载,Hook 会导致 SO 找不到。 + */ + private static final Set 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) { @@ -82,21 +91,28 @@ public final class MariBankShpsNativeHook { XposedBridge.log(TAG + " early hooks OK (proc only)"); } - /** attachBaseContext 之后安装 SHPSSDK 相关 Hook(ClassLoader 已就绪)。 */ + /** attachBaseContext 之后:仅装不干扰 SO/RN 加载的 Hook。 */ public static void installDeferred(XC_LoadPackage.LoadPackageParam lpparam) { if (deferredInstalled) { return; } deferredInstalled = true; - hookLoadLibrary(lpparam); - hookShpssInstall(lpparam); hookRequestDefense(lpparam); - hookShpsNativeBridge(lpparam); - hookShpsNativeCore(lpparam); hookBuildFields(lpparam); XposedBridge.log(TAG + " deferred hooks installed for " + lpparam.packageName); } + /** BkeApplication.onCreate 之后:RN / shpssdk SO 已加载,再装 native 桥接 Hook。 */ + public static void installLateNativeHooks(XC_LoadPackage.LoadPackageParam lpparam) { + if (lateNativeInstalled) { + return; + } + lateNativeInstalled = true; + hookShpsNativeBridge(lpparam); + hookShpsNativeCore(lpparam); + XposedBridge.log(TAG + " late native hooks installed for " + lpparam.packageName); + } + /** native 直接读 /proc/self/maps 查 hook 库;过滤内容。 */ private static void hookProcAccess(XC_LoadPackage.LoadPackageParam lpparam) { try { @@ -233,32 +249,7 @@ public final class MariBankShpsNativeHook { return ""; } - private static void hookLoadLibrary(XC_LoadPackage.LoadPackageParam lpparam) { - XC_MethodHook logHook = new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - String lib = String.valueOf(param.args[param.args.length - 1]); - if (lib.contains("shpssdk")) { - XposedBridge.log(TAG + " loading native lib: " + lib); - } - } - }; - try { - XposedHelpers.findAndHookMethod( - Runtime.class, "loadLibrary0", ClassLoader.class, String.class, logHook); - } catch (Throwable ignored) { - } - try { - XposedHelpers.findAndHookMethod( - System.class, "loadLibrary", String.class, logHook); - } catch (Throwable ignored) { - } - } - - private static void hookShpssInstall(XC_LoadPackage.LoadPackageParam lpparam) { - // 勿 Hook ShpssInstall / vuvuwwwuw:会干扰 SoUtils.loadSoLibrary,导致 libsdkutils.so 死循环白屏。 - } - + /** 保留 requestDefense 执行(生成 x-sap-fixme),仅净化返回值中的 risk 字段。 */ private static void hookRequestDefense(XC_LoadPackage.LoadPackageParam lpparam) { for (String className : new String[]{ "com.shopee.shpssdkbank.SHPSSDK", @@ -271,9 +262,26 @@ public final class MariBankShpsNativeHook { "requestDefense", new XC_MethodHook() { @Override - protected void beforeHookedMethod(MethodHookParam param) { - XposedBridge.log(TAG + " blocked requestDefense"); - param.setResult(null); + 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"); + } + } } } ); @@ -304,6 +312,10 @@ public final class MariBankShpsNativeHook { }; 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); } @@ -360,6 +372,10 @@ public final class MariBankShpsNativeHook { && 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; } @@ -409,6 +425,9 @@ public final class MariBankShpsNativeHook { if (!Modifier.isStatic(method.getModifiers())) { continue; } + if (Modifier.isNative(method.getModifiers())) { + continue; + } Class rt = method.getReturnType(); if (rt == String.class) { XposedBridge.hookMethod(method, new XC_MethodHook() { @@ -547,6 +566,15 @@ public final class MariBankShpsNativeHook { 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"; } diff --git a/xposed-module/src/main/res/values/arrays.xml b/xposed-module/src/main/res/values/arrays.xml index 8b88c73..78f07d6 100644 --- a/xposed-module/src/main/res/values/arrays.xml +++ b/xposed-module/src/main/res/values/arrays.xml @@ -9,5 +9,6 @@ au.com.suncorp.marketplace au.com.bank86400 ph.seabank.seabank + sg.com.maribankmobile.digitalbank