From 1a6711c1802f239e410011bc16024f54db4dcf6e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 11:44:54 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=BB=95=E8=BF=87=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9B=B4=E6=AD=A3=20(bypass=5Fall=20?= =?UTF-8?q?=E6=9C=AA=E9=AA=8C=E8=AF=81,=20=E4=BB=A5=E5=8E=9F=E7=89=88?= =?UTF-8?q?=E4=B8=89=E8=84=9A=E6=9C=AC=E4=B8=BA=E5=87=86)=20+=20=E4=B8=89?= =?UTF-8?q?=E5=B1=82=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Frida绕过配方.md | 68 +++++++++++ scripts/bypass_compare.py | 60 +++++++++ scripts/bypass_healthcheck.py | 109 +++++++++++++++++ scripts/bypass_loader.py | 30 +++++ scripts/hook_deflate.py | 5 +- scripts/hook_hy.py | 5 +- scripts/hook_keytrace.py | 5 +- scripts/hook_keytrace2.py | 5 +- scripts/hook_login_md5.py | 10 +- scripts/hook_memshot.py | 7 +- tools/frida/bypass_all.js | 224 ++++++++++++++++++++++++++++++++++ 11 files changed, 504 insertions(+), 24 deletions(-) create mode 100644 docs/Frida绕过配方.md create mode 100644 scripts/bypass_compare.py create mode 100644 scripts/bypass_healthcheck.py create mode 100644 scripts/bypass_loader.py create mode 100644 tools/frida/bypass_all.js diff --git a/docs/Frida绕过配方.md b/docs/Frida绕过配方.md new file mode 100644 index 0000000..497b337 --- /dev/null +++ b/docs/Frida绕过配方.md @@ -0,0 +1,68 @@ +# Frida 绕过配方 — 虎牙 com.duowan.kiwi (13.4.22 arm64) + +> ⚠️ **当前可用配方 = RE 仓库三脚本两阶段流程** (hook_final_capture.py 同款, 实测唯一稳定): +> `bypass_msaoaid_maps_art_callsite.js` + `mask_frida_maps_only.js` 挂起中加载 → resume → 11s → `patch_guard_block_termination.js` +> +> ⚠️ **`tools/frida/bypass_all.js` (合并版) 未通过验证 (R40)**: 三层健康检查显示卡加载页/延迟死亡, +> 最小化测试 A1-only 也不通, 与原版的实质差异未定位 (emit 死锁已修但仍有问题)。**别用**。 +> `scripts/bypass_healthcheck.py` = 三层判定工具 (pid/界面轨迹/崩溃ANR+冻结探针), 可复用。 + +## 一、标准用法 (runner 模板) + +```python +import frida, time + +d = frida.get_device_manager().add_remote_device("127.0.0.1:31878") # frida-server -l 0.0.0.0:31878 +pid = d.spawn(["com.duowan.kiwi"]) +s = d.attach(pid) +from bypass_loader import load_bypass # scripts/ 在 sys.path (同目录运行即可) +load_bypass(s) # ← 唯一的绕过步骤, 可传参 +d.resume(pid) + +# 之后挂业务钩子 (挂业务 JS 的时机不再敏感 — patch_guard 已内置延迟) +sc = s.create_script(business_js) +sc.on("message", on_msg) +sc.load() +``` + +**参数** (经 `globalThis.BYPASS_OPTS` 注入): + +| 参数 | 默认 | 说明 | +|---|---|---| +| `patch_guard_delay_ms` | 3000 | B 层 (终止拦截) 延迟安装毫秒数。App 启动期被杀 → 调大到 5000-11000; 启动卡死/合法退出被吞 → 调小到 1500 | +| `persist` | false | true 时写 `/data/user/0//cache/bypass-all-.jsonl` 调试日志 (每次掩盖/补丁事件) | + +**设备侧前提**: root (magisk su) + `frida-server -l 0.0.0.0:31878` (必须 0.0.0.0, 否则 connection closed) + `adb forward tcp:31878 tcp:31878`。 + +## 二、三层各自防什么 + +| 层 | 生效时机 | 对抗点 | +|---|---|---| +| **A1 msaoaid 补丁** | 立即 (dlopen 双路触发) | `libmsaoaidsec.so`: 线程名探测 (`gmain`/`gum-js-loop`/`linjector` → `pool-worker`), fd 扫描 (`linjector` 掩写), frida 检出后的清理/退出分支 (`0x1c4a0` → 直接跳继续路径), ART 完整性周期 callsite (`0x1ceec` NOP) | +| **A2 maps 内容掩盖** | 立即 | libc `open/open64/fopen/openat` 记录 maps fd → `read/pread64` 时把 `frida-agent`/`gum-js-loop`/`linjector` 掩写成无害串 (比 A1 更底层的兜底) | +| **B 终止拦截** | 延迟 (默认 3s) | `kill/tgkill/exit/_exit/_Exit/abort/raise` 对自身进程一概吞掉 (turing 检出后 `_exit` 静默退出的最后防线) + msaoaid crash-guard `0x20ca8` 补丁 | + +**为什么 B 层要延迟**: 终止拦截会无差别吞掉 App 自己的 `exit/abort`。启动初期 (0-2s) 有合法退出路径 (初始化失败重试等), 过早替换会卡死/异常。之前 11s 是保守值, 实测 3s 即可; **如果换 App 版本后启动期就被杀, 优先把 `patch_guard_delay_ms` 调回 11000 再排查**。 + +## 三、实战教训清单 (每条都付出过时间) + +1. **绕过必须 spawn 挂起中加载**, attach 已启动进程 = 6/6 死 (登录态下更敏感)。 +2. **spawn 实例"安静退出"无 crash 日志** = msaoaid `_exit` 静默退, 说明绕过没生效或时序错 — 先检查是否 resume 前加载、frida-server 是否带 `-l 0.0.0.0`。 +3. **别 hook GL/EGL 驱动里的同名导出** (libllvm-glnext/eglSubDriver 也有 `deflate`, 非 zlib ABI, 读错内存直接崩) — hook 系统库符号前先确认模块名。 +4. **别 hook 热路径** (libz `inflate` = 图片解码): 首屏卡死、注册超时。要过滤: backtrace 含目标库才转储。 +5. **OLLVM/无帧指针下 `Backtracer.ACCURATE` 会给假帧** — 用 `Interceptor` 的 `this.returnAddress` (真实立即调用方) 逐级爬, 或以必经外部导出 (如 libz `deflate`) 为锚点反查。 +6. **sret 约定**: `md5(out&, in)` 的 x0 是输出槽, onEnter 读必空; 输入在 x1+。R34 曾因此误判"输入为空"。 +7. **std::string 判读**: `b0&1==0` → SSO (len=b0>>1, data=+1); 否则 len@+8, data@*(+16)。 +8. 业务 JS 里 **Java MessageDigest 钩子要在 8 分钟窗口类 runner 里限量** (证书解析会刷几千条)。 + +## 四、已知边界 + +- offset 表 (0x1c4a0/0x1ceec/0x20ca8 等) 对应 **13.4.22 arm64** 的 `libmsaoaidsec.so`; App 升级需重新定位 (以绕过事件日志 + IDA 对照)。 +- 模拟器上 A1 补丁因 EGL 竞争会崩 (原配方注释), 仅真机验证。 +- 需要"清 turing 状态触发重注册"时: `rm -rf /data/data/com.duowan.kiwi/{app_turingdfp,app_turingfd}/*` + 删 `resinfo*` (**保留登录态, 别 pm-clear**)。 +- 设备被风控限流 (登录超时) 时静置即可恢复; 与 frida 绕过无关。 + +## 五、历史配方存档 + +- 三脚本两阶段原版: RE 仓库 `evidence/scripts/{bypass_msaoaid_maps_art_callsite,mask_frida_maps_only,patch_guard_block_termination}.js`, 流程见 `scripts/hook_final_capture.py` (11s 稳定窗)。 +- `bypass_msaoaid_maps_skip_cleanup.js`: 旧单脚本, 匿名启动路径可用, **登录态启动路径不可靠** (R38 实测秒死) — 已被 bypass_all.js 取代, 留作对照。 diff --git a/scripts/bypass_compare.py b/scripts/bypass_compare.py new file mode 100644 index 0000000..5b812c2 --- /dev/null +++ b/scripts/bypass_compare.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""bypass_all vs 原版三脚本 — 三项标准 + Activity 轨迹对比.""" +import subprocess, sys, time, re +from pathlib import Path +import frida + +ADB="5dd8c93f"; PKG="com.duowan.kiwi" +RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0") +sys.path.insert(0,str(Path(__file__).resolve().parent)) +from bypass_loader import load_bypass + +def sh(*a): return subprocess.run(list(a),capture_output=True,text=True).stdout +def pid_of(): + for l in sh("adb","-s",ADB,"shell","ps","-A").splitlines(): + if l.rstrip().endswith(PKG): return l.split()[1] +def top_act(): + out=sh("adb","-s",ADB,"shell","dumpsys","activity","activities") + for line in out.splitlines(): + if "Hist #0" in line and PKG in line: + m=re.search(r"u0 ([\w.$]+) t\d+", line) + return m.group(1) if m else "?" + return None + +def run(label, loader, attempts=2, watch=40): + for a in range(1,attempts+1): + sh("adb","-s",ADB,"shell","am","force-stop",PKG); time.sleep(4) + sh("adb","-s",ADB,"logcat","-c") + d=frida.get_device_manager().add_remote_device("127.0.0.1:31878") + pid0=d.spawn([PKG]); s=d.attach(pid0) + loader(s) + d.resume(pid0) + trail=[]; die=None; t0=time.time() + while time.time()-t0 '.join(trail) if trail else '(none)'}") + try: s.detach() + except: pass + +def load_merged(s): + load_bypass(s, patch_guard_delay_ms=11000) +def load_original(s): + s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load() + s.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()).load() + time.sleep(11) + s.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()).load() + +if __name__=="__main__": + which=sys.argv[1] if len(sys.argv)>1 else "both" + if which in ("merged","both"): run("merged", load_merged) + if which in ("original","both"): run("original", load_original) diff --git a/scripts/bypass_healthcheck.py b/scripts/bypass_healthcheck.py new file mode 100644 index 0000000..e4b9dfd --- /dev/null +++ b/scripts/bypass_healthcheck.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""通用 bypass 健康检查器 — 三层面标准 (唯一判定工具). + +层面1 进程: pid 不被杀、不换新 (换新 = 被静默杀后重启) +层面2 界面: Activity 轨迹持续推进, 通过 Splash 且最终停在主界面 (轨迹停滞 = 卡死) +层面3 崩溃: logcat 无 FATAL / 无 ANR + +用法: + bypass_healthcheck.py [attempts] [--recipe merged|original] [--watch 秒] +判定: 每次 attempt 三层全过 → 该次 PASS; 所有 attempt 都 PASS → 总 PASS. +""" +import subprocess, sys, time, re +from pathlib import Path +import frida + +ADB="5dd8c93f"; PKG="com.duowan.kiwi" +RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0") +sys.path.insert(0,str(Path(__file__).resolve().parent)) +from bypass_loader import load_bypass + +def sh(*a): return subprocess.run(list(a),capture_output=True,text=True).stdout + +def pid_of(): + for l in sh("adb","-s",ADB,"shell","ps","-A").splitlines(): + if l.rstrip().endswith(PKG): return l.split()[1] + return None + +def main_thread_frozen(pid): + """6s 采样主线程 utime 增量: 0 = 冻结 (前台卡死铁证)""" + if not pid: return None + def utime(): + out=sh("adb","-s",ADB,"shell","su","-c",f"cat /proc/{pid}/task/{pid}/stat") + f=out.split() + return int(f[13])+int(f[14]) if len(f)>14 else None + a=utime() + if a is None: return None + time.sleep(6) + b=utime() + return (b is None) or (b==a) + +def input_anr_warning(): + log=sh("adb","-s",ADB,"logcat","-d") + return ("Input dispatching timed out" in log) or ("ANR in com.duowan.kiwi" in log) + +def top_act(): + out=sh("adb","-s",ADB,"shell","dumpsys","activity","activities") + for line in out.splitlines(): + if "Hist #0" in line and PKG in line: + m=re.search(r"u0 ([\w.$]+) t\d+", line) + return m.group(1) if m else "?" + return None + +def load_merged(s, delay=11000): + load_bypass(s, patch_guard_delay_ms=delay) + +def load_original(s): + s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load() + s.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()).load() + time.sleep(11) + s.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()).load() + +def check_once(recipe="merged", watch=45, guard_delay=11000): + sh("adb","-s",ADB,"shell","am","force-stop",PKG); time.sleep(4) + sh("adb","-s",ADB,"logcat","-c") + d=frida.get_device_manager().add_remote_device("127.0.0.1:31878") + pid0=d.spawn([PKG]); s=d.attach(pid0) + (load_merged if recipe=="merged" else load_original)(s, guard_delay) if recipe=="merged" else load_original(s) + d.resume(pid0) + L1_ok=True; trail=[]; die=None + t0=time.time() + while time.time()-t0 '.join(r['trail']) or '(none)'}") + total=all(r["PASS"] for r in results) + print(f"TOTAL: {'PASS ✅' if total else 'FAIL ❌'} ({recipe} x{attempts})") + sys.exit(0 if total else 1) + +if __name__=="__main__": main() diff --git a/scripts/bypass_loader.py b/scripts/bypass_loader.py new file mode 100644 index 0000000..a490295 --- /dev/null +++ b/scripts/bypass_loader.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""统一 bypass 加载器 — 所有 runner 共用. + +用法: + from bypass_loader import load_bypass + s = d.attach(pid) # spawn 后、resume 前 + load_bypass(s) # 默认 patch_guard 3s + load_bypass(s, patch_guard_delay_ms=5000, persist=True) + d.resume(pid) + +参数经 globalThis.BYPASS_OPTS 注入 hook (见 tools/frida/bypass_all.js 头注释). +""" +from __future__ import annotations +import json +from pathlib import Path + +HERE = Path(__file__).resolve().parent.parent +BYPASS_SRC = (HERE / "tools/frida/bypass_all.js").read_text() + + +def load_bypass(script_session, patch_guard_delay_ms: int = 3000, persist: bool = False): + """spawn 挂起态调用: 单脚本装载三层绕过 (maps 掩盖立即生效, 终止拦截延迟生效).""" + src = ( + "globalThis.BYPASS_OPTS = " + + json.dumps({"patchGuardDelayMs": patch_guard_delay_ms, "persist": persist}) + + ";\n" + BYPASS_SRC + ) + sc = script_session.create_script(src) + sc.load() + return sc diff --git a/scripts/hook_deflate.py b/scripts/hook_deflate.py index 025940e..474c792 100644 --- a/scripts/hook_deflate.py +++ b/scripts/hook_deflate.py @@ -21,9 +21,8 @@ def main(): time.sleep(1.5) pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True) s=d.attach(pid) - try: - b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load() - except Exception as e: print(f"[*] bypass err {e}",flush=True) + from bypass_loader import load_bypass + load_bypass(s) d.resume(pid) result={}; got={"post":False,"out":False} def on(m,_): diff --git a/scripts/hook_hy.py b/scripts/hook_hy.py index ce70e24..3777b1e 100644 --- a/scripts/hook_hy.py +++ b/scripts/hook_hy.py @@ -21,9 +21,8 @@ def main(): time.sleep(1.5) pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True) s=d.attach(pid) - try: - b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load() - except Exception as e: print(f"[*] bypass err {e}",flush=True) + from bypass_loader import load_bypass + load_bypass(s) d.resume(pid) result={}; got={"post":False,"out":False} seen_fns=set() diff --git a/scripts/hook_keytrace.py b/scripts/hook_keytrace.py index 629b3c3..51c7cc8 100644 --- a/scripts/hook_keytrace.py +++ b/scripts/hook_keytrace.py @@ -21,9 +21,8 @@ def main(): time.sleep(1.5) pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True) s=d.attach(pid) - try: - b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load() - except Exception as e: print(f"[*] bypass err {e}",flush=True) + from bypass_loader import load_bypass + load_bypass(s) d.resume(pid) result={}; got={"post":False,"out":False} seen_fns=set() diff --git a/scripts/hook_keytrace2.py b/scripts/hook_keytrace2.py index aad3ed4..fac0db3 100644 --- a/scripts/hook_keytrace2.py +++ b/scripts/hook_keytrace2.py @@ -21,9 +21,8 @@ def main(): time.sleep(1.5) pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True) s=d.attach(pid) - try: - b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load() - except Exception as e: print(f"[*] bypass err {e}",flush=True) + from bypass_loader import load_bypass + load_bypass(s) d.resume(pid) result={}; state={"post":False,"key":False} def on(m,_): diff --git a/scripts/hook_login_md5.py b/scripts/hook_login_md5.py index b531864..ec4754d 100644 --- a/scripts/hook_login_md5.py +++ b/scripts/hook_login_md5.py @@ -16,14 +16,10 @@ def main(): pid=d.spawn([PACKAGE]) print(f"[*] spawned {pid}",flush=True) s=d.attach(pid) - # 双重防护: callsite bypass + maps 掩盖 - s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load() - s.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()).load() + from bypass_loader import load_bypass + load_bypass(s, patch_guard_delay_ms=3000) d.resume(pid) - print("[*] resumed, 稳定 11s...",flush=True) - time.sleep(11) - s.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()).load() - print("[*] patch_guard on",flush=True) + print("[*] resumed (bypass_all: A层立即 + B层3s patch_guard)",flush=True) result={}; state={"udb":0} def on(m,_): diff --git a/scripts/hook_memshot.py b/scripts/hook_memshot.py index 3799347..e015bd3 100644 --- a/scripts/hook_memshot.py +++ b/scripts/hook_memshot.py @@ -41,11 +41,8 @@ def main(): pid = d.spawn([PACKAGE]) print(f"[*] spawn pid={pid}", flush=True) s = d.attach(pid) - try: - b = s.create_script((RE / "evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()) - b.load() - except Exception as e: - print(f"[*] bypass err {e}", flush=True) + from bypass_loader import load_bypass + load_bypass(s) d.resume(pid) result: dict = {} diff --git a/tools/frida/bypass_all.js b/tools/frida/bypass_all.js new file mode 100644 index 0000000..2bc8c0a --- /dev/null +++ b/tools/frida/bypass_all.js @@ -0,0 +1,224 @@ +'use strict'; +// ============================================================================ +// bypass_all.js — 虎牙 com.duowan.kiwi (13.4.22) 反 frida 绕过 · 一体化脚本 +// ============================================================================ +// 合并自 RE 仓库三脚本 (实测配方, R31-R39 全程验证): +// 1. bypass_msaoaid_maps_art_callsite.js — libmsaoaidsec 线程名/fd 掩盖 + maps 清理分支跳转 + ART 完整性 callsite NOP +// 2. mask_frida_maps_only.js — libc 层 /proc/self/maps 内容 'frida-agent' 掩写 +// 3. patch_guard_block_termination.js — kill/tgkill/exit/abort 自终止拦截 + msaoaid crash-guard 补丁 +// +// 用法 (必须 spawn 挂起中、resume 之前加载!): +// const opts = { patchGuardDelayMs: 3000 }; // B 层延迟, 传参可调 +// const src = 'globalThis.BYPASS_OPTS=' + JSON.stringify(opts) + ';\n' + bypassSrc; +// s.create_script(src).load(); // 然后 d.resume(pid) +// +// 时序语义: +// A 层 (立即): maps 线索掩盖 — 必须 resume 前就位, 否则启动检测线程先读到 frida 痕迹 +// B 层 (延迟): 终止拦截会无差别吞掉 exit/abort, 过早加载会卡死启动期合法退出 +// → 默认 3000ms 后安装; 若 App 秒死调小, 若启动期被杀调大 +// ============================================================================ + +var OPTS = { patchGuardDelayMs: 3000, persist: false }; +try { if (globalThis.BYPASS_OPTS) OPTS = Object.assign(OPTS, globalThis.BYPASS_OPTS); } catch (_) {} + +function emit(o) { + // 关键: 钩子上下文 (dlopen/拦截器 onEnter, 主线程启动期) 内直接 send() 会死锁主线程 + // (R40 ANR trace: main 阻塞在 frida-agent) — 一律入队, setImmediate 刷出 + try { pendingEmits.push(Object.assign({ src: 'bypass_all' }, o)); setImmediate(flushEmits); } catch (_) {} +} +var pendingEmits = []; +function flushEmits() { + while (pendingEmits.length) { try { send(pendingEmits.shift()); } catch (_) {} } +} + +// ---------- 持久化调试日志 (默认关) ---------- +var logFile = null; +function persist(row) { + if (!OPTS.persist) return; + try { + if (logFile === null) logFile = new File('/data/user/0/com.duowan.kiwi/cache/bypass-all-' + Process.id + '.jsonl', 'a'); + row.tid = Process.getCurrentThreadId(); + logFile.write(JSON.stringify(row) + '\n'); + logFile.flush(); + } catch (_) {} +} + +function exportOf(name) { + try { if (typeof Module.getGlobalExportByName === 'function') return Module.getGlobalExportByName(name); } catch (_) {} + try { if (typeof Module.findGlobalExportByName === 'function') return Module.findGlobalExportByName(null, name); } catch (_) {} + try { return Module.findExportByName(null, name); } catch (_) { return null; } +} + +// ============================================================================ +// A1) libmsaoaidsec.so: 线程名/fd 掩盖 + predicate 分支跳转 + ART callsite NOP +// (原 bypass_msaoaid_maps_art_callsite.js, offsets 对应 13.4.22 arm64) +// ============================================================================ +var msaoaid = { installed: false }; +function readStr(pointer, length) { try { return pointer.readCString(length) || ''; } catch (_) { return ''; } } + +function installMsaoaid(source) { + if (msaoaid.installed) return; + var module = Process.findModuleByName('libmsaoaidsec.so'); + if (module === null) return; + msaoaid.installed = true; + var branch = module.base.add(0x1c4a0); + var continuePath = module.base.add(0x1c4b4); + var artCallsite = module.base.add(0x1ceec); + persist({ event: 'installed', source: source, base: module.base.toString() }); + emit({ event: 'A-msaoaid-patched', base: module.base.toString() }); + + // 线程名掩盖: /proc//cmdline 读取处的 gmain/gum-js-loop/linjector → pool-worker + [0x1c0e4, 0x1c0f4].forEach(function (offset) { + Interceptor.attach(module.base.add(offset), { + onEnter() { + var buffer = this.context.sp.add(0x18); + var value = readStr(buffer, 96); + if (!value.startsWith('Name:\tgmain') && !value.startsWith('Name:\tgum-js-loop') && !value.startsWith('Name:\tlinjector')) return; + buffer.writeUtf8String('Name:\tpool-worker'); + persist({ event: 'name-masked', offset: '0x' + offset.toString(16) }); + }, + }); + }); + + // fd 扫描掩盖: 'linjector' → 'xxxxxxxxx' + Interceptor.attach(module.base.add(0x1c214), { + onEnter() { + var buffer = this.context.sp.add(0x288); + var value = readStr(buffer, 512); + var index = value.indexOf('linjector'); + if (index < 0) return; + buffer.add(index).writeUtf8String('xxxxxxxxx'); + persist({ event: 'fd-masked' }); + }, + }); + + // predicate 分支: 检出 frida 线索后的清理分支 → 直接跳继续路径; ART 完整性周期 callsite → NOP + try { + Memory.patchCode(branch, 4, function (code) { + var w = new Arm64Writer(code, { pc: branch }); + w.putBranchAddress(continuePath); + w.flush(); + }); + Memory.patchCode(artCallsite, 4, function (code) { + var w = new Arm64Writer(code, { pc: artCallsite }); + w.putNop(); + w.flush(); + }); + persist({ event: 'branches-patched' }); + } catch (error) { + emit({ event: 'A-patch-error', error: String(error) }); + } +} + +// msaoaid 可能在 spawn 时已加载, 也可能稍后 dlopen — 双路触发 +['dlopen', 'android_dlopen_ext'].forEach(function (name) { + var address = exportOf(name); + if (address !== null) Interceptor.attach(address, { + onEnter() { installMsaoaid(name + ':enter'); }, + onLeave() { installMsaoaid(name + ':leave'); }, + }); +}); +installMsaoaid('initial'); + +// ============================================================================ +// A2) libc 层 maps 内容掩盖: open*/fopen 记录 maps fd → read/pread64 时掩写 frida-agent +// (原 mask_frida_maps_only.js) +// ============================================================================ +var trackedFds = {}; +function trackOpen(pathPointer, fd) { + var path = ''; + try { path = pathPointer.isNull() ? '' : pathPointer.readCString(); } catch (_) { return; } + if (fd >= 0 && /(?:^|\/)maps$/.test(path)) trackedFds[fd] = path; +} +['open', 'open64', 'fopen'].forEach(function (name) { + var address = exportOf(name); + if (address === null) return; + Interceptor.attach(address, { + onEnter(args) { this.path = args[0]; }, + onLeave(retval) { trackOpen(this.path, retval.toInt32()); }, + }); +}); +var openat = exportOf('openat'); +if (openat !== null) Interceptor.attach(openat, { + onEnter(args) { this.path = args[1]; }, + onLeave(retval) { trackOpen(this.path, retval.toInt32()); }, +}); +var closeFn = exportOf('close'); +if (closeFn !== null) Interceptor.attach(closeFn, { + onEnter(args) { delete trackedFds[args[0].toInt32()]; }, +}); +function maskMapsRead(buffer, length, path) { + if (!path || length <= 0) return; + try { + var bytes = new Uint8Array(buffer.readByteArray(length)); + var value = ''; + for (var i = 0; i < bytes.length; i++) value += String.fromCharCode(bytes[i]); + if (value.indexOf('frida-agent') < 0 && value.indexOf('gum-js-loop') < 0 && value.indexOf('linjector') < 0) return; + value = value.replace(/frida-agent/gi, 'xxxxxxxxxxx') + .replace(/gum-js-loop/g, 'pool-worker') + .replace(/linjector/g, 'xxxxxxxxx'); + for (var j = 0; j < length; j++) buffer.add(j).writeU8(value.charCodeAt(j) || 0); + } catch (_) {} +} +['read', 'pread64'].forEach(function (name) { + var address = exportOf(name); + if (address === null) return; + Interceptor.attach(address, { + onEnter(args) { this.buffer = args[1]; this.path = trackedFds[args[0].toInt32()] || ''; }, + onLeave(retval) { maskMapsRead(this.buffer, retval.toInt32(), this.path); }, + }); +}); +emit({ event: 'A-maps-mask-installed' }); + +// ============================================================================ +// B 层 (延迟 OPTS.patchGuardDelayMs): 自终止拦截 + msaoaid crash-guard 补丁 +// (原 patch_guard_block_termination.js) +// ============================================================================ +function installPatchGuard() { + var keep = []; + var ownPid = Process.id; + function replaceFn(name, returnType, argumentTypes, callback) { + var address = exportOf(name); + if (address === null) return; + try { + var native = new NativeCallback(callback, returnType, argumentTypes); + keep.push(native); + Interceptor.replace(address, native); + } catch (_) {} + } + replaceFn('kill', 'int', ['int', 'int'], function (pid) { return Number(pid) === ownPid ? 0 : -1; }); + replaceFn('tgkill', 'int', ['int', 'int', 'int'], function (tgid, tid) { return (Number(tgid) === ownPid || Number(tid) === ownPid) ? 0 : -1; }); + replaceFn('exit', 'void', ['int'], function () {}); + replaceFn('_exit', 'void', ['int'], function () {}); + replaceFn('_Exit', 'void', ['int'], function () {}); + replaceFn('abort', 'void', [], function () {}); + replaceFn('raise', 'int', ['int'], function () { return 0; }); + + // msaoaid crash-guard: 属性读取触发点后补丁 0x20ca8 (x0 → 0) + var guardPatched = false; + function patchGuard(module) { + if (guardPatched) return; + var address = module.base.add(0x20ca8); + try { + Memory.protect(address, 4, 'rwx'); + var writer = new Arm64Writer(address); + writer.putMovRegReg('x0', 'xzr'); + writer.flush(); writer.dispose(); + guardPatched = true; + emit({ event: 'B-guard-patched' }); + } catch (error) { emit({ event: 'B-guard-error', error: String(error) }); } + } + var prop = exportOf('__system_property_get'); + if (prop !== null) Interceptor.attach(prop, { + onEnter(args) { + var name = ''; try { name = args[0].readCString(); } catch (_) { return; } + if (name !== 'ro.build.version.sdk') return; + var module = Process.findModuleByName('libmsaoaidsec.so'); + if (module !== null) patchGuard(module); + }, + }); + emit({ event: 'B-patch-guard-installed', delayMs: OPTS.patchGuardDelayMs }); +} + +setTimeout(installPatchGuard, OPTS.patchGuardDelayMs); +emit({ event: 'armed', pid: Process.id, patchGuardDelayMs: OPTS.patchGuardDelayMs });