逆向原理 | SetWindowsHookEx 原理探究与实验

逆向原理 | SetWindowsHookEx 原理探究与实验

SetWindowsHook其实是在windows逆向中非常重要的一个api与之对应的是UnhookWindowsHookEx,用于卸载钩子。
但是这个api的机制其实是比较复杂的,之前应该没有记录过,这次准备好好记录一下。
同时之后还准备写几篇关于windows上异常处理机制的文章记录一下。
从原理和实验两个部分出发。
author: Mz1


文章目录

原理部分:

  1. SetWindowsHookEx的基本信息和参数
  2. 调用SetWindowsHookEx的时候,操作系统在做什么

实验部分:

  1. hook自己进程中的线程
  2. hook别的进程中的线程
  3. 全局hook的使用

SetWindowsHookEx的基本信息和参数

在msdn上的描述还是比较清晰的:

HHOOK SetWindowsHookExA(
  [in] int       idHook,
  [in] HOOKPROC  lpfn,
  [in] HINSTANCE hmod,
  [in] DWORD     dwThreadId
);

参数1:idHook

idHook表示安装hook的种类,一般来说是宏定义,包括但不限于下方的宏定义,都在msdn可以查到:
image

比较常用的如下:
image

参数2:lpfn

lpfn指向hook使用的过程处理函数。在这个函数的结尾应该调用CallNextHookEx来传递消息,不然进程大概率挂掉。

注意!
如果dwThreadId这个参数(也就是你要hook的线程)是0(0表示全局hook),或者这个线程不属于当前进程,这个lpfn必须指向一个位于dll中的过程处理函数!
否则该hook只能应用于当前进程下的线程。

这是什么原因呢?其实跟操作系统做的事情有关,我们放在下面慢慢说。

参数3:hmod

上面不是说了,在hook其他进程的线程的时候,我们要把过程处理函数放在dll中吗?
这个hmod就是指向了那个dll。
如果是hook当前进程下的线程且过程处理函数位于当前进程中,则直接NULL就行了。

参数4:dwThreadId (最关键)

这应该是这个api中最重要的一个参数,决定了函数执行以后不同的行为。
dwThreadId也就是想要hook的线程id。
如果这个参数为0,就是我们常说的全局hook,将hook应用于当前桌面的所有应用程序。

返回值

返回这个hook的句柄。
如果失败,返回NULL,可以调用GetLastError查看原因。

说到这里,基本的信息就已经解释完毕了,下面是原理和操作系统的行为。

调用SetWindowsHookEx的时候,操作系统在做什么

在msdn的Remark里面有这样一段:

SetWindowsHookEx can be used to inject a DLL into another process. A 32-bit DLL cannot be injected into a 64-bit process, and a 64-bit DLL cannot be injected into a 32-bit process. If an application requires the use of hooks in other processes, it is required that a 32-bit application call SetWindowsHookEx to inject a 32-bit DLL into 32-bit processes, and a 64-bit application call SetWindowsHookEx to inject a 64-bit DLL into 64-bit processes. The 32-bit and 64-bit DLLs must have different names.

上面这段就是说,安装hook的程序和dll,要和目标程序的位数对应上。

Because hooks run in the context of an application, they must match the "bitness" of the application. If a 32-bit application installs a global hook on 64-bit Windows, the 32-bit hook is injected into each 32-bit process (the usual security boundaries apply). In a 64-bit process, the threads are still marked as "hooked." However, because a 32-bit application must run the hook code, the system executes the hook in the hooking app's context; specifically, on the thread that called SetWindowsHookEx. This means that the hooking application must continue to pump messages or it might block the normal functioning of the 64-bit processes.
If a 64-bit application installs a global hook on 64-bit Windows, the 64-bit hook is injected into each 64-bit process, while all 32-bit processes use a callback to the hooking application.

上面这段就是说,你用32位的代码在64位的操作系统上装全局hook的时候,所有32位的应用程序都会被注入这个dll。
但是,上面也说了,注入程序、注入的dll和被注入的程序位数要对应,因此,对于上面32位代码在64位系统上安装全局hook的时候,操作系统仍然会把64位的程序标记为已经被hook,但是!hook的过程处理函数的代码,会在注入程序(32位)的上下文中执行。
64位的全局hook类似。
补充一篇文章:https://blog.csdn.net/xbgprogrammer/article/details/53240535
在这篇文章中的解释比较清楚:
32位dll是不能注入到64位进程中,同理64位dll不能注入到32位进程中。如果32位进程调用SetWindowsHookEx 注入32位dll,其只能注入到32位进程中,虽然不能注入到64位进程,但是64位进程的线程依然被标注为hooked。当64位进程产生需要被hook处理的事件时,系统会在调用SetWindowsHookEx函数的进程(严格的说是线程)中执行hook例程。这要求调用SetWindowsHookEx的线程拥有一个消息泵,否则会阻止64位进程的执行。
据我的推测,要求调用SetWindowsHookEx的线程拥有一个消息泵,是因为64位进程通过windows消息向调用SetWindowsHookEx的线程发送windows消息,通知钩子事件发生。如果这个线程没有处理消息,通信阻塞,64位进程挂起。如果此时安装hook的进程结束掉,64位进程继续执行。

光看肯定是一头雾水的,下面是实验部分。

hook自己进程中的线程

这里我使用的是32位的vc6进行试验
创建一个mfc程序,比较方便。

随便拖一个界面出来:
image

左边的edit用来显示信息(输出)
右边的edit用来打字测试。

这里以键盘消息hook为例子。
先写好代码的框架:

// 全局hook句柄
HHOOK g_hHook = NULL;


// 回调函数先什么都不做
LRESULT CALLBACK lpfn(int code, WPARAM wParam, LPARAM lParam){
	return CallNextHookEx(g_hHook, code, wParam, lParam);
}


// 安装hook
void CHookmyselfDlg::OnButton1() 
{	
	CString str;
	GetDlgItemText(IDC_EDIT1, str);
	SetDlgItemText(IDC_EDIT1, str + "安装hook \r\n");
	
	// 获取当前进程的消息处理线程
	DWORD dwThreadId = GetCurrentThreadId();
	
	// 安装hook
	HHOOK hHook;
	hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)lpfn, NULL, dwThreadId);
	if (hHook != NULL){
		// 安装成功
		g_hHook = hHook;
		GetDlgItemText(IDC_EDIT1, str);
		SetDlgItemText(IDC_EDIT1, str + "安装成功 \r\n");
	}else{
		::MessageBoxA(0,"安装失败", 0,0);
	}
}

// 卸载hook
void CHookmyselfDlg::OnButton2() 
{
	CString str;
	GetDlgItemText(IDC_EDIT1, str);
	SetDlgItemText(IDC_EDIT1, str + "卸载hook \r\n");
	if (UnhookWindowsHookEx(g_hHook) != 0){
		// 成功
		GetDlgItemText(IDC_EDIT1, str);
		SetDlgItemText(IDC_EDIT1, str + "卸载成功 \r\n");
	}else{
		::MessageBoxA(0,"卸载失败", 0,0);
	}
}

image

测试完毕以后可以成功安装hook,修改回调函数的内容,输出按键消息:

// 回调函数
LRESULT CALLBACK lpfn(int code, WPARAM wParam, LPARAM lParam){
	char old[10000] = {0};
	char buff[10000] = {0};
	sprintf(buff, "按下了%x \r\n",wParam);
	GetDlgItemText(g_hWnd, IDC_EDIT1, old, 9000);
	SetDlgItemText(g_hWnd, IDC_EDIT1, strcat(old, buff));
	return CallNextHookEx(g_hHook, code, wParam, lParam);
}

image

至此,我们对自身进程的hook就完成了。

hook别的进程中的线程

我们分别写一个被hook的程序,和一个dll。

被hook的进程,同样用mfc创建,啥都不用做,拖个输入框出,显示一下自己的pid和线程id就行:
image

现在我们需要把hook的回调函数放在dll中了,对dll进行编写:
注意,dll中sethook和unsethook是安装hook的程序使用的,用来安装和卸载hook。

// dll.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"
#include <stdio.h>


HHOOK g_hHook = NULL;
HANDLE g_hModule = NULL;
FILE* g_fp = NULL;


// 写文件函数
void output(char* s){
	fputs(s, g_fp);
	fflush(g_fp);
}

// 回调函数
LRESULT CALLBACK lpfn(int code, WPARAM wParam, LPARAM lParam){
	char buf[255] = {0};
	sprintf(buf, "按下%x \n", wParam);
	output(buf);

	return CallNextHookEx(g_hHook, code, wParam, lParam);
}

// 安装hook的函数
extern "C"
__declspec(dllexport) VOID SetHook(DWORD dwThreadId){
	g_hHook = SetWindowsHookEx(WH_KEYBOARD, lpfn, (HMODULE)g_hModule, dwThreadId);
	if (g_hHook != NULL){
		output("安装hook成功\n");
	}else{
		output("安装失败\n");
	}
}

// 卸载hook的函数
extern "C"
__declspec(dllexport) VOID UnSetHook(){
	if (UnhookWindowsHookEx(g_hHook)!=0){
		output("卸载成功!\n");
	}else{
		output("卸载失败!\n");
	}
}

// 测试用函数
extern "C"
__declspec(dllexport) VOID test(){
	output("test ok \n");
}

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		// 初始化
		g_fp = fopen("C:\\Users\\thinkpad\\Desktop\\1.txt", "a");
		g_hModule = hModule;
		break;
	case DLL_PROCESS_DETACH:
		break;
	case DLL_THREAD_ATTACH:
		break;
	case DLL_THREAD_DETACH:
		break;
	}
    return TRUE;
}

然后编写注入的程序,这里就不用mfc了,直接用控制台方便一点啊:

#include <windows.h>
#include <stdio.h>

int main(){
	int tid;
	void (*pfunctest)();
	void (*pfunc_sethook)(DWORD);
	void (*pfunc_unsethook)();
	// 输入要hook的线程id
	printf("tid: ");
	scanf("%d", &tid);
	
	// 获取一下函数地址什么的
	HMODULE hModule = LoadLibrary("dll.dll");
	pfunctest = (void (__cdecl *)())GetProcAddress(hModule, "test");
	pfunctest();
	pfunc_sethook = (void (__cdecl *)(unsigned long))GetProcAddress(hModule, "SetHook");
	pfunc_unsethook = (void (__cdecl *)(void))GetProcAddress(hModule, "UnSetHook");

	// 启动hook
	pfunc_sethook(tid);
	
	system("pause");

	// 卸载hook
	pfunc_unsethook();
	return 0;
}

至此,完成对指定进程中的线程进行hook:
image

全局hook的使用

最后,我们要尝试的,就是全局hook,只要在上面的基础上将dwThreadId的值设置为0就可以了。
但是!
特别重要的一点,因为64位的程序也会被挂上钩子,要使用消息代理处理(见上面原理部分)
所以将上面的启动hook的程序改成mfc程序,为了兼容之前的dll,直接把调用SetHook时候的dwThreadId改成0就可以了。

// author:Mz1
int tid = 0;   // 全局hook
void (*pfunctest)();
void (*pfunc_sethook)(DWORD);
void (*pfunc_unsethook)();

// 初始化导入函数等信息
void CSetglobalhookDlg::OnButton3() 
{
	HMODULE hModule = LoadLibrary("dll.dll");
	pfunctest = (void (__cdecl *)())GetProcAddress(hModule, "test");
	pfunctest();
	pfunc_sethook = (void (__cdecl *)(unsigned long))GetProcAddress(hModule, "SetHook");
	pfunc_unsethook = (void (__cdecl *)(void))GetProcAddress(hModule, "UnSetHook");	
}
// 安装hook
void CSetglobalhookDlg::OnButton1() 
{
	pfunc_sethook(tid);
}
// 卸载hook
void CSetglobalhookDlg::OnButton2() 
{
	// TODO: Add your control notification handler code here
	pfunc_unsethook();
	
}

做完以后发现自己没加输出,憨憨了,不过不影响。
image

至此,SetWindowsHookEx原理到实践就基本ok啦!!!!!
终于把这个整理清晰了23333

posted @ 2023-01-17 19:51  Mz1  阅读(1021)  评论(1编辑  收藏  举报