TngRootBypassHook 增强 captcha 诊断、TigerTally/JNIC 分层与 HWUI 策略;新增逆向脚本、Frida 工具与 UI dump;同步 MariBank SG hook 与 tng_exit_guard 更新。
23 KiB
MariBank SG 3100012 根因分析与突破方案
基于
register_20260706_1636.txt(675 行)+ 全部 Hook 源码 + 逆向文档综合分析
一、关键日志发现
1.1 加密前明文已被完整捕获
日志行 #400(uvwuvwuv.vvuuvvv → uvwvuww 加密入口前):
{
"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 帧
linkernamespace 隔离检测
这些 native 检测路径全部绕过当前 Java Xposed Hook。
🟡 D. 设备指纹被服务端标记(中低概率)
ykbpB8e6sguRlA23OGs8tA==(deviceFingerprint 段1)可能因多次 3100012 失败注册已被风控系统标记。但可通过**换 serial/android_id(已做)**后 fingerprint 值是否变化来验证。
三、7个核心问题的逆向答案
Q1: rdVerifyInfo.data 明文结构推断
基于 Shopee SHPSSDK 体系(SeaBank PH 同源 SDK 已知结构):
{
"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 请求):
{
"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 级别:
# 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 bypass3100012(不同字段错误)→ session/device binding 问题,需进一步分析
⚡ 方案 3:Frida spawn 定位 data 生成入口(明天)
# 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 末尾加入:
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(干净机重放验证)
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 明文截获脚本)
'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
优先级最高的三步:
- PlayIntegrityFix → 提升 Integrity 级别至 DEVICE(30 分钟)
- 干净机重放实验 → 验证根因(需干净机配合)
- Frida spawn + RegisterNatives → 定位
vvuwuuvuu偏移 → Ghidra 分析明文结构
2026-07-06 17:05 SGT