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使用笔记

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,490评论 1 11
  • 胆大心细,不怕问题,稳! 遇到问题只能靠自己时,并发出的力量是很强大的。 故障的产生: 应该是电脑被我踢了...
    哈森森阅读 21,934评论 0 1
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,744评论 0 10
  • 多年以后才发现弹奏中蕴藏的人生哲学: 示强是容易的,容易到我们大声说话就是了,就像弹奏ff,我们用上大臂的力量,顺...
    Regina521阅读 355评论 0 1
  • 待到秋来九月八,我花开后百花杀。 冲天香阵透长安,满城尽带黄金甲。
    东北花孔雀阅读 317评论 0 0