C++:模拟键盘

模拟键盘是一个简单的话题,随便普通程序猿都能说出好多种方式。不同的方式应用于不同场合,总的来说分为三大类:
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__

三种方式没有孰优孰劣,根据需求场景应用于各个方面,比如最后一种模拟硬件的看似最强大,但也不能部署在云服务器上。总的来说,上面几种方式可以满足几乎所有的需求了。

Published by

fawdlstty

又一只萌萌哒程序猿~~

One thought on “C++:模拟键盘”

发表评论

电子邮件地址不会被公开。 必填项已用*标注