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