- 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 单元测试覆盖全链路
211 lines
7.1 KiB
Python
211 lines
7.1 KiB
Python
#!/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() |