chore: 虎牙逆向考古资料移出 git 跟踪 — rm --cached + .gitignore 兜底

不再跟踪(本地保留):
- evidence/  抓包证据/反编译/IDA 数据库/.so dump (1133 文件)
- scripts/   逆向实验脚本 (仅保留 migrate_sqlite_to_mysql.py)
- tools/     frida hook/抓包 + unidbg hydev + 虎牙协议复刻工具

保留跟踪:
- e语言/  docs/  core/huya/verification/models/  core/huya/fingerprint/

Dockerfile: COPY scripts/ 收窄为仅业务迁移脚本。
This commit is contained in:
yml2213
2026-08-30 12:56:25 +08:00
parent eef86bd855
commit 1413b4f030
1382 changed files with 10 additions and 394161 deletions
-133
View File
@@ -1,133 +0,0 @@
#!/usr/bin/env python3
"""铸证活体验证:用重铸证书(新随机数)走完整扫码绑定闭环。
实验设计:
A 对照组: 今天 getQUrlData 抓到的原始 wupData(原证书) -> 期望 bind 成功
B 实验组: 同指纹/同cred、全新20B随机数, tools/cert_forge 重铸证书,
原位替换 wupData 内层证书b64字段 -> 期望 bind 成功且 uid 相同
若 B 成功 => 服务端接受离线铸造的证书, 任意账号铸证只剩 cred 来源问题。
用法: /Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python scripts/probe_forge_cert_bind.py
"""
from __future__ import annotations
import base64
import json
import re
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
sys.path.insert(0, str(ROOT / "scripts"))
from tools.cert_forge import build_p1, forge_cert, parse_p1 # noqa: E402
from probe_huya_qr_bind import QrRole, web_behavior, log # noqa: E402
from core.huya.device_fingerprint import get_huya_sdid # noqa: E402
def load_capture():
ev = json.load(open(ROOT / "evidence/cert_keycap.json"))
qurl = next(e["data"] for e in ev if e.get("type") == "qurl_done" and e.get("data"))
p1_hex = next(e for e in ev if e.get("type") == "enc_in")["x1s"]["v"]
return qurl, bytes.fromhex(p1_hex)
def find_cert_span(raw: bytes):
"""定位 wupData 内层证书 b64 字段(解码后194B且以0x0c开头)。"""
for m in re.finditer(rb"[A-Za-z0-9+/]{100,}={0,2}", raw):
try:
d = base64.b64decode(m.group() + b"=" * ((-len(m.group())) % 4))
except Exception:
continue
if len(d) >= 194 and d[0] == 0x0C:
return m.start(), m.end(), d[:194]
raise RuntimeError("未找到内层证书b64字段")
def run_flow(pc: QrRole, ph: QrRole, wup: str) -> dict | None:
beh, page = web_behavior()
resp = pc.call("/qrLgn/getQrId", "70001", {
"behavior": beh, "type": "", "domainList": "", "page": page})
rc = resp.get("returnCode")
qrid = ((resp.get("data") or {}).get("qrId")) if rc == 0 else None
log(f"[getQrId] rc={rc} qrId={qrid}")
if not qrid:
return None
cpage = f"https://aq.huya.com/r/confirm.html?k={qrid}&id=5002"
from urllib.parse import quote
r1 = ph.call("/qrLgn/scanQrPicNotify", "70005", {
"qrId": qrid, "wupData": wup,
"behavior": quote("[]", safe=""), "page": quote(cpage, safe="")})
d1 = r1.get("data") or {}
log(f"[scanQr] rc={r1.get('returnCode')} stage={d1.get('stage')} "
f"data={json.dumps(d1, ensure_ascii=False)[:200]}")
r2 = ph.call("/qrLgn/bindQrLoginUser", "70007", {
"qrId": qrid, "wupData": wup,
"behavior": quote("[]", safe=""), "page": quote(cpage, safe="")})
d2 = r2.get("data") or {}
log(f"[bindQr] rc={r2.get('returnCode')} stage={d2.get('stage')} "
f"msg={r2.get('message')} desc={r2.get('description')}")
if r2.get("returnCode") != 0 or d2.get("stage") != 2:
return {"bind_fail": True, "rc": r2.get("returnCode"),
"desc": r2.get("description"), "scan_uid": d1.get("uid")}
for i in range(10):
rt = pc.call("/qrLgn/tryQrLogin", "70003", {
"qrId": qrid, "remember": "1", "domainList": "",
"behavior": beh, "page": page})
dt = rt.get("data") or {}
if dt.get("stage") == 2 and dt.get("biztoken"):
return {"ok": True, "uid": dt.get("uid"),
"biztoken_len": len(dt["biztoken"]),
"biztoken": dt["biztoken"]}
time.sleep(2)
return {"poll_timeout": True}
def main() -> int:
qurl, P1_old = load_capture()
fields = parse_p1(P1_old)
raw = base64.b64decode(qurl)
s0, s1, orig_cert = find_cert_span(raw)
log(f"wupData {len(raw)}B, 证书b64字段 @{s0}-{s1}, "
f"原cert idx={orig_cert[1]:#04x}")
# ---- 实验组材料 ----
# 已证: 服务端校验 nonce 字段内容(纯随机被40020拒), 但原证书可无限重放
# => 不校验时效。故铸造策略 = 保留原 nonce/指纹, 替换 cred 字段。
# 实验 C: 换上 frida_cred_dump 里更早签发的 hyCred(同账号不同轮换值)。
# 若 bind 成功 => 铸造机制完全打通(任意有效cred即可铸其证书)。
old_cred = json.load(open("/Users/yml/codes/douyu_login_py/evidence/auto_test_cred.json"))
cred_old = bytes.fromhex(old_cred["cred_hex"])
P1_new = build_p1(b"5008", fields["fingerprint"], cred_old,
rnd=fields["rnd"]) # 原始 nonce
assert P1_new != P1_old, "应与原P1不同"
cert_new = forge_cert(P1_new, key_idx=orig_cert[1])
b64_new = base64.b64encode(cert_new).decode()
assert len(b64_new) == s1 - s0, (len(b64_new), s1 - s0)
raw_forged = raw[:s0] + b64_new.encode() + raw[s1:]
wup_forged = base64.b64encode(raw_forged).decode()
sdid = get_huya_sdid(allow_fallback=False).sdid
results = {}
for name, wup in (("C_forged_swapped_cred", wup_forged),):
log(f"\n======== 实验 {name} ========")
pc = QrRole(pc=True, sdid=sdid)
ph = QrRole(pc=False, sdid=sdid)
results[name] = run_flow(pc, ph, wup)
log(f"[result {name}] {json.dumps(results[name], ensure_ascii=False)[:300]}")
ok_c = (results.get("C_forged_swapped_cred") or {}).get("ok")
log("\n==== 结论 ====")
log(f"换cred铸证: {'✅ 通过' if ok_c else '❌ 被拒'} "
f"(uid={results['C_forged_swapped_cred'].get('uid')})")
(ROOT / "evidence/forge_bind_result.json").write_text(json.dumps(
{"results": results,
"p1_new_hex": P1_new.hex(), "ts": time.time()}, ensure_ascii=False, indent=1))
return 0 if ok_c else 1
if __name__ == "__main__":
raise SystemExit(main())
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dfpReport 同步 pair 分析器
- 从 evidence/dfp_pair_sync.json 提取每个 wire 的 tag2 密文 (去掉 10B 魔数 + 9B 尾部字段)
- 结合 plains 内存缓冲 (JSON 头 + 二进制尾) 计算 keystream
- 验证输入布局假设: input = [JSON 554B][binary L-554B]
"""
import json, binascii, sys
MAGIC = bytes.fromhex('571882cf664bb39401ee')
TRAILER = bytes.fromhex('00400c0b8c980ca80c')
EVID = 'evidence/dfp_pair_sync.json'
def load_pairs():
d = json.load(open(EVID))
pairs = []
for i in range(0, len(d), 2):
w, p = d[i], d[i+1]
raw = binascii.unhexlify(w['hex'])
idx = raw.find(b'\r\n\r\n')
body = raw[idx+4:]
mp = body.find(MAGIC)
assert body.endswith(TRAILER), f"pair{i//2} trailer mismatch"
tag2 = body[mp+10 : len(body)-len(TRAILER)]
plains = []
for pl in p['plains']:
if isinstance(pl, dict) and pl.get('hex'):
plains.append({'addr': pl['addr'], 'buf': binascii.unhexlify(pl['hex'])})
pairs.append({'t': w['t'], 'tag2': tag2, 'plains': plains})
return pairs
def find_json_end(buf):
"""locate the end of the 554B JSON string at buf head; return index after closing brace + boundaries of following non-zero region"""
# JSON starts with '{' at offset 0. Find the matching close at known length 554
if buf[:1] != b'{':
return None
# standard JSON serialized 554B for this device
json_bytes = buf[:554]
assert json_bytes.startswith(b'{"appId":"5008"'), json_bytes[:40]
return 554
def nonzero_ranges(buf, start):
"""return list of (start,end) runs of non-zero bytes in buf[start:]"""
runs = []
in_run = False
for i in range(start, len(buf)):
if buf[i] != 0 and not in_run:
s = i; in_run = True
elif buf[i] == 0 and in_run:
runs.append((s, i)); in_run = False
if in_run:
runs.append((s, len(buf)))
return runs
def main():
pairs = load_pairs()
print(f"{len(pairs)} pairs loaded")
for k, pr in enumerate(pairs):
t2 = pr['tag2']
print(f"\n===== pair{k} t={pr['t']} tag2_len={len(t2)} (data={len(t2)}) =====")
for pi, pl in enumerate(pr['plains']):
buf = pl['buf']
je = find_json_end(buf)
runs = nonzero_ranges(buf, 0) if je else []
# focus: runs after JSON end
post = [r for r in runs if r[0] >= 554]
print(f" plain{pi} addr={pl['addr']} buflen={len(buf)} json@0..{je if je else '?'}")
# show a compact view: for each 256B block 0..8192 whether zero or nonzero
blocks = []
for b in range(0, len(buf), 256):
chunk = buf[b:b+256]
nz = sum(1 for x in chunk if x != 0)
blocks.append(f"{b//256}:{nz}")
print(" nz/256B:", ' '.join(blocks))
# print runs summary first 6
for r in post[:6]:
print(f" nonzero {r[0]}..{r[1]} (len {r[1]-r[0]}) head={buf[r[0]:r[0]+32].hex()}")
if __name__ == '__main__':
main()
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env python3
"""通用 aarch64 反汇编工具: 分析内存 dump 的 libhydeviceid.so (解密后).
用法:
arm64_disasm.py <dump.so> <模块基址> <偏移hex> [长度字节]
例:
反汇编 JNI_OnLoad(0x22c4c8) 前 0x300 字节:
arm64_disasm.py evidence/diag_phone/libhydeviceid_dump.so 0x7814211000 0x22c4c8 0x300
打印所有 BL/BLR 分支目标到模块偏移.
"""
from __future__ import annotations
import sys
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM
def load(fn):
with open(fn, "rb") as f:
return f.read()
def main():
dump_path = sys.argv[1]
base = int(sys.argv[2], 16)
start_off = int(sys.argv[3], 16) # 相对模块基址的偏移 (内存态文件里约等于文件偏移)
length = int(sys.argv[4], 16) if len(sys.argv) > 4 else 0x200
data = load(dump_path)
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
md.detail = True
code = data[start_off:start_off + length]
print(f"# base=0x{base:x} start_off=0x{start_off:x} len=0x{length:x}")
for insn in md.disasm(code, base + start_off):
disp = ""
if insn.mnemonic.startswith(("b.", "b ")) or insn.mnemonic in ("bl", "br", "blr", "cbz", "cbnz"):
if insn.operands:
disp = f" ; -> 0x{insn.operands[0].imm:x}"
elif insn.mnemonic == "blr":
disp = " ; [indirect call]"
print(f" 0x{insn.address:x}\t{insn.mnemonic:8s} {insn.op_str}{disp}")
if __name__ == "__main__":
main()
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env python3
"""attach + 全部 bypass: 观察 attach 路径加载 bypass 后 App 是否存活。
用于定位 attach 后反调试点。
"""
from __future__ import annotations
import frida, time, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASSES = ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js", "patch_guard_block_termination.js"]
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"], capture_output=True)
print("app launched normally, waiting 8s...", flush=True)
time.sleep(8)
d = frida.get_device_manager().add_remote_device(REMOTE)
ps = [p for p in d.enumerate_processes() if 'kiwi' in p.name or 'duowan' in p.name]
if not ps:
print("no kiwi process", flush=True); return
pid = ps[0].pid
print("attach pid", pid, flush=True)
s = d.attach(pid)
for name in BYPASSES:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text())
def on_msg(m, dd, n=name):
if m.get('type')=='send':
print(f"[{n}] {m.get('payload')}", flush=True)
elif m.get('type')=='error':
print(f"[{n}] ERR {str(m)[:120]}", flush=True)
sc.on('message', on_msg)
sc.load(); time.sleep(0.3)
except Exception as e:
print(f"[{name}] load ERR {e}", flush=True)
print("all bypass loaded. observing 30s", flush=True)
prev = 0
for t in [3, 6, 10, 15, 20, 30]:
time.sleep(t - prev); prev = t
try:
alive = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
break
print(f" +{t}s alive={bool(alive)}", flush=True)
if not alive:
print(f"=> DEAD at +{t}s", flush=True); break
try: s.detach()
except: pass
if __name__ == "__main__":
main()
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env python3
"""attach + 全部 bypass + 主 hook: 验证 attach 路径能存活且抓到 dfpReport。
这是"不挂起、不 spawn"的路线, 无 EGL 崩问题。
"""
from __future__ import annotations
import frida, time, subprocess, json, re
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_attach_full.json")
BYPASSES = ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js", "patch_guard_block_termination.js"]
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'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];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn)});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"], capture_output=True)
print("app launched, waiting for dfp cold-start... attach fast", flush=True)
d = frida.get_device_manager().add_remote_device(REMOTE)
# 用 adb pidof 确定的 pid (更可靠)
pid = None
for _ in range(15):
r = subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE], capture_output=True, text=True)
p = r.stdout.strip()
if p:
pid = int(p)
break
time.sleep(1)
if not pid:
# fallback frida enumerate
ps = [x for x in d.enumerate_processes() if 'kiwi' in x.name or 'duowan' in x.name]
pid = ps[0].pid if ps else None
print("attach pid", pid, flush=True)
s = d.attach(pid)
for name in BYPASSES:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text()); sc.load(); time.sleep(0.2)
except Exception as e:
print(f"bp err {name} {e}", flush=True)
events = []
def on_main(m, dta):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:120], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed': print("[*] armed", flush=True)
elif t == 'hooked': print("[*] SSL_write hooked", flush=True)
elif t in ('err','readerr'): print("[*]", t, p.get('e'), flush=True)
elif 'cls' in p:
print(f"[*] {p['cls']} len={p['len']}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif t == 'resp':
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[*] RESP len={p['len']}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = s.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 观察 60s, 若触发 dfpReport 会有; 同时可通过 am 触发
print("[*] observing 60s", flush=True)
prev = 0
for t in [5,10,15,20,30,40,50,60]:
time.sleep(t-prev); prev=t
try:
alive = [p for p in d.enumerate_processes() if p.pid==pid]
except Exception: break
if not alive: print(f"=> DEAD at +{t}s", flush=True); break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
-46
View File
@@ -1,46 +0,0 @@
"""模拟器 attach + 真机G2-0055组合bypass 存活测试. attach避开EGL崩, G2-0055过反调试."""
from pathlib import Path
import frida, time, subprocess
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASS="bypass_msaoaid_maps_skip_cleanup.js"
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
for r in range(1,4):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"],capture_output=True)
time.sleep(7)
# attach
pid=None
for _ in range(10):
rr=subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE],capture_output=True,text=True)
if rr.stdout.strip(): pid=int(rr.stdout.strip()); break
time.sleep(1)
if not pid: print(f"r{r} no pid"); continue
try:
s=d.attach(pid)
except Exception as e:
print(f"r{r} attach err {e}"); time.sleep(2); continue
# 加载 G2-0055
def on(m,dd):
if m.get('type')=='send':
p=m.get('payload');
if isinstance(p,dict) and p.get('event') in ('installed','predicate-branch-patched','art-callsite-patched','name-masked','fd-masked','maps-entry','art-entry','art-callsite'):
print(f" r{r} {p['event']}",flush=True)
elif m.get('type')=='error': print(f" r{r} ERR {str(m)[:80]}",flush=True)
sc=s.create_script((RE/"evidence/scripts"/BYPASS).read_text()); sc.on('message',on); sc.load()
print(f"r{r} attach pid={pid} +G2-0055 loaded, 观察存活",flush=True)
prev=0; died=None
for t in [3,6,10,15,20,30,40,50,60]:
time.sleep(t-prev); prev=t
try: alive=[p for p in d.enumerate_processes() if p.pid==pid]
except: break
if not alive:
died=t
print(f" r{r} DEAD at +{t}s",flush=True); break
if not died: print(f" r{r} alive 60s!",flush=True)
try: s.detach()
except: pass
time.sleep(2)
if __name__=="__main__": main()
-60
View File
@@ -1,60 +0,0 @@
#!/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<watch:
time.sleep(4)
p=pid_of()
if p is None: die=f"{time.time()-t0:.0f}s"; break
act=top_act()
if act and (not trail or trail[-1]!=act): trail.append(act)
log=sh("adb","-s",ADB,"logcat","-d")
anr = "ANR in com.duowan.kiwi" in log
fatal = "FATAL EXCEPTION" in log and PKG in log
entered = any("Splash" not in x for x in trail)
tag = f"{label}#{a}"
print(f"{tag}: die={die or 'no'} ANR={anr} FATAL={fatal} entered={entered}")
print(f" trail: {' -> '.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)
-109
View File
@@ -1,109 +0,0 @@
#!/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<watch:
time.sleep(4)
p=pid_of()
if p is None: L1_ok=False; die=f"{time.time()-t0:.0f}s"; break
if p!=pid0: L1_ok=False; die=f"replaced@{time.time()-t0:.0f}s"; break
act=top_act()
if act and (not trail or trail[-1]!=act): trail.append(act)
log=sh("adb","-s",ADB,"logcat","-d")
L3_ok = not (("ANR in com.duowan.kiwi" in log) or ("FATAL EXCEPTION" in log and PKG in log))
entered = any("Splash" not in x for x in trail)
final = trail[-1] if trail else "(none)"
frozen = main_thread_frozen(pid_of()) if not die else None
warn = input_anr_warning()
# 层面2: 通过 splash + 主线程 utime 有增量 (首页没冻死) + 无 input ANR 前兆
L2_ok = entered and (frozen is False) and not warn
r={"L1_pid":L1_ok, "L2_ui":L2_ok, "L3_crash":L3_ok, "die":die, "final_act":final, "trail":trail, "main_frozen":frozen, "input_anr":warn}
r["PASS"]=L1_ok and L2_ok and L3_ok
try: s.detach()
except Exception: pass
return r
def main():
args=sys.argv[1:]
attempts=1; recipe="merged"; watch=45; delay=11000
i=0
while i<len(args):
if args[i]=="--recipe": recipe=args[i+1]; i+=2
elif args[i]=="--watch": watch=int(args[i+1]); i+=2
elif args[i]=="--delay": delay=int(args[i+1]); i+=2
else: attempts=int(args[i]); i+=1
results=[check_once(recipe, watch, delay) for _ in range(attempts)]
for n,r in enumerate(results,1):
print(f"attempt{n}: L1_pid={r['L1_pid']} L2_ui={r['L2_ui']} L3_crash={r['L3_crash']} die={r['die']} final={r['final_act']} frozen={r['main_frozen']} inputANR={r['input_anr']}")
print(f" trail: {' -> '.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()
-30
View File
@@ -1,30 +0,0 @@
#!/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
-127
View File
@@ -1,127 +0,0 @@
#!/usr/bin/env python3
""""一帧"dfpReport(含响应 actionV)—— 自动重试, 直到拿到为止。
目标:每一台干净模拟器, 抓到一帧完整 dfpReport wire + 对应的 606B 响应(actionV),
即获得这台"设备"的纯 Python 可重放身份凭证。App 存活 3-6s 足够(dfpReport 启动即发)。
"""
from __future__ import annotations
from pathlib import Path
import frida, time, subprocess, json, re
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/frame_" + time.strftime("%H%M%S") + ".json")
BYPASS = "bypass_msaoaid_maps_skip_cleanup.js" # 真机验证过的组合, 过 frida 反调试
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var got_dfp=false;
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0){
got_dfp=true;
send({type:'dfp',len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'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];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn)});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def one_try(d, attempt, result):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1.2)
pid = d.spawn([PACKAGE])
print(f"[att{attempt}] spawn pid={pid}", flush=True)
s = d.attach(pid)
# 挂起内加载 bypass (快速)
try:
sc = s.create_script((RE/"evidence/scripts"/BYPASS).read_text()); sc.load()
except Exception as e:
print(f"[att{attempt}] bypass err {e}", flush=True)
d.resume(pid)
print("[att%d] resumed (bypass loaded)" % attempt, flush=True)
got_dfp = False
def on_main(m, dd):
nonlocal got_dfp
if m.get('type') == 'error':
print(f"[att{attempt}] JSErr {str(m)[:100]}", flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print(f"[att{attempt}] SSL_write hooked", flush=True)
elif t == 'dfp':
got_dfp = True
print(f"[att{attempt}] dfpReport len={p['len']}", flush=True)
result['dfp_wire'] = p['hex']
result['dfp_pid'] = pid
elif t == 'resp':
# 保存 606B 响应 (可能含 actionV)
result.setdefault('resps', []).append({'len': p['len'], 'hex': p['hex']})
if p['len'] == 606:
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p['hex'] or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
else:
mark = ""
print(f"[att{attempt}] resp len={p['len']}{mark}", flush=True)
mainsc = s.create_script(MAIN_JS)
mainsc.on('message', on_main)
mainsc.load()
# 观察 ~15s 内抓 dfp (dfp 启动即发, 快)
t0 = time.time()
while time.time() - t0 < 15:
time.sleep(2)
if got_dfp:
time.sleep(3) # 再等一帧响应
break
# 确认拿到 dfp 且其响应缺失则不强求
print(f"[att{attempt}] 结束观察, got_dfp={got_dfp}", flush=True)
try: d.kill(pid)
except: pass
return got_dfp
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
result = {}
for attempt in range(1, 6):
try:
ok = one_try(d, attempt, result)
if ok and result.get('dfp_wire'):
print(f"[*] SUCCESS att{attempt}: dfp_wire_len={len(result['dfp_wire'])//2}$", flush=True)
json.dump(result, open(OUT, 'w'), indent=2)
print(f"[*] saved {OUT}", flush=True)
return
except Exception as e:
print(f"[att{attempt}] ERR {repr(e)}", flush=True)
time.sleep(2)
print("[*] 5 次均未抓到 dfpReport", flush=True)
if __name__ == "__main__":
main()
-130
View File
@@ -1,130 +0,0 @@
#!/usr/bin/env python3
"""捕获 (明文JSON, 密文) 配对: 解 dfpReport 流式 keystream。
同时:
1. hook SSL_write 抓 dfpReport 密文 (魔数开头 3984B)
2. 扫描内存找明文 JSON ({"appId":"5008"...), dump 前 512 + 后 6000
命中配对后立即 pkill。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pair.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var captured = {wire: null, plain: null};
// ---- 1. SSL_write 抓密文 ----
try{
var fns = [];
var r = new ApiResolver('module');
fns = r.enumerateMatchesSync('exports:*!SSL_write').map(function(x){ return x.address; });
fns.slice(0, 6).forEach(function(p){
Interceptor.attach(p, {
onEnter: function(a){
var len = a[2].toInt32();
if (len < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
var arr = new Uint8Array(a[1].readByteArray(len));
var hex = '';
for (var j = 0; j < arr.length; j++) hex += ('0'+arr[j].toString(16)).slice(-2);
captured.wire = {len: len, hex: hex};
send({type:'wire_cap', len: len});
trySend();
}
});
});
}catch(e){}
// ---- 2. 扫描明文 JSON ----
function scanPlain(){
try{
var r2 = new ApiResolver('module');
Process.enumerateRanges('r--').forEach(function(rng){
if (rng.size > 1024*1024*256) return;
try{
var hits = Memory.scanSync(rng.base, rng.size, '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38');
hits.slice(0, 3).forEach(function(x){
var post = '';
try{
var arr = new Uint8Array(x.address.readByteArray(4096));
post = Array.from(arr).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('');
}catch(e){ return; }
captured.plain = {addr: String(x.address), hex: post};
send({type:'plain_cap', addr: String(x.address)});
trySend();
});
}catch(e){}
});
}catch(e){}
}
function trySend(){
if (captured.wire && captured.plain) {
var out = {wire: captured.wire, plain: captured.plain};
send({type:'PAIR', data: out});
}
}
setInterval(function(){ try{ scanPlain(); }catch(e){} }, 3000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "wire_cap":
print(f"[wire] 密文捕获 len={p.get('len')}", flush=True)
elif t == "plain_cap":
print(f"[plain] 明文JSON @{p.get('addr')}", flush=True)
elif t == "PAIR":
OUT.write_text(json.dumps(p.get('data')))
print(f"[PAIR] 保存 -> {OUT}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, 请触发登录, PAIR 出现即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
-126
View File
@@ -1,126 +0,0 @@
#!/usr/bin/env python3
"""抓取真正"同时刻"的 (明文JSON, 密文) 对。
SSL_write 命中 dfpReport 时, 明文刚生成/正在内存, 立即全内存扫描
{"appId":"5008" 快照 -> 与本次密文构成同步 pair。命中即持久化。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pair_sync.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hexb(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return null; }
}
function hexs(buf){ return Array.from(new Uint8Array(buf)).map(function(x){return ('0'+x.toString(16)).slice(-2);}).join(''); }
// 同步: SSL_write 后 50ms 扫描明文 (此时 JSON 应仍在堆)
function scanPlainNow(tag){
var out = [];
try{
Process.enumerateRanges('r--').forEach(function(rng){
if (rng.size > 1024*1024*256) return;
try{
var hits = Memory.scanSync(rng.base, rng.size, '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38');
hits.slice(0, 4).forEach(function(x){
var post = null;
try{ post = x.address.readByteArray(4096); }catch(e){ return; }
out.push({addr: String(x.address), hex: hexs(post)});
});
}catch(e){}
});
}catch(e){}
if (out.length) send({type:'sync_plain', tag:tag, plains:out});
}
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 < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
var hex = hexb(a[1], len);
var t = Date.now();
send({type:'wire', len:len, hex:hex, t:t});
setTimeout(function(){ scanPlainNow(t); }, 80);
}
});
});
}catch(e){}
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "wire":
print(f"[wire] len={p.get('len')}", flush=True)
events.append({"type": "wire", "t": p.get('t'), "hex": p.get('hex')})
OUT.write_text(json.dumps(events))
elif t == "sync_plain":
print(f"[sync_plain] tag={p.get('tag')} {len(p.get('plains'))}", flush=True)
events.append({"type": "plain", "t": p.get('tag'), "plains": p.get('plains')})
OUT.write_text(json.dumps(events))
for pl in p.get('plains'):
try:
h = pl['hex']
j = h.index('7b226170704964223a2235303038')
asc = bytes.fromhex(h[j:j+700])
s = ''.join(chr(x) if 32<=x<127 else '.' for x in asc)
print(f" @{pl['addr']}: {s[:200]}", flush=True)
except Exception: pass
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume. 请 退出登录->重新登录 (wire 后立即抓同刻明文)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""Runtime edge capture on live libhydeviceid generation:
1) hook __system_property_get -> what system inputs drive triple/device derivation
2) hook SSL_write -> the dfpReport wire (to correlate)
Only needs the ~3-6s spawn window (dfpReport fires ~1s after start).
"""
from pathlib import Path
import frida, time, subprocess, json, sys
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT=Path("/Users/yml/codes/douyu_login_py/evidence/propedge_" + time.strftime("%H%M%S") + ".json")
JS = r"""
'use strict';
send({type:'armed'});
function rcs(p,n){try{return p.readCString(n)||'';}catch(e){return '';}}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
var START = Date.now(); var t0 = Math.floor(Date.now()/1000);
// 1) system property reads
try{
var pg = Module.findExportByName('libc.so','__system_property_get');
if(pg){
Interceptor.attach(pg,{onEnter:function(a){this.name=rcs(a[0],256);this.valbuf=a[1];},
onLeave:function(ret){
try{
var val=rcs(this.valbuf,512);
// 只关心设备/身份派生相关属性, 避免把框架进程的读取混进来
var isDevice = this.name.indexOf('ro.product')===0 || this.name.indexOf('ro.serialno')===0
|| this.name.indexOf('ro.boot')===0 || this.name.indexOf('ro.hardware')===0
|| this.name.indexOf('ro.build')===0 || this.name=== 'ro.secure'
|| this.name==='ro.debuggable' || this.name.indexOf('ro.kernel')===0
|| this.name.indexOf('gsm.')===0 || this.name.indexOf('persist.sys')===0
|| this.name.indexOf('qemu')===0;
if(isDevice){ send({type:'prop',dt:Date.now()-START,name:this.name,val:val.slice(0,200)}); }
}catch(e){}
}});
}
send({type:'prop_hooked'});
}catch(e){send({type:'err',e:String(e)});}
// 2) SSL_write dfpReport wire
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<50||len>50000)return;
var h=rcs(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0){
send({type:'dfp',dt:Date.now()-START,len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'dfp_hooked'});
}catch(e){send({type:'dfperr',e:String(e)});}
// 3) also capture plaintext JSON candidate: hook memcpy/strlen leaving std::string? skip, noisy.
"""
def one_run(d, a, result, dump_props):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
pid=d.spawn([PACKAGE])
print(f"[att{a}] 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"[att{a}] bypass err {e}", flush=True)
d.resume(pid)
print(f"[att{a}] resumed", flush=True)
got=False; dfp_hook=False
def on(m,dd):
nonlocal got, dfp_hook
if m.get('type')=='error': print(f"[att{a}] JSErr {str(m)[:120]}", flush=True); return
p=m.get('payload') or {}
t=p.get('type')
if t=='prop_hooked': print(f"[att{a}] prop hooked", flush=True)
elif t=='dfp_hooked': dfp_hook=True; print(f"[att{a}] SSL_write hooked", flush=True)
elif t=='prop':
result.setdefault('props',[]).append({'dt':p.get('dt'),'name':p['name'],'val':p['val']})
elif t=='dfp':
got=True; print(f"[att{a}] dfp len={p['len']}", flush=True)
result['dfp_wire']=p['hex']; result['dfp_dt']=p.get('dt'); result['pid']=pid
sc=s.create_script(JS); sc.on('message',on); sc.load()
t0=time.time()
while time.time()-t0<20:
time.sleep(2)
if got: time.sleep(3); break
print(f"[att{a}] end got_dfp={got} (props={len(result.get('props',[]))})", flush=True)
# unique props by name
seen={}
for pr in result.get('props',[]):
seen.setdefault(pr['name'],[]).append(pr['val'])
if dump_props and seen:
print("--- unique props read ---")
for k,v in sorted(seen.items()):
print(f" {k} = {v[0][:120]} (x{len(v)})", flush=True)
try: d.kill(pid)
except: pass
return got
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
result={}
ok=False
for a in range(1,6):
try:
if one_run(d,a,result,dump_props=(a>=1)):
print(f"[*] SUCCESS att{a}", flush=True)
ok=True; break
except Exception as e:
print(f"[att{a}] ERR {repr(e)}", flush=True); time.sleep(2)
if ok or result.get('props'):
json.dump(result, open(OUT,'w'), indent=2)
print(f"[*] saved {OUT}", flush=True)
else:
print("[*] no capture", flush=True)
if __name__=="__main__": main()
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""精确定位三元组派生输入: hook __system_property_find + __system_property_read_callback,
只记录 name(不读值避免乱码), 并 hook Build 类 Java 侧读取.
目标: 找出 hdid/deviceId/appkey 由哪些系统属性派生.
"""
from pathlib import Path
import frida, time, subprocess, json
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT=Path("/Users/yml/codes/douyu_login_py/evidence/propnames_" + time.strftime("%H%M%S") + ".json")
JS=r"""
'use strict';
var names=new Set(); var dfpGot=false;
function rcs(p,n){try{return p.readCString(n)||'';}catch(e){return '';}}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
// 1) NDK property接口
try{
var pf=Module.findExportByName('libc.so','__system_property_find');
if(pf) Interceptor.attach(pf,{onEnter:function(a){var n=rcs(a[0],256); if(n&&!names.has(n)){names.add(n);send({type:'find',name:n});}}
});
}catch(e){send({type:'e',e:String(e)});}
try{
var pr=Module.findExportByName('libc.so','__system_property_read_callback');
if(pr) Interceptor.attach(pr,{onEnter:function(a){var n=rcs(a[0],256); if(n&&!names.has(n)){names.add(n);send({type:'readcb',name:n});}}
});
}catch(e){send({type:'e',e:String(e)});}
// 2) Java Build 字段访问 (System.getProperty / Build.MODEL等)
try{
Java.perform(function(){
var Sys=Java.use('java.lang.System');
Sys.getProperty.overload('java.lang.String').implementation=function(k){var v=this.getProperty(k); send({type:'sysprop',name:String(k),val:String(v)}); return v;};
var B=Java.use('android.os.Build');
var FINGERPRINT=B.FINGERPRINT.value; var MODEL=B.MODEL.value; var SERIAL=B.SERIAL.value; var MAN=B.MANUFACTURER.value;
send({type:'build',MODEL:String(MODEL),FINGERPRINT:String(FINGERPRINT),SERIAL:String(SERIAL),MAN:String(MAN)});
});
}catch(e){send({type:'jerr',e:String(e)});}
// 3) SSL_write dfp
try{
new ApiResolver('module').enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){var len=a[2].toInt32(); if(len<50||len>50000)return;
var h=rcs(a[1],Math.min(len,1500)); if(h.indexOf('dfpReport')>=0){dfpGot=true;send({type:'dfp',len:len,hex:hexb(a[1],len)});}}});
});
}catch(e){send({type:'e',e:String(e)});}
"""
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
result={}
for a in range(1,6):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True); time.sleep(1)
pid=d.spawn([PACKAGE]); s=d.attach(pid)
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except: pass
d.resume(pid)
found=[]; build=None; dfp=None
def on(m,dd):
nonlocal build, dfp
if m.get('type')=='error': print(f" JSErr {str(m)[:120]}",flush=True); return
p=m.get('payload') or {}; t=p.get('type')
if t=='find':
# 只记设备/系统相关
n=p['name']
if n.startswith(('ro.','persist.','gsm.','init.','sys.')) : found.append(n)
elif t=='sysprop': pass
elif t=='build': build=(p.get('MODEL'),p.get('FINGERPRINT'),p.get('SERIAL'),p.get('MAN')); print(f"[att{a}] Build:",p,flush=True)
elif t=='dfp': dfp=p['hex']; print(f"[att{a}] dfp len={p.get('len')}",flush=True)
sc=s.create_script(JS); sc.on('message',on); sc.load()
t0=time.time()
while time.time()-t0<20:
time.sleep(2)
if dfp: time.sleep(3); break
print(f"[att{a}] 属性共 {len(found)} 个; dfp={'got' if dfp else 'no'}",flush=True)
if found:
result['props']=sorted(set(found))
if dfp: result['dfp_wire']=dfp
try: d.kill(pid)
except: pass
if dfp: break
time.sleep(1)
json.dump(result,open(OUT,'w'),indent=2)
print("saved",OUT,flush=True)
if result.get('props'):
print("=== 设备/系统属性读取清单 ===")
for n in result['props']: print(" ",n,flush=True)
if __name__=="__main__": main()
-83
View File
@@ -1,83 +0,0 @@
#!/usr/bin/env python3
"""抓帧自动验证: spawn抓dfpReport → 重放 → 输出actionV(hdid).
用于对比"改IMEI/设备标识后身份是否变化"
"""
from pathlib import Path
import frida, time, subprocess, json, re, ssl, socket
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT=Path("/Users/yml/codes/douyu_login_py/evidence/identity_" + time.strftime("%H%M%S") + ".json")
MAIN_JS="""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var got=false;
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0){got=true;send({type:'dfp',len:len,hex:hexb(a[1],len)});}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
result={}
for attempt in range(1,7):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
try:
pid=d.spawn([PACKAGE]); s=d.attach(pid)
except Exception as e:
print(f"[att{attempt}] spawn/attach err {e}",flush=True); time.sleep(2); continue
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except: pass
d.resume(pid)
got=False
def on(m,dd):
nonlocal got
if m.get('type')!='send':return
p=m.get('payload') or {}
if p.get('type')=='dfp':
got=True
result['dfp_wire']=p['hex']; result['pid']=pid
print(f"[att{attempt}] dfpReport len={p['len']}",flush=True)
sc=s.create_script(MAIN_JS); sc.on('message',on); sc.load()
t0=time.time()
while time.time()-t0<18:
time.sleep(2)
if got: time.sleep(3); break
if got and result.get('dfp_wire'):
# 重放拿 actionV
wire=bytes.fromhex(result['dfp_wire'])
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
ss=ctx.wrap_socket(socket.create_connection(("wsapi.huya.com",443),timeout=10),server_hostname="wsapi.huya.com")
ss.sendall(wire); ss.settimeout(25); buf=b""
try:
while True:
c=ss.recv(8192)
if not c: break
buf+=c
if len(buf)>5000: break
except socket.timeout: pass
ss.close()
av=re.search(rb'actionV\(([0-9a-f]{40})',buf)
avd=av.group(1).decode() if av else None
result['actionV']=avd
print(f"[att{attempt}] actionV = {avd}",flush=True)
json.dump(result,open(OUT,'w'),indent=2)
print(f"[*] saved {OUT}",flush=True)
try: d.kill(pid)
except: pass
return
try: d.kill(pid)
except: pass
print("[*] 未抓到 dfpReport",flush=True)
if __name__=="__main__": main()
-118
View File
@@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""对照测试: attach + bypass 存活(多轮), 附加 main hook 存活(多轮)。
每种配置重复 ROUNDS 次, 记录每次的存活时间与崩溃点, 避免单次误判。
"""
from __future__ import annotations
import frida, time, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASSES = ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js", "patch_guard_block_termination.js"]
MAIN_JS_HOOK = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
// 只 hook SSL_write, 最小主 hook
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',len:len});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
ROUNDS = 3
OBSERVE = 35 # 每轮观察秒数
def launch():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1.2)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"], capture_output=True)
def get_pid(d, timeout=20):
for _ in range(timeout):
r = subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE], capture_output=True, text=True)
p = r.stdout.strip()
if p:
return int(p)
time.sleep(1)
return None
def load_bypass(s):
for name in BYPASSES:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text()); sc.load(); time.sleep(0.2)
except Exception as e:
print(f" bp {name} err {e}", flush=True)
def observe(d, pid, s, with_hook, tag):
t0 = time.time()
events = []
if with_hook:
sc = s.create_script(MAIN_JS_HOOK)
sc.on('message', lambda m, dd: events.append(m.get('payload')) if m.get('type')=='send' else None)
sc.load()
# 观察
prev = 0
died_at = None
for t in [5, 10, 15, 20, 25, 30, OBSERVE]:
time.sleep(t - prev); prev = t
try:
alive = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
broken = True
died_at = t
break
if not alive:
died_at = t
break
status = f"DEAD at +{died_at}s" if died_at else f"alive>{OBSERVE}s"
dfp = [e for e in events if isinstance(e, dict) and e.get('cls')=='dfpReport']
print(f" [{tag}] {status} | dfp hooks={len(dfp)}", flush=True)
return status, len(dfp)
def run_round(d, with_hook):
launch()
pid = get_pid(d)
if not pid:
return "launch-fail", 0
try:
s = d.attach(pid)
except Exception as e:
return f"attach-err {e}", 0
load_bypass(s)
return observe(d, pid, s, with_hook, "hook" if with_hook else "bypass-only")
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
print("=== A: 纯 attach + bypass (无主hook), 重复 %d 轮 ===" % ROUNDS, flush=True)
for i in range(1, ROUNDS+1):
st, n = run_round(d, with_hook=False)
print(f" A#{i}: {st}", flush=True)
time.sleep(2)
print("=== B: attach + bypass + SSL_write主hook, 重复 %d 轮 ===" % ROUNDS, flush=True)
for i in range(1, ROUNDS+1):
st, n = run_round(d, with_hook=True)
print(f" B#{i}: {st} (dfp={n})", flush=True)
if __name__ == "__main__":
main()
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""dfpReport 生成流程 python 验证模型。
目标: 用手头真实 wire 验证对格式的理解:
wire = HTTP POST / (wsapi.huya.com, okhttp/3.14.9)
+ body(taf/wup): 长度头 + servant + "dfpReport" + "tReq"
+ MAGIC(571882cf664bb39401ee) + cw
cw = [明文JSON ^ keystream][collection ^ keystream]
其中 keystream 是每帧独立随机流(未知生成器)。本脚本验证"格式+布局"理解,
并验证给定完整明文(JSON+collection)与keystream可精确重建真实wire。
"""
from __future__ import annotations
import json, binascii, struct, re
MAGIC = bytes.fromhex('571882cf664bb39401ee')
PREFIX_JSON = b'{"appId":"5008","appVer":"13.4.22"'
# 真机已验证的完整明文 JSON(含三元组)
REAL_JSON = ('{"appId":"5008","appVer":"13.4.22","appkey":"865a4924a40897ac1fcfe6b4c2cbb0e3",'
'"channel":"xiaomi","deviceId":"02df398797432eadefcc12767119ad5e80999389",'
'"deviceName":"M2102J2SC","hdid":"7c5387e0539c023c31c4ff0e807e7256117385ee",'
'"heightPixels":"2120","isCloud":0,"isForbidLog":1,"isHome":0,"isPre":0,'
'"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"sdkVer":"1.0.80138","servantName":"huyaudbwebui",'
'"shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"systemInfo":"android","systemVer":"M2102J2SC,30,11",'
'"terminalType":1,"testEnv":0,"widthPixels":"1080"}')
def build_json(triple=None, device_name='M2102J2SC', system_ver='M2102J2SC,30,11'):
t = triple or dict(appkey='865a4924a40897ac1fcfe6b4c2cbb0e3',
channel='xiaomi',
deviceId='02df398797432eadefcc12767119ad5e80999389',
hdid='7c5387e0539c023c31c4ff0e807e7256117385ee')
return ('{"appId":"5008","appVer":"13.4.22","appkey":"%(appkey)s","channel":"%(channel)s",'
'"deviceId":"%(deviceId)s","deviceName":"'+device_name+'","hdid":"%(hdid)s",'
'"heightPixels":"2120","isCloud":0,"isForbidLog":1,"isHome":0,"isPre":0,'
'"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"sdkVer":"1.0.80138","servantName":"huyaudbwebui",'
'"shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"systemInfo":"android","systemVer":"'+system_ver+'",'
'"terminalType":1,"testEnv":0,"widthPixels":"1080"}') % t
def parse_wire(wire):
"""从真实 wire 提取结构化字段. 返回 dict."""
body_start = wire.find(b'\r\n\r\n') + 4
body = wire[body_start:]
# taf 头: 4字节大端长度, 然后 TLV
# 简化解析: servant名
mi = body.find(MAGIC)
cw = body[mi+10:]
# JSON 密文段长度 + 明文校验
result = {
'body': body,
'cw': cw,
'magic_offset_in_body': mi,
'json_len': None, 'coll': None,
}
# 用已知JSON前缀恢复ks并反推JSON长度定位
if len(cw) >= len(PREFIX_JSON):
# JSON数据是明文XOR (明文可打印). 但我们不知道JSON结束位置(collection开始).
# 用"JSON固定长度"真机值(586)推断: 若明文JSON=XOR后全是可打印ascii且结构符合.
jlen = len(REAL_JSON.encode())
coll = cw[jlen:]
result['json_len'] = jlen
result['json_offset_in_cw'] = 0
result['coll'] = coll
return result
def verify_rebuild(wire):
"""验证: 用解析出的 cw, 从"明文JSON+keystream"能否精确重建 cw[:json_len]."""
p = parse_wire(wire)
cw = p['cw']
jb = REAL_JSON.encode()
jlen = len(jb)
# keystream = cw[:jlen] ^ jb
ks_json = bytes(cw[i]^jb[i] for i in range(jlen))
# 重建 encoded_json = jb ^ ks_json = jb (恒等, 因为ks由它们导出)
# 真正验证: 检查明文JSON在 cw 中位置对应的XOR是否=keystream连续(即JSON在此) — 无法独立验证,
# 但可验证: 明文JSON偏移与 ta f 头一致.
# 关键验证: keystream 在 JSON 段与 collection 段是否"同一生成器连续流".
# 我们对比 JSON 段 ks 和 collection 段(from某plaintext假设). 目前无collection明文故无法.
# 输出 keystream JSON段, 供分析.
return {
'jlen': jlen, 'cw_len': len(cw),
'ks_head': ks_json[:48].hex(),
'coll_len': len(p['coll']),
'ks_json_randomness': len(set(ks_json)),
}
def main():
frames = json.load(open('evidence/frame_real.json'))
wire = binascii.unhexlify(frames[0])
p = parse_wire(wire)
print("wire 总长: %d, body 长: %d, cw 长: %d" % (len(wire), len(p['body']), len(p['cw'])))
print("MAGIC 在 body 偏移: %d" % p['magic_offset_in_body'])
print("JSON 密文长度(586), collection 长度: %d" % len(p['coll']))
v = verify_rebuild(wire)
print("JSON keystream 前48B:", v['ks_head'])
print("keystream JSON段随机性(不同字节):", v['ks_json_randomness'], "/ 586")
# 打印 taf 头可读字符串
b = p['body']
for kw in [b'huyaudbwebui', b'dfpReport', b'android', b'tReq']:
print("body 含 '%s': @%d" % (kw.decode(errors='replace'), b.find(kw)))
if __name__ == '__main__':
main()
-148
View File
@@ -1,148 +0,0 @@
#!/usr/bin/env python3
"""挂起策略梯度实验: 确认 attach+maps遮蔽 是否能绕过 msaoaidsec 且不EGL崩.
E0: spawn→attach(空)→resume = 复现 session gone(静默死)
E1: spawn→attach→load mask脚本→resume = attach+maps遮蔽
E2: spawn→attach→load mask+sslhook→resume = 最终抓包形态
E3: 纯spawn→resume(不attach) = B组对照(应活)
每个 case 跑 2 轮, 每轮观察 30s 或死亡, logcat crash 同步。
"""
from pathlib import Path
import frida, time, json, subprocess, sys, re
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_DIR = Path("/Users/yml/codes/douyu_login_py/evidence/diag_egl_strategy")
SSL_HOOK = r"""
'use strict';
send({type:'armed'});
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){send({type:'ssl',len:a[2].toInt32(),t:Date.now()});}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def detach_cb(reason, detail):
print(f" [detached] reason={reason} detail={detail}", flush=True)
def run_case(d, case, rnd):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = None
session = None
t_spawn = time.time()
try:
if case in ("E4", "E5"):
# B组: 纯spawn→立即resume→App起来后再attach
pid = d.spawn([PACKAGE])
d.resume(pid)
print(f"[{case}.r{rnd}] spawn pid={pid} 立即resumed", flush=True)
time.sleep(1.5 if case == "E4" else 5.0)
t_att = time.time()
session = d.attach(pid)
session.on('detached', detach_cb)
print(f"[{case}.r{rnd}] attach at +{(time.time()-t_spawn)*1000:.0f}ms (attach耗时{(time.time()-t_att)*1000:.0f}ms)", flush=True)
suspend_ms = (time.time() - t_spawn) * 1000
elif case in ("E6", "E7"):
# 极短挂起: 只加载最少 bypass
pid = d.spawn([PACKAGE])
suspend_start = time.time()
session = d.attach(pid)
session.on('detached', detach_cb)
if case == "E6":
sc = session.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()); sc.load()
else:
sc = session.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()); sc.load()
sc2 = session.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()); sc2.load()
suspend_ms = (time.time() - suspend_start) * 1000
d.resume(pid)
print(f"[{case}.r{rnd}] spawn pid={pid} 挂起{suspend_ms:.0f}ms(1个art_callsite补丁) resumed", flush=True)
else:
pid = d.spawn([PACKAGE])
suspend_start = time.time()
if case in ("E1", "E2"):
session = d.attach(pid)
session.on('detached', detach_cb)
if case == "E1":
sc = session.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()); sc.load()
else:
sc = session.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()); sc.load()
sc2 = session.create_script(SSL_HOOK); sc2.load()
elif case == "E0":
session = d.attach(pid)
session.on('detached', detach_cb)
elif case == "E3":
pass # 纯spawn不attach
suspend_ms = (time.time() - suspend_start) * 1000
d.resume(pid)
print(f"[{case}.r{rnd}] spawn pid={pid} 挂起{suspend_ms:.0f}ms resumed", flush=True)
except Exception as e:
print(f"[{case}.r{rnd}] setup ERR {e}", flush=True)
return None
# 观察 30s
t0 = time.time()
died_at = None
pid_changed = None
while time.time() - t0 < 30:
time.sleep(2)
mp = main_pid()
if mp is None:
died_at = time.time() - t0
break
if mp != pid:
pid_changed = (mp, time.time() - t0)
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "20").stdout
egl = "EGL" in cr or "libEGL" in cr
crash_banner = bool(re.search(r"Fatal|signal \d|Abort", cr))
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
result = {
"case": case, "round": rnd, "pid": pid, "suspend_ms": round(suspend_ms, 1),
"died_at_s": round(died_at, 1) if died_at else None,
"pid_changed": pid_changed, "egl_crash": egl,
"crash_banner": crash_banner,
"crash_tail": cr[:300],
}
print(f"{result}", flush=True)
return result
def main():
cases = sys.argv[1:] or ["E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7"]
OUT_DIR.mkdir(parents=True, exist_ok=True)
d = frida.get_device_manager().add_remote_device(REMOTE)
results = []
for c in cases:
for rnd in (1, 2):
r = run_case(d, c, rnd)
if r: results.append(r)
out = OUT_DIR / "egl_strategy.json"
out.write_text(json.dumps(results, ensure_ascii=False, indent=1))
print(f"[*] {len(results)} 结果 -> {out}", flush=True)
if __name__ == "__main__":
main()
-129
View File
@@ -1,129 +0,0 @@
#!/usr/bin/env python3
"""存活/闪退四组对照诊断器 (统一度量):
指标1 proc_main: /proc/<pid>/cmdline == 包名 (主进程存活)
指标2 ui_focus: dumpsys window mCurrentFocus 是否 com.duowan.kiwi/* (UI 未闪退)
指标3 crash: logcat -b crash 是否出现 Fatal (native/java 崩溃)
A. 无frida 正常启动 (baseline)
B. spawn 挂起~0s 立即resume, 不加载任何脚本
C. spawn 挂起 加载2个bypass脚本后再resume (模拟v8/v9/stale)
D. attach 正常启动7s后 attach 主进程 (裸attach, 无脚本)
用法: python3 diag_lifecycle.py [a|b|c|d]
"""
from pathlib import Path
import frida, time, subprocess, sys, json, re
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_DIR = Path("/Users/yml/codes/douyu_login_py/evidence/diag_lifecycle")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def focus_ok():
r = adb("shell", "dumpsys", "window")
m = re.search(r"mCurrentFocus=Window\{([^}]+)\}", r.stdout)
if m:
f = m.group(1)
return PACKAGE in f, f
return False, "?"
def crash_count():
r = adb("shell", "logcat", "-d", "-b", "crash", "-t", "50")
return r.stdout.count("Fatal"), r.stdout[-2000:]
def snapshot(label):
p = main_pid()
ok, foc = focus_ok()
return {"label": label, "proc": p, "ui_ok": ok, "focus": foc}
def main():
which = (sys.argv[1] if len(sys.argv) > 1 else "a").lower()
OUT_DIR.mkdir(parents=True, exist_ok=True)
adb("shell", "logcat", "-c")
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = None
t_start = time.time()
timeline = []
label = which.upper()
if which in ("a",):
print("[A] 无frida 正常启动", flush=True)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
elif which in ("b",):
print("[B] spawn 挂起~0 立即 resume", flush=True)
pid = d.spawn([PACKAGE])
dt = time.time() - t_start
t_start = time.time()
d.resume(pid)
print(f" spawn->resume 挂起 {dt*1000:.0f}ms", flush=True)
elif which in ("c",):
print("[C] spawn 挂起+加载2个bypass再resume", flush=True)
pid = d.spawn([PACKAGE])
s = d.attach(pid)
for name in ("bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"):
t0 = time.time()
sc = s.create_script((RE / "evidence/scripts" / name).read_text())
sc.load()
print(f" loaded {name} +{time.time()-t0:.2f}s", flush=True)
dt = time.time() - t_start
t_start = time.time()
d.resume(pid)
print(f" spawn->resume 挂起 {dt*1000:.0f}ms", flush=True)
elif which in ("d",):
print("[D] 正常启动7s后 attach 主进程(裸)", flush=True)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(7)
pid = main_pid()
print(f" pid={pid}, 开始attach", flush=True)
try:
s = d.attach(pid)
sc = s.create_script("console.log('bare attach ok');")
sc.load()
print(" bare attach loaded", flush=True)
except Exception as e:
print(f" attach err {e}", flush=True)
t_start = time.time()
else:
print(f"未知: {which}"); return
# 观察 60s, 每 2s 快照
t0 = time.time()
while time.time() - t0 < 25:
time.sleep(2)
s = snapshot(label)
s["elapsed"] = round(time.time() - t0, 1)
timeline.append(s)
if s["ui_ok"]:
print(f" +{s['elapsed']:5.1f}s proc={s['proc']} UI={s['focus']}", flush=True)
else:
print(f" +{s['elapsed']:5.1f}s proc={s['proc']} UI_FAIL focus={s['focus']}", flush=True)
if not s["proc"] and s["elapsed"] > 3:
break
crashes, tail = crash_count()
final = snapshot(label)
result = {"case": label, "timeline": timeline, "final": final,
"crashes_in_buffer": crashes, "crash_tail": tail[:1500]}
out = OUT_DIR / f"diag_{label.lower()}.json"
out.write_text(json.dumps(result, ensure_ascii=False, indent=1))
print(f"[*] 结果 -> {out} | 最终: proc={final['proc']} ui_ok={final['ui_ok']} crashes={crashes}", flush=True)
if __name__ == "__main__":
main()
-201
View File
@@ -1,201 +0,0 @@
#!/usr/bin/env python3
"""真机版 存活/闪退四组对照诊断器 (M2102J2SC 5dd8c93f, fs152 15.2.2).
背景: 模拟器 GC3VE 上结论 = attach 主进程必死(msaoaidsec 静默 _exit) /
spawn+脚本必 EGL 崩。换真机后两项都要重测 —— 真机 GPU 正常、无模拟器特征,
msaoaidsec 在真机上的 frida 检测行为是未知数。
A. baseline 无 frida 正常启动 (应该活)
B. spawn-zero spawn 立即 resume, 不加载任何脚本 (模拟器上稳定活)
C. spawn-bypass spawn 挂起中加载 art_callsite 补丁后 resume (模拟器上 EGL 崩)
D. attach 正常启动 7s 后 attach 主进程, 裸 attach (模拟器上静默 _exit)
指标(与模拟器版一致, 可交叉对比):
proc_main /proc/<pid>/cmdline 精确等于 com.duowan.kiwi
ui_focus dumpsys window mCurrentFocus 是否含包名
crash logcat -b crash 的 Fatal/signal/Abort 计数
detached frida session detached reason/detail
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/diag_phone_lifecycle.py [a|b|c|d] [serial] [remote]
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[2] if len(sys.argv) > 2 else "5dd8c93f"
REMOTE = sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT_DIR = REPO / "evidence" / "diag_phone"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
DURATION = 22.0 # 单轮观察时长(s)
def sh(*a):
return subprocess.run(a, capture_output=True, text=True)
def adb(*a):
return sh("adb", "-s", SERIAL, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def focus_state():
r = adb("shell", "dumpsys", "window")
m = re.search(r"mCurrentFocus=Window\{([^}]+)\}", r.stdout)
if m:
f = m.group(1)
return (PACKAGE in f), f
return False, "?"
def crash_tail():
r = adb("shell", "logcat", "-d", "-b", "crash", "-t", "60")
n = r.stdout.count("Fatal") + r.stdout.count("Abort message") + r.stdout.count("signal ")
return n, r.stdout[-1200:]
def snapshot(label):
p = main_pid()
ok, foc = focus_state()
return {"label": label, "proc": p, "ui_ok": ok, "focus": foc}
DETACHED = []
def detach_cb(reason, detail):
DETACHED.append({"reason": reason, "detail": str(detail)})
print(f" [detached] reason={reason} detail={detail}", flush=True)
def run_observe(d, pid, label, extra=None):
"""观察进程到超时或主进程死亡, 每 0.5s 记一次存活. 返回 timeline."""
t0 = time.time()
timeline = []
last_pid = pid
while time.time() - t0 < DURATION:
ok, foc = focus_state()
cur = main_pid()
timeline.append({"t": round(time.time() - t0, 1), "proc": cur, "ui_ok": ok, "focus": foc})
if cur is None or (last_pid is not None and cur != last_pid):
break
if extra is not None and extra():
break
time.sleep(0.5)
return timeline
def case_a():
adb("shell", "logcat", "-c")
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
t0 = time.time()
time.sleep(3)
pid = main_pid()
tl = run_observe(None, pid, "A")
return {"group": "A", "t_spawn": round(time.time() - t0, 1), "pid": pid, "timeline": tl}
def case_b():
d = frida.get_device_manager().add_remote_device(REMOTE)
adb("shell", "logcat", "-c")
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
t0 = time.time()
pid = d.spawn([PACKAGE])
d.resume(pid)
tl = run_observe(d, pid, "B", extra=lambda: False)
return {"group": "B", "pid": pid, "t_spawn": round(time.time() - t0, 1),
"timeline": tl, "detached": DETACHED}
def case_c():
d = frida.get_device_manager().add_remote_device(REMOTE)
adb("shell", "logcat", "-c")
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
t0 = time.time()
pid = d.spawn([PACKAGE])
suspend_start = time.time()
session = d.attach(pid)
session.on("detached", detach_cb)
sc = session.create_script(ART_CALLSITE.read_text())
sc.load()
suspend_ms = (time.time() - suspend_start) * 1000
d.resume(pid)
print(f" [C] spawn pid={pid} 挂起{suspend_ms:.0f}ms(art_callsite) resumed", flush=True)
tl = run_observe(d, pid, "C", extra=lambda: False)
try:
session.detach()
except Exception:
pass
return {"group": "C", "pid": pid, "suspend_ms": round(suspend_ms, 1),
"timeline": tl, "detached": DETACHED}
def case_d():
d = frida.get_device_manager().add_remote_device(REMOTE)
adb("shell", "logcat", "-c")
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(7)
pid = main_pid()
t_att = time.time()
try:
session = d.attach(pid)
session.on("detached", detach_cb)
att_ms = (time.time() - t_att) * 1000
print(f" [D] attach pid={pid} 耗时{att_ms:.0f}ms", flush=True)
except Exception as exc:
return {"group": "D", "pid": pid, "error": f"attach失败: {exc}",
"detached": DETACHED}
tl = run_observe(d, pid, "D", extra=lambda: False)
try:
session.detach()
except Exception:
pass
return {"group": "D", "pid": pid, "attach_ms": round(att_ms, 1),
"timeline": tl, "detached": DETACHED}
def main():
which = (sys.argv[1] if len(sys.argv) > 1 else "a").lower()
OUT_DIR.mkdir(parents=True, exist_ok=True)
cases = {"a": case_a, "b": case_b, "c": case_c, "d": case_d}
result = cases[which]()
n, tail = crash_tail()
result["crash_count"] = n
result["crash_tail"] = tail
out = OUT_DIR / f"phone_{which}.json"
out.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[{which.upper()}] -> {out}")
print(" 主进程最终:", result["timeline"][-1] if result["timeline"] else None)
print(" crash:", n)
if DETACHED:
print(" detached:", DETACHED)
if __name__ == "__main__":
main()
-66
View File
@@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""诊断: spawn + bypass 后进程是否存活, 崩溃原因是什么。
对照组: 不改写数据段。对比之前 patch_t1_experiment(改写 3 处)是否崩溃。
"""
from __future__ import annotations
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'exception', code:d.code, addr:String(d.address), ctx:d.context});
return true;
});
send({type:'armed'});
setInterval(function(){ send({type:'alive', t:Date.now()}); }, 3000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
session.on("detached", lambda reason, crash: print(
f"[DETACHED] reason={reason} crash={crash}", flush=True))
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "armed":
print("[*] armed", flush=True)
elif t == "exception":
print(f"[EXCEPTION] code={p.get('code')} addr={p.get('addr')}", flush=True)
elif t == "alive":
print("[*] alive", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
print("[*] resumed, 观察 30s", flush=True)
time.sleep(30)
print("[*] 结束", flush=True)
if __name__ == "__main__":
main()
-128
View File
@@ -1,128 +0,0 @@
#!/usr/bin/env python3
"""dump libhydeviceid.so .data 段 (datadiv 就地解密区), 提取全部明文串。
datadiv 解码器把 .data 段 0x3c8xxx 区域的密文就地解密为明文。
dump 整个 .data(0x3bb000 ~ 0x3eac10), 提取可读字符串 + 搜魔数。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_data.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var DATA_RANGES = [
[0x36aec0, 0x6e75], // .rodata (含 datadiv 密文? 可能)
[0x3b2460, 0x74d8], // .data.rel.ro
[0x3bb000, 0x2fc08], // .data (datadiv 就地解密区)
];
function dumpData(m){
DATA_RANGES.forEach(function(r){
var off = r[0], size = r[1];
try{
var base = m.base.add(off);
var bytes = Array.from(new Uint8Array(base.readByteArray(size)));
// 提取可读字符串 (>=5 连续可打印)
var strs = [];
var cur = '';
for (var i = 0; i < bytes.length; i++) {
var c = bytes[i];
if (c >= 32 && c < 127) cur += String.fromCharCode(c);
else {
if (cur.length >= 5) strs.push({s: cur, off: off + i - cur.length});
cur = '';
}
}
// 搜魔数
var hasMagic = false;
for (var i = 0; i + 5 < bytes.length; i++) {
if (bytes[i]==0x57 && bytes[i+1]==0x18 && bytes[i+2]==0x82 && bytes[i+3]==0xcf) { hasMagic = true; break; }
}
send({type:'data', sec:off.toString(16), size:size, strs:strs.length,
hasMagic: hasMagic,
sample: strs.slice(0, 60)});
}catch(e){
send({type:'data_err', sec:off.toString(16), e:String(e)});
}
});
}
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var m = null;
try{ m = Process.getModuleByName('libhydeviceid.so'); }catch(e){}
if (m) {
clearInterval(iv);
send({type:'mod', base:String(m.base), size:m.size});
dumpData(m);
} else if (tries > 100) { clearInterval(iv); send({type:'no_mod'}); }
}, 50);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "data":
print(f"[data] sec=0x{p.get('sec')} size={p.get('size')} "
f"strs={p.get('strs')} magic={p.get('hasMagic')}", flush=True)
for it in p.get('sample', [])[:40]:
print(f" 0x{it['off']:x}: {it['s']}", flush=True)
events.append({"section": p.get('sec'), "strings": p.get('sample')})
OUT.write_text(json.dumps(events, indent=1))
elif t == "data_err":
print(f"[data_err] {p.get('sec')}: {p.get('e')}", flush=True)
elif t == "mod":
print(f"[*] libhydeviceid @ {p.get('base')} size={p.get('size')}", flush=True)
elif t == "no_mod":
print("[*] 模块未加载", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, dump 完成即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""完整导出 libhydeviceid.so .data 段解密后的全部字符串(不截断)。
.resume 后 .data 已被 datadiv 就地解密为明文。dump 全部字节 + 全部
字符串(带偏移)到 JSON, 供离线分析 XXTEA key / 魔数 / t1 来源。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_full.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var RANGES = [
[0x36aec0, 0x6e75], // .rodata
[0x3b2460, 0x74d8], // .data.rel.ro
[0x3bb000, 0x2fc08], // .data
];
function dumpSec(m, off, size){
try{
var base = m.base.add(off);
var raw = base.readByteArray(size);
var bytes = Array.from(new Uint8Array(raw));
var strs = [];
var cur = '';
var curOff = 0;
for (var i = 0; i < bytes.length; i++) {
var c = bytes[i];
if (c >= 32 && c < 127) { if (!cur) curOff = i; cur += String.fromCharCode(c); }
else {
if (cur.length >= 4) strs.push([off + curOff, cur]);
cur = '';
}
}
if (cur.length >= 4) strs.push([off + curOff, cur]);
return {sec: off.toString(16), size: size, n: strs.length, strs: strs};
}catch(e){ return {sec: off.toString(16), err: String(e)}; }
}
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var m = null;
try{ m = Process.getModuleByName('libhydeviceid.so'); }catch(e){}
if (m) {
clearInterval(iv);
var out = [];
RANGES.forEach(function(r){ out.push(dumpSec(m, r[0], r[1])); });
send({type:'all', ranges: out});
} else if (tries > 100) { clearInterval(iv); send({type:'no_mod'}); }
}, 50);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "all":
total = sum(r.get('n', 0) for r in p.get('ranges', []))
print(f"[*] 完成, 共 {total} 条字符串 -> {OUT}", flush=True)
OUT.write_text(json.dumps(p.get('ranges'), ensure_ascii=False))
elif t == "no_mod":
print("[*] 模块未加载", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
time.sleep(5)
print("[*] dump 完成, 结束", flush=True)
if __name__ == "__main__":
main()
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/env python3
"""dump 魔数缓冲区: 找到魔数地址后, dump 前后完整内存到证据文件。
命中即写入文件(不依赖退出), 打印明文头与密文长度。
"""
from __future__ import annotations
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_buffer_dump.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
function doScan(){
var found = false;
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
var m = Memory.scanSync(r.base, r.size, '57 18 82 cf 66 4b b3 94');
m.forEach(function(x){
var addr = x.address;
// 只 dump 每个 4KB 页内第一个命中 (去重)
var pre256 = hex(addr.sub(64), 64);
var post = hex(addr.add(10), 4096);
send({type:'dump', addr:String(addr), pre:pre256, post:post});
found = true;
});
}catch(e){}
});
}catch(e){}
send({type:'tick', found:found});
}
var iv = setInterval(function(){ doScan(); }, 3000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
dumped = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:160], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dump":
addr = p.get("addr")
# 去重 (同地址不重复存)
if any(d.get("addr") == addr for d in dumped):
return
rec = {"addr": addr, "pre": p.get("pre"), "post": p.get("post")}
dumped.append(rec)
# 立即持久化
OUT.write_text(__import__("json").dumps(dumped, indent=1))
print(f"[DUMP] @{addr} pre={p.get('pre','')[:64]} postlen={len(p.get('post',''))//2} saved={len(dumped)}", flush=True)
elif t == "tick":
print(f"[tick] found={p.get('found')} total_dumps={len(dumped)}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] dump 扫描中 (每3s), 命中即持久化。请触发登录 (命中后可 pkill)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, 共 {len(dumped)} 个 dump -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""完整 dump 明文采集数据区域 (0x7814096d19 JSON 附近 + 0x78133bf220 键值结构)。
拿到加密前的完整明文 = 解密 dfpReport 的关键! dump 前后 8KB 到文件。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_plaintext_full.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
// 锚点: JSON 明文 + t1 键值结构
var ANCHORS = [
['json', '74 65 72 6d 69 6e 61 6c 54 79 70 65 22 3a 31'], // terminalType":1
['t1kv', '74 65 72 6d 69 6e 61 6c'], // terminal (键值结构内)
];
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
function scan(){
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
ANCHORS.forEach(function(a){
var hits = Memory.scanSync(r.base, r.size, a[1]);
hits.slice(0, 5).forEach(function(x){
var addr = x.address;
// JSON: dump 前 512 + 后 8192
var pre = '', post = '';
try{ pre = hex(addr.sub(512), 512); }catch(e){}
try{ post = hex(addr, 8192); }catch(e){}
send({type:'dump', anchor:a[0], addr:String(addr), pre:pre, post:post});
});
});
}catch(e){}
});
}catch(e){}
send({type:'tick'});
}
setInterval(function(){ try{ scan(); }catch(e){} }, 4000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dump":
addr = p.get('addr')
if any(e.get('addr') == addr for e in events): return
rec = {"anchor": p.get('anchor'), "addr": addr,
"pre": p.get('pre'), "post": p.get('post')}
events.append(rec)
OUT.write_text(json.dumps(events, ensure_ascii=False))
# 打印 post 的可读部分 (前 400B)
post = bytes.fromhex(p.get('post', ''))
asc = ''.join(chr(b) if 32 <= b < 127 else '.' for b in post[:400])
print(f"[dump:{p.get('anchor')}] @{addr}", flush=True)
print(f" {asc}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, dump 明文区 (需触发登录采集后可 pkill)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-137
View File
@@ -1,137 +0,0 @@
#!/usr/bin/env python3
"""dump 设备运行时 libhydeviceid.so 的 datadiv 解密区(就地解密=明文)。
datadiv 解码器在 JNI_OnLoad 时把 0x3c8xxx 数据段密文就地解密为明文。
直接 dump 该区域 + 扫描全进程可读字符串(android- 等), 命中即持久化。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_runtime_strings.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
// 1. dump libhydeviceid.so 数据段 (datadiv 就地解密区 0x3c8000 ~ 0x3ca000)
function dumpModuleData(){
var m = null;
try{ m = Process.getModuleByName('libhydeviceid.so'); }catch(e){ return; }
[0x3c8000, 0x3ca000, 0x3cc000, 0x3d0000, 0x3e0000].forEach(function(off){
try{
var base = m.base.add(off);
var buf = base.readByteArray(0x2000);
var bytes = Array.from(new Uint8Array(buf));
// 提取可读字符串
var strs = [];
var cur = '';
for (var i = 0; i < bytes.length; i++) {
var c = bytes[i];
if (c >= 32 && c < 127) { cur += String.fromCharCode(c); }
else {
if (cur.length >= 5) strs.push(cur);
cur = '';
}
}
if (strs.length) {
send({type:'datastr', off:off.toString(16), n:strs.length,
strs: strs.slice(0, 40)});
}
// 搜魔数
var hexs = bytes.map(function(x){return ('0'+x.toString(16)).slice(-2);}).join('');
if (hexs.indexOf('571882cf') >= 0) send({type:'magic_in_data', off:off.toString(16)});
}catch(e){}
});
}
// 2. 全进程扫描明文设备数据 (android- / UA / JSON)
function scanPlain(){
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*128) return;
try{
// 找 "android-" 出现且周围有可读文本
var hits = Memory.scanSync(r.base, r.size, '61 6e 64 72 6f 69 64 2d');
hits.slice(0, 30).forEach(function(x){
var ctx = '';
try{ ctx = x.address.sub(64).readUtf8String(); }catch(e){}
send({type:'plain', addr:String(x.address), ctx: (ctx || '').slice(0, 200)});
});
}catch(e){}
});
}catch(e){}
send({type:'scan_done'});
}
dumpModuleData();
setTimeout(function(){ scanPlain(); }, 2000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "datastr":
print(f"[datastr] off=0x{p.get('off')} {p.get('n')} 条:", flush=True)
for s in p.get('strs', []):
print(f" {s}", flush=True)
events.append(p)
OUT.write_text(json.dumps(events, indent=1))
elif t == "magic_in_data":
print(f"[MAGIC-IN-DATA] off=0x{p.get('off')}", flush=True)
elif t == "plain":
print(f"[plain] @{p.get('addr')}: {p.get('ctx')!r}", flush=True)
events.append(p)
elif t == "scan_done":
print("[*] scan done", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, dump 完成即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-195
View File
@@ -1,195 +0,0 @@
#!/usr/bin/env python3
"""模拟器 attach+三件套 对照实验:
1) 干净启动 App → 自动走到账号密码登录页 → 填表 → 登录 → 观察验证码是否加载(基线)
2) attach frida 三件套(bypass_msaoaid_maps_skip_cleanup + mask_frida_maps_only + patch_guard)
→ 观察存活 → 重新走登录 → 观察验证码是否加载(对照)
用 RE 仓库 .venv 环境与 RE 仓库 evidence/scripts 三件套。"""
from pathlib import Path
import frida, time, subprocess, json, 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_attach_captcha.json")
TRIO = [
(RE / "evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js", "bypass_main"),
(RE / "evidence/scripts/mask_frida_maps_only.js", "mask_frida"),
(RE / "evidence/scripts/patch_guard_block_termination.js", "patch_guard"),
]
def sh(*args, **kw):
return subprocess.run(args, capture_output=True, text=True, **kw)
def adb(*args):
return sh(*ADB, *args)
def force_stop():
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
def launch():
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(6)
def wait_pid(timeout=20):
for _ in range(timeout):
r = adb("shell", "pidof", PACKAGE)
if r.stdout.strip():
return int(r.stdout.strip().split()[0])
time.sleep(1)
return None
def ui_dump():
adb("shell", "uiautomator", "dump", "/data/local/tmp/ui.xml")
r = adb("shell", "cat", "/data/local/tmp/ui.xml")
return r.stdout
def find_bounds(xml, text):
import re
m = re.search(r'<node[^>]*text="' + re.escape(text) + r'"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
if m:
x1, y1, x2, y2 = map(int, m.groups())
return (x1 + x2) // 2, (y1 + y2) // 2
return None
def tap(x, y):
adb("shell", "input", "tap", str(x), str(y))
def input_text(s):
adb("shell", "input", "text", s)
def go_login_page():
"""从首页走到账号密码登录页. 返回 True 若成功."""
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 not b:
# 已经是验证码登录页, 尝试切换
print(" [!] 未找到账号密码登录tab", flush=True)
return False
tap(*b); time.sleep(2)
return True
def fill_and_submit():
xml = ui_dump()
b = find_bounds(xml, "手机号/虎牙号")
if not b:
b = find_bounds(xml, "请填写手机号码")
if not b:
return False
tap(*b); time.sleep(0.4); input_text("13800138000")
xml = ui_dump()
b = find_bounds(xml, "密码")
if b:
tap(*b); time.sleep(0.4); input_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()
nodes = xml.replace("></", ">\n</")
texts = [m for m in nodes.splitlines() if 'text="' in m]
joined = " | ".join(t for t in texts if 'text="' in t)
has_slider = "滑块" in joined or "拼图" in joined or "安全验证" in joined
has_webview = "android.webkit.WebView" in joined
# 统计关键文本
keys = [k for k in ("安全验证", "滑块", "拼图", "验证码", "网络异常", "加载失败", "请稍后") if k in joined]
return {"ok": has_slider and has_webview, "webview": has_webview, "slider": has_slider,
"keys": keys, "texts": [l.strip() for l in nodes.splitlines() if 'text="' in l and l.strip().startswith('<node')][:12]}
def attach_trio(d, pid):
session = d.attach(pid)
print(f"[*] attach pid={pid}", flush=True)
loaded = []
for js_path, name in TRIO:
try:
sc = session.create_script(js_path.read_text())
sc.on('message', lambda m, dd, n=name: print(f" [{n}] {m.get('payload') if m.get('type')=='send' else m}", flush=True))
sc.load()
loaded.append(name)
time.sleep(0.15)
except Exception as e:
print(f" [!] {name} 加载失败: {e}", flush=True)
print(f"[*] 三件套已加载: {loaded}", flush=True)
return session
def main():
results = {"baseline": None, "with_frida": None, "captcha_baseline": None, "captcha_with_frida": None}
d = frida.get_device_manager().add_remote_device(REMOTE)
# ---- 基线: 无 frida 走登录 ----
print("===== 基线: 无 frida =====", flush=True)
force_stop(); launch()
pid = wait_pid()
results["baseline"] = pid
print(f"[*] app pid={pid}", flush=True)
if pid and go_login_page() and fill_and_submit():
time.sleep(5)
results["captcha_baseline"] = captcha_status()
print(f"[*] 基线验证码: {json.dumps(results['captcha_baseline'], ensure_ascii=False)}", flush=True)
# ---- 对照: attach 三件套 ----
print("===== 对照: attach + 三件套 =====", flush=True)
pid = wait_pid()
if not pid:
print("[!] app 未运行", flush=True)
return
session = attach_trio(d, pid)
results["with_frida"] = pid
# 观察存活
alive = True
t0 = time.time()
while time.time() - t0 < 40:
time.sleep(2)
try:
cur = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
cur = []
if not cur:
alive = False
print(f"[!] attach 后 App 死亡于 +{time.time()-t0:.0f}s", flush=True)
session = None
break
results["frida_survival_s"] = int(time.time() - t0) if alive else int(time.time() - t0)
print(f"[*] attach 后存活: {'YES' if alive else 'NO'} ({results['frida_survival_s']}s)", flush=True)
if alive:
# 回到登录页重新走
if go_login_page() and fill_and_submit():
time.sleep(5)
results["captcha_with_frida"] = captcha_status()
print(f"[*] frida 下验证码: {json.dumps(results['captcha_with_frida'], ensure_ascii=False)}", flush=True)
else:
results["captcha_with_frida"] = "app died"
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=1))
print(f"[*] 结果写入 {OUT}", flush=True)
try:
if session: session.detach()
except Exception:
pass
if __name__ == "__main__":
main()
-142
View File
@@ -1,142 +0,0 @@
#!/usr/bin/env python3
"""完整链路验证: 正常启动App → attach + SSL_write hook → 存活观察 + 抓dfpReport.
基于最新发现: attach 不再必然被杀 (E0-E7 状态依赖)。
流程:
1. monkey 正常启动 App (无 frida)
2. 等 8s (App 完全起来)
3. attach 主进程 + SSL_write/SSL_read hook (零 bypass)
4. 用 /proc 精确监控主进程存活; logcat crash 同步
5. 观察到 dfpReport + actionV 即停
用法: python3 emu_attach_capture_v2.py [--waits N] [--observe S]
"""
from pathlib import Path
import frida, time, json, re, subprocess, sys, argparse
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
MAIN_JS = r"""
'use strict';
send({type:'armed', t:Date.now()});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked', t:Date.now()});
}catch(e){send({type:'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];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--wait", type=float, default=8.0)
ap.add_argument("--observe", type=int, default=40)
ap.add_argument("--out", default="/Users/yml/codes/douyu_login_py/evidence/emu_attach_capture_v2.json")
args = ap.parse_args()
d = frida.get_device_manager().add_remote_device(REMOTE)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "logcat", "-c")
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(args.wait)
pid = main_pid()
print(f"[*] App 启动 {args.wait}s, 主进程 pid={pid}", flush=True)
if pid is None:
print("[!] 无主进程, 退出"); return
t0 = time.time()
try:
session = d.attach(pid)
print(f"[*] attach ok at +{(time.time()-t0)*1000:.0f}ms", flush=True)
except Exception as e:
print(f"[!] attach err {e}", flush=True); return
events = []
got_dfp = [False]
actionVs = []
def on_main(m, dta):
if m.get('type') == 'error':
print(" [JS-ERR]", str(m)[:120], flush=True)
return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print("[*] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[*] {p['cls']} len={p['len']}", flush=True)
events.append(p)
if p['cls'] == 'dfpReport': got_dfp[0] = True
elif t == 'resp':
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
if av: actionVs.append(av.group(1).decode())
if av: print(f"[*] RESP actionV {av.group(1).decode()}", flush=True)
events.append(p)
def on_detach(reason, detail):
print(f"[*] DETACHED reason={reason} detail={str(detail)[:100]}", flush=True)
session.on('detached', on_detach)
try:
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
except Exception as e:
print(f"[!] script err {e}", flush=True)
# 监控: 每2s 主进程存活
died_at = None
while time.time() - t0 < args.observe:
time.sleep(2)
mp = main_pid()
if mp is None:
died_at = time.time() - t0
print(f"[*] 主进程死 at +{died_at:.0f}s", flush=True)
break
if mp != pid:
print(f"[*] pid变化 {pid}->{mp} (KeepAlive?) at +{time.time()-t0:.0f}s", flush=True)
died_at = time.time() - t0
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "10").stdout
print(f"[*] 结果: 存活={died_at is None} 存活时长={died_at or args.observe}s EGL崩={'EGL' in cr} 事件={len(events)} actionV={actionVs[:2]}", flush=True)
Path(args.out).write_text(json.dumps(events, ensure_ascii=False, indent=1))
try: session.detach()
except Exception: pass
print(f"[*] done -> {args.out}", flush=True)
if __name__ == "__main__":
main()
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env python3
"""复现实验: attach + 只读 Interceptor 是否让 msaoaidsec 检测失效 (App 存活).
假设: 空 attach (E0) 2s 静默死; attach+trace的只读 Interceptor 装到 msaoaidsec 内
(+0x1bfac 线程名检测器入口 / +0x1c0e4,0x1c0f8 strstr callsite) 后 App 存活.
实验设计 (每轮):
1. spawn → attach → 加载 trace_msaoaid_thread_strstr_plt.js (只读, 不patch不mask)
2. resume → 用 /proc 精确监控主进程 20s
3. 记录: 存活? 死亡时刻? 是否EGL崩? 收到的strstr事件?
控制组: 相同流程但不加载任何脚本 (= E0 复现)
"""
from pathlib import Path
import frida, time, json, subprocess, sys, re
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")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def run_round(d, rnd, script_name=None, observe=20):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
events = []
def on(m, dta):
if m.get('type') != 'send': return
p = m.get('payload') or {}
events.append(p)
ev = p.get('event', '?')
if ev in ('msaoaid-thread-strstr',):
print(f"[r{rnd} +{time.time()-t0:5.2f}s] STRSTR needle={p.get('needle')} haystack={p.get('haystack','')[:30]!r}", flush=True)
elif ev == 'msaoaid-thread-strstr-return':
print(f"[r{rnd} +{time.time()-t0:5.2f}s] STRSTR-RET needle={p.get('needle')} hit={p.get('returnedNonNull')}", flush=True)
if script_name:
sc = session.create_script((RE / "evidence/scripts" / script_name).read_text())
sc.on('message', on)
sc.load()
d.resume(pid)
print(f"[r{rnd}] resume at +{(time.time()-t0)*1000:.0f}ms script={script_name}", flush=True)
# 监控
died_at = None
while time.time() - t0 < observe:
time.sleep(2)
if main_pid() is None:
died_at = time.time() - t0
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "8").stdout
status = "存活" if died_at is None else f"死@+{died_at:.1f}s"
egl = "EGL" in cr or "libEGL" in cr
print(f"[r{rnd}] 结果: {status} EGL崩={egl} events={len(events)}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
return {"round": rnd, "script": script_name, "died_at": died_at, "egl": egl, "n_events": len(events)}
def main():
scripts = sys.argv[1:] or ["None", "trace_msaoaid_thread_strstr_plt.js"]
d = frida.get_device_manager().add_remote_device(REMOTE)
results = []
for script in scripts:
for rnd in (1, 2):
r = run_round(d, rnd, None if script == "None" else script)
results.append(r)
print(f"{r}", flush=True)
out = Path("/tmp/attach_trace_survival.json")
out.write_text(json.dumps(results, indent=1, ensure_ascii=False))
print(f"[*] -> {out}", flush=True)
if __name__ == "__main__":
main()
-175
View File
@@ -1,175 +0,0 @@
#!/usr/bin/env python3
"""模拟器抓包 v11: B组优先三策略 (基于 diag E0-E7 实验结论).
结论回顾 (GC3VE/Android12/frida15.2.2):
- 纯 spawn→resume (不attach) = 存活, 但无 hook 能力 (B组/E3, 多次3/3存活)
- 任何 attach (挂起中/延迟, 有无脚本) = 被杀: 空attach静默_exit / attach+bypass=EGL崩
- 历史上 spawn+挂起attach+bypass 曾抓到 dfpReport+actionV, 但 App 随后必崩 (抢窗口)
本脚本提供三种模式:
zero (默认) 纯B组: spawn→立即resume→存活观察 (无hook, 验证App真能活/验证码可用)
race B组+延迟attach: resume后2s attach+bypass+SSLhook, 抢dfp窗口 (接受闪退)
stale 旧法对照: 挂起中加载 art_callsite+mask 再resume (复现历史成功路径)
用法: python3 emu_hook_zero_suspend.py [--mode zero|race|stale] [--rounds N] [--out path]
"""
from pathlib import Path
import frida, time, json, re, subprocess, sys, argparse
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")
MAIN_JS = r"""
'use strict';
send({type:'armed', t:Date.now()});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked', t:Date.now()});
}catch(e){send({type:'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];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def detach_cb(reason, detail, tag=""):
print(f" [detached:{tag}] reason={reason} detail={str(detail)[:120]}", flush=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--mode", default="zero", choices=["zero", "race", "stale"])
ap.add_argument("--out", default="/Users/yml/codes/douyu_login_py/evidence/emu_hook_v11.json")
ap.add_argument("--rounds", type=int, default=2)
args = ap.parse_args()
out_path = Path(args.out)
d = frida.get_device_manager().add_remote_device(REMOTE)
print(f"[*] mode={args.mode} rounds={args.rounds} OUT={out_path}", flush=True)
all_events = []
for rnd in range(1, args.rounds + 1):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = None
session = None
try:
pid = d.spawn([PACKAGE])
t_spawn = time.time()
if args.mode == "stale":
# 旧法: 挂起中加载2个脚本再resume
session = d.attach(pid)
session.on('detached', lambda r, dd, t=f"[r{rnd}]": detach_cb(r, dd, t))
for name in ("bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"):
sc = session.create_script((RE/"evidence/scripts"/name).read_text()); sc.load()
suspend_ms = (time.time() - t_spawn) * 1000
d.resume(pid)
print(f"[r{rnd}] stale: spawn pid={pid} 挂起{suspend_ms:.0f}ms(2脚本) resumed", flush=True)
time.sleep(0.5)
else:
# zero/race: 先立即 resume (B组存活)
d.resume(pid)
print(f"[r{rnd}] spawn pid={pid} 立即resumed (B组存活)", flush=True)
if args.mode == "race":
time.sleep(2.0)
t_att = time.time()
session = d.attach(pid)
session.on('detached', lambda r, dd, t=f"[r{rnd}]": detach_cb(r, dd, t))
for name in ("bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"):
sc = session.create_script((RE/"evidence/scripts"/name).read_text()); sc.load()
print(f"[r{rnd}] race: attach at +{(time.time()-t_spawn)*1000:.0f}ms +2脚本 +{(time.time()-t_att)*1000:.0f}ms", flush=True)
events = []
got_dfp = [False]
actionVs = []
def on_main(m, dta):
if m.get('type') == 'error':
return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print(f"[r{rnd}] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[r{rnd}] {p['cls']} len={p['len']}", flush=True)
events.append(p); all_events.append(p)
if p['cls'] == 'dfpReport': got_dfp[0] = True
elif t == 'resp':
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
if av: actionVs.append(av.group(1).decode())
print(f"[r{rnd}] RESP len={p['len']}{mark}", flush=True)
events.append(p); all_events.append(p)
if session:
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 存活监控 (死因判定: crash buffer 有记录=EGL崩, 无=静默_exit)
t0 = time.time()
died_at = None
while time.time() - t0 < 30:
time.sleep(2)
mp = main_pid()
if mp is None:
died_at = time.time() - t0
break
if mp != pid:
print(f"[r{rnd}] pid变化 {pid}->{mp} (KeepAlive重启)", flush=True)
died_at = time.time() - t0
break
if got_dfp[0] and time.time() - t0 > 15:
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "20").stdout
crashed = bool(re.search(r"Fatal|Abort message|signal \d", cr))
print(f"[r{rnd}] 结束: 存活={died_at is None} 死后至+{round(died_at,1) if died_at else '-'} 崩={crashed} 事件={len(events)} actionV={actionVs[:2]}", flush=True)
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
except Exception as e:
print(f"[r{rnd}] ERR {repr(e)[:140]}", flush=True)
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1.5)
out_path.write_text(json.dumps(all_events, ensure_ascii=False, indent=1))
print(f"[*] 结束 事件={len(all_events)} -> {out_path}", flush=True)
if __name__ == "__main__":
main()
-110
View File
@@ -1,110 +0,0 @@
#!/usr/bin/env python3
"""msaoaidsec 检测面精确归因 v2 (修正误判版).
上次 attach_trace_survival 用 main_pid() is None 判断死亡 —— 错!
KeepAlive 重启会换 pid, main_pid() 返回新 pid, 误报"存活"
本版: 死亡判定 = pid 变化 或 无主进程 (均算死), 并采集:
1. trace_msaoaid_thread_strstr_plt.js — 线程名检测器 strstr 命中 (needle=?)
2. trace_msaoaid_exit_sources.js (API兼容修复) — _exit PLT slot 观察, backtrace 帧
每组 spawn+attach (挂起中加载脚本, 立即resume), 观察 12s。
"""
from pathlib import Path
import frida, time, json, subprocess, 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")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
# frida 15 兼容修复: getGlobalExportByName -> findExportByName
def fix_script(text):
return text.replace("Module.getGlobalExportByName(", "Module.findExportByName(null, ")
def run_case(d, rnd, scripts):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
events = []
def on(m, dta):
if m.get('type') != 'send':
if m.get('type') == 'error':
print(f" [JS-ERR] {str(m)[:100]}", flush=True)
return
p = m.get('payload') or {}
ev = p.get('event', '?')
line = f"[r{rnd} +{time.time()-t0:6.2f}s] {ev}"
if ev == 'msaoaid-thread-strstr':
line += f" needle={p.get('needle')} hay={p.get('haystack','')[:30]!r}"
elif ev == 'msaoaid-thread-strstr-return':
line += f" needle={p.get('needle')} hit={p.get('returnedNonNull')} ret={p.get('returnPointer')}"
elif ev == 'msaoaid-exit-source':
line += f" status={p.get('status')} tid={p.get('tid')} frames={p.get('frames')}"
elif ev in ('msaoaid-exit-got-thread-exit', 'msaoaid-exit-got-thread-exit-installed'):
line += f" data={p.get('status') or p.get('slot')}"
print(line, flush=True)
events.append(p)
for name in scripts:
try:
text = (RE / "evidence/scripts" / name).read_text()
sc = session.create_script(fix_script(text))
sc.on('message', on)
sc.load()
print(f"[r{rnd}] loaded {name} +{(time.time()-t0)*1000:.0f}ms", flush=True)
except Exception as e:
print(f"[r{rnd}] load-err {name}: {e}", flush=True)
d.resume(pid)
print(f"[r{rnd}] resumed +{(time.time()-t0)*1000:.0f}ms", flush=True)
# 精确死亡判定: pid变化或无主进程
died = None
change = None
while time.time() - t0 < 12:
time.sleep(1.5)
mp = main_pid()
if mp is None:
died = time.time() - t0
print(f"[r{rnd}] DEAD(no main) at +{died:.1f}s", flush=True)
break
if mp != pid:
change = (mp, round(time.time() - t0, 1))
print(f"[r{rnd}] PID CHANGED {pid}->{mp} at +{change[1]}s (主进程死/重启)", flush=True)
break
if died is None and change is None:
print(f"[r{rnd}] 存活满12s (pid={pid} 未变)", flush=True)
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "6").stdout
egl = "EGL" in cr
print(f"[r{rnd}] EGL崩={egl}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
return {"round": rnd, "scripts": scripts, "died_at": died, "pid_change": change, "egl": egl, "events": events}
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
# 组1: 只读线程名探针 (不patch不mask)
r1 = run_case(d, 1, ["trace_msaoaid_thread_strstr_plt.js"])
# 组2: 只读线程名探针 + exit源观察
r2 = run_case(d, 2, ["trace_msaoaid_thread_strstr_plt.js", "trace_msaoaid_exit_sources.js"])
# 组3: 只读 exit源观察 (无线程名探针, 减少干扰)
r3 = run_case(d, 3, ["trace_msaoaid_exit_sources.js"])
out = Path("/tmp/msaoaid_precise.json")
out.write_text(json.dumps([r1, r2, r3], ensure_ascii=False, indent=1))
print(f"[*] -> {out}", flush=True)
if __name__ == "__main__":
main()
-150
View File
@@ -1,150 +0,0 @@
#!/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()
-156
View File
@@ -1,156 +0,0 @@
#!/usr/bin/env python3
"""铸造验证 v1: 重放 dfpReport wire → 服务端回显(不动设备)。
路线(XOR 自反性,不需要 keystream):
密文 = 明文 XOR ks => 新密文 = 原密文 XOR (原JSON XOR 新JSON)
JSON 区(前 554B)等长替换 hdid/deviceId/appkey,collection 区保持原样。
"""
from __future__ import annotations
import binascii
import json
import random
import ssl
import socket
import time
from pathlib import Path
EVID = Path("/Users/yml/codes/douyu_login_py/evidence")
HOST = "wsapi.huya.com"
MAGIC = bytes.fromhex("571882cf664bb39401ee")
def load_first_wire():
for f in sorted((EVID / "dfp_pipeline_170031.json", EVID / "dfp_pipeline_160855.json"),
reverse=True):
d = json.load(open(f))
for e in d:
if e.get("type") == "wire":
return binascii.unhexlify(e["hex"])
raise SystemExit("no wire found")
def send_http(host, path, body, timeout=60):
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
raw = socket.create_connection((host, 443), timeout=10)
s = ctx.wrap_socket(raw, server_hostname=host)
req = (
f"POST {path} HTTP/1.1\r\n"
f"i-ver: 1\r\n"
f"Content-Type: application/octet-stream\r\n"
f"Content-Length: {len(body)}\r\n"
f"Host: {HOST}\r\n"
f"Connection: Keep-Alive\r\n"
f"Accept-Encoding: identity\r\n"
f"User-Agent: okhttp/3.14.9\r\n"
f"\r\n"
).encode() + body
s.sendall(req)
s.settimeout(timeout)
buf = b""
try:
while True:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
# 响应头拿到 + 至少等 2s 累积 body
if b"\r\n\r\n" in buf and len(buf) > 300:
time.sleep(2)
try:
while True:
c = s.recv(4096)
if not c:
break
buf += c
except socket.timeout:
break
break
finally:
try:
s.close()
except Exception:
pass
return buf
def gen_random_hex(n):
return "".join(random.choice("0123456789abcdef") for _ in range(n))
def main():
wire = load_first_wire()
idx = wire.find(b"\r\n\r\n")
body = wire[idx + 4:]
mp = body.find(MAGIC)
assert mp >= 0, "magic not in body"
cipher = body[mp + 10:]
print(f"[*] wire={len(wire)}B body={len(body)}B cipher={len(cipher)}B")
# 明文 JSON (canonical 554B from pair_sync)
ps = json.load(open(EVID / "dfp_pair_sync.json"))
jsonb = None
for p in ps:
if p.get("type") != "plain":
continue
for item in p["plains"]:
b = bytes.fromhex(item["hex"])
if b[:8] == b'{"appId"' and len(b) >= 554:
jsonb = b[:554]
break
if jsonb:
break
assert jsonb, "no json"
orig = jsonb.decode("utf-8", "replace")
# --- 实验1: 原样重放 ---
print("\n[实验1] 原样重放")
r = send_http(HOST, "/", body)
status = r.split(b"\r\n", 1)[0].decode("utf-8", "replace")
print(f" resp: {status} len={len(r)}")
hdrs, _, rbody = r.partition(b"\r\n\r\n")
print(f" resp body head: {rbody[:80].hex(' ')}")
# --- 实验2: 改明文 hdid/deviceId/appkey (等长) → Δ XOR ---
new_hdid = gen_random_hex(40)
new_devid = gen_random_hex(40)
new_appkey = gen_random_hex(32)
new = orig
new = new.replace(f'"hdid":"{orig.split(chr(34)+"hdid"+chr(34))[1].split(chr(34))[1]}"',
f'"hdid":"{new_hdid}"')
# 更稳妥: 正则替换
import re
new = re.sub(r'"hdid":"[0-9a-f]{40}"', f'"hdid":"{new_hdid}"', new)
new = re.sub(r'"deviceId":"[0-9a-f]{40}"', f'"deviceId":"{new_devid}"', new)
new = re.sub(r'"appkey":"[0-9a-f]{32}"', f'"appkey":"{new_appkey}"', new)
assert new != orig, "no field replaced"
nb = new.encode("utf-8")
assert len(nb) == 554, f"len {len(nb)}"
delta = bytes(a ^ b for a, b in zip(nb, jsonb))
new_cipher = bytes(a ^ b for a, b in zip(cipher, delta + b"\x00" * (len(cipher) - len(delta))))
new_body = body[:mp + 10] + new_cipher
# Content-Length 不变 (等长)
print(f"\n[实验2] 改 hdid={new_hdid[:12]}... deviceId={new_devid[:12]}... appkey={new_appkey[:8]}...")
r = send_http(HOST, "/", new_body)
status = r.split(b"\r\n", 1)[0].decode("utf-8", "replace")
print(f" resp: {status} len={len(r)}")
hdrs, _, rbody = r.partition(b"\r\n\r\n")
print(f" resp body head: {rbody[:160].hex(' ')}")
out = {
"new_hdid": new_hdid, "new_devid": new_devid, "new_appkey": new_appkey,
"new_json": new,
"exp1_status": status,
"exp2_status": status,
"exp2_resp": rbody[:160].hex(),
}
(EVID / "forge_replay_result.json").write_text(json.dumps(out, ensure_ascii=False, indent=1))
print(f"\n[*] saved -> evidence/forge_replay_result.json")
if __name__ == "__main__":
main()
-222
View File
@@ -1,222 +0,0 @@
#!/usr/bin/env python3
"""UdbAESUtil 差分分析 harnessNativeFunction 直调)。
时序(不可变): force-stop -> spawn挂起 -> bypass -> resume -> 稳定10s -> patch_guard -> 业务脚本
静态已确认对象布局: +0x00 vptr | +0x08 S-box拷贝(256B) | +0x108 InvS-box拷贝(256B) | +0x208 轮密钥(176B)
导出符号(.dynsym:
C1(uchar*key) 0x24eb08 构造器
KeyExpansion 0x24ebd4
Cipher(uchar*) 0x24f314 10轮
InvCipher(uchar*) 0x24f778
_encrypt(uchar*,string&) 0x24f9f0
_decrypt 0x24fe90
encrypt(ret&,key?,in?) 0x250038
decrypt 0x2501a4
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
// 段错误防护: 拦截 native 崩溃, 上报后恢复而不是带崩进程
Process.setExceptionHandler(function(det){
send({type:'segv', info:{type:det.type, addr:String(det.address), msg:String(det.memory||'')}});
return true;
});
var MOD = 'libudbauthunify.so';
var base = Process.getModuleByName(MOD).base;
send({type:'armed', base:String(base)});
var A = {
C1: new NativeFunction(base.add(0x24eb08), 'void', ['pointer','pointer']),
Cipher: new NativeFunction(base.add(0x24f314), 'void', ['pointer','pointer']),
InvCipher: new NativeFunction(base.add(0x24f778), 'void', ['pointer','pointer']),
enc_pub: new NativeFunction(base.add(0x250038), 'pointer', ['pointer','pointer','pointer']),
dec_pub: new NativeFunction(base.add(0x2501a4), 'pointer', ['pointer','pointer','pointer']),
};
function hex(p,n){ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
function unhex(s){ var n=s.length>>1, b=new Uint8Array(n); for(var i=0;i<n;i++) b[i]=parseInt(s.substr(i*2,2),16); return b; }
function wrBytes(p, arr){ p.writeByteArray(arr); }
// libc++ std::string 构造 (老布局: cap@0|flagLSB, size@8, data@16 / SSO: b0=len<<1,data@1)
function mkstr(bytes){
var m = Memory.alloc(32);
var n = bytes.length;
if (n <= 22) {
Memory.writeByteArray(m, new Array(24).fill(0));
m.writeU8(n<<1);
if(n) wrBytes(m.add(1), bytes);
} else {
var buf = Memory.alloc(n+1);
wrBytes(buf, bytes); buf.add(n).writeU8(0);
m.writeU64( uint64(((n)<<1)|1) );
m.add(8).writeU64(uint64(n));
m.add(16).writePointer(buf);
}
return m;
}
function strBytes(sp){ // 读回 std::string 内容
var b0=sp.readU8();
if((b0&1)===0){ var l=b0>>1; return l? new Uint8Array(sp.add(1).readByteArray(l)) : new Uint8Array(0); }
var len=parseInt(sp.add(8).readU64().toString());
var dp=sp.add(16).readPointer();
return len? new Uint8Array(dp.readByteArray(len)) : new Uint8Array(0);
}
var OBJLEN = 0x300;
function newObj(){ return Memory.alloc(OBJLEN); }
rpc.exports = {
// 轮密钥展开对拍
sched: function(keyHex){
var o=newObj(), k=Memory.alloc(32);
wrBytes(k, unhex(keyHex));
A.C1(o, k);
return { rk: hex(o.add(0x208), 176), sbox_head: hex(o.add(8),16) };
},
// 单块 Cipher(读改前后,判断是否就地)
cipher: function(keyHex, blkHex){
var o=newObj(), k=Memory.alloc(32), b=Memory.alloc(48);
wrBytes(k, unhex(keyHex)); wrBytes(b, unhex(blkHex));
A.C1(o,k);
var before = hex(b,16);
A.Cipher(o,b);
return { before:before, after:hex(b,16) };
},
invcipher: function(keyHex, ctHex){
var o=newObj(), k=Memory.alloc(32), b=Memory.alloc(48);
wrBytes(k, unhex(keyHex)); wrBytes(b, unhex(ctHex));
A.C1(o,k);
A.InvCipher(o,b);
return { after: hex(b,16) };
},
// 公开 encrypt(ret,this=x1,x2) —— 按静态结论: static(ret=&ret, x1=&plain, x2=key临时)
encpub: function(plainHex, keyBytesHex){
var ret=Memory.alloc(32), plain=mkstr(unhex(plainHex)), key=mkstr(unhex(keyBytesHex));
try {
A.enc_pub(ret, plain, key);
return { out: hex(strBytes(ret)) };
} catch(e){ return { err:String(e) }; }
},
decpub: function(ctHex, keyBytesHex){
var ret=Memory.alloc(32), ct=mkstr(unhex(ctHex)), key=mkstr(unhex(keyBytesHex));
try {
A.dec_pub(ret, ct, key);
return { out: hex(strBytes(ret)) };
} catch(e){ return { err:String(e) }; }
},
ping: function(){ return 'pong'; }
};
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
# 双重防护(与 hook_final_capture 完全一致): 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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
state = {'armed': False, 'segv': 0}
OUT = Path('/Users/yml/codes/douyu_login_py/evidence/aes_diff.json')
def save(k, v):
try:
data = json.loads(OUT.read_text()) if OUT.exists() else {}
except Exception:
data = {}
data[k] = v
OUT.write_text(json.dumps(data, indent=1))
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:300])
elif m.get('type') == 'send':
p = m.get('payload') or {}
if p.get('type') == 'armed':
state['armed'] = True
print('[armed]', p['base'])
elif p.get('type') == 'segv':
state['segv'] += 1
print(f"[SEGV #{state['segv']}] {p['info']}")
sc.on('message', on_msg)
sc.load()
assert state['armed']
print('ping:', sc.exports.ping())
k16 = '000102030405060708090a0b0c0d0e0f'
KEY27 = 'HuyaUdb1928374650qwertyuiop'.encode().hex()
# ---- 阶段1: KeyExpansion 轮密钥对拍 ----
r = sc.exports.sched(k16)
save('sched_kat', r); print('[sched] rk[:32]', r['rk'][:32])
# ---- 阶段2: 单块 Cipher KAT ----
r = sc.exports.cipher(k16, '00112233445566778899aabbccddeeff')
save('cipher_kat', r)
std = '69c4e0d86a7b0430d8cdb78070b4c55a'
ok = r.get('after') == std
print(f'[cipher KAT] {r.get("after")} 标准FIPS-197={std} match={ok}')
if not ok:
print('!! 与标准不一致 -> 自定义点在轮结构/密钥扩展, 后续阶段仍执行')
# ---- 阶段3: HuyaUdb 主密钥 first16 差分 ----
r = sc.exports.cipher(KEY27.encode().hex()[:32], '00' * 16)
save('cipher_huyaudb_first16', r); print('[cipher first16(HuyaUdb)]', r)
# ---- 阶段4: InvCipher 往返 ----
ct = sc.exports.cipher(k16, 'cafebabe' * 4).get('after', '')
pt = sc.exports.invcipher(k16, ct).get('after', '') if ct else ''
print(f'[inv roundtrip] ct={ct[:32]} -> pt={pt} match={pt == "cafebabe" * 4}')
save('inv_roundtrip', {'ct': ct, 'pt': pt})
# ---- 阶段5: 公开 encrypt(最可能崩, 放最后) ----
for name, pt in [('empty', ''), ('15B', 'aa' * 15), ('16B', 'bb' * 16), ('187B', 'cc' * 187)]:
try:
r = sc.exports.encpub(pt, KEY27)
o = r.get('out', '')
print(f'[encpub {name}] -> {len(o)//2}B head={o[:48]}')
save(f'encpub_{name}', r)
except Exception as e:
print(f'[encpub {name}] EXC {e}')
try:
e = sc.exports.encpub('ab' * 40, KEY27)
dd = sc.exports.decpub(e.get('out', ''), KEY27)
print(f'[roundtrip 80B] ct={len(e.get("out",""))//2}B pt_ok={dd.get("out","").startswith("ab"*16)}')
save('roundtrip80', {'e': e, 'd': dd})
except Exception as ex:
print('[roundtrip] EXC', ex)
print('done, segv count:', state['segv'])
if __name__ == '__main__':
for attempt in range(4):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
time.sleep(3)
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR:'+e; } }
Interceptor.attach(base.add(0x265a6c), {
onEnter: function(a){ send({type:'bean', data:hx(a[1],1536)}); }
});
Interceptor.attach(base.add(0x265fb0), {
onEnter: function(a){ send({type:'bean', data:hx(a[2],1536)}); }
});
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type') == 'saveLD_bean' for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/beandump.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-169
View File
@@ -1,169 +0,0 @@
#!/usr/bin/env python3
"""抓取证书加密真实密钥: hook UdbAESUtil::C1(构造器,收16B key) + encrypt 出入口。
背景: 差分已证明 Cipher=标准AES-128-ECB(tools/udb_aes.py selftest)
剩下唯一未知 = 生产者每次加密用的 key 字节。C1 是所有路径的必经点。
时序(不可变): force-stop -> spawn挂起 -> 双bypass -> resume -> 10s -> patch_guard -> hooks
触发: Java桥直呼 LoginProxy.getQUrlData(uid,"","") 复现证书生成。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UID = 1199666914671
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}});
return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function rdStr(p){ // libc++ string 安全读取, 返回 {t,len,v(hex)}
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>4096) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
// 1) 构造器: 每次AES对象创建的16字节真钥
Interceptor.attach(base.add(0x24eb08), {
onEnter: function(a){ send({type:'c1', key:hx(a[1],16), bt:DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,80)}); }
});
// 2) 公开 encrypt(ret,x1,x2,...): 全参原始dump, 不再猜测约定
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){
this.ra = DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,90);
this.retbuf = a[0]; // sret 缓冲指针(onLeave时 x0 可能已变, 用入口快照)
send({type:'enc_in', ra:this.ra,
x0s:rdStr(a[0]), x1s:rdStr(a[1]), x2s:rdStr(a[2]), x3s:rdStr(a[3]),
x1raw:hx(a[1],32), x2raw:hx(a[2],32), x3raw:hx(a[3],32)});
},
onLeave: function(r){ send({type:'enc_out', ret:rdStr(this.retbuf)}); }
});
// 2.5) encode_aes(in,key,out): 看选key逻辑
Interceptor.attach(base.add(0x330218), {
onEnter: function(a){
this.p0=rdStr(a[0]); this.p1=rdStr(a[1]);
this.ra = DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,90);
},
onLeave: function(){ send({type:'encode_aes', ra:this.ra, p0:this.p0, p1:this.p1}); }
});
// 3) _encrypt(block,out&) 入口: 确认其调用方与块内容
Interceptor.attach(base.add(0x24f9f0), {
onEnter: function(a){
send({type:'_enc', ra:DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,90),
blk:hx(a[1],16)});
}
});
// 自动触发证书生成
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
var q=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', len:q?q.length:0, data:q||''});
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 20000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:240]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
if t != 'retry':
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]', p['base'])
elif t == 'c1':
print(f"[C1] key={p['key']} <- {p['bt'][:60]}")
elif t == 'enc_in':
print(f"[enc] ra={p['ra'][:60]}")
print(f" x1s={p['x1s']['t']}:{p['x1s']['len']} head={p['x1s']['v'][:48]}")
print(f" x2s={p['x2s']['t']}:{p['x2s']['len']} head={p['x2s']['v'][:48]}")
print(f" x3s={p['x3s']['t']}:{p['x3s']['len']} head={p['x3s']['v'][:48]}")
elif t == 'enc_out':
print(f" out={p['ret']['t']}:{p['ret']['len']} head={p['ret']['v'][:48]}")
elif t == '_enc':
print(f"[_encrypt] blk={p['blk']} <- {p['ra'][:60]}")
elif t == 'encode_aes':
print(f"[encode_aes] p0={p['p0']['t']}:{p['p0']['len']},{p['p0']['v'][:48]} "
f"p1={p['p1']['t']}:{p['p1']['len']},{p['p1']['v'][:48]} <- {p['ra'][:56]}")
elif t == 'qurl_done':
print('[qurl_done]', p.get('len'), 'head:', (p.get('data') or '')[:60])
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 150
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'qurl_done' for e in events):
time.sleep(8)
break
Path('/Users/yml/codes/douyu_login_py/evidence/cert_keycap.json').write_text(
json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
time.sleep(3)
-172
View File
@@ -1,172 +0,0 @@
#!/usr/bin/env python3
"""一次性抓取: App 存储的 cred + Common/QUrl 两种 wupData 完整输出。"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
import frida
RE_DIR = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
function b64ify(ptr, n) {
var bytes = ptr.readByteArray(n);
var arr = new Uint8Array(bytes);
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var chars = "";
for (var i = 0; i < arr.length; i += 3) {
var b0 = arr[i], b1 = i+1 < arr.length ? arr[i+1] : 0, b2 = i+2 < arr.length ? arr[i+2] : 0;
chars += B64[b0>>2] + B64[((b0&3)<<4)|(b1>>4)] +
(i+1 < arr.length ? B64[((b1&15)<<2)|(b2>>6)] : "=") +
(i+2 < arr.length ? B64[b2&63] : "=");
}
return chars;
}
// otp 加密链 hook(二进制安全)
var base = Process.getModuleByName("libudbauthunify.so").base;
Interceptor.attach(base.add(0x32fa24), {
onEnter: function (args) {
function rd(p) {
try {
var b0 = p.readU8();
if ((b0 & 1) === 0) return {t:"s", v:p.add(1).readUtf8String(b0>>1)};
var len = parseInt(p.add(8).readU64().toString());
return {t:"b64", v:b64ify(p.add(16).readPointer(), Math.min(len,4096))};
} catch(e) { return {t:"err", v:String(e)}; }
}
this.s1 = rd(args[0]); this.b2 = args[2].toInt32();
this.s2 = rd(args[3]); this.s3 = rd(args[4]); this.s4 = rd(args[5]);
this.nonce = args[7].toString();
this.outPtr = this.context.sp.readPointer();
},
onLeave: function () {
function rdo(p) {
try {
var b0 = p.readU8();
if ((b0 & 1) === 0) return {t:"b64", v:b64ify(p.add(1), b0>>1)};
var len = parseInt(p.add(8).readU64().toString());
return {t:"b64", v:b64ify(p.add(16).readPointer(), Math.min(len,4096))};
} catch(e){ return {t:"err", v:String(e)}; }
}
send({type:"otp", s1:this.s1, b2:this.b2, s2:this.s2, s3:this.s3,
s4:this.s4, nonce:this.nonce, out:rdo(this.outPtr)});
}
});
function trigger() {
Java.perform(function () {
var attempt = 0;
var timer = null;
function once() {
attempt += 1;
var done = false;
Java.enumerateClassLoaders({
onMatch: function (loader) {
if (done) return;
try {
var f = Java.ClassFactory.get(loader);
var LP = f.use('com.hysdkproxy.LoginProxy');
var inst = LP.getInstance();
var HAS = f.use('com.huyaudbunify.HuyaAccountSaveUtils').getInstance();
var uid = HAS.getUid();
send({type:'uid', uid: String(uid)});
var HA = f.use('com.huyaudbunify.HuyaAuth').getInstance();
var cred = HA.getCred(uid);
if (cred !== null) {
send({type:'cred',
hyCred: cred.getHyCred() ? cred.getHyCred() : null,
yyCred: cred.getYyCred() ? cred.getYyCred() : null});
} else {
send({type:'cred', hyCred:null, yyCred:null});
}
var common = inst.getH5InfoEx();
send({type:'wup_common', len: common?common.length:0, data: common});
var qurl = inst.getQUrlData(uid, "", "");
send({type:'wup_qurl', len: qurl?qurl.length:0, data: qurl});
done = true;
} catch (e) { /* 下一个 loader */ }
},
onComplete: function () {
if (!done) {
if (attempt % 4 === 0) send({type:'retry', n: attempt});
timer = setTimeout(once, 3000);
} else {
send({type:'all_done', attempts: attempt});
}
}
});
}
once();
});
}
trigger();
"""
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--duration", type=float, default=40.0)
ap.add_argument("--stabilize", type=float, default=10.0)
ap.add_argument("--out", default="/tmp/full_dump.json")
ap.add_argument("--package", default="com.duowan.kiwi")
ap.add_argument("--port", default="127.0.0.1:31877")
args = ap.parse_args()
bypass_src = (RE_DIR / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
patch_guard = (RE_DIR / "evidence/scripts/patch_guard_block_termination.js").read_text()
device = frida.get_device_manager().add_remote_device(args.port)
pid = device.spawn([args.package])
print(f"spawned pid={pid}")
session = device.attach(pid)
session.create_script(bypass_src).load()
device.resume(pid)
time.sleep(args.stabilize)
session.create_script(patch_guard).load()
print("guards loaded")
collected = {}
def on_message(message, _data):
if message.get("type") != "send":
if message.get("type") == "error":
print("ERR:", str(message)[:200])
return
p = message.get("payload") or {}
t = p.get("type")
if t in ("wup_common", "wup_qurl"):
collected[t] = p.get("data")
print(f"{t}: len={p.get('len')}")
elif t == "cred":
collected["cred"] = p
hc, yc = p.get("hyCred"), p.get("yyCred")
print(f"cred: hyCred={str(hc)[:60]}... ({len(hc) if hc else 0}) "
f"yyCred={str(yc)[:60]}... ({len(yc) if yc else 0})")
elif t == "uid":
collected["uid"] = p.get("uid")
print(f"uid={p.get('uid')}")
elif t == "loader_err":
print("loader:", p.get("err"))
else:
print(json.dumps(p, ensure_ascii=False)[:120])
script = session.create_script(JS)
script.on("message", on_message)
script.load()
time.sleep(args.duration - args.stabilize)
out = Path(args.out)
out.write_text(json.dumps(collected, ensure_ascii=False))
print(f"\nsaved: {out} keys={list(collected.keys())}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-147
View File
@@ -1,147 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ send({type:'segv',info:{t:d.type,a:String(d.address)}}); return true; });
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>262144) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?hx(p.add(1),Math.min(l,4096)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>1048576) return {t:'l?',len:len,v:''};
return {t:'l',len:len,v:hx(p.add(16).readPointer(),Math.min(len,4096))};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function ra(ctx){ try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,70);}catch(e){return '?';} }
// 登录态JSON的AES加密(含cred字段) —— 已知走 UdbAESUtil::encrypt
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){ this.x1=rdStr(a[1]); this.x2=rdStr(a[2]); },
onLeave: function(){
if(this.x1.len>800||this.x2.len>800)
send({type:'bigaes', x1:{len:this.x1.len}, x2:this.x2, ra:ra(this)});
}
});
// saveLoginData 全量版: bean dump 768B + backtrace 10帧
Interceptor.attach(base.add(0x265fb0), {
onEnter: function(a){
send({type:'saveLD_str', str:rdStr(a[1]), bean_head:hx(a[2],768),
bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,10)
.map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,64);})});
}
});
Interceptor.attach(base.add(0x265a6c), {
onEnter: function(a){
send({type:'saveLD_bean', bean_head:hx(a[1],768), flag:a[2].toInt32(),
bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,10)
.map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,64);})});
}
});
// cred 打包/解包(网络侧明文必经)
Interceptor.attach(base.add(0x333170), {
onEnter: function(a){ send({type:'cred_unpack', in:rdStr(a[1]), ra:ra(this)}); }
});
// HandlerRequestLoginCred / HandlerResponseLoginCred vtable 探测:
// vtable+0x10 起为虚函数槽, 逐个attach前3个槽, 打印触发与backtrace首帧
function probeVtable(name, vtAddr){
for(var slot=0; slot<4; slot++){
(function(slot){
try{
var fp = vtAddr.add(0x10 + slot*8).readPointer();
if(fp.compare(base) < 0 || fp.compare(base.add(0x480000)) > 0) return;
Interceptor.attach(fp, {
onEnter: function(a){
send({type:'vtable', name:name, slot:slot,
a0:a[0].toString(), a1:rdStr(a[1]), a2:rdStr(a[2]),
bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,3)
.map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,60);})});
}
});
send({type:'vt_attached', name:name, slot:slot, fp:String(fp.sub(base))});
}catch(e){}
})(slot);
}
}
probeVtable('ReqLoginCred', base.add(0x477f80));
probeVtable('RespLoginCred', base.add(0x478018));
probeVtable('GetCred', base.add(0x477000)); // 占位: 若无效仅跳过
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type') == 'saveLD_bean' for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/cred_issue_trace.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-119
View File
@@ -1,119 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){ try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; return {len:l,v:l?hx(p.add(1),Math.min(l,256)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>4096) return {len:len,v:''};
return {len:len,v:hx(p.add(16).readPointer(),len)};
}catch(e){ return {len:-1,v:String(e)}; } }
Interceptor.attach(base.add(0x265a6c), {
onEnter: function(a){ send({type:'bean', data:hx(a[1],2048)}); }
});
Interceptor.attach(base.add(0x265fb0), {
onEnter: function(a){ send({type:'bean', data:hx(a[2],2048)}); }
});
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var done=false;
['com.huyaudbunify.HuyaAuth','com.hysdkproxy.HuyaAuth','com.huyaudbunify.sdk.HuyaAuth'].forEach(function(cn){
if(done) return;
try{
var C=F.use(cn);
var inst = C.getInstance ? C.getInstance() : null;
var r = inst ? inst.getCred(1199666914671) : C.getCred(1199666914671);
send({type:'java_cred', cls:cn, r:String(r).slice(0,400)});
done=true;
}catch(e2){}
});
if(!done) throw new Error('no HuyaAuth');
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 15000);
});
Interceptor.attach(base.add(0x265524), {
onEnter: function(a){ this.uid=a[1].toString(); this.out=a[2]; },
onLeave: function(){ send({type:'getcred', uid:this.uid, out:rdStr(this.out)}); }
});
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type')=='bean' for e in events) and any(e.get('type')=='getcred' for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/credtrace.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-173
View File
@@ -1,173 +0,0 @@
#!/usr/bin/env python3
"""最终轮 hook: 加密函数现场抓取(AES key/明文/密文) + Java 触发 getQUrlData。"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
function b64ify(ptr, n) {
var arr = new Uint8Array(ptr.readByteArray(n));
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var c = "";
for (var i=0;i<arr.length;i+=3){
var b0=arr[i],b1=i+1<arr.length?arr[i+1]:0,b2=i+2<arr.length?arr[i+2]:0;
c+=B64[b0>>2]+B64[((b0&3)<<4)|(b1>>4)]+(i+1<arr.length?B64[((b1&15)<<2)|(b2>>6)]:"=")+(i+2<arr.length?B64[b2&63]:"=");
}
return c;
}
function rd(p){
try{
var b0=p.readU8();
if((b0&1)===0) return {t:"s",v:p.add(1).readUtf8String(b0>>1)};
var len=parseInt(p.add(8).readU64().toString());
if(len>8192) return {t:"err",v:"too long"};
return {t:"b64",v:b64ify(p.add(16).readPointer(),len)};
}catch(e){return {t:"err",v:String(e)};}
}
var armed = false;
var timer = setInterval(function(){
if (armed) { clearInterval(timer); return; }
var base;
try { base = Process.getModuleByName("libudbauthunify.so").base; }
catch(e) { return; }
armed = true;
clearInterval(timer);
send({type:'base', v:String(base)});
// md5_char16(string& src, string& out16)
Interceptor.attach(base.add(0x32e71c), {
onEnter: function(a){
this.inp = rd(a[0]);
this.outp = a[1];
// 原始内存也存一份
try { this.rawin = b64ify(a[0], 40); } catch(e){ this.rawin=''; }
},
onLeave: function(){
send({type:'md5c16', inp:this.inp, out:rd(this.outp), rawin:this.rawin});
}
});
// KeyExpansion: 两个参数都转储
var keCount = 0;
Interceptor.attach(base.add(0x24ebd4), {
onEnter: function(a){
keCount += 1;
if (keCount > 2) return;
try {
var h = function(p,n){ return Array.from(new Uint8Array(p.readByteArray(n))).map(function(x){return ('0'+x.toString(16)).slice(-2);}).join(''); };
send({type:'aeskey_raw', n:keCount, x0:h(a[0],32), x1:h(a[1],48)});
} catch(e){}
}
});
// UdbAESUtil::encrypt: x2 字符串对象的原始内存转储
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){
this.inp = rd(a[1]);
try {
var p = a[2];
var b0 = p.readU8();
var info = {b0:b0};
if ((b0&1)===0) { info.len=b0>>1; info.data=b64ify(p.add(1), Math.min(b0>>1,48)); }
else {
info.len = parseInt(p.add(8).readU64().toString());
info.cap = parseInt(p.readU64().toString());
info.data = b64ify(p.add(16).readPointer(), Math.min(info.len,64));
}
send({type:'aeskey', info:info});
} catch(e){ send({type:'aeskey', info:{err:String(e)}}); }
this.out = a[3];
},
onLeave: function(){ send({type:'aes_out', out:rd(this.out)}); }
});
// hyudb_otp_encrypt
Interceptor.attach(base.add(0x32fa24), {
onEnter: function(a){
this.s1=rd(a[0]); this.b1=a[1].toInt32(); this.b2=a[2].toInt32();
this.s2=rd(a[3]); this.s3=rd(a[4]); this.s4=rd(a[5]);
this.n=a[7].toString();
this.outP=this.context.sp.readPointer();
},
onLeave: function(){
send({type:'otp', s1:this.s1,b1:this.b1,b2:this.b2,s2:this.s2,s3:this.s3,
s4:this.s4,b3:this.b3,nonce:this.n,out:rd(this.outP)});
}
});
send({type:'armed'});
// 触发: 等类可用后轮询
Java.perform(function(){
var n = 0;
function tryOnce(){
try {
var seed = Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F = Java.ClassFactory.get(seed.class.getClassLoader());
var inst = F.use('com.hysdkproxy.LoginProxy').getInstance();
var q = inst.getQUrlData(1199666914671, "", "");
send({type:'qurl_done', len: q?q.length:0});
var c = inst.getH5InfoEx();
send({type:'common_done', len: c?c.length:0});
} catch(e) {
n += 1;
if (n % 6 === 0) send({type:'trig_retry', n:n, err:String(e).substring(0,70)});
setTimeout(tryOnce, 5000);
}
}
setTimeout(tryOnce, 3000);
});
}, 400);
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print('spawned', pid)
s = d.attach(pid)
s.create_script((RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load()
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
d.resume(pid)
print('resumed')
events = []
def on_msg(m, _):
if m.get('type') != 'send':
if m.get('type') == 'error':
print('ERR:', str(m)[:180])
return
p = m.get('payload') or {}
events.append(p)
t = p.get('type')
if t == 'otp':
print(f"\nOTP s1={p['s1']['v'][:20]} b1={p['b1']} b2={p['b2']} "
f"s2={p['s2']['v']} s3={str(p['s3'])[:40]}")
s4 = p['s4']
print(f" s4[{s4.get('t')}] {len(s4.get('v',''))}B nonce={p['nonce']}")
sc = s.create_script(JS)
sc.on('message', on_msg)
sc.load()
time.sleep(75)
Path('/tmp/crypto_events.json').write_text(json.dumps(events, ensure_ascii=False))
aes = [e for e in events if e.get('type') == 'aes_in']
print(f"\n事件统计: otp={sum(1 for e in events if e.get('type')=='otp')} "
f"aes={len(aes)} ctp={sum(1 for e in events if e.get('type')=='ctp')}")
for e in aes[:6]:
ki = e['key']
kv = ki['v'] if isinstance(ki, dict) else str(ki)
ii = e['inp']
iv = ii['v'] if isinstance(ii, dict) else str(ii)
print(f"AES key[{kv[:24]}...] in[{len(iv)}B]")
if __name__ == '__main__':
main()
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env python3
"""重 hook datadiv 解码器: 读返回值指向内存的前 64B hex。
datadiv 函数解密一个数据块(不只是字符串), 魔数 5718 / 设备数据 / key
可能藏在解码后的内存里。命中(含 5718 或 android)即持久化。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_hex.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
var hooked = 0;
function hookMod(modName){
var m = null;
try{ m = Process.getModuleByName(modName); }catch(e){ return false; }
m.enumerateExports().forEach(function(exp){
if (exp.name.indexOf('datadiv') < 0) return;
try{
Interceptor.attach(exp.address, {
onLeave: function(ret){
var p = ret;
if (p.isNull()) return;
var h64 = '';
try{ h64 = hex(p, 64); }catch(e){ return; }
if (h64.indexOf('ERR') >= 0) return;
// 命中关键词: 魔数 / android / 可读字符串
var low = h64.toLowerCase();
var hit = low.indexOf('5718') >= 0 || low.indexOf('616e64726f6964') >= 0;
var str = '';
try{ str = p.readUtf8String(); }catch(e){}
if (hit || (str && str.length > 4 && str.length < 100 && /[a-z]{4,}/i.test(str))) {
send({type:'dd', mod:modName, fn:exp.name, h64:h64, str:str});
}
}
});
hooked += 1;
}catch(e){}
});
return true;
}
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var ok = hookMod('libhydeviceid.so') | hookMod('libudbauthunify.so');
if (tries % 5 === 0) send({type:'waiting', tries:tries, hooked:hooked});
if (ok) { clearInterval(iv); send({type:'hooked_count', n:hooked}); }
if (tries > 60) clearInterval(iv);
}, 1000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:160], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dd":
print(f"[dd] {p.get('mod')} {p.get('fn')}: {p.get('h64')} {p.get('str')!r}", flush=True)
events.append(p)
OUT.write_text(json.dumps(events, indent=1))
elif t == "hooked_count":
print(f"[*] hooked {p.get('n')} datadiv", flush=True)
elif t == "waiting":
print(f"[waiting] tries={p.get('tries')} hooked={p.get('hooked')}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] datadiv 重hook中 (每1s重试), 请触发登录 (命中即持久化)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""resume 前加载 datadiv hook: 抓 JNI_OnLoad 阶段批量解密的字符串。
libhydeviceid.so 的 .datadiv_decode* 在 so 加载/JNI_OnLoad 时执行,
用内部 XXTEA 解密业务字符串(含可能的魔数/dfp 配置)。必须在 resume 前
装载 hook 才能抓到加载期调用。命中(5718 或长可读串)即持久化。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_preload.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
var hookedCount = 0;
function hookModule(modName){
var m = null;
try{ m = Process.getModuleByName(modName); }catch(e){ return; }
m.enumerateExports().forEach(function(exp){
if (exp.name.indexOf('datadiv') < 0) return;
try{
Interceptor.attach(exp.address, {
onEnter: function(){ this.t0 = Date.now(); },
onLeave: function(ret){
var p = ret;
if (p.isNull()) return;
var h64 = '';
try{ h64 = hex(p, 96); }catch(e){ return; }
if (h64.indexOf('ERR') >= 0) return;
var low = h64.toLowerCase();
var str = '';
try{ str = p.readUtf8String(); }catch(e){}
var hit = low.indexOf('5718') >= 0 || (str && str.length >= 8);
if (hit) {
send({type:'dd', mod:modName, fn:exp.name, off:exp.address.sub(m.base).toString(),
h96:h64, str:str, dt:Date.now()-this.t0});
}
}
});
hookedCount += 1;
}catch(e){}
});
}
// 轮询等 so 加载 (JNI_OnLoad 前必须挂上!)
var iv = setInterval(function(){
hookModule('libhydeviceid.so');
hookModule('libudbauthunify.so');
if (hookedCount > 100) { clearInterval(iv); send({type:'hooked', n:hookedCount}); }
else if (hookedCount > 0) { clearInterval(iv); send({type:'hooked', n:hookedCount}); }
}, 20);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dd":
print(f"[dd] {p.get('mod')} {p.get('fn')}({p.get('off')}) "
f"dt={p.get('dt')}ms str={p.get('str')!r}", flush=True)
print(f" h96: {p.get('h96')}", flush=True)
events.append(p)
OUT.write_text(json.dumps(events, indent=1))
elif t == "hooked":
print(f"[*] hooked {p.get('n')} datadiv (resume前)", flush=True)
elif t == "armed":
pass
# 主 hook 在 resume 前 load!
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主hook 已装载(resume前), 现在加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, 请触发登录 (命中即 pkill, 脚本会持续到手动停)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env python3
"""R36: hook libz deflate/inflate — 抓 XXTEA 输入明文 + 隐藏加密路径 backtrace."""
import json, subprocess, time
from pathlib import Path
import frida
REMOTE="127.0.0.1:31878"; ADB="5dd8c93f"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
HERE=Path(__file__).resolve().parent.parent
OUT=HERE/"evidence"/("deflate_"+time.strftime("%H%M%S")+".json")
def clear_turing():
for d in ("app_turingdfp","app_turingfd"):
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"rm -rf /data/data/{PACKAGE}/{d}/*"],capture_output=True)
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"find /data/data/{PACKAGE} -name 'resinfo*' -delete"],capture_output=True)
def main():
clear_turing()
d=frida.get_device_manager().add_remote_device(REMOTE)
subprocess.run(["adb","-s",ADB,"shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.5)
pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True)
s=d.attach(pid)
from bypass_loader import load_bypass
load_bypass(s)
d.resume(pid)
result={}; got={"post":False,"out":False}
def on(m,_):
if m.get("type")=="error":
txt=str(m)
if "ClassNotFoundException" in txt: return
print(f"[JS] {txt[:160]}",flush=True); return
p=m.get("payload") or {}
ev=p.get("event")
if ev=="post": got["post"]=True; print(f"[+] POST {p['num']}B",flush=True)
elif ev=="deflate-out": got["out"]=True; print(f"[+] deflate-out #{p['i']} in={p['in_len']} out={p['total']}",flush=True)
elif ev=="deflate-found": print(f"[*] deflate export in {p['mod']}",flush=True)
elif ev=="inflate-in": print(f"[+] inflate-in {p['len']}B via {p['mod']}",flush=True)
result.setdefault(ev or "misc",[]).append(p)
sc=s.create_script((HERE/"tools/frida/hook_deflate.js").read_text())
sc.on("message",on); sc.load()
t0=time.time()
while time.time()-t0<150:
time.sleep(2)
if got["out"] and got["post"]: time.sleep(4); break
json.dump(result,open(OUT,"w"),indent=2)
print(f"[*] saved {OUT}",flush=True)
try: s.detach()
except Exception: pass
if __name__=="__main__": main()
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env python3
"""hook BusinessCfg::setSafeDeviceId + getHdid, 抓真实 hdid/safeDeviceId 生成值 + 调用栈.
setSafeDeviceId(a1+1008=safeDeviceId, a1+1088=hdid), a2=argv1(safeDeviceId str), a3=argv2(hdid str)
同时 hook createWupDeviceInfo 观察 DeviceInfo 组装.
"""
from pathlib import Path
import frida, time, subprocess, json
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS=r"""
'use strict';
function rcs(p,n){try{return p.readCString(n)||'';}catch(e){return '';}}
function st7(p){ // std::string (libc++ short/long form)
try{ var s=rcs(p,64); return s; }catch(e){return '<?>';}
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
// setSafeDeviceId: a2=std::string*a=safeDeviceId, a3=std::string*a2=hdid
function tryInstall(){
var md=Process.findModuleByName('libudbauthunify.so');
if(!md){ send({type:'waiting'}); return false; }
send({type:'mod',base:md.base.toString()});
Interceptor.attach(md.base.add(0x26A2E0),{
onEnter:function(a){
var sd=st7(a[1]); var hd=st7(a[2]);
send({type:'setSd',sd:sd.slice(0,64),hd:hd.slice(0,64),tid:Process.getCurrentThreadId()});
var bt=Thread.backtrace(this.context,Backtracer.ACCURATE).slice(0,14).map(function(x){return x.toString();});
send({type:'bt',bt:bt});
}
});
send({type:'setSd_hooked'});
Interceptor.attach(md.base.add(0x26A484),{
onEnter:function(){ this.out=this.context.x8; },
onLeave:function(retval){
try{
var out=this.out;
if(out.isNull()){ send({type:'getHdid',str:'NULL'}); return; }
// std::string: 字节0的低位=SSO标志
var first=out.readU8();
if((first & 1)===0){
// short string: len = first>>1; data in [out+1..]
var len=first>>1; var s=out.add(1).readCString(Math.max(0,Math.min(len,80)));
send({type:'getHdid',str:''+s,ln:len});
} else {
var p=out.readPointer(); var len=out.add(8).readU64(); var cap=out.add(16).readU64();
var s=p.readCString(Math.max(0,Math.min(len,80)));
send({type:'getHdid',str:''+s,ln:len,heap:true});
}
}catch(e){ send({type:'getHdid',err:String(e)}); }
}
});
send({type:'getHdid_hooked'});
return true;
}
var installed=false;
function poll(){
if(!installed){ installed=tryInstall(); }
if(!installed){ setTimeout(poll,300); }
}
poll();
"""
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
for a in range(1,4):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True); time.sleep(1)
pid=d.spawn([PACKAGE]); s=d.attach(pid)
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except: pass
d.resume(pid)
def on(m,dd):
if m.get('type')=='error': print(f" JSErr {str(m)[:150]}",flush=True); return
p=m.get('payload') or {}; t=p.get('type')
if t in ('setSd','getHdid'):
print(f"[att{a}] {t}: {p}",flush=True)
elif t=='bt': print(" backtrace:",flush=True);
elif t=='err' or t=='err2': print(f" {t}: {p}",flush=True)
sc=s.create_script(JS); sc.on('message',on); sc.load()
time.sleep(16)
try: d.kill(pid)
except: pass
time.sleep(1)
print("done",flush=True)
if __name__=="__main__": main()
-179
View File
@@ -1,179 +0,0 @@
#!/usr/bin/env python3
"""attach 到已运行的虎牙App进程, hook dfpReport 加密链 + datadiv 解码器。
不 spawn,直接 attach 到用户正在操作的实例, 避免挂错进程。
用法: 脚本跑起来后, 在手机上 退出登录 -> 重新登录 一次。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_attach_capture.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hx(p,n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>524288) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?hx(p.add(1),Math.min(l,16384)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>1048576) return {t:'l?',len:len,v:''};
return {t:'l',len:len,v:hx(p.add(16).readPointer(),Math.min(len,16384))};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function ra(ctx){
try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,90);}catch(e){return '?';}
}
var KEYWORDS = ['dfp', '5718', '82cf', 'b394', 'collect', 'report'];
// 1) datadiv 解码器 (libhydeviceid + libudbauthunify)
var ddHooked = 0;
['libhydeviceid.so', 'libudbauthunify.so'].forEach(function(modName){
var m = null;
try{ m = Process.getModuleByName(modName); }catch(e){ return; }
m.enumerateExports().forEach(function(exp){
if (exp.name.indexOf('datadiv') < 0) return;
try{
Interceptor.attach(exp.address, {
onLeave: function(ret){
var s = '';
try{ s = ret.readUtf8String(); }catch(e){}
if (!s || s.length < 3 || s.length > 2000) return;
var low = s.toLowerCase();
var hit = false;
KEYWORDS.forEach(function(k){ if (low.indexOf(k) >= 0) hit = true; });
if (hit) send({type:'dd', mod:modName, fn:exp.name, str:s.slice(0,300)});
}
});
ddHooked += 1;
}catch(e){}
});
});
// 2) xxtea (hyudb_crypt_util + hyudbxxt)
var base = Process.getModuleByName('libudbauthunify.so').base;
try{
Interceptor.attach(base.add(0x32e83c), {
onEnter: function(a){
send({type:'xxtea', r:ra(this), in:rdStr(a[1]), key:rdStr(a[2])});
}
});
}catch(e){}
// 3) UdbAESUtil::encrypt (in len 大 -> dfp)
try{
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){
var inp = rdStr(a[0]);
if (inp.len > 500) {
send({type:'aes_in', r:ra(this), in:inp});
}
},
onLeave: function(ret){
var out = rdStr(this.context.x0);
if (out.len > 500) send({type:'aes_out', out:out});
}
});
}catch(e){}
// 4) enpack_header / enpack_body (打包)
try{
Interceptor.attach(base.add(0x32ff44), {
onEnter: function(a){ this.h=a[0].toInt32()&0xff; this.r=ra(this); },
onLeave: function(){ send({type:'enpack_hdr', h:this.h, r:this.r, out:rdStr(this.context.x2)}); }
});
}catch(e){}
try{
Interceptor.attach(base.add(0x3300c8), {
onEnter: function(a){
this.h=a[0].toInt32()&0xff; this.r=ra(this);
send({type:'enpack_body_in', h:this.h, r:this.r,
s1:rdStr(a[1]), s2:rdStr(a[2]), s3:rdStr(a[3]), s4:rdStr(a[4])});
},
onLeave: function(){ send({type:'enpack_body_out', h:this.h, out:rdStr(this.context.x5)}); }
});
}catch(e){}
send({type:'dd_hooked', n:ddHooked});
"""
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--pid", type=int, default=7296)
args = parser.parse_args()
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
session = device.attach(args.pid)
print(f"[*] attached pid={args.pid}")
# attach 模式不需要 msaoaid bypass (进程已过检测点), 但保险起见加载 patch_guard
try:
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
except Exception as e:
print("[warn] patch_guard:", str(e)[:80])
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:160]); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dd":
print(f"[dd] {p.get('mod')} {p.get('fn')}: {p.get('str')!r}")
elif t == "xxtea":
inp = p.get("in", {}); key = p.get("key", {})
print(f"[xxtea] in.len={inp.get('len')} key={key.get('v','')[:40]} r={p.get('r','')[:50]}")
elif t == "aes_in":
inp = p.get("in", {})
print(f"[aes_in] len={inp.get('len')} head={inp.get('v','')[:64]} r={p.get('r','')[:50]}")
elif t == "aes_out":
out = p.get("out", {})
print(f"[aes_out] len={out.get('len')} head={out.get('v','')[:64]}")
elif t == "enpack_hdr":
print(f"[enpack_hdr] h={p.get('h')} out.len={p.get('out',{}).get('len','?')} r={p.get('r','')[:40]}")
elif t == "enpack_body_in":
print(f"[enpack_body_in] h={p.get('h')} s1={p.get('s1',{}).get('v','')[:48]} r={p.get('r','')[:40]}")
elif t == "enpack_body_out":
print(f"[enpack_body_out] h={p.get('h')} out.len={p.get('out',{}).get('len','?')}")
elif t == "armed":
print("[*] armed")
elif t == "dd_hooked":
print(f"[*] datadiv hooked: {p.get('n')}")
events.append(p)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] hooks 装好, 请在手机上 退出登录 -> 重新登录 (180s)")
deadline = time.time() + 180
try:
while time.time() < deadline:
time.sleep(5)
except KeyboardInterrupt:
pass
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
print(f"[*] 捕获 {len(events)} 条 -> {OUT}")
if __name__ == "__main__":
main()
-215
View File
@@ -1,215 +0,0 @@
#!/usr/bin/env python3
"""定位 dfpReport t2 (魔数+密文) 原生组装函数 —— 轻量版。
v1 教训: 全量 memcpy hook (24B~20KB 都插桩) 把 App 拖到 ANR 被杀。
本版只插桩大拷贝 (2500~20000B):
- src/dst 命中 10B 魔数 -> 深捕获 + 回溯 (稀有, 不拖慢)
- 其余大拷贝只压轻量环形 (size/src/dst), 供 SSL_write 时与输出缓冲关联
流程: spawn+三件套 bypass -> 自动 dfpReport -> 命中即持久化 (实时写文件)
"""
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/dfp_builder2.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var MAGIC = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94,0x01,0xee];
var processStart = Date.now();
function hexb(p, n){
try{
var arr = Array.from(new Uint8Array(p.readByteArray(n)));
var s = '';
for (var i=0;i<arr.length;i++) s += ('0'+arr[i].toString(16)).slice(-2);
return s;
}catch(e){ return null; }
}
function startsWith(p, arr){
try{
var b = new Uint8Array(p.readByteArray(arr.length));
for (var i=0;i<arr.length;i++) if (b[i]!==arr[i]) return false;
return true;
}catch(e){ return false; }
}
function bt(){
var out = [];
try{
var list = Thread.backtrace(this.context, Backtracer.ACCURATE);
for (var i=0;i<list.length && i<40;i++){
var a = list[i];
var m = Process.findModuleByAddress(a);
if (m){
out.push(m.name + '!' + a.sub(m.base).toString());
}
}
}catch(e){}
return out;
}
var ring = [];
function onCopy(fn, dst, src, size){
var now = Date.now() - processStart;
if (size >= 2500 && size <= 20000){
ring.push({t: now, fn: fn, size: size, dst: String(dst), src: String(src)});
if (ring.length > 200) ring.shift();
var dm = startsWith(dst, MAGIC);
var sm = startsWith(src, MAGIC);
if (dm || sm){
var cap = {t: now, fn: fn, size: size, dst: String(dst), src: String(src),
dmagic: dm, smagic: sm,
srchead: hexb(src, 64), dsthead: hexb(dst, 64),
stack: bt.call(this)};
ring.push({t: now, fn: 'CAP:'+fn, size: size, dst: String(dst), src: String(src),
stack: cap.stack});
send({type:'capture', cap: cap});
}
}
}
function hookLibc(){
['memcpy','memmove'].forEach(function(fn){
try{
var p = Module.findExportByName('libc.so', fn);
if (!p) return;
Interceptor.attach(p, {
onEnter: function(a){
try{ onCopy.call(this, fn, a[0], a[1], a[2].toInt32()); }catch(e){}
}
});
send({type:'hooked_copy', fn: fn, at: String(p)});
}catch(e){ send({type:'hook_fail', fn: fn, e: String(e)}); }
});
}
hookLibc();
function scanMagic(){
var hits = [];
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
var m = Memory.scanSync(r.base, r.size, '57 18 82 cf 66 4b b3 94 01 ee');
m.slice(0, 14).forEach(function(x){
var mm = Process.findModuleByAddress(x.address);
hits.push({addr: String(x.address), mod: mm ? mm.name : 'anon',
pre: hexb(x.address.sub(64), 64), post: hexb(x.address.add(10), 48)});
});
}catch(e){}
});
}catch(e){}
return hits;
}
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 < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
send({type:'wire', len:len, hex:hexb(a[1], len), t:Date.now(),
ring: ring.slice(-150), magics: scanMagic()});
}
});
});
send({type:'ssl_hooked'});
}catch(e){ send({type:'ssl_err', e:String(e)}); }
"""
def main():
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 == "hooked_copy":
print(f"[hook] {p.get('fn')} @ {p.get('at')}", flush=True)
elif t == "ssl_hooked":
print("[*] SSL_write hooked", flush=True)
elif t == "capture":
c = p.get('cap') or {}
print(f"[CAPTURE] {c.get('fn')} size={c.get('size')} dmagic={c.get('dmagic')} smagic={c.get('smagic')}", flush=True)
print(f" src={c.get('src')} -> dst={c.get('dst')}", flush=True)
print(f" srchead={c.get('srchead','')[:40]} dsthead={c.get('dsthead','')[:40]}", flush=True)
print(f" stack: {(c.get('stack') or [])[:16]}", flush=True)
events.append({"type": "capture", "cap": c})
OUT.write_text(json.dumps(events))
elif t == "wire":
print(f"[wire] len={p.get('len')}", flush=True)
ev = {"type": "wire", "len": p.get('len'), "t": p.get('t'), "hex": p.get('hex'),
"ring": p.get('ring'), "magics": p.get('magics')}
events.append(ev)
OUT.write_text(json.dumps(events))
mg = p.get('magics') or []
print(f" magics: {len(mg)}", flush=True)
for mm in mg[:8]:
print(f" {mm.get('addr')} mod={mm.get('mod')} pre={mm.get('pre','')[-32:]} post={mm.get('post','')[:20]}", flush=True)
# ring entries that wrote near magic addresses
if mg:
for mm in mg[:3]:
try:
target = int(mm.get('addr'), 16)
for e in (p.get('ring') or []):
try:
d = int(e.get('dst'), 16)
s = int(e.get('dst'), 16) + (e.get('size') or 0)
if d <= target <= s:
print(f" ring-write-to-magic: {e}", flush=True)
except Exception:
pass
except Exception:
pass
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载, 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume。等待自动 dfpReport", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-262
View File
@@ -1,262 +0,0 @@
#!/usr/bin/env python3
"""动态捕获 dfpReport 加密体生成链 (真机 Frida)。
目标: 搞清 dfpReport t2 加密体 (10B魔数 571882cf664bb39401ee + 变长密文) 的
明文结构 / 加密算法 / key 来源, 以便纯代码铸造全新设备身份。
Hook 点 (libudbauthunify.so, 偏移来自 work/sos 符号表):
0x32e83c hyudb_crypt_util::xxtea_encrypt(out&, in&, key&) # XXTEA 加密
0x2527ec hyudbxxt::xxtea_encrypt(string, string) # 另一套 XXTEA
0x250038 UdbAESUtil::encrypt(in&, out&) # AES 加密
0x32ff44 enpack_header(h, NEWKEY, out&) # 打包头
0x3300c8 enpack_body(h, string, ...) # 打包体
0x26871c AESkeyMgr::getkey(i, i) # AES key 源
dfpReport 响应处理 (wsapi) -> 记录服务端下发 t1/t2/t5
用法: 脚本跑起来后 spawn 虎牙App (com.duowan.kiwi), 触发一次设备注册链
(App 启动即自动 getDfpConfig->selectOperator->dfpReport), 抓取明文/key。
"""
from __future__ import annotations
import json
import sys
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/dfp_hook_capture.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}});
return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function hx(p,n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR:'+String(e).slice(0,40); }
}
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>524288) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?hx(p.add(1),Math.min(l,16384)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>1048576) return {t:'l?',len:len,v:''};
return {t:'l',len:len,v:hx(p.add(16).readPointer(),Math.min(len,16384))};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function ra(ctx){
try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,100);}catch(e){return '?';}
}
function hookStr(fn, off, name){
try{
Interceptor.attach(base.add(off), {
onEnter: function(a){
this.r=ra(this);
try{
var in1=rdStr(a[1]);
send({type:'enc', name:name, off:off, r:this.r,
in:in1, key:rdStr(a[2]), out_before:rdStr(a[0])});
}catch(e){ send({type:'enc_err', name:name, off:off, e:String(e)}); }
},
onLeave: function(ret){
send({type:'enc_out', name:name, off:off,
out:rdStr(this.context.x0)});
}
});
send({type:'hooked', name:name, off:off});
}catch(e){ send({type:'hook_fail', name:name, off:off, e:String(e)}); }
}
// hyudb_crypt_util::xxtea_encrypt(string& out, const string& in, const string& key)
hookStr(0x32e83c, 0x32e83c, 'xxtea_crypt_util');
// hyudbxxt::xxtea_encrypt(const string& in, const string& key, string& out) 参数顺序不同
try{
Interceptor.attach(base.add(0x2527ec), {
onEnter: function(a){
send({type:'enc', name:'xxtea_xxt', off:0x2527ec, r:ra(this),
in:rdStr(a[0]), key:rdStr(a[1]), out_before:rdStr(a[2])});
},
onLeave: function(ret){
send({type:'enc_out', name:'xxtea_xxt', out:rdStr(this.context.x0)});
}
});
send({type:'hooked', name:'xxtea_xxt', off:0x2527ec});
}catch(e){ send({type:'hook_fail', name:'xxtea_xxt', e:String(e)}); }
// UdbAESUtil::encrypt(string& in, string& out) —— 打印输入前 64B + 完整长度
try{
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){
this.r=ra(this);
var inp=rdStr(a[0]);
send({type:'aes_in', r:this.r, in:inp});
},
onLeave: function(ret){
send({type:'aes_out', out:rdStr(this.context.x0)});
}
});
send({type:'hooked', name:'aes_encrypt', off:0x250038});
}catch(e){ send({type:'hook_fail', name:'aes_encrypt', e:String(e)}); }
// AESkeyMgr::getkey(int a, int b)
try{
Interceptor.attach(base.add(0x26871c), {
onEnter: function(a){ this.a=a[0].toInt32(); this.b=a[1].toInt32(); },
onLeave: function(ret){
send({type:'aeskey', a:this.a, b:this.b, out:rdStr(this.context.x0)});
}
});
send({type:'hooked', name:'aeskey', off:0x26871c});
}catch(e){ send({type:'hook_fail', name:'aeskey', e:String(e)}); }
// enpack_header(uchar w0, NEWKEY x1, string& x2)
try{
Interceptor.attach(base.add(0x32ff44), {
onEnter: function(a){
this.h=a[0].toInt32()&0xff;
this.r=ra(this);
},
onLeave: function(ret){
send({type:'enpack_header', h:this.h, r:this.r,
out:rdStr(this.context.x2)});
}
});
send({type:'hooked', name:'enpack_header', off:0x32ff44});
}catch(e){ send({type:'hook_fail', name:'enpack_header', e:String(e)}); }
// enpack_body(uchar w0, string& x1, const string& x2, const string& x3, const string& x4, string& x5)
try{
Interceptor.attach(base.add(0x3300c8), {
onEnter: function(a){
this.h=a[0].toInt32()&0xff;
this.r=ra(this);
send({type:'enpack_body_in', h:this.h, r:this.r,
s1:rdStr(a[1]), s2:rdStr(a[2]), s3:rdStr(a[3]), s4:rdStr(a[4])});
},
onLeave: function(ret){
send({type:'enpack_body_out', h:this.h, out:rdStr(this.context.x5)});
}
});
send({type:'hooked', name:'enpack_body', off:0x3300c8});
}catch(e){ send({type:'hook_fail', name:'enpack_body', e:String(e)}); }
// datadiv 解码器 (libhydeviceid + libudbauthunify) —— 魔数可能是运行时解码的字符串
var KEYWORDS = ['dfp', '5718', '82cf', 'b394', 'collect', 'report', 'device'];
var ddHooked = 0;
['libhydeviceid.so', 'libudbauthunify.so'].forEach(function(modName){
var m = null;
try{ m = Process.getModuleByName(modName); }catch(e){ return; }
m.enumerateExports().forEach(function(exp){
if (exp.name.indexOf('datadiv') < 0) return;
try{
Interceptor.attach(exp.address, {
onLeave: function(ret){
var s = '';
try{ s = ret.readUtf8String(); }catch(e){}
if (!s || s.length < 3 || s.length > 2000) return;
var low = s.toLowerCase();
var hit = false;
KEYWORDS.forEach(function(k){ if (low.indexOf(k) >= 0) hit = true; });
if (hit) send({type:'dd', mod:modName, fn:exp.name, str:s.slice(0,300)});
}
});
ddHooked += 1;
}catch(e){}
});
});
send({type:'dd_hooked', n:ddHooked});
"""
def on_message(message, data):
if message.get("type") == "send":
payload = message.get("payload", {})
events.append(payload)
t = payload.get("type")
if t in ("enc", "enc_out", "aeskey", "enpack_header", "enpack_body_in", "enpack_body_out"):
# 只打印关键摘要, 防刷屏
name = payload.get("name", t)
inp = payload.get("in", {})
key = payload.get("key", {})
if t == "enc":
print(f"[{name}] in.len={inp.get('len') if isinstance(inp, dict) else '?'} "
f"key.len={key.get('len') if isinstance(key, dict) else '?'} "
f"r={payload.get('r','')}")
if isinstance(inp, dict) and inp.get('len') and inp.get('len') < 600:
print(f" in: {inp.get('v','')[:200]}")
if isinstance(key, dict) and key.get('len') and key.get('len') < 200:
print(f" key: {key.get('v','')[:120]}")
elif t == "enc_out":
out = payload.get("out", {})
print(f"[{name}] out.len={out.get('len') if isinstance(out, dict) else '?'}")
elif t == "aes_in":
inp = payload.get("in", {})
v = inp.get("v", "") if isinstance(inp, dict) else ""
print(f"[aes_in] r={payload.get('r','')[:60]} len={inp.get('len')} head={v[:64]}")
elif t == "aes_out":
out = payload.get("out", {})
v = out.get("v", "") if isinstance(out, dict) else ""
print(f"[aes_out] len={out.get('len')} head={v[:64]}")
elif t == "enpack_header":
print(f"[enpack_header] h={payload.get('h')} out.len={payload.get('out',{}).get('len','?')}")
elif t == "enpack_body_in":
print(f"[enpack_body] h={payload.get('h')} s1={payload.get('s1',{}).get('v','')[:60]}")
elif t == "enpack_body_out":
out = payload.get("out", {})
print(f"[enpack_body] out.len={out.get('len') if isinstance(out, dict) else '?'}")
elif t == "dd":
print(f"[dd] {payload.get('mod')} {payload.get('fn')}: {payload.get('str')!r}")
elif t == "dd_hooked":
print(f"[+] datadiv hooked: {payload.get('n')}")
elif t == "hooked":
print(f"[+] hooked: {payload.get('name')}")
elif t == "hook_fail":
print(f"[-] hook fail: {payload.get('name')}: {payload.get('e')}")
elif message.get("type") == "error":
print(f"[frida-error] {message.get('description')}", file=sys.stderr)
events = []
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn([PACKAGE])
print(f"[*] spawned {PACKAGE} pid={pid}")
session = device.attach(pid)
# 双 bypass + patch_guard (与 hook_genbiz.py 同款, 稳定)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] resumed, 等待 dfpReport 链触发 (App 启动自动执行 + 请手动退出登录->重新登录)...")
try:
time.sleep(150)
except KeyboardInterrupt:
pass
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
print(f"[*] 捕获 {len(events)} 条 -> {OUT}")
print(f" 类型分布: { {t: sum(1 for e in events if e.get('type')==t) for t in set(e.get('type') for e in events)} }")
if __name__ == "__main__":
main()
-211
View File
@@ -1,211 +0,0 @@
#!/usr/bin/env python3
"""hook libhydeviceid.so!0x91ce8: t2 [魔数+密文] 拷贝点 -> 抓完整加密输入。
0x91ce8 是 t2 组装中的 memcpy 调用点 (src 已含 魔数+密文, size~3867-3986)。
onEnter:
1) 从 x0/x1/x2/sp 定位 [魔数+密文] 输出缓冲, dump 4096B
2) 全内存扫 {"appId":"5008 找加密输入 (JSON+二进制) 缓冲, dump 4096B
3) SSL_write 抓 wire (密文基准)
"""
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/dfp_input_capture.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var MAGIC_STR = '57 18 82 cf 66 4b b3 94 01 ee';
var JSON_STR = '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38'; // {"appId":"5008
function hexb(p, n){
try{
var arr = Array.from(new Uint8Array(p.readByteArray(n)));
var s = '';
for (var i=0;i<arr.length;i++) s += ('0'+arr[i].toString(16)).slice(-2);
return s;
}catch(e){ return null; }
}
function startsWithHex(p, hexstr){
try{
var want = hexstr.split(' ').map(function(x){return parseInt(x,16);});
var b = new Uint8Array(p.readByteArray(want.length));
for (var i=0;i<want.length;i++) if (b[i]!==want[i]) return false;
return true;
}catch(e){ return false; }
}
function scanPattern(pattern, cap){
var hits = [];
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
if (hits.length >= cap) return;
try{
var m = Memory.scanSync(r.base, r.size, pattern);
m.slice(0, cap - hits.length).forEach(function(x){
hits.push({addr: String(x.address),
mod: (function(){ var mm=Process.findModuleByAddress(x.address); return mm? mm.name : 'anon'; })()});
});
}catch(e){}
});
}catch(e){}
return hits;
}
function dumpAround(addr, pre, post){
try{
var p = addr.sub(pre);
var d = hexb(p, pre + post);
return {base: String(p), pre: pre, post: post, hex: d};
}catch(e){ return null; }
}
// 找到含魔数的缓冲 (扫描参数指向的内存)
function findMagicBuf(ctx){
var cands = [];
[ctx.x0, ctx.x1, ctx.x2, ctx.x3, ctx.x4].forEach(function(p, i){
try{
if (p.isNull()) return;
if (startsWithHex(p, MAGIC_STR)) cands.push({reg: 'x'+i, addr: String(p)});
// 也看看指针对应的数据是否在 sp 附近 (std::string 结构: ptr 在偏移8/16)
var q = p.readPointer();
if (!q.isNull() && startsWithHex(q, MAGIC_STR)) cands.push({reg: 'x'+i+'_ptr', addr: String(q)});
}catch(e){}
});
// 扫最近的匿名内存找 [魔数+密文] (限 512MB 内, 快)
if (!cands.length){
try{
var m = Memory.scanSync(ctx.sp, 64*1024*1024, MAGIC_STR);
m.slice(0, 3).forEach(function(x){ cands.push({reg: 'sp_scan', addr: String(x.address)}); });
}catch(e){}
}
return cands;
}
var nCalls = 0;
var hooked91 = false;
function tryHook91(){
if (hooked91) return;
try{
var base = Process.getModuleByName('libhydeviceid.so').base;
Interceptor.attach(base.add(0x91ce8), {
onEnter: function(a){
nCalls++;
var ctx = this.context;
var ev = {type:'t2copy', n:nCalls, x0:String(ctx.x0), x1:String(ctx.x1),
x2:String(ctx.x2), x3:String(ctx.x3), x4:String(ctx.x4)};
var mbs = findMagicBuf(ctx);
ev.magicbufs = mbs;
for (var i=0;i<mbs.length && i<2;i++){
var d = dumpAround(ptr(mbs[i].addr), 0, 4096);
ev['magicdump'+i] = d;
}
// 全内存扫明文 JSON (加密输入)
var jh = scanPattern(JSON_STR, 6);
ev.jsonhits = jh;
for (var j=0;j<jh.length && j<4;j++){
var d = dumpAround(ptr(jh[j].addr), 0, 4096);
ev['jsondump'+j] = d;
}
send({type:'t2copy', ev: ev});
}
});
hooked91 = true;
send({type:'hooked_91ce8', base:String(base)});
}catch(e){
setTimeout(tryHook91, 1000);
}
}
setTimeout(tryHook91, 500);
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 < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
send({type:'wire', len:len, hex:hexb(a[1], len), t:Date.now()});
}
});
});
send({type:'ssl_hooked'});
}catch(e){ send({type:'ssl_err', e:String(e)}); }
"""
def main():
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 == "hooked_91ce8":
print(f"[*] hooked 0x91ce8 @ {p.get('base')}", flush=True)
elif t == "ssl_hooked":
print("[*] SSL_write hooked", flush=True)
elif t == "hook_err":
print("[hook_err]", p.get('e'), flush=True)
elif t == "t2copy":
ev = p.get('ev') or {}
print(f"[t2copy] n={ev.get('n')} x0={ev.get('x0')} x1={ev.get('x1')} x2={ev.get('x2')}", flush=True)
for mb in (ev.get('magicbufs') or [])[:3]:
print(f" magicbuf reg={mb.get('reg')} @{mb.get('addr')}", flush=True)
for jh in (ev.get('jsonhits') or [])[:6]:
print(f" jsonhit @{jh.get('addr')} mod={jh.get('mod')}", flush=True)
events.append({"type": "t2copy", "ev": ev})
OUT.write_text(json.dumps(events))
elif t == "wire":
print(f"[wire] len={p.get('len')}", flush=True)
events.append({"type": "wire", "len": p.get('len'), "t": p.get('t'), "hex": p.get('hex')})
OUT.write_text(json.dumps(events))
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载, 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume。等待自动 dfpReport (0x91ce8 命中)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-343
View File
@@ -1,343 +0,0 @@
#!/usr/bin/env python3
"""散弹枪 v2: 完整解码 dfpReport 管道参数 (指针数组 / (ptr,len) / std::string)。
挂载: topA_20e924, midA_5a938, cryptoA_5b1f4, copy_61098, pktA_7f744, pktB_92128
对每个 onEnter 的 x0..x4:
1) 直接缓冲探测 (JSON/MAGIC 前缀 -> full dump 6000B)
2) (ptr,len) 解释: 读 16B -> 若两个 u64 第二个 < 100000 且第一个可读 -> dump
3) 指针数组解释: 读 4x8B 指针, 对每个指针尝试 (2)
4) std::string 解释: [size u64][data u64][cap u64] 或 [data][size][cap]
所有事件带 t (ms since start)。SSL_write 抓 wire 对照。
"""
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")
import time as _t
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pipeline_" + _t.strftime("%H%M%S") + ".json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var t0 = Date.now();
var JSON_HDR = [0x7b,0x22,0x61,0x70,0x70,0x49,0x64,0x22,0x3a,0x22,0x35,0x30,0x30,0x38];
var MAGIC_HDR = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94,0x01,0xee];
function hexb(p, n){
try{
var arr = Array.from(new Uint8Array(p.readByteArray(n)));
var s = '';
for (var i=0;i<arr.length;i++) s += ('0'+arr[i].toString(16)).slice(-2);
return s;
}catch(e){ return null; }
}
function startsWith(p, arr){
try{
var b = new Uint8Array(p.readByteArray(arr.length));
for (var i=0;i<arr.length;i++) if (b[i]!==arr[i]) return false;
return true;
}catch(e){ return false; }
}
function isReadable(p){
try{ p.readU8(); return true; }catch(e){ return false; }
}
function u64(p){
try{ return p.readU64().toString(10); }catch(e){ return null; }
}
function decodeArg(p, tag){
var res = {reg: tag};
try{
if (!p || p.isNull()) return null;
res.addr = String(p);
// 1) direct buffer
if (startsWith(p, JSON_HDR)){
res.kind = 'JSON'; res.full = hexb(p, 6000); return res;
}
if (startsWith(p, MAGIC_HDR)){
res.kind = 'MAGIC';
var pre = '';
try{ pre = hexb(p.sub(8192), 8192); }catch(e){}
res.pre = pre;
res.full = hexb(p, 9000);
return res;
}
// 2) (ptr,len) pair: [ptr][len]
var p0 = p.readPointer(); var l0 = p.add(8).readU64();
if (!p0.isNull() && isReadable(p0) && l0 > 0 && l0 < 20000 && p0 != p){
var b0 = hexb(p0, Math.min(Number(l0), 6000));
var kind = 'seg';
if (startsWith(p0, JSON_HDR)) kind = 'JSON';
else if (startsWith(p0, MAGIC_HDR)) kind = 'MAGIC';
res.kind = kind;
res.ptrlen = {ptr: String(p0), len: Number(l0)};
res.seg = b0;
return res;
}
// 3) pointer array (4 x 8B)
var ptrs = [];
for (var i=0;i<4;i++){
var q = p.add(i*8).readPointer();
if (q.isNull() || !isReadable(q)) break;
ptrs.push(String(q));
}
if (ptrs.length >= 2){
res.kind = 'ptrs'; res.ptrs = ptrs;
// decode each pointer as (ptr,len)
var segs = [];
for (var j=0;j<ptrs.length;j++){
var qj = ptr(j);
var q0 = qj.readPointer();
var lj = qj.add(8).readU64();
if (!q0.isNull() && lj > 0 && lj < 20000 && isReadable(q0)){
segs.push({ptr: String(q0), len: Number(lj), head: hexb(q0, 48),
kind: startsWith(q0, JSON_HDR) ? 'JSON' : (startsWith(q0, MAGIC_HDR) ? 'MAGIC' : 'raw')});
}
}
res.segs = segs;
return res;
}
// 4) raw
res.kind = 'raw'; res.head = hexb(p, 32) || '';
return res;
}catch(e){ return null; }
}
// deep decode: scan 1024B at p for plausible pointers, dump 9000B at each
function deepDeref(p){
var out = {reg: 'deep', addr: String(p)};
var found = [];
var seen = {};
try{
// also dump page-aligned base 12000B
try{
var pg = p.and(ptr('0xfffffffffffff000'));
if (isReadable(pg)){
found.push({kind: 'page', ptr: String(pg), full: hexb(pg, 12000)});
seen[String(pg)] = true;
}
}catch(e){}
var base = p.sub(512);
for (var i = 0; i < 1024; i += 8){
var v;
try{ v = base.add(i).readU64(); }catch(e){ continue; }
var cand = v.and(ptr('0x7fffffffffff'));
var s = cand.toString();
if (cand == ptr('0')) continue;
// heap-ish: 0x7[7-9ca].. or 0xb4.. or 0x77..
if (!(/^0x(7[78a-c9]|b4)[0-9a-f]{9,}$/.test(s))) continue;
if (seen[s]) continue;
if (!isReadable(cand)) continue;
seen[s] = true;
var entry = {off: i - 512, ptr: s};
var kind = 'raw';
if (startsWith(cand, JSON_HDR)) kind = 'JSON';
else if (startsWith(cand, MAGIC_HDR)) kind = 'MAGIC';
entry.kind = kind;
if (kind === 'JSON' || kind === 'MAGIC'){
entry.full = hexb(cand, 9000);
} else {
entry.head = hexb(cand, 64) || '';
}
found.push(entry);
}
}catch(e){}
out.entries = found;
return out;
}
function hookFn(off, name){
try{
var base = Process.getModuleByName('libhydeviceid.so').base;
Interceptor.attach(base.add(off), {
onEnter: function(a){
var ctx = this.context;
var ev = {fn: name, phase: 'enter', t: Date.now() - t0};
['x0','x1','x2','x3','x4'].forEach(function(r, i){
var pr = decodeArg(ctx[r], r);
if (pr) ev[r] = pr;
});
// cryptoA: deep deref x0
if (name === 'cryptoA_5b1f4'){
ev.deep = deepDeref(ctx.x0);
}
send({type:'pipe', ev: ev});
}
});
send({type:'hooked', name: name});
}catch(e){ send({type:'hook_fail', name: name, e: String(e)}); }
}
var H = [
[0x20e924, 'topA_20e924'],
[0x5a938, 'midA_5a938'],
[0x5b1f4, 'cryptoA_5b1f4'],
[0x61098, 'copy_61098'],
[0x7f744, 'pktA_7f744'],
[0x92128, 'pktB_92128'],
];
var hookedN = 0;
function attachOne(base, off, name){
try{
Interceptor.attach(base.add(off), {
onEnter: function(a){
var ctx = this.context;
var ev = {fn: name, phase: 'enter', t: Date.now() - t0};
['x0','x1','x2','x3','x4'].forEach(function(r, i){
var pr = decodeArg(ctx[r], r);
if (pr) ev[r] = pr;
});
// cryptoA: deep deref x0
if (name === 'cryptoA_5b1f4'){
ev.deep = deepDeref(ctx.x0);
}
// copy_61098 second call (x0=MAGIC): heap scan for JSON + dump 4600B
if (name === 'copy_61098' && (ev.x0||{}).kind === 'MAGIC'){
var hits = [];
try{
var pat = '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38';
var ranges = Process.enumerateRanges('rw-');
for (var ri = 0; ri < ranges.length; ri++){
var rng = ranges[ri];
if (rng.size > 0x10000000) continue;
var m = Memory.scanSync(rng.base, rng.size, pat);
for (var mi = 0; mi < m.length; mi++){
var full = hexb(m[mi].address, 4600) || '';
var h = full.substring(0, 96);
hits.push({addr: String(m[mi].address), head: h, full: full});
}
}
}catch(e){}
send({type:'jsonhits', n: hits.length, hits: hits, t: Date.now() - t0});
}
send({type:'pipe', ev: ev});
}
});
hookedN++;
return true;
}catch(e){ return false; }
}
function tryHookAll(){
try{
var m = Process.getModuleByName('libhydeviceid.so');
for (var i=0;i<H.length;i++){
if (attachOne(m.base, H[i][0], H[i][1])){}
}
send({type:'hooked_all', n: hookedN});
}catch(e){ setTimeout(tryHookAll, 800); }
}
setTimeout(tryHookAll, 500);
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 < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
send({type:'wire', len:len, hex:hexb(a[1], len), t:Date.now()-t0});
}
});
});
send({type:'ssl_hooked'});
}catch(e){ send({type:'ssl_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():
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 == "hooked_all":
print(f"[*] hooked {p.get('n')} fns", flush=True)
elif t == "ssl_hooked":
print("[*] SSL_write hooked", flush=True)
elif t == "pipe":
ev = p.get('ev') or {}
parts = []
for r in ['x0','x1','x2','x3','x4']:
pr = ev.get(r)
if not pr or not isinstance(pr, dict):
continue
k = pr.get('kind')
if k == 'JSON':
parts.append(f"{r}=JSON full")
elif k == 'MAGIC':
parts.append(f"{r}=MAGIC full")
elif k == 'seg':
parts.append(f"{r}=seg({pr.get('ptrlen',{}).get('len')})@{pr.get('ptrlen',{}).get('ptr')}")
elif k == 'ptrs':
parts.append(f"{r}=ptrs {pr.get('ptrs')} segs={[(s.get('kind'), s.get('len')) for s in pr.get('segs') or []]}")
elif k == 'raw':
parts.append(f"{r}=raw {pr.get('addr')} {pr.get('head','')[:16]}")
print(f"[{ev.get('t')}ms] {ev.get('fn')}::{ev.get('phase')} " + " ".join(parts), flush=True)
events.append({"type": "pipe", "ev": ev})
OUT.write_text(json.dumps(events))
elif t == "jsonhits":
print(f"[{p.get('t')}ms] [jsonhits] n={p.get('n')}", flush=True)
events.append({"type": "jsonhits", "t": p.get('t'), "hits": p.get('hits')})
OUT.write_text(json.dumps(events))
elif t == "wire":
print(f"[{p.get('t')}ms] [wire] len={p.get('len')}", flush=True)
events.append({"type": "wire", "len": p.get('len'), "t": p.get('t'), "hex": p.get('hex')})
OUT.write_text(json.dumps(events))
# 严格按约定顺序: spawn -> bypass x2 -> resume + sleep 11 -> patch_guard -> 最后主 hook JS
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。等待 dfpReport", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-127
View File
@@ -1,127 +0,0 @@
#!/usr/bin/env python3
"""hook SSL_write, 抓 dfpReport 请求发出时的完整调用栈, 定位加密体组装函数。
dfpReport 是 TAF 请求(含 wupudbrequest/huyaudbwebui 特征), 发送时栈上
必然经过 libhydeviceid.so / libudbauthunify.so 的组装函数。
"""
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/dfp_wire_stack.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hx(p,n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
function ra(ctx, depth){
var out = [];
try{
var bt = Thread.backtrace(ctx, Backtracer.ACCURATE);
for (var i = 0; i < bt.length && i < 40; i++) {
var name = '';
try{ name = DebugSymbol.fromAddress(bt[i]).toString().slice(0, 110); }catch(e){}
if (name.indexOf('libudbauthunify.so') >= 0 || name.indexOf('libhydeviceid.so') >= 0 ||
name.indexOf('libhycrypto.so') >= 0 || name.indexOf('libtscsdk.so') >= 0) {
out.push(name);
}
}
}catch(e){}
return out;
}
var hooked = 0;
var fns = [];
try{ fns = DebugSymbol.findFunctionsNamed('SSL_write'); }catch(e){}
if (!fns.length) {
try{
var r = new ApiResolver('module');
fns = r.enumerateMatchesSync('exports:*!SSL_write').map(function(x){ return x.address; });
}catch(e2){}
}
fns.slice(0, 6).forEach(function(p, i){
Interceptor.attach(p, {
onEnter: function(a){
var len = a[2].toInt32();
if (len < 40 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('wupudbrequest') < 0 && head.indexOf('huyaudbwebui') < 0 &&
head.indexOf('dfpReport') < 0 && head.indexOf('getDfpConfig') < 0) return;
var arr = new Uint8Array(a[1].readByteArray(len));
var hex = '';
for (var j = 0; j < arr.length; j++) hex += ('0'+arr[j].toString(16)).slice(-2);
send({type:'wire', idx:i, len:len, stack:ra(this.context, 40), hex:hex});
}
});
hooked++;
send({type:'hooked', idx:i, at:String(p)});
});
if (hooked === 0) send({type:'nowrite'});
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn([PACKAGE])
print(f"[*] spawned {PACKAGE} pid={pid}")
session = device.attach(pid)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200]); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "wire":
print(f"[wire] len={p.get('len')}")
for s in (p.get("stack") or []):
print(f" {s}")
print(f" hex head: {p.get('hex','')[:64]}")
events.append(p)
elif t == "hooked":
print(f"[+] SSL_write hooked idx={p.get('idx')} at={p.get('at')}")
elif t == "nowrite":
print("[-] SSL_write not found!")
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 请在手机上 退出登录 -> 重新登录 (150s)")
deadline = time.time() + 150
try:
while time.time() < deadline:
time.sleep(5)
except KeyboardInterrupt:
pass
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
print(f"[*] 捕获 {len(events)} 条 -> {OUT}")
if __name__ == "__main__":
main()
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env python3
"""模拟器 attach 版 dfpReport 捕获 (轮询重启 attach, 覆盖冷启动窗口)。
App 在模拟器上 spawn 会 EGL 崩, 所以用 attach。但 attach 只能抓 attach 之后的请求,
而 dfpReport 在冷启动早期就发。解法: 脚本后台反复 attach, 若 App 重启则重新 attach,
每次保住最长的 hook 窗口。配合外部 force-stop 重启 App 来完整覆盖。
"""
from __future__ import annotations
import frida, time, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_attach_dfp.json")
JS = r"""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
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<50||len>50000)return;
var h='';try{h=a[1].readCString(Math.min(len,1500));}catch(e){return;}
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++;
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
events = []
seen_sessions = set()
print(f"[*] attach 循环开始 (OUT={OUT})", flush=True)
try:
while True:
# 找 kiwi 进程
pid = None
try:
for p in d.enumerate_processes():
if 'kiwi' in p.name or 'duowan' in p.name:
pid = p.pid
break
except Exception:
pass
if pid and pid not in seen_sessions:
try:
s = d.attach(pid)
seen_sessions.add(pid)
print(f"[*] attached pid={pid}", flush=True)
sc = s.create_script(JS)
sc.on('message', lambda m, data: _on(m, events))
sc.load()
print("[*] hooked", flush=True)
# 保留这个会话
_keep[pid] = (s, sc)
except Exception as e:
print(f"[*] attach {pid} err {e}", flush=True)
time.sleep(3)
except KeyboardInterrupt:
pass
OUT.write_text(json.dumps(events))
print(f"[*] saved {len(events)} -> {OUT}", flush=True)
_keep = {}
def _on(message, events):
if message.get('type') != 'send':
if message.get('type') == 'error':
print("[JS-ERR]", str(message)[:150], flush=True)
return
p = message.get('payload') or {}
t = p.get('type')
if t == 'armed':
return
elif t == 'hooked':
print("[*] ssl hooked on this attach", flush=True)
elif t == 'err':
print("[*] err", p.get('e'), flush=True)
elif 'cls' in p:
import re
print(f"[{p.get('t')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p)
OUT.write_text(json.dumps(events))
if __name__ == "__main__":
main()
-132
View File
@@ -1,132 +0,0 @@
#!/usr/bin/env python3
"""模拟器版 dfpReport 捕获: 验证全新模拟器是否为"新设备身份"
连接 127.0.0.1:31878 (模拟器 frida 15.2.2), spawn 方式。
只 hook SSL_write, 抓 dfpReport 请求 + 对应 606B 响应(内含 actionV/真或新 hdid)。
若 attach/反调试, 用 bypass 三件套 (可选开关)。
"""
from __future__ import annotations
import json, 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/emu_dfp_" + time.strftime("%H%M%S") + ".json")
REMOTE = "127.0.0.1:31878"
USE_BYPASS = True # 若 spawn 后闪退, 设为 False 再试
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 ''; } }
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(!h)return;
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++;
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,
t:Date.now()-t0,hex:hexb(a[1],len)});
}
}});
});
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];},
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(session, js, n=5, wait=2):
for i in range(n):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print(f"[retry {i}] {e}", flush=True); time.sleep(wait)
return None
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
from subprocess import run
try:
pid = d.spawn([PACKAGE])
print(f"[*] spawn {PACKAGE} pid={pid}", flush=True)
except Exception as e:
# fallback attach
pid = [p for p in d.enumerate_processes() if 'kiwi' in p.name or 'duowan' in p.name]
pid = pid[0].pid if pid else None
print(f"[*] spawn failed, attach pid={pid}", flush=True)
session = d.attach(pid)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed':
print("[*] 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')} len={p.get('len')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif t == 'resp':
import re
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
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(p); OUT.write_text(json.dumps(events))
elif t in ('sslwrite_err','sslread_err'):
print(f"[*] {t}: {p.get('e')}", flush=True)
if USE_BYPASS:
print("[*] loading bypass", flush=True)
load(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text(), wait=1)
load(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text(), wait=1)
d.resume(pid)
time.sleep(9)
load(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text(), wait=1)
print("[*] loading 主 JS", flush=True)
script = load(session, JS)
else:
d.resume(pid)
time.sleep(6)
script = load(session, JS)
if script:
script.on('message', on_message)
print(f"[*] running. OUT={OUT}", flush=True)
try:
while True:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-124
View File
@@ -1,124 +0,0 @@
#!/usr/bin/env python3
"""attach 模式抓模拟器自生成 32hex hdid (对比真机 ed0db8...).
核心: hook libudbauthunify.so:
- BusinessCfg::setSafeDeviceId(0x26A2E0): a1+1008=sd, a1+1088=hdid (两者都写)
- BusinessCfg::getHdid(0x26A484): this+1088 是 std::string 对象
- createWupDeviceInfo(0x2746A0): 返回结构 +152 = hdid std::string
App 正常启动后 attach, 不做 spawn (spawn 挂起会 EGL 闪退)。
"""
import sys, time, json
import frida
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
// short string: len = first>>1
var len = first >> 1;
return p.add(1).readUtf8String(len > 0 && len < 128 ? len : 0) || '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
if (len > 0 && len < 512) return data.readUtf8String(len) || '';
return null;
}
}catch(e){ return null; }
}
function log(t, m){ send({t:t, m:m}); }
function tryInstall(){
var md = Process.findModuleByName('libudbauthunify.so');
if (!md){ return false; }
log('mod', md.base.toString() + ' size=' + md.size);
// setSafeDeviceId: a2=sd, a3=hdid (std::string*)
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter: function(a){
this.sd = readStdString(a[1]);
this.hd = readStdString(a[2]);
},
onLeave: function(){
log('setSafeDeviceId', JSON.stringify({sd: (this.sd||'').slice(0,48), hd: (this.hd||'').slice(0,48)}));
}
});
// getHdid: this+1088 = std::string
Interceptor.attach(md.base.add(0x26A484), {
onEnter: function(a){ this.thiz = a[0]; },
onLeave: function(){
try{
var s = readStdString(this.thiz.add(1088));
log('getHdid', s ? ''+s : 'EMPTY');
}catch(e){ log('getHdid', 'ERR ' + e); }
}
});
// createWupDeviceInfo 输出 +152 hdid
Interceptor.attach(md.base.add(0x2746A0), {
onLeave: function(ret){
try{
var s = readStdString(ret.add(152));
log('wupDeviceInfo.hdid', s ? ''+s : 'EMPTY');
}catch(e){}
}
});
log('hooked', 'all');
return true;
}
var installed = false;
function poll(){
if (!installed){ installed = tryInstall(); }
if (!installed){ setTimeout(poll, 300); }
}
poll();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
# 找 App 进程 attach
pid = None
for p in dev.enumerate_processes():
if p.name and PACKAGE in p.name:
pid = p.pid
break
if not pid:
print("App 未运行, 先启动:", PACKAGE)
import subprocess
subprocess.run(["adb", "-s", "127.0.0.1:5555", "shell",
"monkey -p %s -c android.intent.category.LAUNCHER 1" % PACKAGE],
capture_output=True)
time.sleep(12) # 等 lib 加载
for p in dev.enumerate_processes():
if p.name and PACKAGE in p.name:
pid = p.pid
break
if not pid:
print("仍找不到进程"); return
print("attach pid =", pid, flush=True)
session = dev.attach(pid)
def on_message(msg, data):
if msg.get("type") == "send":
pl = msg["payload"]
print("[%s] %s" % (pl["t"], pl["m"]), flush=True)
elif msg.get("type") == "error":
print("JSErr:", str(msg)[:200], flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("等待 hook 事件 (60s)...", flush=True)
time.sleep(60)
print("done", flush=True)
if __name__ == "__main__":
main()
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""守护式抓取模拟器 32hex hdid: 持续 attach kiwi 主进程, hook getHdid/setSafeDeviceId.
App 闪退/重启也不怕 —— 循环枚举进程, 遇新主进程立即 hook。
找到 hdid(≠ed0db8 或任意32hex)后写入 /tmp/emu_hdid_found.txt 并退出。
"""
import time, json, sys
import frida
REMOTE = "127.0.0.1:31878"
FOUND = "/tmp/emu_hdid_found.txt"
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function emit(t,m){ send({t:t,m:m}); }
function hex32(s){ return /^[0-9a-fA-F]{32}$/.test(s||''); }
var hooked=false;
function tryHook(){
if(hooked) return true;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return false;
emit('mod','base='+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); var sd=(this.sd||'');
emit('SETSD', 'hdid='+hd+' sd='+sd.slice(0,32));
if(hex32(hd)) emit('FOUND_HDID', hd);
}
});
}catch(e){ emit('warn','setSd hook fail: '+e); }
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ this.thiz=a[0]; },
onLeave:function(){
try{
var s=readStdString(this.thiz.add(1088));
if(s) emit('GETHDID', ''+s);
if(hex32(s)) emit('FOUND_HDID', ''+s);
}catch(e){}
}
});
}catch(e){ emit('warn','getHdid hook fail: '+e); }
emit('ready','hooked');
hooked=true;
return true;
}
var tries=0;
function poll(){
tries++;
tryHook();
if(!hooked && tries<200){ setTimeout(poll, 250); }
}
poll();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
seen = set()
found_hdid = None
start = time.time()
while time.time() - start < 600:
procs = []
try:
procs = dev.enumerate_processes()
except Exception:
time.sleep(2); continue
for p in procs:
if not p.name or 'kiwi' not in p.name:
continue
if ':cloudpatch' in p.name or ':logcat' in p.name:
continue
if p.pid in seen:
continue
seen.add(p.pid)
print(f"[{time.strftime('%H:%M:%S')}] 新主进程 pid={p.pid} {p.name}attach中...", flush=True)
try:
session = dev.attach(p.pid)
except Exception as e:
print(f" attach失败: {e}", flush=True)
continue
def on_msg(msg, data, pid=p.pid):
global found_hdid
if msg.get('type') == 'send':
pl = msg['payload']
t, m = pl['t'], pl['m']
if t == 'FOUND_HDID':
print(f"\n★★★★★★ 模拟器 hdid = {m} ★★★★★★", flush=True)
with open(FOUND, 'w') as f:
f.write(m)
return
print(f"[pid {pid}][{t}] {m}", flush=True)
elif msg.get('type') == 'error':
print(f"[pid {pid}] JSErr: {str(msg)[:150]}", flush=True)
try:
sc = session.create_script(JS)
sc.on('message', on_msg)
sc.load()
print(f" hook 已装 (pid {pid})", flush=True)
except Exception as e:
print(f" script失败: {e}", flush=True)
time.sleep(1.5)
print("10分钟超时结束", flush=True)
if __name__ == "__main__":
main()
-171
View File
@@ -1,171 +0,0 @@
#!/usr/bin/env python3
"""模拟器 32hex hdid 抓取 v3: poll 等待模块 + hook createWupDeviceInfo(ret+152=hdid).
createWupDeviceInfo(0x2746A0): 每次 wup 打包必调用 (udb 上报/登录帧都走它),
返回 DeviceInfo 结构 +152 = hdid std::string (文档十九节真机验证过偏移)。
同时 hook setSafeDeviceId(0x26A2E0) / getHdid(0x26A484) 双保险。
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v3.json")
HDID_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function emit(tag, val){ send({type:'hdid', tag:tag, val:''+(val||''), ts:Date.now()}); }
function install(){
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return false;
emit('mod-base', ''+md.base);
// 1) createWupDeviceInfo: 返回结构 +152 = hdid
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{
var s = readStdString(ret.add(152));
if (s) emit('WUPDEV_HDID', ''+s);
}catch(e){ emit('WUPDEV_ERR', ''+e); }
}
});
emit('createWupDev-hooked','');
}catch(e){ emit('createWupDev-err', ''+e); }
// 2) setSafeDeviceId: a2=sd, a3=hdid
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){
this.sd = readStdString(a[1]);
this.hd = readStdString(a[2]);
},
onLeave:function(){
emit('SETSD', (this.hd||'') + ' | sd=' + (this.sd||'').slice(0,24));
}
});
emit('setSd-hooked','');
}catch(e){ emit('setSd-err', ''+e); }
// 3) getHdid: this+1088 (x8)
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){
try{ this.thiz = this.context.x8; }catch(e){ this.thiz = null; }
},
onLeave:function(){
if (!this.thiz) return;
try{
var s = readStdString(this.thiz.add(1088));
emit('GETHDID', ''+s);
}catch(e){}
}
});
emit('getHdid-hooked','');
}catch(e){ emit('getHdid-err', ''+e); }
emit('ready','');
return true;
}
var installed = false;
function poll(){
if (!installed) installed = install();
if (!installed) setTimeout(poll, 200);
}
poll();
"""
MAIN_JS = r"""
'use strict';
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,800));
if(h.indexOf('hypasswordLogin')>=0||h.indexOf('huyaudbwebui')>=0){
send({type:'wup', len:len, hex:hexb(a[1],len), t:Date.now()});
}
}});
});
}catch(e){ send({type:'err', e:String(e)}); }
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:150], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
hdid_sc = load(session, HDID_JS)
print("[*] HDID v3 loaded (poll-wait)", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hdid':
print(f"[HDID {p.get('tag')}] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'wup':
b = bytes.fromhex(p.get('hex',''))
if b'hidden' in b[:50]:
print(f"[WUP] len={p.get('len')}", flush=True)
else:
h = re.search(rb'hdid.{0,120}', b)
print(f"[WUP] len={p.get('len')} ctx={h.group(0)[:120] if h else b[:60]}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'err':
print("[err]", p.get('e'), flush=True)
hdid_sc.on('message', on_message)
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 90s ...", flush=True)
t0 = time.time()
try:
while time.time() - t0 < 90:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
-164
View File
@@ -1,164 +0,0 @@
#!/usr/bin/env python3
"""模拟器 32hex hdid 抓取 v7 (严格按约定 doc)。
时序(严格复刻成功案例 hook_emu_spawn_bypass.py):
1. spawn(挂起)
2. load bypass_msaoaid_maps_art_callsite.js (挂起)
3. load mask_frida_maps_only.js (挂起)
4. resume
5. sleep 11
6. load patch_guard_block_termination.js
7. load 主 hook JS: 轮询等模块 -> hook setSafeDeviceId/getHdid/createWupDeviceInfo + SSL_write
命中即写 /tmp/emu_hdid_v7.json, 实时打印。
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v7.json")
MAIN_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); }
var hooked=false, sslDone=false;
function hookUdb(){
if (hooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return; // 未加载, 等下一轮
emit('mod-base', ''+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24));
if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd);
}
});
}catch(e){ emit('setSd-err',''+e); }
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{
var s=readStdString(this.thiz.add(1088));
if(s){ emit('GETHDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); }
}catch(e){}
}
});
}catch(e){ emit('getHdid-err',''+e); }
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{
var s=readStdString(ret.add(152));
if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); }
}catch(e){ emit('wupdev-err',''+e); }
}
});
}catch(e){ emit('wupdev-err2',''+e); }
emit('ready','all hooked');
hooked=true;
}
function hookSSL(){
if (sslDone) return;
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<50||len>50000)return;
var h=head(a[1],Math.min(len,600));
if(h.indexOf('hypasswordLogin')>=0||h.indexOf('huyaudbwebui')>=0){
emit('WUP_FRAME', len+'B '+h.slice(0,80));
}
}});
});
send({type:'sslwrite-hooked', tag:'ssl'});
}catch(e){}
sslDone=true;
}
setInterval(function(){ hookUdb(); }, 300);
setInterval(function(){ hookSSL(); }, 1000);
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:160], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
# 挂起阶段: 只加载两个 bypass (复刻成功案例)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text())
print("[*] bypass_msaoaid loaded", flush=True)
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
print("[*] mask_frida loaded", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
# 立即加载 patch_guard (不 sleep 11! bash-118 成功案例 resume 后立即加载, sleep 期间 App 被杀)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] patch_guard loaded", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t in ('SETSD','GETHDID','WUPDEV_HDID','FOUND_HDID_32','mod-base','ready',
'setSd-err','getHdid-err','wupdev-err','wupdev-err2'):
print(f"[{t}] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'WUP_FRAME':
print(f"[WUP_FRAME] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'sslwrite-hooked':
print("[*] SSL_write hooked", flush=True)
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 150s", flush=True)
t0 = time.time()
while time.time() - t0 < 150:
time.sleep(3)
if any(e.get('tag') in ('FOUND_HDID_32','SETSD','GETHDID','WUPDEV_HDID') for e in events):
print("[*] 命中 hdid, 提前停止", flush=True)
break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
-194
View File
@@ -1,194 +0,0 @@
#!/usr/bin/env python3
"""模拟器 hdid v8: spawn + 三件套 + 内存扫描 32hex (App 启动即扫, 无需触发登录).
时序(成功验证): spawn → bypass×2(挂起) → resume → 立即 patch_guard → 主JS:
1. 轮询 hook setSafeDeviceId/getHdid/createWupDeviceInfo (保险)
2. 轮询: mod-base 出现后 3s, 扫描进程内存找 [0-9a-f]{32} ASCII 字符串并去重
3. SSL_write 抓 hypasswordLogin (万一触发登录)
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v8.json")
MAIN_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); }
var hooked=false, scanned=false, sslDone=false;
function hookUdb(){
if (hooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return;
emit('mod-base', ''+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24));
if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd);
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{ var s=readStdString(this.thiz.add(1088)); if(s){ emit('GETHDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{ var s=readStdString(ret.add(152)); if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
emit('ready','all hooked');
hooked = true;
}
function scanMem(){
// 扫描所有可读内存, 找 32hex ASCII 字符串 (含大小写hex)
var found = {};
var re32 = /[0-9a-f]{32}/i;
var ranges = Process.enumerateRanges({protection:'r--', coalesce:true});
ranges.forEach(function(r){
try{
var size = Math.min(r.size, 24*1024*1024);
var buf = Memory.readByteArray(r.base, size);
if (!buf) return;
var s = String.fromCharCode.apply(null, new Uint8Array(buf));
var m;
var re = /[0-9a-f]{32,40}/gi;
while ((m = re.exec(s)) !== null){
var v = m[0].toLowerCase();
if (!/^[0-9a-f]{32}$/.test(v)) continue;
if (found[v]) { found[v]++; continue; }
found[v] = 1;
}
}catch(e){}
});
var keys = Object.keys(found);
emit('scan-result', 'found32=' + keys.length + ' | ' + keys.slice(0,40).join(' '));
keys.forEach(function(k){ if(found[k]>=1) emit('H32CAND', k); });
scanned = true;
}
function hookSSL(){
if (sslDone) return;
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<50||len>50000)return;
var h=head(a[1],Math.min(len,600));
if(h.indexOf('hypasswordLogin')>=0){
emit('WUP_LOGIN', len+'B '+hexb(a[1],len));
}
}});
});
send({type:'sslwrite-hooked', tag:'ssl'});
}catch(e){}
sslDone=true;
}
setInterval(hookUdb, 100);
setInterval(hookSSL, 1000);
// 模块出现后 3s 启动内存扫描
var scanLaunched = false;
setInterval(function(){
if (!scanLaunched && hooked && !scanned){
scanLaunched = true;
setTimeout(scanMem, 3000);
}
}, 500);
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:160], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text())
print("[*] bypass_msaoaid loaded", flush=True)
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
print("[*] mask_frida loaded", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] patch_guard loaded", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t in ('mod-base','ready','setSd-err','getHdid-err','wupdev-err'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t == 'SETSD' or t == 'GETHDID' or t == 'WUPDEV_HDID':
print(f"[{t}] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'FOUND_HDID_32':
print(f"\n★★★ FOUND 32HEX HDID = {p.get('val')} ★★★", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'scan-result':
print(f"[SCAN] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'H32CAND':
print(f"[H32CAND] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'WUP_LOGIN':
print(f"[WUP_LOGIN] {p.get('val')[:80]}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'sslwrite-hooked':
print("[*] SSL_write hooked", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 160s", flush=True)
t0 = time.time()
while time.time() - t0 < 160:
time.sleep(3)
if any(e.get('tag')=='FOUND_HDID_32' for e in events):
print("[*] 命中, 提前停止", flush=True)
break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
-203
View File
@@ -1,203 +0,0 @@
#!/usr/bin/env python3
"""模拟器 hdid v9: spawn + 三件套 + 稳健内存扫描 32hex + hook 三函数 + SSL_write 抓登录帧.
v8 失败点: String.fromCharCode.apply 大数组崩 → 改分块扫描(chunk 256KB), 累计计数。
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v9.json")
MAIN_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); }
var hooked=false, scanned=false, sslDone=false;
function hookUdb(){
if (hooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return;
emit('mod-base', ''+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24));
if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd);
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{ var s=readStdString(this.thiz.add(1088)); if(s){ emit('GETHDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{ var s=readStdString(ret.add(152)); if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
emit('ready','all hooked');
hooked = true;
}
// 稳健内存扫描: 分块读取, 用正则找 32hex ASCII
function scanMem(){
var found = {};
try{
var ranges = Process.enumerateRanges({protection:'r--', coalesce:true});
var total = ranges.length, scannedB = 0;
ranges.forEach(function(r, ri){
try{
var size = r.size;
if (size <= 0) return;
var step = 256*1024;
var off = 0;
while (off < size){
var n = Math.min(step, size - off);
var buf = Memory.readByteArray(r.base.add(off), n);
if (buf){
var sbuf = new Uint8Array(buf);
var s = '';
for (var i=0;i<sbuf.length;i++) s += String.fromCharCode(sbuf[i]);
var re = /[0-9a-f]{32,40}/gi;
var m;
while ((m = re.exec(s)) !== null){
var v = m[0].toLowerCase();
if (!/^[0-9a-f]{32}$/.test(v)) continue;
found[v] = (found[v]||0) + 1;
}
scannedB += n;
}
off += step;
}
}catch(e){}
});
}catch(e){ emit('scan-err',''+e); }
var keys = Object.keys(found);
emit('scan-result', 'ranges='+total+' scannedB='+scannedB+' found32+='+keys.length);
keys.sort().forEach(function(k, i){
if (i < 50) emit('H32CAND', k+' x'+found[k]);
// 过滤: 排除常见噪音
if (/^(00000000|ffffffff|0123456789)/.test(k)) return;
emit('H32CAND_NZ', k+' x'+found[k]);
});
scanned = true;
}
function hookSSL(){
if (sslDone) return;
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<50||len>50000)return;
var h=head(a[1],Math.min(len,600));
if(h.indexOf('hypasswordLogin')>=0){
emit('WUP_LOGIN_FULL', len+'B '+hexb(a[1],len));
}
}});
});
send({type:'sslwrite-hooked', tag:'ssl'});
}catch(e){}
sslDone=true;
}
setInterval(hookUdb, 100);
setInterval(hookSSL, 1000);
var scanLaunched = false;
setInterval(function(){
if (!scanLaunched && hooked && !scanned){
scanLaunched = true;
setTimeout(scanMem, 2500);
}
}, 500);
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:160], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text())
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
print("[*] bypass x2 loaded", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] patch_guard loaded", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t in ('mod-base','ready','scan-err'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t in ('SETSD','GETHDID','WUPDEV_HDID'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t == 'FOUND_HDID_32':
print(f"\n★★★ FOUND 32HEX HDID = {p.get('val')} ★★★", flush=True)
elif t == 'scan-result':
print(f"[SCAN] {p.get('val')}", flush=True)
elif t in ('H32CAND','H32CAND_NZ'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t == 'WUP_LOGIN_FULL':
b = bytes.fromhex(p.get('val','').split('B ')[-1]) if 'B ' in p.get('val','') else b''
h = re.search(rb'hdid.{0,100}', b)
print(f"[WUP_LOGIN] len={p.get('val','').split('B')[0]} ctx={(h.group(0)[:110] if h else b[:60])}", flush=True)
elif t == 'sslwrite-hooked':
print("[*] SSL_write hooked", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 170s", flush=True)
t0 = time.time()
while time.time() - t0 < 170:
time.sleep(3)
if any(e.get('tag')=='FOUND_HDID_32' for e in events):
print("[*] 命中, 提前停止", flush=True)
break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python3
"""抓模拟器自生成 32hex hdid — attach 主进程版 (PID=10200).
hook libudbauthunify.so:
- BusinessCfg::setSafeDeviceId(0x26A2E0): a2=sd, a3=hdid
- BusinessCfg::getHdid(0x26A484): this+1088 = std::string
- createWupDeviceInfo(0x2746A0): 返回 +152 = hdid
"""
import time, frida
REMOTE = "127.0.0.1:31878"
PID = 10200
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function log(t,m){ send({t:t,m:m}); }
function install(){
var md = Process.findModuleByName('libudbauthunify.so');
if(!md){ log('err','no module'); return; }
log('mod','base='+md.base+' size='+md.size);
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){ log('setSafeDeviceId', JSON.stringify({sd:(this.sd||'').slice(0,48), hd:(this.hd||'').slice(0,48)})); }
});
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ this.thiz=a[0]; },
onLeave:function(){
try{
var s=readStdString(this.thiz.add(1088));
log('getHdid', (s!==null&&s!==undefined?'':s)||'EMPTY');
}catch(e){ log('getHdid','ERR '+e); }
}
});
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{
var s=readStdString(ret.add(152));
if (s) log('wupDevInfo.hdid', ''+s);
}catch(e){}
}
});
// 也 hook libhydeviceid 数据区常见字符串? 先看 getHdid 输出
log('hooked','all OK');
}
install();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
pid = PID
try:
s = dev.attach(pid)
except Exception as e:
print("attach失败:", e); return
print("attached", pid, flush=True)
sc = s.create_script(JS)
def on_msg(m,d):
if m.get('type')=='send':
p=m['payload']
print("[%s] %s" % (p['t'], p['m']), flush=True)
elif m.get('type')=='error':
print("JSErr:", str(m)[:200], flush=True)
sc.on('message', on_msg)
sc.load()
print("等待 90s ...", flush=True)
time.sleep(90)
print("done", flush=True)
if __name__ == "__main__":
main()
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""attach 模式抓模拟器 32hex hdid v4: hook createWupDeviceInfo(ret+152=hdid).
直接 attach 正在运行的主进程(不 spawn, 无 EGL 崩溃风险)。
createWupDeviceInfo 每次 udb 上报/wup 打包都调用 → 上报即触发, 无需登录UI。
"""
import frida, time, re, json, sys
REMOTE = "127.0.0.1:31878"
OUT = Path_ = "/tmp/emu_hdid_v4.json"
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function emit(tag, val){ send({type:'hdid', tag:tag, val:''+(val||''), ts:Date.now()}); }
function install(){
var md = Process.findModuleByName('libudbauthunify.so');
if (!md){ emit('state','NO_MOD'); return; }
emit('state','mod='+md.base);
try{
Interceptor.attach(md.base.add(0x2746A0), {
onEnter:function(){ emit('tick','createWupDeviceInfo #' + (++window.__n||(window.__n=1))); },
onLeave:function(ret){
try{
var s = readStdString(ret.add(152));
emit('WUPDEV_HDID', ''+s);
}catch(e){ emit('WUPDEV_ERR', ''+e); }
}
});
emit('state','createWupDev hooked');
}catch(e){ emit('state','createWupDev ERR '+e); }
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){ emit('SETSD', (this.hd||'')+' | sd='+(this.sd||'').slice(0,24)); }
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{ var s=readStdString(this.thiz.add(1088)); if(s) emit('GETHDID',''+s); }catch(e){}
}
});
}catch(e){}
emit('state','all hooked, waiting events');
}
install();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
pid = None
for p in dev.enumerate_processes():
if p.name and p.name == 'com.duowan.kiwi':
pid = p.pid; break
if not pid:
print("找不到主进程"); return
print("attach pid =", pid, flush=True)
s = dev.attach(pid)
events = []
def on_msg(m, d):
if m.get('type') == 'send':
p = m['payload']
print(f"[{p.get('tag')}] {p.get('val')}", flush=True)
events.append(p); open(OUT,'w').write(json.dumps(events, indent=1))
elif m.get('type') == 'error':
print("JSErr:", str(m)[:150], flush=True)
sc = s.create_script(JS)
sc.on('message', on_msg)
sc.load()
print("运行 100s (App 上报会自动触发 createWupDeviceInfo)...", flush=True)
time.sleep(100)
print("done,", len(events), "events", flush=True)
if __name__ == "__main__":
main()
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env python3
"""模拟器 spawn+立即resume 版 dfpReport 捕获。
关键: spawn 后【立即】resume(App 避免 EGL 崩溃), 然后立即 attach+load 主 hook。
dfpReport 冷启动约 1s 后自动发, attach 足够快能赶上。
"""
from __future__ import annotations
import frida, time, json, re
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_spawn_dfp.json")
JS = r"""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
# 立即 resume (防 EGL 崩)
d.resume(pid)
time.sleep(0.4)
session = d.attach(pid)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed':
print("[*] armed", flush=True)
elif t == 'hooked':
print("[*] SSL_write hooked", flush=True)
elif t == 'err':
print("[*] err", p.get('e'), flush=True)
elif 'cls' in p:
print(f"[{p.get('t')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = session.create_script(JS)
sc.on('message', on_message)
sc.load()
print(f"[*] loaded. running (OUT={OUT})", flush=True)
t0 = time.time()
try:
while time.time() - t0 < 60:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done, {len(events)} dfpRevent(s)", flush=True)
if __name__ == "__main__":
main()
-119
View File
@@ -1,119 +0,0 @@
#!/usr/bin/env python3
"""模拟器 spawn + frida 绕过 版 dfpReport 捕获。
时序(模拟器需快速 resume 防 EGL 崩, 但反调试需在 resume 前 hook):
spawn(挂起) → 快速 load bypass_msaoaid + mask_frida (~1s内)
→ resume → patch_guard → 主 hook (抓 dfpReport+响应 actionV)
bypass 脚本用动态定位(Process.findModuleByName), 偏移为 lib 内偏移,
同一 apk(13.4.22)下与真机一致, 直接复用。
"""
from __future__ import annotations
import frida, time, json, re, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_spawn_bypass_dfp.json")
JS = r"""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'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];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>3200)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:150], flush=True); return None
def main():
subprocess.run(["adb", "-s", "127.0.0.1:5555", "shell", "am", "force-stop", PACKAGE], capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
# 1) 挂起时快速加载两个 bypass
print("[*] load bypass_msaoaid (suspended)", flush=True)
load(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
print("[*] load mask_frida (suspended)", flush=True)
load(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
# 2) resume (防 EGL 崩), 快速
d.resume(pid)
print("[*] resumed", flush=True)
# 3) patch_guard + 主 hook
load(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] load 主 hook JS", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed': print("[*] armed", flush=True)
elif t == 'hooked': print("[*] SSL_write hooked", flush=True)
elif t == 'readhooked': print("[*] SSL_read hooked", flush=True)
elif t in ('err','readerr'): print("[*]", t, p.get('e'), flush=True)
elif t == 'event': print("[bypass]", p.get('event'), flush=True)
elif 'cls' in p:
print(f"[{p.get('t')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif 'resp' in t or t == 'resp':
import re
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[{p.get('t')}] RESP len={p.get('len')}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = load(session, JS)
if sc:
sc.on('message', on_message)
print(f"[*] running (OUT={OUT})", flush=True)
t0 = time.time()
try:
while time.time() - t0 < 70:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
-128
View File
@@ -1,128 +0,0 @@
#!/usr/bin/env python3
"""模拟器 稳定版 dfpReport 抓取: spawn→挂起快速加载2个bypass→立即resume→patch_guard+主hook。
内置自动重试(EGL 偶发崩 + 反调试偶发), 一旦抓到 dfpReport+actionV 或达上限即停。
"""
from __future__ import annotations
import frida, time, json, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_stable_dfp.json")
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
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<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'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];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def run_once(d, events, attempt):
"""一次尝试, 成功(抓到 actionV 或 dfp) 返回 True."""
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
pid = d.spawn([PACKAGE])
print(f"[att{attempt}] spawn pid={pid}", flush=True)
session = d.attach(pid)
# 挂起时快速加载两个 bypass (极快, 不 sleep)
def fast_load(path):
try:
s = session.create_script(path.read_text()); s.load(); return True
except Exception as e:
print(f" [bypass-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")
# 立即 resume
d.resume(pid)
print(f"[att{attempt}] resumed", flush=True)
got_dfp = False
def on_main(m, dta):
nonlocal got_dfp
if m.get('type') == 'error':
return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print(f"[att{attempt}] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[att{attempt}] {p['cls']} len={p['len']}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
if p['cls'] == 'dfpReport':
got_dfp = True
elif t == 'resp':
import re
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[att{attempt}] RESP len={p['len']}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
# patch_guard + 主 hook (resume 后)
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)
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 观察 ~35s, 抓到 dfp 即提前停
for _ in range(8):
time.sleep(4)
if got_dfp:
# 再等 actionV 响应
time.sleep(4)
print(f"[att{attempt}] dfp captured, stopping", flush=True)
break
try: d.kill(pid)
except: pass
return got_dfp
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
events = []
for attempt in range(1, 6): # 最多 5 次
try:
if run_once(d, events, attempt):
print(f"[*] SUCCESS on attempt {attempt}", flush=True)
break
except Exception as e:
print(f"[att{attempt}] ERROR {repr(e)}", flush=True)
time.sleep(2)
print(f"[*] done, {len(events)} events, saved {OUT}", flush=True)
if __name__ == "__main__":
main()
-169
View File
@@ -1,169 +0,0 @@
#!/usr/bin/env python3
"""按已验证成功的时序采集证书生成全链路数据。
时序(不可变): spawn挂起 -> bypass单独装载 -> resume -> 稳定10s -> patch_guard -> 业务hooks
然后提示用户 手动退出登录 -> 重新登录。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
var base = Process.getModuleByName("libudbauthunify.so").base;
send({type:'armed', base:String(base)});
function b64ify(ptr, n) {
var arr = new Uint8Array(ptr.readByteArray(n));
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var c = "";
for (var i=0;i<arr.length;i+=3){
var b0=arr[i],b1=i+1<arr.length?arr[i+1]:0,b2=i+2<arr.length?arr[i+2]:0;
c+=B64[b0>>2]+B64[((b0&3)<<4)|(b1>>4)]+(i+1<arr.length?B64[((b1&15)<<2)|(b2>>6)]:"=")+(i+2<arr.length?B64[b2&63]:"=");
}
return c;
}
function rdStrObj(p){
try{
var b0=p.readU8();
if((b0&1)===0) return {t:"s",len:b0>>1,v:p.add(1).readUtf8String(b0>>1)};
var len=parseInt(p.add(8).readU64().toString());
if(len>16384) return {t:"err",v:"too long"};
return {t:"b64",v:b64ify(p.add(16).readPointer(),len)};
}catch(e){return {t:"err",v:String(e)};}
}
function hexof(p,n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(function(x){return ('0'+x.toString(16)).slice(-2);}).join(''); }
catch(e){ return 'ERR'; }
}
Interceptor.attach(base.add(0x32e71c), {
onEnter: function(a){ this.inp = rdStrObj(a[0]); this.outp = a[1]; },
onLeave: function(){ send({type:'md5', inp:this.inp, out:rdStrObj(this.outp)}); }
});
Interceptor.attach(base.add(0x32fa24), {
onEnter: function(a){
this.a = {s1:rdStrObj(a[0]), b1:a[1].toInt32(), b2:a[2].toInt32(),
s2:rdStrObj(a[3]), s3:rdStrObj(a[4]), s4:rdStrObj(a[5]),
b3:a[6].toInt32(), nonce:a[7].toString()};
this.outp = this.context.sp.readPointer();
},
onLeave: function(){ send({type:'otp', args:this.a, out:rdStrObj(this.outp)}); }
});
// sret 约定: x0=返回串, x1=this, x2=in, x3=key
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){
this.retp = a[0];
this.inp = rdStrObj(a[2]);
this.key = rdStrObj(a[3]);
this.thishead = hexof(a[1],48);
},
onLeave: function(){
send({type:'aes', inp:this.inp, key:this.key, out:rdStrObj(this.retp),
this_head:this.thishead});
}
});
Interceptor.attach(base.add(0x38dab0), {
onEnter: function(a){ this.ret = a[0]; },
onLeave: function(){ send({type:'createWup', result:rdStrObj(this.ret)}); }
});
Java.perform(function(){
var n = 0;
function go(){
try {
var seed = Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F = Java.ClassFactory.get(seed.class.getClassLoader());
var inst = F.use('com.hysdkproxy.LoginProxy').getInstance();
var q = inst.getQUrlData(1199666914671, "", "");
send({type:'qurl_done', len: q?q.length:0});
} catch(e){ n+=1; if(n%8===0) send({type:'retry', n:n}); setTimeout(go, 5000); }
}
setTimeout(go, 20000);
});
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
events = []
def on_msg(m, _):
if m.get('type') != 'send':
if m.get('type') == 'error':
print('ERR:', str(m)[:160])
return
p = m.get('payload') or {}
t = p.get('type')
if t != 'retry':
events.append(p)
if t == 'armed':
print(f'[armed] base={p["base"]}')
elif t == 'otp':
a = p['args']
print(f"[otp] uid={str(a['s1'].get('v'))[:18]} b={a['b1']},{a['b2']},{a['b3']}")
elif t == 'md5':
print(f"[md5] in={str(p['inp'].get('v',''))[:36]} out={str(p['out'].get('v',''))[:40]}")
elif t == 'aes':
k = p.get('key',{})
print(f"[aes] key[{k.get('t')}]={str(k.get('v',''))[:44]}")
print(f" in={str(p.get('inp',{}).get('v',''))[:90]}")
print(f" out={str(p.get('out',{}).get('v',''))[:60]}")
elif t == 'createWup':
print(f"[createWup] len={len(str(p['result'].get('v','')))}")
elif t == 'qurl_done':
print(f"[qurl_done] {p.get('len')}")
sc = s.create_script(JS)
sc.on('message', on_msg)
sc.load()
print('\n>>> 请现在在手机上: 手动退出登录 -> 重新登录 <<<\n')
deadline = time.time() + 260
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'qurl_done' for e in events):
time.sleep(6)
break
Path('/tmp/final_capture.json').write_text(json.dumps(events, ensure_ascii=False))
types = {}
for e in events:
types[e.get('type')] = types.get(e.get('type'), 0) + 1
print('saved:', types)
if __name__ == '__main__':
import subprocess
for attempt in range(4):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb','shell','am','force-stop','com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
time.sleep(3)
-158
View File
@@ -1,158 +0,0 @@
#!/usr/bin/env python3
"""解剖 gen_biz_token: 抓 BIZTOKEN 枚举/入参/xxtea 明文与密钥/enpack_body 字段。
目的: 搞清 P1 中 20B nonce 字段的生成规则(服务端校验它 -> 重铸被 40020 拒)。
时序(不可变): force-stop -> spawn挂起 -> 双bypass -> resume -> 10s -> patch_guard -> hooks
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UID = 1199666914671
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}});
return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>65536) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?hx(p.add(1),l):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>262144) return {t:'l?',len:len,v:''};
return {t:'l',len:len,v:hx(p.add(16).readPointer(),Math.min(len,4096))};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function ra(ctx){ try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,80);}catch(e){return '?';} }
// gen_biz_token(BIZTOKEN w0, const string& x1, const string& x2, string& x3)
Interceptor.attach(base.add(0x3307e4), {
onEnter: function(a){
this.enum=a[0].toInt32()&0xff;
this.s1=rdStr(a[1]); this.s2=rdStr(a[2]);
this.r=ra(this);
},
onLeave: function(){ send({type:'genbiz', enum:this.enum, r:this.r,
s1:this.s1, s2:this.s2}); }
});
// hyudb_crypt_util::xxtea_encrypt(out&, const in&, key*) void
Interceptor.attach(base.add(0x32e83c), {
onEnter: function(a){
send({type:'xxtea', r:ra(this),
out_len:rdStr(a[0]).len, inp:rdStr(a[1]), key:rdStr(a[2])});
}
});
// enpack_header(uchar w0, NEWKEY x1, string& x2)
Interceptor.attach(base.add(0x32ff44), {
onEnter: function(a){ send({type:'hdr', h:a[0].toInt32()&0xff,
newkey:rdStr(a[1]), r:ra(this)}); }
});
// enpack_body(uchar x0, str* x1(byval), const str& x2, s3, s4, s5)
Interceptor.attach(base.add(0x3300c8), {
onEnter: function(a){ send({type:'body', h:a[0].toInt32()&0xff,
p1:rdStr(a[1]), p2:rdStr(a[2]), p3:rdStr(a[3]), p4:rdStr(a[4]), r:ra(this)}); }
});
// encode_aes(P1, key24, out) —— 最终明文+密钥对
Interceptor.attach(base.add(0x330218), {
onEnter: function(a){ this.p0=rdStr(a[0]); this.p1=rdStr(a[1]); },
onLeave: function(){ send({type:'encode_aes', p0:this.p0, p1:this.p1}); }
});
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done'});
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 20000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
sc = s.create_script(JS)
events, armed = [], []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
if t not in ('retry',):
events.append(p)
if t == 'armed':
armed.append(1)
elif t == 'genbiz':
print(f"[genbiz] enum={p['enum']} s1={p['s1']['t']}:{p['s1']['len']}={p['s1']['v'][:64]} "
f"s2={p['s2']['t']}:{p['s2']['len']}={p['s2']['v'][:64]} <- {p['r'][:48]}")
elif t == 'xxtea':
print(f"[xxtea] in={p['inp']['t']}:{p['inp']['len']}:{p['inp']['v'][:64]} "
f"key={p['key']['t']}:{p['key']['len']}:{p['key']['v'][:40]}")
elif t == 'hdr':
print(f"[enpack_header] h={p['h']:#04x} newkey={p['newkey']}")
elif t == 'body':
print(f"[enpack_body] h={p['h']:#04x} p1={p['p1']['t']}:{p['p1']['len']}:{p['p1']['v'][:96]} "
f"p2={p['p2']['t']}:{p['p2']['len']} p3={p['p3']['t']}:{p['p3']['len']}:{p['p3']['v'][:64]} "
f"p4={p['p4']['t']}:{p['p4']['len']}:{p['p4']['v'][:64]}")
elif t == 'encode_aes':
print(f"[encode_aes] P1={p['p0']['t']}:{p['p0']['len']} head={p['p0']['v'][:96]} "
f"key={p['p1']['t']}:{p['p1']['len']}:{p['p1']['v']}")
sc.on('message', on_msg)
sc.load()
assert armed
deadline = time.time() + 90
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'qurl_done' for e in events):
time.sleep(8)
break
Path('/Users/yml/codes/douyu_login_py/evidence/genbiz_trace.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
time.sleep(3)
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){ try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; return {len:l,v:l?hx(p.add(1),Math.min(l,256)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>4096) return {len:len,v:''};
return {len:len,v:hx(p.add(16).readPointer(),len)};
}catch(e){ return {len:-1,v:String(e)}; } }
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var done=false;
['com.huyaudbunify.HuyaAuth','com.hysdkproxy.HuyaAuth','com.huyaudbunify.sdk.HuyaAuth'].forEach(function(cn){
if(done) return;
try{
var C=F.use(cn);
var inst = C.getInstance ? C.getInstance() : null;
var r = inst ? inst.getCred(1199666914671) : C.getCred(1199666914671);
send({type:'java_cred', cls:cn, r:String(r).slice(0,400)});
done=true;
}catch(e2){}
});
if(!done) throw new Error('no HuyaAuth');
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 15000);
});
Interceptor.attach(base.add(0x265524), {
onEnter: function(a){ this.uid=a[1].toString(); this.out=a[2]; },
onLeave: function(){ send({type:'getcred', uid:this.uid, out:rdStr(this.out)}); }
});
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type') in ('java_cred','getcred') for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/getcred.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-194
View File
@@ -1,194 +0,0 @@
#!/usr/bin/env python3
"""间接调用解析器 v3 (按需启用, 零常驻开销):
默认不挂任何桩 hook; copy_61098 轮次边界(x1=MAGIC)触发时动态 attach 72 个 br 桩,
3 秒后全部 detach。捕获 crypto 调用图谱定位 keystream 内核。
"""
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/got_resolve.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var t0 = Date.now();
var MAGIC_HDR = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94,0x01,0xee];
var modBase = null;
var counts = new Map();
var roundN = 0;
var stubHandles = []; // Interceptor handles to detach
var stubsActive = false;
var activeSince = 0;
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 startsWith(p, arr){
try{ var b = new Uint8Array(p.readByteArray(arr.length)); for (var i=0;i<arr.length;i++) if (b[i]!==arr[i]) return false; return true; }catch(e){ return false; }
}
function hookBrSite(addr){
try{
var h = Interceptor.attach(addr, {
onEnter: function(a){
var ctx = this.context;
var tgt = ctx.x17;
if (tgt.isNull()) return;
var off = tgt.sub(modBase).toInt32();
counts.set(off, (counts.get(off) || 0) + 1);
}
});
return h;
}catch(e){ return null; }
}
function enableStubs(){
if (stubsActive) return;
stubsActive = true;
activeSince = Date.now();
stubHandles = [];
for (var i = 0; i < 24; i++){
var h = hookBrSite(modBase.add(0x461c0 + i*0x10 + 0xc));
if (h) stubHandles.push(h);
}
for (var i = 0; i < 48; i++){
var h = hookBrSite(modBase.add(0x45aa0 + i*0x10 + 0xc));
if (h) stubHandles.push(h);
}
send({type:'stubs_on', n: stubHandles.length, t: Date.now()-t0});
// 3s 后自动摘除
setTimeout(function(){
if (!stubsActive) return;
for (var j = 0; j < stubHandles.length; j++){
try{ stubHandles[j].detach(); }catch(e){}
}
stubHandles = [];
stubsActive = false;
send({type:'stubs_off', t: Date.now()-t0});
}, 3000);
}
function tryHookRound(){
try{
var m = Process.getModuleByName('libhydeviceid.so');
modBase = m.base;
Interceptor.attach(m.base.add(0x61098), {
onEnter: function(a){
var ctx = this.context;
if (!startsWith(ctx.x1, MAGIC_HDR)) return;
roundN++;
// dump round data BEFORE enabling stubs (crypto already done)
var arr = [];
counts.forEach(function(v, k){ if (v > 2) arr.push([k, v]); });
send({type:'round', n: roundN, t: Date.now()-t0, counts: arr, activeMs: Date.now()-activeSince});
counts = new Map();
send({type:'magic', t: Date.now()-t0, full: hexb(ctx.x1, 4300)});
// 延迟到回调外再挂桩(frida 回调内 attach 会失败)
setTimeout(function(){ counts = new Map(); enableStubs(); }, 15);
}
});
send({type:'round_hooked'});
}catch(e){ setTimeout(tryHookRound, 500); }
}
setTimeout(tryHookRound, 400);
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 < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
send({type:'wire', len:len, t: Date.now()-t0});
}
});
});
send({type:'ssl_hooked'});
}catch(e){ send({type:'ssl_err', e:String(e)}); }
"""
def load_with_retry(session, js, n=4):
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():
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)[:200], 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 == "round_hooked":
print("[*] round boundary hooked", flush=True)
elif t == "stubs_on":
print(f"[t={p.get('t')}ms] stubs ON x{p.get('n')}", flush=True)
elif t == "stubs_off":
print(f"[t={p.get('t')}ms] stubs OFF", flush=True)
elif t == "round":
c = dict(p.get('counts') or [])
top = sorted(c.items(), key=lambda x: -x[1])[:15]
print(f"[t={p.get('t')}ms round{p.get('n')}] (stubs active {p.get('activeMs')}ms) calls={sum(c.values())} distinct={len(c)}", flush=True)
for off, cnt in top:
print(f" off=0x{off:x} x{cnt}", flush=True)
events.append({"type": "round", **p})
OUT.write_text(json.dumps(events))
elif t == "magic":
events.append({"type": "magic", "t": p.get('t'), "full": p.get('full')})
OUT.write_text(json.dumps(events))
elif t == "wire":
print(f"[t={p.get('t')}ms] [wire] len={p.get('len')}", flush=True)
events.append({"type": "wire", **p})
OUT.write_text(json.dumps(events))
# 严格按约定顺序: spawn -> bypass x2 -> resume + sleep 11 -> patch_guard -> 最后主 hook JS
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("[*] all loaded. waiting dfpReport", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] done -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-240
View File
@@ -1,240 +0,0 @@
#!/usr/bin/env python3
"""全自动 hook:触发并抓取虎牙 App 的 H5InfoEx(证书)生成全链路。
流程: spawn 挂起 → 构造窗口挂 bypass → resume → 稳定后挂业务 hooks
→ 通过 Java 桥直呼 LoginProxy.getH5InfoEx() 触发生成(零人工)
输出: /tmp/h5infoex_dump.jsonl
"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
import frida
RE_DIR = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS_AGENT = r"""
'use strict';
function rdStr(p) {
try {
var b0 = p.readU8();
if ((b0 & 1) === 0) {
var n = b0 >> 1;
return {t:"s", v: p.add(1).readUtf8String(n)};
}
var len = parseInt(p.add(8).readU64().toString());
var dp = p.add(16).readPointer();
return {t:"s", v: dp.readUtf8String(len)};
} catch (e) {
// 二进制内容: 走 base64
try {
var b0b = p.readU8();
if ((b0b & 1) === 0) {
var nb = b0b >> 1;
return {t:"b64", v: base64ify(p.add(1), nb)};
}
var len2 = parseInt(p.add(8).readU64().toString());
var dp2 = p.add(16).readPointer();
return {t:"b64", v: base64ify(dp2, Math.min(len2, 4096))};
} catch (e2) {
return {t:"err", v: String(e)};
}
}
}
function base64ify(ptr, n) {
var bytes = ptr.readByteArray(n);
var arr = new Uint8Array(bytes);
var chars = "";
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (var i = 0; i < arr.length; i += 3) {
var b0 = arr[i], b1 = i+1 < arr.length ? arr[i+1] : 0, b2 = i+2 < arr.length ? arr[i+2] : 0;
chars += B64[b0>>2] + B64[((b0&3)<<4)|(b1>>4)] +
(i+1 < arr.length ? B64[((b1&15)<<2)|(b2>>6)] : "=") +
(i+2 < arr.length ? B64[b2&63] : "=");
}
return chars;
}
var base = Process.getModuleByName("libudbauthunify.so").base;
send({type:"info", msg:"base=" + base});
// hyudb_otp_encrypt(string,u8,u8,string,string,string,u8,ulong,string&)
Interceptor.attach(base.add(0x32fa24), {
onEnter: function (args) {
this.s1 = rdStr(args[0]);
this.b1 = args[1].toInt32();
this.b2 = args[2].toInt32();
this.s2 = rdStr(args[3]);
this.s3 = rdStr(args[4]);
this.s4 = rdStr(args[5]);
this.b3 = args[6].toInt32();
this.nonce = args[7].toString();
this.outPtr = this.context.sp.readPointer();
},
onLeave: function () {
send({type:"otp", s1:this.s1, b1:this.b1, b2:this.b2, s2:this.s2,
s3:this.s3, s4:this.s4, b3:this.b3, nonce:this.nonce,
out: rdStr(this.outPtr)});
}
});
// BusinessCfg::getOtp(u64, string&, int&)
Interceptor.attach(base.add(0x2689f4), {
onEnter: function (args) {
this.uid = args[1].toString();
this.outStr = args[2];
},
onLeave: function () {
send({type:"getOtp", uid:this.uid, out: rdStr(this.outStr)});
}
});
// WupDataPackage<AppCommonData>::createWupRequestData(...)
Interceptor.attach(base.add(0x38dab0), {
onEnter: function (args) {
this.ret = args[0];
this.c3 = args[3].readCString();
this.c4 = args[4].readCString();
},
onLeave: function () {
send({type:"createWup", servant:this.c3, func:this.c4,
result_b64: rdStr(this.ret)});
}
});
send({type:"ready"});
"""
JS_TRIGGER = r"""
'use strict';
function tryTrigger() {
Java.perform(function () {
var found = false;
Java.enumerateClassLoaders({
onMatch: function (loader) {
if (found) return;
try {
var factory = Java.ClassFactory.get(loader);
var LP = factory.use('com.hysdkproxy.LoginProxy');
var inst = LP.getInstance();
var r = inst.getH5InfoEx();
found = true;
send({type:'trigger_ok', len: r ? r.length : 0,
head: r ? r.substring(0, 80) : 'null'});
} catch (e) {
send({type:'trigger_try_err', err: String(e).substring(0,120)});
}
},
onComplete: function () {
if (!found) send({type:'trigger_fail'});
}
});
});
}
tryTrigger();
"""
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--duration", type=float, default=90.0)
ap.add_argument("--stabilize", type=float, default=10.0)
ap.add_argument("--out", default="/tmp/h5infoex_dump.jsonl")
ap.add_argument("--package", default="com.duowan.kiwi")
ap.add_argument("--port", default="127.0.0.1:31877")
args = ap.parse_args()
bypass_src = (RE_DIR / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
patch_guard = (RE_DIR / "evidence/scripts/patch_guard_block_termination.js").read_text()
device = frida.get_device_manager().add_remote_device(args.port)
print(f"remote device ok")
# 1) spawn 挂起 → 构造窗口期只挂 bypass
pid = device.spawn([args.package])
print(f"spawned pid={pid}")
session = device.attach(pid)
s_bypass = session.create_script(bypass_src)
s_bypass.load()
device.resume(pid)
print("bypass loaded, resumed")
time.sleep(args.stabilize)
# 2) patch_guard + 业务 hooks
session.create_script(patch_guard).load()
out_path = Path(args.out)
fout = out_path.open("w")
got = {"otp": False, "wup": False}
def on_message(message, _data):
if message.get("type") != "send":
if message.get("type") == "error":
print("SCRIPT ERR:", str(message)[:200])
return
p = message.get("payload") or {}
fout.write(json.dumps(p, ensure_ascii=False) + "\n")
fout.flush()
t = p.get("type")
def unwrap(x):
if isinstance(x, dict) and "t" in x:
v = x["v"]
tag = x["t"]
if isinstance(v, str) and len(v) > 200:
return f"[{tag}]{v[:150]}...({len(v)}B)"
return f"[{tag}]{v}"
return x
if t == "otp":
got["otp"] = True
print("\n=== hyudb_otp_encrypt ===")
for k in ("s1","b1","b2","s2","s3","s4","b3","nonce"):
print(f" {k} = {unwrap(p.get(k))}")
print(f" out = {unwrap(p.get('out'))}")
elif t == "createWup":
got["wup"] = True
r = p["result_b64"]
print(f"\n=== createWupRequestData servant={p['servant']} func={p['func']} ===")
print(f" result({len(r)}B) = {r[:140]}...")
elif t == "getOtp":
o = p.get("out")
if isinstance(o, dict):
o = f"[{o['t']}]{str(o['v'])[:140]}"
print(f"\n=== getOtp uid={p['uid']} out={o}")
elif t in ("ready", "info"):
print(f"[{t}] {p.get('msg','')}")
script = session.create_script(JS_AGENT)
script.on("message", on_message)
script.load()
print("\nhooks armed")
# 3) 轮询式 Java 触发(等 App 完全初始化)
s_trig = session.create_script(JS_TRIGGER)
s_trig.on("message", on_message)
deadline = time.time() + args.duration
triggered = False
while time.time() < deadline and not triggered:
try:
s_t2 = session.create_script(JS_TRIGGER)
s_t2.on("message", on_message)
s_t2.load()
time.sleep(6)
if got["otp"] or got["wup"]:
triggered = True
except Exception as e:
print("trigger retry:", str(e)[:80])
time.sleep(3)
# 等最后的异步 dump 落盘
time.sleep(3)
fout.close()
print(f"\nresult: otp={got['otp']} wup={got['wup']} -> {out_path}")
return 0 if (got["otp"] and got["wup"]) else 1
if __name__ == "__main__":
raise SystemExit(main())
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""R36: hook libz deflate/inflate — 抓 XXTEA 输入明文 + 隐藏加密路径 backtrace."""
import json, subprocess, time
from pathlib import Path
import frida
REMOTE="127.0.0.1:31878"; ADB="5dd8c93f"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
HERE=Path(__file__).resolve().parent.parent
OUT=HERE/"evidence"/("hy_"+time.strftime("%H%M%S")+".json")
def clear_turing():
for d in ("app_turingdfp","app_turingfd"):
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"rm -rf /data/data/{PACKAGE}/{d}/*"],capture_output=True)
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"find /data/data/{PACKAGE} -name 'resinfo*' -delete"],capture_output=True)
def main():
clear_turing()
d=frida.get_device_manager().add_remote_device(REMOTE)
subprocess.run(["adb","-s",ADB,"shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.5)
pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True)
s=d.attach(pid)
from bypass_loader import load_bypass
load_bypass(s)
d.resume(pid)
result={}; got={"post":False,"out":False}
seen_fns=set()
def events_ok(p):
seen_fns.add(p.get('fn')); return True
def on(m,_):
if m.get("type")=="error":
txt=str(m)
if "ClassNotFoundException" in txt: return
print(f"[JS] {txt[:160]}",flush=True); return
p=m.get("payload") or {}
ev=p.get("event")
if ev=="post": got["post"]=True; print(f"[+] POST {p['num']}B",flush=True)
elif ev=="armed-hy": print("[*] hy chain armed",flush=True)
elif ev=="call":
if len(seen_fns)<99: pass
elif ev=="call" and events_ok(p): got["out"]=True
elif ev=="deflate-found": print(f"[*] deflate export in {p['mod']}",flush=True)
elif ev=="inflate-in": print(f"[+] inflate-in {p['len']}B via {p['mod']}",flush=True)
result.setdefault(ev or "misc",[]).append(p)
sc=s.create_script((HERE/"tools/frida/hook_hy.js").read_text())
sc.on("message",on); sc.load()
t0=time.time()
while time.time()-t0<150:
time.sleep(2)
if got["out"] and got["post"]: time.sleep(4); break
json.dump(result,open(OUT,"w"),indent=2)
print(f"[*] saved {OUT}",flush=True)
try: s.detach()
except Exception: pass
if __name__=="__main__": main()
-129
View File
@@ -1,129 +0,0 @@
#!/usr/bin/env python3
"""定位 k1 存储: hook getInstance(0x281270) 拿单例, 读 +0x10; 内存搜 k1 出现位置看周边结构。"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
K1 = "865a4924a40897ac1fcfe6b4c2cbb0e3"
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true; });
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>8192) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
// 1) getInstance -> 单例地址 + 读 +0x10
try {
Interceptor.attach(base.add(0x281270), {
onLeave: function(r){
var s = rdStr(r.add(0x10));
send({type:'cfg', addr:String(r), k1:s, k1ascii:(function(h){var o='';for(var i=0;i<h.length;i+=2){var c=parseInt(h.substr(i,2),16);o+=(c>=32&&c<127)?String.fromCharCode(c):'.';}return o;})(s.v)});
// dump 前 0x60 成员指针(可能含其它 string)
try {
var hex='';
for (var off=0; off<0x60; off+=8) hex += hx(r.add(off),8);
send({type:'cfg_dump', hex:hex});
} catch(e){}
}
});
} catch(e){ send({type:'hookerr', where:'getInstance', msg:''+e}); }
// 2) hook loadLoginData(未知地址) 跳过; 直接定时 dump
setTimeout(function(){
try {
// 找已缓存的单例: getInstance 是懒加载, 定时再调一次读
var f = new NativeFunction(base.add(0x281270), 'pointer', []);
var inst = f();
var s = rdStr(inst.add(0x10));
send({type:'tick_k1', addr:String(inst), k1:s});
} catch(e){ send({type:'tick_err', msg:''+e}); }
}, 30000);
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:240]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'cfg':
print(f"[cfg] addr={p['addr']} k1len={p['k1']['len']} k1ascii={p['k1ascii']}")
elif t == 'cfg_dump':
print(f"[cfg_dump] {p['hex']}")
elif t == 'tick_k1':
print(f"[tick_k1] addr={p['addr']} k1={p['k1']['v'][:40]}({p['k1']['len']})")
elif t == 'tick_err':
print('[tick_err]', p['msg'][:120])
elif t == 'hookerr':
print('[hookerr]', p)
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 90
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'tick_k1' for e in events):
time.sleep(3)
break
Path('/tmp/opencode/k1_cfg.json').write_text(json.dumps(events))
print('saved /tmp/opencode/k1_cfg.json')
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-157
View File
@@ -1,157 +0,0 @@
#!/usr/bin/env python3
"""k1 账号绑定判定: 对多个不同 uid 调 getQUrlData, 看 xxtea key 是否变化。
若 k1(=this+0x10, MD5输入的一半)随 uid 变 -> 账号绑定, 纯协议死路;
若 k1 不变 -> 设备级常量, 任意账号可本地复算 nonce, 纯协议成立!
同时验证 serviceTime/nonce_next 的生成与 uid 无关。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UIDS = [1199666914671, 1199666911746] # 可信账号 + 另一账号(新账号uid可能不存在, 但getQUrlData只看参数)
def build_js(uids):
arr = ",".join(str(u) for u in uids)
return r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>8192) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function ascii(h){ if(!h) return ''; var s=''; for(var i=0;i<h.length;i+=2){ var c=parseInt(h.substr(i,2),16); s += (c>=32&&c<127)?String.fromCharCode(c):'.'; } return s; }
// gen_biz_token(struct, uid, k1, k2, out)
try { Interceptor.attach(base.add(0x3307e4), {
onEnter: function(a){
send({type:'gbt_in', struct:hx_s(a[0],0x60), uid:rdStr(a[1]), k1:rdStr(a[2]), k2:rdStr(a[3])});
}
}); } catch(e){ send({type:'hookerr',where:'gen_biz_token',msg:''+e}); }
// xxtea
try { Interceptor.attach(base.add(0x32e83c), {
onEnter: function(a){
this.out=a[0];
send({type:'xxtea_in', data:rdStr(a[1]), key:rdStr(a[2]),
key_ascii:ascii(rdStr(a[2]).v)});
},
onLeave: function(){ var o=rdStr(this.out); send({type:'xxtea_out', out:o}); }
}); } catch(e){ send({type:'hookerr',where:'xxtea',msg:''+e}); }
try { Interceptor.attach(base.add(0x32ff10), {
onEnter: function(a){ this.m=a[0]; },
onLeave: function(r){ send({type:'nonce_next', m:'0x'+this.m.toString(16), ret:'0x'+r.toString(16)}); }
}); } catch(e){}
function hx_s(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
// 依次触发多个 uid
Java.perform(function(){
var uids = __UIDS__;
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
for (var i=0;i<uids.length;i++){
var uid=uids[i];
send({type:'stage', msg:'call uid='+uid});
var q=inst.getQUrlData(uid, "", "");
send({type:'qurl_done', uid:uid, len:q?q.length:0, data:q||''});
}
send({type:'stage', msg:'ALL DONE'});
}catch(e){ n+=1; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,120)}); setTimeout(go,5000); }
}
setTimeout(go, 16000);
});
""".replace('__UIDS__', json.dumps(uids))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(build_js(UIDS))
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'gbt_in':
print(f"[gbt] uid={ascii(p['uid']['v'])} k1={p['k1']['v'][:40]}({p['k1']['len']}) k2={p['k2']['v'][:24]}")
elif t == 'xxtea_in':
print(f"[xxtea] data={p['data']['v'][:40]} key_ascii={p['key_ascii'][:60]}")
elif t == 'xxtea_out':
print(f"[xxtea.out] {p['out']['v'][:44]}")
elif t == 'nonce_next':
print(f"[nonce] m={p['m']} ret={p['ret']}")
elif t == 'qurl_done':
print(f"[qurl uid={p['uid']}] len={p['len']}")
elif t == 'stage':
print('[stage]', p['msg'])
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 120
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'stage' and e.get('msg') == 'ALL DONE' for e in events):
time.sleep(3)
break
Path('/tmp/opencode/k1_multi.json').write_text(json.dumps(events))
print('saved /tmp/opencode/k1_multi.json')
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-135
View File
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
"""追踪 k1(this+0x10)写入者: hook std::string::operator=(0x3e6464) + getInstance(0x281270)。"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true; });
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
var cfgAddr = null;
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>8192) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
// getInstance @0x281270 -> 单例
try {
Interceptor.attach(base.add(0x281270), {
onLeave: function(r){
cfgAddr = r;
var s = rdStr(r.add(0x10));
send({type:'cfg', addr:String(r), k1v:s.v, k1len:s.len});
}
});
} catch(e){ send({type:'hookerr', where:'getInstance', msg:''+e}); }
// operator= @0x3e6464: a[0]=目标, a[1]=源
try {
Interceptor.attach(base.add(0x3e6464), {
onEnter: function(a){
if (!cfgAddr) return;
if (a[0].equals(cfgAddr.add(0x10))) {
var v = rdStr(a[1]);
send({type:'write_k1', val:v.v, len:v.len,
bt:Thread.backtrace(this.context, Backtracer.ACCURATE).map(function(x){
return DebugSymbol.fromAddress(x).toString().slice(0,90);}).slice(0,5)});
}
}
});
} catch(e){ send({type:'hookerr', where:'op=', msg:''+e}); }
setTimeout(function(){
if (cfgAddr) {
var s = rdStr(cfgAddr.add(0x10));
send({type:'final_k1', k1:s.v, len:s.len});
} else {
send({type:'final_k1', k1:'NO_CFG'});
}
}, 90000);
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:240]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'cfg':
print(f"[cfg] addr={p['addr']} k1len={p['k1len']}")
elif t == 'write_k1':
print(f"[WRITE k1] len={p['len']} val={p['val'][:60]}")
for f in p['bt']:
print(" <", f)
elif t == 'final_k1':
print(f"[FINAL k1] len={p['len']} {p['k1'][:40]}")
elif t == 'hookerr':
print('[hookerr]', p)
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 150
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'final_k1' for e in events):
time.sleep(3)
break
Path('/tmp/opencode/k1_trace2.json').write_text(json.dumps(events))
print('saved /tmp/opencode/k1_trace2.json')
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-150
View File
@@ -1,150 +0,0 @@
#!/usr/bin/env python3
"""追踪 this+0x10(k1) 的写入者: hook std::string::operator=, 比对目标地址==BusinessCfg+0x10。
启动后等初始化完成, 直接读 BusinessCfg 实例的 +0x10 最终值, 并记录所有写到该地址的调用。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true; });
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>8192) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
// BusinessCfg::getInstance 返回单例 -> 拿 this 地址
var cfgAddr = null;
try {
Interceptor.attach(base.add(0x454310 - 0x454310 + 0x26a880), { // getInstance 附近, 用符号找
});
} catch(e){}
// 用 export 查找 getInstance 真实地址
var syms = Process.getModuleByName('libudbauthunify.so').enumerateSymbols();
var getInst = null;
syms.forEach(function(s){ if (s.name.indexOf('BusinessCfg11getInstance') >= 0) getInst = s.address; });
send({type:'info', getInst:String(getInst)});
if (getInst) {
Interceptor.attach(getInst, {
onLeave: function(r){
cfgAddr = r;
var s = rdStr(r.add(0x10));
send({type:'cfg', addr:String(r), k1: s});
}
});
}
// hook std::string::operator=(string const&) @0x452e40
try {
Interceptor.attach(base.add(0x452e40), {
onEnter: function(a){
if (!cfgAddr) return;
// a[0] = this(目标), a[1] = src
if (a[0].equals(cfgAddr.add(0x10))) {
var v = rdStr(a[1]);
send({type:'write_k1', val:v,
bt:Thread.backtrace(this.context, Backtracer.ACCURATE).map(function(x){
return DebugSymbol.fromAddress(x).toString().slice(0,80);}).slice(0,4)});
}
}
});
} catch(e){ send({type:'hookerr', where:'op=', msg:''+e}); }
// 直接读最终值
setTimeout(function(){
if (cfgAddr) {
var s = rdStr(cfgAddr.add(0x10));
send({type:'final_k1', k1:s});
}
}, 60000);
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:240]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'info':
print('[info] getInstance =', p['getInst'])
elif t == 'cfg':
print(f"[cfg] addr={p['addr']} k1={p['k1']['v'][:40]}({p['k1']['len']})")
elif t == 'write_k1':
print(f"[WRITE k1] val={p['val']['v'][:60]}")
for f in p['bt']:
print(" <", f)
elif t == 'final_k1':
print(f"[FINAL k1] {p['k1']['v'][:40]}({p['k1']['len']})")
elif t == 'hookerr':
print('[hookerr]', p)
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 130
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'final_k1' for e in events):
time.sleep(3)
break
Path('/tmp/opencode/k1_writer.json').write_text(json.dumps(events))
print('saved /tmp/opencode/k1_writer.json')
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""R36: hook libz deflate/inflate — 抓 XXTEA 输入明文 + 隐藏加密路径 backtrace."""
import json, subprocess, time
from pathlib import Path
import frida
REMOTE="127.0.0.1:31878"; ADB="5dd8c93f"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
HERE=Path(__file__).resolve().parent.parent
OUT=HERE/"evidence"/("keytrace_"+time.strftime("%H%M%S")+".json")
def clear_turing():
for d in ("app_turingdfp","app_turingfd"):
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"rm -rf /data/data/{PACKAGE}/{d}/*"],capture_output=True)
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"find /data/data/{PACKAGE} -name 'resinfo*' -delete"],capture_output=True)
def main():
clear_turing()
d=frida.get_device_manager().add_remote_device(REMOTE)
subprocess.run(["adb","-s",ADB,"shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.5)
pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True)
s=d.attach(pid)
from bypass_loader import load_bypass
load_bypass(s)
d.resume(pid)
result={}; got={"post":False,"out":False}
seen_fns=set()
def events_ok(p):
seen_fns.add(p.get('fn')); return True
def on(m,_):
if m.get("type")=="error":
txt=str(m)
if "ClassNotFoundException" in txt: return
print(f"[JS] {txt[:160]}",flush=True); return
p=m.get("payload") or {}
ev=p.get("event")
if ev=="post": got["post"]=True; print(f"[+] POST {p['num']}B",flush=True)
elif ev=="armed-hy": print("[*] hy chain armed",flush=True)
elif ev=="call":
if len(seen_fns)<99: pass
elif ev=="call" and events_ok(p): got["out"]=True
elif ev=="deflate-found": print(f"[*] deflate export in {p['mod']}",flush=True)
elif ev=="inflate-in": print(f"[+] inflate-in {p['len']}B via {p['mod']}",flush=True)
result.setdefault(ev or "misc",[]).append(p)
sc=s.create_script((HERE/"tools/frida/hook_keytrace.js").read_text())
sc.on("message",on); sc.load()
t0=time.time()
while time.time()-t0<150:
time.sleep(2)
if got["out"] and got["post"]: time.sleep(4); break
json.dump(result,open(OUT,"w"),indent=2)
print(f"[*] saved {OUT}",flush=True)
try: s.detach()
except Exception: pass
if __name__=="__main__": main()
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""R38b: RC4 key 诞生点追踪 (MD5 transform 消息积累)."""
import json, subprocess, time
from pathlib import Path
import frida
REMOTE="127.0.0.1:31878"; ADB="5dd8c93f"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
HERE=Path(__file__).resolve().parent.parent
OUT=HERE/"evidence"/("keytrace2_"+time.strftime("%H%M%S")+".json")
def clear_turing():
for d in ("app_turingdfp","app_turingfd"):
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"rm -rf /data/data/{PACKAGE}/{d}/*"],capture_output=True)
subprocess.run(["adb","-s",ADB,"shell","su","-c",f"find /data/data/{PACKAGE} -name 'resinfo*' -delete"],capture_output=True)
def main():
clear_turing()
d=frida.get_device_manager().add_remote_device(REMOTE)
subprocess.run(["adb","-s",ADB,"shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.5)
pid=d.spawn([PACKAGE]); print(f"[*] spawn pid={pid}",flush=True)
s=d.attach(pid)
from bypass_loader import load_bypass
load_bypass(s)
d.resume(pid)
result={}; state={"post":False,"key":False}
def on(m,_):
if m.get("type")=="error":
t=str(m)
if "ClassNotFoundException" not in t: print(f"[JS] {t[:150]}",flush=True)
return
p=m.get("payload") or {}
ev=p.get("event")
if ev=="post": state["post"]=True; print("[+] POST fired",flush=True)
elif ev=="key-site": state["key"]=True; print(f"[+] KEY ARRIVED: {p.get('key')} md5calls={p.get('md5calls')} states={p.get('nstates')}",flush=True)
elif ev=="hy-armed": print("[*] hy armed",flush=True)
elif ev=="java-md5-hooked": print("[*] java digest hooked",flush=True)
elif ev=="key-copies-done": print(f"[*] key copies: {p.get('n')}",flush=True)
result.setdefault(ev or "misc",[]).append(p)
sc=s.create_script((HERE/"tools/frida/hook_keytrace2.js").read_text())
sc.on("message",on); sc.load()
t0=time.time()
while time.time()-t0<220:
time.sleep(3)
if state["key"] and state["post"]: time.sleep(5); break
json.dump(result,open(OUT,"w"),indent=2)
print(f"[*] saved {OUT} post={state['post']} key={state['key']}",flush=True)
try: s.detach()
except Exception: pass
if __name__=="__main__": main()
-53
View File
@@ -1,53 +0,0 @@
#!/usr/bin/env python3
"""R39: 登录流程 md5 输入捕获 — 两阶段配方 (与 hook_final_capture.py 同款):
spawn挂起 -> 双bypass装载 -> resume -> 稳定11s -> patch_guard -> 业务钩子
"""
import json, time
from pathlib import Path
import frida
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
HERE=Path(__file__).resolve().parent.parent
OUT=HERE/"evidence"/("loginmd5_"+time.strftime("%H%M%S")+".json")
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
pid=d.spawn([PACKAGE])
print(f"[*] spawned {pid}",flush=True)
s=d.attach(pid)
from bypass_loader import load_bypass
load_bypass(s, patch_guard_delay_ms=3000)
d.resume(pid)
print("[*] resumed (bypass_all: A层立即 + B层3s patch_guard)",flush=True)
result={}; state={"udb":0}
def on(m,_):
if m.get("type")=="error":
t=str(m)
if "ClassNotFoundException" not in t: print(f"[JS] {t[:150]}",flush=True)
return
p=m.get("payload") or {}
ev=p.get("event")
if ev=="udb-md5":
state["udb"]+=1
print(f"[+] udb-md5 #{state['udb']} in1={str(p.get('in1'))[:100]} out={str(p.get('out'))[:80]}",flush=True)
elif ev=="udb-armed": print("[*] udb md5 armed",flush=True)
elif ev=="java-digest":
out=p.get('out','')
if out.startswith('865a') or out.startswith('') and False: pass
print(f"[J] digest {p.get('algo')} out={out[:16]} in={repr((p.get('in_parts') or ''))[:70]}",flush=True)
result.setdefault(ev or "misc",[]).append(p)
sc=s.create_script((HERE/"tools/frida/hook_login_md5.js").read_text())
sc.on("message",on); sc.load()
print("[*] 请在 App 中: 退出登录 -> 账号密码重新登录 (窗口 8 分钟)",flush=True)
t0=time.time()
while time.time()-t0<480:
time.sleep(5)
if state["udb"]>=12: time.sleep(6); break
json.dump(result,open(OUT,"w"),indent=2)
print(f"[*] saved {OUT} udb={state['udb']}",flush=True)
try: s.detach()
except Exception: pass
if __name__=="__main__": main()
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env python3
"""R35: 清 turing 状态(保留登录) -> spawn -> bypass -> hook_memshot 内存快照.
产出 evidence/memshot_<HHMMSS>.json:
- dfp wire 全量 + backtrace
- 线上密文在进程内存中的全部拷贝 + ±2KB 邻域
- Perseus 会话密钥 + 静态密钥出现位置 + 16 字符候选密钥串
- zlib 流候选 (78 xx) 供主机 inflate 验证
"""
from __future__ import annotations
import json, subprocess, time
from pathlib import Path
import frida
REMOTE = "127.0.0.1:31878"
ADB = "5dd8c93f"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
HERE = Path(__file__).resolve().parent.parent
OUT = HERE / "evidence" / ("memshot_" + time.strftime("%H%M%S") + ".json")
def clear_turing():
"""只清 turing 状态, 保留登录态 -> ~40s 内自动重注册上报."""
dirs = ["app_turingdfp", "app_turingfd"]
for d in dirs:
subprocess.run(["adb", "-s", ADB, "shell", "su", "-c",
f"rm -rf /data/data/{PACKAGE}/{d}/*"], capture_output=True)
# resinfo 清理 (配方 §11.55)
subprocess.run(["adb", "-s", ADB, "shell", "su", "-c",
f"find /data/data/{PACKAGE} -name 'resinfo*' -delete"], capture_output=True)
def main():
print("[*] clearing turing state (login kept)", flush=True)
clear_turing()
d = frida.get_device_manager().add_remote_device(REMOTE)
subprocess.run(["adb", "-s", ADB, "shell", "am", "force-stop", PACKAGE], capture_output=True)
time.sleep(1.5)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
s = d.attach(pid)
from bypass_loader import load_bypass
load_bypass(s)
d.resume(pid)
result: dict = {}
got = {"post": False, "done": False}
def on(m, _):
if m.get("type") == "error":
print(f"[JS] {str(m)[:200]}", flush=True)
return
p = m.get("payload") or {}
ev = p.get("event")
if ev in ("post", "scan-done"):
got["post" if ev == "post" else "done"] = True
if ev == "post":
print(f"[+] dfpReport POST {p['num']}B", flush=True)
elif ev == "ranges":
print(f"[*] scanning {p['n']} rw- ranges ({p['bytes']//1024//1024}MB)", flush=True)
elif ev == "cipher-copies":
print(f"[+] cipher copies in memory: {p['n']}", flush=True)
elif ev == "perseus":
print(f"[+] Perseus key: {p.get('key')}", flush=True)
elif ev == "key-cands":
print(f"[+] key candidates: {p['n']}", flush=True)
elif ev == "zcand":
pass
elif ev == "scan-done":
print(f"[*] scan complete (zlib cands={p['zcands']})", flush=True)
result.setdefault(ev or "misc", []).append(p)
sc = s.create_script((HERE / "tools/frida/hook_memshot.js").read_text())
sc.on("message", on)
sc.load()
t0 = time.time()
while time.time() - t0 < 120:
time.sleep(2)
if got["done"]:
break
if got["post"] and time.time() - t0 > 60:
break
json.dump(result, open(OUT, "w"), indent=2)
print(f"[*] saved {OUT}", flush=True)
try:
s.detach()
except Exception:
pass
if __name__ == "__main__":
main()
-253
View File
@@ -1,253 +0,0 @@
#!/usr/bin/env python3
"""nonce链路动态hook —— 与静态反汇编互相验证。
抓取 getQUrlData 触发证书生成时的完整运行时实参链:
nonce_next(m) -> 64位OTP计数
getServiceTime() -> 服务时间
hyudb_crypt_util::xxtea_encrypt(out, data, key) -> 20B rnd 的直接来源
enpack_header(h, key, out) -> cert 头 [0x0c][key_idx]
enpack_body(h, a,b,c,d, out) -> P1 明文组装
gen_biz_token(struct, uid, k1, k2, out) -> 总装
encode_aes(p1, key, out) -> AES加密
getOtpEx(uid, idx, a3, out) -> 入口
目的: 拿到 xxtea 的 data/key 原文与输出, 验证 20B rnd 是否 = f(serviceTime, nonce, uid, 设备常量),
从而判定"信封nonce能否脱离设备本地复算"
时序(不可变, 同 hook_cert_keycap): force-stop -> spawn挂起 -> 双bypass -> resume ->
10s -> patch_guard -> 装hook -> Java桥触发 getQUrlData。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UID = 1199666914671
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>8192) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function ascii(h){ if(!h) return ''; var s=''; for(var i=0;i<h.length;i+=2){ var c=parseInt(h.substr(i,2),16); s += (c>=32&&c<127)?String.fromCharCode(c):'.'; } return s; }
function bt(ctx){ return Thread.backtrace(ctx, Backtracer.ACCURATE).map(function(a){
return DebugSymbol.fromAddress(a).toString().slice(0,90); }).slice(0,5); }
// ---------- 1) nonce_next(m) ----------
try { Interceptor.attach(base.add(0x32ff10), {
onEnter: function(a){ this.m=a[0]; },
onLeave: function(r){ send({type:'nonce_next', m:'0x'+this.m.toString(16), ret:'0x'+r.toString(16)}); }
}); } catch(e){ send({type:'hookerr',where:'nonce_next',msg:''+e}); }
// ---------- 2) getServiceTime() ----------
try { Interceptor.attach(base.add(0x268070), {
onLeave: function(r){ send({type:'service_time', v:'0x'+r.toString(16), dec:r.toString()}); }
}); } catch(e){ send({type:'hookerr',where:'getServiceTime',msg:''+e}); }
// ---------- 3) hyudb_crypt_util::xxtea_encrypt(out&, data, key) ----------
try { Interceptor.attach(base.add(0x32e83c), {
onEnter: function(a){
this.out=a[0]; this.d=rdStr(a[1]); this.k=rdStr(a[2]);
send({type:'xxtea_in', data:this.d, key:this.k,
data_ascii:ascii(this.d.v), key_ascii:ascii(this.k.v)});
},
onLeave: function(r){ var o=rdStr(this.out); send({type:'xxtea_out', out:o, out_ascii:ascii(o.v)}); }
}); } catch(e){ send({type:'hookerr',where:'xxtea_encrypt',msg:''+e}); }
// ---------- 4) enpack_header(h, NEWKEY, out&) ----------
try { Interceptor.attach(base.add(0x32ff44), {
onEnter: function(a){ this.out=a[2]; this.h=a[0]; this.k=a[1]; },
onLeave: function(r){ var o=rdStr(this.out);
send({type:'enpack_header', h:'0x'+this.h.toInt32().toString(16), key:'0x'+this.k.toInt32().toString(16), out:o}); }
}); } catch(e){ send({type:'hookerr',where:'enpack_header',msg:''+e}); }
// ---------- 5) enpack_body(h, a,b,c,d, out&) ----------
try { Interceptor.attach(base.add(0x3300c8), {
onEnter: function(a){
this.out=a[5]; this.h=a[0];
this.a=rdStr(a[1]); this.b=rdStr(a[2]); this.c=rdStr(a[3]); this.d=rdStr(a[4]);
send({type:'enpack_body_in', h:'0x'+this.h.toInt32().toString(16),
a:this.a, b:this.b, c:this.c, d:this.d,
a_ascii:ascii(this.a.v).slice(0,80), c_ascii:ascii(this.c.v).slice(0,80)});
},
onLeave: function(r){ var o=rdStr(this.out); send({type:'enpack_body_out', out:o}); }
}); } catch(e){ send({type:'hookerr',where:'enpack_body',msg:''+e}); }
// ---------- 6) gen_biz_token(struct, uid, k1, k2, out&) ----------
try { Interceptor.attach(base.add(0x3307e4), {
onEnter: function(a){
this.out=a[4];
// BIZTOKEN struct 前0x60字节: [0]=type [1]=key_idx [2]=1 [8..]=str1 [0x20]=st [0x28]=nonce [0x30]=str2 [0x48]=str3
send({type:'gbt_in', struct:hx(a[0],0x60),
struct_ascii:ascii(hx(a[0],0x60)),
uid:rdStr(a[1]), k1:rdStr(a[2]), k2:rdStr(a[3]),
k1_ascii:ascii(rdStr(a[2]).v).slice(0,80), k2_ascii:ascii(rdStr(a[3]).v).slice(0,80)});
},
onLeave: function(r){ var o=rdStr(this.out); send({type:'gbt_out', out:o}); }
}); } catch(e){ send({type:'hookerr',where:'gen_biz_token',msg:''+e}); }
// ---------- 7) encode_aes(p1, key, out&) ----------
try { Interceptor.attach(base.add(0x330218), {
onEnter: function(a){ this.out=a[2]; this.p1=rdStr(a[0]); this.key=rdStr(a[1]); },
onLeave: function(r){ var o=rdStr(this.out);
send({type:'encode_aes', p1:this.p1, key:this.key, key_ascii:ascii(this.key.v).slice(0,60), out:o}); }
}); } catch(e){ send({type:'hookerr',where:'encode_aes',msg:''+e}); }
// ---------- 8) getOtpEx(uid, idx, a3, out&) ----------
try { Interceptor.attach(base.add(0x2681b4), {
onEnter: function(a){ this.out=a[4]; this.uid=a[1]; this.idx=a[2]; this.a3=rdStr(a[3]); },
onLeave: function(r){ var o=rdStr(this.out);
send({type:'getOtpEx', uid:'0x'+this.uid.toString(16), idx:this.idx.toInt32(), a3:this.a3, out:o}); }
}); } catch(e){ send({type:'hookerr',where:'getOtpEx',msg:''+e}); }
// ---------- 9) AESkeyMgr::getkey ----------
try { Interceptor.attach(base.add(0x454350), {
onEnter: function(a){ this.a0=a[0]; this.i1=a[1]; this.i2=a[2]; },
onLeave: function(r){
// getkey 返回 std::string (sret in x8), 但 onLeave x8 不可靠; 读 x0 若为短串
try { var s=rdStr(this.a0.add(0)); if(s.len>0 && s.len<64) send({type:'getkey', i1:this.i1.toInt32(), i2:this.i2.toInt32(), key:s}); }
catch(e){}
}
}); } catch(e){ send({type:'hookerr',where:'getkey',msg:''+e}); }
// ---------- 触发 ----------
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
send({type:'stage', msg:'call #1'});
var q1=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', idx:1, len:q1?q1.length:0, data:q1||''});
var q2=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', idx:2, len:q2?q2.length:0, data:q2||''});
}catch(e){ n+=1; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,120)}); setTimeout(go,5000); }
}
setTimeout(go, 18000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
if t != 'retry':
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'nonce_next':
print(f"[nonce_next] m={p['m']} ret={p['ret']}")
elif t == 'service_time':
print(f"[service_time] {p['v']} ({p['dec']})")
elif t == 'xxtea_in':
print(f"[xxtea.in] data len={p['data']['len']} {p['data']['v'][:40]}")
print(f" key len={p['key']['len']} {p['key']['v'][:60]} ascii={p['key_ascii'][:50]}")
elif t == 'xxtea_out':
print(f"[xxtea.out] len={p['out']['len']} {p['out']['v'][:48]}")
elif t == 'enpack_header':
print(f"[enpack_header] h={p['h']} key={p['key']} out len={p['out']['len']} {p['out']['v'][:20]}")
elif t == 'enpack_body_in':
print(f"[enpack_body] h={p['h']}")
print(f" a(len{p['a']['len']})={p['a']['v'][:60]} ascii={p['a_ascii']}")
print(f" b(len{p['b']['len']})={p['b']['v'][:48]}")
print(f" c(len{p['c']['len']})={p['c']['v'][:60]} ascii={p['c_ascii']}")
print(f" d(len{p['d']['len']})={p['d']['v'][:24]}")
elif t == 'enpack_body_out':
print(f"[enpack_body.out] len={p['out']['len']} {p['out']['v'][:80]}")
elif t == 'gbt_in':
print(f"[gen_biz_token.in] struct={p['struct'][:100]}")
print(f" uid(len{p['uid']['len']})={p['uid']['v'][:40]}")
print(f" k1(len{p['k1']['len']})={p['k1']['v'][:60]} ascii={p['k1_ascii']}")
print(f" k2(len{p['k2']['len']})={p['k2']['v'][:60]} ascii={p['k2_ascii']}")
elif t == 'gbt_out':
print(f"[gen_biz_token.out] len={p['out']['len']} {p['out']['v'][:60]}")
elif t == 'encode_aes':
print(f"[encode_aes] p1 len={p['p1']['len']} {p['p1']['v'][:70]}")
print(f" key={p['key_ascii'][:50]} out len={p['out']['len']}")
elif t == 'getOtpEx':
print(f"[getOtpEx] uid={p['uid']} idx={p['idx']} a3(len{p['a3']['len']}) out len={p['out']['len']}")
elif t == 'getkey':
print(f"[getkey] i1={p['i1']} i2={p['i2']} key={p['key']['v'][:40]} ascii={ascii_short(p['key']['v'])}")
elif t == 'qurl_done':
print(f"[qurl#{p['idx']}] len={p['len']} head={(p.get('data') or '')[:40]}")
elif t == 'stage':
print('[stage]', p['msg'])
def ascii_short(h):
s = ''
for i in range(0, len(h), 2):
c = int(h[i:i+2], 16)
s += chr(c) if 32 <= c < 127 else '.'
return s[:40]
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 120
while time.time() < deadline:
time.sleep(2)
if sum(1 for e in events if e.get('type') == 'qurl_done') >= 2:
time.sleep(5)
break
Path('/tmp/opencode/nonce_chain.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-208
View File
@@ -1,208 +0,0 @@
#!/usr/bin/env python3
"""native侧追查nonce生成: 枚举libudbauthunify.so符号 + 窗口内hook sha/hmac/rand。
时序(不可变): force-stop -> spawn挂起 -> 双bypass -> resume -> 10s -> patch_guard
-> 枚举符号 -> hook命中函数 -> Java桥触发getQUrlData -> 收集调用与nonce比对。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UID = 1199666914671
ENUM_JS = r"""
'use strict';
var m = Process.getModuleByName('libudbauthunify.so');
send({type:'info', base:String(m.base), size:m.size});
var pat = /sha1|sha-1|hmac|rand|urandom|sign|digest|nonce|md5|aes_key/i;
var syms = m.enumerateSymbols().filter(function(s){
return s.name && pat.test(s.name);
}).map(function(s){ return {name:s.name, addr:String(s.address)}; });
var exps = m.enumerateExports().filter(function(e){ return pat.test(e.name); })
.map(function(e){ return {name:e.name, addr:String(e.address)}; });
send({type:'syms', syms:syms, exps:exps});
"""
HOOK_JS_TPL = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
var WIN_ON = false;
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>4096) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function bt(ctx){ return Thread.backtrace(ctx, Backtracer.ACCURATE).map(function(a){
return DebugSymbol.fromAddress(a).toString().slice(0,110); }).slice(0,6); }
// __HITS__: [ [addrStr, name], ... ]
var hits = __HITS__;
hits.forEach(function(h){
try {
Interceptor.attach(ptr(h[0]), {
onEnter: function(a){
if (!WIN_ON) return;
this.a = [a[0],a[1],a[2],a[3]];
this.bt = bt(this.context);
},
onLeave: function(r){
if (!WIN_ON) return;
send({type:'hit', name:h[1].slice(0,90),
args:[rdStr(this.a[0]), rdStr(this.a[1])],
ret:rdStr(r), bt:this.bt});
}
});
send({type:'hooked', name:h[1].slice(0,80)});
} catch(e){ send({type:'hookerr', where:h[1], msg:''+e}); }
});
// 窗口控制 + 触发
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
WIN_ON = true;
send({type:'stage', msg:'window ON'});
var q=inst.getQUrlData(__UID__, "", "");
WIN_ON = false;
send({type:'qurl_done', len:q?q.length:0, data:q||''});
}catch(e){ n+=1; window.on=false; if(n%4===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 6000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
# 1) 枚举符号
sc0 = s.create_script(ENUM_JS)
out = {}
def on0(m, _):
if m.get('type') == 'send':
p = m['payload']
if p.get('type') in ('info', 'syms'):
out[p['type']] = p
sc0.on('message', on0)
sc0.load()
time.sleep(3)
info = out['info']
syms = out['syms']['syms'] + out['syms']['exps']
print(f"base={info['base']} size={info['size']} 命中符号={len(syms)}")
for x in syms[:30]:
print(" ", x['name'][:100])
# 2) 选 hook 目标: 定向 SHA1/md5/random_device, 上限12个
def want(nm):
ln = nm.lower()
return ('sha_go' in ln or 'shainit' in ln or 'adddatalen' in ln or
'random_device' in ln or 'md5_char' in ln or
'huyamd5' in ln and ('digest' in ln or 'tostring' in ln))
picks = []
seen = set()
for x in syms:
nm = x['name']
off = int(x['addr'], 16) - int(info['base'], 16)
if off <= 0 or off > info['size']:
continue
if want(nm) and nm not in seen:
seen.add(nm)
picks.append([x['addr'], nm])
if len(picks) >= 12:
break
print("hook目标:", [p[1][:60] for p in picks])
# 3) 装钩 + 触发
js = HOOK_JS_TPL.replace('__HITS__', json.dumps(picks))
sc = s.create_script(js)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'hooked':
print('[hooked]', p['name'][:70])
elif t == 'hookerr':
print('[hookerr]', p)
elif t == 'hit':
a0, a1 = p['args'][0], p['args'][1]
print(f"\n[HIT] {p['name'][:80]}")
print(f" arg0={a0['t']}:{a0['len']} {a0['v'][:64]}")
print(f" arg1={a1['t']}:{a1['len']} {a1['v'][:64]}")
print(f" ret={p['ret']['t']}:{p['ret']['len']} {p['ret']['v'][:64]}")
for f in p['bt'][:4]:
print(" <", f)
elif t == 'stage':
print('[stage]', p['msg'])
elif t == 'qurl_done':
print('[qurl_done]', p.get('len'), (p.get('data') or '')[:80])
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 90
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'qurl_done' for e in events):
time.sleep(5)
break
Path('/tmp/opencode/nonce_native.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-131
View File
@@ -1,131 +0,0 @@
#!/usr/bin/env python3
"""追查 wupData 信封内 20B nonce 的生成源头。
时序(不可变, 同 hook_cert_keycap): force-stop -> spawn挂起 -> 双bypass ->
resume -> 10s -> patch_guard -> 随机源hooks + Java桥触发 getQUrlData。
命中判定: 窗口内随机源输出与P1中nonce比对 / 直接看SecureRandom调用栈。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UID = 1199666914671
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}});
return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
// ---- Java 随机源 ----
Java.perform(function(){
try {
var SR = Java.use('java.security.SecureRandom');
SR.nextBytes.overload('[B').implementation = function(b){
var st = Java.use('android.util.Log')
.getStackTraceString(Java.use('java.lang.Exception').$new());
var r = this.nextBytes(b);
var hex = Array.from(new Uint8Array(b)).map(function(x){
return ('0'+x.toString(16)).slice(-2);}).join('').slice(0,48);
send({type:'jsec', n:b.length, head:hex, stack:st});
return r;
};
} catch(e){ send({type:'hookerr', where:'SecureRandom', msg:''+e}); }
});
// ---- 触发证书生成(getQUrlData 组 P1 含 nonce) ----
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
var q=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', len:q?q.length:0, data:q||''});
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 8000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:240]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
if t != 'retry':
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]', p['base'])
elif t == 'jsec':
lines = [x.strip() for x in p['stack'].split('\n')]
frames = [x for x in lines if 'huya' in x.lower() or 'udb' in x.lower()
or 'nonce' in x.lower()][:5]
print(f"[SecureRandom] {p['n']}B head={p['head']}")
for f in (frames or lines[1:4]):
print(" ", f[:110])
elif t == 'hookerr':
print('[hookerr]', p)
elif t == 'qurl_done':
print('[qurl_done]', p.get('len'), 'head:', (p.get('data') or '')[:60])
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 120
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'qurl_done' for e in events):
time.sleep(8)
break
Path('/tmp/opencode/nonce_trace.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/env python3
"""nonce溯源v2: 修正random_device返回值读取 + libc熵源 + 同进程双调用对比。"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
UID = 1199666914671
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}}); return true;
});
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed', base:String(base)});
var WIN_ON = false;
// ---- random_device::operator() 返回uint32 ----
try {
var sym = null;
Process.getModuleByName('libudbauthunify.so').enumerateSymbols().forEach(function(s){
if (s.name === '_ZNSt6__ndk113random_deviceclEv') sym = s.address;
});
if (sym) Interceptor.attach(sym, {
onLeave: function(r){
if (!WIN_ON) return;
try { send({type:'rd32', v:'0x'+r.toInt32().toString(16), bt:DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,60)}); } catch(e){}
}
});
} catch(e){ send({type:'hookerr', where:'rd_op', msg:''+e}); }
// ---- libc 熵源 ----
['getrandom','getentropy','arc4random_buf'].forEach(function(fn){
var p = Module.findExportByName('libc.so', fn);
if (!p) return;
Interceptor.attach(p, {
onEnter: function(a){ this.buf=a[0]||a[1]; this.n=a[1] ? a[1].toInt32() : a[0].toInt32(); },
onLeave: function(_){
if (!WIN_ON || this.n<=0 || this.n>64) return;
try {
var b = Memory.readByteArray(this.buf, Math.min(this.n,32));
send({type:'libc', fn:fn, n:this.n,
hex:Array.from(new Uint8Array(b)).map(function(x){return ('0'+x.toString(16)).slice(-2)}).join('')});
} catch(e){}
}
});
});
var rd = Module.findExportByName('libc.so','read');
Interceptor.attach(rd, {onEnter:function(a){ this.fd=a[0].toInt32(); this.buf=a[1]; this.n=a[2].toInt32(); },
onLeave:function(_){ if (!WIN_ON) return; }});
// ---- 双调用触发 ----
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
WIN_ON = true;
send({type:'stage', msg:'call #1'});
var q1=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', idx:1, data:q1||''});
var q2=inst.getQUrlData(__UID__, "", "");
WIN_ON = false;
send({type:'qurl_done', idx:2, data:q2||''});
}catch(e){ n+=1; WIN_ON=false; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,90)}); setTimeout(go,5000); }
}
setTimeout(go, 20000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
s = d.attach(pid)
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()
d.resume(pid)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'rd32':
print(f"[rd32] {p['v']} <- {p['bt'][:50]}")
elif t == 'libc':
print(f"[{p['fn']}] n={p['n']} {p['hex'][:40]}")
elif t == 'stage':
print('[stage]', p['msg'])
elif t == 'qurl_done':
print(f"[qurl#{p.get('idx')}] len={p.get('len')} head={(p.get('data') or '')[:40]}")
elif t == 'retry':
print('[retry]', p.get('n'))
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 100
while time.time() < deadline:
time.sleep(2)
if sum(1 for e in events if e.get('type') == 'qurl_done') >= 2:
time.sleep(5)
break
Path('/tmp/opencode/nonce_v2.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-78
View File
@@ -1,78 +0,0 @@
"""用 frida attach 到运行中 App, hook __system_property_get,
记录虎牙派生读取的系统属性, 并测试替换某些属性后 hdid 是否变化.
"""
import frida, time, subprocess, sys, json, re, ssl, socket
PACKAGE="com.duowan.kiwi"
REMOTE="127.0.0.1:31878"
JS = r"""
'use strict';
send({type:'armed'});
function rcsv(p){try{return p.readCString(256)||'';}catch(e){return '';}}
var interest = ['ro.serialno','ro.product.model','ro.product.device',
'ro.boot.serialno','ro.hardware','ro.build.fingerprint','ro.build.id',
'ro.product.board','ro.product.manufacturer','ro.ril.miui.imei0',
'persist.sys.imei','gsm.imei','ro.boot.image','ro.boot.product',
'ro.kernel.qemu','init.svc.adbd','ro.product.cpu.abi','ro.build.version.sdk',
'ro.build.version.release','ro.product.brand','ro.product.name'];
function hook(){
var m = Process.findModuleByName('libc.so');
// dl_ prefixed variants first
var syms = ['__system_property_get','free'];
var target = Module.findExportByName('libc.so','__system_property_get');
if(!target){send({type:'info',k:'no-propget'});return;}
send({type:'info',k:'found',a:target.toString()});
Interceptor.attach(target,{
onEnter:function(a){
this.name = rcsv(a[0]);
},
onLeave:function(ret){
var ov = rcsv(this.ctx.x1); // __system_property_get 第二参数 value buffer (x1)
var sig = this.name+'='+ov;
// 只记录感兴趣的; 若想起记则全记
if(interest.indexOf(this.name)>=0 || 1){
send({type:'prop',name:this.name,val:ov.slice(0,80)});
}
// 可选替换
// if(this.name=='ro.serialno'){ this.ctx.x1.writeUtf8String('99999999999'); }
}
});
}
hook();
"""
def get_pid(d):
for _ in range(10):
r=subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE],capture_output=True,text=True)
if r.stdout.strip(): return int(r.stdout.strip().split()[0])
time.sleep(1)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"],capture_output=True)
# attach 前等 app 起来但不触发派生(login 触发)
time.sleep(6)
d=frida.get_device_manager().add_remote_device(REMOTE)
pid=get_pid(d)
print("attaching pid",pid,flush=True)
if not pid:
print("no pid"); return
s=d.attach(pid)
props=[]
def on(m,dd):
if m.get('type')!='send':return
p=m.get('payload') or {}
if p.get('type')=='prop':
props.append((p['name'],p['val']))
print(f" {p['name']} = {p['val']}",flush=True)
sc=s.create_script(JS); sc.on('message',on); sc.load()
print("[*] hooked, 触发登录/活动 观察派生属性 20s...",flush=True)
# 触发动作让 app 读取属性
time.sleep(20)
print(f"[*] 共捕获 {len(props)} 次属性读取",flush=True)
try: s.detach()
except: pass
if __name__=="__main__": main()
-176
View File
@@ -1,176 +0,0 @@
#!/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()
-181
View File
@@ -1,181 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
Process.setExceptionHandler(function(d){ send({type:'segv',info:{t:d.type}}); return true; });
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){ try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>1048576) return {len:l,v:''};
return {len:l,v:l?hx(p.add(1),Math.min(l,8192)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>2097152) return {len:len,v:''};
return {len:len,v:hx(p.add(16).readPointer(),Math.min(len,8192))};
}catch(e){ return {len:-1,v:String(e)}; } }
function rdC(p){ try{ var t=p.readCString(); if(t===null||t.length>30000) return {len:-1,v:''};
var bin=false; for(var i=0;i<t.length;i++){var c=t.charCodeAt(i); if(c<9||(c>13&&c<32)){bin=true;break;}}
return {len:t.length, bin:bin, v: bin? hx(p,Math.min(t.length,512)) : t.slice(0,4000)}; }catch(e){ return {len:-1,v:String(e)}; } }
[0x24ab68, 0x24ab7c].forEach(function(off){
Interceptor.attach(base.add(off), {
onEnter: function(a){
var c1=rdC(a[2]), c2=rdC(a[3]);
if(c1.len<=0 && c2.len<=0) return;
send({type:'sendmsg', off:off, a1:String(a[1]), s1:c1, s2:c2});
}
});
});
// 收包处理: MsgLoop::doMSG(UdbMsgBase*, char*)
Interceptor.attach(base.add(0x245240), {
onEnter: function(a){
var mb=a[1];
var head=hx(mb,64);
send({type:'domsg', base_head:head, p:rdStr(a[2])});
}
});
""
'use strict';
Process.setExceptionHandler(function(d){ send({type:'segv',info:{t:d.type,a:String(d.address)}}); return true; });
var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>262144) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?hx(p.add(1),Math.min(l,4096)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>1048576) return {t:'l?',len:len,v:''};
return {t:'l',len:len,v:hx(p.add(16).readPointer(),Math.min(len,4096))};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
function ra(ctx){ try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,70);}catch(e){return '?';} }
// 登录态JSON的AES加密(含cred字段) —— 已知走 UdbAESUtil::encrypt
Interceptor.attach(base.add(0x250038), {
onEnter: function(a){ this.x1=rdStr(a[1]); this.x2=rdStr(a[2]); },
onLeave: function(){
if(this.x1.len>800||this.x2.len>800)
send({type:'bigaes', x1:{len:this.x1.len}, x2:this.x2, ra:ra(this)});
}
});
// saveLoginData 全量版: bean dump 768B + backtrace 10帧
Interceptor.attach(base.add(0x265fb0), {
onEnter: function(a){
send({type:'saveLD_str', str:rdStr(a[1]), bean_head:hx(a[2],768),
bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,10)
.map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,64);})});
}
});
Interceptor.attach(base.add(0x265a6c), {
onEnter: function(a){
send({type:'saveLD_bean', bean_head:hx(a[1],768), flag:a[2].toInt32(),
bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,10)
.map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,64);})});
}
});
// cred 打包/解包(网络侧明文必经)
Interceptor.attach(base.add(0x333170), {
onEnter: function(a){ send({type:'cred_unpack', in:rdStr(a[1]), ra:ra(this)}); }
});
// HandlerRequestLoginCred / HandlerResponseLoginCred vtable 探测:
// vtable+0x10 起为虚函数槽, 逐个attach前3个槽, 打印触发与backtrace首帧
function probeVtable(name, vtAddr){
for(var slot=0; slot<4; slot++){
(function(slot){
try{
var fp = vtAddr.add(0x10 + slot*8).readPointer();
if(fp.compare(base) < 0 || fp.compare(base.add(0x480000)) > 0) return;
Interceptor.attach(fp, {
onEnter: function(a){
send({type:'vtable', name:name, slot:slot,
a0:a[0].toString(), a1:rdStr(a[1]), a2:rdStr(a[2]),
bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,3)
.map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,60);})});
}
});
send({type:'vt_attached', name:name, slot:slot, fp:String(fp.sub(base))});
}catch(e){}
})(slot);
}
}
probeVtable('ReqLoginCred', base.add(0x477f80));
probeVtable('RespLoginCred', base.add(0x478018));
probeVtable('GetCred', base.add(0x477000)); // 占位: 若无效仅跳过
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type') == 'saveLD_bean' for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/cred_issue_trace.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-93
View File
@@ -1,93 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
var hooked=0;
var fns=[];
try{ fns=DebugSymbol.findFunctionsNamed('SSL_write'); }catch(e){}
if(!fns.length){ try{ var r=new ApiResolver('module'); fns=r.enumerateMatchesSync('exports:*!SSL_write').map(function(x){return x.address;}); }catch(e2){} }
fns.slice(0,6).forEach(function(p,i){
Interceptor.attach(p,{ onEnter:function(a){
var len=a[2].toInt32(); if(len<40||len>20000) return;
var head=''; try{ head=a[1].readCString(Math.min(len,400)); }catch(e){ return; }
if(head.indexOf('wupudbrequest')<0 && head.indexOf('huyaudbwebui')<0) return;
var arr=new Uint8Array(a[1].readByteArray(len));
var hex=''; for(var j=0;j<arr.length;j++) hex+=('0'+arr[j].toString(16)).slice(-2);
send({type:'wire', idx:i, len:len, head:head.slice(0,300), hex:hex});
}});
hooked++;
send({type:'hooked', idx:i, at:String(p)});
});
if(hooked===0) send({type:'nowrite'});"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type') == 'saveLD_bean' for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/sslwrite.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdStr(p){ try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; return {len:l,v:l?hx(p.add(1),Math.min(l,4096)):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {len:len,v:''};
return {len:len,v:hx(p.add(16).readPointer(),Math.min(len,8192))};
}catch(e){ return {len:-1,v:''}; } }
function rdC(p){ try{ var t=p.readCString(); return t&&t.length<8000?t.slice(0,6000):''; }catch(e){ return ''; } }
// writeFileEx(name, content, out&) 两个重载
[0x252254,0x2523a0].forEach(function(off){
Interceptor.attach(base.add(off), {
onEnter: function(a){
var nm=rdC(a[1]);
var c=rdStr(a[3]);
send({type:'wfex', off:off, name:nm.slice(0,60),
len:c.len, v:c.v});
}
});
});
"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
Path('/Users/yml/codes/douyu_login_py/evidence/wfextrace.json').write_text(json.dumps(events))
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type')=='bean' for e in events) and any(e.get('type')=='getcred' for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/wfextrace.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。
用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。
捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。
"""
from __future__ import annotations
import json
import subprocess
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""var base = Process.getModuleByName('libudbauthunify.so').base;
send({type:'armed'});
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
function rdC(p){ try{ var t=p.readCString(); return t&&t.length<20000?t.slice(0,3000):''; }catch(e){ return ''; } }
Interceptor.attach(base.add(0x274ea4), {
onEnter: function(a){
var pl=rdC(a[1]);
send({type:'wupbuild', id:String(a[2]), payload:pl, payload_head:hx(a[1],96)});
}
});"""
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
s = d.attach(pid)
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()
d.resume(pid)
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('armed. >>> 请在手机上退出登录并重新登录 <<<')
sc = s.create_script(JS)
events = []
def on_msg(m, _):
if m.get('type') == 'error':
print('JS ERR:', str(m)[:200]); return
if m.get('type') != 'send':
return
p = m.get('payload') or {}
t = p.get('type')
events.append(p)
Path('/Users/yml/codes/douyu_login_py/evidence/wupbuild.json').write_text(json.dumps(events))
if t == 'vt_attached':
print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}")
elif t == 'saveLD_str':
print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'saveLD_bean':
print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}")
elif t == 'cred_unpack':
print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}")
elif t == 'bigaes':
print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}")
elif t == 'vtable':
print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}")
sc.on('message', on_msg)
sc.load()
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
if any(e.get('type') in ('java_cred','wupbuild') for e in events):
time.sleep(10)
break
Path('/Users/yml/codes/douyu_login_py/evidence/wupbuild.json').write_text(json.dumps(events))
from collections import Counter
print('saved:', Counter(e.get('type') for e in events))
if __name__ == '__main__':
for attempt in range(4):
try:
main()
break
except frida.InvalidOperationError as e:
print('detached:', e)
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
-155
View File
@@ -1,155 +0,0 @@
#!/usr/bin/env python3
"""hook xxtea_crypt_util + hyudbxxt::xxtea_*: 抓 dfpReport 加密的 (in, key, out)。
xxtea_crypt_util @ libudbauthunify.so offset 3336252 (0x32e9bc附近)。
调用者 UdbUserFilterUtils::xxTeaAndBase64 处理 WUP编码体 = 与 dfpReport 同构。
修复 string 参数解析(SSO), onLeave 读输出。命中 len>2000 即持久化。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_xxtea_crypt.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hexb(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return null; }
}
// 解析 __ndk1 basic_string (SSO): 字节0低bit: 1=内联(sso), 0=堆指针
function readString(p){
try{
var b0 = p.readU8();
var sso = (b0 & 1) === 1;
if (sso) {
var ln = b0 >> 1;
var data = p.add(1);
return {len: ln, data: data};
} else {
var ln = p.add(8).readPointer().toInt32();
var data = p.add(16).readPointer();
return {len: ln, data: data};
}
}catch(e){ return null; }
}
function hookOne(name, off, maxLen){
var mod = Process.findModuleByName('libudbauthunify.so');
if (!mod) return false;
var addr = mod.base.add(off);
try{
Interceptor.attach(addr, {
onEnter: function(args){
this.a = args;
// 尝试解析 (ret, in, key) / (in, key) / (this, in, key) 多种
this.in0 = readString(args[1]); // 常见: (this, in, key)
this.key0 = readString(args[2]);
if (!this.in0 && readString(args[0])) { this.in0 = readString(args[0]); this.key0 = readString(args[1]); }
if (!this.in0) { this.in0 = readString(args[2]); this.key0 = readString(args[3]); }
this.t0 = Date.now();
},
onLeave: function(ret){
if (!this.in0) return;
var ln = this.in0.len;
if (ln > 5000 || ln < 100) return; // dfpReport 明文 ~3980B, 只抓大输入
var inhex = hexb(this.in0.data, Math.min(ln, 250));
if (!inhex) return;
// 过滤: 找 TAF/JSON 特征
var low = inhex;
var isWup = low.indexOf('dfpReport') >= 0 ||
low.indexOf('74726571') >= 0 || // 'tReq'
low.indexOf('68757961') >= 0 || // 'huya'
low.indexOf('61707049') >= 0 || // 'appI'
low.indexOf('7b226170') >= 0; // '{"ap'
if (!isWup && ln < 2900) return;
var keyhex = this.key0 ? hexb(this.key0.data, Math.min(this.key0.len, 32)) : null;
send({type:'xxtea', fn:name, off:off.toString(16),
inlen:ln, in:inhex, keylen: this.key0 ? this.key0.len : -1, key:keyhex,
ret:String(ret)});
}
});
return true;
}catch(e){ return false; }
}
var T = setTimeout(function(){
var n1 = hookOne('xxtea_crypt_util', 3336252, 99999);
// hyudbxxt 家族
var n2 = hookOne('hyudb_xxtea_enc1', 0x25261c, 99999);
var n3 = hookOne('hyudb_xxtea_dec1', 0x252708, 99999);
var n4 = hookOne('hyudb_xxtea_enc2', 0x2528e0, 99999);
send({type:'hooked', n:[n1,n2,n3,n4].filter(Boolean).length});
if (!n1 && !n2 && !n3 && !n4) setTimeout(arguments.callee, 1000);
}, 800);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "hooked":
print(f"[*] hooked {p.get('n')} 个 xxtea", flush=True)
elif t == "xxtea":
print(f"[XXTEA] {p.get('fn')}@{p.get('off')} inlen={p.get('inlen')} "
f"keylen={p.get('keylen')}", flush=True)
if p.get('key'):
print(f" key: {p.get('key')}", flush=True)
inh = p.get('in', '')
print(f" in[:100]: {inh[:100]}", flush=True)
# 尝试 ascii
try:
bb = bytes.fromhex(inh[:200])
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in bb)
print(f" in ascii: {asc[:120]}", flush=True)
except Exception: pass
events.append(p)
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, hooking xxtea. 请 退出登录->重新登录, 命中即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/env python3
"""从 dfpReport wire 恢复明文 JSON + 三元组(hdid/deviceId/appkey)。
原理: 明文 = '{"appId":"5008"...' 的已知前缀 XOR keystream 恢复 ks 前段;
再用"完整JSON结构模板"+ 已知 deviceName/systemVer(每设备不同但可从 DeviceInfo 或 build 推算)
重建 JSON, 校验 ks 前缀一致性, 提取各 unique 字段.
已知: 所有帧的 JSON 结构一致 (字段顺序固定), 仅 4 个 unique 值不同:
appkey(?), channel, deviceId(40hex), hdid(40hex), deviceName(变长), systemVer(变长)
"""
from __future__ import annotations
import json, binascii, re
MAGIC = bytes.fromhex('571882cf664bb39401ee')
# 已知明文 JSON 前缀 (所有设备相同)
PREFIX = b'{"appId":"5008","appVer":"13.4.22"'
def extract_body(wire):
bs = wire.find(b'\r\n\r\n') + 4
body = wire[bs:]
mi = body.find(MAGIC)
return bytes(body[mi+10:])
def recover_plaintext(cw, device_model='M2102J2SC', sysver_maxlen=40):
"""用 PREFIX 恢复 ks 前段, 再用 JSON 模板重建明文. 返回 (plaintext_bytes, json_str)"""
ks = bytes(cw[i]^PREFIX[i] for i in range(len(PREFIX)))
# 模板: 全部字段, unique 值用已知结构 + 已知固定值
# 由于 hdid/deviceId/appkey 值未知但都是 hex, deviceName 已知,
# 我们分两段恢复:
# 段1: 从 prefix 到 "appkey":"... 之前(结构固定), 逐字节可读 ASCII 推进, appkey值=as hex guess
# 暂时只做前缀验证 + 结构重建
return ks
def try_template(cw, json_str):
Jb=json_str.encode();
if len(Jb)>len(cw): return None
ok=all(cw[i]^Jb[i]== (cw[i]^PREFIX[i] if False else cw[i]^Jb[i]) for i in range(0))
# 校验核心: 用已知前缀比较 ks
ks_t=cw[:len(PREFIX)]
ks_t=bytes(cw[i]^Jb[i] for i in range(len(PREFIX)))
# 与 PREFIX 派生的 ks 应相等(如果Jb在prefix处正确)
match=all((cw[i]^Jb[i])==(cw[i]^PREFIX[i]) for i in range(len(PREFIX)))
return match
def process_file(path):
d=json.load(open(path))
wire=binascii.unhexlify(d.get('dfp_wire',d.get('dfp','')))
cw=extract_body(wire)
return cw
if __name__=='__main__':
import glob
print("对已知帧应用恢复(验证JSON结构):")
frames = glob.glob('evidence/frame_*.json')+glob.glob('evidence/identity_*.json')
for f in sorted(frames):
d=json.load(open(f))
w=binascii.unhexlify(d.get('dfp_wire',''))
if not w: continue
cw=extract_body(w)
print(f" {f}: cw_len={len(cw)}", end='')
# 校验 prefix 匹配
ok=all(cw[i]== (w[0]^w[0]) for i in range(0))
if len(cw)>=len(PREFIX):
# 若 cw[0:len]== cw[j:j+len] 无意义; 校验PREFIX是否在明文位置: ks=cw^PREFIX 应任意
print(" prefix_ok")
else:
print()
-187
View File
@@ -1,187 +0,0 @@
#!/usr/bin/env python3
"""改写明文JSON字段实验: 验证服务端回显本地明文。
明文JSON {hdid: 7c5387..., appkey: 865a...cbb0e3, deviceId: ...} 在堆中。
等长替换 hdid/appkey/deviceId -> 新值, 触发登录, 看 dfpReport 响应 t5 是否变化。
同时 hook SSL_write 抓密文 + SSL_read 抓响应。
"""
from __future__ import annotations
import json
import random
import string
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_plain_patch.json")
NEW_HDID = "".join(random.choice("0123456789abcdef") for _ in range(40))
NEW_APPKEY = "".join(random.choice("0123456789abcdef") for _ in range(32))
NEW_DEVID = "".join(random.choice("0123456789abcdef") for _ in range(40))
print(f"[*] 新 hdid = {NEW_HDID}")
print(f"[*] 新 appkey = {NEW_APPKEY}")
print(f"[*] 新 deviceId= {NEW_DEVID}")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var NEW_HDID = '%s'; // 40hex
var NEW_APPKEY = '%s'; // 32hex
var NEW_DEVID = '%s'; // 40hex
function hexb(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return null; }
}
// 找明文 JSON {"appId":"5008" 并等长替换字段
function patchJson(){
try{
Process.enumerateRanges('r--').forEach(function(rng){
if (rng.size > 1024*1024*256) return;
try{
var hits = Memory.scanSync(rng.base, rng.size, '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38');
hits.slice(0, 6).forEach(function(x){
var p = x.address;
var s = '';
try{ s = p.readUtf8String(1200); }catch(e){ return; }
if (s.indexOf('"hdid"') < 0) return;
send({type:'found_json', addr:String(p), len:s.length});
// 等长替换 hdid / appkey / deviceId
try{
Memory.protect(p, s.length + 16, 'rwx');
var out = s;
out = out.replace(/"hdid":"[0-9a-f]{40}"/, '"hdid":"' + NEW_HDID + '"');
out = out.replace(/"appkey":"[0-9a-f]{32}"/, '"appkey":"' + NEW_APPKEY + '"');
out = out.replace(/"deviceId":"[0-9a-f]{40}"/, '"deviceId":"' + NEW_DEVID + '"');
if (out !== s) {
p.writeUtf8String(out);
var after = p.readUtf8String(120);
send({type:'patched_json', addr:String(p), after:after.slice(0, 180)});
}
}catch(e){ send({type:'patch_err', addr:String(p), e:String(e)}); }
});
}catch(e){}
});
}catch(e){}
}
// SSL_write 抓 dfpReport 密文
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 < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
var hex = hexb(a[1], len);
send({type:'wire', len:len, hex:hex});
}
});
});
}catch(e){}
// SSL_read 抓响应 (找 t5 / t1)
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.len = a[2].toInt32();
},
onLeave: function(ret){
var n = ret.toInt32();
if (n <= 0 || n > 65536) return;
try{
var hex = hexb(this.buf, n);
send({type:'sslread', len:n, hex:hex});
}catch(e){}
}
});
});
}catch(e){}
// 每 5s patch 一次 (登录时 JSON 重新生成)
setInterval(function(){ try{ patchJson(); }catch(e){} }, 5000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "found_json":
print(f"[json] @{p.get('addr')} len={p.get('len')}", flush=True)
events.append({"found": p.get('addr'), "new_hdid": NEW_HDID,
"new_appkey": NEW_APPKEY, "new_devid": NEW_DEVID})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
elif t == "patched_json":
print(f"[PATCH-OK] @{p.get('addr')}: {p.get('after')!r}", flush=True)
elif t == "patch_err":
print(f"[patch-err] @{p.get('addr')}: {p.get('e')}", flush=True)
elif t == "wire":
body = bytes.fromhex(p.get('hex'))
try: hp = body.index(b'\r\n\r\n') + 4
except ValueError: hp = 0
b = body[hp:]
j = b.find(b'\x57\x18\x82\xcf\x66\x4b\xb3\x94')
print(f"[wire] len={p.get('len')} 魔数@{j}", flush=True)
if j >= 0:
print(f" 密文头: {b[j+10:j+30].hex(' ')}", flush=True)
events.append({"wire": p.get('hex')})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
elif t == "sslread":
h = p.get('hex', '')
low = h.lower()
# dfpReport 响应里找 40hex t5
if len(h) > 200 and ('7c5387' in low or 't1' in low or 't5' in low or len(h) < 4000):
print(f"[read] len={p.get('len')} hex[:120]: {h[:120]}", flush=True)
events.append({"read": h})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print(f"[*] 已 resume. 将把 hdid={NEW_HDID} appkey={NEW_APPKEY} deviceId={NEW_DEVID} 写入明文JSON", flush=True)
print("[*] 请 退出登录->重新登录, 观察响应 t5 是否为新 hdid", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-156
View File
@@ -1,156 +0,0 @@
#!/usr/bin/env python3
"""改写 libhydeviceid.so .data 中的 t1 指纹, 验证 dfpReport 响应变化。
t1 = 865a4924a40897ac1fcfe6b4c2cbb045 (3处静态内置, datadiv 解密)。
改为随机 32hex 后重新登录, 看 dfpReport 响应 t1 是否变化。
同时 hook SSL_write 抓新请求 body 对比魔数后密文是否变化。
"""
from __future__ import annotations
import json
import random
import string
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_t1_patch.json")
# 新 t1 (随机 32hex)
NEW_T1 = "".join(random.choice("0123456789abcdef") for _ in range(32))
print(f"[*] 新 t1 = {NEW_T1}")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var NEW_T1 = '%s';
var T1_OFFS = [0x3bb190, 0x3bb230, 0x3e0e50];
// 等待: so 加载后延时 800ms, 确保 datadiv 就地解密已完成
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var m = null;
try{ m = Process.getModuleByName('libhydeviceid.so'); }catch(e){}
if (m) {
clearInterval(iv);
setTimeout(function(){
patchAll(m);
}, 800);
} else if (tries > 100) { clearInterval(iv); send({type:'no_mod'}); }
}, 50);
function patchAll(m){
// 先验证当前明文, 若仍是密文说明 datadiv 未跑完, 再等
try{
var cur = m.base.add(0x3bb190).readUtf8String(32);
if (cur.indexOf('865a') !== 0 && cur.indexOf('c1') !== 0) {
// 还没解密好, 再等
setTimeout(function(){ patchAll(m); }, 400);
return;
}
}catch(e){}
T1_OFFS.forEach(function(off){
try{
var p = m.base.add(off);
var cur = p.readUtf8String(32);
send({type:'backup', off:off.toString(16), cur:cur});
Memory.protect(p, 64, 'rwx');
p.writeUtf8String(NEW_T1);
var after = p.readUtf8String(32);
send({type:'patched', off:off.toString(16), after:after, ok: after === NEW_T1});
}catch(e){ send({type:'patch_err', off:off.toString(16), e:String(e)}); }
});
send({type:'patch_done'});
}
// hook SSL_write 抓 dfpReport 请求
try{
var fns = [];
var r = new ApiResolver('module');
fns = r.enumerateMatchesSync('exports:*!SSL_write').map(function(x){ return x.address; });
fns.slice(0, 6).forEach(function(p){
Interceptor.attach(p, {
onEnter: function(a){
var len = a[2].toInt32();
if (len < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
var arr = new Uint8Array(a[1].readByteArray(len));
var hex = '';
for (var j = 0; j < arr.length; j++) hex += ('0'+arr[j].toString(16)).slice(-2);
send({type:'wire', len:len, hex:hex});
}
});
});
}catch(e){ send({type:'ssl_err', e:String(e)}); }
""" % NEW_T1
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "backup":
print(f"[备份] 0x{p.get('off')}: {p.get('cur')}", flush=True)
elif t == "patched":
print(f"[改写] 0x{p.get('off')}: -> {p.get('after')} ok={p.get('ok')}", flush=True)
elif t == "patch_done":
print(f"[*] 改写完成 {p.get('patched')}", flush=True)
events.append({"patched": True, "new_t1": NEW_T1})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
elif t == "wire":
body = bytes.fromhex(p.get('hex'))
try: hp = body.index(b'\r\n\r\n') + 4
except ValueError: hp = 0
b = body[hp:]
j = b.find(b'\x57\x18\x82\xcf\x66\x4b\xb3\x94')
print(f"[wire] len={p.get('len')} 魔数@{j}", flush=True)
if j >= 0:
print(f" 密文头: {b[j+10:j+30].hex(' ')}", flush=True)
rec = {"type": "wire", "len": p.get('len'), "hex": p.get('hex')}
events.append(rec)
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print(f"[*] 已 resume, t1 已改为 {NEW_T1}。请在手机 退出登录->重新登录", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-100
View File
@@ -1,100 +0,0 @@
#!/usr/bin/env python3
"""真机 dump 解密后的 libhydeviceid.so (JNI_OnLoad 壳解密完成后).
用真机稳定注入通道 spawn+art_callsite, 等 libhydeviceid.so 加载且壳解密
(JNI_OnLoad 返回)后 Memory.dump 整个模块, 保存为本地 .so 供反汇编分析.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_dump_hydev.py [serial] [remote]
输出:
evidence/diag_phone/libhydeviceid_dump.so
"""
from __future__ import annotations
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "libhydeviceid_dump.so"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
var dumped = false;
function tryDump(){
if(dumped) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
dumped = true;
var ranges = md.enumerateRanges('r--');
send({type:'plan', count:ranges.length, modBase:''+md.base});
ranges.forEach(function(r, i){
try{
var buf = Memory.readByteArray(r.base, r.size);
send({type:'seg', i:i, n:ranges.length, off:''+r.base.sub(md.base), size:r.size}, buf);
}catch(e){ send({type:'seg-err', i:i, off:''+r.base.sub(md.base), e:String(e)}); }
});
send({type:'done', count:ranges.length});
}
setInterval(tryDump, 500);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
session.on("detached", lambda r, dd: print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"got": 0, "expected": 0}
fh = OUT.open("wb")
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t == "plan":
result["expected"] = p["count"]
print(f"[*] 计划 dump {p['count']} 个 r-x 段", flush=True)
elif t == "seg":
off = int(p["off"], 16)
fh.seek(off)
fh.write(data)
result["got"] += 1
print(f"[*] seg {p['i']+1}/{p['n']} off=0x{p['off']} size={p['size']}", flush=True)
elif t == "done":
print(f"[*] dump done, got {result['got']}/{result['expected']}", flush=True)
elif t == "seg-err":
print(f"[seg-err] {p}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, waiting dump...", flush=True)
t0 = time.time()
while (result["got"] < result["expected"] or result["expected"] == 0) and time.time() - t0 < 20:
time.sleep(0.5)
fh.close()
OUT.chmod(0o644)
print(f"[*] done got={result['got']}/{result['expected']} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-101
View File
@@ -1,101 +0,0 @@
#!/usr/bin/env python3
"""真机 FULL dump libhydeviceid.so (r-x + r-- + rw- 全部段, 含解密后的 .data/.got).
上次 phone_dump_hydev 只抓 r-- (代码+rodata), .data/.got/.bss 全零 —— unidbg 需要
完整解密镜像(密钥/GOT 都在 .data). 本脚本抓到全部可读内存.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_dump_hydev_full.py [serial] [remote]
输出: evidence/diag_phone/libhydeviceid_dump_full.so
"""
from __future__ import annotations
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "libhydeviceid_dump_full.so"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
var dumped = false;
function tryDump(){
if(dumped) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
dumped = true;
var ranges = md.enumerateRanges('r--').concat(md.enumerateRanges('rw-'));
send({type:'plan', count:ranges.length, modBase:''+md.base});
ranges.forEach(function(r, i){
try{
var buf = Memory.readByteArray(r.base, r.size);
send({type:'seg', i:i, n:ranges.length, off:''+r.base.sub(md.base), size:r.size, prot:r.protection}, buf);
}catch(e){ send({type:'seg-err', i:i, off:''+r.base.sub(md.base), e:String(e)}); }
});
send({type:'done', count:ranges.length});
}
setInterval(tryDump, 500);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
session.on("detached", lambda r, dd: print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"got": 0, "expected": 0, "base": 0}
fh = OUT.open("wb")
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t == "plan":
result["expected"] = p["count"]
result["base"] = int(p["modBase"], 16)
print(f"[*] 计划 dump {p['count']} 段, module base={p['modBase']}", flush=True)
elif t == "seg":
off = int(p["off"], 16)
fh.seek(off)
fh.write(data)
result["got"] += 1
print(f"[*] seg {p['i']+1}/{p['n']} off=0x{p['off']} size={p['size']} prot={p.get('prot')}", flush=True)
elif t == "done":
print(f"[*] dump done {result['got']}/{result['expected']}", flush=True)
elif t == "seg-err":
print(f"[seg-err] {p}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
t0 = time.time()
while (result["got"] < result["expected"] or result["expected"] == 0) and time.time() - t0 < 25:
time.sleep(0.5)
fh.close()
OUT.chmod(0o644)
print(f"[*] done got={result['got']}/{result['expected']} base=0x{result['base']:x} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/env python3
"""真机: 确认 getGUID 生成时是否 ioctl/openat 读底层硬件(而非属性/文件).
hook libc ioctl + openat + __system_property_get, getGUID(0x20f8c8) onEnter/onLeave 设窗口,
窗口内记录调用的 ioctl(fd/request) / openat(path) / 属性读取。决定 32hex 硬件输入来源。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_getguid_ioctl.py [serial] [remote]
输出:
evidence/diag_phone/getguid_ioctl.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "getguid_ioctl.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
GETGUID_OFF = 0x20F8C8
JS = r"""
'use strict';
var inGuid=false, hooked=false, iosHooked=false;
function hookIo(){
if(iosHooked) return;
try{
var ioctl = Module.findExportByName('libc.so','ioctl');
var openat = Module.findExportByName('libc.so','openat');
var open = Module.findExportByName('libc.so','open');
if(!ioctl && !openat && !open) return;
iosHooked=true;
if(ioctl) Interceptor.attach(ioctl,{onEnter:function(a){ if(inGuid){ try{ send({type:'ioctl', fd:a[0].toInt32(), req:a[1].toInt32()}); }catch(e){} } }});
if(openat) Interceptor.attach(openat,{onEnter:function(a){ if(inGuid){ try{ var p=a[1].readCString()||'?'; send({type:'openat', p:p}); }catch(e){} } }});
if(open) Interceptor.attach(open,{onEnter:function(a){ if(inGuid){ try{ send({type:'open', p:a[0].readCString()||'?'}); }catch(e){} } }});
send({type:'io-hooked'});
}catch(e){ send({type:'io-err', e:String(e)}); }
}
function hookGetGUID(){
if(hooked) return;
var md=Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hooked=true;
Interceptor.attach(md.base.add(0x20f8c8),{
onEnter:function(){ inGuid=true; send({type:'g-enter'}); },
onLeave:function(){ inGuid=false; send({type:'g-leave'}); }
});
send({type:'guid-hooked'});
}
setInterval(hookGetGUID,200);
setInterval(hookIo,200);
setTimeout(function(){
Java.perform(function(){
try{
var NE=Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'cls-ok'});
send({type:'call', v:NE['getGUID']()});
}catch(e){ send({type:'java-err', e:String(e).slice(0,120)}); }
});
}, 8000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"ioctl": [], "openat": [], "props": [], "detached": det}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t == "ioctl":
result["ioctl"].append(p); print(f"[ioctl] fd={p.get('fd')} req=0x{p.get('req'):x}", flush=True)
elif t == "openat":
result["openat"].append(p); print(f"[openat] {p.get('p')}", flush=True)
elif t == "open":
result["openat"].append(p); print(f"[open] {p.get('p')}", flush=True)
elif t in ("g-enter","g-leave","guid-hooked","io-hooked","cls-ok","call"):
print(f"[*] {t}", p if t=="call" else "", flush=True)
elif t in ("io-err","java-err"):
print(f"[!] {t}: {p.get('e')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(9)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] ioctl={len(result['ioctl'])} openat={len(result['openat'])} saved {OUT}")
if __name__ == "__main__":
main()
-292
View File
@@ -1,292 +0,0 @@
#!/usr/bin/env python3
"""真机 E2-v4: 身份输入全捕获 — svc 位点 + libc FILE* + Java 侧候选 API.
背景: GUID 与 serialno/ANDROID_ID/ro.product.*/IMEI属性/ro.boot.cpuid/全清数据 均无关,
属性层完全不可触碰 -> 种子来自 (a)内联 svc 文件读(SoC serial 等) (b)JNI->Java
(TelephonyManager/NetworkInterface/蓝牙) (c)TEE. libc 钩子看不见 (a)/(b).
本脚本三层齐抓:
1. svc 位点 (dump 定位的 6 个 svc #0): x8=系统调用号 + x0-x3 参数 + (read->内容)
2. libc fopen64/fopen/fread/fgets (兜底 FILE* 路径)
3. Java 侧: TelephonyManager.getImei/getDeviceId/getSubscriberId/SimSerial、NetworkInterface
getHardwareAddress/getNetworkInterfaces、BluetoothAdapter.getAddress/getName、
Settings.Secure/Global.getString —— 带参数与返回值
窗口: init/get* 全部标注 fn; 6s 后触发一次 Java 重算(捕获 Java 侧).
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_identity_inputs.py [serial] [remote]
输出: evidence/diag_phone/identity_inputs.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "identity_inputs.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
WINDOW_OFFS = {
"init": 0x20E6E4, "getGUID": 0x20F8C8, "getCDID": 0x20EB08,
"getSDID": 0x20EDE8, "getHDID": 0x20F0C8, "getMID": 0x20FBA8,
}
SVC_SITES = [0x30BEF4, 0x30BF04, 0x30BF14, 0x30BF24, 0x30BF34, 0x30BF58]
SYSCALLS = {
29: "ioctl", 48: "faccessat", 56: "openat", 57: "close", 63: "read", 64: "write",
65: "readv", 66: "writev", 67: "pread64", 79: "fstat", 61: "lseek",
97: "futex", 99: "set_robust_list", 113: "clock_gettime", 178: "gettid",
222: "mmap", 226: "mprotect", 278: "getrandom", 291: "statx", 263: "faccessat2",
}
JS_TEMPLATE = r"""
'use strict';
var LIB = 'libhydeviceid.so';
var WINDOW = __WINDOW_JSON__;
var SITES = __SITES_JSON__;
var SYSCALL = __SYSCALL_JSON__;
var libBase = null, libSize = 0;
var inLibWindow = 0;
var hooked = false;
function inWindow(){ return inLibWindow > 0; }
function inLibCall(inv){
try{
var ra = inv.returnAddress;
if (!ra || ra.isNull()) return false;
return (ra.compare(libBase) >= 0 && ra.compare(libBase.add(libSize)) < 0);
}catch(e){ return false; }
}
function snap(inv){
var c = inv.context;
return { x0: ptr(c.x0), x1: ptr(c.x1), x2: ptr(c.x2), x3: ptr(c.x3), x4: ptr(c.x4), x5: ptr(c.x5), x8: ptr(c.x8) };
}
function hookAll(){
if(hooked) return;
var md = Process.findModuleByName(LIB);
if(!md){ return; }
libBase = md.base; libSize = md.size;
hooked = true;
Object.keys(WINDOW).forEach(function(fn){
try{
Interceptor.attach(libBase.add(parseInt(WINDOW[fn], 16)), {
onEnter: function(){ inLibWindow++; send({type:'w-open', fn:fn, tid:Process.getCurrentThreadId()}); },
onLeave: function(){ inLibWindow--; send({type:'w-close', fn:fn}); }
});
}catch(e){ send({type:'w-err', fn:fn, e:String(e)}); }
});
send({type:'windows-hooked'});
function addHook(name, t, onEnter, onLeave){
try{
if(!t) return;
Interceptor.attach(t, {
onEnter: function(){
if(!inWindow()) return;
if(!inLibCall(this)) return;
var s = snap(this);
this._valid = true;
try{ onEnter(this, s); }catch(e){ send({type:'hook-err', name:name, e:String(e).slice(0,120)}); }
},
onLeave: function(){
if(!this._valid) return;
try{ onLeave(this); }catch(e){ send({type:'hook-err', name:name, e:String(e).slice(0,120)}); }
}
});
}catch(e){ send({type:'attach-err', name:name, e:String(e).slice(0,120)}); }
}
// ---- 1) svc 位点: 全 syscall census (x8 在 onEnter 需读寄存器, 此处特化)
SITES.forEach(function(site){
try{
Interceptor.attach(libBase.add(site), {
onEnter: function(){
if(!inWindow()) return;
var s = snap(this);
this._nr = s.x8.toInt32();
this._s = s;
this._valid = true;
var name = SYSCALL[this._nr]||('nr'+this._nr);
var extra = '';
if ((name==='faccessat'||name==='openat'||name==='faccessat2') ) {
try{ extra = s.x1.readCString()||'?'; }catch(e){ extra='?'; }
}
send({type:'svc', site:site, nr:this._nr, name:name, p:extra,
a0:s.x0.toString(), a1:s.x1.toString(), a2:s.x2.toString(), a3:s.x3.toString(),
tid:Process.getCurrentThreadId()});
},
onLeave: function(){
if(!this._valid) return;
var name = SYSCALL[this._nr]||('nr'+this._nr);
var ret = 0;
try{ ret = this.context.x0.toInt32(); }catch(e){}
if ((name==='read'||name==='pread64') && ret>0){
var cnt = Math.min(ret,512);
var hex='';
try{ hex = this._s.x1.readByteArray(cnt) ? Array.from(new Uint8Array(this._s.x1.readByteArray(cnt))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('') : ''; }catch(e){}
send({type:'svc-read', site:site, nr:this._nr, name:name, n:ret, hex:hex, tid:Process.getCurrentThreadId()});
} else if (name==='openat' && ret>=0){
var p='';
try{ p = this._s.x1.readCString()||'?'; }catch(e){ p='?'; }
send({type:'svc-open', site:site, nr:this._nr, name:name, p:p, fd:ret, tid:Process.getCurrentThreadId()});
} else if (name==='ioctl'){
send({type:'svc-ioctl', site:site, fd:this._s.x0.toInt32(), req:this._s.x1.toInt32(), ret:ret, tid:Process.getCurrentThreadId()});
}
}
});
}catch(e){ send({type:'attach-err', name:'svc@'+site.toString(16), e:String(e)}); }
});
send({type:'svc-hooked'});
// ---- 2) libc FILE* 兜底
addHook('libc:fopen64', Module.findExportByName('libc.so','fopen64'),
function(th,s){ th._p = s.x0.readCString()||'?'; },
function(th){ send({type:'fopen', p:th._p, ret:th.returnValue.toString(), tid:Process.getCurrentThreadId()}); });
addHook('libc:fopen', Module.findExportByName('libc.so','fopen'),
function(th,s){ th._p = s.x0.readCString()||'?'; },
function(th){ send({type:'fopen', p:th._p, ret:th.returnValue.toString(), tid:Process.getCurrentThreadId()}); });
addHook('libc:fread', Module.findExportByName('libc.so','fread'),
function(th,s){ th._buf=s.x0; th._len=s.x2.toInt32(); },
function(th){ var n=th.returnValue.toInt32(); var hex='';
if(n>0){ try{ hex = th._buf.readByteArray(Math.min(n*1,512)) ? Array.from(new Uint8Array(th._buf.readByteArray(Math.min(n*1,512)))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('') : ''; }catch(e){} }
send({type:'fread-c', n:n, hex:hex, tid:Process.getCurrentThreadId()}); });
addHook('libc:__system_property_get', Module.findExportByName('libc.so','__system_property_get'),
function(th,s){ th._key=s.x0.readCString()||''; th._vp=s.x1; },
function(th){ var v=''; try{ v=th._vp.readCString()||''; }catch(e){}
send({type:'prop', k:th._key, v:v, tid:Process.getCurrentThreadId()}); });
send({type:'libc-hooked'});
}
setInterval(function(){ try{ hookAll(); }catch(e){} }, 150);
// ---- 3) Java 侧候选 API (启动后装)
function hookJava(){
try{
Java.perform(function(){
function wrap(cls, m, showArgs){
try{
Java.use(cls)[m].overloads.forEach(function(ov){
ov.implementation = function(){
var r;
try{ r = ov.apply(this, arguments); }catch(e){ throw e; }
var args = [];
try{ if(showArgs){ for(var i=0;i<arguments.length;i++){ var a=arguments[i]; args.push((a!==null&&a!==undefined)?String(a):''); } } }catch(e){}
send({type:'java', cls:cls, m:m, r:(r===null||r===undefined)?'null':String(r), args:args, tid:Process.getCurrentThreadId()});
return r;
};
});
}catch(e){ send({type:'java-err', cls:cls, m:m, e:String(e).slice(0,120)}); }
}
wrap('android.telephony.TelephonyManager','getImei',false);
wrap('android.telephony.TelephonyManager','getDeviceId',false);
wrap('android.telephony.TelephonyManager','getSubscriberId',false);
wrap('android.telephony.TelephonyManager','getSimSerialNumber',false);
wrap('android.telephony.TelephonyManager','getNetworkOperator',false);
wrap('java.net.NetworkInterface','getHardwareAddress',false);
wrap('java.net.NetworkInterface','getNetworkInterfaces',false);
wrap('android.bluetooth.BluetoothAdapter','getAddress',false);
wrap('android.bluetooth.BluetoothAdapter','getName',false);
wrap('android.provider.Settings$Secure','getString',true);
wrap('android.provider.Settings$Global','getString',true);
wrap('android.os.Build','getSerial',false);
send({type:'java-hooked'});
});
}catch(e){ send({type:'java-setup-err', e:String(e).slice(0,150)}); }
}
setTimeout(hookJava, 4500);
setTimeout(function(){
Java.perform(function(){
try{
var NE = Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'cls-ok'});
NE.init();
var out = {};
['getGUID','getCDID','getHDID','getMID','getSDID'].forEach(function(m){
try{ out[m] = NE[m]()||''; send({type:'val', m:m, v:out[m]}); }catch(e){ out[m]='ERR'; }
});
send({type:'all-vals', out:out});
}catch(e){ send({type:'java-err', e:String(e).slice(0,150)}); }
});
}, 6000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"events": [], "values": {}, "detached": det}
printed = [0]
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:180], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t in ("svc","svc-read","svc-open","svc-ioctl","fopen","fread-c","prop","java"):
result["events"].append(p)
if printed[0] < 800:
printed[0] += 1
if t=="svc" and p.get("p"): print(f"[svc] {p.get('name')} {p.get('p')}", flush=True)
elif t=="svc": print(f"[svc] {p.get('name')} a0={p.get('a0')} a1={p.get('a1')} a2={p.get('a2')}", flush=True)
elif t=="svc-read": print(f"[svc-read] {p.get('name')} n={p.get('n')} hex={p.get('hex','')[:70]}", flush=True)
elif t=="svc-open": print(f"[svc-open] {p.get('p')} fd={p.get('fd')}", flush=True)
elif t=="svc-ioctl": print(f"[svc-ioctl] fd={p.get('fd')} req={p.get('req')} ret={p.get('ret')}", flush=True)
elif t=="fopen": print(f"[fopen] {p.get('p')} ret={p.get('ret')}", flush=True)
elif t=="fread-c": print(f"[fread] n={p.get('n')} hex={p.get('hex','')[:70]}", flush=True)
elif t=="prop": print(f"[prop] {p.get('k')}={p.get('v')}", flush=True)
elif t=="java": print(f"[java] {p.get('cls')}.{p.get('m')}({p.get('args','')}) -> {p.get('r')}", flush=True)
elif t == "val":
result["values"][p["m"]] = p["v"]
print(f"[*] {p['m']} = {p['v']}", flush=True)
elif t == "all-vals":
result["values"].update(p["out"])
elif t in ("w-open","w-close","windows-hooked","svc-hooked","libc-hooked","java-hooked","cls-ok"):
print(f"[*] {t}", flush=True)
elif "err" in t:
print(f"[!] {t}: {p.get('name','')} {p.get('e')}", flush=True)
JS = (JS_TEMPLATE
.replace("__WINDOW_JSON__", json.dumps({k: hex(v) for k, v in WINDOW_OFFS.items()}))
.replace("__SITES_JSON__", json.dumps([hex(s) for s in SVC_SITES]))
.replace("__SYSCALL_JSON__", json.dumps(SYSCALLS)))
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed; 等待自然 init + 4.5s Java 钩子 + 6s 触发重算", flush=True)
time.sleep(12)
print("[*] 采集结束", flush=True)
time.sleep(2)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT} (events={len(result['events'])})", flush=True)
if __name__ == "__main__":
main()
-300
View File
@@ -1,300 +0,0 @@
#!/usr/bin/env python3
"""真机 E2 v2: init()/get* 库内定向输入图 — 带参数值快照的完整输入清单.
v2 关键修正:
- lib 文件/ioctl 读走自身 hy_syscall(内联 svc), libc 钩子看不见
(E2 v1 实测: 窗口内只有 futex/gettid 类系统调用). 直挂:
hy_syscall @0x30bf3c (nr=a1..a3 -> 解码 openat/read/ioctl/close...)
hyfopen64 @0xf1298 (path)
hy_read @0x30bef0 (fp/buf/len, 试探性记录)
- 窗口从"单线程"改为"全局计数"(init 可能跨线程); 归属过滤用
returnAddress 是否落在 lib 区间(精确, 不用 FUZZY backtrace)
- 参数全部从 this.context.xN 寄存器快照取(跨 frida 版本稳定)
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_init_inputmap.py [serial] [remote]
输出:
evidence/diag_phone/init_inputmap.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "init_inputmap.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
WINDOW_OFFS = {
"init": 0x20E6E4,
"getGUID": 0x20F8C8,
"getCDID": 0x20EB08,
"getSDID": 0x20EDE8,
"getHDID": 0x20F0C8,
"getMID": 0x20FBA8,
}
HY_SYSCALL_OFF = 0x30BF3C
HY_FOPEN64_OFF = 0xF1298
HY_READ_OFF = 0x30BEF0
# aarch64 关键系统调用号 (linux asm-generic)
SYSCALLS = {
29: "ioctl", 56: "openat", 57: "close", 63: "read", 64: "write",
65: "readv", 67: "pread64", 78: "readlinkat", 79: "fstat", 61: "lseek",
97: "futex", 98: "futex2", 99: "set_robust_list", 113: "clock_gettime",
169: "gettimeofday", 171: "adjtimex", 174: "rt_sigaction", 178: "gettid",
222: "mmap", 226: "mprotect", 278: "getrandom", 291: "statx",
263: "faccessat2", 258: "newfstatat",
}
JS_TEMPLATE = r"""
'use strict';
var LIB = 'libhydeviceid.so';
var WINDOW = __WINDOW_JSON__;
var libBase = null, libSize = 0;
var inLibWindow = 0;
var hooked = false;
var SYSCALL = __SYSCALL_JSON__;
function inWindow(){ return inLibWindow > 0; }
function inLibCall(inv){
try{
var ra = inv.returnAddress;
if (!ra || ra.isNull()) return false;
return (ra.compare(libBase) >= 0 && ra.compare(libBase.add(libSize)) < 0);
}catch(e){ return false; }
}
function snap(inv){
var c = inv.context;
return { x0: ptr(c.x0), x1: ptr(c.x1), x2: ptr(c.x2), x3: ptr(c.x3) };
}
function hookAll(){
if(hooked) return;
var md = Process.findModuleByName(LIB);
if(!md){ return; }
libBase = md.base; libSize = md.size;
hooked = true;
Object.keys(WINDOW).forEach(function(fn){
try{
Interceptor.attach(libBase.add(parseInt(WINDOW[fn], 16)), {
onEnter: function(){ inLibWindow++; send({type:'w-open', fn:fn, tid:Process.getCurrentThreadId()}); },
onLeave: function(){ inLibWindow--; send({type:'w-close', fn:fn}); }
});
}catch(e){ send({type:'w-err', fn:fn, e:String(e)}); }
});
send({type:'windows-hooked'});
function addHook(name, t, onEnter, onLeave){
try{
if(!t) return;
Interceptor.attach(t, {
onEnter: function(){
if(!inWindow()) return;
if(!inLibCall(this)) return;
var s = snap(this);
this._valid = true;
try{ onEnter(this, s); }catch(e){ send({type:'hook-err', name:name, e:String(e).slice(0,120)}); }
},
onLeave: function(){
if(!this._valid) return;
try{ onLeave(this); }catch(e){ send({type:'hook-err', name:name, e:String(e).slice(0,120)}); }
}
});
}catch(e){ send({type:'attach-err', name:name, e:String(e).slice(0,120)}); }
}
// ---- libc 层 (兜底: 仍可能走 libc 的路径)
addHook('libc:__system_property_get', Module.findExportByName('libc.so', '__system_property_get'),
function(th, s){
th._key = s.x0.readCString() || '';
th._vp = s.x1;
}, function(th){
var v = '';
try{ v = th._vp.readCString() || ''; }catch(e){}
send({type:'prop', tid:Process.getCurrentThreadId(), k:th._key, v:v});
});
addHook('libc:openat', Module.findExportByName('libc.so', 'openat'),
function(th, s){ th._p = s.x1.readCString() || '?'; },
function(th){ send({type:'open', tid:Process.getCurrentThreadId(), p:th._p, fd:th.returnValue.toInt32()}); });
addHook('libc:open', Module.findExportByName('libc.so', 'open'),
function(th, s){ th._p = s.x0.readCString() || '?'; },
function(th){ send({type:'open', tid:Process.getCurrentThreadId(), p:th._p, fd:th.returnValue.toInt32()}); });
['read','pread','pread64'].forEach(function(n){
addHook('libc:'+n, Module.findExportByName('libc.so', n),
function(th, s){
th._fd = s.x0.toInt32(); th._buf = s.x1; th._len = s.x2.toInt32();
}, function(th){
var n = th.returnValue.toInt32();
if(n <= 0) return;
var cnt = Math.min(n, 512);
var hex = '';
try{ hex = th._buf.readByteArray(cnt) ? Array.from(new Uint8Array(th._buf.readByteArray(cnt))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('') : ''; }catch(e){}
send({type:'read', tid:Process.getCurrentThreadId(), fd:th._fd, n:n, hex:hex});
});
});
addHook('libc:ioctl', Module.findExportByName('libc.so', 'ioctl'),
function(th, s){ th._fd = s.x0.toInt32(); th._req = s.x1.toInt32(); },
function(th){ send({type:'ioctl', tid:Process.getCurrentThreadId(), fd:th._fd, req:th._req}); });
addHook('libc:syscall', Module.findExportByName('libc.so', 'syscall'),
function(th, s){ th._nr = s.x0.toInt32(); th._a1 = s.x1.toInt32(); th._a2 = s.x2.toInt32(); },
function(th){ send({type:'sys', tid:Process.getCurrentThreadId(), nr:th._nr, a1:th._a1, a2:th._a2}); });
// ---- lib 自身壳 (主路径)
addHook('hy_syscall', libBase.add(HY_SYSCALL_OFF),
function(th, s){
th._nr = s.x0.toInt32(); th._a1 = s.x1; th._a2 = s.x2; th._a3 = s.x3;
var name = SYSCALL[th._nr] || ('nr'+th._nr);
send({type:'hysys', tid:Process.getCurrentThreadId(), nr:th._nr, name:name,
a1:th._a1.toString(), a2:th._a2.toString(), a3:th._a3.toString()});
}, function(th){
var name = SYSCALL[th._nr] || ('nr'+th._nr);
if (name === 'read' || name === 'pread64') {
var n = th.returnValue.toInt32();
if (n > 0) {
var cnt = Math.min(n, 512);
var hex = '';
try{ hex = th._a2.readByteArray(cnt) ? Array.from(new Uint8Array(th._a2.readByteArray(cnt))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('') : ''; }catch(e){}
send({type:'hysys-read', tid:Process.getCurrentThreadId(), nr:th._nr, name:name, n:n, hex:hex});
}
} else if (name === 'openat') {
try{ var p = th._a1.readCString() || '?'; send({type:'hysys-open', tid:Process.getCurrentThreadId(), p:p, fd:th.returnValue.toInt32()}); }catch(e){}
}
});
addHook('hyfopen64', libBase.add(HY_FOPEN64_OFF),
function(th, s){ try{ th._p = s.x0.readCString() || '?'; }catch(e){ th._p = '?'; } },
function(th){ send({type:'hyfopen', tid:Process.getCurrentThreadId(), p:th._p, ret:th.returnValue.toString()}); });
addHook('hy_read', libBase.add(HY_READ_OFF),
function(th, s){
th._x0 = s.x0.toString(); th._buf = s.x1; th._len = s.x2.toInt32();
}, function(th){
var n = th.returnValue.toInt32();
var hex = '';
if (n > 0) { try{ hex = th._buf.readByteArray(Math.min(n,512)) ? Array.from(new Uint8Array(th._buf.readByteArray(Math.min(n,512)))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('') : ''; }catch(e){} }
send({type:'hyread', tid:Process.getCurrentThreadId(), x0:th._x0, len:th._len, n:n, hex:hex});
});
send({type:'libc-hooked'});
}
setInterval(function(){ try{ hookAll(); }catch(e){} }, 150);
setTimeout(function(){
Java.perform(function(){
try{
var NE = Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'cls-ok'});
var out = {};
['init','getGUID','getCDID','getHDID','getMID','getSDID'].forEach(function(m){
try{
if(m==='init'){ NE[m](); out[m]='ok'; }
else{
var v = NE[m]();
out[m] = v;
send({type:'val', m:m, v:v});
}
}catch(e){ out[m]='ERR:'+String(e).slice(0,60); }
});
send({type:'all-vals', out:out});
}catch(e){ send({type:'java-err', e:String(e).slice(0,150)}); }
});
}, 6000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"events": [], "values": {}, "lib": None, "detached": det}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t in ("prop", "open", "ioctl", "read", "sys", "hysys", "hysys-read", "hysys-open", "hyfopen", "hyread"):
result["events"].append(p)
# 打印去重: 同一(key)只打印前几次
key = (t, p.get("k") or p.get("p") or p.get("name") or str(p.get("nr")) or str(p.get("fd")), p.get("v") or str(p.get("n") or ""))
if result["values"].get("_print") is None: result["values"]["_print"] = 0
if result["values"]["_print"] < 400:
result["values"]["_print"] += 1
if t == "hysys":
print(f"[hysys] {p.get('name')} nr={p.get('nr')} a1={p.get('a1')} a2={p.get('a2')} a3={p.get('a3')}", flush=True)
elif t == "hysys-read":
print(f"[hysys-read] {p.get('name')} n={p.get('n')} hex={p.get('hex','')[:60]}...", flush=True)
elif t == "hysys-open":
print(f"[hysys-open] {p.get('p')} fd={p.get('fd')}", flush=True)
elif t == "hyfopen":
print(f"[hyfopen] {p.get('p')} ret={p.get('ret')}", flush=True)
elif t == "hyread":
print(f"[hyread] x0={p.get('x0')} len={p.get('len')} n={p.get('n')} hex={p.get('hex','')[:60]}...", flush=True)
elif t == "prop":
print(f"[prop] {p.get('k')}={p.get('v')}", flush=True)
elif t == "open":
print(f"[open] {p.get('p')} fd={p.get('fd')}", flush=True)
elif t == "read":
print(f"[read] fd={p.get('fd')} n={p.get('n')} hex={p.get('hex','')[:60]}...", flush=True)
elif t == "ioctl":
print(f"[ioctl] fd={p.get('fd')} req={p.get('req')}", flush=True)
elif t == "sys":
print(f"[sys] nr={p.get('nr')} a1={p.get('a1')} a2={p.get('a2')}", flush=True)
elif t == "val":
result["values"][p["m"]] = p["v"]
print(f"[*] {p['m']} = {p['v']}", flush=True)
elif t == "all-vals":
result["values"].update(p["out"])
elif t in ("w-open", "w-close", "windows-hooked", "libc-hooked", "cls-ok"):
print(f"[*] {t}", flush=True)
elif "err" in t:
print(f"[!] {t}: {p.get('e')}", flush=True)
JS = JS_TEMPLATE.replace("__WINDOW_JSON__", json.dumps({k: hex(v) for k, v in WINDOW_OFFS.items()})
).replace("__SYSCALL_JSON__", json.dumps(SYSCALLS))
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, 等待 init 窗口(6s 后主动触发 Java 调用)...", flush=True)
time.sleep(11)
print("[*] 采集结束", flush=True)
time.sleep(2)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT} (events={len(result['events'])})", flush=True)
if __name__ == "__main__":
main()
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/env python3
"""真机: 确认 getGUID 生成时是否 ioctl/openat 读底层硬件(而非属性/文件).
hook libc ioctl + openat + __system_property_get, getGUID(0x20f8c8) onEnter/onLeave 设窗口,
窗口内记录调用的 ioctl(fd/request) / openat(path) / 属性读取。决定 32hex 硬件输入来源。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_getguid_ioctl.py [serial] [remote]
输出:
evidence/diag_phone/getguid_ioctl.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "getguid_init.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
GETGUID_OFF = 0x20E6E4
JS = r"""
'use strict';
var inGuid=false, hooked=false, iosHooked=false;
function hookIo(){
if(iosHooked) return;
try{
var ioctl = Module.findExportByName('libc.so','ioctl');
var openat = Module.findExportByName('libc.so','openat');
var open = Module.findExportByName('libc.so','open');
if(!ioctl && !openat && !open) return;
iosHooked=true;
if(ioctl) Interceptor.attach(ioctl,{onEnter:function(a){ if(inGuid){ try{ send({type:'ioctl', fd:a[0].toInt32(), req:a[1].toInt32()}); }catch(e){} } }});
if(openat) Interceptor.attach(openat,{onEnter:function(a){ if(inGuid){ try{ var p=a[1].readCString()||'?'; send({type:'openat', p:p}); }catch(e){} } }});
if(open) Interceptor.attach(open,{onEnter:function(a){ if(inGuid){ try{ send({type:'open', p:a[0].readCString()||'?'}); }catch(e){} } }});
send({type:'io-hooked'});
}catch(e){ send({type:'io-err', e:String(e)}); }
}
function hookGetGUID(){
if(hooked) return;
var md=Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hooked=true;
Interceptor.attach(md.base.add(0x20e6e4),{ // init()
onEnter:function(){ inGuid=true; send({type:'g-enter'}); },
onLeave:function(){ inGuid=false; send({type:'g-leave'}); }
});
send({type:'guid-hooked'});
}
setInterval(hookGetGUID,200);
setInterval(hookIo,200);
setTimeout(function(){
Java.perform(function(){
try{
var NE=Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'cls-ok'});
send({type:'call', v:NE['init']()}); // 主动 init
}catch(e){ send({type:'java-err', e:String(e).slice(0,120)}); }
});
}, 8000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"ioctl": [], "openat": [], "props": [], "detached": det}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t == "ioctl":
result["ioctl"].append(p); print(f"[ioctl] fd={p.get('fd')} req=0x{p.get('req'):x}", flush=True)
elif t == "openat":
result["openat"].append(p); print(f"[openat] {p.get('p')}", flush=True)
elif t == "open":
result["openat"].append(p); print(f"[open] {p.get('p')}", flush=True)
elif t in ("g-enter","g-leave","guid-hooked","io-hooked","cls-ok","call"):
print(f"[*] {t}", p if t=="call" else "", flush=True)
elif t in ("io-err","java-err"):
print(f"[!] {t}: {p.get('e')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(9)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] ioctl={len(result['ioctl'])} openat={len(result['openat'])} saved {OUT}")
if __name__ == "__main__":
main()
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""真机: 主动 Java 调用 NativeEntry 的 getGUID/getMID/getCDID/getSDID/getHDID, 找 32hex hdid(field1.tag0).
hypasswordLogin 登录帧 field1.tag0 的 32hex hdid 未在 SSL 帧/其他 get* 中抓到,
最可能是 getGUID()/getMID()(之前未触发) 的返回值。本脚本在 Java VM 就绪后
用 Java.use 主动调用全部 get 方法并打印, 确定 32hex hdid 的来源方法。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_java_call_nativeentry.py [serial] [remote]
输出:
evidence/diag_phone/java_nativeentry.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "java_nativeentry.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
setTimeout(function(){
Java.perform(function(){
var out = {};
try{
var NE = Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'class-ok', cls:'com.huya.security.hydeviceid.NativeEntry'});
['init','getGUID','getMID','getCDID','getSDID','getHDID'].forEach(function(m){
try{ out[m] = NE[m](); }catch(e){ out[m] = '<ERR:'+String(e).slice(0,50)+'>'; }
send({type:'val', method:m, value:out[m]});
});
send({type:'all', out:out});
}catch(e){
send({type:'use-err', e:String(e).slice(0,200)});
}
});
}, 6000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"vals": {}, "detached": det}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
t = p.get("type")
if t == "val":
result["vals"][p["method"]] = p["value"]
print(f"[val] {p['method']} = {p['value']}", flush=True)
elif t == "class-ok":
print(f"[*] class found", flush=True)
elif t == "all":
print(f"[*] 全部读取完成", flush=True)
elif t in ("use-err",):
print(f"[!] use-err: {p.get('e')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(20)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT}")
if __name__ == "__main__":
main()
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""真机探针: 枚举 libhydeviceid.so / libudbauthunify.so 导出符号, 定位 32hex hdid 生成入口.
用真机稳定注入通道 (spawn + art_callsite bypass + resume) 长时存活。
App 启动即加载这两个 so, 枚举 exports 拿函数名+基址偏移, 为下一步
针对性 hook 生成函数(抓输入)做准备。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe_exports.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe_exports.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
PROBE_JS = r"""
'use strict';
function listExports(modName, tag){
try{
var md = Process.findModuleByName(modName);
if(!md){ send({type:'mod', name:modName, state:'not-loaded'}); return false; }
var ex = md.enumerateExports();
var sym = md.enumerateSymbols();
send({type:'mod', name:modName, base:''+md.base, size:md.size,
exports: ex.map(function(e){return {name:e.name, type:e.type, off:''+e.address.sub(md.base)};}),
symCount: sym.length,
syms: sym.map(function(s){return {name:s.name, off:''+s.address.sub(md.base)};})});
return true;
}catch(e){ send({type:'err', name:modName, e:String(e)}); return false; }
}
function hookPropGet(){
var t = Module.findExportByName('libc.so','__system_property_get');
if(!t) return;
Interceptor.attach(t,{
onEnter:function(a){ this.k = a[0].readCString()||''; },
onLeave:function(ret){
try{ send({type:'prop', k:this.k, v:(this.ctx.x1.readCString()||'')}); }catch(e){}
}
});
send({type:'propget-hooked'});
}
// App 启动早期就抓 libhydeviceid.so 加载后的符号 + 派生属性读取
setTimeout(function(){ listExports('libhydeviceid.so','hydev'); }, 2000);
setTimeout(function(){ listExports('libudbauthunify.so','udb'); }, 2000);
setTimeout(hookPropGet, 2000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
d.resume(pid)
print("[*] bypass loaded + resumed", flush=True)
result = {"modules": {}, "props": []}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True)
return
p = m.get("payload") or {}
t = p.get("type")
if t == "mod":
result["modules"][p["name"]] = p
print(f"[mod] {p['name']} base={p.get('base')} exports={len(p.get('exports') or [])} syms={p.get('symCount')}", flush=True)
elif t == "prop":
result["props"].append({"k": p.get("k"), "v": p.get("v")})
elif t == "propget-hooked":
print("[*] __system_property_get hooked", flush=True)
elif t == "err":
print(f"[err] {p.get('name')}: {p.get('e')}", flush=True)
sc = session.create_script(PROBE_JS)
sc.on("message", on_message)
sc.load()
print("[*] probe loaded, collecting 12s", flush=True)
time.sleep(12)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
# 打印关键: 各 so 的导出符号名(去重)
for name, m in result["modules"].items():
print(f"\n=== {name} exports ({len(m.get('exports') or [])}) ===")
for e in (m.get("exports") or [])[:60]:
print(f" 0x{e['off']} {e['name']}")
print("\n=== 派生属性 (去重) ===")
seen = {}
for pr in result["props"]:
seen[pr["k"]] = pr["v"]
for k, v in seen.items():
print(f" {k} = {v}")
print(f"\n[*] saved {OUT}")
if __name__ == "__main__":
main()
-143
View File
@@ -1,143 +0,0 @@
#!/usr/bin/env python3
"""真机探针 v3: 等 so 加载后 hook, 抓 32hex hdid 生成/使用链路 + 登录触发.
修正 v1/v2: 挂起时 libhydeviceid.so 尚未加载 -> 用 setInterval 轮询,
so 一加载即 hook:
- libhydeviceid.so: 枚举非 runtime 导出 + hook JNI_OnLoad(0x22c4c8)
- libudbauthunify.so: hook BusinessCfg::getHdid(0x26a484) / setSafeDeviceId(0x26a2e0) 带调用栈
resume 后 am start GameSdkLoginActivity 触发设备上报/登录, 让 hdid 被读取/生成.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid3.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe3.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe3.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first&1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<256)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
var hydev=null, udb=null;
function hookHydev(){
if(hydev) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hydev = md;
send({type:'hydev', base:''+md.base, size:md.size});
var ex = md.enumerateExports();
var useful = ex.filter(function(e){ return !/^_Z/.test(e.name) && !/^__/.test(e.name) && e.name!==''; });
send({type:'hydev-exports', count:useful.length,
exports: useful.map(function(e){ return {off:''+e.address.sub(md.base), name:e.name}; })});
try{
Interceptor.attach(md.base.add(0x22c4c8), {
onEnter:function(){ send({type:'jni-onload-enter', tid:Process.getCurrentThreadId()}); },
onLeave:function(ret){ send({type:'jni-onload-leave', ret:''+ret}); }
});
send({type:'jni-onload-hooked'});
}catch(e){ send({type:'hydev-err', e:String(e)}); }
}
function hookUdb(){
if(udb) return;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udb = md;
send({type:'udb', base:''+md.base});
try{
Interceptor.attach(md.base.add(0x26a484), {
onLeave:function(ret){ send({type:'hdid', str:stdstr(ret), tid:Process.getCurrentThreadId()}); }
});
send({type:'hdid-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
try{
Interceptor.attach(md.base.add(0x26a2e0), {
onEnter:function(a){
send({type:'setsd', sd:stdstr(a[1]), hd:stdstr(a[2]), tid:Process.getCurrentThreadId()});
try{
var bt=Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,20).map(function(x){return x.toString();});
send({type:'bt', bt:bt});
}catch(e){}
}
});
send({type:'setsd-hooked'});
}catch(e){}
}
setInterval(hookHydev, 40);
setInterval(hookUdb, 40);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"events": [], "detached": det}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
result["events"].append(p)
t = p.get("type")
if t in ("hdid", "setsd"):
print(f"[{t}] {p.get('hd') or p.get('str')} tid={p.get('tid')}", flush=True)
elif t == "bt":
print(" bt:", " <- ".join(p["bt"][:8]), flush=True)
elif t == "hydev":
print(f"[*] libhydeviceid loaded base={p.get('base')}", flush=True)
elif t == "hydev-exports":
print(f"[*] hydev 非runtime导出 {p.get('count')}:", flush=True)
for e in (p.get("exports") or [])[:40]:
print(f" 0x{e['off']} {e['name']}")
elif t in ("udb","hdid-hooked","setsd-hooked","jni-onload-hooked","jni-onload-enter","jni-onload-leave"):
print(f"[*] {t}", p if t in ("jni-onload-leave",) else "", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(5)
print("[*] 触发登录 Activity...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(15)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT}")
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More