263 lines
10 KiB
Python
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()
|