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:
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -1,222 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""UdbAESUtil 差分分析 harness(NativeFunction 直调)。
|
||||
|
||||
时序(不可变): 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
Reference in New Issue
Block a user