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)
This commit is contained in:
yml2213
2026-08-26 00:49:01 +08:00
parent da9937707c
commit 97e6d14139
16 changed files with 1831 additions and 127 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""k1 账号绑定判定: 对多个不同 uid 调 getQUrlData, 看 xxtea key 是否变化。
若 k1(=this+0x10, MD5输入的一半)随 uid 变 -> 账号绑定, 纯协议死路;
若 k1 不变 -> 设备级常量, 任意账号可本地复算 nonce, 纯协议成立!
同时验证 serviceTime/nonce_next 的生成与 uid 无关。
"""
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")
UIDS = [1199666914671, 1199666911746] # 可信账号 + 另一账号(新账号uid可能不存在, 但getQUrlData只看参数)
def build_js(uids):
arr = ",".join(str(u) for u in uids)
return 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 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; }
// gen_biz_token(struct, uid, k1, k2, out)
try { Interceptor.attach(base.add(0x3307e4), {
onEnter: function(a){
send({type:'gbt_in', struct:hx_s(a[0],0x60), uid:rdStr(a[1]), k1:rdStr(a[2]), k2:rdStr(a[3])});
}
}); } catch(e){ send({type:'hookerr',where:'gen_biz_token',msg:''+e}); }
// xxtea
try { Interceptor.attach(base.add(0x32e83c), {
onEnter: function(a){
this.out=a[0];
send({type:'xxtea_in', data:rdStr(a[1]), key:rdStr(a[2]),
key_ascii:ascii(rdStr(a[2]).v)});
},
onLeave: function(){ var o=rdStr(this.out); send({type:'xxtea_out', out:o}); }
}); } catch(e){ send({type:'hookerr',where:'xxtea',msg:''+e}); }
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){}
function hx_s(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
// 依次触发多个 uid
Java.perform(function(){
var uids = __UIDS__;
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();
for (var i=0;i<uids.length;i++){
var uid=uids[i];
send({type:'stage', msg:'call uid='+uid});
var q=inst.getQUrlData(uid, "", "");
send({type:'qurl_done', uid:uid, len:q?q.length:0, data:q||''});
}
send({type:'stage', msg:'ALL DONE'});
}catch(e){ n+=1; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,120)}); setTimeout(go,5000); }
}
setTimeout(go, 16000);
});
""".replace('__UIDS__', json.dumps(uids))
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(build_js(UIDS))
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')
events.append(p)
if t == 'armed':
got_armed.append(1); print('[armed]')
elif t == 'gbt_in':
print(f"[gbt] uid={ascii(p['uid']['v'])} k1={p['k1']['v'][:40]}({p['k1']['len']}) k2={p['k2']['v'][:24]}")
elif t == 'xxtea_in':
print(f"[xxtea] data={p['data']['v'][:40]} key_ascii={p['key_ascii'][:60]}")
elif t == 'xxtea_out':
print(f"[xxtea.out] {p['out']['v'][:44]}")
elif t == 'nonce_next':
print(f"[nonce] m={p['m']} ret={p['ret']}")
elif t == 'qurl_done':
print(f"[qurl uid={p['uid']}] len={p['len']}")
elif t == 'stage':
print('[stage]', p['msg'])
sc.on('message', on_msg)
sc.load()
assert got_armed
deadline = time.time() + 120
while time.time() < deadline:
time.sleep(2)
if any(e.get('type') == 'stage' and e.get('msg') == 'ALL DONE' for e in events):
time.sleep(3)
break
Path('/tmp/opencode/k1_multi.json').write_text(json.dumps(events))
print('saved /tmp/opencode/k1_multi.json')
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)