feat(frida): 自愈式多轮运行器 + 两阶段注入稳定性定案 (R13b)

- run_capture.py: spawn-only-bypass两阶段 + 进程死亡自愈重试 + 多轮事件汇总
- 配方: bypass_msaoaid_maps_art_callsite.js(唯一) + 延迟注入钩子; 记录三脚本冲突为反模式
- 实测存活>120s (旧配方<60s)
- docs §11.43-10: 稳定性方法论
This commit is contained in:
yml2213
2026-08-29 05:18:07 +08:00
parent 128a57cbf3
commit cb049a590b
2 changed files with 87 additions and 56 deletions
+7
View File
@@ -983,3 +983,10 @@ hypasswordLogin/MsgLoginReq/LogLoginReq → mid 各异, tail 恒定 8b38f1 → a
- 后续路线: (a) libhydeviceid 0x64670 区静态攻坚 (datadiv 已解, OLLVM 状态机) - 后续路线: (a) libhydeviceid 0x64670 区静态攻坚 (datadiv 已解, OLLVM 状态机)
(b) frida 真机 (stable bypass) 抓 setDeviceInfo 入参 → 32hex 直读 (b) frida 真机 (stable bypass) 抓 setDeviceInfo 入参 → 32hex 直读
(c) 接受"hdid=设备级证书不可纯代码铸造"结论, 维持金样本 hdid 共用方案 (已跑通多账号) (c) 接受"hdid=设备级证书不可纯代码铸造"结论, 维持金样本 hdid 共用方案 (已跑通多账号)
### 10) R13b: frida 稳定性优化 (2026-08-29)
- 配方定案: **spawn 挂起 + 只注入 bypass_msaoaid_maps_art_callsite.js + resume**, 后延迟+6s 注入业务钩子
(同时注入 frida_bypass.js/javaexit = 模拟器向配方, 会与 msaoaid 补丁冲突 → 真机启动卡死, 勿用)
- 自愈运行器: tools/frida/run_capture.py (多轮自动重 spawn, 事件汇总单 JSONL, 进程死亡检测)
- 实测: 两阶段配方下 app 存活 >120s, crypto-otp/getHdid 全触发 (vs 旧三脚本配方 <60s 被杀)
- 已知残余: msaoaid solist 快照面 (G2-0019) 未掩; 登录流程仍不推荐带 frida (标记上报)
+80 -56
View File
@@ -1,67 +1,67 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""虎牙 OTP 定向捕获运行器 """虎牙 OTP / setDeviceInfo 定向捕获运行器 (自愈多轮版)
稳定性设计 (2026-08-29 第二轮):
- 两阶段注入: spawn 挂起只注入 bypass_msaoaid_maps_art_callsite.js (真机唯一稳定配方)
-> resume -> 等 app 启动后 (+6s) 再注入 OTP/设备信息钩子 (避开 msaoaid 启动扫描竞争)
- 自愈: 每轮 app 被杀/窗口结束自动 force-stop 重 spawn, 最多 N 轮, 事件汇总到同一 JSONL
- 单轮窗口 150s (真机稳定绿区 ~90s+, 加上两阶段补偿)
流程:
1) adb 启动手机端 frida-server (re.frida.server/fs152, 监听 127.0.0.1:31878)
2) 远程连接 127.0.0.1:31878, spawn com.duowan.kiwi
3) 依次注入: frida_bypass.js (maps 伪装) -> frida_java_exit.js (Java kill 拦截)
-> hook_otp_capture.js (OTP 六元组 + setSafeDeviceId 捕获)
4) resume, 收集 send() 事件 -> JSONL 落盘 + 实时打印
用法: 用法:
python3 run_capture.py # 默认输出 ./capture_<ts>.jsonl python3 run_capture.py
CAP_OUT=/tmp/otp.jsonl python3 run_capture.py CAP_MAX_ATTEMPTS=12 CAP_WINDOW=160 python3 run_capture.py
""" """
import json import json
import os import os
import subprocess import subprocess
import sys import sys
import time import time
import threading
from datetime import datetime from datetime import datetime
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
OUT_DIR = os.environ.get("CAP_OUT_DIR", HERE) OUT_DIR = os.environ.get("CAP_OUT_DIR", HERE)
PKG = "com.duowan.kiwi" PKG = "com.duowan.kiwi"
SERVER_PATH = "/data/local/tmp/re.frida.server/fs152" SERVER_PATH = "/data/local/tmp/fs152"
SERVER_PORT = "31878" SERVER_PORT = "31878"
BYpass_RE = "/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/evidence/scripts/bypass_msaoaid_maps_art_callsite.js" BYpass_RE = "/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/evidence/scripts/bypass_msaoaid_maps_art_callsite.js"
JAVA_EXIT_RE = "" # 真机通道: 单一 bypass 脚本最稳, 不加载 java-exit
HOOK_LOCAL = os.path.join(HERE, "hook_otp_capture.js") HOOK_LOCAL = os.path.join(HERE, "hook_otp_capture.js")
MAX_ATTEMPTS = int(os.environ.get("CAP_MAX_ATTEMPTS", "8"))
WINDOW_SEC = int(os.environ.get("CAP_WINDOW", "150"))
HOOK_DELAY = float(os.environ.get("CAP_HOOK_DELAY", "6"))
_count = {"events": 0}
def adb(*args): def adb(*args):
return subprocess.run(["adb"] + list(args), capture_output=True, text=True) return subprocess.run(["adb"] + list(args), capture_output=True, text=True)
def main(): def run_once(fout, out_path):
ts = datetime.now().strftime("%Y%m%d_%H%M%S") """单次 spawn 捕获一轮; 返回该轮事件数 (0 = 该轮无捕获)."""
out_path = os.path.join(OUT_DIR, f"capture_{ts}.jsonl")
fout = open(out_path, "w", encoding="utf-8")
print(f"[runner] output -> {out_path}", flush=True)
# 1) 启动手机端 frida-server (root, 后台)
print("[runner] starting frida-server on device ...", flush=True)
adb("shell", "su", "-c",
f"nohup {SERVER_PATH} -l 127.0.0.1:{SERVER_PORT} >/data/local/tmp/re.frida.server/fs152.log 2>&1 &")
time.sleep(2.0)
import frida import frida
# 1) frida-server 存活
adb("shell", "su", "-c",
f"if ! pgrep -f {SERVER_PATH}; then nohup {SERVER_PATH} -l 127.0.0.1:{SERVER_PORT} >/data/local/tmp/fs152.log 2>&1 & fi")
time.sleep(1.5)
dev = None dev = None
for i in range(6): for i in range(6):
try: try:
dev = frida.get_device_manager().add_remote_device(f"127.0.0.1:{SERVER_PORT}") dev = frida.get_device_manager().add_remote_device(f"127.0.0.1:{SERVER_PORT}")
apps = dev.enumerate_processes() n = len(dev.enumerate_processes())
print(f"[runner] frida-server OK ({len(apps)} procs)", flush=True) print(f"[runner] frida-server OK ({n} procs)", flush=True)
break break
except Exception as e: except Exception as e:
print(f"[runner] wait frida-server [{i}] {str(e)[:80]}", flush=True) print(f"[runner] wait frida-server [{i}] {str(e)[:80]}", flush=True)
time.sleep(2.0) time.sleep(2.0)
if dev is None: if dev is None:
print("[runner] FATAL: frida-server unreachable", flush=True) print("[runner] FATAL: frida-server unreachable", flush=True)
sys.exit(1) return 0
# 2) 确保旧进程关闭, 然后 spawn
adb("shell", "am", "force-stop", PKG) adb("shell", "am", "force-stop", PKG)
time.sleep(1.0) time.sleep(1.0)
try: try:
@@ -69,7 +69,7 @@ def main():
print(f"[runner] spawned pid={pid}", flush=True) print(f"[runner] spawned pid={pid}", flush=True)
except Exception as e: except Exception as e:
print(f"[runner] spawn failed: {e}", flush=True) print(f"[runner] spawn failed: {e}", flush=True)
sys.exit(1) return 0
session = dev.attach(pid) session = dev.attach(pid)
@@ -80,57 +80,81 @@ def main():
line = json.dumps(payload, ensure_ascii=False) line = json.dumps(payload, ensure_ascii=False)
fout.write(line + "\n") fout.write(line + "\n")
fout.flush() fout.flush()
_count["events"] += 1
ev = payload.get("event", "") ev = payload.get("event", "")
if ev in ("crypto-otp", "setdi", "gethdid", "hook-missing", "hook-installed"): if ev in ("crypto-otp", "setdi", "gethdid", "hook-missing", "hook-installed"):
print(f"[cap] {line[:400]}", flush=True) print(f"[cap] {line[:400]}", flush=True)
elif msg.get("type") == "error": elif msg.get("type") == "error":
print(f"[cap-err] {msg.get('stack', msg)}", flush=True) print(f"[cap-err] {msg.get('stack', msg)[:300]}", flush=True)
elif msg.get("type") == "device": elif msg.get("type") == "device":
print(f"[cap-dev] {msg.get('payload')}", flush=True) print(f"[cap-dev] {msg.get('payload')}", flush=True)
scripts = [] # 阶段1: 仅注入 bypass
# 阶段1: 只注入 bypass (STATUS.md 验证的 90s 稳定配方, 避免启动期注入竞争) try:
with open(BYpass_RE, "r", encoding="utf-8") as f: with open(BYpass_RE, "r", encoding="utf-8") as f:
sc = session.create_script(f.read()) sc = session.create_script(f.read())
sc.on("message", on_message) sc.on("message", on_message)
sc.load() sc.load()
scripts.append(sc) print("[runner] loaded bypass (phase-1)", flush=True)
print("[runner] loaded bypass (phase-1)", flush=True) except Exception as e:
print(f"[runner] bypass load failed: {e}", flush=True)
session.detach()
return 0
dev.resume(pid) dev.resume(pid)
print("[runner] RESUMED (phase-1). 等待 app 启动 ...", flush=True) print("[runner] RESUMED (phase-1). 等待 app 启动...", flush=True)
# 阶段2: app 启动后延迟注入 otp 钩子 (避开 msaoaid 启动扫描 + EGL 竞争) # 阶段2: 延迟注入 otp 钩子
def inject_phase2(): def inject_phase2():
time.sleep(8) time.sleep(HOOK_DELAY)
try: try:
with open(HOOK_LOCAL, "r", encoding="utf-8") as f: with open(HOOK_LOCAL, "r", encoding="utf-8") as f:
src = f.read() sc2 = session.create_script(f.read())
sc2 = session.create_script(src)
sc2.on("message", on_message) sc2.on("message", on_message)
sc2.load() sc2.load()
scripts.append(sc2)
print("[runner] loaded otp-hook (phase-2)", flush=True) print("[runner] loaded otp-hook (phase-2)", flush=True)
except Exception as e: except Exception as e:
print(f"[runner] phase-2 inject failed: {e}", flush=True) print(f"[runner] phase-2 inject failed: {e[:200]}", flush=True)
import threading
threading.Thread(target=inject_phase2, daemon=True).start() threading.Thread(target=inject_phase2, daemon=True).start()
# 稳定窗口 ~120s 后自动收工 # 窗口等待 + 进程死亡检测 (自愈: 死了立刻重试)
def auto_stop(): start = time.time()
time.sleep(120) while time.time() - start < WINDOW_SEC:
print("[runner] auto-stop", flush=True) time.sleep(1.0)
os._exit(0) out = adb("shell", "su", "-c", f"kill -0 {pid} 2>/dev/null && echo alive || echo dead")
threading.Thread(target=auto_stop, daemon=True).start() if "dead" in out.stdout:
print(f"[runner] process {pid} died at {int(time.time()-start)}s", flush=True)
break
try: try:
while True: session.detach()
time.sleep(1.0) except Exception:
except KeyboardInterrupt:
pass pass
adb("shell", "am", "force-stop", PKG)
print(f"[runner] round done: {out_path}", flush=True)
return _count["events"]
def main():
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = os.path.join(OUT_DIR, f"capture_{ts}.jsonl")
fout = open(out_path, "w", encoding="utf-8")
print(f"[runner] output -> {out_path} attempts<={MAX_ATTEMPTS} window={WINDOW_SEC}s", flush=True)
attempts = 0
while attempts < MAX_ATTEMPTS:
attempts += 1
before = _count["events"]
try:
run_once(fout, out_path)
except Exception as e:
print(f"[runner] round {attempts} exception: {str(e)[:150]}", flush=True)
if _count["events"] > before:
print(f"[runner] attempts={attempts} events={_count['events']} — stopping (capture got data)", flush=True)
break
time.sleep(3)
fout.close() fout.close()
print(f"[runner] done -> {out_path}", flush=True) print(f"[runner] FINAL attempts={attempts} events={_count['events']} -> {out_path}", flush=True)
if __name__ == "__main__": if __name__ == "__main__":