天天看點

【GamingAnywhere源碼分析之知識補充二】Windows鈎子機制

本文提綱:

       1. 目前GA(Gaminganywhere)開發完成以及正在進行的工作;

       2. Windows Hook(鈎子)在GA中的應用;

       3. 關于Windows Hook(鈎子);

       4. Windows Hook(鈎子)代碼舉例,完成線程鈎子,監聽QQ特定對話視窗并解析鍵盤操作,将鍵盤資訊儲存到檔案中;

       5. 程式運作截圖;

一、目前GA(Gaminganywhere)開發完成以及正在進行的工作;

       1) 以《極品飛車》為例生成支援x64遊戲以及采用DirectX11方式啟動方式的代碼改造;

            針對《極品飛車》遊戲的d11方式啟動已經修改完成,期間也碰到了很多問題,在這些問題的解決中對GA的局限性以及我們自己的能動性也有了較新的認識,首先:支援d11方式啟動的遊戲在底層的實作細節中不一定是按照GA代碼中特定的傳統方式,比如說先通過D3D11CreateDeviceAndSwapChain建立出Device以及Swap,然後再進行Present()等,在修改代碼的過程中,思維一直局限在按它的路子應該是先hook  D3D11CreateDeviceAndSwapChain然後再向下執行,是以在調試的過程中一直過不去,直到檢視到一篇文章:https://github.com/scen/stanleycen.com/blob/master/posts/directx11-hooking.md 上面提到:并不是所有的遊戲都需要調用D3D11CreateDeviceAndSwapChain函數,其它的方式是通過CreateDXGIFactory或CreateDXGIFactory1來代替,當采用這種方式的時候還需要調用CreateSwapChain,從這裡就可以獲得SwapChain,然後就可以進行後續的工作,而不采用D3D11CreateDeviceAndSwapChain()函數的一個表現就是通過調用它獲得的swapChain一直等于NULL,而且沒有報錯,看到這裡的時候我就知道肯定是這個原因了,于是利用detour到CreateDXGIFactory1上來進而代碼執行安裝預想的方式進行了;

       2) GA服務端建構守護程序,同時用戶端配合修改以完成一個用戶端連接配接過來,直接啟動遊戲、建立連接配接、并進行資料傳送;

            關于這一部分,GA伺服器端的守護程序已經成功建立,監聽用戶端建立連接配接的請求,原本GA的運作機制是運作伺服器端将遊戲啟動起來後,用戶端再連接配接過來擷取視訊流,我們改造後是GA守護程序啟動,用戶端請求的連接配接中跟随遊戲的名稱,即需要啟動哪些遊戲,伺服器端與用戶端進行一次socket通信擷取到用戶端需要啟動什麼程式,然後将該socket儲存下來,在利用rtsp發送資料的時候就可以針對不同的用戶端啟動不同的應用,然後待應用程式啟動後再進行一次通信通知用戶端程序繼續向下執行,這種解決方式應該會對并發連接配接的session上有較好的支援,這部分已經完成了一半,對我們的設想還沒有測試。

二、Windows Hook(鈎子)在GA中的應用:

        Windows鈎子的應用在GA的了解中起着很重要的作用,這是基于窗體捕捉的核心,在遊戲啟動之前它會挂上全局的鈎子,當視窗啟動就會調用鈎子的回調函數,完成一系列的視訊捕捉以及資料包的發送。由于是全局鈎子,是以所有視窗的啟動它都可以捕捉到,那我們如何區分哪個是我們真正需要捕捉的呢?在GA中如果是正常啟動的遊戲在進入挂鈎之前都會寫環境變量,在正式捕捉前會判斷該環境變量的值是否存在,如果存在則說明是我們啟動的遊戲,如果沒有則說明它是無關的視窗,直接detach掉就可以了。

三、關于Windows Hook(鈎子);

        鈎子(Hook),是Windows消息處理機制的一個平台,應用程式可以在上面設定子程以監視指定視窗的某種消息,而且所監視的視窗可以是其它程序所建立的。當消息到達後,在目标視窗處理函數之前處理它。鈎子機制允許應用程式截獲處理Window消息或特定事件。鈎子實際上是一個處理消息的程式段,通過系統調用,把它挂入系統。每當特定的消息發出,在沒有到達目的視窗前,鈎子程式就先捕獲該消息,亦即鈎子函數先得到控制權,這時鈎子函數即可以加工處理(改變)該消息,也可以不作處理而繼續傳遞該消息,還可以強制結束消息的傳遞。

        每一個Hook都有一個與之關聯的指針清單,稱之為鈎子連結清單,由系統來維護。這個清單的指針指向指定的,應用程式定義的,被Hook子程調用的回調函數,也就是該鈎子的各個處理子程。當與指定的Hook類型關聯的消息發生時,系統就把這個消息傳遞到Hook子程。一些Hook子程可以隻監視消息,或者修改消息,或者停止消息的前進,避免這些消息傳遞到下一個Hook子程或者目的視窗。最近安裝的鈎子放在鍊的開始,而最早安裝的鈎子放在最後,也就是後加入的先獲得控制權。Windows并不要求鈎子子程的解除安裝順序一定得和安裝順序相反。每當有一個鈎子被解除安裝,windows便釋放其占用的記憶體,并更新整個Hook連結清單。如果程式安裝了鈎子,但是在尚未解除安裝鈎子子程之前就結束了,那麼系統會自動為它做解除安裝鈎子的操作。鈎子子程是一個應用程式定義的回調函數(callback function),不能定義成某個類的成員函數,隻能定義為普通的C函數。

        鈎子函數既可以Hook本程式中的線程,也可以對其它程式中的某個線程進行Hook,為了實作後者的效果必須将鈎子函數放在DLL中,别的程序會将該DLL映射到該程序的位址空間中,進而可以使用共享的程式段。

四、Windows Hook(鈎子)代碼舉例,完成線程鈎子,監聽QQ特定對話視窗并解析鍵盤操作,将鍵盤資訊儲存到檔案中;

        這裡用到動态連結庫DLL,關于這方面的知識在上一篇中有相關介紹,下面貼出主要實作代碼:

鈎子DLL實作的CPP檔案:

#include "stdafx.h"
#include "do-hook.h"
#include <stdlib.h>
#include <stdio.h>

// 設定程序共享資料段,不通程序之間可以共享之間的資料
// 注意:位于共享資料端内的變量必須進行初始化
#pragma data_seg("SharedData");
DWORD pthread_ID = 0 ;
int EXIT_CODE = 0 ;
#pragma data_seg();

#pragma comment(linker , "/SECTION:SharedData,RWS")

static HHOOK myHook_m = NULL ;
static HHOOK myHook_w = NULL ;

static HMODULE  hInst = NULL ;
static int i = 0 ;
FILE * f_handle = fopen("C:\\dxm.txt" , "w+");


// This is an example of an exported variable
DOHOOK_API int ndohook=0;

// This is an example of an exported function.
DOHOOK_API int fndohook(void)
{
	return 42;
}

// This is the constructor of a class that has been exported.
// see do-hook.h for the class definition
Cdohook::Cdohook()
{
	return;
}

MODULE MODULE_EXPORT 
int install_hook()
{

	// 利用FindWindowc查找QQ某對話框的句柄
	HWND qq_w = FindWindow("TXGuiFoundation","段曉明");
	if(qq_w == NULL){
		printf("FindWindow failed and erro num is : %d.\n" , GetLastError());
		return -1;
	}

	// 根據視窗句柄查找線程ID
	pthread_ID = GetWindowThreadProcessId(qq_w , NULL);
	if(!pthread_ID){
		printf("GetWindowThreadProcessId error and pthread_ID = %d.\n" , pthread_ID);
		return -1 ;
	}else{
	    printf("GetWindowThreadProcessId success and pthread_ID = %d.\n" , pthread_ID);
	}

    // 利用線程ID挂鈎子
	// 注意:WH_KEYBOARD_LL隻能用于全局鈎子,否則傳回錯誤碼為:1429 , 描述:
    // ERROR_GLOBAL_ONLY_HOOK Error description : This hook procedure can only be set globally
	// so use WH_KEYBOARD
	// 這是鍵盤鈎子
	if((myHook_m = SetWindowsHookEx(WH_KEYBOARD , (HOOKPROC)do_hook , hInst , pthread_ID)) == NULL){
		printf("SetWindowsHookEx() execute error and error code is %d. \n" , GetLastError());
		return -1;
	}else{
	    printf("SetWindowsHookEx() execute success. \n");
	}

	// 設定視窗鈎子,監聽線程視窗關閉的消息
	if((myHook_w = SetWindowsHookEx(WH_SHELL , (HOOKPROC)do_hook_w , hInst , pthread_ID)) == NULL){
		printf("SetWindowsHookEx() execute error and error code is %d. \n" , GetLastError());
		return -1;
	}else{
	    printf("SetWindowsHookEx() execute success. \n");
	}

	return 0 ;
}

// 解除安裝鈎子
MODULE MODULE_EXPORT 
int uninstall_hook()
{
	if(myHook_m != NULL){
	    UnhookWindowsHookEx(myHook_m);
		myHook_m = NULL ;
		printf("UnhookWindowsHookEx() success. \n");
	}

	if(myHook_w != NULL){
	    UnhookWindowsHookEx(myHook_w);
		myHook_w = NULL ;
		printf("UnhookWindowsHookEx() success. \n");
	}

	if(f_handle != NULL){
		fclose(f_handle);
		f_handle = NULL;
		printf("File fclose() success. \n");
	}
	
	return 0 ;
}

// 擷取線程ID
MODULE MODULE_EXPORT
DWORD GetThreadID()
{
	return pthread_ID;
}

// 擷取線程退出碼
MODULE MODULE_EXPORT
int GetThreadExitCode()
{
	return EXIT_CODE;
}

// 鈎子的回調函數
LRESULT CALLBACK do_hook(int nCode, WPARAM wParam, LPARAM lParam) 
{
// nCode:是hook代碼,Hook子程利用這個參數來确定任務,該參數的值依賴于Hook類型,每一種hook都有自己的hook代碼特征字元集
// wParm,lParm的值依賴與hook代碼,他們的典型值是包含了關于發送和接收消息的資訊

	/*
	// lParam指向一個EVENTMSG的結構體
	EVENTMSG *msg = (EVENTMSG*)lParam;

	// 判斷是系統擊鍵消息
	if(msg->message == WM_KEYUP || msg->message == WM_KEYDOWN){

		// 擷取按鍵的虛拟碼
		//UINT paramL = msg->paramL ;
		MessageBox(NULL , L"You press the key." , L"Demo" , MB_OK);
		
		// 将鍵盤虛拟碼轉換為ASCII碼
	}
	*/

	if(nCode == HC_ACTION){
		if(lParam & 0x80000000){

			 // 下面的兩句導緻QQ程式報錯
             //KBDLLHOOKSTRUCT *key_info = (KBDLLHOOKSTRUCT *)lParam ;
			 char buf[256];
			 memset(buf , 0 , sizeof(buf));

			 // 過濾掉特殊字元,例如:空格鍵,Ctrl鍵等
			 if(wParam == VK_SPACE || wParam == VK_BACK){
				 memcpy(buf , "  " , strlen("  "));
				 fwrite(buf , 1 , strlen(buf) , f_handle);
				 fflush(f_handle);
				 return true ;
			 }

			 // 擷取虛拟鍵盤碼對應的字元
			 GetKeyNameText(MapVirtualKey(wParam,0)<<16, buf , 32);
			 
			 fwrite(buf , 1 , strlen(buf) , f_handle);

			 fflush(f_handle);
		}
	}

	return CallNextHookEx(myHook_m, nCode, wParam, lParam);
}


// 監聽視窗是否關閉的鈎子回調函數
LRESULT CALLBACK do_hook_w(int nCode, WPARAM wParam, LPARAM lParam) 
{
	// 視窗銷毀
	if(nCode == HSHELL_WINDOWDESTROYED){
		//MessageBox(NULL , "視窗即将銷毀" , "友情提示" , MB_OK);		
		EXIT_CODE = 1 ;

		fflush(f_handle);
	}

	return CallNextHookEx(myHook_w, nCode, wParam, lParam);
}



BOOL APIENTRY 
DllMain( HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		hInst = hModule ;
		printf("hello word ...\n");
		break ;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}
           

調用測試程式HookTest.cpp檔案:

#include "stdafx.h"
//#include "do-hook.h"
#include <Windows.h>

#pragma comment(lib , "do-hook.lib")

// 聲明函數指針類型
typedef int (*INSTALL_HOOK)();   // 鈎子安裝函數指針
typedef int (*UN_INSTALL_HOOK)();  // 鈎子解除安裝函數指針
typedef int (*GetThreadExitCode)();     // 擷取線程退出狀态函數指針
typedef DWORD (*GetThreadID)();         // 擷取線程ID指針

int main(int argc, char * argv[])
{
	HINSTANCE hDLL ;
	INSTALL_HOOK install_hook;
	UN_INSTALL_HOOK un_install_hook ;
	GetThreadExitCode getExitCode;
	GetThreadID getThreadID ;

	STARTUPINFO startupInfo ;
	PROCESS_INFORMATION procInfo ;

	// 注意:這裡的應用程式位數要對應程式編譯的位數
	char * app_exe = "C:\\Windows\\System32\\mspaint.exe";

	printf("================== start ================\n");
	// 1> Load dll
	// LoadLibrary():載入指定的動态連結庫,并将它映射到目前程序使用的位址空間;一旦載入,即可通路庫内儲存的資源
	if((hDLL = LoadLibrary("do-hook.dll")) == NULL){
		printf("LoadLibrary() execute error and error code : %d .\n" , GetLastError());
		system("pause");
		return -1 ;
	}

	// 2> Load function address
	if((install_hook = (int (*)())GetProcAddress(hDLL , "install_hook")) == NULL)
	{
		printf("GetProcAddress install_hook failed and error code : %d . \n" , GetLastError());
		system("pause");
	    return -1 ;
	}

	if((un_install_hook = (int (*)())GetProcAddress(hDLL , "uninstall_hook")) == NULL){
		printf("GetProcAddress uninstall_hook failed and error code : %d . \n" , GetLastError());
		system("pause");
		return -1 ;
	}

	if((getExitCode = (int (*)())GetProcAddress(hDLL , "GetThreadExitCode")) == NULL){
		printf("GetProcAddress getExitCode failed and error code : %d . \n" , GetLastError());
		system("pause");
		return -1 ;
	}

	if((getThreadID = (DWORD (*)())GetProcAddress(hDLL , "GetThreadID")) == NULL){
		printf("GetProcAddress getThreadID failed and error code : %d . \n" , GetLastError());
		system("pause");
		return -1 ;
	}

	// 3> Call install_hook function
	install_hook();

	// 4> CreateProcess
	/*
	ZeroMemory(&startupInfo , sizeof(startupInfo));
	ZeroMemory(&procInfo , sizeof(procInfo));
	if(CreateProcess(app_exe , NULL , NULL , NULL , FALSE , CREATE_NEW_CONSOLE , NULL , NULL , &startupInfo , &procInfo) == 0){

		printf("CreateProcess execute failed and error code = %d.\n" , GetLastError());
		system("pause");
		return -1 ;
	}

	// 5> WaitForSingleObject
	WaitForSingleObject(procInfo.hProcess , INFINITE);
    */

	// 方法一:通過線程ID擷取線程句柄,然後通過WaitForSingleObject()函數等待線程結束
	/*
	DWORD pThreadID = getThreadID();
	HANDLE pThreadHandle = OpenThread(PROCESS_ALL_ACCESS , FALSE , pThreadID);

	printf("Before WaitForSingleObject() function.\n");
	WaitForSingleObject(pThreadHandle , INFINITE);
	printf("After WaitForSingleObject() function. \n");
	*/


	// 方法二:循環等待
	while(1)
	{
		int exitCode = getExitCode();
		if(exitCode){
			printf("The thread has exited.\n");
			break;
		}
	}

	system("pause");

	return 0;
}
           

五、程式運作截圖;

        運作程式,QQ輸入對話消息:

【GamingAnywhere源碼分析之知識補充二】Windows鈎子機制

       關閉視窗,主視窗截獲消息:

【GamingAnywhere源碼分析之知識補充二】Windows鈎子機制

       消息儲存的檔案,由于沒有對特殊按鍵做處理,是以會有其它按鍵的辨別,但這已經是捕獲了我們的輸入:

【GamingAnywhere源碼分析之知識補充二】Windows鈎子機制

寫在結束:

        要回去過年了,開發工作要暫停一段時間了,我也會給自己多充電,讓自己掌握的更多;還是那個原則,隻要給自己一點新的認識,新的思路,對我來說就是進步,加油。