C# 模拟键盘输入

1. 使用.Net Framework的库函数

SendKeys.SendWait("123{TAB}abc");

namespace System.Windows.Forms命名空间下的SendKeys是.Net提供的模拟键盘输入的工具类。其中有Send()SendWait()这两个方法,都可以发送按键消息。区别在于SendWait()是会等待按键消息被处理完成才返回的,而Send()则不用。这就类似于SendMessagePostMessage的关系。
上面代码中的{TAB}代表Tab键。键盘上一些特殊的按键都有对应的代码,具体的对照表可以参照微软MSDN上的介绍:SendKeys Class

当然,还可以使用Windows API,API原型如下:

        /// <summary>
        /// 合成一次击键事件
        /// </summary>
        /// <param name="bVk">定义一个虚拟键码。键码值必须在1~254之间</param>
        /// <param name="bScan">定义该键的硬件扫描码</param>
        /// <param name="dwFlags">定义函数操作的各个方面的一个标志位集。应用程序可使用如下一些预定义常数的组合设置标志位</param>
        /// <param name="dwExtraInfo">定义与击键相关的附加的32位值</param>
        [DllImport("user32")]
        public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, 
uint dwExtraInfo);

甚至用SendMessagePostMessage也是可以做到的。

但以上这些实现方法都是在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使用笔记



作者:白日l梦想家
链接:https://www.jianshu.com/p/a0c88a765bfd
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2020-11-24 17:12  左正  阅读(6790)  评论(1编辑  收藏  举报