feat(huya): 整合 App 协议登录获取 Cookie 全链路并与 Web 登录解耦
- 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 单元测试覆盖全链路
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user