☰
Interception 1.0.1 驱动层键鼠拦截实战:从安装到输入录制回放
2026/10/7 22:54:35 网站建设 项目流程

简介:Interception 1.0.1 是一套面向 Windows 平台的开源键盘鼠标输入拦截库,适合从事自动化测试、安全监控、游戏辅助及底层驱动开发的程序员参考。它通过设备驱动接口在低层捕获并操纵硬件输入事件,帮助开发者理解全局钩子、事件回调注册与线程安全等关键技术。压缩包共 44 个文件,约 126KB,以 cpp 与 c 源码、h 头文件、makefile 与 cmd 构建脚本为主,另含 sources 配置、rc 资源脚本、pdf 文档及 readme 说明,覆盖库本体、示例与编译流程。内容预览显示包内提供 interception.c、interception.h 及 samples 目录下的 axes、caps2esc、cadstop 等示例,便于对照学习事件处理与多平台差异。目前已有 308 人学习下载,读者可借此掌握输入拦截的初始化、回调处理与调试排错思路,快速将库集成到自身项目中。

1. 从驱动层截获键鼠:Interception 1.0.1 到底能解决什么

如果你做过按键映射、宏工具、KVM 切换器或者游戏辅助输入层,大概率遇到过同一个天花板:用SetWindowsHookEx挂全局钩子,在普通窗口里跑得好好的,一进全屏游戏、一碰带反作弊的进程、或者遇到以管理员权限运行的窗口,钩子就哑了。这不是代码写错了,而是 Windows 的钩子机制本身有边界——它工作在用户态的消息链上,优先级天然低于内核里真正处理 HID 报告的那一层。Interception 1.0.1 就是冲着这个天花板去的:它把自己做成一个内核态过滤驱动(keyboard.sys/mouse.sys),插在键盘鼠标驱动栈中间,在硬件层下达基本指令之后、系统把输入分发给各个应用之前,把原始的键盘鼠标事件截下来交给你处理。你拿到的是设备级的输入流,不是窗口消息,所以全屏、高权限、多桌面这些场景都不再是障碍。

这份资源的核心是驱动加一套用户态库:驱动负责拦截,用户态通过interception.dll和interception.h跟驱动通信,用interception_send/interception_receive收发事件。它适合三类人:一是要做底层按键重映射、组合键、连发宏的开发者;二是要写输入录制回放、自动化测试工具的人;三是研究输入栈、想做设备级过滤的工程师。它不适合只想改个快捷键的普通用户——装驱动、签名、写代码这套流程对纯使用者来说太重了。下面按「装起来 → 跑通第一个例子 → 处理多设备与上下文 → 避坑 → 进阶」的顺序拆,每一步都落到能复现的命令和代码。

2. 驱动安装与第一个可运行例子:从 inf 到 receive 循环

2.1 安装前必须搞清楚的驱动模型

Interception 的驱动是过滤驱动(filter driver),不是功能驱动。它不替代kbdclass/mouclass,而是挂在这些类驱动之上,所以安装方式跟普通设备驱动不一样。包里通常有install-interception.exe这类安装器,也有对应的.inf和.sys。安装的本质是往注册表里写过滤驱动的 UpperFilter 项,让系统在加载键盘鼠标类驱动时把 Interception 的过滤驱动一起拉起来。这一步决定了后面所有代码能不能收到事件——驱动没挂上,interception_create_context会返回一个无效句柄,interception_receive直接返回 0,你会以为是代码问题,其实是驱动没生效。

安装一般需要管理员权限,装完建议重启一次,让过滤驱动在设备栈重新枚举时挂载。验证是否挂上的办法不是看设备管理器里有没有新设备(过滤驱动通常不显示为独立设备),而是直接跑一段最小代码看能不能收到事件。我一般会先写个只打印设备 ID 和按键码的循环,能打印出来就说明链路通了。

2.2 最小可运行代码:创建上下文并接收事件

下面这段是能直接编译运行的最小例子,作用是拦截键盘事件并原样转发,同时把扫描码打印出来。它不改变任何输入行为,只验证驱动通信是否正常。

#include <stdio.h> #include "interception.h" int main(void) { // 创建与驱动通信的上下文,失败说明驱动没装好或权限不够 InterceptionContext context = interception_create_context(); if (context == NULL) { printf("create_context failed, driver not loaded?\n"); return 1; } // 只过滤键盘,鼠标用 INTERCEPTION_FILTER_MOUSE_ALL interception_set_filter(context, interception_is_keyboard, INTERCEPTION_FILTER_KEY_ALL); InterceptionDevice device; InterceptionStroke stroke; while (interception_receive(context, device = interception_wait(context), &stroke, 1) > 0) { if (interception_is_keyboard(device)) { InterceptionKeyStroke *ks = (InterceptionKeyStroke *)&stroke; printf("device=%d code=0x%04x state=%d\n", device, ks->code, ks->state); } // 原样发回,保证系统仍能收到这次输入 interception_send(context, device, &stroke, 1); } interception_destroy_context(context); return 0; }

逻辑上分四步:interception_create_context建立用户态和驱动的通道;interception_set_filter告诉驱动你关心哪类事件,这里用interception_is_keyboard做设备判定、INTERCEPTION_FILTER_KEY_ALL表示键盘全量事件;interception_wait阻塞等下一个事件,interception_receive把事件读进stroke;最后interception_send把事件转发回去。关键参数是 filter 的粒度——如果你只关心按下,可以用INTERCEPTION_FILTER_KEY_DOWN,减少无谓唤醒;interception_wait返回的是设备句柄,多设备场景下靠它区分是哪个键盘。

编译时链接interception.lib,运行时interception.dll要和 exe 放一起。注意位数必须匹配:驱动和用户态库都是分 32/64 位的,64 位系统上用了 32 位 dll 会出现上下文创建成功但收不到事件的情况,这个坑后面还会细说。

2.3 事件结构里几个容易读错的字段

InterceptionKeyStroke里最常被误用的是state。它不是简单的 0/1,而是包含INTERCEPTION_KEY_DOWN、INTERCEPTION_KEY_UP、INTERCEPTION_KEY_E0、INTERCEPTION_KEY_E1这些标志位。扩展键(比如方向键、右 Ctrl)会带 E0 标志,如果你只判断state == 1来识别按下,扩展键就会漏掉。正确做法是按位与:if (ks->state & INTERCEPTION_KEY_DOWN)。code是扫描码不是虚拟键码,想转成 VK 得自己查表或者用MapVirtualKey,这一点在写映射逻辑时特别关键——很多人拿扫描码当 VK 用,结果映射全错位。

鼠标事件结构InterceptionMouseStroke里,flags区分按键和移动,rolling是滚轮增量,x/y是相对位移。注意鼠标移动是相对量,不是绝对坐标,做绝对定位要自己累加维护状态。

3. 多设备识别与上下文过滤:把「哪个键盘」这件事说清楚

3.1 设备 ID 的分配规律与识别方法

Interception 给每个设备分配一个从 1 开始的 ID,键盘和鼠标共用这个编号空间,靠interception_is_keyboard/interception_is_mouse区分类型。多键盘场景下,ID 的分配顺序跟设备枚举顺序有关,重启或插拔后可能变化,所以不能把「ID=1 就是我的主键盘」写死。稳妥做法是在收到事件时记录设备特征,或者用interception_get_hardware_id拿硬件 ID 做匹配。

// 拿到设备硬件 ID,用于区分具体是哪个键盘 unsigned short hw_id[512]; int len = interception_get_hardware_id(context, device, hw_id, sizeof(hw_id)); if (len > 0) { wprintf(L"hardware id: %s\n", hw_id); }

interception_get_hardware_id返回的是宽字符硬件 ID 字符串,长度写缓冲区字节数。这个 ID 在同一台机器上相对稳定,适合做「只拦截某个特定键盘」的白名单逻辑。参数上注意缓冲区给够,太小会截断,返回长度是实际写入的字节数。

3.2 按设备做差异化过滤

实际项目里经常需要「键盘 A 正常用,键盘 B 走宏逻辑」。做法是在 receive 循环里按设备 ID 分流,而不是全局设一个 filter。filter 是全局的,但你可以对不需要处理的设备直接interception_send转发,对目标设备才做加工。

while (interception_receive(context, device = interception_wait(context), &stroke, 1) > 0) { if (interception_is_keyboard(device)) { if (device == target_keyboard_id) { // 目标键盘:加工后再发,或吞掉不发 InterceptionKeyStroke *ks = (InterceptionKeyStroke *)&stroke; if ((ks->state & INTERCEPTION_KEY_DOWN) && ks->code == 0x1E) { // 把 A 键改成不发,实现屏蔽 continue; } } } interception_send(context, device, &stroke, 1); }

这里continue跳过interception_send就等于吞掉了这次输入,系统收不到。这是实现屏蔽、重映射的基础手法。要注意吞掉按键后,对应的抬起事件如果还转发,系统会收到一个没有按下的抬起,某些应用会异常,所以成对处理按下和抬起是基本纪律。

3.3 上下文与线程模型

interception_receive是阻塞的,一个上下文对应一个事件队列。如果你在主线程里跑 receive 循环,界面就会卡死。常见做法是开独立线程专门跑 receive,主线程通过队列或共享状态跟它交互。多个上下文能不能开?可以,但每个上下文都会独立跟驱动建立通道,事件会被分发到哪个上下文取决于驱动的分发策略,容易乱。我一般一个进程只开一个上下文,用单线程 receive,把处理逻辑做轻,重活丢给工作线程。

提示:interception_wait可以配合超时使用,但标准接口里它是无限阻塞的。需要可中断的循环时,用interception_receive配合设备集合,或者自己在收到事件后检查退出标志。

4. 避坑与排查:驱动装上了却收不到事件的五种情况

4.1 现象:create_context 成功但 receive 一直返回 0

原因通常是位数不匹配。64 位系统上如果编译出 32 位程序,却加载了 64 位驱动,上下文能创建,但事件通道对不上。解决:确认 exe、dll、sys 三者位数一致,64 位系统统一用 64 位。另一个可能是 filter 设错了设备类型,比如对鼠标设了键盘 filter,自然收不到。

4.2 现象:普通窗口能收到,全屏游戏里失效

这通常不是 Interception 的问题,而是游戏用了 Raw Input 或 DirectInput,绕过了你处理的那一层。Interception 工作在类驱动过滤层,理论上在 Raw Input 之下,但某些反作弊会额外挂自己的过滤驱动,顺序在你之上,把你的拦截架空了。解决:确认驱动加载顺序,必要时调整 UpperFilter 的注册顺序;如果对方是内核级反作弊,这条路基本走不通,别硬刚。

4.3 现象:按键吞掉后系统「卡键」

原因就是前面说的,只吞了按下没吞抬起,或者反过来。系统状态机里那个键一直处于按下态,表现为一直触发。解决:维护一个按键状态表,按下和抬起成对处理,程序退出前把所有被吞的键补发抬起事件,这是血泪经验,不加这步调试时能把自己坑死。

4.4 现象:多键盘时事件串台

设备 ID 在插拔后变化,写死的 ID 匹配到了别的键盘。解决:用硬件 ID 做匹配,或者在启动时枚举所有设备建立映射表,运行中监听设备变化重新枚举。别依赖固定 ID。

4.5 现象:安装驱动后系统蓝屏或键鼠失灵

过滤驱动写注册表时如果键路径写错,或者驱动签名有问题,可能导致类驱动加载失败,键鼠直接不能用。解决:装之前建好系统还原点;用官方安装器而不是手动改注册表;出问题进安全模式卸载过滤驱动项。这个坑一旦踩上,没有后悔药,只能靠还原点。

5. 进阶:用 Interception 做输入录制回放与状态机映射

5.1 录制回放的基本结构

录制就是把 receive 到的 stroke 按时间戳存下来,回放就是按时间间隔用interception_send发出去。难点在时间精度和状态一致性。下面是一个简化的录制结构:

typedef struct { DWORD tick; // GetTickCount() 时间戳 int is_keyboard; // 1 键盘 0 鼠标 InterceptionStroke stroke; } RecordedEvent; // 录制循环里 RecordedEvent ev; ev.tick = GetTickCount(); ev.is_keyboard = interception_is_keyboard(device); ev.stroke = stroke; fwrite(&ev, sizeof(ev), 1, fp); interception_send(context, device, &stroke, 1);

回放时按tick差值 sleep,再interception_send。参数上注意InterceptionStroke是联合体,键盘和鼠标共用同一块内存,存的时候要按类型正确解释,读回来也要按is_keyboard分支处理,否则字段会错位。

5.2 用状态机做组合键映射

组合键的本质是状态机:记录当前按下了哪些修饰键,当主键按下时判断是否满足组合条件,满足就改写事件。比如把 CapsLock 当 Ctrl 用:

static int caps_down = 0; if (ks->code == 0x3A) { // CapsLock 扫描码 if (ks->state & INTERCEPTION_KEY_DOWN) { caps_down = 1; ks->code = 0x1D; // 改成左 Ctrl } else { caps_down = 0; ks->code = 0x1D; } }

这里改的是code,转发出去的就是 Ctrl 事件。注意修饰键状态要自己维护,因为系统看到的是你改写后的事件,原始 CapsLock 状态它不知道。复杂组合键建议用一张映射表加状态机,别堆 if-else,否则扩展键和抬起逻辑会把你绕晕。

5.3 验证方法:怎么确认拦截真的生效

最直接的验证是写一个只打印不转发的程序,跑起来后敲键盘,如果系统完全没反应但程序在打印,说明拦截生效了。再逐步加转发,观察系统行为恢复。另一个办法是用mouse test lab这类输入测试工具观察原始事件,对比拦截前后的差异。我习惯在改动映射逻辑后,先用一个「透传模式」跑一遍,确认所有按键都能正常转发,再开映射,这样能把「拦截问题」和「映射逻辑问题」分开定位。

从那以后我每次动输入层代码,都强制先跑一遍透传验证,再上映射,退出前补发所有按下键的抬起事件。这套习惯帮我省了无数次重启。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询