# 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*
