網(wǎng)絡(luò)安全編程:模擬鼠標鍵盤按鍵的操作
鼠標和鍵盤的操作也會被轉(zhuǎn)換為相應(yīng)的系統(tǒng)消息,窗口過程中在接收到鼠標或鍵盤消息后會進行相應(yīng)的處理。可以通過SendMessage()和PostMessage()發(fā)送消息到指定的窗口過程中,那么使用這兩個函數(shù)來發(fā)送鼠標和鍵盤的相關(guān)消息就可以進行鼠標和鍵盤的模擬操作。除了SendMessage()和PostMessage()外,還可以通過keybd_event()和mouse_event()兩個專用的函數(shù)進行鼠標和鍵盤按鍵的模擬操作。
01 基于發(fā)送消息的模擬
Windows的應(yīng)用程序是基于消息機制的,對于鼠標和鍵盤的操作也會被系統(tǒng)轉(zhuǎn)化為相應(yīng)的消息。首先來學(xué)習(xí)如何通過發(fā)送消息進行鼠標和鍵盤的模擬操作。
1. 鼠標、鍵盤按鍵常用的消息
無論是鼠標指針(或光標)的移動、單擊,還是鍵盤的按鍵,通常在Windows應(yīng)用程序中都會轉(zhuǎn)換成相應(yīng)的消息。在操作鼠標時,使用最多的是移動鼠標和單擊鼠標鍵。比如,在教新手使用計算機時會告訴他,將鼠標指針(或光標)移動到“我的電腦”上,然后單擊鼠標右鍵,在彈出的快捷菜單中用鼠標左鍵單擊選擇“屬性”對話框。當移動鼠標光標的時候,系統(tǒng)中對應(yīng)的消息是WM_MOUSEMOVE消息,按下鼠標左鍵時的對應(yīng)的消息是WM_LBUTTONDOWN,釋放鼠標左鍵時,對應(yīng)的消息是WM_LBUTTONUP。在系統(tǒng)中,鼠標的消息有很多。在MSDN中查詢到的鼠標消息如圖1所示。
圖1 鼠標相關(guān)消息
同樣,在系統(tǒng)中也定義了鍵盤的按下與抬起的消息。鍵盤按下的消息是WM_KEY DOWN,與之對應(yīng)的鍵盤抬起的消息是WM_KEYUP。除了這兩個消息外,還有一個消息是比較常用的,就是WM_CHAR消息。鍵盤的消息相對于鼠標要少很多,在MSDN中查詢到的鍵盤消息如圖2所示。
圖2 鍵盤相關(guān)消息
2. PostMessage()函數(shù)對鍵盤按鍵的模擬
PostMessage()和SendMessage()這兩個函數(shù)可以對指定的窗口發(fā)送消息。既然鼠標和鍵盤按鍵的操作被系統(tǒng)轉(zhuǎn)換為相應(yīng)的消息,那么就可以使用PostMessage()和SendMessage()通過按鼠標和鍵盤按鍵發(fā)送的消息來模擬它們的操作。對于模擬鍵盤按鍵消息,最好使用PostMessage()而不要使用SendMessage()。在很多情況下,SendMessage()是不會成功的。
現(xiàn)在編寫一個簡單的小工具,它通過PostMessage()函數(shù)模擬鍵盤發(fā)送(發(fā)送F5鍵的消息來模擬網(wǎng)頁的刷新)的信息來刷新網(wǎng)頁。首先打開VC6.0,創(chuàng)建一個MFC對話框工程,按照圖3所示設(shè)置界面。
圖3 模擬鍵盤刷新網(wǎng)頁界面布局
按照圖3所示的界面進行布局,然后為“開始”按鈕設(shè)置控件變量。這個小程序在“IE瀏覽器標題”處輸入要刷新的頁面的標題,在“刷新頻率”處輸入一個刷新的時間間隔,單位是秒。
當了解程序的功能并且將程序的界面布置好以后,就可以開始編寫程序的代碼了。程序的代碼分為兩部分,第一部分是程序要處理“開始”按鈕的事件,第二部分是要按照指定的時間間隔對指定的瀏覽器發(fā)送按F5鍵的消息來刷新網(wǎng)頁。
首先來編寫響應(yīng)“開始”按鈕事件的代碼,雙擊“開始”按鈕來編寫它的響應(yīng)事件。代碼如下:
- 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 == "開始" )
- {
- // 設(shè)置定時器
- SetTimer(1, nInterval * 1000, NULL);
- m_Start.SetWindowText("停止");
- GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(FALSE);
- GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(FALSE);
- }
- else
- {
- // 結(jié)束定時器
- KillTimer(1);
- m_Start.SetWindowText("開始");
- GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(TRUE);
- GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(TRUE);
- }
- }
在代碼中,首先判斷按鈕的文本,如果是“開始”,則通過SetTimer()函數(shù)設(shè)置一個定時器;如果按鈕的文本不是“開始”,則通過KillTimer()函數(shù)關(guān)閉定時器。
這里的SetTimer()和KillTimer()是MFC中CWnd類的兩個成員函數(shù),不是API函數(shù)。很多MFC中的類成員函數(shù)和API函數(shù)的寫法是一樣的,但是它們還是有區(qū)別的。比較一下SetTimer()在MFC中的定義和API函數(shù)的定義的差別。
MFC中的定義如下:
- UINT SetTimer(
- UINT nIDEvent,
- UINT nElapse,
- void (CALLBACK EXPORT* lpfnTimer)(
- HWND, UINT, UINT, DWORD) );
API函數(shù)的定義如下:
- UINT_PTR SetTimer(
- HWND hWnd,
- UINT_PTR nIDEvent,
- UINT uElapse,
- TIMERPROC lpTimerFunc
- );
從定義中可以看出,MFC中SetTimer()函數(shù)的定義比API中SetTimer()函數(shù)的定義少了一個參數(shù),即HWND的窗口句柄的參數(shù)。在MFC中,窗口相關(guān)的成員函數(shù)都不需要指定窗口句柄,在MFC的內(nèi)部已經(jīng)維護了一個m_hWnd的句柄變量(如果想要查看或使用MFC內(nèi)部維護的m_hWnd成員變量,可以直接使用它,也可以通過調(diào)用GetSafeHwnd()成員函數(shù)來得到它,推薦使用第二種方法)。
在按鈕事件中添加定時器,那么定時器會按照指定的時間間隔進行相應(yīng)的處理。定時器部分的代碼如下:
- void CKeyBoardDlg::OnTimer(UINT nIDEvent)
- {
- // 在此處添加處理程序代碼
- HWND hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));
- // 發(fā)送鍵盤按下消息
- ::PostMessage(hWnd, WM_KEYDOWN, VK_F5, 1);
- Sleep(50);
- // 發(fā)送鍵盤抬起消息
- ::PostMessage(hWnd, WM_KEYUP, VK_F5, 1);
- CDialog::OnTimer(nIDEvent);
- }
關(guān)于定時器的處理非常簡單,通過FindWindow()函數(shù)得到要刷新窗口的句柄,然后發(fā)送WM_KEYDOWN和WM_KEYUP消息來模擬鍵盤按鍵即可。其實在模擬的過程中,可以省去WM_KEYUP消息的發(fā)送,但是為了模擬效果更接近真實性,建議在模擬時將消息成對發(fā)送。
將寫好的程序編譯連接后運行起來看效果,在“IE瀏覽器標題”處輸入瀏覽器的標題,這個標題可以通過Spy++獲得,然后在“刷新頻率”處輸入1。然后單擊“開始”按鈕,觀察瀏覽器每個1秒進行刷新一次。當單擊“停止”按鈕后,程序不再對瀏覽器進行刷新按鍵模擬。
到此,通過PostMessage()函數(shù)發(fā)送按F5鍵進行鍵盤按鍵模擬的程序就完成了。使用PostMessage()函數(shù)的好處是目標窗口可以在后臺,而不需要窗口處于激活狀態(tài)。可以將被刷新的瀏覽器最小化,然后運行刷新網(wǎng)頁的小程序,在任務(wù)欄可以看到瀏覽器仍然在不斷刷新。
02 通過API函數(shù)模擬鼠標鍵盤按鍵的操作
在開發(fā)程序時,總是依靠發(fā)送消息是非常辛苦的事情,因為消息的類型非常多,并且不同消息的附件參數(shù)也因不同的消息類型而異。Windows幾乎為每個常用的消息都提供了相應(yīng)的API函數(shù)。為了不必記憶過多的消息,使用API函數(shù)進行開發(fā)是相對比較直觀的。
1. 鼠標鍵盤按鍵模擬函數(shù)
在使用Windows的系統(tǒng)消息進行模擬鼠標或鍵盤按鍵操作時,可能顯得不直觀,也不方便。微軟公司在進行設(shè)計時已經(jīng)考慮到了這點,因此在Windows下的大部分消息都可以直接使用對應(yīng)的等價API函數(shù),不必直接通過發(fā)送消息。比如可以用WM_GETTEXT消息去獲取文本的內(nèi)容,對應(yīng)的函數(shù)有GetWindowText()。試想一下,如果程序中一眼看去都是SendMessage()與PostMessage()之類的函數(shù),豈不是很嚇人。
下面介紹兩個函數(shù),分別用來模擬鼠標和鍵盤的輸入,它們分別是keybd_event()和mouse_event(),定義如下:
- VOID keybd_event(
- BYTE bVk,
- BYTE bScan,
- DWORD dwFlags,
- ULONG_PTR dwExtraInfo
- );
- VOID mouse_event(
- DWORD dwFlags,
- DWORD dx,
- DWORD dy,
- DWORD dwData,
- ULONG_PTR dwExtraInfo
- );
從函數(shù)的名稱就能看出,這兩個API函數(shù)分別對應(yīng)的是鍵盤事件和鼠標事件,在程序里使用時,對于閱讀代碼的人來說就比較直觀了。下面使用keybd_event()和mouse_event()兩個函數(shù)來完成上面刷新網(wǎng)頁的小工具。
2. 網(wǎng)頁刷新工具
keybd_event()和 mouse_event()這兩個 API 函數(shù),從函數(shù)的參數(shù)上來看,不需要給它們傳遞窗口句柄當作參數(shù)。那么這兩個函數(shù)在進行鼠標和鍵盤的模擬時就必須將目標窗口激活并處于所有窗口的最前端。因此在程序中首先要完成的是將目標窗口設(shè)置到最前面,并且處于激活狀態(tài)。先來看一下程序的界面部分,如圖4所示。
圖4 模擬鼠標鍵盤
這次的窗口相比上個程序的窗口要簡單些。在界面上有兩個按鈕,第1個按鈕“模擬鍵盤”是通過keybd_event()來模擬按F5鍵從而刷新網(wǎng)頁,第2個按鈕“模擬鼠標”是通過mouse_event()來模擬鼠標右鍵,從而彈出瀏覽器的快捷菜單,再通過keybd_event()模擬按R鍵來刷新網(wǎng)頁。
知道了程序要實現(xiàn)的功能,先來完成將目標窗口設(shè)置到最前面并處于激活狀態(tài)的部分,代碼如下:
- VOID CSimInputDlg::FindAndFocus()
- {
- GetDlgItemText(IDC_EDIT_CAPTION, m_StrCaption);
- // 判斷輸入是否為空
- if ( m_StrCaption == "" )
- {
- return ;
- }
- m_hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));
- // 該函數(shù)將創(chuàng)建指定窗口的線程設(shè)置到前臺
- // 并且激活該窗口
- ::SetForegroundWindow(m_hWnd);
- }
這個自定義函數(shù)非常簡單,分別調(diào)用了FindWindow()和SetForegroundWindow()兩個API函數(shù)。SetForegroundWindow()函數(shù)的使用比較簡單,它會將指定的窗口設(shè)置到最前面并處于激活狀態(tài),該函數(shù)只有1個參數(shù),是目標窗口的窗口句柄(這里的窗口句柄變量m_hWnd就是由MFC提供的變量,該值也可以使用GetSafeHwnd()函數(shù)來進行獲取。)
“模擬鍵盤”按鈕對應(yīng)的代碼如下:
- void CSimInputDlg::OnBtnSimkeybd()
- {
- // 在此處添加處理程序代碼
- // 找到窗口
- // 將其設(shè)置到前臺并激活
- 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);
- }
在進行模擬鍵盤按鍵前,首先要調(diào)用自定義函數(shù)FindAndFocus()將瀏覽器設(shè)置到最前面并處于激活狀態(tài)(在“模擬鼠標”按鈕中同樣要先調(diào)用FindAndFocus()自定義函數(shù))。通過調(diào)用keybd_event()函數(shù)來模擬F5鍵進行了3次網(wǎng)頁的刷新。
“模擬鼠標”按鈕對應(yīng)的代碼如下:
- void CSimInputDlg::OnBtnSimmouse()
- {
- // 在此處添加處理程序代碼
- FindAndFocus();
- // 得到窗口在屏幕的坐標(x, y)
- POINT pt = { 0 };
- ::ClientToScreen(m_hWnd, &pt);
- // 設(shè)置鼠標位置
- 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函數(shù),分別是ClientToScreen ()和SetCursorPos()。它們的定義如下:
- BOOL ClientToScreen(
- HWND hWnd, // handle to window
- LPPOINT lpPoint // screen coordinates
- );
ClientToScreen()函數(shù)的作用是將窗口區(qū)域的坐標轉(zhuǎn)換為屏幕的坐標。更直接的解釋是,得到指定窗口在屏幕中的坐標位置。
- BOOL SetCursorPos(
- int X, // horizontal position
- int Y // vertical position
- );
SetCursorPos()函數(shù)的作用是將鼠標光標移動到指定的坐標位置。
在程序中為什么不使用mouse_event()來移動鼠標光標的位置,而是使用SetCursorPos()的位置呢?在API函數(shù)中,與SetCursorPos()對應(yīng)的一個函數(shù)是GetCursorPos(),而SetCursorPos()函數(shù)往往會與GetCursorPos()函數(shù)一起使用。因為在很多情況下,程序設(shè)置鼠標光標位置進行一系列操作后,仍需要將鼠標光標的位置設(shè)置回原來的位置,那么在調(diào)用SetCursorPos()前,就需要調(diào)用GetCursorPos()得到鼠標光標的當前位置,這樣才可以在操作完成后把鼠標光標設(shè)置為原來的位置。由此也可以看出,很多API函數(shù)是成對出現(xiàn)的,有Set也有Get,這樣在記憶的時候非常的方便。
在程序中調(diào)用SetCursorPos()函數(shù)時,參數(shù)中的x坐標和y坐標分別加了兩個整型的常量,這里可能比較費解。這兩個整型常量的作用是通過ClientToScreen()函數(shù)得到的是瀏覽器左上角的x和y坐標,而瀏覽器的鼠標右鍵菜單必須在瀏覽器的客戶區(qū)中才能激活,因此需要在左上角坐標的基礎(chǔ)上增加兩個偏移,代碼里的兩個整型常量就是一個偏移(這里的偏移值可以自己隨意修改,只要保證鼠標能夠落在瀏覽器窗口中即可)。
對于鼠標和鍵盤按鍵的模擬在很多地方都會使用,比如有的病毒用模擬鼠標單擊殺毒軟件的警告提示,比如游戲輔助工具通過模擬鼠標進行快速單擊……對于鼠標和鍵盤按鍵的模擬并不簡單。在常規(guī)的情況下,可以通過上面介紹的內(nèi)容來進行鼠標和鍵盤按鍵的模擬操作。但是對于有些情況就不行了,比如有些游戲過濾了PostMessage()函數(shù)發(fā)送來的消息,有些游戲hook了keybd_event()和mouse_event()函數(shù),有些游戲使用了DX來響應(yīng)鼠標和鍵盤……