- 诊断报告: attach 主进程静默退出/EGL 崩溃, 仅约 4s 窗口可抓帧 - scripts: attach/spawn/hook/emu 系列 Frida 脚本与抓帧/验证工具 - evidence: identity/reqchain/frame/inputbuf/magic_buf/propedge 抓取样本, emu_* 存活对比, diag_* 策略实验, baseline 裸测基准
176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""请求链捕获器: 冷启动 → 完整登录链。
|
|
|
|
不再只盯 dfpReport。hook SSL_write + SSL_read:
|
|
- SSL_write: 读取 body 前 2000B, 按文本标记分类 (dfpReport/hy*Credlogin/hylogout/dckey/PrtReq 等),
|
|
记录完整 hex + len + t + 分类。
|
|
- SSL_read: 记录响应 hex + len + t, 按时间与请求配对, 看身份链 (t1/t5/actionV 的建立)。
|
|
用途: 搞清楚 dfpReport 在登录链中的角色, 设备身份(t1/t5)到底由哪个接口发放。
|
|
|
|
约定: 后台启动 + tail 实时看 + 命中即杀 (docs/Hook脚本运行与捕获约定.md)。
|
|
spawn 顺序: bypass x2 -> resume+11s -> patch_guard -> 最后主 hook JS。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import frida
|
|
|
|
PACKAGE = "com.duowan.kiwi"
|
|
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
|
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/reqchain_" + time.strftime("%H%M%S") + ".json")
|
|
|
|
# 请求分类关键字 (wup payload 名 / http 头)
|
|
MARKERS = [
|
|
("dfpReport", "dfpReport"),
|
|
("hylogout", "hylogout"),
|
|
("hyCredlogin", "hyCredlogin"),
|
|
("hyanonymousCredlogin", "hyanonymousCredlogin"),
|
|
("dckey", "dckey"),
|
|
("PrtReq", "PrtReq"),
|
|
("CheckUidIsBind", "CheckUidIsBind"),
|
|
("GetUidByGameZone", "GetUidByGameZone"),
|
|
("get3", "get3"),
|
|
("verify3", "verify3"),
|
|
]
|
|
|
|
|
|
def classify(head: bytes) -> str:
|
|
for marker, name in MARKERS:
|
|
if marker.encode() in head:
|
|
return name
|
|
return "other"
|
|
|
|
|
|
JS = r"""
|
|
'use strict';
|
|
Process.setExceptionHandler(function(d){ return true; });
|
|
send({type:'armed'});
|
|
var t0 = Date.now();
|
|
|
|
function hexb(p, n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(function(b){return ('0'+b.toString(16)).slice(-2)}).join(''); }catch(e){ return ''; } }
|
|
function head(p, n){ try{ return p.readCString(n); }catch(e){ return ''; } }
|
|
function classify(head){
|
|
var ms = [['dfpReport','dfpReport'],['hylogout','hylogut'],['hyanonymousCredlogin','hyanonym'],
|
|
['hyCredlogin','hyCredlogin'],['dckey','dckey'],['PrtReq','PrtReq'],['get3','get3'],
|
|
['verify3','verify3']];
|
|
for (var i=0;i<ms.length;i++){ if (head.indexOf(ms[i][0]) >= 0) return ms[i][1]; }
|
|
return 'other';
|
|
}
|
|
|
|
var n = 0;
|
|
try{
|
|
var r = new ApiResolver('module');
|
|
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
|
|
Interceptor.attach(m.address, {
|
|
onEnter: function(a){
|
|
var len = a[2].toInt32();
|
|
if (len < 40 || len > 50000) return;
|
|
var h = head(a[1], Math.min(len, 2000));
|
|
if (!h) return;
|
|
var name = classify(h);
|
|
// 过滤明显无关 (cert请求等), 保留虎牙业务
|
|
if (name === 'other' && h.indexOf('POST /') < 0 && h.indexOf(' 1@') < 0 && h.indexOf('PUT /') < 0) return;
|
|
n++;
|
|
send({type:'req', n:n, cls:name, len:len, head:h.slice(0,80),
|
|
hex:hexb(a[1], len), t:Date.now()-t0});
|
|
}
|
|
});
|
|
});
|
|
send({type:'sslwrite_hooked'});
|
|
}catch(e){ send({type:'sslwrite_err', e:String(e)}); }
|
|
|
|
try{
|
|
var r2 = new ApiResolver('module');
|
|
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
|
|
Interceptor.attach(m.address, {
|
|
onEnter: function(a){ this.buf=a[1]; this.cap=a[2].toInt32(); },
|
|
onLeave: function(ret){
|
|
var n2 = ret.toInt32();
|
|
if (n2 <= 0 || n2 > 3200) return;
|
|
send({type:'resp', len:n2, hex:hexb(this.buf, n2), t:Date.now()-t0});
|
|
}
|
|
});
|
|
});
|
|
send({type:'sslread_hooked'});
|
|
}catch(e){ send({type:'sslread_err', e:String(e)}); }
|
|
"""
|
|
|
|
|
|
def load_with_retry(session, js, n=5):
|
|
last = None
|
|
for i in range(n):
|
|
try:
|
|
s = session.create_script(js)
|
|
s.load()
|
|
return s
|
|
except Exception as e:
|
|
last = e
|
|
print(f"[retry {i}] {e}", flush=True)
|
|
time.sleep(3)
|
|
raise last
|
|
|
|
|
|
def main():
|
|
# 清旧实例 (多进程坑)
|
|
import subprocess
|
|
subprocess.run(["adb", "shell", "am", "force-stop", PACKAGE], capture_output=True)
|
|
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
|
|
pid = device.spawn([PACKAGE])
|
|
print(f"[*] spawned {PACKAGE} pid={pid}", flush=True)
|
|
session = device.attach(pid)
|
|
|
|
events = []
|
|
|
|
def on_message(message, data):
|
|
if message.get("type") == "error":
|
|
print("[JS-ERR]", str(message)[:300], flush=True)
|
|
return
|
|
if message.get("type") != "send":
|
|
return
|
|
p = message.get("payload") or {}
|
|
t = p.get("type")
|
|
if t == "armed":
|
|
print("[*] JS armed", flush=True)
|
|
elif t == "sslwrite_hooked":
|
|
print("[*] SSL_write hooked", flush=True)
|
|
elif t == "sslread_hooked":
|
|
print("[*] SSL_read hooked", flush=True)
|
|
elif t == "req":
|
|
print(f"[{p.get('t')}ms] [{p.get('n')}] {p.get('cls'):12s} len={p.get('len')}", flush=True)
|
|
events.append({"type": "req", **p})
|
|
OUT.write_text(json.dumps(events))
|
|
elif t == "resp":
|
|
h = p.get('hex', '')
|
|
import re as _re
|
|
av = _re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(h))
|
|
mark = f" actionV={av.group(1).decode()}" if av else ""
|
|
print(f"[{p.get('t')}ms] [resp] len={p.get('len')}{mark}", flush=True)
|
|
events.append({"type": "resp", **p})
|
|
OUT.write_text(json.dumps(events))
|
|
elif t in ("sslwrite_err", "sslread_err"):
|
|
print(f"[*] {t}: {p.get('e')}", flush=True)
|
|
|
|
# 严格约定顺序
|
|
print("[*] loading bypass", flush=True)
|
|
load_with_retry(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
|
|
load_with_retry(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
|
|
device.resume(pid)
|
|
time.sleep(11)
|
|
load_with_retry(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text())
|
|
print("[*] loading 主 hook JS (最后)", flush=True)
|
|
script = load_with_retry(session, JS)
|
|
script.on("message", on_message)
|
|
print("[*] 已 resume。捕获取请求链 (冷启动自动触发 + 登录触发)", flush=True)
|
|
try:
|
|
while True:
|
|
time.sleep(5)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
print(f"[*] 结束 -> {OUT}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |