Interception 驱动层键盘鼠标拦截与重映射实战指南

发布时间:2026/10/8 16:47:10
Interception 驱动层键盘鼠标拦截与重映射实战指南 简介Interception-1.0.1 是一套面向 Windows 平台的开源输入拦截库聚焦键盘与鼠标硬件事件的低层捕获、过滤与重定向适合具备一定 C/C 基础、从事自动化测试、安全监控或输入设备定制的开发者参考。压缩包共 44 个文件约 126KB以 cpp 源码、h 头文件、makefile 与 sources 构建配置为主辅以 cmd 编译脚本、c 工具源码、pdf 文档及 readme 说明覆盖库本体、示例程序与构建流程。内容包含 interception.c/h 核心实现、samples 下的 axes、caps2esc、cadstop 等示例以及 tools 目录中的辅助工具便于理解回调注册、事件处理与钩子机制。目前已有 308 人学习下载可帮助读者快速掌握设备驱动接口调用、跨平台差异与线程安全等要点并借助现成示例搭建自己的输入拦截方案。1. Interception 键盘鼠标驱动从内核拦截到应用层重映射的完整路径如果你做过键盘改键、鼠标宏、多设备切换或者游戏外设的定制逻辑大概率绕不开一个名字Interception。这个项目的标题Interception-1.0.1_interception_keyboard_mouse_指向的正是它的核心能力——在 Windows 驱动层拦截键盘和鼠标的原始输入流然后由你在应用层决定这些事件是放行、修改还是彻底吞掉。它解决的不是“模拟按键”这种表层问题而是“在系统看到输入之前就拿到控制权”的底层需求。适合谁做外设固件验证的嵌入式工程师、需要做输入重映射的工具开发者、以及想搞清楚键盘鼠标事件从硬件到应用到底经过哪些环节的进阶玩家。热搜里频繁出现的 interception、keyboard、mouse 三个词恰好对应了它的三个关键词拦截框架、键盘通道、鼠标通道。2. Interception 的驱动层拦截原理与最小可跑环境2.1 为什么应用层钩子不够用从 Raw Input 到驱动过滤很多人第一反应是用SetWindowsHookEx或者 Raw Input API 来做键盘鼠标的拦截和重映射。这两个方案在普通场景下确实能用但有几个硬伤WH_KEYBOARD_LL这类低级钩子依赖消息循环一旦目标进程卡住或者钩子链被破坏输入就会延迟甚至丢失Raw Input 虽然能拿到设备原始数据但你只能“看”不能“改”更没法阻止事件继续传递给系统。Interception 的思路完全不同——它把自己注册成一个键盘类过滤驱动keyboard class filter driver和一个鼠标类过滤驱动挂在 Windows 输入栈的kbdclass和mouclass之上。当硬件产生扫描码、驱动栈开始向上传递时Interception 的过滤驱动先拿到数据你可以选择修改、丢弃或者原样放行。这意味着你拿到的是最原始的扫描码和鼠标位移增量而不是已经被系统翻译过的虚拟键码。这个架构带来的直接好处是你的处理逻辑运行在用户态但拦截点在内核态。用户态程序通过CreateFile打开\\.\interception00这类设备名用DeviceIoControl发送IOCTL_SET_EVENT之类的控制码来读写事件。驱动本身不做任何策略判断它只负责把事件从内核队列搬到用户态缓冲区再把你的决定写回去。这种“内核拦截、用户态决策”的分工既避免了在内核里写复杂逻辑的风险又保证了拦截的实时性和完整性。2.2 编译与安装从源码到可加载驱动的完整命令Interception 的源码结构很清晰driver/目录下是驱动工程library/是用户态通信库samples/里有一些示例。编译驱动需要 Windows Driver KitWDK版本要和你的目标系统匹配。我一般用 Visual Studio 2019 WDK 2004 的组合在 x64 Native Tools 命令行里操作。# 进入 driver 目录用 msbuild 编译驱动工程 cd interception/driver msbuild interception.sln /p:ConfigurationRelease /p:Platformx64 # 编译完成后在 x64/Release 下会得到 interception.sys 和 interception.inf # 安装驱动需要管理员权限用 pnputil 或者 devcon pnputil /add-driver x64/Release/interception.inf /install驱动安装成功后在设备管理器里能看到“Interception Keyboard Filter”和“Interception Mouse Filter”两个设备。用户态库的编译更简单直接用 CMake 或者 VS 工程生成interception.lib和interception.dll即可。这里有个关键点驱动安装后需要重启才能生效因为键盘鼠标过滤驱动是在系统启动时加载的。如果你只是编译了用户态库而没有正确安装驱动调用interception_create_context()会返回 NULL错误码通常是ERROR_FILE_NOT_FOUND。2.3 用户态通信打开设备、设置过滤、读写事件用户态和驱动通信的核心是interception_create_context()和interception_set_filter()。前者打开设备句柄后者告诉驱动你关心哪些事件。下面是一个最小可跑的 C 代码片段展示如何捕获键盘按键并打印扫描码。#include interception.h #include stdio.h int main() { InterceptionContext context; InterceptionDevice device; InterceptionStroke stroke; // 创建上下文内部会打开 \\.\interception00 到 interception0F context interception_create_context(); if (context NULL) { printf(驱动未安装或权限不足\n); return 1; } // 设置过滤只关心键盘设备且只关心按键按下事件 interception_set_filter(context, interception_is_keyboard, INTERCEPTION_FILTER_KEY_DOWN); while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { InterceptionKeyStroke *ks (InterceptionKeyStroke *)stroke; printf(设备 %d 扫描码 0x%02X 状态 %d\n, device, ks-code, ks-state); // 原样放行不修改 interception_send(context, device, stroke, 1); } interception_destroy_context(context); return 0; }这段代码的逻辑很直白interception_wait阻塞等待事件interception_receive把事件从驱动队列读到stroke结构体interception_send把事件写回驱动栈。如果你不调用interception_send事件就被吞掉了系统和其他应用完全感知不到。参数方面interception_set_filter的第二个参数是设备类型判断函数第三个参数是过滤掩码。键盘的掩码有INTERCEPTION_FILTER_KEY_DOWN、INTERCEPTION_FILTER_KEY_UP、INTERCEPTION_FILTER_KEY_E0等鼠标则有INTERCEPTION_FILTER_MOUSE_MOVE、INTERCEPTION_FILTER_MOUSE_LEFT_BUTTON_DOWN等。你可以用按位或组合多个掩码。注意interception_send必须在interception_receive之后调用且要传入相同的device和stroke。如果你修改了stroke的内容再发送系统看到的就是修改后的事件。3. 键盘通道实战扫描码重映射与组合键拦截3.1 扫描码到虚拟键码Set 1 与 Set 2 的转换逻辑Interception 拿到的是键盘扫描码不是虚拟键码。键盘扫描码有两套Set 1 是 IBM PC/XT 标准Set 2 是 IBM AT 标准。现代 USB 键盘内部用 Set 2但经过 i8042 控制器或者 USB HID 转换后Windows 输入栈看到的通常是 Set 1 的扫描码。比如Esc键的 Set 1 扫描码是0x01A键是0x1E左 Shift是0x2A。Interception 的InterceptionKeyStroke结构体里code字段就是扫描码state字段表示按下还是抬起INTERCEPTION_KEY_DOWN或INTERCEPTION_KEY_UP。如果你想把Caps Lock改成Esc不能直接改虚拟键码因为驱动层还没有虚拟键码的概念。你需要做的是捕获Caps Lock的扫描码0x3A然后构造一个Esc的扫描码0x01事件发送回去。但这里有个坑Caps Lock在系统里有锁定状态你只改扫描码的话系统仍然会切换 Caps Lock 指示灯。正确的做法是同时吞掉原始的Caps Lock按下和抬起事件然后手动发送Esc的按下和抬起事件。下面是一个完整的重映射示例。// 将 Caps Lock 重映射为 Esc while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { InterceptionKeyStroke *ks (InterceptionKeyStroke *)stroke; if (ks-code 0x3A) { // Caps Lock 扫描码 // 构造 Esc 事件 InterceptionKeyStroke esc_stroke; esc_stroke.code 0x01; // Esc 扫描码 esc_stroke.state ks-state; // 保持按下/抬起状态一致 esc_stroke.information 0; // 发送 Esc 事件吞掉原始 Caps Lock 事件 interception_send(context, device, (InterceptionStroke *)esc_stroke, 1); } else { // 其他按键原样放行 interception_send(context, device, stroke, 1); } }这段代码的关键在于esc_stroke.state必须和原始事件的state一致否则会出现“按下 Esc 但系统认为一直按着”或者“抬起 Esc 但系统没收到按下”的异常。另外information字段一般填 0它用于传递额外的设备信息普通重映射用不到。3.2 组合键拦截用状态机实现 CtrlAltDel 的替代逻辑拦截组合键比单键重映射复杂因为你需要跟踪多个键的按下和抬起顺序。比如你想实现“CtrlShiftQ 触发某个自定义动作”就需要维护一个状态机当 Ctrl 按下时记录状态Shift 按下时更新状态Q 按下时检查前两个状态是否都满足。Interception 的事件是逐个到达的所以状态机必须放在循环外面用静态变量或者结构体保存。typedef struct { int ctrl_pressed; int shift_pressed; } ModifierState; ModifierState state {0, 0}; while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { InterceptionKeyStroke *ks (InterceptionKeyStroke *)stroke; // 更新修饰键状态 if (ks-code 0x1D) { // 左 Ctrl state.ctrl_pressed (ks-state INTERCEPTION_KEY_DOWN); } if (ks-code 0x2A) { // 左 Shift state.shift_pressed (ks-state INTERCEPTION_KEY_DOWN); } // 检查组合键CtrlShiftQ if (ks-code 0x10 ks-state INTERCEPTION_KEY_DOWN) { // Q 键 if (state.ctrl_pressed state.shift_pressed) { printf(触发自定义动作\n); // 吞掉 Q 键事件不发送给系统 continue; } } // 其他事件原样放行 interception_send(context, device, stroke, 1); }这里有个细节continue跳过了interception_send所以 Q 键事件被完全吞掉。但 Ctrl 和 Shift 的按下/抬起事件仍然会发送给系统这可能导致某些应用看到“按了 Ctrl 和 Shift 但没按 Q”的状态。如果你希望完全隔离这个组合键需要在检测到组合键时把之前已经发送的 Ctrl 和 Shift 事件也“撤销”掉——但 Interception 不支持撤销已发送的事件。所以更稳妥的做法是在组合键触发时发送 Ctrl 和 Shift 的抬起事件模拟用户松开了这两个键。3.3 键盘指示灯的同步问题与解决方案用 Interception 做键盘重映射时指示灯同步是个容易被忽略的坑。比如你把Num Lock重映射为其他键但系统仍然认为Num Lock被按下了指示灯会亮起而实际功能已经变了。更麻烦的是如果你吞掉了Num Lock的按下事件但没有发送抬起事件系统会认为Num Lock一直处于按下状态导致键盘重复率异常。解决方案是对于有锁定状态的键Caps Lock、Num Lock、Scroll Lock在重映射时同时处理按下和抬起事件并且手动发送一个“虚假”的抬起事件来重置系统状态。具体做法是当检测到Num Lock按下时吞掉原始事件发送目标键的按下和抬起事件然后再发送一个Num Lock的抬起事件。这样系统会认为Num Lock被按了一下又松开了指示灯状态不会改变。如果你希望指示灯跟随目标键的状态那就需要更复杂的逻辑跟踪目标键的锁定状态然后手动控制Num Lock的指示灯。Interception 本身不提供直接控制指示灯的 API但你可以通过发送Num Lock的按下/抬起事件来间接控制。4. 鼠标通道实战位移增量处理与多设备区分4.1 鼠标事件结构位移、按键与滚轮的编码方式Interception 的鼠标事件用InterceptionMouseStroke结构体表示字段包括flags、rolling、x、y、information。flags是一个位掩码表示事件类型INTERCEPTION_MOUSE_MOVE表示移动INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN表示左键按下INTERCEPTION_MOUSE_WHEEL表示滚轮。x和y是相对位移增量不是绝对坐标。rolling是滚轮增量正值表示向上滚负值表示向下滚。这里有个关键点鼠标移动事件是高频事件一秒钟可能产生几百个。如果你在用户态处理太慢驱动队列会溢出导致鼠标卡顿。所以鼠标处理逻辑要尽量轻量避免在循环里做复杂计算或者阻塞操作。我一般会把鼠标事件先缓存到一个环形缓冲区然后用单独的线程做处理主循环只负责读写驱动。// 鼠标事件处理将垂直滚动转换为水平滚动 while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { InterceptionMouseStroke *ms (InterceptionMouseStroke *)stroke; if (ms-flags INTERCEPTION_MOUSE_WHEEL) { // 将垂直滚动转换为水平滚动 InterceptionMouseStroke h_stroke *ms; h_stroke.flags INTERCEPTION_MOUSE_HWHEEL; h_stroke.rolling ms-rolling; // 保持滚动量 interception_send(context, device, (InterceptionStroke *)h_stroke, 1); } else { interception_send(context, device, stroke, 1); } }这段代码把垂直滚轮事件改成了水平滚轮事件。INTERCEPTION_MOUSE_HWHEEL是水平滚轮的标志系统收到后会执行水平滚动。注意rolling字段的符号正值向上/向右负值向下/向左。4.2 多鼠标设备区分用 device 编号做独立映射Interception 支持同时拦截多个键盘和鼠标设备。每个设备在打开时会被分配一个编号interception_wait返回的device参数就是设备编号。键盘设备的编号范围是 1 到 10鼠标设备是 11 到 20。你可以用interception_is_keyboard(device)和interception_is_mouse(device)来判断设备类型。这个特性在多鼠标场景下非常有用。比如你有两个鼠标一个普通鼠标用于日常操作一个轨迹球用于绘图。你可以让轨迹球的移动事件乘以一个缩放系数实现更精细的控制而普通鼠标保持原样。while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { InterceptionMouseStroke *ms (InterceptionMouseStroke *)stroke; // 假设设备 11 是轨迹球设备 12 是普通鼠标 if (device 11 (ms-flags INTERCEPTION_MOUSE_MOVE)) { // 轨迹球移动量减半实现精细控制 ms-x ms-x / 2; ms-y ms-y / 2; } interception_send(context, device, stroke, 1); }这里有个坑设备编号不是固定的每次系统启动或者设备插拔后可能会变化。如果你硬编码设备编号换一个 USB 口就可能失效。更可靠的做法是用interception_get_hardware_id获取设备的硬件 ID然后根据硬件 ID 来区分设备。硬件 ID 是一个字符串包含 VID、PID 和序列号等信息。4.3 鼠标加速与原始位移如何绕过系统指针加速Windows 默认会对鼠标移动应用指针加速pointer acceleration也就是移动速度越快指针移动距离越大。这个特性在日常使用中很舒服但在游戏或者绘图场景下会带来问题你无法精确控制指针位置。Interception 拿到的是原始位移增量但如果你直接把这些增量发送回去系统仍然会应用加速。要绕过加速需要在发送事件时设置INTERCEPTION_MOUSE_MOVE_ABSOLUTE标志把相对位移转换成绝对坐标。绝对坐标的范围是 0 到 65535对应屏幕的左上角和右下角。你需要维护一个虚拟的指针位置每次收到相对位移时更新这个位置然后发送绝对坐标事件。// 维护虚拟指针位置绕过系统加速 int virtual_x 32768; // 屏幕中心 int virtual_y 32768; while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { InterceptionMouseStroke *ms (InterceptionMouseStroke *)stroke; if (ms-flags INTERCEPTION_MOUSE_MOVE) { // 更新虚拟位置 virtual_x ms-x; virtual_y ms-y; // 限制在 0-65535 范围内 if (virtual_x 0) virtual_x 0; if (virtual_x 65535) virtual_x 65535; if (virtual_y 0) virtual_y 0; if (virtual_y 65535) virtual_y 65535; // 构造绝对坐标事件 InterceptionMouseStroke abs_stroke *ms; abs_stroke.flags INTERCEPTION_MOUSE_MOVE_ABSOLUTE; abs_stroke.x virtual_x; abs_stroke.y virtual_y; interception_send(context, device, (InterceptionStroke *)abs_stroke, 1); } else { interception_send(context, device, stroke, 1); } }这个方案的缺点是绝对坐标模式下的鼠标移动会失去“无限滚动”的特性因为指针被限制在屏幕范围内。如果你需要无限滚动就不能用绝对坐标只能接受系统加速。另一个折中方案是在应用层做反向加速计算根据当前移动速度动态调整位移增量抵消系统的加速效果。但这个计算比较复杂而且不同 Windows 版本的加速曲线不一样需要做大量测试。5. 避坑与排查Interception 落地时最容易翻车的五个地方5.1 驱动签名与测试模式为什么你的驱动加载失败现象用pnputil安装驱动时提示“无法安装驱动因为驱动未签名”或者“驱动签名无效”。原因Windows 10 1607 之后内核模式驱动必须经过微软的 WHQL 签名或者使用 EV 证书签名否则无法加载。Interception 的源码编译出来的驱动是未签名的只能用于测试。解决方案在测试环境下用bcdedit /set testsigning on开启测试签名模式然后重启。重启后桌面右下角会显示“测试模式”水印这是正常的。如果你不想看到水印可以用bcdedit /set testsigning off关闭但驱动会停止加载。生产环境必须购买代码签名证书对驱动进行签名。5.2 事件丢失与队列溢出鼠标卡顿的根因分析现象鼠标移动时偶尔卡顿或者键盘按键丢失。原因Interception 的驱动队列有大小限制默认是 1024 个事件。如果你的用户态程序处理速度跟不上事件产生速度队列就会溢出新事件被丢弃。鼠标移动事件频率很高一秒钟可能产生 500 个以上如果每个事件都做复杂处理很容易溢出。解决方案优化用户态处理逻辑把耗时操作放到单独线程增大驱动队列大小需要修改驱动源码中的QUEUE_SIZE宏并重新编译或者用interception_set_filter只拦截你关心的事件减少事件量。5.3 键盘重复率异常吞掉抬起事件的后果现象重映射某个键后该键的重复率变得异常快或者异常慢。原因你吞掉了按下事件但没有吞掉抬起事件或者反过来。系统认为某个键一直处于按下状态触发了键盘重复。解决方案对于每个重映射的键必须同时处理按下和抬起事件。如果你只想拦截按下事件那抬起事件也要原样放行否则系统会认为键卡住了。更稳妥的做法是在重映射逻辑中同时检查state字段确保按下和抬起事件成对处理。5.4 多设备编号漂移硬编码 device 的隐患现象程序在开发机上运行正常换一台机器或者换一个 USB 口后设备编号变了逻辑失效。原因Interception 的设备编号是系统启动时动态分配的取决于设备的枚举顺序。解决方案不要硬编码设备编号用interception_get_hardware_id获取硬件 ID然后根据 VID/PID 来匹配设备。硬件 ID 的格式类似HID\VID_046DPID_C52BREV_2400你可以用字符串匹配来识别特定设备。5.5 与安全软件冲突为什么某些游戏的反作弊会拦截你的驱动现象安装 Interception 驱动后某些游戏无法启动或者提示“检测到非法驱动”。原因Interception 的驱动行为类似于键盘记录器某些反作弊系统如 Easy Anti-Cheat、BattlEye会检测并拦截这类驱动。解决方案在游戏运行时卸载 Interception 驱动或者把游戏加入白名单。但更根本的问题是Interception 的驱动没有隐藏自身任何安全软件都能看到它的设备名和驱动文件。如果你需要在不被检测的情况下使用那就要做驱动隐藏但这涉及到更复杂的内核技术而且可能违反游戏的服务条款。6. 进阶技巧用 Interception 做输入录制与回放6.1 事件时间戳与录制格式设计Interception 的事件结构体里没有时间戳字段但你可以用QueryPerformanceCounter或者GetTickCount64在用户态打时间戳。录制时我一般把事件和时间戳一起写入一个二进制文件格式是每行 8 字节时间戳 4 字节设备编号 4 字节事件类型 8 字节事件数据。这样回放时可以精确控制事件间隔。// 录制事件到文件 FILE *fp fopen(recording.bin, wb); LARGE_INTEGER freq, counter; QueryPerformanceFrequency(freq); while (interception_receive(context, device interception_wait(context), stroke, 1) 0) { QueryPerformanceCounter(counter); double timestamp (double)counter.QuadPart / freq.QuadPart; // 写入时间戳、设备编号、事件数据 fwrite(timestamp, sizeof(double), 1, fp); fwrite(device, sizeof(InterceptionDevice), 1, fp); fwrite(stroke, sizeof(InterceptionStroke), 1, fp); interception_send(context, device, stroke, 1); } fclose(fp);回放时读取文件按照时间戳的间隔发送事件。这里的关键是回放速度可以调整比如 2 倍速回放就是把时间间隔除以 2。但要注意如果回放速度太快驱动队列可能溢出需要加适当的延迟。6.2 回放时的时序控制与精度问题回放的最大挑战是时序精度。Windows 不是实时操作系统Sleep函数的精度只有 15.6 毫秒左右对于需要毫秒级精度的回放来说不够。解决方案是用timeBeginPeriod(1)把系统定时器精度提高到 1 毫秒然后用忙等待或者Sleep(0)配合QueryPerformanceCounter来做精确延时。// 高精度回放 timeBeginPeriod(1); // 提高定时器精度 double last_time 0; while (fread(timestamp, sizeof(double), 1, fp) 1) { fread(device, sizeof(InterceptionDevice), 1, fp); fread(stroke, sizeof(InterceptionStroke), 1, fp); // 计算需要等待的时间 double wait_time (timestamp - last_time) / playback_speed; if (wait_time 0) { DWORD wait_ms (DWORD)(wait_time * 1000); if (wait_ms 0) Sleep(wait_ms); // 忙等待剩余时间 LARGE_INTEGER start, now; QueryPerformanceCounter(start); double target wait_time; do { QueryPerformanceCounter(now); } while ((double)(now.QuadPart - start.QuadPart) / freq.QuadPart target); } interception_send(context, device, stroke, 1); last_time timestamp; } timeEndPeriod(1);这个方案的回放精度可以做到 1 毫秒以内对于大多数输入回放场景足够了。但如果你需要微秒级精度那就只能上实时内核或者用 FPGA 来做了。6.3 录制回放的典型应用自动化测试与输入分析录制回放最直接的应用是自动化测试。比如你开发了一个键盘宏工具可以用 Interception 录制一组按键序列然后在测试环境中回放验证工具的行为是否符合预期。另一个应用是输入分析录制用户的实际操作统计按键频率、鼠标移动轨迹、点击热区等。这些数据可以用来优化 UI 布局或者做用户行为研究。我自己的习惯是在开发任何输入相关的工具时先用 Interception 录一段基准数据然后每次修改代码后回放同样的数据对比输出结果。这样可以快速发现回归问题比手动测试靠谱得多。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询