遊戲修改器製作教程一:鍵盤滑鼠模擬
本教程面向有C\C++基礎的人,最好還要懂一些Windows程式設計知識
程式碼一律用Visual Studio 2013編譯,如果你還在用VC6請趁早丟掉它...
寫這個教程只是為了讓玩家更好地體驗所愛的單機遊戲,順便學到些逆向知識,我不會用網路遊戲做示範,請自重
先從最簡單的模擬操作講起
模擬鍵盤滑鼠有很多方法,我大體分為訊息模擬、API模擬、驅動模擬
對於網頁的話還可以用JavaScript模擬,雖然這不在本教程範圍
訊息模擬
學習Windows程式設計都知道Windows程式會響應視窗訊息,那麼我們自己發個訊息過去程式就會認為是人在操作而響應了
看看要用到的API
// 傳送訊息到指定視窗,不用等待訊息處理就返回,引數和視窗過程裡的一樣 BOOL WINAPI PostMessage( _In_opt_ HWND hWnd, _In_ UINT Msg, _In_ WPARAM wParam, _In_ LPARAM lParam ); // 獲取視窗控制代碼,引數是視窗類名和視窗標題,其中一個可以傳入NULL表示通配 HWND FindWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName ); // 用來獲取子視窗控制代碼 HWND WINAPI FindWindowEx( _In_opt_ HWND hwndParent, _In_opt_ HWND hwndChildAfter, _In_opt_ LPCTSTR lpszClass, _In_opt_ LPCTSTR lpszWindow );
以記事本為例子
首先要知道記事本的視窗類名
開啟記事本,開啟VS2013,在工具裡找到spy++
在工具條找到查詢視窗,把查詢程式工具拖到記事本視窗,得到了記事本視窗的類名"Notepad"
同理可以知道編輯框的類名是Edit
我們寫個程式模擬在編輯框按下A健
HWND notepadWnd = FindWindow(_T("Notepad"), NULL); // 記事本視窗控制代碼 if (notepadWnd == NULL) { printf("沒有找到記事本視窗\n"); return 0; } HWND editWnd = FindWindowEx(notepadWnd, NULL, _T("Edit"), NULL); // 編輯框視窗控制代碼 const BYTE vk = 'A'; // 虛擬鍵碼 //UINT scanCode = MapVirtualKey(vk, MAPVK_VK_TO_VSC); // 掃描碼 PostMessage(editWnd, WM_KEYDOWN, vk, 1 /*| scanCode << 16*/); Sleep(100); PostMessage(editWnd, WM_KEYUP, vk, 1 /*| scanCode << 16*/ | 1 << 30 | 1 << 31);
執行程式,看看記事本里是不是多了個a
再寫個程式模擬點選滑鼠右鍵
用到的新API
// 取座標處視窗控制代碼
HWND WINAPI WindowFromPoint(
_In_ POINT Point
);
// 取滑鼠座標
BOOL WINAPI GetCursorPos(
_Out_ LPPOINT lpPoint
);
// 把螢幕座標轉為相對於視窗客戶區的座標
BOOL ScreenToClient(
_In_ HWND hWnd,
LPPOINT lpPoint
);
模擬滑鼠右鍵點選的程式
Sleep(3000); // 等待3秒把滑鼠移到指定視窗 POINT pos; // 滑鼠座標 GetCursorPos(&pos); HWND wnd = WindowFromPoint(pos); // 滑鼠指向的視窗的控制代碼 ScreenToClient(wnd, &pos); // 把pos轉成相對於視窗客戶區的座標 LPARAM lParam = MAKELPARAM(pos.x, pos.y); PostMessage(wnd, WM_RBUTTONDOWN, 0, lParam); Sleep(100); PostMessage(wnd, WM_RBUTTONUP, 0, lParam);
執行後把滑鼠移到記事本,會彈出選單
傳送訊息模擬輸入的方法好處是就算視窗最小化了也可以模擬,但是缺點是不是所有程式都會處理視窗訊息,比如大部分遊戲是用DInput輸入的
API模擬
API模擬就是用Windows提供的API模擬輸入,比如keybd_event、mouse_event、SendInput,但是微軟建議用SendInput代替另外兩個,那我就只講SendInput怎麼用了
用到的API
UINT WINAPI SendInput(
_In_ UINT nInputs,
_In_ LPINPUT pInputs,
_In_ int cbSize
);
typedef struct tagINPUT {
DWORD type;
union {
MOUSEINPUT mi;
KEYBDINPUT ki;
HARDWAREINPUT hi;
};
} INPUT, *PINPUT;
這個API可以模擬鍵盤按下、滑鼠移動、滑鼠點選等事件,引數是INPUT結構的數量、INPUT陣列的指標、INPUT結構的大小
INPUT中type取值為INPUT_MOUSE、INPUT_KEYBOARD、INPUT_HARDWARE,分別表示使用mi、ki、hi結構
模擬滑鼠移動到螢幕中間點選右鍵
INPUT input[3];
ZeroMemory(&input, sizeof(input));
// 滑鼠移動到螢幕中間,也可以用SetCursorPos(x, y)
input[0].type = INPUT_MOUSE;
input[0].mi.dx = 65535 / 2; // 座標取值範圍是0-65535
input[0].mi.dy = 65535 / 2;
input[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
// 點選滑鼠右鍵
input[1].type = INPUT_MOUSE;
input[1].mi.dwFlags = MOUSEEVENTF_RIGHTDOWN;
input[2].type = INPUT_MOUSE;
input[2].mi.dwFlags = MOUSEEVENTF_RIGHTUP;
SendInput(_countof(input), input, sizeof(INPUT));
模擬按下A鍵:
INPUT input[2];
ZeroMemory(&input, sizeof(input));
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 'A';
// 也可以不加這句但是對DInput輸入的程式會沒用
input[0].ki.wScan = MapVirtualKey(input[0].ki.wVk, MAPVK_VK_TO_VSC);
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = input[0].ki.wVk;
input[1].ki.wScan = input[0].ki.wScan;
input[1].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(_countof(input), input, sizeof(INPUT));
來個高階點的例子:東方花映冢Z鍵連打
東方花映冢裡想發輕彈幕就要不停按Z鍵,這樣很費勁,所以我想實現按住C鍵就能自動發輕彈幕的功能(就像妖精大戰爭那樣)
這個程式用到了MFC,看不懂的話建議學一下MFC程式設計
// 開啟
void CC2ZDlg::OnBnClickedButton1()
{
m_enableButton.EnableWindow(FALSE);
m_disableButton.EnableWindow(TRUE);
SetTimer(1, 200, timerProc); // 每0.2s檢測C鍵是否按下,並模擬Z鍵
}
// 關閉
void CC2ZDlg::OnBnClickedButton2()
{
m_enableButton.EnableWindow(TRUE);
m_disableButton.EnableWindow(FALSE);
KillTimer(1);
}
//定時模擬按下Z
void CALLBACK CC2ZDlg::timerProc(HWND hWnd, UINT nMsg, UINT nTimerid, DWORD dwTime)
{
if ((GetKeyState('C') & (1 << 15)) != 0) // C鍵按下
{
INPUT input;
ZeroMemory(&input, sizeof(input));
input.type = INPUT_KEYBOARD;
input.ki.wVk = 'Z';
input.ki.wScan = MapVirtualKey(input.ki.wVk, MAPVK_VK_TO_VSC);
SendInput(1, &input, sizeof(INPUT)); // 按下Z鍵
Sleep(100); // 可能東方是在處理邏輯時檢測一下Z鍵是否按下才發彈幕,如果這時Z鍵剛好彈起就沒有反應,所以要延遲一下
input.ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, &input, sizeof(INPUT)); // 彈起Z鍵
}
}
這樣就可以模擬大部分遊戲的輸入了,但是有些遊戲會有保護,這樣就要用到驅動模擬
驅動模擬
驅動模擬就是自己寫驅動程式,在系統核心裡面操作I/O埠,給連線鍵盤的積體電路(一般是8042晶片)傳送指令,讓它產生一個按下按鍵的資訊,這樣你的模擬輸入對於所有程式來說就是從一個真實的裝置發出的,而且可以繞過很多保護
(需要操作I/O埠的話可以學習一下WinIo庫)
然而我並不會寫這種驅動_(:з」∠)_,而且x64系統中載入驅動需要有可信任的數字簽名,否則會比較麻煩,而且還要知道8042晶片相關的底層知識...
所以我找了個別人寫的庫實現驅動模擬
Interception官網
Interception API的Git庫
它的驅動有數字簽名而且在XP到win10的平臺上都測試過了
它還可以攔截並修改輸入(包括CTRL+ALT+DELETE),不過這裡我只講模擬輸入所以自己研究吧...
(好像它的模擬輸入也不是操作埠而是核心版的SendInput?)
安裝方法:
(可以去我的網盤)下載Interception.zip,解壓後執行install-interception.exe
環境搭建:如果目標系統是64位的要先在配置管理器里加入x64配置
在你的專案屬性裡找到VC++目錄,包含目錄加上Interception\library,庫目錄根據目標系統是64位還是32位加上library\x64或library\x86
找到連結器-輸入,附加依賴項加上interception.lib
然後把library\x64或library\x86裡的interception.dll放到你的程式同目錄下
最後在你的原始碼裡#include <interception.h>
模擬滑鼠移動到螢幕中間點選右鍵:
InterceptionContext context = interception_create_context();
InterceptionMouseStroke mouseStroke[3];
ZeroMemory(mouseStroke, sizeof(mouseStroke));
// 滑鼠移動到螢幕中間
mouseStroke[0].flags = INTERCEPTION_MOUSE_MOVE_ABSOLUTE;
mouseStroke[0].x = 65535 / 2; // 座標取值範圍是0-65535
mouseStroke[0].y = 65535 / 2;
// 點選滑鼠右鍵
mouseStroke[1].state = INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN;
mouseStroke[2].state = INTERCEPTION_MOUSE_RIGHT_BUTTON_UP;
interception_send(context, INTERCEPTION_MOUSE(0), (InterceptionStroke*)mouseStroke, _countof(mouseStroke));
interception_destroy_context(context);
模擬按下A鍵:
InterceptionContext context = interception_create_context();
InterceptionKeyStroke keyStroke[2];
ZeroMemory(keyStroke, sizeof(keyStroke));
keyStroke[0].code = MapVirtualKey('A', MAPVK_VK_TO_VSC);
keyStroke[0].state = INTERCEPTION_KEY_DOWN;
keyStroke[1].code = keyStroke[0].code;
keyStroke[1].state = INTERCEPTION_KEY_UP;
interception_send(context, INTERCEPTION_KEYBOARD(0), (InterceptionStroke*)keyStroke, _countof(keyStroke));
interception_destroy_context(context);
驅動模擬很強大,不過比較麻煩,一般也用不到_(:з」∠)_