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,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""hook libhydeviceid.so!0x91ce8: t2 [魔数+密文] 拷贝点 -> 抓完整加密输入。
|
||||
|
||||
0x91ce8 是 t2 组装中的 memcpy 调用点 (src 已含 魔数+密文, size~3867-3986)。
|
||||
onEnter:
|
||||
1) 从 x0/x1/x2/sp 定位 [魔数+密文] 输出缓冲, dump 4096B
|
||||
2) 全内存扫 {"appId":"5008 找加密输入 (JSON+二进制) 缓冲, dump 4096B
|
||||
3) 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_input_capture.json")
|
||||
|
||||
JS = r"""
|
||||
'use strict';
|
||||
Process.setExceptionHandler(function(d){ return true; });
|
||||
send({type:'armed'});
|
||||
|
||||
var MAGIC_STR = '57 18 82 cf 66 4b b3 94 01 ee';
|
||||
var JSON_STR = '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38'; // {"appId":"5008
|
||||
|
||||
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 startsWithHex(p, hexstr){
|
||||
try{
|
||||
var want = hexstr.split(' ').map(function(x){return parseInt(x,16);});
|
||||
var b = new Uint8Array(p.readByteArray(want.length));
|
||||
for (var i=0;i<want.length;i++) if (b[i]!==want[i]) return false;
|
||||
return true;
|
||||
}catch(e){ return false; }
|
||||
}
|
||||
function scanPattern(pattern, cap){
|
||||
var hits = [];
|
||||
try{
|
||||
Process.enumerateRanges('r--').forEach(function(r){
|
||||
if (r.size > 1024*1024*256) return;
|
||||
if (hits.length >= cap) return;
|
||||
try{
|
||||
var m = Memory.scanSync(r.base, r.size, pattern);
|
||||
m.slice(0, cap - hits.length).forEach(function(x){
|
||||
hits.push({addr: String(x.address),
|
||||
mod: (function(){ var mm=Process.findModuleByAddress(x.address); return mm? mm.name : 'anon'; })()});
|
||||
});
|
||||
}catch(e){}
|
||||
});
|
||||
}catch(e){}
|
||||
return hits;
|
||||
}
|
||||
function dumpAround(addr, pre, post){
|
||||
try{
|
||||
var p = addr.sub(pre);
|
||||
var d = hexb(p, pre + post);
|
||||
return {base: String(p), pre: pre, post: post, hex: d};
|
||||
}catch(e){ return null; }
|
||||
}
|
||||
|
||||
// 找到含魔数的缓冲 (扫描参数指向的内存)
|
||||
function findMagicBuf(ctx){
|
||||
var cands = [];
|
||||
[ctx.x0, ctx.x1, ctx.x2, ctx.x3, ctx.x4].forEach(function(p, i){
|
||||
try{
|
||||
if (p.isNull()) return;
|
||||
if (startsWithHex(p, MAGIC_STR)) cands.push({reg: 'x'+i, addr: String(p)});
|
||||
// 也看看指针对应的数据是否在 sp 附近 (std::string 结构: ptr 在偏移8/16)
|
||||
var q = p.readPointer();
|
||||
if (!q.isNull() && startsWithHex(q, MAGIC_STR)) cands.push({reg: 'x'+i+'_ptr', addr: String(q)});
|
||||
}catch(e){}
|
||||
});
|
||||
// 扫最近的匿名内存找 [魔数+密文] (限 512MB 内, 快)
|
||||
if (!cands.length){
|
||||
try{
|
||||
var m = Memory.scanSync(ctx.sp, 64*1024*1024, MAGIC_STR);
|
||||
m.slice(0, 3).forEach(function(x){ cands.push({reg: 'sp_scan', addr: String(x.address)}); });
|
||||
}catch(e){}
|
||||
}
|
||||
return cands;
|
||||
}
|
||||
|
||||
var nCalls = 0;
|
||||
var hooked91 = false;
|
||||
function tryHook91(){
|
||||
if (hooked91) return;
|
||||
try{
|
||||
var base = Process.getModuleByName('libhydeviceid.so').base;
|
||||
Interceptor.attach(base.add(0x91ce8), {
|
||||
onEnter: function(a){
|
||||
nCalls++;
|
||||
var ctx = this.context;
|
||||
var ev = {type:'t2copy', n:nCalls, x0:String(ctx.x0), x1:String(ctx.x1),
|
||||
x2:String(ctx.x2), x3:String(ctx.x3), x4:String(ctx.x4)};
|
||||
var mbs = findMagicBuf(ctx);
|
||||
ev.magicbufs = mbs;
|
||||
for (var i=0;i<mbs.length && i<2;i++){
|
||||
var d = dumpAround(ptr(mbs[i].addr), 0, 4096);
|
||||
ev['magicdump'+i] = d;
|
||||
}
|
||||
// 全内存扫明文 JSON (加密输入)
|
||||
var jh = scanPattern(JSON_STR, 6);
|
||||
ev.jsonhits = jh;
|
||||
for (var j=0;j<jh.length && j<4;j++){
|
||||
var d = dumpAround(ptr(jh[j].addr), 0, 4096);
|
||||
ev['jsondump'+j] = d;
|
||||
}
|
||||
send({type:'t2copy', ev: ev});
|
||||
}
|
||||
});
|
||||
hooked91 = true;
|
||||
send({type:'hooked_91ce8', base:String(base)});
|
||||
}catch(e){
|
||||
setTimeout(tryHook91, 1000);
|
||||
}
|
||||
}
|
||||
setTimeout(tryHook91, 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()});
|
||||
}
|
||||
});
|
||||
});
|
||||
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_91ce8":
|
||||
print(f"[*] hooked 0x91ce8 @ {p.get('base')}", flush=True)
|
||||
elif t == "ssl_hooked":
|
||||
print("[*] SSL_write hooked", flush=True)
|
||||
elif t == "hook_err":
|
||||
print("[hook_err]", p.get('e'), flush=True)
|
||||
elif t == "t2copy":
|
||||
ev = p.get('ev') or {}
|
||||
print(f"[t2copy] n={ev.get('n')} x0={ev.get('x0')} x1={ev.get('x1')} x2={ev.get('x2')}", flush=True)
|
||||
for mb in (ev.get('magicbufs') or [])[:3]:
|
||||
print(f" magicbuf reg={mb.get('reg')} @{mb.get('addr')}", flush=True)
|
||||
for jh in (ev.get('jsonhits') or [])[:6]:
|
||||
print(f" jsonhit @{jh.get('addr')} mod={jh.get('mod')}", flush=True)
|
||||
events.append({"type": "t2copy", "ev": ev})
|
||||
OUT.write_text(json.dumps(events))
|
||||
elif t == "wire":
|
||||
print(f"[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 (0x91ce8 命中)", 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