343 lines
12 KiB
Python
343 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""散弹枪 v2: 完整解码 dfpReport 管道参数 (指针数组 / (ptr,len) / std::string)。
|
|
|
|
挂载: topA_20e924, midA_5a938, cryptoA_5b1f4, copy_61098, pktA_7f744, pktB_92128
|
|
对每个 onEnter 的 x0..x4:
|
|
1) 直接缓冲探测 (JSON/MAGIC 前缀 -> full dump 6000B)
|
|
2) (ptr,len) 解释: 读 16B -> 若两个 u64 第二个 < 100000 且第一个可读 -> dump
|
|
3) 指针数组解释: 读 4x8B 指针, 对每个指针尝试 (2)
|
|
4) std::string 解释: [size u64][data u64][cap u64] 或 [data][size][cap]
|
|
所有事件带 t (ms since start)。SSL_write 抓 wire 对照。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import frida
|
|
|
|
PACKAGE = "com.duowan.kiwi"
|
|
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
|
import time as _t
|
|
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pipeline_" + _t.strftime("%H%M%S") + ".json")
|
|
|
|
JS = r"""
|
|
'use strict';
|
|
Process.setExceptionHandler(function(d){ return true; });
|
|
send({type:'armed'});
|
|
|
|
var t0 = Date.now();
|
|
var JSON_HDR = [0x7b,0x22,0x61,0x70,0x70,0x49,0x64,0x22,0x3a,0x22,0x35,0x30,0x30,0x38];
|
|
var MAGIC_HDR = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94,0x01,0xee];
|
|
|
|
function hexb(p, n){
|
|
try{
|
|
var arr = Array.from(new Uint8Array(p.readByteArray(n)));
|
|
var s = '';
|
|
for (var i=0;i<arr.length;i++) s += ('0'+arr[i].toString(16)).slice(-2);
|
|
return s;
|
|
}catch(e){ return null; }
|
|
}
|
|
function startsWith(p, arr){
|
|
try{
|
|
var b = new Uint8Array(p.readByteArray(arr.length));
|
|
for (var i=0;i<arr.length;i++) if (b[i]!==arr[i]) return false;
|
|
return true;
|
|
}catch(e){ return false; }
|
|
}
|
|
function isReadable(p){
|
|
try{ p.readU8(); return true; }catch(e){ return false; }
|
|
}
|
|
function u64(p){
|
|
try{ return p.readU64().toString(10); }catch(e){ return null; }
|
|
}
|
|
function decodeArg(p, tag){
|
|
var res = {reg: tag};
|
|
try{
|
|
if (!p || p.isNull()) return null;
|
|
res.addr = String(p);
|
|
// 1) direct buffer
|
|
if (startsWith(p, JSON_HDR)){
|
|
res.kind = 'JSON'; res.full = hexb(p, 6000); return res;
|
|
}
|
|
if (startsWith(p, MAGIC_HDR)){
|
|
res.kind = 'MAGIC';
|
|
var pre = '';
|
|
try{ pre = hexb(p.sub(8192), 8192); }catch(e){}
|
|
res.pre = pre;
|
|
res.full = hexb(p, 9000);
|
|
return res;
|
|
}
|
|
// 2) (ptr,len) pair: [ptr][len]
|
|
var p0 = p.readPointer(); var l0 = p.add(8).readU64();
|
|
if (!p0.isNull() && isReadable(p0) && l0 > 0 && l0 < 20000 && p0 != p){
|
|
var b0 = hexb(p0, Math.min(Number(l0), 6000));
|
|
var kind = 'seg';
|
|
if (startsWith(p0, JSON_HDR)) kind = 'JSON';
|
|
else if (startsWith(p0, MAGIC_HDR)) kind = 'MAGIC';
|
|
res.kind = kind;
|
|
res.ptrlen = {ptr: String(p0), len: Number(l0)};
|
|
res.seg = b0;
|
|
return res;
|
|
}
|
|
// 3) pointer array (4 x 8B)
|
|
var ptrs = [];
|
|
for (var i=0;i<4;i++){
|
|
var q = p.add(i*8).readPointer();
|
|
if (q.isNull() || !isReadable(q)) break;
|
|
ptrs.push(String(q));
|
|
}
|
|
if (ptrs.length >= 2){
|
|
res.kind = 'ptrs'; res.ptrs = ptrs;
|
|
// decode each pointer as (ptr,len)
|
|
var segs = [];
|
|
for (var j=0;j<ptrs.length;j++){
|
|
var qj = ptr(j);
|
|
var q0 = qj.readPointer();
|
|
var lj = qj.add(8).readU64();
|
|
if (!q0.isNull() && lj > 0 && lj < 20000 && isReadable(q0)){
|
|
segs.push({ptr: String(q0), len: Number(lj), head: hexb(q0, 48),
|
|
kind: startsWith(q0, JSON_HDR) ? 'JSON' : (startsWith(q0, MAGIC_HDR) ? 'MAGIC' : 'raw')});
|
|
}
|
|
}
|
|
res.segs = segs;
|
|
return res;
|
|
}
|
|
// 4) raw
|
|
res.kind = 'raw'; res.head = hexb(p, 32) || '';
|
|
return res;
|
|
}catch(e){ return null; }
|
|
}
|
|
|
|
// deep decode: scan 1024B at p for plausible pointers, dump 9000B at each
|
|
function deepDeref(p){
|
|
var out = {reg: 'deep', addr: String(p)};
|
|
var found = [];
|
|
var seen = {};
|
|
try{
|
|
// also dump page-aligned base 12000B
|
|
try{
|
|
var pg = p.and(ptr('0xfffffffffffff000'));
|
|
if (isReadable(pg)){
|
|
found.push({kind: 'page', ptr: String(pg), full: hexb(pg, 12000)});
|
|
seen[String(pg)] = true;
|
|
}
|
|
}catch(e){}
|
|
var base = p.sub(512);
|
|
for (var i = 0; i < 1024; i += 8){
|
|
var v;
|
|
try{ v = base.add(i).readU64(); }catch(e){ continue; }
|
|
var cand = v.and(ptr('0x7fffffffffff'));
|
|
var s = cand.toString();
|
|
if (cand == ptr('0')) continue;
|
|
// heap-ish: 0x7[7-9ca].. or 0xb4.. or 0x77..
|
|
if (!(/^0x(7[78a-c9]|b4)[0-9a-f]{9,}$/.test(s))) continue;
|
|
if (seen[s]) continue;
|
|
if (!isReadable(cand)) continue;
|
|
seen[s] = true;
|
|
var entry = {off: i - 512, ptr: s};
|
|
var kind = 'raw';
|
|
if (startsWith(cand, JSON_HDR)) kind = 'JSON';
|
|
else if (startsWith(cand, MAGIC_HDR)) kind = 'MAGIC';
|
|
entry.kind = kind;
|
|
if (kind === 'JSON' || kind === 'MAGIC'){
|
|
entry.full = hexb(cand, 9000);
|
|
} else {
|
|
entry.head = hexb(cand, 64) || '';
|
|
}
|
|
found.push(entry);
|
|
}
|
|
}catch(e){}
|
|
out.entries = found;
|
|
return out;
|
|
}
|
|
|
|
function hookFn(off, name){
|
|
try{
|
|
var base = Process.getModuleByName('libhydeviceid.so').base;
|
|
Interceptor.attach(base.add(off), {
|
|
onEnter: function(a){
|
|
var ctx = this.context;
|
|
var ev = {fn: name, phase: 'enter', t: Date.now() - t0};
|
|
['x0','x1','x2','x3','x4'].forEach(function(r, i){
|
|
var pr = decodeArg(ctx[r], r);
|
|
if (pr) ev[r] = pr;
|
|
});
|
|
// cryptoA: deep deref x0
|
|
if (name === 'cryptoA_5b1f4'){
|
|
ev.deep = deepDeref(ctx.x0);
|
|
}
|
|
send({type:'pipe', ev: ev});
|
|
}
|
|
});
|
|
send({type:'hooked', name: name});
|
|
}catch(e){ send({type:'hook_fail', name: name, e: String(e)}); }
|
|
}
|
|
|
|
var H = [
|
|
[0x20e924, 'topA_20e924'],
|
|
[0x5a938, 'midA_5a938'],
|
|
[0x5b1f4, 'cryptoA_5b1f4'],
|
|
[0x61098, 'copy_61098'],
|
|
[0x7f744, 'pktA_7f744'],
|
|
[0x92128, 'pktB_92128'],
|
|
];
|
|
var hookedN = 0;
|
|
function attachOne(base, off, name){
|
|
try{
|
|
Interceptor.attach(base.add(off), {
|
|
onEnter: function(a){
|
|
var ctx = this.context;
|
|
var ev = {fn: name, phase: 'enter', t: Date.now() - t0};
|
|
['x0','x1','x2','x3','x4'].forEach(function(r, i){
|
|
var pr = decodeArg(ctx[r], r);
|
|
if (pr) ev[r] = pr;
|
|
});
|
|
// cryptoA: deep deref x0
|
|
if (name === 'cryptoA_5b1f4'){
|
|
ev.deep = deepDeref(ctx.x0);
|
|
}
|
|
// copy_61098 second call (x0=MAGIC): heap scan for JSON + dump 4600B
|
|
if (name === 'copy_61098' && (ev.x0||{}).kind === 'MAGIC'){
|
|
var hits = [];
|
|
try{
|
|
var pat = '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38';
|
|
var ranges = Process.enumerateRanges('rw-');
|
|
for (var ri = 0; ri < ranges.length; ri++){
|
|
var rng = ranges[ri];
|
|
if (rng.size > 0x10000000) continue;
|
|
var m = Memory.scanSync(rng.base, rng.size, pat);
|
|
for (var mi = 0; mi < m.length; mi++){
|
|
var full = hexb(m[mi].address, 4600) || '';
|
|
var h = full.substring(0, 96);
|
|
hits.push({addr: String(m[mi].address), head: h, full: full});
|
|
}
|
|
}
|
|
}catch(e){}
|
|
send({type:'jsonhits', n: hits.length, hits: hits, t: Date.now() - t0});
|
|
}
|
|
send({type:'pipe', ev: ev});
|
|
}
|
|
});
|
|
hookedN++;
|
|
return true;
|
|
}catch(e){ return false; }
|
|
}
|
|
function tryHookAll(){
|
|
try{
|
|
var m = Process.getModuleByName('libhydeviceid.so');
|
|
for (var i=0;i<H.length;i++){
|
|
if (attachOne(m.base, H[i][0], H[i][1])){}
|
|
}
|
|
send({type:'hooked_all', n: hookedN});
|
|
}catch(e){ setTimeout(tryHookAll, 800); }
|
|
}
|
|
setTimeout(tryHookAll, 500);
|
|
|
|
try{
|
|
var r = new ApiResolver('module');
|
|
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
|
|
Interceptor.attach(m.address, {
|
|
onEnter: function(a){
|
|
var len = a[2].toInt32();
|
|
if (len < 100 || len > 20000) return;
|
|
var head = '';
|
|
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
|
|
if (head.indexOf('dfpReport') < 0) return;
|
|
send({type:'wire', len:len, hex:hexb(a[1], len), t:Date.now()-t0});
|
|
}
|
|
});
|
|
});
|
|
send({type:'ssl_hooked'});
|
|
}catch(e){ send({type:'ssl_err', e:String(e)}); }
|
|
"""
|
|
|
|
|
|
def load_with_retry(session, js, n=5):
|
|
last = None
|
|
for i in range(n):
|
|
try:
|
|
s = session.create_script(js)
|
|
s.load()
|
|
return s
|
|
except Exception as e:
|
|
last = e
|
|
print(f"[retry {i}] {e}", flush=True)
|
|
time.sleep(3)
|
|
raise last
|
|
|
|
|
|
def main():
|
|
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
|
|
pid = device.spawn([PACKAGE])
|
|
print(f"[*] spawned {PACKAGE} pid={pid}", flush=True)
|
|
session = device.attach(pid)
|
|
|
|
events = []
|
|
|
|
def on_message(message, data):
|
|
if message.get("type") == "error":
|
|
print("[JS-ERR]", str(message)[:300], flush=True)
|
|
return
|
|
if message.get("type") != "send":
|
|
return
|
|
p = message.get("payload") or {}
|
|
t = p.get("type")
|
|
if t == "armed":
|
|
print("[*] JS armed", flush=True)
|
|
elif t == "hooked_all":
|
|
print(f"[*] hooked {p.get('n')} fns", flush=True)
|
|
elif t == "ssl_hooked":
|
|
print("[*] SSL_write hooked", flush=True)
|
|
elif t == "pipe":
|
|
ev = p.get('ev') or {}
|
|
parts = []
|
|
for r in ['x0','x1','x2','x3','x4']:
|
|
pr = ev.get(r)
|
|
if not pr or not isinstance(pr, dict):
|
|
continue
|
|
k = pr.get('kind')
|
|
if k == 'JSON':
|
|
parts.append(f"{r}=JSON full")
|
|
elif k == 'MAGIC':
|
|
parts.append(f"{r}=MAGIC full")
|
|
elif k == 'seg':
|
|
parts.append(f"{r}=seg({pr.get('ptrlen',{}).get('len')})@{pr.get('ptrlen',{}).get('ptr')}")
|
|
elif k == 'ptrs':
|
|
parts.append(f"{r}=ptrs {pr.get('ptrs')} segs={[(s.get('kind'), s.get('len')) for s in pr.get('segs') or []]}")
|
|
elif k == 'raw':
|
|
parts.append(f"{r}=raw {pr.get('addr')} {pr.get('head','')[:16]}")
|
|
print(f"[{ev.get('t')}ms] {ev.get('fn')}::{ev.get('phase')} " + " ".join(parts), flush=True)
|
|
events.append({"type": "pipe", "ev": ev})
|
|
OUT.write_text(json.dumps(events))
|
|
elif t == "jsonhits":
|
|
print(f"[{p.get('t')}ms] [jsonhits] n={p.get('n')}", flush=True)
|
|
events.append({"type": "jsonhits", "t": p.get('t'), "hits": p.get('hits')})
|
|
OUT.write_text(json.dumps(events))
|
|
elif t == "wire":
|
|
print(f"[{p.get('t')}ms] [wire] len={p.get('len')}", flush=True)
|
|
events.append({"type": "wire", "len": p.get('len'), "t": p.get('t'), "hex": p.get('hex')})
|
|
OUT.write_text(json.dumps(events))
|
|
|
|
# 严格按约定顺序: spawn -> bypass x2 -> resume + sleep 11 -> patch_guard -> 最后主 hook JS
|
|
print("[*] loading bypass", flush=True)
|
|
load_with_retry(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
|
|
load_with_retry(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
|
|
device.resume(pid)
|
|
time.sleep(11)
|
|
load_with_retry(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text())
|
|
print("[*] loading 主 hook JS (最后)", flush=True)
|
|
script = load_with_retry(session, JS)
|
|
script.on("message", on_message)
|
|
print("[*] 已 resume。等待 dfpReport", flush=True)
|
|
try:
|
|
while True:
|
|
time.sleep(5)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
print(f"[*] 结束 -> {OUT}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |