公式: appSign = MD5(appId + '_' + appVersion + '_' + k1)
金样本: MD5('5008_13.4.22_865a4924a40897ac1fcfe6b4c2cbb0e3') = ed0db8334cadd236c00cadf7e11ab5a5 ✅
发现链路: createWupProtoInfo@0x273f1c反汇编 -> HuyaMd5(全局单例3串@0x484000+0xe0) -> frida实机读串(5008/13.4.22/k1) -> toString直出ed0db8 -> python标准md5复现
- 修改: hook_otp_capture.js(+appsign/huya-md5 hook, SSO/LONG修正, tostr-hex/ctor-hex)
- docs §11.45: 完整公式+发现链路+结构性解释+工程意义
374 lines
14 KiB
JavaScript
374 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
// ============================================================================
|
|
// 虎牙 hdid(appSign) 定向捕获 —— 基于 hook_huya_crypto.js 增强
|
|
// 目标: 1) 抓登录路径 getOtp 六元组 (in,cnt,s3,s4,s5,nonce) + 输出 out
|
|
// 2) 抓 BusinessCfg::setSafeDeviceId 写入的 hdid(32hex) = WUP t1.t0 值
|
|
// 3) 调用点标注(backtrace) 区分 登录/证书/biztoken 路径
|
|
// 用法: frida -U -n com.duowan.kiwi -l hook_otp_capture.js (配 anti-frida bypass)
|
|
// 然后正常执行密码登录; 事件经 send() 输出, 用 frida -l ... 的 stdout/json 收集
|
|
// ============================================================================
|
|
|
|
const MAX_HEX = 2048;
|
|
const MAX_EVENTS = 4000;
|
|
let events = 0;
|
|
let installed = false;
|
|
|
|
function emit(row) {
|
|
if (events >= MAX_EVENTS) return;
|
|
events++;
|
|
row.pid = Process.id;
|
|
row.tid = Process.getCurrentThreadId();
|
|
row.ts = Date.now();
|
|
row.seq = events;
|
|
send(row);
|
|
}
|
|
|
|
function hexOf(ptr, n) {
|
|
try {
|
|
const len = Math.min(Number(n), MAX_HEX);
|
|
const bytes = ptr.readByteArray(len);
|
|
if (bytes === null) return '';
|
|
const a = new Uint8Array(bytes);
|
|
let out = '';
|
|
for (let i = 0; i < a.length; i++) {
|
|
const h = a[i].toString(16);
|
|
out += (h.length < 2 ? '0' + h : h);
|
|
}
|
|
return out;
|
|
} catch (_) { return ''; }
|
|
}
|
|
|
|
// libc++ std::string (NDK __ndk1): [0..23] union; offset23 bit0=SSO
|
|
function parseStr(addr) {
|
|
try {
|
|
if (addr.isNull()) return null;
|
|
const last = addr.add(23).readU8();
|
|
let size, data;
|
|
if (last & 1) { size = last >> 1; data = addr; }
|
|
else {
|
|
size = addr.add(8).readU64().toNumber();
|
|
data = addr.add(16).readPointer();
|
|
}
|
|
if (size < 0 || size > 0x100000 || data.isNull()) return null;
|
|
return { size, data };
|
|
} catch (_) { return null; }
|
|
}
|
|
|
|
function readStr(addr) {
|
|
const s = parseStr(addr);
|
|
if (s === null) return null;
|
|
try { return s.data.readUtf8String(s.size); } catch (_) { return null; }
|
|
}
|
|
|
|
function readStrHex(addr) {
|
|
const s = parseStr(addr);
|
|
if (s === null) return null;
|
|
return { size: s.size, hex: hexOf(s.data, s.size) };
|
|
}
|
|
|
|
function bt() {
|
|
try {
|
|
const tr = Thread.backtrace(this.context, Backtracer.ACCURATE)
|
|
.slice(0, 6).map(a => {
|
|
const m = Process.findModuleByAddress(a);
|
|
const off = m ? '0x' + a.sub(m.base).toString(16) : a.toString();
|
|
const p = DebugSymbol.fromAddress(a);
|
|
const nm = p ? p.name : '';
|
|
return (m ? m.name : '?') + '!' + off + (nm ? ' (' + nm + ')' : '');
|
|
});
|
|
return tr.join(' <- ');
|
|
} catch (_) { return ''; }
|
|
}
|
|
|
|
function byName(needle) {
|
|
try {
|
|
const mod = Process.findModuleByName('libudbauthunify.so');
|
|
if (mod === null) return null;
|
|
for (const e of mod.enumerateExports()) {
|
|
if (e.type === 'function' && e.name.indexOf(needle) >= 0) return e.address;
|
|
}
|
|
return null;
|
|
} catch (_) { return null; }
|
|
}
|
|
|
|
function hookByName(needle, event, onEnterFn, onLeaveFn) {
|
|
const addr = byName(needle);
|
|
if (addr === null) { emit({ event: 'hook-missing', needle }); return false; }
|
|
try {
|
|
Interceptor.attach(addr, {
|
|
onEnter(args) {
|
|
try { if (onEnterFn) onEnterFn.call(this, args); }
|
|
catch (e) { emit({ event, err: 'enter:' + e }); }
|
|
},
|
|
onLeave(retval) {
|
|
try { if (onLeaveFn) onLeaveFn.call(this, retval); }
|
|
catch (e) { emit({ event, err: 'leave:' + e }); }
|
|
},
|
|
});
|
|
emit({ event: 'hook-installed', needle, address: addr.toString() });
|
|
return true;
|
|
} catch (e) {
|
|
emit({ event: 'hook-error', needle, error: String(e) });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 栈上 std::string& 出参 (第9+个参数): AAPCS64 栈上首槽 = [sp] (返回地址在 x30/lr)
|
|
function stackStrPtr(ctx) {
|
|
try { return ctx.sp.readPointer(); } catch (_) { return null; }
|
|
}
|
|
|
|
function install() {
|
|
if (installed) return;
|
|
const mod = Process.findModuleByName('libudbauthunify.so');
|
|
if (mod === null) return;
|
|
installed = true;
|
|
emit({ event: 'module-found', name: mod.name, base: mod.base.toString(), size: mod.size });
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 1) hyudb_otp_encrypt(std::string key/in, uchar, uchar, std::string s3,
|
|
// std::string s4, std::string s5, uchar, ulong nonce,
|
|
// std::string& out @sp+8)
|
|
// -------------------------------------------------------------------------
|
|
hookByName('hyudb_otp_encrypt', 'crypto-otp',
|
|
function (args) {
|
|
const keyHex = readStrHex(args[0]); // x0 = in (xxtea key)
|
|
const s3 = readStrHex(args[3]);
|
|
const s4 = readStrHex(args[4]);
|
|
const s5 = readStrHex(args[5]);
|
|
this._outPtr = stackStrPtr(this.context);
|
|
emit({
|
|
event: 'crypto-otp', stage: 'enter',
|
|
bt: bt.call(this),
|
|
xxteaKeyStr: readStr(args[0]),
|
|
xxteaKeyHex: keyHex ? keyHex.hex : '',
|
|
xxteaKeySize: keyHex ? keyHex.size : 0,
|
|
arg1: args[1].toUInt32(), // 固定 2
|
|
arg2_cnt: args[2].toUInt32(), // AES counter (getkey b)
|
|
arg3: s3 ? s3.hex : null, // BusinessCfg+0x40
|
|
arg4: s4 ? s4.hex : null, // BusinessCfg+0x10 (k1)
|
|
arg5: s5 ? s5.hex : null, // ALD+0x28 blob
|
|
arg5Size: s5 ? s5.size : 0,
|
|
arg6: args[6].toUInt32(), // 固定 4
|
|
arg7_nonce: args[7].toString(16), // nonce = counter|st<<16
|
|
stMs: args[7].shr(16).toString(10), // serviceTime
|
|
nc: args[7].and(0xffff).toUInt32(), // nonce counter
|
|
});
|
|
},
|
|
function (retval) {
|
|
const outHex = this._outPtr ? readStrHex(this._outPtr) : null;
|
|
emit({
|
|
event: 'crypto-otp', stage: 'leave',
|
|
bt: bt.call(this),
|
|
outStr: this._outPtr ? readStr(this._outPtr) : null,
|
|
outHex: outHex ? outHex.hex : '',
|
|
outSize: outHex ? outHex.size : 0,
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 1b) BusinessCfg::getOtp —— 外层包装, 标注调用点
|
|
// -------------------------------------------------------------------------
|
|
hookByName('BusinessCfg6getOtp', 'crypto-getotp',
|
|
function (args) {
|
|
emit({
|
|
event: 'crypto-getotp', stage: 'enter',
|
|
bt: bt.call(this),
|
|
uidLo: args[1].toUInt32(), uidHi: args[1].shr(32).toUInt32(),
|
|
});
|
|
},
|
|
null);
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 2) BusinessCfg::setSafeDeviceId —— 写入 this+1008=safeDeviceId, this+1088=hdid
|
|
// 读回存储值 = WUP t1.t0 的 32hex 源头(若设入的 hdid 即登录 hdid)
|
|
// -------------------------------------------------------------------------
|
|
hookByName('setSafeDeviceId', 'setdi',
|
|
function (args) {
|
|
this._self = args[0];
|
|
const a1 = readStrHex(args[1]);
|
|
const a2 = readStrHex(args[2]);
|
|
const a3 = readStrHex(args[3]);
|
|
const a4 = readStrHex(args[4]);
|
|
emit({
|
|
event: 'setdi', stage: 'enter',
|
|
bt: bt.call(this),
|
|
arg1: a1 ? { str: readStr(args[1]), hex: a1.hex, size: a1.size } : null,
|
|
arg2: a2 ? { str: readStr(args[2]), hex: a2.hex, size: a2.size } : null,
|
|
arg3: a3 ? { str: readStr(args[3]), hex: a3.hex, size: a3.size } : null,
|
|
arg4: a4 ? { str: readStr(args[4]), hex: a4.hex, size: a4.size } : null,
|
|
});
|
|
},
|
|
function (retval) {
|
|
// 读回 BusinessCfg+1088 (hdid) / +1008 (safeDeviceId)
|
|
const self = this._self;
|
|
if (self === null || self.isNull()) return;
|
|
const readSlot = (off) => {
|
|
const p = readStrHex(self.add(off));
|
|
return p ? { str: readStr(self.add(off)), hex: p.hex, size: p.size } : null;
|
|
};
|
|
emit({
|
|
event: 'setdi', stage: 'leave',
|
|
slot1008_safeDeviceId: readSlot(1008),
|
|
slot1088_hdid: readSlot(1088),
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 3) BusinessCfg::getHdid / getSafeDeviceId —— WUP 构建读取点
|
|
// -------------------------------------------------------------------------
|
|
hookByName('getHdid', 'gethdid',
|
|
function (args) {
|
|
this._self = args[0];
|
|
emit({ event: 'gethdid', stage: 'enter', bt: bt.call(this) });
|
|
},
|
|
function (retval) {
|
|
const p = this._self ? readStrHex(this._self.add(1088)) : null;
|
|
emit({ event: 'gethdid', stage: 'leave', slot1088: p ? p.hex : null, size: p ? p.size : 0 });
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 4) AESkeyMgr::getkey —— 表内容(与 15 键表对拍)
|
|
// -------------------------------------------------------------------------
|
|
hookByName('AESkeyMgr6getkey', 'crypto-aeskey',
|
|
function (args) {
|
|
this._sret = this.context.x8;
|
|
emit({ event: 'crypto-aeskey', stage: 'enter', a: args[1].toUInt32(), b: args[2].toUInt32() });
|
|
},
|
|
function (retval) {
|
|
const key = this._sret ? readStr(this._sret) : null;
|
|
emit({ event: 'crypto-aeskey', stage: 'leave', key, b: this._b });
|
|
});
|
|
|
|
// md5/aes 链(与旧脚本一致, 用于交叉验证)
|
|
hookByName('md5_char16', 'crypto-md5',
|
|
function (args) {
|
|
this._outPtr = args[0];
|
|
emit({ event: 'crypto-md5', stage: 'enter', inStr: readStr(args[1]) });
|
|
},
|
|
function (retval) {
|
|
const outHex = this._outPtr ? readStrHex(this._outPtr) : null;
|
|
emit({ event: 'crypto-md5', stage: 'leave', keyHex: outHex ? outHex.hex : '' });
|
|
});
|
|
}
|
|
|
|
function exportOf(name) {
|
|
try {
|
|
if (typeof Module.getGlobalExportByName === 'function') return Module.getGlobalExportByName(name);
|
|
if (typeof Module.findGlobalExportByName === 'function') return Module.findGlobalExportByName(null, name);
|
|
return Module.findExportByName(null, name);
|
|
} catch (_) { return null; }
|
|
}
|
|
|
|
for (const name of ['dlopen', 'android_dlopen_ext']) {
|
|
const address = exportOf(name);
|
|
if (address !== null) {
|
|
Interceptor.attach(address, {
|
|
onEnter() { install(); },
|
|
onLeave() { install(); },
|
|
});
|
|
}
|
|
}
|
|
setImmediate(install);
|
|
// ---- 辅助 (appsign 段用) ----
|
|
function readStdString(addr) {
|
|
try {
|
|
const flag = addr.readU8();
|
|
if ((flag & 1) === 0) { // SSO
|
|
const len = flag >> 1;
|
|
return len === 0 ? '' : addr.readUtf8String(Math.min(len, 64));
|
|
}
|
|
// long (libc++ 经典布局: cap@0-LSB, size@8, data-ptr@16)
|
|
const size = addr.add(8).readU64();
|
|
const cap = addr.add(16).readU64();
|
|
if (size > 1024 * 1024 || cap > 16 * 1024 * 1024) return '<bad:' + size + '>';
|
|
const data = addr.add(16).readPointer();
|
|
return size === 0 ? '' : data.readUtf8String(Math.min(size, 4096));
|
|
} catch (e) { return '<err:' + e.message.slice(0, 30) + '>'; }
|
|
}
|
|
function btBrief() {
|
|
try {
|
|
return Thread.backtrace(this.context, Backtracer.ACCURATE)
|
|
.slice(0, 4).map(a => {
|
|
const m = Process.findModuleByAddress(a);
|
|
const off = m ? a.sub(m.base) : a;
|
|
return m ? m.name + '!' + off : a.toString();
|
|
}).join(' <- ');
|
|
} catch (e) { return ''; }
|
|
}
|
|
|
|
// ================== appSign 真源 hook ==================
|
|
// createWupProtoInfo(ProtoInfo&) @0x273f1c — appSign = HuyaMd5(全局单例3串 + "_" 拼接)
|
|
// 全局单例: base+0x484000+0xe0 → obj; 3 个 std::string @ obj+0x10 / +0x28 / +0x40
|
|
(function () {
|
|
const targetName = 'libudbauthunify.so';
|
|
const mod = Process.findModuleByName(targetName);
|
|
if (mod === null) return;
|
|
const offsets = [0x273f1c, 0x273f18]; // createWupProtoInfo 入口(含stp前)
|
|
const hookAddr = mod.base.add(0x273f1c);
|
|
Interceptor.attach(hookAddr, {
|
|
onEnter() {
|
|
const g = mod.base.add(0x484000 + 0xe0).readPointer();
|
|
if (g.isNull()) { emit({ event: 'appsign', stage: 'global-null', bt: btBrief() }); return; }
|
|
const s1 = readStdString(g.add(0x10));
|
|
const s2 = readStdString(g.add(0x28));
|
|
const s3 = readStdString(g.add(0x40));
|
|
emit({
|
|
event: 'appsign', stage: 'enter',
|
|
proto: this.context.x0.toString(),
|
|
g3: s3, g2: s2, g1: s1,
|
|
joined: (s3 || '') + '_' + (s2 || '') + '_' + (s1 || ''),
|
|
bt: btBrief(),
|
|
});
|
|
},
|
|
});
|
|
emit({ event: 'hook-installed', needle: 'createWupProtoInfo', address: hookAddr.toString(), pid: Process.id, tid: Process.getCurrentThreadId() });
|
|
|
|
// HuyaMd5(const string&) — 输入
|
|
const md5ctor = Process.getModuleByName(targetName).getExportByName('_ZN7HuyaMd5C1ERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE');
|
|
if (md5ctor) {
|
|
Interceptor.attach(md5ctor, {
|
|
onEnter(args) {
|
|
let hex = '';
|
|
try {
|
|
const a = args[1];
|
|
const flag = a.readU8();
|
|
if ((flag & 1) === 0) {
|
|
const len = flag >> 1;
|
|
if (len > 0 && len <= 256) hex = Array.from(new Uint8Array(a.add(1).readByteArray(len))).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
} else {
|
|
const size = a.add(8).readU64();
|
|
if (size <= 256) hex = Array.from(new Uint8Array(a.add(16).readPointer().readByteArray(Number(size)))).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
} catch (e) { hex = '<err>'; }
|
|
emit({ event: 'huya-md5', stage: 'ctor', inHex: hex, inStr: readStdString(args[1]), bt: btBrief() });
|
|
},
|
|
});
|
|
emit({ event: 'hook-installed', needle: 'HuyaMd5C1' });
|
|
}
|
|
// HuyaMd5::toString() — 输出
|
|
const md5ts = Process.getModuleByName(targetName).getExportByName('_ZN7HuyaMd58toStringEv');
|
|
if (md5ts) {
|
|
Interceptor.attach(md5ts, {
|
|
onLeave(retval) {
|
|
let hex = '', str = '';
|
|
try {
|
|
const flag = retval.readU8();
|
|
if ((flag & 1) === 0) {
|
|
const len = flag >> 1;
|
|
if (len > 0) hex = retval.readByteArray(Math.min(len, 64)).then ? '' : Array.from(new Uint8Array(retval.readByteArray(Math.min(len, 64)))).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
} else {
|
|
const size = retval.add(8).readU64();
|
|
const data = retval.add(16).readPointer();
|
|
if (size <= 96) hex = Array.from(new Uint8Array(data.readByteArray(Number(size)))).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
str = readStdString(retval);
|
|
} catch (e) { hex = '<err:' + e.message.slice(0, 40) + '>'; }
|
|
emit({ event: 'huya-md5', stage: 'tostr', outHex: hex, outStr: str });
|
|
},
|
|
});
|
|
emit({ event: 'hook-installed', needle: 'HuyaMd58toString' });
|
|
}
|
|
})();
|