windows钩子

本文详细介绍了Windows钩子技术的基本概念、安装与卸载过程,包括进程内钩子和全局钩子的应用。并通过实例展示了如何使用钩子拦截特定类型的输入事件。

钩子的概念:

钩子:是windows消息处理机制中的一个监视点,应用程序可以在这里安装一个子程序(钩子函数)以监视指定窗口某种类型的消息。

钩子函数:是一个处理消息的程序段,通过调用相关的API函数(SetWindowsHookEx()),把他挂入系统,每当特定的消息发出,当没有到达目的窗口前,钩子程序就先捕获该消息,这时,钩子函数既可以加工处理该消息,也可以不作处理而继续传递该消息。

钩子的安装与卸载:

SetWindowsHookEx函数把应用程序定义的钩子函数安装到系统中。该函数原型如下:

WINUSERAPI
HHOOK
WINAPI
SetWindowsHookExW(
    __in int idHook,
    __in HOOKPROC lpfn,
    __in_opt HINSTANCE hmod,
    __in DWORD dwThreadId);

idHook:指定了要安装的钩子类型;

dwThreadId:指定要与钩子函数关联的线程ID号,如果设为0,那么该钩子就是系统范围内的,即钩子函数将关联到系统内的所有线程。

lpfn:指定相应的钩子过程,也就是钩子函数的地址,如果dwThreadId参数为0,或者指定一个由其它进程创建的线程ID,那么参数lpfn指向的钩子函数必须位于一个DLL中。这是因为进程的地址空间是相互隔离的,发生事件的进程不能调用其它进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,在相关事件发生时,系统会把这个DLL插入到发生事件的进程的地址空间,使它能够调用钩子函数。这种需要把钩子函数写入DLL以便挂钩其它进程事件的钩子称为远程钩子

hmod:指定钩子函数所在DLL的句柄。如果不是远程钩子,那么该参数就设为NULL。

注意:多个钩子过程形成钩子链,最后安装的钩子过程总是排列在该链的前面。钩子会是系统变慢,因为增加了系统对每个消息的处理,应该在必要时才安装钩子,而且在不需要时应尽快移除。

函数UnhookWindowsHookEx(HHOOK hhk); 卸载钩子 // hhk为要卸载的钩子句柄

CallNextHookEx(): 把钩子信息传递给钩子链中写一个等待接收信息的钩子过程。

 

 

进程内钩子:

安装进程内钩子时,不需要用到DLL。

首先要声明钩子函数,下面分别声明了鼠标钩子函数和键盘钩子函数:

LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);

以及它们的实现:

//鼠标钩子函数
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return 1;
}

//键盘钩子函数
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	//if(VK_SPACE == wParam)
	if(VK_F4 == wParam &&(1 == (lParam>>29 & 1)))
		return 1;
	else
		return ::CallNextHookEx(g_hKeyboard, nCode, wParam, lParam);
}

需要注意的地方:如果钩子函数返回非0值,表示已经对当前消息进行了处理,这样系统就不会再将这个消息传递给目标窗口过程了。

为了保存SetWindowsHookEx函数返回的钩子过程句柄,我们要定义连个全局变量来保存鼠标和键盘的钩子句柄:

HHOOK g_hMouse	 = NULL;
HHOOK g_hKeyboard = NULL;

 

接下来就是安装钩子了,下面我们分别安装了鼠标和键盘钩子:

//安装鼠标钩子
	g_hMouse =
		::SetWindowsHookExA(WH_MOUSE, MouseProc, NULL, ::GetCurrentThreadId());
	//安装键盘钩子
	g_hKeyboard = 
		::SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, NULL, ::GetCurrentThreadId());


其中GetCurrentThreadId()函数获取当前线程的ID。

关于键盘消息的处理

参数wParam是产生当前按键消息的键盘按键的虚拟键代码,这是Windows定义的,与设备无关。当按下键盘上的按键时,它实际上发送的是一个脉冲信号。Windows定义了一些虚拟键代码来表示这些信号,并由键盘设备驱动程序负责解释。 键盘虚拟键的宏都是以" VK_"开头的。

怎么判断组合键???  例如 Alt+F4

在键盘钩子函数中,还有另外一个参数lParam,它是一个32为的整数,它的每一位或某些位表示特定的含义,用来指定按键重复的次数、扫描码、扩展键标记、上下文代码等标记。

判断组合键Alt+F4:VK_F4 == wParam && (1 == (lParam>>29 & 1))

如何让程序在按下某个特定的按键时退出???  例如  按下F2退出程序

为了让程序退出,可以利用函数SendMessage()向主程序发送WM_CLOSE消息。

定义一个全局变量 g_hWnd 来保存窗口句柄:

HWND g_hWnd = NULL;

保存窗口句柄:

g_hWnd = m_hWnd;

发送WM_CLOSE消息:

if(VK_F2 == wParam){
		::SendMessageA(g_hWnd, WM_CLOSE, 0, 0);
		::UnhookWindowsHookEx(g_hKeyboard);
		::UnhookWindowsHookEx(g_hMouse);
	}
	return 1;



全局钩子:
 

如果想要屏蔽当前正在运行的所有进程的鼠标消息和键盘消息,那么安装钩子过程的代码必须放到动态链接库中去实现。如果想要让安装的钩子过程与所有进程相关,应该将SetWindowsHookEx函数的第四个参数设置为0,并将它的第三个参数指定为安装钩子过程的代码所在的DLL的句柄。

首先,为了安装全局钩子,我们必须先生成一个.dll文件,于是,创建一个DLL工程:Hook, 用来生成Hook.dll

在Hook工程中的dllmain.cpp文件中完成钩子鼠标函数的定义和安装鼠标钩子:

//鼠标钩子函数
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return 1;
}

//安装鼠标钩子过程的函数
void SetHook()
{
	g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
}


为了安装钩子过程,需要指定安装钩子过程所在DLL模块的句柄(SetWindowsHookEx的第三个参数),我们有两种方法来获得。

第一种:为DLL程序提供DllMain函数,当第一次加载DLL时,系统会调用这个函数,并传递当前DLL模块的句柄。因此,我们可以定义一个全局的实例变量g_hInst来保存系统传递来的DLL模块句柄,之后就可以在调用SetHook函数中使用这个句柄了。

第二种:调用GetModuleHandle函数来得到指定的DLL模块的句柄。GetModuleHandle函数返回值是HMODULE类型,HMODULE和HINSTANCE类型可以通用。

	//第一种方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二种方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);


在Hook工程中要将SetHook函数声明为导出函数,并且要解决掉名字改变问题,所以我的SetHook函数如下:

extern "C" _declspec(dllexport) void SetHook()
{
	//第一种方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二种方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
}


整个dllmain.cpp文件如下:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include <Windows.h>

HHOOK g_hMouse = NULL;
HINSTANCE g_hInst;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	g_hInst = hModule;
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

//鼠标钩子函数
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return 1;
}

//安装鼠标钩子过程的函数
extern "C" _declspec(dllexport) void SetHook()
{
	//第一种方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二种方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
}

最后,我们就可以生成Hook.dll文件了。

 

接下来就是测试这个DLL文件,我们新建一个基于对话框的MFC工程。

1.在工程中要调用Hook.dll文件中的SetHook函数,所以应在调用前进行声明,并引入Hook.lib这个导入库文件:

#pragma comment (lib, "F:\\windows\\000MFC\\HookTest\\Debug\\Hook.lib")
extern "C" _declspec(dllimport) void SetHook();


2.在需要的地方就可以直接调用SetHook函数了。例如在OnInitDialog函数中直接调用SetHook函数。

 

接下来,做一件有趣的事情,我们屏蔽掉所有键盘消息,但是除下F2键(我们要为程序开个后门,让程序退出),当按下F2键是程序退出,按下其他键不做任何处理。怎么实现呢???

为了退出程序,应该向调用进程的主窗口发送一个WM_CLOSE消息。但是在动态链接库中如何才能得到调用进程的主窗口句柄呢

很好解决,我们在调用DLL中的导出函数时,可以将主窗口的句柄作为参数传递给导出函数,我们在DLL中用一个全局变量将主窗口的句柄保存起来。这样问题就轻松解决了!

DLL工程中:

HWND g_hWnd;  //用来保存主窗口的句柄

SetHook函数接收主窗口句柄作为参数:

void SetHook(HWND hwnd)
{
	g_hWnd = hwnd;
	//第一种方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二种方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
	g_hKeyboard = ::SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, g_hInst, 0);
}

HookTest工程中:

SetHook(m_hWnd);   将主窗口句柄传递给DLL中的函数SetHook。

注意:上面的例子中,当切换到其他进程的情况下,按下F2键并不能终止HookTest程序,这是为什么呢?

我们知道,DLL可以被多个进程供享DLL的代码和数据,但是如果多个进程共享同一份可写入的数据的话,你可以想象一下会有什么后果。为了解决这个问题,windows中采用了写入时复制机制。当DLL有一个数据被两个进程共享,如果第二个进程想修改DLL数据页面上的数据,操作系统会分配一个新的页面,并将数据页面上的数据复制一份到这个新的页面中。

 

那么,怎么才能使在切换到其他进程的情况下,仍然可以使HookTest程序退出

解决方法:为Hook.dll创建一个新的节,并将此节设置为一个共享的节,然后将全局变量:g_hWnd放到此节中,让该全局变量在多个进程间共享。

利用dumpbin -headers [.dll路径] 查看dll中各节的列表信息。

接下来我们为Hook.dll创建一个新的节:MySec,并将全局变量g_hWnd放到此节中。创建节可以用指令#pragma data_seg来实现。

#pragma data_seg("MySec")
HWND g_hWnd = NULL;
#pragma data_seg()
#pragma comment(linker, "/section:MySec,RWS")


注意:g_hwnd一定要初始化,否则不会放到新的节中。新建的节的权限为read 和 write,我们要将它设置为共享的节,可以使用#pragma comment(linker, "/section:MySec,RWS")。 这里将指令类型指定为linker,表明该行代码用来指定连接选项。而字符串"/section:MySec,RWS"的含义是表明将MySec这个节设置为读(R)写(W)共享(S)类型。

为了将节设置为共享类型,我们还可以使用def文件:在DEF文件中利用SEGMENTS关键字来实现。

例如:

SEGMENTS

MySec READ WRITE SHARED

这里不区分大小写,但不能是缩写。

二、API Hook的原理 这里的API既包括传统的Win32 APIs,也包括任何Module输出的函数调用。熟悉PE文件格 式的朋友都知道,PE文件将对外部Module输出函数的调用信息保存在输入表中,即.idata段。 下面首先介绍本段的结构。 输入表首先以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式链接 进来的DLL都有一个IID.在这个数组中的最后一个单元是NULL,可以由此计算出该数组的项数。 例如,某个PE文件从两个DLL中引入函数,就存在两个IID结构来描述这些DLL文件,并在两个 IID结构的最后由一个内容全为0的IID结构作为结束。几个结构定义如下: IMAGE_IMPORT_DESCRIPTOR struct union{ DWORD Characteristics; ;00h DWORD OriginalFirstThunk; }; TimeDateStamp DWORD ;04h ForwarderChain DWORD ;08h Name DWORD ;0Ch FirstThunk DWORD ;10h IMAGE_IMPROT_DESCRIPTOR ends typedef struct _IMAGE_THUNK_DATA{ union{ PBYTE ForwarderString; PDWORD Functions; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; }u1; } IMAGE_IMPORT_BY_NAME结构保存一个输入函数的相关信息: IMAGE_IMPORT_BY_NAME struct Hint WORD ? ;本函数在其所驻留DLL的输出表中的序号 Name BYTE ? ;输入函数的函数名,以NULL结尾的ASCII字符串 IMAGE_IMPORT_BY_NAME ends OriginalFirstThunk(Characteristics):这是一个IMAGE_THUNK_DATA数组的RVA(相对于PE文件 起始处)。其中每个指针都指向IMAGE_IMPORT_BY_NAME结构。 TimeDateStamp:一个32位的时间标志,可以忽略。 ForwarderChain:正向链接索引,一般为0。当程序引用一个DLL中的API,而这个API又引用别的 DLL的API时使用。 NameLL名字的指针。是个以00结尾的ASCII字符的RVA地址,如"KERNEL32.DLL"。 FirstThunk:通常也是一个IMAGE_THUNK_DATA数组的RVA。如果不是一个指针,它就是该功能在 DLL中的序号。 OriginalFirstThunk与FirstThunk指向两个本质相同的数组IMAGE_THUNK_DATA,但名称不同, 分别是输入名称表(Import Name Table,INT)和输入地址表(Import Address Table,IAT)。 IMAGE_THUNK_DATA结构是个双字,在不同时刻有不同的含义,当双字最高位为1时,表示函数以 序号输入,低位就是函数序号。当双字最高位为0时,表示函数以字符串类型的函数名 方式输入,这时它是指向IMAGE_IMPORT_BY_NAME结构的RVA。 三个结构关系如下图: IMAGE_IMPORT_DESCRIPTOR INT IMAGE_IMPORT_BY_NAME IAT -------------------- /-->---------------- ---------- ---------------- |01| 函数1 ||02| 函数2 || n| ... |"USER32.dll" | |--------------------| | | FirstThunk |---------------------------------------------------------------/ -------------------- 在PE文件中对DLL输出函数的调用,主要以这种形式出现: call dword ptr[xxxxxxxx] 或 jmp [xxxxxxxx] 其中地址xxxxxxxx就是IAT中一个IMAGE_THUNK_DATA结构的地址,[xxxxxxxx]取值为IMAGE_THUNK_DATA 的值,即IMAGE_IMPORT_BY_NAME的地址。在操作系统加载PE文件的过程中,通过IID中的Name加载相应 的DLL,然后根据INT或IAT所指向的IMAGE_IMPORT_BY_NAME中的输入函数信息,在DLL中确定函数地址, 然后将函数地址写到IAT中,此时IAT将不再指向IMAGE_IMPORT_BY_NAME数组。这样[xxxxxxxx]取到的 就是真正的API地址。 从以上分析可以看出,要拦截API的调用,可以通过改写IAT来实现,将自己函数的地址写到IAT中, 达到拦截目的。 另外一种方法的原理更简单,也更直接。我们不是要拦截吗,先在内存中定位要拦截的API的地址, 然后改写代码的前几个字节为 jmp xxxxxxxx,其中xxxxxxxx为我们的API的地址。这样对欲拦截API的 调用实际上就跳转到了咱们的API调用去了,完成了拦截。不拦截时,再改写回来就是了。 这都是自己从网上辛辛苦苦找来的,真的很好啊
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值