Files

129 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 遥控器 HID 报文旁路(Frida 脚本,挂在 WUDFHost.exe 里)
*
* 原理:BLE HID(HOGP)在 Windows 上是 UMDF 驱动,跑在 WUDFHost.exe 中。
* 驱动通过 ntdll!NtDeviceIoControlFile + IOCTL 0x80018483 把 GATT 特征
* (= HID 输入报告)读上来,报告就在这次调用的**输出缓冲区**里。
* 在那个缓冲区上抄一份,就绕开了整个 HID 映射层。
*
* 只做两件事:
* 1. 把 3 字节的遥控器报告送给 Python(内容有变化才送,避免空闲帧刷屏)
* 2. 把「已被映射的键」的 usage 原地写 0 —— 驱动以为没按键,原生动作
* 不再发生,于是不会「原生 + 映射」双发(音量键就不会顺带调音量)
*
* 为什么只认 3 字节:同一个 WUDFHost 还服务别的蓝牙键鼠,它们吐 9 字节
* 键盘报告,混在一起。遥控器是纯 Consumer Control,固定 3 字节。
*/
const READ_IOCTL = 0x80018483;
let lastRaw = null;
let sent = 0;
let total = 0;
let blockCount = 0;
let blockUsages = {};
/* 诊断计数:真机上出现过"钩子挂上了、界面显示就绪、但一个按键报文都收不到"
* (另一支遥控器 / 挂错了宿主)。只看成功与否分不清是哪种,所以把原始计数报给 Python:
* ioctl = 命中"遥控器那个读 IOCTL"的次数(0 → 多半挂错了宿主)
* lens = 该 IOCTL 的输出长度分布(出现 3 以外的长度 → 报文格式跟预期不同)
* weird = 3 字节但首字节不是 0x02 的样例(最多留 6 条) */
let ioctl = 0;
let lens = {};
let weird = {};
/* 见过的 usage 清单(usage → 次数)。用来"认人":日志里把这台遥控器**实际发过哪些键**
* 列出来,兼容款/山寨款多出来的那些 usage(比如语音键)就能一眼看到 —— 换遥控器时不用猜。 */
let seen = {};
function toHex(ptr, len) {
const b = new Uint8Array(ptr.readByteArray(len));
let s = "";
for (let i = 0; i < b.length; i++) {
s += b[i].toString(16).padStart(2, "0");
if (i + 1 < b.length) s += " ";
}
return s;
}
/* 若这份报告是「已被映射的键」,把 usage 两字节写 0,抹掉原生动作。 */
function nullify(ptr, len) {
if (ptr.isNull() || len !== 3) return;
try {
const b = new Uint8Array(ptr.readByteArray(3));
if (b[0] !== 0x02) return; // 不是遥控器那份报告
const usage = b[1] | (b[2] << 8);
if (!usage || !blockUsages[usage]) return;
ptr.writeByteArray([0x00, 0x00]); // usage_lo / usage_hi = 0
blockCount++;
} catch (e) {
/* 写失败就算了:最坏情况是原生动作也会发生一次 */
}
}
recv(function (msg) {
if (msg && msg.type === "block") {
const next = {};
(msg.usages || []).forEach(function (u) { next[u >>> 0] = true; });
blockUsages = next;
send({ kind: "block_ack", count: Object.keys(next).length });
}
});
const ntdll = Process.findModuleByName("ntdll.dll");
const target = ntdll ? ntdll.findExportByName("NtDeviceIoControlFile") : null;
if (target === null) {
send({ kind: "error", message: "没找到 ntdll!NtDeviceIoControlFile" });
} else {
send({ kind: "ready", pid: Process.id });
Interceptor.attach(target, {
onEnter(args) {
// 0=FileHandle 1=Event 2=ApcRoutine 3=ApcContext 4=IoStatusBlock
// 5=IoControlCode 6=InBuf 7=InLen 8=OutBuf 9=OutLen
total++;
if (args[5].toUInt32() === READ_IOCTL) {
this.cap = true;
this.out = args[8];
this.outLen = args[9].toUInt32();
ioctl++;
}
},
onLeave(retval) {
if (!this.cap) return;
this.cap = false;
const n = this.outLen;
lens[n] = (lens[n] || 0) + 1;
// 3 字节且首字节是 0x02 的,把它的 usage 记进"见过哪些键"(含没被映射的)
if (n === 3 && !this.out.isNull()) {
try {
const b3 = new Uint8Array(this.out.readByteArray(3));
if (b3[0] === 0x02) {
const u = b3[1] | (b3[2] << 8);
if (u) seen[u] = (seen[u] || 0) + 1;
} else if (Object.keys(weird).length < 6) {
weird[toHex(this.out, 3)] = 1;
}
} catch (e) { /* 读不到就算了 */ }
}
// 只认 3 字节 + 成功返回;其它长度是同一宿主里别的蓝牙键鼠
if (retval.toUInt32() !== 0 || this.outLen !== 3 || this.out.isNull()) return;
let raw;
try {
raw = toHex(this.out, 3);
} catch (e) {
return;
}
if (raw !== lastRaw) { // 内容有变化才上报:空闲帧不再刷屏
lastRaw = raw;
sent++;
send({ kind: "report", raw: raw });
}
// 快照已发出,现在才可以安全改写输出缓冲区
nullify(this.out, this.outLen);
}
});
}
// 轻量心跳:只给 Python 做健康判断用(20 秒一次,可忽略不计)
setInterval(function () {
send({ kind: "hb", total: total, sent: sent, blocked: blockCount,
ioctl: ioctl, lens: lens, weird: weird, seen: seen });
}, 20000);