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
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""追查 wupData 信封内 20B nonce 的生成源头。
时序(不可变, 同 hook_cert_keycap): force-stop -> spawn挂起 -> 双bypass ->
resume -> 10s -> patch_guard -> 随机源hooks + Java桥触发 getQUrlData。
命中判定: 窗口内随机源输出与P1中nonce比对 / 直接看SecureRandom调用栈。
"""
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)});
// ---- Java 随机源 ----
Java.perform(function(){
try {
var SR = Java.use('java.security.SecureRandom');
SR.nextBytes.overload('[B').implementation = function(b){
var st = Java.use('android.util.Log')
.getStackTraceString(Java.use('java.lang.Exception').$new());
var r = this.nextBytes(b);
var hex = Array.from(new Uint8Array(b)).map(function(x){
return ('0'+x.toString(16)).slice(-2);}).join('').slice(0,48);
send({type:'jsec', n:b.length, head:hex, stack:st});
return r;
};
} catch(e){ send({type:'hookerr', where:'SecureRandom', msg:''+e}); }
});
// ---- 触发证书生成(getQUrlData 组 P1 含 nonce) ----
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();
var q=inst.getQUrlData(__UID__, "", "");
send({type:'qurl_done', len:q?q.length:0, data:q||''});
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
}
setTimeout(go, 8000);
});
""".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)[:240]); 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]', p['base'])
elif t == 'jsec':
lines = [x.strip() for x in p['stack'].split('\n')]
frames = [x for x in lines if 'huya' in x.lower() or 'udb' in x.lower()
or 'nonce' in x.lower()][:5]
print(f"[SecureRandom] {p['n']}B head={p['head']}")
for f in (frames or lines[1:4]):
print(" ", f[:110])
elif t == 'hookerr':
print('[hookerr]', p)
elif t == 'qurl_done':
print('[qurl_done]', p.get('len'), 'head:', (p.get('data') or '')[:60])
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') == 'qurl_done' for e in events):
time.sleep(8)
break
Path('/tmp/opencode/nonce_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):
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)