Files
live-hub-py/scripts/emu_spawn_captcha_test.py
T
yml2213 49c5c36c05 docs(huya): 模拟器存活闪退诊断报告与 Frida 探测脚本证据
- 诊断报告: attach 主进程静默退出/EGL 崩溃, 仅约 4s 窗口可抓帧
- scripts: attach/spawn/hook/emu 系列 Frida 脚本与抓帧/验证工具
- evidence: identity/reqchain/frame/inputbuf/magic_buf/propedge 抓取样本,
  emu_* 存活对比, diag_* 策略实验, baseline 裸测基准
2026-08-27 17:58:32 +08:00

150 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""frida 存活期间验证码加载对照实验 (模拟器, spawn+bypass 三件套).
复用 hook_emu_stable.py 的加载顺序 (art_callsite 版 bypass → 立即 resume → patch_guard),
主 hook 只挂轻量 SSL hook (不干扰 UI), 然后用 adb UI 自动化走到登录→验证码,
验证: spawn+bypass 存活时, 极验验证码能否正常加载。
"""
from pathlib import Path
import frida, time, json, subprocess, re, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/emu_spawn_captcha.json")
def sh(*args): return subprocess.run(args, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
MAIN_JS = """
'use strict';
send({type:'armed'});
/* 轻量: 只投递存活心跳, 不碰 UI 线程逻辑 */
setInterval(function(){ send({type:'alive', t:Date.now()}); }, 5000);
"""
def ui_dump():
adb("shell", "uiautomator", "dump", "/data/local/tmp/ui.xml")
return adb("shell", "cat", "/data/local/tmp/ui.xml").stdout
def find_bounds(xml, text):
m = re.search(r'<node[^>]*text="' + re.escape(text) + r'"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
if not m: return None
x1, y1, x2, y2 = map(int, m.groups())
return (x1 + x2) // 2, (y1 + y2) // 2
def tap(x, y): adb("shell", "input", "tap", str(x), str(y))
def type_text(s): adb("shell", "input", "text", s)
def go_login(force=False):
xml = ui_dump()
b = find_bounds(xml, "我的")
if not b: return False
tap(*b); time.sleep(2.5)
xml = ui_dump()
b = find_bounds(xml, "立即登录")
if not b: return False
tap(*b); time.sleep(3)
xml = ui_dump()
b = find_bounds(xml, "账号密码登录")
if b:
tap(*b); time.sleep(2)
return True
def fill_submit():
xml = ui_dump()
b = find_bounds(xml, "手机号/虎牙号") or find_bounds(xml, "请填写手机号码")
if not b: return False
tap(*b); time.sleep(0.4); type_text("13800138000")
xml = ui_dump()
b = find_bounds(xml, "密码")
if b:
tap(*b); time.sleep(0.4); type_text("test12345678")
time.sleep(0.6)
xml = ui_dump()
b = find_bounds(xml, "立即登录")
if not b: return False
tap(*b); time.sleep(2)
xml = ui_dump()
b = find_bounds(xml, "同意并继续")
if b:
tap(*b); time.sleep(4)
return True
def captcha_status():
xml = ui_dump()
joined = xml
webview = "android.webkit.WebView" in joined
slider = any(k in joined for k in ("滑块", "拼图", "安全验证"))
return {"webview": webview, "slider": slider,
"focus": adb("shell", "dumpsys window").stdout.count("OakVerifyActivity") > 0}
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
result = {}
# ===== 阶段1: 无 frida 基线 =====
print("== 阶段1: 无frida 基线 ==", flush=True)
adb("shell", "am", "force-stop", PACKAGE); time.sleep(1.2)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(8)
if go_login() and fill_submit():
time.sleep(5)
result["baseline_no_frida"] = captcha_status()
print(" 验证码:", result["baseline_no_frida"], flush=True)
else:
print(" 基线流程失败", flush=True)
# ===== 阶段2: spawn+bypass 存活时 =====
print("== 阶段2: spawn+bypass ==", flush=True)
adb("shell", "am", "force-stop", PACKAGE); time.sleep(1.2)
pid = d.spawn([PACKAGE])
print(f" spawn pid={pid}", flush=True)
session = d.attach(pid)
def fast_load(path):
try:
s = session.create_script(path.read_text()); s.load(); return True
except Exception as e:
print(f" [load-err] {e}", flush=True); return False
fast_load(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js")
fast_load(RE / "evidence/scripts/mask_frida_maps_only.js")
d.resume(pid)
print(" resumed", flush=True)
try:
sg = session.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()); sg.load()
except Exception as e:
print(f" [guard-err] {e}", flush=True)
alive_events = []
def on_main(m, dta):
if m.get('type') == 'send':
p = m.get('payload') or {}
if p.get('type') == 'alive':
alive_events.append(p)
elif m.get('type') == 'error':
print(" [JS-ERR]", str(m)[:120], flush=True)
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 等 UI 起来
time.sleep(8)
alive = [p for p in d.enumerate_processes() if p.pid == pid]
result["spawn_alive_at_8s"] = bool(alive)
print(f" 8s 存活: {bool(alive)}", flush=True)
if alive and go_login() and fill_submit():
time.sleep(5)
result["captcha_with_frida_alive"] = captcha_status()
print(" frida存活时验证码:", result["captcha_with_frida_alive"], flush=True)
alive2 = [p for p in d.enumerate_processes() if p.pid == pid]
result["spawn_alive_at_end"] = bool(alive2)
result["alive_heartbeats"] = len(alive_events)
print(f" 最终存活: {bool(alive2)}, 心跳数: {len(alive_events)}", flush=True)
try: d.kill(pid)
except: pass
OUT.write_text(json.dumps(result, ensure_ascii=False, indent=1))
print("[*] done ->", OUT, flush=True)
if __name__ == "__main__":
main()