chore: 备份 TNG 注册/captcha 逆向与 MariBank SG bypass 进展
TngRootBypassHook 增强 captcha 诊断、TigerTally/JNIC 分层与 HWUI 策略;新增逆向脚本、Frida 工具与 UI dump;同步 MariBank SG hook 与 tng_exit_guard 更新。
This commit is contained in:
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*
|
||||
@@ -248,6 +248,7 @@ $adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
| [`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 工作记录 |
|
||||
|
||||
---
|
||||
|
||||
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*
|
||||
@@ -90,7 +90,7 @@ cd C:\Users\Administrator\Desktop\notiMessage
|
||||
|
||||
### 4.2 新加坡 MariBank
|
||||
|
||||
流程同上,包名为 `sg.com.maribankmobile.digitalbank`。当前仍可能 **3100012**,见 [`MariBank_2026-07-06_菲律宾突破.md`](MariBank_2026-07-06_菲律宾突破.md)。
|
||||
流程同上,包名为 `sg.com.maribankmobile.digitalbank`。详见 **[`MariBank新加坡突破.md`](MariBank新加坡突破.md)**(3100012 排查与 `maribank-sg-register.ps1`)。
|
||||
|
||||
### 4.3 notiMessage 抓 Telegram
|
||||
|
||||
|
||||
12
docs/更新说明.md
12
docs/更新说明.md
@@ -1,5 +1,17 @@
|
||||
# 更新说明
|
||||
|
||||
## 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`)
|
||||
|
||||
Reference in New Issue
Block a user