Files
live-hub-py/scripts/hook_nonce_chain.py
T
yml2213 97e6d14139 feat(huya): nonce生成机制完全破解 - 任意账号零设备出完整CK闭环
动态hook实锤(scripts/hook_nonce_chain.py):
  rnd = XXTEA(pack(st) + pack(counter|st<<16), key=MD5hex(uid_str + k1)[:16])
  - st=毫秒时间戳, counter同st内递增
  - k1=设备常量865a4924...(不随账号变, evidence/nonce_k1.json)
  - Python复刻(tools/nonce_forge.py)对设备两轮rnd逐字节MATCH

推翻v2误判(X2'nonce账号绑定令牌'实为nonce内编码uid):
  - 换账号用目标uid重算nonce而非复用旧nonce!
  - 纯随机nonce必40020因解不出合法st/counter结构

实测: 同账号+跨账号(hy_300024708从未设备登录)本地nonce铸证bind均通过,
  full_web_cookie/full_auto_safe 任意账号密码->全套cookie(udb_cred/udb_biztoken)
2026-08-26 00:49:01 +08:00

254 lines
12 KiB
Python

#!/usr/bin/env python3
"""nonce链路动态hook —— 与静态反汇编互相验证。
抓取 getQUrlData 触发证书生成时的完整运行时实参链:
nonce_next(m) -> 64位OTP计数
getServiceTime() -> 服务时间
hyudb_crypt_util::xxtea_encrypt(out, data, key) -> 20B rnd 的直接来源
enpack_header(h, key, out) -> cert 头 [0x0c][key_idx]
enpack_body(h, a,b,c,d, out) -> P1 明文组装
gen_biz_token(struct, uid, k1, k2, out) -> 总装
encode_aes(p1, key, out) -> AES加密
getOtpEx(uid, idx, a3, out) -> 入口
目的: 拿到 xxtea 的 data/key 原文与输出, 验证 20B rnd 是否 = f(serviceTime, nonce, uid, 设备常量),
从而判定"信封nonce能否脱离设备本地复算"。
时序(不可变, 同 hook_cert_keycap): force-stop -> spawn挂起 -> 双bypass -> resume ->
10s -> patch_guard -> 装hook -> Java桥触发 getQUrlData。
"""
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")
UID = 1199666914671
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 rdStr(p){
try{
var b0=p.readU8();
if((b0&1)===0){ var l=b0>>1; if(l>8192) return {t:'s?',len:l,v:''};
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
var len=parseInt(p.add(8).readU64().toString());
if(len>65536) return {t:'l?',len:len,v:''};
var dp=p.add(16).readPointer();
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
}
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 ascii(h){ if(!h) return ''; var s=''; for(var i=0;i<h.length;i+=2){ var c=parseInt(h.substr(i,2),16); s += (c>=32&&c<127)?String.fromCharCode(c):'.'; } return s; }
function bt(ctx){ return Thread.backtrace(ctx, Backtracer.ACCURATE).map(function(a){
return DebugSymbol.fromAddress(a).toString().slice(0,90); }).slice(0,5); }
// ---------- 1) nonce_next(m) ----------
try { Interceptor.attach(base.add(0x32ff10), {
onEnter: function(a){ this.m=a[0]; },
onLeave: function(r){ send({type:'nonce_next', m:'0x'+this.m.toString(16), ret:'0x'+r.toString(16)}); }
}); } catch(e){ send({type:'hookerr',where:'nonce_next',msg:''+e}); }
// ---------- 2) getServiceTime() ----------
try { Interceptor.attach(base.add(0x268070), {
onLeave: function(r){ send({type:'service_time', v:'0x'+r.toString(16), dec:r.toString()}); }
}); } catch(e){ send({type:'hookerr',where:'getServiceTime',msg:''+e}); }
// ---------- 3) hyudb_crypt_util::xxtea_encrypt(out&, data, key) ----------
try { Interceptor.attach(base.add(0x32e83c), {
onEnter: function(a){
this.out=a[0]; this.d=rdStr(a[1]); this.k=rdStr(a[2]);
send({type:'xxtea_in', data:this.d, key:this.k,
data_ascii:ascii(this.d.v), key_ascii:ascii(this.k.v)});
},
onLeave: function(r){ var o=rdStr(this.out); send({type:'xxtea_out', out:o, out_ascii:ascii(o.v)}); }
}); } catch(e){ send({type:'hookerr',where:'xxtea_encrypt',msg:''+e}); }
// ---------- 4) enpack_header(h, NEWKEY, out&) ----------
try { Interceptor.attach(base.add(0x32ff44), {
onEnter: function(a){ this.out=a[2]; this.h=a[0]; this.k=a[1]; },
onLeave: function(r){ var o=rdStr(this.out);
send({type:'enpack_header', h:'0x'+this.h.toInt32().toString(16), key:'0x'+this.k.toInt32().toString(16), out:o}); }
}); } catch(e){ send({type:'hookerr',where:'enpack_header',msg:''+e}); }
// ---------- 5) enpack_body(h, a,b,c,d, out&) ----------
try { Interceptor.attach(base.add(0x3300c8), {
onEnter: function(a){
this.out=a[5]; this.h=a[0];
this.a=rdStr(a[1]); this.b=rdStr(a[2]); this.c=rdStr(a[3]); this.d=rdStr(a[4]);
send({type:'enpack_body_in', h:'0x'+this.h.toInt32().toString(16),
a:this.a, b:this.b, c:this.c, d:this.d,
a_ascii:ascii(this.a.v).slice(0,80), c_ascii:ascii(this.c.v).slice(0,80)});
},
onLeave: function(r){ var o=rdStr(this.out); send({type:'enpack_body_out', out:o}); }
}); } catch(e){ send({type:'hookerr',where:'enpack_body',msg:''+e}); }
// ---------- 6) gen_biz_token(struct, uid, k1, k2, out&) ----------
try { Interceptor.attach(base.add(0x3307e4), {
onEnter: function(a){
this.out=a[4];
// BIZTOKEN struct 前0x60字节: [0]=type [1]=key_idx [2]=1 [8..]=str1 [0x20]=st [0x28]=nonce [0x30]=str2 [0x48]=str3
send({type:'gbt_in', struct:hx(a[0],0x60),
struct_ascii:ascii(hx(a[0],0x60)),
uid:rdStr(a[1]), k1:rdStr(a[2]), k2:rdStr(a[3]),
k1_ascii:ascii(rdStr(a[2]).v).slice(0,80), k2_ascii:ascii(rdStr(a[3]).v).slice(0,80)});
},
onLeave: function(r){ var o=rdStr(this.out); send({type:'gbt_out', out:o}); }
}); } catch(e){ send({type:'hookerr',where:'gen_biz_token',msg:''+e}); }
// ---------- 7) encode_aes(p1, key, out&) ----------
try { Interceptor.attach(base.add(0x330218), {
onEnter: function(a){ this.out=a[2]; this.p1=rdStr(a[0]); this.key=rdStr(a[1]); },
onLeave: function(r){ var o=rdStr(this.out);
send({type:'encode_aes', p1:this.p1, key:this.key, key_ascii:ascii(this.key.v).slice(0,60), out:o}); }
}); } catch(e){ send({type:'hookerr',where:'encode_aes',msg:''+e}); }
// ---------- 8) getOtpEx(uid, idx, a3, out&) ----------
try { Interceptor.attach(base.add(0x2681b4), {
onEnter: function(a){ this.out=a[4]; this.uid=a[1]; this.idx=a[2]; this.a3=rdStr(a[3]); },
onLeave: function(r){ var o=rdStr(this.out);
send({type:'getOtpEx', uid:'0x'+this.uid.toString(16), idx:this.idx.toInt32(), a3:this.a3, out:o}); }
}); } catch(e){ send({type:'hookerr',where:'getOtpEx',msg:''+e}); }
// ---------- 9) AESkeyMgr::getkey ----------
try { Interceptor.attach(base.add(0x454350), {
onEnter: function(a){ this.a0=a[0]; this.i1=a[1]; this.i2=a[2]; },
onLeave: function(r){
// getkey 返回 std::string (sret in x8), 但 onLeave x8 不可靠; 读 x0 若为短串
try { var s=rdStr(this.a0.add(0)); if(s.len>0 && s.len<64) send({type:'getkey', i1:this.i1.toInt32(), i2:this.i2.toInt32(), key:s}); }
catch(e){}
}
}); } catch(e){ send({type:'hookerr',where:'getkey',msg:''+e}); }
// ---------- 触发 ----------
Java.perform(function(){
var n=0;
function go(){
try{
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F=Java.ClassFactory.get(seed.class.getClassLoader());
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
send({type:'stage', msg:'call #1'});
var q1=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', idx:1, len:q1?q1.length:0, data:q1||''});
var q2=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', idx:2, len:q2?q2.length:0, data:q2||''});
}catch(e){ n+=1; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,120)}); setTimeout(go,5000); }
}
setTimeout(go, 18000);
});
""".replace('__UID__', str(UID))
def main():
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
pid = d.spawn(['com.duowan.kiwi'])
print(f'spawned {pid}')
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)
print('resumed')
time.sleep(11)
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
print('patch_guard on')
sc = s.create_script(JS)
events = []
got_armed = []
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')
if t != 'retry':
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'nonce_next':
print(f"[nonce_next] m={p['m']} ret={p['ret']}")
elif t == 'service_time':
print(f"[service_time] {p['v']} ({p['dec']})")
elif t == 'xxtea_in':
print(f"[xxtea.in] data len={p['data']['len']} {p['data']['v'][:40]}")
print(f" key len={p['key']['len']} {p['key']['v'][:60]} ascii={p['key_ascii'][:50]}")
elif t == 'xxtea_out':
print(f"[xxtea.out] len={p['out']['len']} {p['out']['v'][:48]}")
elif t == 'enpack_header':
print(f"[enpack_header] h={p['h']} key={p['key']} out len={p['out']['len']} {p['out']['v'][:20]}")
elif t == 'enpack_body_in':
print(f"[enpack_body] h={p['h']}")
print(f" a(len{p['a']['len']})={p['a']['v'][:60]} ascii={p['a_ascii']}")
print(f" b(len{p['b']['len']})={p['b']['v'][:48]}")
print(f" c(len{p['c']['len']})={p['c']['v'][:60]} ascii={p['c_ascii']}")
print(f" d(len{p['d']['len']})={p['d']['v'][:24]}")
elif t == 'enpack_body_out':
print(f"[enpack_body.out] len={p['out']['len']} {p['out']['v'][:80]}")
elif t == 'gbt_in':
print(f"[gen_biz_token.in] struct={p['struct'][:100]}")
print(f" uid(len{p['uid']['len']})={p['uid']['v'][:40]}")
print(f" k1(len{p['k1']['len']})={p['k1']['v'][:60]} ascii={p['k1_ascii']}")
print(f" k2(len{p['k2']['len']})={p['k2']['v'][:60]} ascii={p['k2_ascii']}")
elif t == 'gbt_out':
print(f"[gen_biz_token.out] len={p['out']['len']} {p['out']['v'][:60]}")
elif t == 'encode_aes':
print(f"[encode_aes] p1 len={p['p1']['len']} {p['p1']['v'][:70]}")
print(f" key={p['key_ascii'][:50]} out len={p['out']['len']}")
elif t == 'getOtpEx':
print(f"[getOtpEx] uid={p['uid']} idx={p['idx']} a3(len{p['a3']['len']}) out len={p['out']['len']}")
elif t == 'getkey':
print(f"[getkey] i1={p['i1']} i2={p['i2']} key={p['key']['v'][:40]} ascii={ascii_short(p['key']['v'])}")
elif t == 'qurl_done':
print(f"[qurl#{p['idx']}] len={p['len']} head={(p.get('data') or '')[:40]}")
elif t == 'stage':
print('[stage]', p['msg'])
def ascii_short(h):
s = ''
for i in range(0, len(h), 2):
c = int(h[i:i+2], 16)
s += chr(c) if 32 <= c < 127 else '.'
return s[:40]
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 120
while time.time() < deadline:
time.sleep(2)
if sum(1 for e in events if e.get('type') == 'qurl_done') >= 2:
time.sleep(5)
break
Path('/tmp/opencode/nonce_chain.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(3):
print(f'===== 尝试 #{attempt+1} =====')
try:
main()
break
except frida.InvalidOperationError as e:
print(f'session detached: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)
except Exception as e:
print(f'err: {e}')
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
time.sleep(3)