☰
驱动级鼠标键盘模拟:绕过SendInput,用WinIo直接操作8042
2026/10/20 8:54:02 网站建设 项目流程

简介:这是一份面向C#开发者及驱动编程学习者的驱动级鼠标键盘模拟资源包,基于WinIo内核驱动在底层模拟键鼠输入,适用于自动化测试、外设控制等场景。包内共85个文件,约260KB,包含C#工程源码(22个cs)、C/C++驱动源码(cpp与h)、WinIo32/64驱动本体(sys与dll)、CHM帮助文档及可运行exe示例。楼主附带的“键盘鼠标映射(驱动级)”小例子可直接编译运行,演示通过P/Invoke调用WinIo.dll写入端口扫描码的完整流程;另一“灵剑士卡刀(驱动级)”示例展示游戏场景下的键鼠自动操作,便于对照学习硬件端口操作与扫描码映射。已有4818人学习下载,适合希望在操作系统底层加深理解、开展输入自动化实验的开发者参考研究。

1. 驱动级鼠标键盘模拟:当 SendInput 失灵时,直接把扫描码写进 8042

做远程协助或自动化测试的人应该都遇到过这种玄学:同样一段 SendInput 代码,在普通窗口里一切正常,切到游戏、管理员权限窗口或者某些远程会话里就完全没反应,日志打了一堆,目标进程就是收不到。原因不难理解,SendInput 走的是系统输入队列,目标程序只要挂一个低层钩子或做输入过滤就能把它挡下来。这套以 WinIo 驱动的鼠标键盘模拟源码给出的方案是:绕开应用层,让 C# 通过 P/Invoke 直接读写 8042 键盘控制器的 I/O 端口,把按键扫描码和鼠标数据包“喂”到控制器的输出缓冲区,从硬件层面制造一次真实的外设输入。包里带完整 WinIo 驱动资源和可直接运行的示例工程,适合对输入可靠性有要求的自动化场景。

2. 架构与技术选型:WinIo 驱动 + C# P/Invoke 的上下分层

2.1 三层结构:用户态 DLL、内核驱动、硬件端口

WinIo 的老资格大家都清楚,很多底层小工具靠它读 CPU 温度、控制风扇、读写 CMOS。它的结构分三层:最上层是 WinIo.dll,提供 InitializeWinIo、GetPortVal、SetPortVal 这类导出函数;中间是内核驱动 winio32.sys 或 winio64.sys,负责真正访问硬件端口;最底下才是物理 I/O 端口。用户态程序调用 DLL 的导出函数后,DLL 通过 DeviceIoControl 把请求发给内核驱动,驱动执行 in/out 指令完成端口读写。

这套架构的好处非常直接。全局钩子和 SendInput 都还在“系统输入模型”里打转,而 WinIo 直接跳到硬件层。对 8042 控制器来说,往端口 0x60 写一个值,和物理键盘芯片把扫描码送进来没有本质区别。目标程序无论是用户态钩子还是内核态回调,看到的就是一次控制器发出的中断,无从分辨数据来自真实按键还是模拟注入。

2.2 为什么选 C#:开发效率和示例工程价值

驱动本身用 C 写不奇怪,但控制层为什么用 C#?其实就是省事。WinIo.dll 导出的是标准 C 接口,C# 用 DllImport 声明一下就能调,不用像 C++ 那样管理头文件和链接库。而这个资源包的价值恰恰在示例工程上——初始化、端口读写、扫描码表都写好了,接手的人只需要理解流程,不用从零看驱动代码。

另外,C# 做上层逻辑确实方便。坐标计算、按键组合、时间控制这些自动化逻辑,写起来比 C++ 快一个量级。一个典型的分层是:最底层是 WinIo 封装类,往上是键盘/鼠标模拟类,再往上才是具体业务脚本。这个结构在示例工程里能很清楚地看到。

2.3 源码包目录结构与关键文件说明

拿到资源后先别急着双击运行,目录结构要心里有数。一般是这样:

文件/目录说明
WinIo.dll32/64 位用户态动态库,后续封装类全靠它
winio32.sys / winio64.sys内核驱动本体,分别对应 32/64 位系统
WinIo.csC# 的 DllImport 声明层,等于 WinIo 的 C# 头文件
KeyboardSimulator.cs键盘模拟类,核心是扫描码注入
MouseSimulator.cs鼠标模拟类,核心是 PS/2 数据包注入
Program.cs 或 MainForm.cs示例工程入口,演示按键和移鼠标
WinIo 相关文档驱动安装说明、常见报错对应表

注意 winio32.sys 和 winio64.sys 不能混用,64 位系统上加载 32 位驱动会直接失败。示例工程里有个初始化判断,会根据进程位数选择驱动文件,这个逻辑建议保留。

3. 核心模块拆解:一个按键从 C# 方法到硬件端口的完整旅程

3.1 P/Invoke 声明与 WinIo 初始化

所有功能的第一步都是先声明并初始化 WinIo。DllImport 声明如下:

using System; using System.Runtime.InteropServices; public class WinIoPort { [DllImport("WinIo.dll", EntryPoint = "InitializeWinIo")] public static extern bool InitializeWinIo(); [DllImport("WinIo.dll", EntryPoint = "ShutdownWinIo")] public static extern void ShutdownWinIo(); [DllImport("WinIo.dll", EntryPoint = "SetPortVal")] public static extern bool SetPortVal(ushort wPortAddr, uint dwPortVal, byte bSize); [DllImport("WinIo.dll", EntryPoint = "GetPortVal")] public static extern bool GetPortVal(ushort wPortAddr, out uint pdwPortVal, byte bSize); }

这段声明的关键在 SetPortVal 的第三个参数 bSize——它指定写入宽度,1 表示按字节写,2 是字,4 是双字。操作 8042 端口时全部用 1 字节写入。GetPortVal 的 out 参数用于读状态寄存器,下面判断输入缓冲是否空闲就是靠它。

初始化代码如下:

public static bool Init() { if (!InitializeWinIo()) { // 常见原因:驱动没装、以 32 位进程跑在 64 位系统、权限不足 return false; } return true; } public static void Cleanup() { ShutdownWinIo(); }

InitializeWinIo 内部会尝试加载 winio32.sys 或 winio64.sys,加载成功后建立内核通道。这个函数返回 false 时,优先怀疑三件事:是否用管理员身份运行、驱动文件是否和进程位数匹配、杀毒软件有没有拦截驱动加载。其中杀软拦截最坑,界面没有任何提示,InitializeWinIo 就是返回 false。

3.2 键盘模拟:0xD2 注入与扫描码映射

键盘模拟的核心是 8042 控制器的“写输出缓冲”命令 0xD2。流程是先向端口 0x64 写 0xD2,再向端口 0x60 写一个数据字节,控制器会把该字节放进自己的输出缓冲区,并且触发一次中断——效果和物理键盘送来一个扫描码完全一样。下面是完整的注入逻辑:

public class KeyboardSimulator { // 等待 8042 输入缓冲为空,避免写入被丢弃 private static void WaitInputBufferEmpty() { uint status; do { WinIoPort.GetPortVal(0x64, out status, 1); } while ((status & 0x02) != 0); } // 通过 0xD2 命令注入一个字节到控制器输出缓冲 private static void InjectByte(byte data) { WaitInputBufferEmpty(); WinIoPort.SetPortVal(0x64, 0xD2, 1); WaitInputBufferEmpty(); WinIoPort.SetPortVal(0x60, data, 1); } // 按下按键,scanCode 为通码(make code) public static void KeyDown(byte scanCode) { InjectByte(scanCode); } // 松开按键,断码 = 通码 | 0x80 public static void KeyUp(byte scanCode) { InjectByte((byte)(scanCode | 0x80)); } // 完整敲击:按下、短暂延时、松开 public static void TapKey(byte scanCode, int holdMs = 50) { KeyDown(scanCode); Thread.Sleep(holdMs); KeyUp(scanCode); } }

几个参数需要注意。WaitInputBufferEmpty 里读的是端口 0x64 的状态寄存器,bit 1 为 1 表示输入缓冲满,此时写数据会被控制器丢弃,所以必须等到它为 0。holdMs 控制按键按住时间,模拟真实敲击一般 30~80 毫秒够用,太小可能被目标程序判定为异常输入。

0xD2 是“命令”,写的是 0x64;后面跟的“数据”写的是 0x60。两个端口别搞反,很多第一次接触的人把数据也写到 0x64,结果控制器把数据当成命令执行,效果就是按键完全没反应,甚至把控制器搞进未知状态。

常用的扫描码表,做自动化脚本时直接查:

按键通码 (HEX)断码 (HEX)
A0x1E0x9E
D0x200xA0
W0x110x91
Enter0x1C0x9C
Space0x390xB9
Left Ctrl0x1D0x9D
F10x3B0xBB

3.3 鼠标模拟:PS/2 辅助通道数据包注入

鼠标走的是 8042 的辅助设备通道,注入方式比键盘多两步:先要启用辅助设备和数据上报,再按 PS/2 协议包格式注入 3 字节数据包。

public class MouseSimulator { public static void EnableMouse() { // 0xA8: 启用辅助设备 WinIoPort.SetPortVal(0x64, 0xA8, 1); WaitInputBufferEmpty(); // 0xD4: 通知控制器,下一个字节发给鼠标 WinIoPort.SetPortVal(0x64, 0xD4, 1); WaitInputBufferEmpty(); // 0xF4: 鼠标命令,启用数据上报 WinIoPort.SetPortVal(0x60, 0xF4, 1); } public static void Move(int dx, int dy) { // 第 1 字节:标志位,bit3 恒为 1 byte flag = 0x08; if (dx < 0) flag |= 0x10; // X 负向 if (dy < 0) flag |= 0x20; // Y 负向 byte[] packet = new byte[] { flag, (byte)Math.Abs(dx), (byte)Math.Abs(dy) }; // 每个字节都通过 0xD2 注入输出缓冲 foreach (byte b in packet) { KeyboardSimulator.InjectByte(b); } } }

EnableMouse 的两个关键命令:0xD4 告诉控制器“下一个写 0x60 的字节是给鼠标的命令”,所以紧接着往 0x60 写 0xF4(启用数据上报)。没有这一步,鼠标通道处于关闭状态,注入的数据包不会被 i8042 驱动处理。Move 方法里 dx、dy 是相对位移,范围是 -127 到 127,超过这个范围要分多次发送。PS/2 鼠标包三个字节,长度固定,顺序不能乱。

3.4 示例工程走读

包里附带的示例工程就是一个控制台程序,初始化 WinIo 后先枚举一遍扫描码表,让用户输入要测试的按键,然后调用 TapKey 敲一下,再演示一次鼠标相对移动。整体逻辑很简单:Init → EnableMouse → 输入检测 → TapKey/Move → Cleanup。跑通这个示例比读代码本身更重要,因为驱动级模拟的验证方式特殊——你看不到输入队列里的消息,只能通过目标窗口的实际反应来确认。示例通常会有两行日志提示“注入成功”,但这只是端口写入返回 true,不代表目标窗口收到了按键。

4. 编译与运行验证:把源码跑起来需要做的事

4.1 环境要求与前置检查

运行这套代码的环境要求不高,但有三个硬性条件。第一,Windows 系统,WinIo 只支持 Windows;第二,必须是 x86 或 x64 架构,ARM 设备上 WinIo 驱动跑不了;第三,当前用户要有管理员权限,驱动加载绕不开 UAC。如果是在虚拟机里测试,建议给虚拟机启用 PS/2 鼠标支持,纯 USB 鼠标的环境下注入效果会有差异。

动手编译之前,先在设备管理器里看一眼“键盘”和“鼠标和其他指针设备”下面有没有 PS/2 开头的设备。如果键盘和鼠标都显示为 USB 设备,说明 8042 控制器可能没有启用在用设备,注入的数据可能没有驱动去消费。很多项目跑起来“没反应”,问题就出在这。

4.2 编译步骤与参数说明

打开示例工程(.sln 或 .csproj),确认目标平台和驱动位数一致。一般来说,64 位系统用 x64 配置编译,32 位用 x86。这里有个常见的翻车点:编译成 AnyCPU 且不勾选“首选 32 位”,在 64 位系统上运行时进程是 64 位的,加载 winio64.sys 没问题;但如果勾选了“首选 32 位”,进程变成 32 位却还是尝试加载 winio32.sys,逻辑上也能跑,只是要确保 sys 文件齐全。

编译快捷键就是 F6 或 dotnet build,示例工程没有额外 NuGet 依赖,不需要还原包。生成目录里除了 exe,还要保证 WinIo.dll 和对应的 sys 文件在同一个目录。右键 exe“以管理员身份运行”,控制台打印出 WinIo 初始化成功的信息,说明驱动层已经通了。

4.3 验证模拟是否生效

有几种验证方式,从简单到严格排列。最简单的是打开记事本,程序里设定敲一个 A 键,记事本里出现字母 a 就是注入成功。稍微严格一点,打开一个不接受 SendInput 注入的场景来测——比如某些游戏大厅,用键位设置界面检测按键,看它能不能识别出驱动级的按键输入。

还有一个验证细节容易被忽略:注入鼠标移动后,看光标是否按照 dx、dy 的相对方向移动。如果光标完全不动,先回 EnableMouse 的调用顺序;如果光标乱飘,多半是数据包字节序或者溢出位没处理好。验证通过后再跑目标程序,才算真正完成一次驱动级模拟的闭环。

5. 踩坑记录:驱动级模拟项目的 4 个高频问题

5.1 现象:InitializeWinIo 一直返回 false

新手最容易在这个函数上卡一整天。我在某台机器上遇到过同样代码在同事电脑上正常,自己电脑上就是 false。逐项排查最后发现是杀毒软件把 winio64.sys 当风险驱动隔离了。WinIo 驱动做的是底层端口访问,行为特征确实像恶意程序,安全软件拦截是常事。

原因就三类:权限不足、驱动被拦、位数不匹配。解决方法是先用管理员身份跑;然后在杀软隔离区找有没有被清掉的 sys 文件,恢复并加白名单;最后确认 exe 和 sys 位数一致。如果这三步都过了还不行,卸载驱动重装一次。

5.2 现象:64 位系统上驱动加载直接失败

64 位 Windows 强制要求内核驱动有数字签名,WinIo 这种老库的驱动往往没有合法签名,加载时会直接报错或被系统拒绝。这是 64 位系统上最常见的问题,不是代码错了。

解决路径一般是:开机进高级启动选项,选择禁用驱动签名强制,然后正常加载驱动。或者把系统测试签名模式打开,用测试证书给 sys 文件签名。我一般优先用前者,操作快,适合验证阶段;正式要分发给别人用,需要走签名流程,否则每个目标机器都要手动进一次启动选项。

5.3 现象:按键注入后没有任何反应,但端口写入返回 true

这个坑最容易误导人。SetPortVal 返回 true 只代表端口写操作成功,不代表 8042 控制器把数据当作有效 Device Input 处理了。我在某次尝试中连写 20 个字节全部返回 true,目标窗口一个字符都没收到。后来发现是键盘注入前没有调用 EnableMouse——键盘其实不依赖鼠标,但我在测试时把键盘代码放在了 EnableMouse 之后,而 EnableMouse 里的 0xA8 命令把 8042 的辅助设备启用状态改了,导致整个控制器的中断状态错乱。

解决方法是确认键盘注入链路里没有多余命令。键盘模拟只需要 0xD2 + 数据字节,不要混入 0xD4、0xA8 这些辅助设备命令。另外检查目标窗口是否真的处于前台激活状态,驱动级注入同样要求目标窗口有焦点。

5.4 现象:模拟鼠标移动时光标乱跳或飞向屏幕边缘

光标乱飞通常是数据包没按 PS/2 协议拼。我见过有人直接把 dx 的绝对值塞进第二个字节,却忘了在标志字节里设置方向位,结果向右移动被解析成向左,光标往反方向跑。更严重的情况是 dx 超过 127,一个字节存不下,溢出后被解析成负数。

解决方法是严格按协议拼包:标志字节的 bit3 恒为 1,bit4 表示 X 负方向,bit5 表示 Y 负方向,位移字节存绝对值。dx、dy 超范围时拆成多次移动,每次不超过 127。如果多个鼠标注入设备和真实鼠标同时存在,数据包优先级冲突也可能导致乱跳,建议测试时拔掉物理 PS/2 鼠标。

6. 二次开发:把示例扩展成带随机延时的通用输入框架

6.1 封装为可复用的 InputSimulator 类

示例工程适合验证,不适合直接搬进业务。我一般会再包一层,把键盘和鼠标封装成一个统一的输入接口,对外只暴露 TapKey、PressKey、MoveMouse、ClickMouse 这几个方法,内部再拆一个 RandomDelay 工具类。这样业务脚本层不感知底层是 WinIo 还是 SendInput,哪天换实现方式也不用改业务代码。

public class InputSimulator { private static readonly Random rnd = new Random(); public static void TapKey(byte scanCode) { KeyboardSimulator.KeyDown(scanCode); Thread.Sleep(rnd.Next(40, 90)); // 随机按住时长 KeyboardSimulator.KeyUp(scanCode); } public static void MoveMouse(int dx, int dy) { // 超过 127 的位移拆包发送 int steps = Math.Max(Math.Abs(dx), Math.Abs(dy)) / 100 + 1; for (int i = 0; i < steps; i++) { int stepX = dx / steps; int stepY = dy / steps; MouseSimulator.Move(stepX, stepY); Thread.Sleep(rnd.Next(10, 25)); } } }

6.2 增加随机化与时间控制

自动化脚本最怕的是“节奏太规律”,一眼就能看出是机器在操作。固定延时 50 毫秒的循环,和真人敲击差距太明显。改造时把每次操作的间隔做成一个范围随机,比如 80~160 毫秒,按住时长为 40~90 毫秒,这样行为曲线更接近真人。这个改动很小,但对运行效果的影响非常明显。

6.3 改造后的验证路径

改完之后不要只看端口返回值,要回到 4.3 的验证流程重新走一遍:记事本测字符、目标程序测按键识别、光标移动测方向。我会额外做一个回归脚本,把常用操作序列跑 50 遍,统计失败率。从那以后我每次改完驱动级模拟代码都强制走一遍这个回归流程,给自己定的标准是失败率低于千分之一才能进业务。这套流程救过我很多次,希望帮到你。

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

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

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

立即咨询