动态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)
147 lines
5.0 KiB
Python
147 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""nonce溯源v2: 修正random_device返回值读取 + libc熵源 + 同进程双调用对比。"""
|
|
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)});
|
|
var WIN_ON = false;
|
|
|
|
// ---- random_device::operator() 返回uint32 ----
|
|
try {
|
|
var sym = null;
|
|
Process.getModuleByName('libudbauthunify.so').enumerateSymbols().forEach(function(s){
|
|
if (s.name === '_ZNSt6__ndk113random_deviceclEv') sym = s.address;
|
|
});
|
|
if (sym) Interceptor.attach(sym, {
|
|
onLeave: function(r){
|
|
if (!WIN_ON) return;
|
|
try { send({type:'rd32', v:'0x'+r.toInt32().toString(16), bt:DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,60)}); } catch(e){}
|
|
}
|
|
});
|
|
} catch(e){ send({type:'hookerr', where:'rd_op', msg:''+e}); }
|
|
|
|
// ---- libc 熵源 ----
|
|
['getrandom','getentropy','arc4random_buf'].forEach(function(fn){
|
|
var p = Module.findExportByName('libc.so', fn);
|
|
if (!p) return;
|
|
Interceptor.attach(p, {
|
|
onEnter: function(a){ this.buf=a[0]||a[1]; this.n=a[1] ? a[1].toInt32() : a[0].toInt32(); },
|
|
onLeave: function(_){
|
|
if (!WIN_ON || this.n<=0 || this.n>64) return;
|
|
try {
|
|
var b = Memory.readByteArray(this.buf, Math.min(this.n,32));
|
|
send({type:'libc', fn:fn, n:this.n,
|
|
hex:Array.from(new Uint8Array(b)).map(function(x){return ('0'+x.toString(16)).slice(-2)}).join('')});
|
|
} catch(e){}
|
|
}
|
|
});
|
|
});
|
|
var rd = Module.findExportByName('libc.so','read');
|
|
Interceptor.attach(rd, {onEnter:function(a){ this.fd=a[0].toInt32(); this.buf=a[1]; this.n=a[2].toInt32(); },
|
|
onLeave:function(_){ if (!WIN_ON) return; }});
|
|
|
|
// ---- 双调用触发 ----
|
|
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();
|
|
WIN_ON = true;
|
|
send({type:'stage', msg:'call #1'});
|
|
var q1=inst.getQUrlData(__UID__, "", "");
|
|
send({type:'qurl_done', idx:1, data:q1||''});
|
|
var q2=inst.getQUrlData(__UID__, "", "");
|
|
WIN_ON = false;
|
|
send({type:'qurl_done', idx:2, data:q2||''});
|
|
}catch(e){ n+=1; WIN_ON=false; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,90)}); setTimeout(go,5000); }
|
|
}
|
|
setTimeout(go, 20000);
|
|
});
|
|
""".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')
|
|
events.append(p)
|
|
if t == 'armed':
|
|
got_armed.append(1); print('[armed]')
|
|
elif t == 'rd32':
|
|
print(f"[rd32] {p['v']} <- {p['bt'][:50]}")
|
|
elif t == 'libc':
|
|
print(f"[{p['fn']}] n={p['n']} {p['hex'][:40]}")
|
|
elif t == 'stage':
|
|
print('[stage]', p['msg'])
|
|
elif t == 'qurl_done':
|
|
print(f"[qurl#{p.get('idx')}] len={p.get('len')} head={(p.get('data') or '')[:40]}")
|
|
elif t == 'retry':
|
|
print('[retry]', p.get('n'))
|
|
|
|
sc.on('message', on_msg)
|
|
sc.load()
|
|
assert got_armed
|
|
|
|
deadline = time.time() + 100
|
|
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_v2.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)
|