#!/usr/bin/env python3 """抓 hyCred 签发链: 挂 saveLoginData(落盘) + LoginCred handler(vtable探测)。 用法: 脚本跑起来后, 在手机上 手动退出登录 -> 重新登录 一次。 捕获: 登录态JSON明文(含cred字段) / saveLoginData 入参 / LoginCred handler 触发。 """ from __future__ import annotations import json import subprocess import time from pathlib import Path import frida RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0") JS = r""" Process.setExceptionHandler(function(d){ send({type:'segv',info:{t:d.type}}); return true; }); var base = Process.getModuleByName('libudbauthunify.so').base; 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>1048576) return {len:l,v:''}; return {len:l,v:l?hx(p.add(1),Math.min(l,8192)):''}; } var len=parseInt(p.add(8).readU64().toString()); if(len>2097152) return {len:len,v:''}; return {len:len,v:hx(p.add(16).readPointer(),Math.min(len,8192))}; }catch(e){ return {len:-1,v:String(e)}; } } function rdC(p){ try{ var t=p.readCString(); if(t===null||t.length>30000) return {len:-1,v:''}; var bin=false; for(var i=0;i13&&c<32)){bin=true;break;}} return {len:t.length, bin:bin, v: bin? hx(p,Math.min(t.length,512)) : t.slice(0,4000)}; }catch(e){ return {len:-1,v:String(e)}; } } [0x24ab68, 0x24ab7c].forEach(function(off){ Interceptor.attach(base.add(off), { onEnter: function(a){ var c1=rdC(a[2]), c2=rdC(a[3]); if(c1.len<=0 && c2.len<=0) return; send({type:'sendmsg', off:off, a1:String(a[1]), s1:c1, s2:c2}); } }); }); // 收包处理: MsgLoop::doMSG(UdbMsgBase*, char*) Interceptor.attach(base.add(0x245240), { onEnter: function(a){ var mb=a[1]; var head=hx(mb,64); send({type:'domsg', base_head:head, p:rdStr(a[2])}); } }); "" 'use strict'; Process.setExceptionHandler(function(d){ send({type:'segv',info:{t:d.type,a:String(d.address)}}); return true; }); var base = Process.getModuleByName('libudbauthunify.so').base; 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>262144) return {t:'s?',len:l,v:''}; return {t:'s',len:l,v:l?hx(p.add(1),Math.min(l,4096)):''}; } 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,4096))}; }catch(e){ return {t:'err',len:-1,v:String(e)}; } } function ra(ctx){ try{ return DebugSymbol.fromAddress(ctx.returnAddress).toString().slice(0,70);}catch(e){return '?';} } // 登录态JSON的AES加密(含cred字段) —— 已知走 UdbAESUtil::encrypt Interceptor.attach(base.add(0x250038), { onEnter: function(a){ this.x1=rdStr(a[1]); this.x2=rdStr(a[2]); }, onLeave: function(){ if(this.x1.len>800||this.x2.len>800) send({type:'bigaes', x1:{len:this.x1.len}, x2:this.x2, ra:ra(this)}); } }); // saveLoginData 全量版: bean dump 768B + backtrace 10帧 Interceptor.attach(base.add(0x265fb0), { onEnter: function(a){ send({type:'saveLD_str', str:rdStr(a[1]), bean_head:hx(a[2],768), bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,10) .map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,64);})}); } }); Interceptor.attach(base.add(0x265a6c), { onEnter: function(a){ send({type:'saveLD_bean', bean_head:hx(a[1],768), flag:a[2].toInt32(), bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,10) .map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,64);})}); } }); // cred 打包/解包(网络侧明文必经) Interceptor.attach(base.add(0x333170), { onEnter: function(a){ send({type:'cred_unpack', in:rdStr(a[1]), ra:ra(this)}); } }); // HandlerRequestLoginCred / HandlerResponseLoginCred vtable 探测: // vtable+0x10 起为虚函数槽, 逐个attach前3个槽, 打印触发与backtrace首帧 function probeVtable(name, vtAddr){ for(var slot=0; slot<4; slot++){ (function(slot){ try{ var fp = vtAddr.add(0x10 + slot*8).readPointer(); if(fp.compare(base) < 0 || fp.compare(base.add(0x480000)) > 0) return; Interceptor.attach(fp, { onEnter: function(a){ send({type:'vtable', name:name, slot:slot, a0:a[0].toString(), a1:rdStr(a[1]), a2:rdStr(a[2]), bt:Thread.backtrace(this.context, Backtracer.FUZZY).slice(0,3) .map(function(x){return DebugSymbol.fromAddress(x).toString().slice(0,60);})}); } }); send({type:'vt_attached', name:name, slot:slot, fp:String(fp.sub(base))}); }catch(e){} })(slot); } } probeVtable('ReqLoginCred', base.add(0x477f80)); probeVtable('RespLoginCred', base.add(0x478018)); probeVtable('GetCred', base.add(0x477000)); // 占位: 若无效仅跳过 """ def main(): d = frida.get_device_manager().add_remote_device('127.0.0.1:31877') pid = d.spawn(['com.duowan.kiwi']) s = d.attach(pid) s.create_script((RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load() s.create_script((RE / "evidence/scripts/mask_frida_maps_only.js").read_text()).load() d.resume(pid) time.sleep(11) s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load() print('armed. >>> 请在手机上退出登录并重新登录 <<<') sc = s.create_script(JS) events = [] def on_msg(m, _): if m.get('type') == 'error': print('JS ERR:', str(m)[:200]); return if m.get('type') != 'send': return p = m.get('payload') or {} t = p.get('type') events.append(p) if t == 'vt_attached': print(f"[vt] {p['name']} slot{p['slot']} @+{p['fp']}") elif t == 'saveLD_str': print(f"[saveLD_str] str={p['str']['t']}:{p['str']['len']}\n bean={p['bean_head']}\n bt={p['bt']}") elif t == 'saveLD_bean': print(f"[saveLD_bean] flag={p['flag']}\n bean={p['bean_head']}\n bt={p['bt']}") elif t == 'cred_unpack': print(f"[cred_unpack] in={p['in']['t']}:{p['in']['len']}:{p['in']['v'][:160]} <- {p['ra']}") elif t == 'bigaes': print(f"[bigaes] len={p['x2']['len']} head={p['x2']['v'][:120]}") elif t == 'vtable': print(f"[VT!] {p['name']}#{p['slot']} a1={p['a1']} bt={p['bt']}") sc.on('message', on_msg) sc.load() deadline = time.time() + 300 while time.time() < deadline: time.sleep(3) if any(e.get('type') == 'saveLD_bean' for e in events): time.sleep(10) break Path('/Users/yml/codes/douyu_login_py/evidence/cred_issue_trace.json').write_text(json.dumps(events)) from collections import Counter print('saved:', Counter(e.get('type') for e in events)) if __name__ == '__main__': for attempt in range(4): try: main() break except frida.InvalidOperationError as e: print('detached:', e) subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi']) time.sleep(3)