模拟键盘是一个简单的话题,随便普通程序猿都能说出好多种方式。不同的方式应用于不同场合,总的来说分为三大类:
1、用户层模拟键盘
这个层来模拟是最方便的,但也是最容易无效的。总的来说有三种方式,第一种是直接往目标窗口发送按键消息;第二种是使用剪贴板复制待粘贴消息然后在目标窗口模拟Ctrl+V;第三种是用户层触发按键事件
这儿贴一个模拟输入一个字节的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | bool input (char ch) { DWORD KeyScan = ::OemKeyScan (ch); bool bShift = !!(KeyScan >> 16); UINT uCode = KeyScan & 0xFFFF; UINT uScanCode = ::MapVirtualKey (uCode, MAPVK_VK_TO_VSC); UINT uShiftScanCode = ::MapVirtualKey (VK_SHIFT, MAPVK_VK_TO_VSC); if (bShift) { ::keybd_event (VK_SHIFT, (BYTE) uShiftScanCode, KEYEVENTF_EXTENDEDKEY, 0); std::this_thread::sleep_for (std::chrono::milliseconds (20)); } ::keybd_event (uCode, (BYTE) uScanCode, KEYEVENTF_EXTENDEDKEY, 0); std::this_thread::sleep_for (std::chrono::milliseconds (20)); ::keybd_event (uCode, (BYTE) uScanCode, KEYEVENTF_KEYUP, 0); std::this_thread::sleep_for (std::chrono::milliseconds (20)); if (bShift) { ::keybd_event (VK_SHIFT, (BYTE) uShiftScanCode, KEYEVENTF_KEYUP, 0); std::this_thread::sleep_for (std::chrono::milliseconds (20)); } return true; } |
这个函数很可能在一些有简单安全措施的软件里面失效。软件屏蔽键盘模拟按键一般是使用钩子,那么可以dll注入然后反钩子,挂钩SetWindowsHookEx即可。注意反钩子必须和目标在同一个进程里,否则M$的Copy-On-Write会让反钩子失效。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | BOOL func_unhook (LPVOID func_ptr, WORD param_num, DWORD ret_val) { // mov eax, 12345678h // ret 0004h BYTE bBuf [] = { '\xB8', '\x00', '\x00', '\x00', '\x00', '\xC2', '\x00', '\x00' }; *(DWORD*) (&bBuf [1]) = ret_val; *(WORD*) (&bBuf [6]) = param_num * 4; DWORD dw = 0, dw2 = 0; BOOL bRet = ::VirtualProtect (func_ptr, 8, PAGE_EXECUTE_READWRITE, &dw); if (!bRet) return FALSE; ::memcpy (func_ptr, bBuf, 8); if (func_ptr != ::VirtualProtect) ::VirtualProtect (func_ptr, 8, dw, &dw2); return TRUE; } func_unhook (::SetWindowsHookA, 2, 1); func_unhook (::SetWindowsHookW, 2, 1); func_unhook (::UnhookWindowsHook, 2, 1); func_unhook (::SetWindowsHookExA, 4, 1); func_unhook (::SetWindowsHookExW, 4, 1); func_unhook (::UnhookWindowsHookEx, 1, 1); |
反钩子后模拟键盘事件差不多可以过绝大部分弱保护的安全措施了,不过这也不完全总是灵的。如果以上方法都不行,可以试试其他方案。
2、驱动层模拟键盘
可以用网上的用户层库来调用驱动层模拟键盘驱动。使用的最广泛的两个是winio和DD驱动。除了用户层调用外,还能使用网上的驱动程序然后通过DeviceIoControl等函数来与驱动进行通讯。
winio和DD都不完全完美。winio必须系统装有ps2/8042键盘驱动才能用,简单的说机箱后面有圆形的键盘插口,插入一个键盘并且可用之后,这个驱动就没问题了。Win10上使用这个驱动要先找到sys文件,然后安装签名,将签名设置为受信任的签名,然后驱动就能加载了。相对来说这个驱动如果可用的话那挺稳定的。其次是DD驱动,使用它首先需要系统支持HID驱动键盘,其次这玩意真心不稳定,很容易出各种问题。另外它还有限制,必须联网才可用。个人非常不推荐这种。
DD驱动不稳定,自己编译驱动模拟键盘代码先不谈,最终剩下的这个winio驱动,如果可用,那几乎满足大多数需求了。在特殊需求或者winio不可用的时候,还能考虑第三种方式。
3、硬件模拟键盘
通常是使用USB键盘协议来实现,具体方式有N种,总的来说,外部通讯方式或者写死的方式通过USB键盘接口协议来实现模拟USB键盘,那么都是可行的。比如串口转USB键盘线、WIFI转USB键盘、蓝牙转USB键盘等等。其中串口转USB键盘线是一条线,一端是串口接口,一端是USB接口,可以插同一台电脑上,也可以插两台电脑上。只要向串口写入字符,那么就能实现模拟键盘输入字符了;WIFI转USB键盘、蓝牙转USB键盘是两个类似U盘的玩意,那玩意不方便驾驭,就说说最简单的串口吧。使用以下类用于模拟输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | #ifndef __HAN_COMM_HPP__ #define __HAN_COMM_HPP__ #pragma once #include #include class hanComm { public: hanComm () {} ~hanComm () { close (); } void input (std::string str) { if (open ()) { DWORD dw = 0; ::WriteFile (m_hCom, str.c_str (), str.length (), &dw, NULL); close (); } } private: bool open () { if (m_hCom != INVALID_HANDLE_VALUE) return true; m_hCom = ::CreateFileA ("COM2", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (m_hCom == INVALID_HANDLE_VALUE) return false; ::SetupComm (m_hCom, 1024, 1024); // 设置超时 COMMTIMEOUTS cto = { 0 }; cto.ReadIntervalTimeout = MAXDWORD; cto.ReadTotalTimeoutMultiplier = 0; cto.ReadTotalTimeoutConstant = 0; cto.WriteTotalTimeoutMultiplier = 500; cto.WriteTotalTimeoutConstant = 500; ::SetCommTimeouts (m_hCom, &cto); // 设置串口具体信息 DCB dcb = { sizeof (DCB) }; if (!::GetCommState (m_hCom, &dcb)) { ::CloseHandle (m_hCom); m_hCom = INVALID_HANDLE_VALUE; return false; } dcb.BaudRate = 9600; dcb.Parity = NOPARITY; dcb.ByteSize = 8; dcb.StopBits = ONESTOPBIT; if (!::SetCommState (m_hCom, &dcb)) { ::CloseHandle (m_hCom); m_hCom = INVALID_HANDLE_VALUE; return false; } //::PurgeComm (m_hCom, PURGE_TXCLEAR | PURGE_RXCLEAR); return true; } void close () { if (m_hCom != INVALID_HANDLE_VALUE) { ::CloseHandle (m_hCom); m_hCom = INVALID_HANDLE_VALUE; } } private: HANDLE m_hCom = INVALID_HANDLE_VALUE; }; #endif //__HAN_COMM_HPP__ |
三种方式没有孰优孰劣,根据需求场景应用于各个方面,比如最后一种模拟硬件的看似最强大,但也不能部署在云服务器上。总的来说,上面几种方式可以满足几乎所有的需求了。
C艹大佬 新春快乐 万事如意!