动态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)
209 lines
7.3 KiB
Python
209 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""native侧追查nonce生成: 枚举libudbauthunify.so符号 + 窗口内hook sha/hmac/rand。
|
|
|
|
时序(不可变): force-stop -> spawn挂起 -> 双bypass -> resume -> 10s -> patch_guard
|
|
-> 枚举符号 -> hook命中函数 -> Java桥触发getQUrlData -> 收集调用与nonce比对。
|
|
"""
|
|
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
|
|
|
|
ENUM_JS = r"""
|
|
'use strict';
|
|
var m = Process.getModuleByName('libudbauthunify.so');
|
|
send({type:'info', base:String(m.base), size:m.size});
|
|
var pat = /sha1|sha-1|hmac|rand|urandom|sign|digest|nonce|md5|aes_key/i;
|
|
var syms = m.enumerateSymbols().filter(function(s){
|
|
return s.name && pat.test(s.name);
|
|
}).map(function(s){ return {name:s.name, addr:String(s.address)}; });
|
|
var exps = m.enumerateExports().filter(function(e){ return pat.test(e.name); })
|
|
.map(function(e){ return {name:e.name, addr:String(e.address)}; });
|
|
send({type:'syms', syms:syms, exps:exps});
|
|
"""
|
|
|
|
HOOK_JS_TPL = 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;
|
|
|
|
function rdStr(p){
|
|
try{
|
|
var b0=p.readU8();
|
|
if((b0&1)===0){ var l=b0>>1; if(l>4096) 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 bt(ctx){ return Thread.backtrace(ctx, Backtracer.ACCURATE).map(function(a){
|
|
return DebugSymbol.fromAddress(a).toString().slice(0,110); }).slice(0,6); }
|
|
|
|
// __HITS__: [ [addrStr, name], ... ]
|
|
var hits = __HITS__;
|
|
hits.forEach(function(h){
|
|
try {
|
|
Interceptor.attach(ptr(h[0]), {
|
|
onEnter: function(a){
|
|
if (!WIN_ON) return;
|
|
this.a = [a[0],a[1],a[2],a[3]];
|
|
this.bt = bt(this.context);
|
|
},
|
|
onLeave: function(r){
|
|
if (!WIN_ON) return;
|
|
send({type:'hit', name:h[1].slice(0,90),
|
|
args:[rdStr(this.a[0]), rdStr(this.a[1])],
|
|
ret:rdStr(r), bt:this.bt});
|
|
}
|
|
});
|
|
send({type:'hooked', name:h[1].slice(0,80)});
|
|
} catch(e){ send({type:'hookerr', where:h[1], 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();
|
|
WIN_ON = true;
|
|
send({type:'stage', msg:'window ON'});
|
|
var q=inst.getQUrlData(__UID__, "", "");
|
|
WIN_ON = false;
|
|
send({type:'qurl_done', len:q?q.length:0, data:q||''});
|
|
}catch(e){ n+=1; window.on=false; if(n%4===0) send({type:'retry',n:n}); setTimeout(go,5000); }
|
|
}
|
|
setTimeout(go, 6000);
|
|
});
|
|
""".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')
|
|
|
|
# 1) 枚举符号
|
|
sc0 = s.create_script(ENUM_JS)
|
|
out = {}
|
|
def on0(m, _):
|
|
if m.get('type') == 'send':
|
|
p = m['payload']
|
|
if p.get('type') in ('info', 'syms'):
|
|
out[p['type']] = p
|
|
sc0.on('message', on0)
|
|
sc0.load()
|
|
time.sleep(3)
|
|
info = out['info']
|
|
syms = out['syms']['syms'] + out['syms']['exps']
|
|
print(f"base={info['base']} size={info['size']} 命中符号={len(syms)}")
|
|
for x in syms[:30]:
|
|
print(" ", x['name'][:100])
|
|
|
|
# 2) 选 hook 目标: 定向 SHA1/md5/random_device, 上限12个
|
|
def want(nm):
|
|
ln = nm.lower()
|
|
return ('sha_go' in ln or 'shainit' in ln or 'adddatalen' in ln or
|
|
'random_device' in ln or 'md5_char' in ln or
|
|
'huyamd5' in ln and ('digest' in ln or 'tostring' in ln))
|
|
picks = []
|
|
seen = set()
|
|
for x in syms:
|
|
nm = x['name']
|
|
off = int(x['addr'], 16) - int(info['base'], 16)
|
|
if off <= 0 or off > info['size']:
|
|
continue
|
|
if want(nm) and nm not in seen:
|
|
seen.add(nm)
|
|
picks.append([x['addr'], nm])
|
|
if len(picks) >= 12:
|
|
break
|
|
print("hook目标:", [p[1][:60] for p in picks])
|
|
|
|
# 3) 装钩 + 触发
|
|
js = HOOK_JS_TPL.replace('__HITS__', json.dumps(picks))
|
|
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 == 'hooked':
|
|
print('[hooked]', p['name'][:70])
|
|
elif t == 'hookerr':
|
|
print('[hookerr]', p)
|
|
elif t == 'hit':
|
|
a0, a1 = p['args'][0], p['args'][1]
|
|
print(f"\n[HIT] {p['name'][:80]}")
|
|
print(f" arg0={a0['t']}:{a0['len']} {a0['v'][:64]}")
|
|
print(f" arg1={a1['t']}:{a1['len']} {a1['v'][:64]}")
|
|
print(f" ret={p['ret']['t']}:{p['ret']['len']} {p['ret']['v'][:64]}")
|
|
for f in p['bt'][:4]:
|
|
print(" <", f)
|
|
elif t == 'stage':
|
|
print('[stage]', p['msg'])
|
|
elif t == 'qurl_done':
|
|
print('[qurl_done]', p.get('len'), (p.get('data') or '')[:80])
|
|
|
|
sc.on('message', on_msg)
|
|
sc.load()
|
|
assert got_armed
|
|
|
|
deadline = time.time() + 90
|
|
while time.time() < deadline:
|
|
time.sleep(2)
|
|
if any(e.get('type') == 'qurl_done' for e in events):
|
|
time.sleep(5)
|
|
break
|
|
Path('/tmp/opencode/nonce_native.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)
|