DLL注入-Windows消息钩取

发布时间 2023-03-23 20:10:20作者: Zer0o

0x01 钩子

钩子,英文Hook,泛指钓取所需东西而使用的一切工具。后来延伸为“偷看或截取信息时所用的手段或工具”。

  • 挂钩:为了偷看或截取来往信息而在中间设置岗哨的行为
  • 钩取:偷看或操作信息的行为

0x02 消息钩子

敲击键盘时,消息会从OS(Operating System的简称)移动到应用程序,而消息钩子就是在这个过程中偷看信息。

下面以键盘信息为例

常规Windows消息流:
1、发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue];
2、OS判断哪个应用程序中发生了事件,然后从[OS message queue]中取出消息,添加到相应应用程序的[application message queue]中;
3、应用程序监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理。
附带钩子的信息流:

  1. 发生键盘输入事件,WM_KEYDOWN消息被添加到OS消息队列;
  2. OS判断哪个应用程序发生了事件,从OS消息队列中取出消息,发送给应用程序;
  3. 钩子程序截取信息,对消息采取一定的动作(因钩子目的而定);
  4. 如钩子程序不拦截消息,消息最终传输给应用程序,此时的消息可能经过了钩子程序的修改。

如下图,OS消息队列和应用程序消息队列之间存在一条钩链(Hook Chain),设置好键盘消息钩子后,处于钩链中的键盘消息钩子会比应用程序先一步看到相应信息。在键盘消息钩子函数的内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,阻止消息传递。可以同时设置多个相同的键盘消息钩子,按照设置的顺序依次调用,从而组成的链条称为钩链。
image.png

0x03 SetWindowsHookEx()

在Windows中可以使用SetWindowsHookEx()API来设置消息钩子,这个函数除了可以设置当前进程的钩子之外,它还可以设置全局钩子。全局钩子,顾名思义,即当前正在运行的进程都会被设置相应的钩子。

Windows API作用类似是一个个功能函数。

定义如下

HHOOK SetWindowsHookExA(
   int       idHook,      //hook type 钩子类型
   HOOKPROC  lpfn,        //hook procedure 回调函数地址
   HINSTANCE hMod,        //hook procedure所属的dll句柄
   DWORD     dwThreadId   //想要挂钩的线程PID
);

第一个参数表征钩子的类型,但钩子的类型是微软规定好的,你只能选一种,自己不能乱写
第二个参数是钩子执行程序,即当钩子勾到所需信息时运行的程序
第三个参数是要注入的dll句柄
第四个参数是想要挂载的线程ID,如果该参数为0,则表明钩子是一个全局钩子

HHOOK:返回值,钩子句柄,需要保留,等不使用钩子时通过UnhookWindowsHookEx函数卸载钩子。
idHook:钩子的拦截消息类型,选择钩子程序的拦截范围,具体值参考文章结尾的消息类型。
Lpfn:消息的回调函数地址,钩子子程的地址指针,一般是填函数名。
hMod:钩子函数所在的实例的句柄。对于线程钩子,该参数为NULL;对于系统钩子,该参数为钩子函数所在的DLL句柄。在dll中可通过AfxInitExtensionModule(MousehookDLL, hInstance)获得DLL句柄。
dwThreadId:钩子所监视的线程的线程号,可通过GetCurrentThreadId()获得线程号。对于全局钩子,该参数为NULL(或0)。

使用SetWindowsHookEx()设置好钩子后,在某个进程中生成指定消息时,OS会将相关的DLL文件强制注入相应的进程,然后调用注册的钩子过程。

0x04 键盘消息钩取练习

下面做一个简单练习,先看图
image.png
KeyHook.dll文件是一个含有钩子过程(KeyboardProc)的DLL文件,HookMain.exe是最先加载KeyHook.dll并安装键盘钩子的程序。HookMain.exe加载KeyHook.dll后使用SetWindowsHookEx()安装键盘钩子;若其他进程(如图中所示)发生键盘输入事件,OS就会强制将KeyHook.dll加载到像一个进程的内存,然后调用KeyboardProc()函数。
先分析源码

keyHook.cpp

//KeyHook.cpp
 
#include "stdio.h"
#include "windows.h"
 
//定义目标进程名为notepad.exe
#define DEF_PROCESS_NAME "notepad.exe"
 
//定义全局变量
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
 
//DllMain()函数在DLL被加载到进程后会自动执行
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved){
	switch( dwReason ){
		case DLL_PROCESS_ATTACH:
			g_hInstance = hinstDLL;
			break;
 
		case DLL_PROCESS_DETACH:
			break;
	}
 
	return TRUE;
}
 
//
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam){
	char szPath[MAX_PATH] = {0,};
	char *p = NULL;
	
	if( nCode >= 0 ){
 
		//释放键盘按键时,bit 31 : 0 => press, 1 => release
		if( !(lParam & 0x80000000) ){
			GetModuleFileNameA(NULL, szPath, MAX_PATH);
			p = strrchr(szPath, '\\');
 
			//比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序或下一个钩子函数
			//_stricmp()函数用于比较字符串,i表示不区分大小写,若两个值相等则返回0
			if( !_stricmp(p + 1, DEF_PROCESS_NAME) ){
				return 1;
			}
		}
	}
 
	//比较当前进程名称,若非notepad.exe,则消息传递给应用程序或下一个钩子函数
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
 
//在C++中调用C的库文件,用extern "C"告知编译器,因为C++支持函数重载而C不支持,两者的编译规则不同
#ifdef __cplusplus
extern "C"{
#endif
	//__declspec,针对编译器的关键字,用于指出导出函数
	//当调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链
	__declspec(dllexport) void HookStart(){
		g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
	}
 
	__declspec(dllexport) void HookStop(){
		if(g_hHook){
			UnhookWindowsHookEx(g_hHook);
			g_hHook = NULL;
		}
	}
#ifdef __cplusplus
}
#endif

因为要生成的是KeyHook.dll文件,因而在开始创建项目时应先选择Win 32控制台应用程序,再到应用程序类型中勾选DLL,编写好cpp文件后选择Release再生成文件即可得到DLL文件。
当调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链。安装好键盘钩子后,无论哪个进程,只要发生键盘输入事件,OS都会强制将KeyHook.dll注入相应的进程中。
KeyboardProc()函数中发生键盘输入事件时,会比较当前进程名称和“notepad.exe”是否一致,若一致则返回1,终止KeyboardProc()函数,即截获并删除消息,从而实现对notepad.exe程序的键盘输入事件进行钩取并截获删除、键盘消息不能传递到notepad.exe的消息队列中。

KeyboardProc()函数定义如下:

LRESULT CALLBACK KeyboardProc(
    int code,			//HC_ACTION(0), HC_NOREMOVE(3)
    WPARAM wParam,		//virtual-key code
    LPARAM lParam		//extra information
);

其中wParam指用户按下的键盘按键的虚拟键值。

HookMain.cpp

//HookMain
 
#include "stdio.h"
#include "windows.h"
//Console Input/Output,定义了通过控制台进行数据输入和数据输出的函数
//主要是一些用户通过按键盘产生的对应操作,比如getch()函数等等
#include "conio.h"
 
//定义一些常量
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
 
//定义两个参数为空、返回值为void即没有的函数指针
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
 
void main(){
	//定义及初始化句柄变量和函数指针
	HMODULE hDll = NULL;
	PFN_HOOKSTART HookStart = NULL;
	PFN_HOOKSTOP HookStop = NULL;
 
	//加载KeyHook.dll
	hDll = LoadLibraryA(DEF_DLL_NAME);
 
	//若加载不成功,则输出错误信息
	if( hDll == NULL ){
		printf("[-]无法加载%s [%d]\n", DEF_DLL_NAME, GetLastError());
		return;
	}
 
	//获取导出函数地址
	HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
	HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
 
	//开始钩取
	HookStart();
 
	//直至用户输入“q”退出钩取
	printf("[*]等待输入 'q' 来停止钩取...\n");
	while( _getch() != 'q' );
 
	//终止钩取
	HookStop();
 
	//卸载KeyHook.dll
	FreeLibrary(hDll);
}

先加载KeyHook.dll,再调用HookStart()函数开始钩取,当获取到用户输入“q”后调用HookStop()函数终止钩取。

代码流程分析概括

安装好键盘钩子后,无论在哪个进程中,只要发生了键盘输入事件,OS就会强制将KeyHook.dll注入到进程中,加载了KeyHook.dll的进程,发生键盘事件时会首先调用执行KeyHook.KetyboardProc()

KetyboardProc()函数中发生键盘输入事件时,会比较当前进程的名称与“notepad.exe”是否相同,相同返回1,终止KetyboardProc()函数,意味着截获并删除了消息,这样键盘消息就不会传递到notepad.exe程序的消息队列。

练习示例HookMain.exe

注:HookMain.exe 要和 KeyHook.dll放在一个目录下
运行HookMain.exe之后,再打开notepad.exe,输入键盘信息,发现打不上去,notepad.exe卡死。
我使用的系统是win10 64位系统,经过多方面搜索,终于确定问题,版本不匹配,dll因为CPU架构不匹配不能注入时,程序会通过SendMessage来调用你的KeyBoardProc,然后程序就卡住了,一直卡在对话框界面,卡死…
直接编译成64位的程序解决问题,同时,注入程序和dll都要是64位.
(参考知乎回答https://www.zhihu.com/question/64221483)

这里就不便示例了,就是大概这样
image.png

0x05 调试HookMain.exe

使用Ollydbg打开,可以看到是典型的VC++启动函数,图中显示的是HookMain.exe的EP(Entry Point,程序入口地址)代码
image.png

查找核心代码

我们关心的是核心的键盘钩取部分的代码,如何查找核心代码?

  1. 逐步跟踪(除非迫不得已!)
  2. 检索相关API (Ctrl+G)
  3. 检索相关字符串

我们已经知道程序的功能,会在控制台显示字符串“press ‘q’ to quit!”,所以先检查程序导入的字符串(Search for -All referencen text strings):
image.png
地址40104d处引用了要查找的字符串,双击跳转:
image.png
来到main函数处。

调试main函数

401000双击下断F9运行,到断点停下来后,开始F8/F7调试,了解main函数中主要的代码流。401006地址处调用LoadLibraryA(Keyhook.dll),然后由40104b地址处的CALL EBX指令调用KeyHook.HookStart()函数。在40104b处,F7单步跟进查看:
image.png
这里的代码是被加载到HookMain.exe进程中的KeyHook.dll的HookStart()函数,第一句就是调用SetWindowsHookExW()函数,在进行参数入栈操作后,我们可以在栈中看到函数的4个参数值。
再附上一张作者写的流程
image.png

参考博客
dll注入
看雪原创Windows消息钩取
CSDN—Windows消息钩取
关于逆向工程核心原理-Windows消息钩取
关于《逆向工程核心原理》键盘钩子的总结