Interception驱动:内核级键盘鼠标拦截与注入实战

发布时间:2026/10/7 8:40:55
Interception驱动:内核级键盘鼠标拦截与注入实战 简介Interception-1.0.1 是一套面向底层输入开发的开源库与示例合集聚焦键盘、鼠标硬件事件的低级别拦截与操控适合具备一定 C/C 基础、希望研究设备驱动接口与系统钩子技术的开发者可应用于自动化测试、安全监控及输入重映射等场景。压缩包共 44 个文件约 126KB以 cpp 源码、h 头文件、makefile 与 sources 构建配置为主辅以 cmd 构建脚本、c 工具源码、pdf 文档及 readme 说明覆盖库接口定义、编译链接与示例演示等环节。内容预览显示包内包含 interception 核心库、samples 示例目录以及 axes、caps2esc、cadstop 等工具模块便于读者理解事件回调注册、输入捕获流程与多平台构建差异。目前已有 308 人学习下载可作为研究输入拦截与钩子技术的实践参考。1. Interception 驱动到底解决什么问题从键盘鼠标事件被拦截说起如果你做过按键精灵、宏工具、远程桌面、KVM 切换器或者写过游戏辅助类的输入模拟大概率遇到过同一个玄学问题用SendInput或keybd_event发出去的键鼠事件在某些窗口里就是没反应。尤其是目标程序带反作弊、带 Raw Input 采集、或者跑在更高权限的会话里时你发的消息像石沉大海。Interception 这个项目的定位就是绕开 Win32 消息层直接在驱动层把键盘鼠标的输入流接管过来让你能读原始事件、也能注入伪造事件。标题里的Interception-1.0.1_interception_keyboard_mouse_指的就是这套驱动加库的组合核心能力是键盘和鼠标两个设备的底层拦截与重放。它适合谁适合那些被用户态 API 卡住、需要确定性输入注入的工程师而不是只想做个简单热键的普通脚本作者。这一章先把「为什么用户态不够用」讲透后面再落到编译、安装、写代码和排错。2. 驱动层拦截与用户态模拟的差别为什么 SendInput 会失效2.1 输入栈的分层从硬件中断到窗口消息Windows 的输入路径大致是硬件产生中断HID 驱动解析成输入报告再由kbdclass/mouclass这类类驱动向上传递经过 Raw Input 线程、win32k的消息队列最后才变成WM_KEYDOWN、WM_MOUSEMOVE投递到窗口过程。用户态的SendInput是在消息队列这一层做文章它伪造的是「已经解析完的输入事件」。问题在于很多程序不从这里取数据。带 Raw Input 注册的程序直接读 HID 报告反作弊驱动会校验事件来源高完整性级别进程会过滤低完整性进程注入的消息。这就是为什么你明明调用了SendInput目标窗口却毫无反应。Interception 的做法是把自己插到类驱动和上层之间作为一个过滤驱动filter driver挂载到键盘和鼠标设备栈上。它拿到的是还没被win32k加工过的输入数据同时也能把自定义事件往下游塞。这个位置决定了它能做到用户态做不到的事跨会话、跨完整性级别、对 Raw Input 可见。代价是它需要内核驱动签名安装时要管理员权限卸载不干净会留下设备残留。2.2 选型对比Interception 与几种常见替代方案方案作用层对 Raw Input 可见需要签名典型失效场景SendInput用户态消息队列否否反作弊、Raw Input 程序驱动级模拟Interception内核过滤驱动是是驱动未正确安装硬件模拟器Arduino 等物理 USB是不涉及需要外接硬件远程桌面协议注入会话层部分否本地独占程序从表里能看出来Interception 的定位是「纯软件、驱动层、对上层透明」。它不像硬件模拟器那样需要额外设备也不像SendInput那样容易被识别。代价就是驱动签名和安装复杂度。我一般会建议如果目标程序不涉及反作弊先用SendInput快速验证逻辑一旦发现事件被吞再上 Interception。不要一上来就搞驱动调试成本差一个数量级。2.3 最小验证确认驱动是否真的在工作在写任何业务代码之前先确认驱动装好了、设备能打开。Interception 安装后会创建\\.\interception00这类设备接口键盘和鼠标各占一个。下面这段代码用来枚举并打开设备确认驱动层可达。#include interception.h #include stdio.h int main() { InterceptionContext ctx interception_create_context(); if (ctx NULL) { printf(create_context failed, driver not loaded?\n); return 1; } // 遍历所有键盘设备打印设备 ID InterceptionDevice device; for (device INTERCEPTION_KEYBOARD(0); device INTERCEPTION_KEYBOARD(INTERCEPTION_MAX_KEYBOARD - 1); device) { if (interception_is_keyboard(device)) { printf(keyboard device id: %d\n, device); } } // 鼠标设备同理 for (device INTERCEPTION_MOUSE(0); device INTERCEPTION_MOUSE(INTERCEPTION_MAX_MOUSE - 1); device) { if (interception_is_mouse(device)) { printf(mouse device id: %d\n, device); } } interception_destroy_context(ctx); return 0; }逻辑说明interception_create_context会尝试打开驱动设备返回 NULL 说明驱动没装好或权限不够。INTERCEPTION_KEYBOARD(i)是个宏把索引映射成设备 ID。interception_is_keyboard用来过滤掉无效槽位。参数方面INTERCEPTION_MAX_KEYBOARD和INTERCEPTION_MAX_MOUSE是编译期常量一般各 10 个实际插上的设备远少于这个数。如果这段代码打印不出任何设备先别往下写去设备管理器看有没有带感叹号的 Interception 设备。提示这段代码必须以管理员权限运行否则create_context会失败。普通用户态进程打不开内核设备句柄。3. 从零编译 Interception 1.0.1驱动签名与库文件生成3.1 源码结构与构建依赖Interception 的仓库通常包含三部分驱动源码driver/、用户态库library/、示例程序samples/。驱动部分需要 WDKWindows Driver Kit库部分需要 Visual Studio 的 C 工具链。1.0.1 这个版本号对应的是库的 ABI 版本驱动和库要配套使用不要混用不同版本的interception.h和interception.sys。常见做法是用 VS 打开解决方案文件先编译interception库项目再编译install项目来安装驱动。构建前确认几件事WDK 版本要和 VS 版本匹配测试签名模式要打开bcdedit /set testsigning on否则未签名的驱动加载会被拒绝。如果你有 EV 证书可以走正式签名流程没有的话测试签名加testsigning on是开发阶段的常规路径。注意testsigning on会在桌面右下角显示水印这是正常现象不是系统坏了。3.2 安装驱动与验证设备节点编译完成后install项目会调用sc create注册服务并加载驱动。手动安装的话命令行大致如下# 以管理员身份运行 sc create interception type kernel binPath C:\path\to\interception.sys sc start interception # 确认服务状态 sc query interception逻辑说明sc create注册一个内核服务type kernel表示这是驱动。binPath指向编译出来的.sys文件路径不要带空格否则要加引号。sc start加载驱动如果返回FAILED 577或1275基本就是签名问题。sc query看STATE是不是RUNNING。参数上type和binPath等号后面必须跟一个空格这是sc命令的历史坑很多人第一次写会漏掉。安装成功后设备管理器里会出现 Interception 的键盘和鼠标过滤设备。这时候再跑 2.3 那段枚举代码应该能看到设备 ID。如果看不到检查是不是装到了错误的设备栈上或者系统里有多个键盘设备导致索引偏移。3.3 库的链接方式与头文件包含顺序用户态程序链接interception.lib包含interception.h。注意这个头文件里用了 Windows 类型所以#include windows.h要放在它前面否则会报一堆类型未定义。链接时如果报LNK2019找不到interception_create_context检查库的位数是不是和程序一致32 位程序链 64 位库是常见翻车点。#include windows.h // 必须先包含 #include interception.h #pragma comment(lib, interception.lib)逻辑说明windows.h提供DWORD、HANDLE等基础类型interception.h依赖它们。#pragma comment是 MSVC 的懒人链接方式等价于在项目属性里加库依赖。参数上没有可调的但要注意库的搜索路径interception.lib所在目录要加到Additional Library Directories。4. 键盘鼠标事件拦截与注入的代码骨架4.1 事件循环interception_receive 的阻塞特性Interception 的核心 API 是interception_receive和interception_send。receive是阻塞的它会一直等到有输入事件才返回。这意味着你不能在主线程里既跑 UI 又跑事件循环常见做法是开一个专用线程。下面是一个最小的事件循环骨架把键盘事件原样转发同时打印扫描码。#include windows.h #include interception.h #include stdio.h DWORD WINAPI input_thread(LPVOID lpParam) { InterceptionContext ctx interception_create_context(); InterceptionStroke stroke; InterceptionDevice device; while (interception_receive(ctx, device interception_wait(ctx), stroke, 1) 0) { if (interception_is_keyboard(device)) { InterceptionKeyStroke *ks (InterceptionKeyStroke *)stroke; printf(key code%d state%d\n, ks-code, ks-state); } // 原样发回保证正常输入不被吞掉 interception_send(ctx, device, stroke, 1); } interception_destroy_context(ctx); return 0; }逻辑说明interception_wait阻塞等待任意设备有事件返回设备 ID。interception_receive把事件读到stroke里返回读到的数量。InterceptionKeyStroke结构里code是扫描码state是按下/抬起标志。关键点是最后必须interception_send把事件发回去否则你的键盘就真的「失灵」了——这是新手最容易踩的坑拦截了不转发用户会以为键盘坏了。参数上receive的第四个参数是缓冲区大小填 1 表示一次读一个事件够用。4.2 鼠标事件结构与坐标处理鼠标事件用InterceptionMouseStroke字段包括flags、rolling、x、y、information。x和y是相对位移不是绝对坐标这一点和SetCursorPos完全不同。如果你要做绝对定位得自己累计位移或者用MOUSEEVENTF_ABSOLUTE标志配合归一化坐标。下面演示如何构造一个鼠标左键点击。void simulate_left_click(InterceptionContext ctx, InterceptionDevice mouse) { InterceptionMouseStroke down {0}; down.flags INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN; interception_send(ctx, mouse, (InterceptionStroke *)down, 1); InterceptionMouseStroke up {0}; up.flags INTERCEPTION_MOUSE_LEFT_BUTTON_UP; interception_send(ctx, mouse, (InterceptionStroke *)up, 1); }逻辑说明flags用位标志描述按钮状态INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN和_UP成对出现。x、y留 0 表示不移动。参数上rolling是滚轮增量正负代表方向。注意interception_send的第三个参数要强制转成InterceptionStroke *因为 API 是统一签名。如果点击没生效先确认mouse这个设备 ID 是不是当前活跃的鼠标设备多鼠标环境下容易发错设备。4.3 过滤与放行如何只拦截特定按键实际业务里很少全量拦截通常是「只处理某几个键其余放行」。做法是在事件循环里判断code命中就处理不命中就send回去。下面是一个只拦截 F12 的例子。if (interception_is_keyboard(device)) { InterceptionKeyStroke *ks (InterceptionKeyStroke *)stroke; if (ks-code 0x58 ks-state INTERCEPTION_KEY_DOWN) { // F12 扫描码 printf(F12 intercepted, not forwarded\n); continue; // 不 send事件被吞掉 } } interception_send(ctx, device, stroke, 1);逻辑说明0x58是 F12 的扫描码不同键盘布局可能不同用MapVirtualKey换算更稳。state判断按下还是抬起只在按下时处理避免重复触发。continue跳过send事件就不会传到上层。参数上扫描码和虚拟键码是两套体系InterceptionKeyStroke.code是扫描码别拿VK_F12直接比这是高频错误。5. 避坑与排查驱动装不上、事件丢失、蓝屏的常见原因5.1 驱动加载失败错误码 577 或 1275现象sc start interception返回FAILED 577设备管理器里驱动带黄色感叹号。原因驱动没有有效签名或者testsigning没开。解决管理员命令行执行bcdedit /set testsigning on后重启确认桌面右下角出现测试模式水印。如果还不行检查.sys文件是不是被安全软件拦截了部分杀软会把未签名驱动直接隔离。5.2 键盘鼠标完全失灵只能强制重启现象程序运行后物理键盘鼠标没反应CtrlAltDel 也无效。原因事件循环拦截了事件但没有interception_send转发或者程序崩溃导致驱动缓冲区堆积。解决开发阶段一定保留一个「逃生通道」比如检测到某个组合键就interception_destroy_context退出。更稳妥的做法是在虚拟机或备用机上调试别拿主力机当小白鼠。我自己的血泪经验是第一次跑拦截循环忘了转发只能长按电源键。5.3 注入的事件目标程序收不到现象interception_send返回成功但目标窗口没反应。原因发到了错误的设备 ID或者目标程序用的是 Raw Input 且过滤了非物理设备来源。解决先用 2.3 的枚举代码确认设备 ID多键盘环境下索引会变。如果目标程序有反作弊Interception 也可能被识别这时候纯软件方案基本到头了得考虑硬件模拟。参数上interception_send的返回值是发送的事件数返回 0 说明设备句柄无效。5.4 卸载后设备残留重新安装失败现象卸载驱动后重新安装报设备已存在或服务启动失败。原因sc delete没有清干净或者设备节点还挂在注册表里。解决先sc stop interception再sc delete interception然后去设备管理器扫描硬件改动。如果还不行用pnputil /enum-drivers找到对应的 oem 驱动包用pnputil /delete-driver删掉。注意删驱动前确认没有其他程序在用否则会蓝屏。5.5 多线程下事件乱序或丢失现象开了多个线程同时receive事件顺序错乱。原因InterceptionContext不是线程安全的多个线程共用一个 context 会竞争。解决一个 context 只在一个线程里用需要多路处理就开多个 context每个绑定不同的设备。参数上interception_wait本身是阻塞的多线程等待同一个 context 没有意义只会让事件分配不可预测。6. 进阶技巧用 Interception 做输入录制与回放把拦截和注入结合起来最实用的进阶玩法是输入录制回放先录一段真实的键鼠事件序列存成文件之后按时间戳重放。这个在自动化测试、演示复现、压力测试里都用得上。核心思路是给每个事件打时间戳回放时按间隔Sleep。// 录制把事件和时间戳写入文件 DWORD start GetTickCount(); while (interception_receive(ctx, device interception_wait(ctx), stroke, 1) 0) { DWORD now GetTickCount() - start; fwrite(now, sizeof(DWORD), 1, fp); fwrite(device, sizeof(InterceptionDevice), 1, fp); fwrite(stroke, sizeof(InterceptionStroke), 1, fp); interception_send(ctx, device, stroke, 1); // 录制时不影响正常使用 }逻辑说明GetTickCount返回毫秒级时间够用要高精度换QueryPerformanceCounter。每条记录包含相对时间、设备 ID、原始 stroke。回放时读一条、Sleep差值、interception_send一条。参数上InterceptionStroke是联合体大小固定直接fwrite没问题但跨版本回放要注意结构体布局可能变最好加个版本号头。回放时有个细节鼠标的相对位移在回放时依赖当前光标位置如果两次回放之间用户动了鼠标结果会偏。解决办法是回放前先把光标移到固定位置或者录制时同时记录绝对坐标做校正。另一个坑是键盘的重复率物理键盘长按会产生重复的DOWN事件回放时如果原样重放目标程序的重复率设置不同会导致行为不一致。我一般会在回放前把重复的DOWN合并只保留首次按下和最终抬起。验证回放是否成功不要只看「程序没报错」要对比目标程序的实际状态。比如录一段在画图里画线的操作回放后看线条是否一致。如果线条抖动或断裂多半是时间戳精度不够或者Sleep被系统调度拉长。可以把Sleep换成忙等待加timeBeginPeriod(1)提高定时精度代价是 CPU 占用上升。最后说个习惯我每次改完拦截逻辑都会先在虚拟机里跑一遍全量拦截加转发的循环确认键盘鼠标还能用再上真机。驱动层的东西没有后悔药一次蓝屏可能丢的是没保存的工作。希望帮到你。本文还有配套的精品资源点击获取