- core/huya: 新增 app_login, wup_encoder, nonce_forge, cert_forge, envelope_forge, device_profile, udb_aes - core/huya/__init__.py: 导出 login_huya_app_password 与 HuyaAppPasswordLogin - web/backend: 新增 /accounts/app-password-login 与 /accounts/app-password-login/selected 路由及 Schema,与原 Web 密码登录独立分开 - web/frontend: 增加 App 登录 API 与前端界面“App 登录选中”操作,弹窗结果明确区分 - tests: 新增 test_huya_app_login.py 单元测试覆盖全链路
259 lines
8.5 KiB
Python
259 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""散弹枪 v2: 完整解码 dfpReport 管道参数 (指针数组 / (ptr,len) / std::string)。
|
|
|
|
挂载: topA_20e924, midA_5a938, cryptoA_5b1f4, copy_61098, pktA_7f744, pktB_92128
|
|
对每个 onEnter 的 x0..x4:
|
|
1) 直接缓冲探测 (JSON/MAGIC 前缀 -> full dump 6000B)
|
|
2) (ptr,len) 解释: 读 16B -> 若两个 u64 第二个 < 100000 且第一个可读 -> dump
|
|
3) 指针数组解释: 读 4x8B 指针, 对每个指针尝试 (2)
|
|
4) std::string 解释: [size u64][data u64][cap u64] 或 [data][size][cap]
|
|
所有事件带 t (ms since start)。SSL_write 抓 wire 对照。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import frida
|
|
|
|
PACKAGE = "com.duowan.kiwi"
|
|
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
|
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pipeline_v2.json")
|
|
|
|
JS = r"""
|
|
'use strict';
|
|
Process.setExceptionHandler(function(d){ return true; });
|
|
send({type:'armed'});
|
|
|
|
var t0 = Date.now();
|
|
var JSON_HDR = [0x7b,0x22,0x61,0x70,0x70,0x49,0x64,0x22,0x3a,0x22,0x35,0x30,0x30,0x38];
|
|
var MAGIC_HDR = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94,0x01,0xee];
|
|
|
|
function hexb(p, n){
|
|
try{
|
|
var arr = Array.from(new Uint8Array(p.readByteArray(n)));
|
|
var s = '';
|
|
for (var i=0;i<arr.length;i++) s += ('0'+arr[i].toString(16)).slice(-2);
|
|
return s;
|
|
}catch(e){ return null; }
|
|
}
|
|
function startsWith(p, arr){
|
|
try{
|
|
var b = new Uint8Array(p.readByteArray(arr.length));
|
|
for (var i=0;i<arr.length;i++) if (b[i]!==arr[i]) return false;
|
|
return true;
|
|
}catch(e){ return false; }
|
|
}
|
|
function isReadable(p){
|
|
try{ p.readU8(); return true; }catch(e){ return false; }
|
|
}
|
|
function u64(p){
|
|
try{ return p.readU64().toString(10); }catch(e){ return null; }
|
|
}
|
|
function decodeArg(p, tag){
|
|
var res = {reg: tag};
|
|
try{
|
|
if (!p || p.isNull()) return null;
|
|
res.addr = String(p);
|
|
// 1) direct buffer
|
|
if (startsWith(p, JSON_HDR)){
|
|
res.kind = 'JSON'; res.full = hexb(p, 6000); return res;
|
|
}
|
|
if (startsWith(p, MAGIC_HDR)){
|
|
res.kind = 'MAGIC';
|
|
var pre = '';
|
|
try{ pre = hexb(p.sub(1536), 1536); }catch(e){}
|
|
res.pre = pre;
|
|
res.full = hexb(p, 9000);
|
|
return res;
|
|
}
|
|
// 2) (ptr,len) pair: [ptr][len]
|
|
var p0 = p.readPointer(); var l0 = p.add(8).readU64();
|
|
if (!p0.isNull() && isReadable(p0) && l0 > 0 && l0 < 20000 && p0 != p){
|
|
var b0 = hexb(p0, Math.min(Number(l0), 6000));
|
|
var kind = 'seg';
|
|
if (startsWith(p0, JSON_HDR)) kind = 'JSON';
|
|
else if (startsWith(p0, MAGIC_HDR)) kind = 'MAGIC';
|
|
res.kind = kind;
|
|
res.ptrlen = {ptr: String(p0), len: Number(l0)};
|
|
res.seg = b0;
|
|
return res;
|
|
}
|
|
// 3) pointer array (4 x 8B)
|
|
var ptrs = [];
|
|
for (var i=0;i<4;i++){
|
|
var q = p.add(i*8).readPointer();
|
|
if (q.isNull() || !isReadable(q)) break;
|
|
ptrs.push(String(q));
|
|
}
|
|
if (ptrs.length >= 2){
|
|
res.kind = 'ptrs'; res.ptrs = ptrs;
|
|
// decode each pointer as (ptr,len)
|
|
var segs = [];
|
|
for (var j=0;j<ptrs.length;j++){
|
|
var qj = ptr(j);
|
|
var q0 = qj.readPointer();
|
|
var lj = qj.add(8).readU64();
|
|
if (!q0.isNull() && lj > 0 && lj < 20000 && isReadable(q0)){
|
|
segs.push({ptr: String(q0), len: Number(lj), head: hexb(q0, 48),
|
|
kind: startsWith(q0, JSON_HDR) ? 'JSON' : (startsWith(q0, MAGIC_HDR) ? 'MAGIC' : 'raw')});
|
|
}
|
|
}
|
|
res.segs = segs;
|
|
return res;
|
|
}
|
|
// 4) raw
|
|
res.kind = 'raw'; res.head = hexb(p, 32) || '';
|
|
return res;
|
|
}catch(e){ return null; }
|
|
}
|
|
|
|
function hookFn(off, name){
|
|
try{
|
|
var base = Process.getModuleByName('libhydeviceid.so').base;
|
|
Interceptor.attach(base.add(off), {
|
|
onEnter: function(a){
|
|
var ctx = this.context;
|
|
var ev = {fn: name, phase: 'enter', t: Date.now() - t0};
|
|
['x0','x1','x2','x3','x4'].forEach(function(r, i){
|
|
var pr = decodeArg(ctx[r], r);
|
|
if (pr) ev[r] = pr;
|
|
});
|
|
send({type:'pipe', ev: ev});
|
|
}
|
|
});
|
|
send({type:'hooked', name: name});
|
|
}catch(e){ send({type:'hook_fail', name: name, e: String(e)}); }
|
|
}
|
|
|
|
var H = [
|
|
[0x20e924, 'topA_20e924'],
|
|
[0x5a938, 'midA_5a938'],
|
|
[0x5b1f4, 'cryptoA_5b1f4'],
|
|
[0x61098, 'copy_61098'],
|
|
[0x7f744, 'pktA_7f744'],
|
|
[0x92128, 'pktB_92128'],
|
|
];
|
|
var hookedN = 0;
|
|
function attachOne(base, off, name){
|
|
try{
|
|
Interceptor.attach(base.add(off), {
|
|
onEnter: function(a){
|
|
var ctx = this.context;
|
|
var ev = {fn: name, phase: 'enter', t: Date.now() - t0};
|
|
['x0','x1','x2','x3','x4'].forEach(function(r, i){
|
|
var pr = decodeArg(ctx[r], r);
|
|
if (pr) ev[r] = pr;
|
|
});
|
|
send({type:'pipe', ev: ev});
|
|
}
|
|
});
|
|
hookedN++;
|
|
return true;
|
|
}catch(e){ return false; }
|
|
}
|
|
function tryHookAll(){
|
|
try{
|
|
var m = Process.getModuleByName('libhydeviceid.so');
|
|
for (var i=0;i<H.length;i++){
|
|
if (attachOne(m.base, H[i][0], H[i][1])){}
|
|
}
|
|
send({type:'hooked_all', n: hookedN});
|
|
}catch(e){ setTimeout(tryHookAll, 800); }
|
|
}
|
|
setTimeout(tryHookAll, 500);
|
|
|
|
try{
|
|
var r = new ApiResolver('module');
|
|
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
|
|
Interceptor.attach(m.address, {
|
|
onEnter: function(a){
|
|
var len = a[2].toInt32();
|
|
if (len < 100 || len > 20000) return;
|
|
var head = '';
|
|
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
|
|
if (head.indexOf('dfpReport') < 0) return;
|
|
send({type:'wire', len:len, hex:hexb(a[1], len), t:Date.now()-t0});
|
|
}
|
|
});
|
|
});
|
|
send({type:'ssl_hooked'});
|
|
}catch(e){ send({type:'ssl_err', e:String(e)}); }
|
|
"""
|
|
|
|
|
|
def main():
|
|
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
|
|
pid = device.spawn([PACKAGE])
|
|
print(f"[*] spawned {PACKAGE} pid={pid}", flush=True)
|
|
session = device.attach(pid)
|
|
|
|
events = []
|
|
|
|
def on_message(message, data):
|
|
if message.get("type") == "error":
|
|
print("[JS-ERR]", str(message)[:300], flush=True)
|
|
return
|
|
if message.get("type") != "send":
|
|
return
|
|
p = message.get("payload") or {}
|
|
t = p.get("type")
|
|
if t == "armed":
|
|
print("[*] JS armed", flush=True)
|
|
elif t == "hooked_all":
|
|
print(f"[*] hooked {p.get('n')} fns", flush=True)
|
|
elif t == "ssl_hooked":
|
|
print("[*] SSL_write hooked", flush=True)
|
|
elif t == "pipe":
|
|
ev = p.get('ev') or {}
|
|
parts = []
|
|
for r in ['x0','x1','x2','x3','x4']:
|
|
pr = ev.get(r)
|
|
if not pr or not isinstance(pr, dict):
|
|
continue
|
|
k = pr.get('kind')
|
|
if k == 'JSON':
|
|
parts.append(f"{r}=JSON full")
|
|
elif k == 'MAGIC':
|
|
parts.append(f"{r}=MAGIC full")
|
|
elif k == 'seg':
|
|
parts.append(f"{r}=seg({pr.get('ptrlen',{}).get('len')})@{pr.get('ptrlen',{}).get('ptr')}")
|
|
elif k == 'ptrs':
|
|
parts.append(f"{r}=ptrs {pr.get('ptrs')} segs={[(s.get('kind'), s.get('len')) for s in pr.get('segs') or []]}")
|
|
elif k == 'raw':
|
|
parts.append(f"{r}=raw {pr.get('addr')} {pr.get('head','')[:16]}")
|
|
print(f"[{ev.get('t')}ms] {ev.get('fn')}::{ev.get('phase')} " + " ".join(parts), flush=True)
|
|
events.append({"type": "pipe", "ev": ev})
|
|
OUT.write_text(json.dumps(events))
|
|
elif t == "wire":
|
|
print(f"[{p.get('t')}ms] [wire] len={p.get('len')}", flush=True)
|
|
events.append({"type": "wire", "len": p.get('len'), "t": p.get('t'), "hex": p.get('hex')})
|
|
OUT.write_text(json.dumps(events))
|
|
|
|
script = session.create_script(JS)
|
|
script.on("message", on_message)
|
|
script.load()
|
|
print("[*] 主JS 已装载, 加载 bypass + resume", flush=True)
|
|
session.create_script(
|
|
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
|
|
).load()
|
|
session.create_script(
|
|
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
|
|
).load()
|
|
device.resume(pid)
|
|
time.sleep(11)
|
|
session.create_script(
|
|
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
|
|
).load()
|
|
print("[*] 已 resume。等待 dfpReport", flush=True)
|
|
try:
|
|
while True:
|
|
time.sleep(5)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
print(f"[*] 结束 -> {OUT}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |