1. 使用.Net Framework的库函数
SendKeys.SendWait("123{TAB}abc");
namespace System.Windows.Forms
命名空间下的SendKeys
是.Net提供的模拟键盘输入的工具类。其中有Send()
和SendWait()
这两个方法,都可以发送按键消息。区别在于SendWait()
是会等待按键消息被处理完成才返回的,而Send()
则不用。这就类似于SendMessage
和PostMessage
的关系。
上面代码中的{TAB}
代表Tab键。键盘上一些特殊的按键都有对应的代码,具体的对照表可以参照微软MSDN上的介绍:SendKeys Class
当然,还可以使用Windows API,API原型如下:
///
/// 合成一次击键事件
///
/// 定义一个虚拟键码。键码值必须在1~254之间
/// 定义该键的硬件扫描码
/// 定义函数操作的各个方面的一个标志位集。应用程序可使用如下一些预定义常数的组合设置标志位
/// 定义与击键相关的附加的32位值
[DllImport("user32")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags,
uint dwExtraInfo);
甚至用SendMessage
和PostMessage
也是可以做到的。
但以上这些实现方法都是在Windows消息层面的,对于像记事本等常规应用程序是没问题的。但是对于一些游戏、QQ登录框、网银登录框等则无效。这是因为这些程序不是从Windows消息中获取按键信息的,而是直接从底层驱动层面获取按键信息的。
在一篇关于研究游戏外挂的博文里看到博主描述了这个问题:
引自:https://blog.csdn.net/zdrl/article/details/12707835
在解释更详细的原理之前,我们先来抓出幕后黑手,看看是哪个给游戏撑腰?让它有胆子违抗Windows消息命令。究竟是判断了真实键盘信息,还是有其他原因。结果在DirectX编程中发现了DirectInput这个API。就是它绕过了Windows的消息机制,它的目的是为了让游戏的实时性控制更好、更快。Windows消息是队列形式的,在传递过程中会有延时,比如格斗类游戏对实时性控制要求是非常高的,Window消息机制不能满足这个需求。而DirectInput直接和键盘驱动程序打交道,效率当然要高出一大截。我认为大部分游戏不响应消息的真正的原因在这里,而不是故意写了反作弊系统。
2. 使用WinIO.dll
由于上述方法只能模拟Windows消息层面的按键,以至于对一些应用程序无效,所以下面就采用直接在驱动层面模拟按键的方法。
这里需要用到一个组件,那就是使用WinIO.dll,这是是国外大佬开发的一个dll。
引自百度百科:https://baike.baidu.com/item/winio/2877240?fr=aladdin
WinIO程序库允许在32位的Windows应用程序中直接对I/O端口和物理内存进行存取操作。通过使用一种内核模式的设备驱动器和其它几种底层编程技巧,它绕过了Windows系统的保护机制。
使用此组件的环境要求:
- 系统Win7或Win10均可。
- 需要PS/2键盘(老式的针孔插头的键盘),USB键盘不行。
- 正规使用的话需要官方授权签名,否则就得将Windows开启测试模式。
- 使用此组件的应用程序需要以管理员的身份启动。
- 此组件还有32位和64位的区分。
- 与dll配套的还有个.sys的文件,要跟dll放在同一目录下。
Windows开启测试模式的方法:
以管理员身份打开cmd,输入开启测试模式的命令并执行。然后重启电脑,看到桌面右下角出现“测试模式”字样即可。
开启测试模式的命令:
bcdedit /set testsigning on
关闭测试模式的命令:
bcdedit /set testsigning off
开启测试模式成功:
调用WinIO64.dll的示例代码:
public class WinIO64
{
private const int KBC_KEY_CMD = 0x64;
private const int KBC_KEY_DATA = 0x60;
#region WinIo64.dll
[DllImport("WinIo64.dll")]
public static extern bool InitializeWinIo();
[DllImport("WinIo64.dll")]
public static extern bool GetPortVal(IntPtr wPortAddr, out int pdwPortVal,
byte bSize);
[DllImport("WinIo64.dll")]
public static extern bool SetPortVal(uint wPortAddr, IntPtr dwPortVal, byte
bSize);
[DllImport("WinIo64.dll")]
public static extern byte MapPhysToLin(byte pbPhysAddr, uint dwPhysSize,
IntPtr PhysicalMemoryHandle);
[DllImport("WinIo64.dll")]
public static extern bool UnmapPhysicalMemory(IntPtr PhysicalMemoryHandle,
byte pbLinAddr);
[DllImport("WinIo64.dll")]
public static extern bool GetPhysLong(IntPtr pbPhysAddr, byte pdwPhysVal);
[DllImport("WinIo64.dll")]
public static extern bool SetPhysLong(IntPtr pbPhysAddr, byte dwPhysVal);
[DllImport("WinIo64.dll")]
public static extern void ShutdownWinIo();
#endregion
[DllImport("user32.dll")]
public static extern int MapVirtualKey(uint Ucode, uint uMapType);
private WinIO64()
{
IsInitialize = true;
}
public static void Initialize()
{
if (InitializeWinIo())
{
KBCWait4IBE();
IsInitialize = true;
}
else
MessageBox.Show("Load WinIO Failed!");
}
public static void Shutdown()
{
if (IsInitialize)
ShutdownWinIo();
IsInitialize = false;
}
private static bool IsInitialize { get; set; }
///等待键盘缓冲区为空
private static void KBCWait4IBE()
{
int dwVal = 0;
do
{
bool flag = GetPortVal((IntPtr)0x64, out dwVal, 1);
}
while ((dwVal & 0x2) > 0);
}
/// 模拟键盘标按下
public static void KeyDown(Keys vKeyCoad)
{
if (!IsInitialize) return;
int btScancode = 0;
btScancode = MapVirtualKey((uint)vKeyCoad, 0);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)0x60, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)btScancode, 1);
}
/// 模拟键盘弹出
public static void KeyUp(Keys vKeyCoad)
{
if (!IsInitialize) return;
int btScancode = 0;
btScancode = MapVirtualKey((uint)vKeyCoad, 0);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)0x60, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)(btScancode | 0x80), 1);
}
}
}
3. 使用WinRing0x64.dll
这里还有另外一个组件WinRing0x64.dll,可以实现同样的效果。不需要授权签名,不需要开启测试模式,使用起来要方便很多。
使用此组件的环境要求:
- 系统Win7或Win10均可。
- 需要PS/2键盘(老式的针孔插头的键盘),USB键盘不行。
- 使用此组件的应用程序需要以管理员的身份启动。
- 与dll配套的还有个.sys的文件,要跟dll放在同一目录下。
- 此组件应该也是区分32位和64位的,只是我只找到64位的,没再去管32位的。
调用此组件的示例代码有点长,这里就懒得贴了。
对于2和3这两种方式,我写了一个完整、可行的Demo,放在GitHub上了。包括需要的组件都在里面。
链接:
模拟键盘输入的Demo
URL地址:https://github.com/Zzz2333/TestKeyboard
搜集的参考资料汇总:
爬虫应对银行安全控件
驱动级键盘模拟(C#)
C#模拟鼠标和键盘操作
Windows下对硬件端口的操作---WinIo库的使用
WinIo使用笔记