Files
live-hub-py/scripts/hook_dfp_capture.py
T

263 lines
10 KiB
Python

#!/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()