14、键盘与鼠标过滤:PS/2过滤、USB HID过滤、按键记录、鼠标事件拦截、输入保护
键盘和鼠标,是我们和计算机最直接的交互方式。你想想看,每一次按键、每一次鼠标点击,背后都有一整套硬件和驱动的协作。而作为驱动开发者,我们有时候需要介入这个流程——可能是为了记录按键(比如安全审计),可能是为了拦截某些鼠标操作(比如防误触),也可能是为了做输入保护(比如防键盘记录器)。
这一章,我们就来聊聊这些技术。我会从最底层的 PS/2 过滤讲起,再到 USB HID 过滤,最后聊到按键记录和鼠标事件拦截。嗯,这里面的坑不少,我当年也踩过不少。
核心知识点一览:
- PS/2 键盘鼠标过滤驱动架构
- USB HID 过滤驱动的实现思路
- 按键记录与鼠标事件拦截的合法边界
- 输入保护机制的常见方案
14.1 PS/2 过滤驱动:从端口到中断
PS/2 接口,虽然现在新主板越来越少见了,但在工控机、老式笔记本、以及某些安全要求高的场景下,它依然存在。PS/2 的过滤,说白了就是拦截键盘和鼠标与 CPU 之间的通信。
PS/2 设备通过两个 I/O 端口(0x60 和 0x64)与主机通信。0x60 是数据端口,0x64 是命令/状态端口。每次按键或鼠标移动,都会产生一个硬件中断(通常是 IRQ1 给键盘,IRQ12 给鼠标)。
我个人习惯的做法是:写一个过滤驱动,挂载到 i8042prt 或 kbdclass 的驱动栈上。这样你就能在 IRP 层面拦截到所有输入数据。但如果你想要更底层,可以直接挂钩中断处理函数——不过那需要处理很多同步问题,我建议非必要不碰。
小技巧:在 Windows 上,PS/2 过滤驱动最常用的方式是写一个键盘类过滤驱动(kbdclass 的过滤)。你只需要在 AddDevice 里创建一个过滤设备,然后挂到设备栈上。所有按键扫描码都会经过你的 IRP 处理函数。
这里有个关键点:PS/2 的扫描码分两种——Make 码(按下)和 Break 码(松开)。如果你要做按键记录,通常只关心 Make 码。但如果你要做输入保护(比如屏蔽某些键),那就要同时处理 Make 和 Break,否则会出现按键卡住的情况。
我曾经在一个项目中,只过滤了 Make 码,结果用户按下一个键后,系统一直认为那个键被按着。嗯,那场面,挺尴尬的。
14.2 USB HID 过滤:更现代的方式
现在绝大多数键盘鼠标都是 USB 接口,走的是 HID(Human Interface Device)协议。USB HID 过滤比 PS/2 复杂一些,因为数据不是简单的扫描码,而是 HID 报告(Report)。
HID 报告的结构由报告描述符定义。键盘的 HID 报告通常包含 8 个字节:第一个字节是修饰键(Ctrl、Shift 等),后面 6 个字节是当前按下的键。鼠标的报告则包含按键状态和 X/Y 位移。
要过滤 USB HID 设备,我建议的做法是写一个 USB 过滤驱动,绑定到 HID 类驱动(hidusb.sys)或直接绑定到 USB 集线器驱动。更常见的做法是写一个 HID 迷你驱动(HID minidriver),但这需要处理很多 HID 协议细节。
我个人更推荐另一种方式:在应用层通过 ReadFile 或 HidD_GetInputReport 直接读取 HID 报告,然后做过滤。但这种方式有一个问题——你只能读取数据,不能拦截。如果你需要真正拦截(比如不让某个按键到达系统),那还是得写内核驱动。
// 一个简化的 HID 过滤驱动 IRP_MJ_READ 处理示例
NTSTATUS HIDFilterRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
// 先传递 IRP 到下层驱动
IoCopyCurrentIrpStackLocationToNext(Irp);
NTSTATUS status = IoCallDriver(devExt->LowerDeviceObject, Irp);
// 如果 IRP 完成,检查数据
if (NT_SUCCESS(status) && Irp->IoStatus.Information > 0) {
PUCHAR buffer = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;
ULONG length = (ULONG)Irp->IoStatus.Information;
// 解析 HID 报告
// 对于键盘:buffer[0] 是修饰键,buffer[2..7] 是按键码
// 对于鼠标:buffer[0] 是按键状态,buffer[1..2] 是位移
// 这里可以做过滤逻辑
// 比如:如果检测到某个按键,清空 buffer
if (buffer[2] == 0x1E) { // 0x1E 是 'A' 键的 HID 用法 ID
RtlZeroMemory(buffer, length);
Irp->IoStatus.Information = 0;
}
}
return status;
}
警告:在 IRP 完成回调中修改数据要非常小心。如果你在 IoCallDriver 返回后直接修改 buffer,可能会和下层驱动的 DPC 产生竞争。我建议使用完成例程(CompletionRoutine)来处理数据,并在其中使用 IoMarkIrpPending 和 KeWaitForSingleObject 来同步。
14.3 按键记录:技术实现与法律边界
按键记录(Keylogger)是输入过滤中最敏感的话题。技术上,你可以通过以下方式实现:
- 内核层:键盘类过滤驱动、PS/2 中断挂钩、USB HID 报告拦截
- 应用层:SetWindowsHookEx(WH_KEYBOARD_LL)、DirectInput 钩子、轮询 GetAsyncKeyState
从技术角度,内核层的按键记录最可靠,因为它能捕获所有按键,包括登录密码、安全对话框等。但这也意味着它最危险——一旦被恶意软件利用,后果很严重。
我记得有一次,一个客户要求做企业终端的按键审计,用于监控员工是否在输入敏感信息。我当时的方案是写一个键盘类过滤驱动,将按键数据加密后写入一个受保护的系统日志文件。但这里有个关键:绝对不能记录密码框的内容。Windows 的密码框(ES_PASSWORD 样式)在应用层会屏蔽显示,但在内核层,按键数据依然会经过驱动。所以你必须自己判断当前焦点窗口是否在密码框中——这需要结合窗口管理器的信息。
法律提醒:按键记录技术在很多国家和地区受到严格法律限制。未经用户明确同意,记录按键数据可能违反《计算机欺诈和滥用法》《个人信息保护法》等。即使是在企业环境中,也需要有明确的员工知情同意和合法的监控目的。我建议你在任何项目中,都要先和法务团队确认合规性。
14.4 鼠标事件拦截:不只是移动和点击
鼠标事件的拦截,常见场景包括:
- 屏蔽鼠标右键(比如在自助查询机上)
- 限制鼠标移动范围(比如在全屏游戏中锁定光标)
- 拦截鼠标滚轮事件(防止误滚动)
- 模拟鼠标点击(自动化测试)
鼠标事件的拦截方式和键盘类似。在 PS/2 上,鼠标数据通过 0x60 端口读取,数据包格式是 3 字节或 4 字节(取决于鼠标模式)。在 USB HID 上,鼠标报告通常包含 4 个字节:按钮状态、X 位移、Y 位移、滚轮位移。
我做过一个项目,需要在工业触摸屏上屏蔽鼠标右键菜单。触摸屏模拟的是鼠标左键,但有些操作会触发右键事件(比如长按)。我的做法是在 HID 过滤驱动中,将鼠标报告的按钮状态字节中的右键位(bit 1)强制清零。这样系统就永远收不到右键按下的事件。
// 鼠标 HID 报告过滤示例
// 假设报告格式:byte0=按钮, byte1=X, byte2=Y, byte3=滚轮
if (reportLength >= 4) {
UCHAR buttonState = report[0];
// 屏蔽右键(bit 1)
buttonState &= ~0x02;
report[0] = buttonState;
}
小技巧:如果你只是想在应用层拦截鼠标事件,可以使用 SetWindowsHookEx(WH_MOUSE_LL) 设置低级鼠标钩子。这个钩子运行在全局钩子链中,可以拦截所有鼠标事件。但要注意,UAC 提权后的窗口(如任务管理器)不会经过低级钩子。
14.5 输入保护:防记录、防篡改
输入保护,说白了就是防止恶意软件通过键盘记录器或鼠标钩子窃取你的输入。常见的保护手段包括:
| 保护手段 | 原理 | 优缺点 |
|---|---|---|
| 内核层过滤驱动 | 在驱动栈中拦截并加密输入数据 | 安全性高,但实现复杂 |
| 应用层加密输入 | 在应用层对按键进行加密后再传输 | 实现简单,但无法防内核级记录 |
| 虚拟键盘 | 使用屏幕键盘,通过鼠标点击输入 | 防键盘记录,但易被鼠标钩子攻击 |
| 硬件加密键盘 | 键盘内部完成加密,主机只接收密文 | 最安全,但成本高 |
我个人认为,最实用的方案是「内核层过滤驱动 + 应用层加密」的组合。内核驱动负责确保输入数据不被其他驱动拦截或篡改,应用层负责将数据加密后传输到目标程序。这样即使有内核级的键盘记录器,它拿到的也是加密后的数据。
我曾经参与过一个网上银行的输入保护方案。我们在内核层写了一个过滤驱动,拦截所有键盘输入,然后通过一个安全的通道(使用 DPAPI 加密)将按键数据直接发送到浏览器的插件中。这样,即使系统中有键盘记录器,它也只能看到原始的扫描码,而无法获取到真正的输入内容。
注意:输入保护不是万能的。如果攻击者已经拿到了系统内核的控制权(比如通过驱动漏洞),那么任何保护措施都可能被绕过。输入保护的核心是「提高攻击成本」,而不是「绝对安全」。
好了,这一章的内容就到这里。键盘和鼠标过滤,说难不难,说简单也不简单。关键是要理解底层的通信机制,然后根据你的需求选择合适的过滤层级。PS/2 适合老设备,USB HID 是主流,按键记录和鼠标拦截要谨慎使用,输入保护则要结合具体场景来设计。
如果你在实际项目中遇到了什么奇怪的问题,欢迎来和我交流。我这些年踩过的坑,可能正好能帮你避开一个。