1. 程式人生 > >遊戲修改器製作教程一:鍵盤滑鼠模擬

遊戲修改器製作教程一:鍵盤滑鼠模擬

本教程面向有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);

驅動模擬很強大,不過比較麻煩,一般也用不到_(:з」∠)_