dfpReport: 确定加密为逐字节XOR(动态keystream)+同步pair捕获+IDA导出线索, 记录破解进度

This commit is contained in:
yml2213
2026-08-26 14:17:58 +08:00
parent acb0a737eb
commit 4eec01568f
38 changed files with 4942 additions and 3 deletions
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""捕获 (明文JSON, 密文) 配对: 解 dfpReport 流式 keystream。
同时:
1. hook SSL_write 抓 dfpReport 密文 (魔数开头 3984B)
2. 扫描内存找明文 JSON ({"appId":"5008"...), dump 前 512 + 后 6000
命中配对后立即 pkill。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pair.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var captured = {wire: null, plain: null};
// ---- 1. SSL_write 抓密文 ----
try{
var fns = [];
var r = new ApiResolver('module');
fns = r.enumerateMatchesSync('exports:*!SSL_write').map(function(x){ return x.address; });
fns.slice(0, 6).forEach(function(p){
Interceptor.attach(p, {
onEnter: function(a){
var len = a[2].toInt32();
if (len < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
var arr = new Uint8Array(a[1].readByteArray(len));
var hex = '';
for (var j = 0; j < arr.length; j++) hex += ('0'+arr[j].toString(16)).slice(-2);
captured.wire = {len: len, hex: hex};
send({type:'wire_cap', len: len});
trySend();
}
});
});
}catch(e){}
// ---- 2. 扫描明文 JSON ----
function scanPlain(){
try{
var r2 = new ApiResolver('module');
Process.enumerateRanges('r--').forEach(function(rng){
if (rng.size > 1024*1024*256) return;
try{
var hits = Memory.scanSync(rng.base, rng.size, '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38');
hits.slice(0, 3).forEach(function(x){
var post = '';
try{
var arr = new Uint8Array(x.address.readByteArray(4096));
post = Array.from(arr).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('');
}catch(e){ return; }
captured.plain = {addr: String(x.address), hex: post};
send({type:'plain_cap', addr: String(x.address)});
trySend();
});
}catch(e){}
});
}catch(e){}
}
function trySend(){
if (captured.wire && captured.plain) {
var out = {wire: captured.wire, plain: captured.plain};
send({type:'PAIR', data: out});
}
}
setInterval(function(){ try{ scanPlain(); }catch(e){} }, 3000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "wire_cap":
print(f"[wire] 密文捕获 len={p.get('len')}", flush=True)
elif t == "plain_cap":
print(f"[plain] 明文JSON @{p.get('addr')}", flush=True)
elif t == "PAIR":
OUT.write_text(json.dumps(p.get('data')))
print(f"[PAIR] 保存 -> {OUT}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, 请触发登录, PAIR 出现即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
+126
View File
@@ -0,0 +1,126 @@
#!/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()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""诊断: spawn + bypass 后进程是否存活, 崩溃原因是什么。
对照组: 不改写数据段。对比之前 patch_t1_experiment(改写 3 处)是否崩溃。
"""
from __future__ import annotations
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'exception', code:d.code, addr:String(d.address), ctx:d.context});
return true;
});
send({type:'armed'});
setInterval(function(){ send({type:'alive', t:Date.now()}); }, 3000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
session.on("detached", lambda reason, crash: print(
f"[DETACHED] reason={reason} crash={crash}", flush=True))
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "armed":
print("[*] armed", flush=True)
elif t == "exception":
print(f"[EXCEPTION] code={p.get('code')} addr={p.get('addr')}", flush=True)
elif t == "alive":
print("[*] alive", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
print("[*] resumed, 观察 30s", flush=True)
time.sleep(30)
print("[*] 结束", flush=True)
if __name__ == "__main__":
main()
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""dump libhydeviceid.so .data 段 (datadiv 就地解密区), 提取全部明文串。
datadiv 解码器把 .data 段 0x3c8xxx 区域的密文就地解密为明文。
dump 整个 .data(0x3bb000 ~ 0x3eac10), 提取可读字符串 + 搜魔数。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_data.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var DATA_RANGES = [
[0x36aec0, 0x6e75], // .rodata (含 datadiv 密文? 可能)
[0x3b2460, 0x74d8], // .data.rel.ro
[0x3bb000, 0x2fc08], // .data (datadiv 就地解密区)
];
function dumpData(m){
DATA_RANGES.forEach(function(r){
var off = r[0], size = r[1];
try{
var base = m.base.add(off);
var bytes = Array.from(new Uint8Array(base.readByteArray(size)));
// 提取可读字符串 (>=5 连续可打印)
var strs = [];
var cur = '';
for (var i = 0; i < bytes.length; i++) {
var c = bytes[i];
if (c >= 32 && c < 127) cur += String.fromCharCode(c);
else {
if (cur.length >= 5) strs.push({s: cur, off: off + i - cur.length});
cur = '';
}
}
// 搜魔数
var hasMagic = false;
for (var i = 0; i + 5 < bytes.length; i++) {
if (bytes[i]==0x57 && bytes[i+1]==0x18 && bytes[i+2]==0x82 && bytes[i+3]==0xcf) { hasMagic = true; break; }
}
send({type:'data', sec:off.toString(16), size:size, strs:strs.length,
hasMagic: hasMagic,
sample: strs.slice(0, 60)});
}catch(e){
send({type:'data_err', sec:off.toString(16), e:String(e)});
}
});
}
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var m = null;
try{ m = Process.getModuleByName('libhydeviceid.so'); }catch(e){}
if (m) {
clearInterval(iv);
send({type:'mod', base:String(m.base), size:m.size});
dumpData(m);
} else if (tries > 100) { clearInterval(iv); send({type:'no_mod'}); }
}, 50);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "data":
print(f"[data] sec=0x{p.get('sec')} size={p.get('size')} "
f"strs={p.get('strs')} magic={p.get('hasMagic')}", flush=True)
for it in p.get('sample', [])[:40]:
print(f" 0x{it['off']:x}: {it['s']}", flush=True)
events.append({"section": p.get('sec'), "strings": p.get('sample')})
OUT.write_text(json.dumps(events, indent=1))
elif t == "data_err":
print(f"[data_err] {p.get('sec')}: {p.get('e')}", flush=True)
elif t == "mod":
print(f"[*] libhydeviceid @ {p.get('base')} size={p.get('size')}", flush=True)
elif t == "no_mod":
print("[*] 模块未加载", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, dump 完成即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""完整导出 libhydeviceid.so .data 段解密后的全部字符串(不截断)。
.resume 后 .data 已被 datadiv 就地解密为明文。dump 全部字节 + 全部
字符串(带偏移)到 JSON, 供离线分析 XXTEA key / 魔数 / t1 来源。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_full.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var RANGES = [
[0x36aec0, 0x6e75], // .rodata
[0x3b2460, 0x74d8], // .data.rel.ro
[0x3bb000, 0x2fc08], // .data
];
function dumpSec(m, off, size){
try{
var base = m.base.add(off);
var raw = base.readByteArray(size);
var bytes = Array.from(new Uint8Array(raw));
var strs = [];
var cur = '';
var curOff = 0;
for (var i = 0; i < bytes.length; i++) {
var c = bytes[i];
if (c >= 32 && c < 127) { if (!cur) curOff = i; cur += String.fromCharCode(c); }
else {
if (cur.length >= 4) strs.push([off + curOff, cur]);
cur = '';
}
}
if (cur.length >= 4) strs.push([off + curOff, cur]);
return {sec: off.toString(16), size: size, n: strs.length, strs: strs};
}catch(e){ return {sec: off.toString(16), err: String(e)}; }
}
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var m = null;
try{ m = Process.getModuleByName('libhydeviceid.so'); }catch(e){}
if (m) {
clearInterval(iv);
var out = [];
RANGES.forEach(function(r){ out.push(dumpSec(m, r[0], r[1])); });
send({type:'all', ranges: out});
} else if (tries > 100) { clearInterval(iv); send({type:'no_mod'}); }
}, 50);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "all":
total = sum(r.get('n', 0) for r in p.get('ranges', []))
print(f"[*] 完成, 共 {total} 条字符串 -> {OUT}", flush=True)
OUT.write_text(json.dumps(p.get('ranges'), ensure_ascii=False))
elif t == "no_mod":
print("[*] 模块未加载", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
time.sleep(5)
print("[*] dump 完成, 结束", flush=True)
if __name__ == "__main__":
main()
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""dump 魔数缓冲区: 找到魔数地址后, dump 前后完整内存到证据文件。
命中即写入文件(不依赖退出), 打印明文头与密文长度。
"""
from __future__ import annotations
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_buffer_dump.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
function doScan(){
var found = false;
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
var m = Memory.scanSync(r.base, r.size, '57 18 82 cf 66 4b b3 94');
m.forEach(function(x){
var addr = x.address;
// 只 dump 每个 4KB 页内第一个命中 (去重)
var pre256 = hex(addr.sub(64), 64);
var post = hex(addr.add(10), 4096);
send({type:'dump', addr:String(addr), pre:pre256, post:post});
found = true;
});
}catch(e){}
});
}catch(e){}
send({type:'tick', found:found});
}
var iv = setInterval(function(){ doScan(); }, 3000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
dumped = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:160], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dump":
addr = p.get("addr")
# 去重 (同地址不重复存)
if any(d.get("addr") == addr for d in dumped):
return
rec = {"addr": addr, "pre": p.get("pre"), "post": p.get("post")}
dumped.append(rec)
# 立即持久化
OUT.write_text(__import__("json").dumps(dumped, indent=1))
print(f"[DUMP] @{addr} pre={p.get('pre','')[:64]} postlen={len(p.get('post',''))//2} saved={len(dumped)}", flush=True)
elif t == "tick":
print(f"[tick] found={p.get('found')} total_dumps={len(dumped)}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] dump 扫描中 (每3s), 命中即持久化。请触发登录 (命中后可 pkill)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, 共 {len(dumped)} 个 dump -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""完整 dump 明文采集数据区域 (0x7814096d19 JSON 附近 + 0x78133bf220 键值结构)。
拿到加密前的完整明文 = 解密 dfpReport 的关键! dump 前后 8KB 到文件。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_plaintext_full.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
// 锚点: JSON 明文 + t1 键值结构
var ANCHORS = [
['json', '74 65 72 6d 69 6e 61 6c 54 79 70 65 22 3a 31'], // terminalType":1
['t1kv', '74 65 72 6d 69 6e 61 6c'], // terminal (键值结构内)
];
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
function scan(){
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
ANCHORS.forEach(function(a){
var hits = Memory.scanSync(r.base, r.size, a[1]);
hits.slice(0, 5).forEach(function(x){
var addr = x.address;
// JSON: dump 前 512 + 后 8192
var pre = '', post = '';
try{ pre = hex(addr.sub(512), 512); }catch(e){}
try{ post = hex(addr, 8192); }catch(e){}
send({type:'dump', anchor:a[0], addr:String(addr), pre:pre, post:post});
});
});
}catch(e){}
});
}catch(e){}
send({type:'tick'});
}
setInterval(function(){ try{ scan(); }catch(e){} }, 4000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dump":
addr = p.get('addr')
if any(e.get('addr') == addr for e in events): return
rec = {"anchor": p.get('anchor'), "addr": addr,
"pre": p.get('pre'), "post": p.get('post')}
events.append(rec)
OUT.write_text(json.dumps(events, ensure_ascii=False))
# 打印 post 的可读部分 (前 400B)
post = bytes.fromhex(p.get('post', ''))
asc = ''.join(chr(b) if 32 <= b < 127 else '.' for b in post[:400])
print(f"[dump:{p.get('anchor')}] @{addr}", flush=True)
print(f" {asc}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print("[*] 已 resume, dump 明文区 (需触发登录采集后可 pkill)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+137
View File
@@ -0,0 +1,137 @@
#!/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()
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""重 hook datadiv 解码器: 读返回值指向内存的前 64B hex。
datadiv 函数解密一个数据块(不只是字符串), 魔数 5718 / 设备数据 / key
可能藏在解码后的内存里。命中(含 5718 或 android)即持久化。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_datadiv_hex.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function hex(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return 'ERR'; }
}
var hooked = 0;
function hookMod(modName){
var m = null;
try{ m = Process.getModuleByName(modName); }catch(e){ return false; }
m.enumerateExports().forEach(function(exp){
if (exp.name.indexOf('datadiv') < 0) return;
try{
Interceptor.attach(exp.address, {
onLeave: function(ret){
var p = ret;
if (p.isNull()) return;
var h64 = '';
try{ h64 = hex(p, 64); }catch(e){ return; }
if (h64.indexOf('ERR') >= 0) return;
// 命中关键词: 魔数 / android / 可读字符串
var low = h64.toLowerCase();
var hit = low.indexOf('5718') >= 0 || low.indexOf('616e64726f6964') >= 0;
var str = '';
try{ str = p.readUtf8String(); }catch(e){}
if (hit || (str && str.length > 4 && str.length < 100 && /[a-z]{4,}/i.test(str))) {
send({type:'dd', mod:modName, fn:exp.name, h64:h64, str:str});
}
}
});
hooked += 1;
}catch(e){}
});
return true;
}
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var ok = hookMod('libhydeviceid.so') | hookMod('libudbauthunify.so');
if (tries % 5 === 0) send({type:'waiting', tries:tries, hooked:hooked});
if (ok) { clearInterval(iv); send({type:'hooked_count', n:hooked}); }
if (tries > 60) clearInterval(iv);
}, 1000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:160], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "dd":
print(f"[dd] {p.get('mod')} {p.get('fn')}: {p.get('h64')} {p.get('str')!r}", flush=True)
events.append(p)
OUT.write_text(json.dumps(events, indent=1))
elif t == "hooked_count":
print(f"[*] hooked {p.get('n')} datadiv", flush=True)
elif t == "waiting":
print(f"[waiting] tries={p.get('tries')} hooked={p.get('hooked')}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] datadiv 重hook中 (每1s重试), 请触发登录 (命中即持久化)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/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()
+179
View File
@@ -0,0 +1,179 @@
#!/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()
+262
View File
@@ -0,0 +1,262 @@
#!/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()
+127
View File
@@ -0,0 +1,127 @@
#!/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()
+155
View File
@@ -0,0 +1,155 @@
#!/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()
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""改写明文JSON字段实验: 验证服务端回显本地明文。
明文JSON {hdid: 7c5387..., appkey: 865a...cbb0e3, deviceId: ...} 在堆中。
等长替换 hdid/appkey/deviceId -> 新值, 触发登录, 看 dfpReport 响应 t5 是否变化。
同时 hook SSL_write 抓密文 + SSL_read 抓响应。
"""
from __future__ import annotations
import json
import random
import string
import time
from pathlib import Path
import frida
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_plain_patch.json")
NEW_HDID = "".join(random.choice("0123456789abcdef") for _ in range(40))
NEW_APPKEY = "".join(random.choice("0123456789abcdef") for _ in range(32))
NEW_DEVID = "".join(random.choice("0123456789abcdef") for _ in range(40))
print(f"[*] 新 hdid = {NEW_HDID}")
print(f"[*] 新 appkey = {NEW_APPKEY}")
print(f"[*] 新 deviceId= {NEW_DEVID}")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var NEW_HDID = '%s'; // 40hex
var NEW_APPKEY = '%s'; // 32hex
var NEW_DEVID = '%s'; // 40hex
function hexb(p, n){
try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
catch(e){ return null; }
}
// 找明文 JSON {"appId":"5008" 并等长替换字段
function patchJson(){
try{
Process.enumerateRanges('r--').forEach(function(rng){
if (rng.size > 1024*1024*256) return;
try{
var hits = Memory.scanSync(rng.base, rng.size, '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38');
hits.slice(0, 6).forEach(function(x){
var p = x.address;
var s = '';
try{ s = p.readUtf8String(1200); }catch(e){ return; }
if (s.indexOf('"hdid"') < 0) return;
send({type:'found_json', addr:String(p), len:s.length});
// 等长替换 hdid / appkey / deviceId
try{
Memory.protect(p, s.length + 16, 'rwx');
var out = s;
out = out.replace(/"hdid":"[0-9a-f]{40}"/, '"hdid":"' + NEW_HDID + '"');
out = out.replace(/"appkey":"[0-9a-f]{32}"/, '"appkey":"' + NEW_APPKEY + '"');
out = out.replace(/"deviceId":"[0-9a-f]{40}"/, '"deviceId":"' + NEW_DEVID + '"');
if (out !== s) {
p.writeUtf8String(out);
var after = p.readUtf8String(120);
send({type:'patched_json', addr:String(p), after:after.slice(0, 180)});
}
}catch(e){ send({type:'patch_err', addr:String(p), e:String(e)}); }
});
}catch(e){}
});
}catch(e){}
}
// SSL_write 抓 dfpReport 密文
try{
var r = new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address, {
onEnter: function(a){
var len = a[2].toInt32();
if (len < 100 || len > 20000) return;
var head = '';
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
if (head.indexOf('dfpReport') < 0) return;
var hex = hexb(a[1], len);
send({type:'wire', len:len, hex:hex});
}
});
});
}catch(e){}
// SSL_read 抓响应 (找 t5 / t1)
try{
var r2 = new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address, {
onEnter: function(a){
this.buf = a[1]; this.len = a[2].toInt32();
},
onLeave: function(ret){
var n = ret.toInt32();
if (n <= 0 || n > 65536) return;
try{
var hex = hexb(this.buf, n);
send({type:'sslread', len:n, hex:hex});
}catch(e){}
}
});
});
}catch(e){}
// 每 5s patch 一次 (登录时 JSON 重新生成)
setInterval(function(){ try{ patchJson(); }catch(e){} }, 5000);
"""
def main():
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn(["com.duowan.kiwi"])
print(f"[*] spawned pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:200], flush=True); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "found_json":
print(f"[json] @{p.get('addr')} len={p.get('len')}", flush=True)
events.append({"found": p.get('addr'), "new_hdid": NEW_HDID,
"new_appkey": NEW_APPKEY, "new_devid": NEW_DEVID})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
elif t == "patched_json":
print(f"[PATCH-OK] @{p.get('addr')}: {p.get('after')!r}", flush=True)
elif t == "patch_err":
print(f"[patch-err] @{p.get('addr')}: {p.get('e')}", flush=True)
elif t == "wire":
body = bytes.fromhex(p.get('hex'))
try: hp = body.index(b'\r\n\r\n') + 4
except ValueError: hp = 0
b = body[hp:]
j = b.find(b'\x57\x18\x82\xcf\x66\x4b\xb3\x94')
print(f"[wire] len={p.get('len')} 魔数@{j}", flush=True)
if j >= 0:
print(f" 密文头: {b[j+10:j+30].hex(' ')}", flush=True)
events.append({"wire": p.get('hex')})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
elif t == "sslread":
h = p.get('hex', '')
low = h.lower()
# dfpReport 响应里找 40hex t5
if len(h) > 200 and ('7c5387' in low or 't1' in low or 't5' in low or len(h) < 4000):
print(f"[read] len={p.get('len')} hex[:120]: {h[:120]}", flush=True)
events.append({"read": h})
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 主JS 已装载(resume前), 加载 bypass + resume", flush=True)
session.create_script(
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
).load()
session.create_script(
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
).load()
device.resume(pid)
time.sleep(11)
session.create_script(
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
).load()
print(f"[*] 已 resume. 将把 hdid={NEW_HDID} appkey={NEW_APPKEY} deviceId={NEW_DEVID} 写入明文JSON", flush=True)
print("[*] 请 退出登录->重新登录, 观察响应 t5 是否为新 hdid", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+156
View File
@@ -0,0 +1,156 @@
#!/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()
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""定位 dfpReport 加密体生成函数: 批量 hook libhydeviceid.so / libtscsdk.so
导出函数, 记录返回长度 >500B 的调用。登录触发后, 找输出含魔数
571882cf664bb39401ee 或长度 ≈3908B 的函数。
"""
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_bigout_capture.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){
send({type:'segv', info:{type:d.type, addr:String(d.address)}});
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){
try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,90);}catch(e){return '?';}
}
var TARGETS = ['libhydeviceid.so', 'libtscsdk.so', 'libturingga.so'];
var MIN_LEN = 3000;
var MAX_LEN = 6000;
var hooked = 0;
TARGETS.forEach(function(modName){
try{
var m = Process.getModuleByName(modName);
}catch(e){ send({type:'skip', mod:modName}); return; }
var exps = m.enumerateExports();
exps.forEach(function(exp){
var n = exp.name;
if (n.indexOf('datadiv') === 0) return; // 字符串解码器, 跳过
if (n.indexOf('std::') === 0) return; // std 符号, 跳过
if (/^(_Z|Java_|JNI_)/.test(n)) return; // C++ 符号/Java 方法名太长, 跳过
try{
Interceptor.attach(exp.address, {
onEnter: function(args){
this.t0 = Date.now();
this.args = [];
for (var i = 0; i < Math.min(4, args.length); i++) {
try{
var v = args[i].toInt32();
if (v > 0 && v < 10000000) this.args.push(v);
}catch(e){}
}
},
onLeave: function(ret){
var dt = Date.now() - this.t0;
var v = ret.toInt32();
if ((v >= MIN_LEN && v <= MAX_LEN) || dt > 30) {
send({type:'big', mod:modName, fn:exp.name, ret:v, dt:dt,
args:this.args, r:ra(this.ctx)});
}
}
});
hooked += 1;
}catch(e){}
});
});
send({type:'hooked_count', n:hooked});
"""
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)[:160]); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "big":
print(f"[big] {p.get('mod')} {p.get('fn')} ret={p.get('ret')} dt={p.get('dt')}ms r={p.get('r','')}")
events.append(p)
elif t == "hooked_count":
print(f"[*] hooked {p.get('n')} exports")
elif t == "skip":
print(f"[skip] {p.get('mod')}")
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] hooks 装好, 请在手机上 退出登录 -> 重新登录 触发 dfpReport 链 (120s)")
deadline = time.time() + 120
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()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Hook .datadiv_decode* 字符串解码器, 找 571882cf 魔数 / dfp 相关字符串的来源。
libhydeviceid.so 和 libudbauthunify.so 的业务字符串全被 datadiv 混淆,
运行时才解码。魔数 571882cf664bb39401ee 静态搜不到 -> 一定是解码产物。
"""
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_datadiv_capture.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
var MAGIC_HEX = '571882cf664bb39401ee';
var KEYWORDS = ['dfp', '5718', '82cf', 'b394', 'collect', 'encrypt', 'device'];
var hooked = 0;
function hookMod(modName){
var m = null;
try{ m = Process.getModuleByName(modName); }catch(e){ return false; }
var exps = m.enumerateExports();
exps.forEach(function(exp){
if (exp.name.indexOf('datadiv') < 0) return;
try{
Interceptor.attach(exp.address, {
onEnter: function(args){ this.t0=Date.now(); },
onLeave: function(ret){
var p = ret;
var s = '';
try{ s = p.readUtf8String(); }catch(e){}
var hit = false;
if (s && s.length > 3 && s.length < 2000) {
var low = s.toLowerCase();
KEYWORDS.forEach(function(k){
if (low.indexOf(k) >= 0) hit = true;
});
}
if (hit) {
send({type:'hit', mod:modName, fn:exp.name,
str: s.slice(0, 300)});
}
}
});
hooked += 1;
}catch(e){}
});
return true;
}
// 轮询等待模块加载
var tries = 0;
var iv = setInterval(function(){
tries += 1;
var ok1 = hookMod('libhydeviceid.so');
var ok2 = hookMod('libudbauthunify.so');
if (tries % 5 === 0) send({type:'waiting', tries:tries, hooked:hooked});
if (ok1 && ok2) { 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([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)[:160]); return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "hit":
print(f"[hit] {p.get('mod')} {p.get('fn')}: {p.get('str')!r}")
events.append(p)
elif t == "hooked_count":
print(f"[*] hooked {p.get('n')} datadiv decoders")
elif t == "skip":
print(f"[skip] {p.get('mod')}")
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 请在手机上 退出登录 -> 重新登录 触发 dfpReport 链 (120s)")
deadline = time.time() + 120
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()
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""侦察: 虎牙App 加载的模块中, 哪些含 dfp/hydevice/tsc/xxtea/aes 相关符号。
决定 dfpReport 加密体生成所在的 so, 再针对性 hook。
"""
from __future__ import annotations
import sys
from pathlib import Path
import frida
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS = r"""
'use strict';
var mods = Process.enumerateModules();
var kw = /dfp|hydevice|deviceid|tsc|udb|xxtea|aes|finger|crypt|secsdk|turing|risk/;
var out = [];
mods.forEach(function(m){
var n = m.name;
if (kw.test(n.toLowerCase())) out.push({name:n, base:String(m.base), size:m.size});
});
send({type:'mods', list:out});
"""
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()
script = session.create_script(JS)
def on_message(message, data):
if message.get("type") == "send":
payload = message.get("payload") or {}
if payload.get("type") == "mods":
for m in payload["list"]:
print(f" {m['name']:<32} {m['base']} size={m['size']}")
script.on("message", on_message)
script.load()
time.sleep(8)
print("[*] done")
session.detach()
if __name__ == "__main__":
import time
main()
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""扫描堆中"加密前的明文":设备采集数据(含 android- 开头、JSON/键值)。
dfpReport 明文 = [魔数][时间戳][设备数据], 加密前明文在堆里。
扫描含 'android''deviceinfo' 等键值的大块可读内存, 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_plain_scan.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
function tryRead(p, n){
try{ return p.readUtf8String(n); }catch(e){ return null; }
}
function scan(){
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
// 搜 "android-" 明文 (设备 UA 前缀, 加密前会出现)
var hits = Memory.scanSync(r.base, r.size, '61 6e 64 72 6f 69 64 2d');
hits.forEach(function(x){
var s = tryRead(x.address, 300);
if (s && s.length > 30 && /[a-z0-9]{3,}/.test(s)) {
send({type:'plain', addr:String(x.address), s:s.slice(0, 300)});
}
});
// 搜 "deviceinfo"
var hits2 = Memory.scanSync(r.base, r.size, '64 65 76 69 63 65 69 6e 66 6f');
hits2.forEach(function(x){
var s = tryRead(x.address, 300);
if (s) send({type:'plain', addr:String(x.address), s:s.slice(0, 300)});
});
}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 == "plain":
s = p.get('s', '')
# 过滤明显的噪音 (路径/类名)
if '/data/app' in s or 'library' in s or '.jar' in s: return
key = p.get('addr')
if any(e.get('addr') == key for e in events): return
events.append(p)
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
print(f"[plain] @{key}: {s[:180]!r}", 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, 扫描明文 (4s轮询), 请触发登录, 命中即 pkill", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} 条 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""扫描内存找加密前的 dfpReport 明文采集数据。
明文 = 设备采集序列化(含 deviceinfo/appid/机型 thyme/M2102J2SC 等)。
找到后 dump 前后 16KB, 与密文(魔数开头)配对解 keystream。
"""
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_plain_big.json")
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
// 设备特征串 (加密前明文会出现)
var PATTERNS = [
['thyme', '74 68 79 6d 65'],
['M2102J2SC', '4d 32 31 30 32 4a 32 53 43'],
['deviceinfo', '64 65 76 69 63 65 69 6e 66 6f'],
['appid', '61 70 70 69 64'],
['terminal', '74 65 72 6d 69 6e 61 6c'],
['android-', '61 6e 64 72 6f 69 64 2d'],
['Xiaomi', '58 69 61 6f 6d 69'],
['sdk', '73 64 6b'],
];
function tryRead(p, n){
try{ return p.readByteArray(n); }catch(e){ return null; }
}
function scan(){
var reported = {};
try{
Process.enumerateRanges('r--').forEach(function(r){
if (r.size > 1024*1024*256) return;
try{
PATTERNS.forEach(function(pat){
// 用打头模式避免每次整个段 MB 级扫描太慢: 只对前几个模式做全扫
var key = pat[0];
if (reported[key] > 20) return;
var m = Memory.scanSync(r.base, r.size, pat[1]);
if (!m.length) return;
reported[key] = (reported[key] || 0) + m.length;
m.slice(0, 8).forEach(function(x){
var addr = x.address;
// dump 前后: 前找结构头, 后 4KB
var pre = null, post = null;
try{ pre = addr.sub(256).readByteArray(512); }catch(e){}
try{ post = addr.readByteArray(4096); }catch(e){}
if (post) {
var arr = Array.from(new Uint8Array(post));
var hex = arr.map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('');
send({type:'hit', pat:key, addr:String(addr), hex:hex});
}
});
});
}catch(e){}
});
}catch(e){}
send({type:'tick'});
}
setInterval(function(){ try{ scan(); }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 == "hit":
h = p.get('hex', '')
addr = p.get('addr')
# 去重: 同地址只留一次
if any(e.get('addr') == addr for e in events): return
tag = p.get('pat')
events.append({"addr": addr, "pat": tag, "hex": h})
OUT.write_text(json.dumps(events, ensure_ascii=False))
# 打印: 特征串 + 前 64B 明文上下文
try:
idx = h.encode().decode('hex') if False else h
except Exception: pass
print(f"[hit:{tag}] @{addr}", flush=True)
# 提取特征串前后 3B 看明文结构
print(f" hex[:96]: {h[:96]}", 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, 扫描中 (特征: thyme/M2102J2SC/deviceinfo/appid/android-)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束, {len(events)} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""内存扫描: 找魔数 571882cf664bb39401ee 在进程内存中的位置。
App 组装 dfpReport 时魔数一定在内存中。扫描模块内存,
定位后 dump 周围字节 + 反查所在模块/偏移。
"""
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){ return true; });
send({type:'armed'});
var MAGIC8 = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94];
function scanRange(base, size, name){
try{
var m = Memory.scanSync(base, size, '57 18 82 cf 66 4b b3 94');
m.forEach(function(x){
var addr = x.address;
var ctx = [];
try{
var bytes = addr.sub(16).readByteArray(48);
ctx = Array.from(new Uint8Array(bytes));
}catch(e){}
send({type:'found', mod:name, addr:String(addr),
offInMod: addr.sub(base).toString(),
ctx: ctx.map(function(b){return ('0'+b.toString(16)).slice(-2);}).join(' ')});
});
}catch(e){}
}
function doScan(){
var mods = Process.enumerateModules();
mods.forEach(function(m){
var n = m.name.toLowerCase();
if (n.indexOf('hydevice')>=0 || n.indexOf('udb')>=0 || n.indexOf('tsc')>=0 ||
n.indexOf('hycrypto')>=0 || n.indexOf('libc')>=0 || n.indexOf('kiwi')>=0) {
try{ scanRange(m.base, m.size, m.name); }catch(e){}
}
});
// 也扫匿名可读内存 (堆)
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 ctx = [];
try{ ctx = Array.from(new Uint8Array(x.address.sub(16).readByteArray(48))); }catch(e){}
// dump 魔数前 256B + 后 512B 的 ascii 找明文
var pre = '', post = '';
try{ pre = x.address.sub(256).readUtf8String(); }catch(e){}
try{ post = x.address.add(10).readUtf8String(); }catch(e){}
send({type:'found_heap', addr:String(x.address),
ctx: ctx.map(function(b){return ('0'+b.toString(16)).slice(-2);}).join(' '),
pre: pre, post: post});
});
}catch(e){}
});
}catch(e){}
send({type:'scan_done'});
}
// 每 5 秒扫一次, 实时找魔数 (App 登录触发 dfpReport 时出现)
var iv = setInterval(function(){
try{ doScan(); }catch(e){ send({type:'scan_err', e:String(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}")
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()
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 == "found":
print(f"[FOUND] {p.get('mod')} @{p.get('addr')} off={p.get('offInMod')}", flush=True)
print(f" ctx: {p.get('ctx')}", flush=True)
elif t == "found_heap":
print(f"[FOUND-HEAP] @{p.get('addr')}", flush=True)
print(f" ctx: {p.get('ctx')}", flush=True)
pre = p.get('pre', '') or ''
post = p.get('post', '') or ''
if pre.strip(): print(f" pre256: {pre[:256]!r}", flush=True)
if post.strip(): print(f" post512: {post[:512]!r}", flush=True)
elif t == "mods":
print(f"[*] modules: {p.get('n')}", flush=True)
elif t == "scan_done":
print("[*] scan tick done", flush=True)
elif t == "scan_err":
print(f"[scan_err] {p.get('e')}", flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 内存扫描中 (每5s), 请在手机上 退出登录->重新登录 (持续扫描, 命中即停止)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print("[*] done", flush=True)
session.detach()
if __name__ == "__main__":
main()