dfpReport: 确定加密为逐字节XOR(动态keystream)+同步pair捕获+IDA导出线索, 记录破解进度
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user