C++ 黑客编程揭秘与防范(第2版)
上QQ阅读APP看书,第一时间看更新

1.3 模拟鼠标键盘按键的操作

鼠标和键盘的操作也会被转换为相应的系统消息,窗口过程中在接收到鼠标或键盘消息后会进行相应的处理。通过前面的内容了解到,可以通过 SendMessage()和 PostMessage()发送消息到指定的窗口过程中,那么使用这两个函数来发送鼠标和键盘的相关消息就可以进行鼠标和键盘的模拟操作。除了 SendMessage()和 PostMessage()外,还可以通过 keybd_event()和mouse_event()两个专用的函数进行鼠标和键盘按键的模拟操作。关于鼠标和键盘按键的模拟的用处就不多说了,想必读者都是知道的。

1.3.1 基于发送消息的模拟

通过前面的知识已经明白,Windows的应用程序是基于消息机制的,对于鼠标和键盘的操作也会被系统转化为相应的消息。首先来学习如何通过发送消息进行鼠标和键盘的模拟操作。

1.鼠标、键盘按键常用的消息

无论是鼠标指针(或光标)的移动、单击,还是键盘的按键,通常在Windows应用程序中都会转换成相应的消息。在操作鼠标时,使用最多的是移动鼠标和单击鼠标键。比如,在教新手使用计算机时会告诉他,将鼠标指针(或光标)移动到“我的电脑”上,然后单击鼠标右键,在弹出的快捷菜单中用鼠标左键单击选择“属性”对话框。当移动鼠标光标的时候,系统中对应的消息是 WM_MOUSEMOVE 消息,按下鼠标左键时的对应的消息是WM_LBUTTONDOWN,释放鼠标左键时,对应的消息是 WM_LBUTTONUP。在系统中,鼠标的消息有很多。在MSDN中查询到的鼠标消息如图1-8所示。

图1-8 鼠标相关消息

同样,在系统中也定义了键盘的按下与抬起的消息。键盘按下的消息是 WM_KEY DOWN,与之对应的键盘抬起的消息是WM_KEYUP。除了这两个消息外,还有一个消息是比较常用的,这个消息在前面介绍消息循环时介绍过,就是 WM_CHAR 消息。键盘的消息相对于鼠标要少很多,在MSDN中查询到的键盘消息如图1-9所示。

图1-9 键盘相关消息

2.PostMessage()函数对键盘按键的模拟

通过前面的知识已经知道,PostMessage()和SendMessage()这两个函数可以对指定的窗口发送消息。既然鼠标和键盘按键的操作被系统转换为相应的消息,那么就可以使用PostMessage()和SendMessage()通过按鼠标和键盘按键发送的消息来模拟它们的操作。对于模拟键盘按键消息,最好使用 PostMessage()而不要使用 SendMessage()。在很多情况下, SendMessage()是不会成功的。

现在编写一个简单的通过 PostMessage()函数模拟键盘发送(按 F5 键发送)的信息来刷新网页的小工具。首先打开VC6.0,创建一个对话框的工程,按照图1-10所示设置界面。

图1-10 模拟键盘刷新网页界面布局

按照如图1-10所示的界面进行布局,然后为“开始”按钮设置控件变量。这个小程序在“IE浏览器标题”处输入要刷新的页面的标题,在“刷新频率”处输入一个刷新的时间间隔,单位是秒。

当了解程序的功能并且将程序的界面布置好以后,就可以开始编写程序的代码了。程序的代码分为两部分,第一部分是程序要处理“开始”按钮的事件,第二部分是要按照指定的时间间隔对指定的浏览器发送按F5键的消息来刷新网页。

首先来编写响应“开始”按钮事件的代码,双击“开始”按钮来编写它的响应事件。代码如下:

void CKeyBoardDlg::OnBtnStart()

{

//TODO: Add your control notification handler code here

CString strBtn;

int nInterval = 0;

//获取输入的浏览器标题

GetDlgItemText(IDC_EDIT_CAPTION, m_StrCaption);

//获取输入的刷新频率

nInterval = GetDlgItemInt(IDC_EDIT_INTERVAL, FALSE, TRUE);

//判断输入的值是否非法

if ( m_StrCaption ==""|| nInterval == 0 )

{

return;

}

//获取按钮的标题

m_Start.GetWindowText(strBtn);

if ( strBtn == "开始" )

{

//设置定时器

SetTimer(1, nInterval * 1000, NULL);

m_Start.SetWindowText("停止");

GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(FALSE);

GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(FALSE);

}

else

{

//结束定时器

KillTimer(1);

m_Start.SetWindowText("开始");

GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(TRUE);

GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(TRUE);

}

}

在代码中,首先判断按钮的文本,如果是“开始”,则通过SetTimer()函数设置一个定时器;如果按钮的文本不是“开始”,则通过KillTimer()函数关闭定时器。

这里的SetTimer()和KillTimer()是MFC中CWnd类的两个成员函数,不是API函数。很多 MFC 中的类成员函数和 API 函数的写法是一样的,但是它们还是有区别的。比较一下SetTimer()在MFC中的定义和API函数的定义的差别。

MFC中的定义如下:

UINT SetTimer(

UINT nIDEvent,

UINT nElapse,

void (CALLBACK EXPORT* lpfnTimer)(

HWND, UINT, UINT, DWORD) );

API函数的定义如下:

UINT_PTR SetTimer(

HWND hWnd,   // handle to window

UINT_PTR nIDEvent, // timer identifier

UINT uElapse,  // time-out value

TIMERPROC lpTimerFunc // timer procedure

);

从定义中可以看出,MFC中SetTimer()函数的定义比API中SetTimer()函数的定义少了一个参数,即HWND 的窗口句柄的参数。在MFC 中,窗口相关的成员函数都不需要指定窗口句柄,在MFC的内部已经维护了一个m_hWnd的句柄变量。

在按钮事件中添加定时器,那么定时器会按照指定的时间间隔进行相应的处理。定时器部分的代码如下:

void CKeyBoardDlg::OnTimer(UINT nIDEvent)

{

//TODO: Add your message handler code here and/or call default

HWND hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));

//发送键盘按下

::PostMessage(hWnd, WM_KEYDOWN, VK_F5, 1);

Sleep(50);

//发送键盘抬起

::PostMessage(hWnd, WM_KEYUP, VK_F5, 1);

CDialog::OnTimer(nIDEvent);

}

关于定时器的处理非常简单,通过 FindWindow()函数得到要刷新窗口的句柄,然后发送WM_KEYDOWN和WM_KEYUP消息来模拟键盘按键即可。其实在模拟的过程中,可以省去WM_KEYUP消息的发送,但是为了模拟效果更接近真实性,建议在模拟时将消息成对发送。

将写好的程序编译连接后运行起来看效果,在“IE浏览器标题”处输入浏览器的标题,这个标题可以通过Spy++获得,然后在“刷新频率”处输入1。然后单击“开始”按钮,观察浏览器每个1秒进行刷新一次。当单击“停止”按钮后,程序不再对浏览器进行刷新按键模拟。

到此,通过 PostMessage()函数发送按 F5 键进行键盘按键模拟的程序就完成了。使用PostMessage()函数的好处是目标窗口可以在后台,而不需要窗口处于激活状态。可以将被刷新的浏览器最小化,然后运行刷新网页的小程序,在任务栏可以看到浏览器仍然在不断刷新。

1.3.2 通过API函数模拟鼠标键盘按键的操作

在开发程序时,总是依靠发送消息是非常辛苦的事情,因为消息的类型非常多,并且不同消息的附件参数也因不同的消息类型而异。Windows为几乎每个常用的消息都提供了相应的API函数。为了能够不去记忆过多的消息,使用API函数进行开发是相对比较直观的。

1.鼠标键盘按键模拟函数

在使用Windows的系统消息进行模拟鼠标或键盘按键操作时,可能显得不直观,也不方便。这是因为Windows下的大部分消息都可以直接使用对应的等价API函数,不必直接通过发送消息。比如可以WM_GETTEXT消息去获取文本的内容,对应的函数有GetWindowText()。试想一下,如果程序中一眼看去都是SendMessage()与PostMessage()之类的函数,岂不是很吓人。

本节介绍两个函数,分别用来模拟鼠标和键盘的输入,它们分别是 keybd_event()和mouse_event(),定义如下:

VOID keybd_event(

BYTE bVk,   // virtual-key code

BYTE bScan,   // hardware scan code

DWORD dwFlags,  // function options

ULONG_PTR dwExtraInfo // additional keystroke data

);

VOID mouse_event(

DWORD dwFlags,  // motion and click options

DWORD dx,   // horizontal position or change

DWORD dy,   // vertical position or change

DWORD dwData,  // wheel movement

ULONG_PTR dwExtraInfo // application-defined information

);

从函数的名称就能看出,这两个API函数分别是键盘事件和鼠标事件,在程序里使用时,对于阅读代码的人来说就比较直观了。下面将使用keybd_event()和mouse_event()两个函数来完成上一小节编写的刷新网页的小工具。

2.网页刷新工具

keybd_event()和mouse_event()这两个API函数,从函数的参数上来看,不需要给它们传递窗口句柄当作参数。那么这两个函数在进行鼠标和键盘的模拟时就必须将目标窗口激活并处于所有窗口的最前端。因此在程序中首先要完成的是将目标窗口设置到最前面,并且处于激活状态。先来看一下程序的界面部分,如图1-11所示。

图1-11 模拟鼠标键盘

这次的窗口相比上个程序的窗口要简单些。在界面上有2个按钮,第1个按钮“模拟键盘”是通过keybd_event()来模拟按F5键从而刷新网页,第2个按钮“模拟鼠标”是通过mouse_event()来模拟鼠标右键,从而弹出浏览器的快捷菜单,再通过keybd_event()模拟按R键来刷新网页。

知道了程序要实现的功能,先来完成将目标窗口设置到最前面并处于激活状态的部分,代码如下:

VOID CSimInputDlg::FindAndFocus()

{

GetDlgItemText(IDC_EDIT_CAPTION, m_StrCaption);

//判断输入是否为空

if ( m_StrCaption == "" )

{

return ;

}

m_hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));

//该函数将创建指定窗口的线程设置到前台

//并且激活该窗口

::SetForegroundWindow(m_hWnd);

}

这个自定义函数非常简单,分别调用了FindWindow()和SetForegroundWindow()两个API函数。FindWindow()函数在前面部分已经介绍过了。SetForegroundWindow()函数的使用比较简单,它会将指定的窗口设置到最前面并处于激活状态,该函数只有1个参数,是目标窗口的窗口句柄。

“模拟键盘”按钮对应的代码如下:

void CSimInputDlg::OnBtnSimkeybd()

{

//TODO: Add your control notification handler code here

//找到窗口

//将其设置到前台并激活

FindAndFocus();

Sleep(1000);

//模拟F5三次

keybd_event(VK_F5, 0, 0, 0);

Sleep(1000);

keybd_event(VK_F5, 0, 0, 0);

Sleep(1000);

keybd_event(VK_F5, 0, 0, 0);

}

在进行模拟键盘按键前,首先要调用自定义函数 FindAndFocus()将浏览器设置到最前面并处于激活状态(在“模拟鼠标”按钮中同样要先调用FindAndFocus()自定义函数)。通过调用keybd_event()函数来模拟F5键进行了3次网页的刷新。

“模拟鼠标”按钮对应的代码如下:

void CSimInputDlg::OnBtnSimmouse()

{

//TODO: Add your control notification handler code here

FindAndFocus();

//得到窗口在屏幕的坐标(x, y)

POINT pt = { 0 };

::ClientToScreen(m_hWnd, &pt);

//设置鼠标位置

SetCursorPos(pt.x + 36, pt.y + 395);

//模拟单击鼠标右键

//单击鼠标右键后,浏览器会弹出快捷菜单

mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);

Sleep(100);

mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);

Sleep(1000);

//0x52 = R

//在弹出右键菜单后按下R键

//会刷新页面

keybd_event(0x52, 0, 0, 0);

}

代码中用到了两个陌生的API 函数,分别是ClientToScreen ()和SetCursorPos()。它们的定义如下:

BOOL ClientToScreen(

HWND hWnd,  // handle to window

LPPOINT lpPoint // screen coordinates

);

ClientToScreen()函数的作用是将窗口区域的坐标转换为屏幕的坐标。更直接的解释是,得到指定窗口在屏幕中的坐标位置。

BOOL SetCursorPos(

int X, // horizontal position

int Y // vertical position

);

SetCursorPos()函数的作用是将鼠标移动到指定的坐标位置。

在程序中为什么不使用mouse_event()来移动鼠标的位置,而是使用SetCursorPos()的位置呢?在API函数中,与SetCursorPos()对应的一个函数是GetCursorPos(),而SetCursorPos()函数往往会与GetCursorPos()函数一起使用。因为在很多情况下,程序设置鼠标光标位置进行一系列操作后,仍需要将鼠标光标的位置设置回原来的位置,那么在调用 SetCursorPos()前,就需要调用GetCursorPos()得到鼠标光标的当前位置,这样才可以在操作完成后把鼠标光标设置为原来的位置。

在程序中调用SetCursorPos()函数时,参数中的x坐标和y坐标分别加了两个整型的常量,这里可能比较费解。这两个整型常量的作用是通过ClientToScreen()函数得到的是浏览器左上角的x和y坐标,而浏览器的鼠标右键菜单必须在浏览器的客户区中才能激活,因此需要在左上角坐标的基础上增加2个偏移,代码里的2个整型常量就是一个偏移。

3.小结

对于鼠标和键盘按键的模拟在很多地方都会使用,比如有的病毒用模拟鼠标单击杀毒软件的警告提示,比如游戏辅助工具通过模拟鼠标进行快速单击……对于鼠标和键盘按键的模拟并不简单。在常规的情况下,可以通过上面介绍的内容来进行鼠标和键盘按键的模拟操作。但是对于有些情况就不行了,比如有些游戏过滤了PostMessage()函数发送来的消息,有些游戏hook了keybd_event()和mouse_event()函数,有些游戏使用了DX来响应鼠标和键盘……