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