逆向原理 | SetWindowsHookEx 原理探究与实验
SetWindowsHook其实是在windows逆向中非常重要的一个api与之对应的是UnhookWindowsHookEx,用于卸载钩子。
但是这个api的机制其实是比较复杂的,之前应该没有记录过,这次准备好好记录一下。
同时之后还准备写几篇关于windows上异常处理机制的文章记录一下。
从原理和实验两个部分出发。
author: Mz1
文章目录
原理部分:
- SetWindowsHookEx的基本信息和参数
- 调用SetWindowsHookEx的时候,操作系统在做什么
实验部分:
- hook自己进程中的线程
- hook别的进程中的线程
- 全局hook的使用
SetWindowsHookEx的基本信息和参数
在msdn上的描述还是比较清晰的:
HHOOK SetWindowsHookExA(
[in] int idHook,
[in] HOOKPROC lpfn,
[in] HINSTANCE hmod,
[in] DWORD dwThreadId
);
参数1:idHook
idHook表示安装hook的种类,一般来说是宏定义,包括但不限于下方的宏定义,都在msdn可以查到:
比较常用的如下:
参数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程序,比较方便。
随便拖一个界面出来:
左边的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);
}
}
测试完毕以后可以成功安装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);
}
至此,我们对自身进程的hook就完成了。
hook别的进程中的线程
我们分别写一个被hook的程序,和一个dll。
被hook的进程,同样用mfc创建,啥都不用做,拖个输入框出,显示一下自己的pid和线程id就行:
现在我们需要把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:
全局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();
}
做完以后发现自己没加输出,憨憨了,不过不影响。
至此,SetWindowsHookEx原理到实践就基本ok啦!!!!!
终于把这个整理清晰了23333