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:
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""定位 k1 存储: hook getInstance(0x281270) 拿单例, 读 +0x10; 内存搜 k1 出现位置看周边结构。"""
|
||||
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")
|
||||
K1 = "865a4924a40897ac1fcfe6b4c2cbb0e3"
|
||||
|
||||
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)});
|
||||
|
||||
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)}; }
|
||||
}
|
||||
|
||||
// 1) getInstance -> 单例地址 + 读 +0x10
|
||||
try {
|
||||
Interceptor.attach(base.add(0x281270), {
|
||||
onLeave: function(r){
|
||||
var s = rdStr(r.add(0x10));
|
||||
send({type:'cfg', addr:String(r), k1:s, k1ascii:(function(h){var o='';for(var i=0;i<h.length;i+=2){var c=parseInt(h.substr(i,2),16);o+=(c>=32&&c<127)?String.fromCharCode(c):'.';}return o;})(s.v)});
|
||||
// dump 前 0x60 成员指针(可能含其它 string)
|
||||
try {
|
||||
var hex='';
|
||||
for (var off=0; off<0x60; off+=8) hex += hx(r.add(off),8);
|
||||
send({type:'cfg_dump', hex:hex});
|
||||
} catch(e){}
|
||||
}
|
||||
});
|
||||
} catch(e){ send({type:'hookerr', where:'getInstance', msg:''+e}); }
|
||||
|
||||
// 2) hook loadLoginData(未知地址) 跳过; 直接定时 dump
|
||||
setTimeout(function(){
|
||||
try {
|
||||
// 找已缓存的单例: getInstance 是懒加载, 定时再调一次读
|
||||
var f = new NativeFunction(base.add(0x281270), 'pointer', []);
|
||||
var inst = f();
|
||||
var s = rdStr(inst.add(0x10));
|
||||
send({type:'tick_k1', addr:String(inst), k1:s});
|
||||
} catch(e){ send({type:'tick_err', msg:''+e}); }
|
||||
}, 30000);
|
||||
|
||||
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'; } }
|
||||
"""
|
||||
|
||||
|
||||
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')
|
||||
events.append(p)
|
||||
if t == 'armed':
|
||||
got_armed.append(1); print('[armed]')
|
||||
elif t == 'cfg':
|
||||
print(f"[cfg] addr={p['addr']} k1len={p['k1']['len']} k1ascii={p['k1ascii']}")
|
||||
elif t == 'cfg_dump':
|
||||
print(f"[cfg_dump] {p['hex']}")
|
||||
elif t == 'tick_k1':
|
||||
print(f"[tick_k1] addr={p['addr']} k1={p['k1']['v'][:40]}({p['k1']['len']})")
|
||||
elif t == 'tick_err':
|
||||
print('[tick_err]', p['msg'][:120])
|
||||
elif t == 'hookerr':
|
||||
print('[hookerr]', p)
|
||||
|
||||
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') == 'tick_k1' for e in events):
|
||||
time.sleep(3)
|
||||
break
|
||||
Path('/tmp/opencode/k1_cfg.json').write_text(json.dumps(events))
|
||||
print('saved /tmp/opencode/k1_cfg.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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""追踪 k1(this+0x10)写入者: hook std::string::operator=(0x3e6464) + getInstance(0x281270)。"""
|
||||
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")
|
||||
|
||||
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 cfgAddr = null;
|
||||
|
||||
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)}; }
|
||||
}
|
||||
|
||||
// getInstance @0x281270 -> 单例
|
||||
try {
|
||||
Interceptor.attach(base.add(0x281270), {
|
||||
onLeave: function(r){
|
||||
cfgAddr = r;
|
||||
var s = rdStr(r.add(0x10));
|
||||
send({type:'cfg', addr:String(r), k1v:s.v, k1len:s.len});
|
||||
}
|
||||
});
|
||||
} catch(e){ send({type:'hookerr', where:'getInstance', msg:''+e}); }
|
||||
|
||||
// operator= @0x3e6464: a[0]=目标, a[1]=源
|
||||
try {
|
||||
Interceptor.attach(base.add(0x3e6464), {
|
||||
onEnter: function(a){
|
||||
if (!cfgAddr) return;
|
||||
if (a[0].equals(cfgAddr.add(0x10))) {
|
||||
var v = rdStr(a[1]);
|
||||
send({type:'write_k1', val:v.v, len:v.len,
|
||||
bt:Thread.backtrace(this.context, Backtracer.ACCURATE).map(function(x){
|
||||
return DebugSymbol.fromAddress(x).toString().slice(0,90);}).slice(0,5)});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(e){ send({type:'hookerr', where:'op=', msg:''+e}); }
|
||||
|
||||
setTimeout(function(){
|
||||
if (cfgAddr) {
|
||||
var s = rdStr(cfgAddr.add(0x10));
|
||||
send({type:'final_k1', k1:s.v, len:s.len});
|
||||
} else {
|
||||
send({type:'final_k1', k1:'NO_CFG'});
|
||||
}
|
||||
}, 90000);
|
||||
"""
|
||||
|
||||
|
||||
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')
|
||||
events.append(p)
|
||||
if t == 'armed':
|
||||
got_armed.append(1); print('[armed]')
|
||||
elif t == 'cfg':
|
||||
print(f"[cfg] addr={p['addr']} k1len={p['k1len']}")
|
||||
elif t == 'write_k1':
|
||||
print(f"[WRITE k1] len={p['len']} val={p['val'][:60]}")
|
||||
for f in p['bt']:
|
||||
print(" <", f)
|
||||
elif t == 'final_k1':
|
||||
print(f"[FINAL k1] len={p['len']} {p['k1'][:40]}")
|
||||
elif t == 'hookerr':
|
||||
print('[hookerr]', p)
|
||||
|
||||
sc.on('message', on_msg)
|
||||
sc.load()
|
||||
assert got_armed
|
||||
|
||||
deadline = time.time() + 150
|
||||
while time.time() < deadline:
|
||||
time.sleep(2)
|
||||
if any(e.get('type') == 'final_k1' for e in events):
|
||||
time.sleep(3)
|
||||
break
|
||||
Path('/tmp/opencode/k1_trace2.json').write_text(json.dumps(events))
|
||||
print('saved /tmp/opencode/k1_trace2.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)
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""追踪 this+0x10(k1) 的写入者: hook std::string::operator=, 比对目标地址==BusinessCfg+0x10。
|
||||
启动后等初始化完成, 直接读 BusinessCfg 实例的 +0x10 最终值, 并记录所有写到该地址的调用。
|
||||
"""
|
||||
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")
|
||||
|
||||
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)});
|
||||
|
||||
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)}; }
|
||||
}
|
||||
|
||||
// BusinessCfg::getInstance 返回单例 -> 拿 this 地址
|
||||
var cfgAddr = null;
|
||||
try {
|
||||
Interceptor.attach(base.add(0x454310 - 0x454310 + 0x26a880), { // getInstance 附近, 用符号找
|
||||
});
|
||||
} catch(e){}
|
||||
|
||||
// 用 export 查找 getInstance 真实地址
|
||||
var syms = Process.getModuleByName('libudbauthunify.so').enumerateSymbols();
|
||||
var getInst = null;
|
||||
syms.forEach(function(s){ if (s.name.indexOf('BusinessCfg11getInstance') >= 0) getInst = s.address; });
|
||||
send({type:'info', getInst:String(getInst)});
|
||||
|
||||
if (getInst) {
|
||||
Interceptor.attach(getInst, {
|
||||
onLeave: function(r){
|
||||
cfgAddr = r;
|
||||
var s = rdStr(r.add(0x10));
|
||||
send({type:'cfg', addr:String(r), k1: s});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// hook std::string::operator=(string const&) @0x452e40
|
||||
try {
|
||||
Interceptor.attach(base.add(0x452e40), {
|
||||
onEnter: function(a){
|
||||
if (!cfgAddr) return;
|
||||
// a[0] = this(目标), a[1] = src
|
||||
if (a[0].equals(cfgAddr.add(0x10))) {
|
||||
var v = rdStr(a[1]);
|
||||
send({type:'write_k1', val:v,
|
||||
bt:Thread.backtrace(this.context, Backtracer.ACCURATE).map(function(x){
|
||||
return DebugSymbol.fromAddress(x).toString().slice(0,80);}).slice(0,4)});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(e){ send({type:'hookerr', where:'op=', msg:''+e}); }
|
||||
|
||||
// 直接读最终值
|
||||
setTimeout(function(){
|
||||
if (cfgAddr) {
|
||||
var s = rdStr(cfgAddr.add(0x10));
|
||||
send({type:'final_k1', k1:s});
|
||||
}
|
||||
}, 60000);
|
||||
"""
|
||||
|
||||
|
||||
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')
|
||||
events.append(p)
|
||||
if t == 'armed':
|
||||
got_armed.append(1); print('[armed]')
|
||||
elif t == 'info':
|
||||
print('[info] getInstance =', p['getInst'])
|
||||
elif t == 'cfg':
|
||||
print(f"[cfg] addr={p['addr']} k1={p['k1']['v'][:40]}({p['k1']['len']})")
|
||||
elif t == 'write_k1':
|
||||
print(f"[WRITE k1] val={p['val']['v'][:60]}")
|
||||
for f in p['bt']:
|
||||
print(" <", f)
|
||||
elif t == 'final_k1':
|
||||
print(f"[FINAL k1] {p['k1']['v'][:40]}({p['k1']['len']})")
|
||||
elif t == 'hookerr':
|
||||
print('[hookerr]', p)
|
||||
|
||||
sc.on('message', on_msg)
|
||||
sc.load()
|
||||
assert got_armed
|
||||
|
||||
deadline = time.time() + 130
|
||||
while time.time() < deadline:
|
||||
time.sleep(2)
|
||||
if any(e.get('type') == 'final_k1' for e in events):
|
||||
time.sleep(3)
|
||||
break
|
||||
Path('/tmp/opencode/k1_writer.json').write_text(json.dumps(events))
|
||||
print('saved /tmp/opencode/k1_writer.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)
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""nonce链路动态hook —— 与静态反汇编互相验证。
|
||||
|
||||
抓取 getQUrlData 触发证书生成时的完整运行时实参链:
|
||||
nonce_next(m) -> 64位OTP计数
|
||||
getServiceTime() -> 服务时间
|
||||
hyudb_crypt_util::xxtea_encrypt(out, data, key) -> 20B rnd 的直接来源
|
||||
enpack_header(h, key, out) -> cert 头 [0x0c][key_idx]
|
||||
enpack_body(h, a,b,c,d, out) -> P1 明文组装
|
||||
gen_biz_token(struct, uid, k1, k2, out) -> 总装
|
||||
encode_aes(p1, key, out) -> AES加密
|
||||
getOtpEx(uid, idx, a3, out) -> 入口
|
||||
目的: 拿到 xxtea 的 data/key 原文与输出, 验证 20B rnd 是否 = f(serviceTime, nonce, uid, 设备常量),
|
||||
从而判定"信封nonce能否脱离设备本地复算"。
|
||||
|
||||
时序(不可变, 同 hook_cert_keycap): force-stop -> spawn挂起 -> 双bypass -> resume ->
|
||||
10s -> patch_guard -> 装hook -> Java桥触发 getQUrlData。
|
||||
"""
|
||||
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)});
|
||||
|
||||
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 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 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; }
|
||||
function bt(ctx){ return Thread.backtrace(ctx, Backtracer.ACCURATE).map(function(a){
|
||||
return DebugSymbol.fromAddress(a).toString().slice(0,90); }).slice(0,5); }
|
||||
|
||||
// ---------- 1) nonce_next(m) ----------
|
||||
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){ send({type:'hookerr',where:'nonce_next',msg:''+e}); }
|
||||
|
||||
// ---------- 2) getServiceTime() ----------
|
||||
try { Interceptor.attach(base.add(0x268070), {
|
||||
onLeave: function(r){ send({type:'service_time', v:'0x'+r.toString(16), dec:r.toString()}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'getServiceTime',msg:''+e}); }
|
||||
|
||||
// ---------- 3) hyudb_crypt_util::xxtea_encrypt(out&, data, key) ----------
|
||||
try { Interceptor.attach(base.add(0x32e83c), {
|
||||
onEnter: function(a){
|
||||
this.out=a[0]; this.d=rdStr(a[1]); this.k=rdStr(a[2]);
|
||||
send({type:'xxtea_in', data:this.d, key:this.k,
|
||||
data_ascii:ascii(this.d.v), key_ascii:ascii(this.k.v)});
|
||||
},
|
||||
onLeave: function(r){ var o=rdStr(this.out); send({type:'xxtea_out', out:o, out_ascii:ascii(o.v)}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'xxtea_encrypt',msg:''+e}); }
|
||||
|
||||
// ---------- 4) enpack_header(h, NEWKEY, out&) ----------
|
||||
try { Interceptor.attach(base.add(0x32ff44), {
|
||||
onEnter: function(a){ this.out=a[2]; this.h=a[0]; this.k=a[1]; },
|
||||
onLeave: function(r){ var o=rdStr(this.out);
|
||||
send({type:'enpack_header', h:'0x'+this.h.toInt32().toString(16), key:'0x'+this.k.toInt32().toString(16), out:o}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'enpack_header',msg:''+e}); }
|
||||
|
||||
// ---------- 5) enpack_body(h, a,b,c,d, out&) ----------
|
||||
try { Interceptor.attach(base.add(0x3300c8), {
|
||||
onEnter: function(a){
|
||||
this.out=a[5]; this.h=a[0];
|
||||
this.a=rdStr(a[1]); this.b=rdStr(a[2]); this.c=rdStr(a[3]); this.d=rdStr(a[4]);
|
||||
send({type:'enpack_body_in', h:'0x'+this.h.toInt32().toString(16),
|
||||
a:this.a, b:this.b, c:this.c, d:this.d,
|
||||
a_ascii:ascii(this.a.v).slice(0,80), c_ascii:ascii(this.c.v).slice(0,80)});
|
||||
},
|
||||
onLeave: function(r){ var o=rdStr(this.out); send({type:'enpack_body_out', out:o}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'enpack_body',msg:''+e}); }
|
||||
|
||||
// ---------- 6) gen_biz_token(struct, uid, k1, k2, out&) ----------
|
||||
try { Interceptor.attach(base.add(0x3307e4), {
|
||||
onEnter: function(a){
|
||||
this.out=a[4];
|
||||
// BIZTOKEN struct 前0x60字节: [0]=type [1]=key_idx [2]=1 [8..]=str1 [0x20]=st [0x28]=nonce [0x30]=str2 [0x48]=str3
|
||||
send({type:'gbt_in', struct:hx(a[0],0x60),
|
||||
struct_ascii:ascii(hx(a[0],0x60)),
|
||||
uid:rdStr(a[1]), k1:rdStr(a[2]), k2:rdStr(a[3]),
|
||||
k1_ascii:ascii(rdStr(a[2]).v).slice(0,80), k2_ascii:ascii(rdStr(a[3]).v).slice(0,80)});
|
||||
},
|
||||
onLeave: function(r){ var o=rdStr(this.out); send({type:'gbt_out', out:o}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'gen_biz_token',msg:''+e}); }
|
||||
|
||||
// ---------- 7) encode_aes(p1, key, out&) ----------
|
||||
try { Interceptor.attach(base.add(0x330218), {
|
||||
onEnter: function(a){ this.out=a[2]; this.p1=rdStr(a[0]); this.key=rdStr(a[1]); },
|
||||
onLeave: function(r){ var o=rdStr(this.out);
|
||||
send({type:'encode_aes', p1:this.p1, key:this.key, key_ascii:ascii(this.key.v).slice(0,60), out:o}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'encode_aes',msg:''+e}); }
|
||||
|
||||
// ---------- 8) getOtpEx(uid, idx, a3, out&) ----------
|
||||
try { Interceptor.attach(base.add(0x2681b4), {
|
||||
onEnter: function(a){ this.out=a[4]; this.uid=a[1]; this.idx=a[2]; this.a3=rdStr(a[3]); },
|
||||
onLeave: function(r){ var o=rdStr(this.out);
|
||||
send({type:'getOtpEx', uid:'0x'+this.uid.toString(16), idx:this.idx.toInt32(), a3:this.a3, out:o}); }
|
||||
}); } catch(e){ send({type:'hookerr',where:'getOtpEx',msg:''+e}); }
|
||||
|
||||
// ---------- 9) AESkeyMgr::getkey ----------
|
||||
try { Interceptor.attach(base.add(0x454350), {
|
||||
onEnter: function(a){ this.a0=a[0]; this.i1=a[1]; this.i2=a[2]; },
|
||||
onLeave: function(r){
|
||||
// getkey 返回 std::string (sret in x8), 但 onLeave x8 不可靠; 读 x0 若为短串
|
||||
try { var s=rdStr(this.a0.add(0)); if(s.len>0 && s.len<64) send({type:'getkey', i1:this.i1.toInt32(), i2:this.i2.toInt32(), key:s}); }
|
||||
catch(e){}
|
||||
}
|
||||
}); } catch(e){ send({type:'hookerr',where:'getkey',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();
|
||||
send({type:'stage', msg:'call #1'});
|
||||
var q1=inst.getQUrlData(__UID__, "", "");
|
||||
send({type:'qurl_done', idx:1, len:q1?q1.length:0, data:q1||''});
|
||||
var q2=inst.getQUrlData(__UID__, "", "");
|
||||
send({type:'qurl_done', idx:2, len:q2?q2.length:0, data:q2||''});
|
||||
}catch(e){ n+=1; if(n%4===0) send({type:'retry',n:n,err:String(e).slice(0,120)}); setTimeout(go,5000); }
|
||||
}
|
||||
setTimeout(go, 18000);
|
||||
});
|
||||
""".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')
|
||||
if t != 'retry':
|
||||
events.append(p)
|
||||
if t == 'armed':
|
||||
got_armed.append(1); print('[armed]')
|
||||
elif t == 'nonce_next':
|
||||
print(f"[nonce_next] m={p['m']} ret={p['ret']}")
|
||||
elif t == 'service_time':
|
||||
print(f"[service_time] {p['v']} ({p['dec']})")
|
||||
elif t == 'xxtea_in':
|
||||
print(f"[xxtea.in] data len={p['data']['len']} {p['data']['v'][:40]}")
|
||||
print(f" key len={p['key']['len']} {p['key']['v'][:60]} ascii={p['key_ascii'][:50]}")
|
||||
elif t == 'xxtea_out':
|
||||
print(f"[xxtea.out] len={p['out']['len']} {p['out']['v'][:48]}")
|
||||
elif t == 'enpack_header':
|
||||
print(f"[enpack_header] h={p['h']} key={p['key']} out len={p['out']['len']} {p['out']['v'][:20]}")
|
||||
elif t == 'enpack_body_in':
|
||||
print(f"[enpack_body] h={p['h']}")
|
||||
print(f" a(len{p['a']['len']})={p['a']['v'][:60]} ascii={p['a_ascii']}")
|
||||
print(f" b(len{p['b']['len']})={p['b']['v'][:48]}")
|
||||
print(f" c(len{p['c']['len']})={p['c']['v'][:60]} ascii={p['c_ascii']}")
|
||||
print(f" d(len{p['d']['len']})={p['d']['v'][:24]}")
|
||||
elif t == 'enpack_body_out':
|
||||
print(f"[enpack_body.out] len={p['out']['len']} {p['out']['v'][:80]}")
|
||||
elif t == 'gbt_in':
|
||||
print(f"[gen_biz_token.in] struct={p['struct'][:100]}")
|
||||
print(f" uid(len{p['uid']['len']})={p['uid']['v'][:40]}")
|
||||
print(f" k1(len{p['k1']['len']})={p['k1']['v'][:60]} ascii={p['k1_ascii']}")
|
||||
print(f" k2(len{p['k2']['len']})={p['k2']['v'][:60]} ascii={p['k2_ascii']}")
|
||||
elif t == 'gbt_out':
|
||||
print(f"[gen_biz_token.out] len={p['out']['len']} {p['out']['v'][:60]}")
|
||||
elif t == 'encode_aes':
|
||||
print(f"[encode_aes] p1 len={p['p1']['len']} {p['p1']['v'][:70]}")
|
||||
print(f" key={p['key_ascii'][:50]} out len={p['out']['len']}")
|
||||
elif t == 'getOtpEx':
|
||||
print(f"[getOtpEx] uid={p['uid']} idx={p['idx']} a3(len{p['a3']['len']}) out len={p['out']['len']}")
|
||||
elif t == 'getkey':
|
||||
print(f"[getkey] i1={p['i1']} i2={p['i2']} key={p['key']['v'][:40]} ascii={ascii_short(p['key']['v'])}")
|
||||
elif t == 'qurl_done':
|
||||
print(f"[qurl#{p['idx']}] len={p['len']} head={(p.get('data') or '')[:40]}")
|
||||
elif t == 'stage':
|
||||
print('[stage]', p['msg'])
|
||||
|
||||
def ascii_short(h):
|
||||
s = ''
|
||||
for i in range(0, len(h), 2):
|
||||
c = int(h[i:i+2], 16)
|
||||
s += chr(c) if 32 <= c < 127 else '.'
|
||||
return s[:40]
|
||||
|
||||
sc.on('message', on_msg)
|
||||
sc.load()
|
||||
assert got_armed
|
||||
|
||||
deadline = time.time() + 120
|
||||
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_chain.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)
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""纯协议关键验证: 本地重算 nonce 铸证是否被服务端接受(同账号实验)。
|
||||
|
||||
背景: 旧结论"必须复用信封原nonce(纯随机必40020)"是建立在不了解nonce结构的基础上。
|
||||
现已破解: nonce = XXTEA(pack(st)+pack(counter|st<<16), key=MD5hex(uid+k1)[:16])
|
||||
=> 服务端校验的是 nonce 能否用 t3.uid 解密成功, 而非"设备是否生成过该nonce"。
|
||||
本实验: 用【可信账号uid + k1】本地重算 nonce 铸证, 替换当前新鲜信封的 cert, bind。
|
||||
✅ 成功 => 服务端接受本地nonce, 纯协议基石成立!
|
||||
失败(40020) => 服务端仍校验nonce签发源, 需进一步逆向。
|
||||
|
||||
用法: RE_PY scripts/probe_nonce_bind.py <账号> <密码>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from app_login_flow import QrAuthRequiredError, login_cred # noqa: E402
|
||||
from cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1 # noqa: E402
|
||||
from envelope_forge import Envelope # noqa: E402
|
||||
from nonce_forge import K1_DEFAULT, gen_nonce # noqa: E402
|
||||
from probe_huya_qr_bind import QrRole, web_behavior # noqa: E402
|
||||
from core.huya.device_fingerprint import get_huya_sdid # noqa: E402
|
||||
|
||||
|
||||
def try_bind(wup_b64: str, name: str) -> dict | None:
|
||||
from urllib.parse import quote
|
||||
sdid = get_huya_sdid(allow_fallback=False).sdid
|
||||
pc = QrRole(pc=True, sdid=sdid)
|
||||
ph = QrRole(pc=False, sdid=sdid)
|
||||
beh, page = web_behavior()
|
||||
resp = pc.call("/qrLgn/getQrId", "70001",
|
||||
{"behavior": beh, "type": "", "domainList": "", "page": page})
|
||||
qrid = ((resp.get("data") or {}).get("qrId")) if resp.get("returnCode") == 0 else None
|
||||
if not qrid:
|
||||
print(f" [{name}] getQrId失败 {resp.get('returnCode')}")
|
||||
return None
|
||||
cp = f"https://aq.huya.com/r/confirm.html?k={qrid}&id=5002"
|
||||
ph.call("/qrLgn/scanQrPicNotify", "70005",
|
||||
{"qrId": qrid, "wupData": wup_b64,
|
||||
"behavior": quote("[]", safe=""), "page": quote(cp, safe="")})
|
||||
r2 = ph.call("/qrLgn/bindQrLoginUser", "70007",
|
||||
{"qrId": qrid, "wupData": wup_b64,
|
||||
"behavior": quote("[]", safe=""), "page": quote(cp, safe="")})
|
||||
print(f" [{name}] bindQr rc={r2.get('returnCode')} "
|
||||
f"{r2.get('message') or r2.get('description') or ''}")
|
||||
if r2.get("returnCode") != 0:
|
||||
return {"rc": r2.get("returnCode"),
|
||||
"msg": r2.get("message") or r2.get("description")}
|
||||
for _ in range(4):
|
||||
rt = pc.call("/qrLgn/tryQrLogin", "70003",
|
||||
{"qrId": qrid, "remember": "1", "domainList": "",
|
||||
"behavior": beh, "page": page})
|
||||
dt = rt.get("data") or {}
|
||||
if dt.get("stage") == 2 and dt.get("biztoken"):
|
||||
return {"ok": True, "uid": dt.get("uid"),
|
||||
"biztoken_len": len(dt["biztoken"])}
|
||||
time.sleep(1.5)
|
||||
return {"stage2_no_token": True}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
acct, pwd = sys.argv[1], sys.argv[2]
|
||||
# 可选第三参: 目标uid覆盖(跨账号实验用); 缺省=信封原uid
|
||||
target_uid = int(sys.argv[3]) if len(sys.argv) > 3 else None
|
||||
print(f"[1/3] 密码登录 {acct} ...")
|
||||
try:
|
||||
cred = login_cred(acct, pwd)
|
||||
except QrAuthRequiredError as exc:
|
||||
print(f"❌ {exc}")
|
||||
return 2
|
||||
print(f" cred {len(cred)}B {cred[:8].hex()}")
|
||||
|
||||
env = Envelope.load()
|
||||
orig = base64.b64decode(env.cert_b64)
|
||||
f = parse_p1(decrypt_cert(orig))
|
||||
env_uid = env.uid
|
||||
use_uid = target_uid if target_uid is not None else env_uid
|
||||
print(f"[2/3] 本地重算 nonce (信封uid={env_uid}, 目标uid={use_uid}, "
|
||||
f"k1={K1_DEFAULT[:16]}...)")
|
||||
st = int(time.time() * 1000)
|
||||
rnd = gen_nonce(use_uid, K1_DEFAULT, service_time_ms=st, counter=0)
|
||||
print(f" serviceTime={st} nonce={rnd.hex()}")
|
||||
P1 = build_p1(f["app_id"], f["fingerprint"], cred, rnd=rnd)
|
||||
cert = forge_cert(P1, key_idx=orig[1])
|
||||
b64 = base64.b64encode(cert).decode()
|
||||
if len(b64) != env.cert_len:
|
||||
print(f" cert b64 长度不符 {len(b64)} != {env.cert_len}, 中止")
|
||||
return 1
|
||||
raw = bytearray(env.raw)
|
||||
raw[env.cert_off:env.cert_off + env.cert_len] = b64.encode()
|
||||
if use_uid != env_uid:
|
||||
import struct
|
||||
print(f"[patch] 信封uid {env_uid} -> {use_uid}")
|
||||
struct.pack_into(">Q", raw, env.uid_off, use_uid)
|
||||
wup = base64.b64encode(bytes(raw)).decode()
|
||||
print(f"[3/3] bind (wup {len(raw)}B, uid={use_uid}) ...")
|
||||
r = try_bind(wup, "local_nonce")
|
||||
print("结果:", r)
|
||||
(ROOT / "evidence/nonce_bind_result.json").write_text(json.dumps(
|
||||
{"local_nonce": r, "env_uid": env_uid, "uid": use_uid, "st": st,
|
||||
"nonce_hex": rnd.hex(), "ts": time.time()}, ensure_ascii=False, indent=1))
|
||||
return 0 if r and r.get("ok") else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user