'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);