Windows钩子的使用

我们知道Windows中的窗口程序是基于消息,由事件驱动的,在某些情况下可能需要捕获或者修改消息,从而完成一些特殊的功能(MFC框架就利用Windows钩子对消息进行引导)。对于捕获消息而言,无法使用IAT或Inline Hook之类的方式去进行捕获,这就要用到接下来要介绍的Windows提供的专门用于处理消息的钩子函数。

1. 挂钩原理

Windows下的应用程序大部分都是基于消息机制的,它们都会有一个消息过程函数,根据不同的消息完成不同的功能。Windows操作系统提供的钩子机制的作用就是用来截获和监视这些系统中的消息。Windows钩子琳琅满目,可以用来应对各种不同的消息。

按照钩子作用的范围不同,又可以分为局部钩子和全局钩子。局部钩子是针对某个线程的;而全局钩子则是作用于整个系统中基于消息的应用。全局钩子需要使用DLL文件,在DLL中实现相应的钩子函数。在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会被操作系统自动或强行地加载到该进程中。因此,设置消息钩子,也可以达到DLL注入的目的。

2. 钩子函数

HHOOK SetWindowsHookEx(      

    int idHook,
    HOOKPROC lpfn,
    HINSTANCE hMod,
    DWORD dwThreadId
);

该函数的返回值是一个钩子句柄。参数介绍如下:

lpfn:指定Hook函数的地址。如果dwThreadId参数被赋值为0,或者被设置为一个其他进程中的线程ID,那么lpfn属于DLL中的函数过程。如果dwThreadId为当前进程中的一个线程ID,那么lpfn可以使指向当前进程模块中的函数,当然,也可以使DLL模块中的函数。

hMod:该参数指定钩子函数所在模块的模块句柄。即lpfn所在的模块句柄。如果dwThreadId为当前进程中的线程ID,且lpfn所指函数在当前进程中,则该参数被设置为NULL。

dwThreadId:指定需要被挂钩的线程ID号。如果设置为0,表示在所有基于消息的线程中挂钩;如果设置了具体的线程ID,表示在指定线程中挂钩。该参数影响上面两个参数的取值,同时也决定了该钩子是全局钩子还是局部钩子。

idHook:该参数表示钩子的类型。常用的几种如下:

※  WH_GETMESSAGE

按照该钩子的作用是监视被投递到消息队列中的消息。也就是当调用GetMessage或PeekMessage函数时,函数从程序的消息队列中获取一个消息后调用该钩子。

WH_GETMESSAGEG的钩子函数如下:

LRESULT CALLBACK GetMsgProc(      

    int code,		//hook code
    WPARAM wParam,		//removal option
    LPARAM lParam        //message
);

※  WH_MOUSE

该钩子用于监视鼠标消息。钩子函数如下:

LRESULT CALLBACK MouseProc(      

    int nCode,		//hook code
    WPARAM wParam,		//message identifier
    LPARAM lParam		//mouse coordinates
);

※  WH_KEYBOARD

该钩子用于监视键盘消息。钩子函数如下:

LRESULT CALLBACK KeyboardProc(      

    int code,		//hook code
    WPARAM wParam,		//virtual-key code
    LPARAM lParam		//keystroke-message information
);

※  WH_DEBUG

用于调试其它钩子。钩子函数如下:

LRESULT CALLBACK DebugProc(      

    int nCode,		//hook code
    WPARAM wParam,		//hook type
    LPARAM lParam		//debugging information
);

对于以上钩子函数的详情还请各位看客老爷们自行挪步到MSDN了。

移除先前用SetWindowsHookEx安装的钩子:

BOOL UnhookWindowsHookEx(      

    HHOOK hhk
);

唯一的参数是待移除的钩子句柄。

在实际应用中,可以多次调用SetWindowsHookEx函数来安装钩子,而且可以安装多个同样类型的钩子。这样,钩子就会形成一条钩子链,最后安装的钩子会首先截获到消息。当该钩子对消息处理完毕后,可以选择返回或者把消息继续传递下去。如果是为了屏蔽某消息,可以在安装的钩子函数中直接返回非零值。如果希望我们的钩子函数处理完消息后可以继续传递给目标窗口,则必须选择将消息继续传递。继续传递消息的函数定义如下:

LRESULT CallNextHookEx(      

    HHOOK hhk,		//handle to current hook
    int nCode,		//hook code passed to hook procedure
    WPARAM wParam,		//value passed to hook procedure
    LPARAM lParam		//value passed to hook procedure
);

第一个参数是钩子句柄,就是调用SetWindowsHookEx函数的返回值;后面3个参数是钩子的参数,直接一次copy即可。例如:

HHOOK g_Hook = SetWindowsHookEx(…);
LRESULT CALLBACK GetMsgProc(      

    int code,		//hook code
    WPARAM wParam,		//removal option
    LPARAM lParam        //message
)
{
	return CallNextHookEx(g_Hook, code, wParam, lParam);
}


3. 钩子实例

Windows钩子的使用场景比较广泛,我们就几种比较常见的情况做一个应用示例。

3.1全局键盘钩子

先新建一个DLL程序(这个不会可以看我以前的博客,这里就不重复了),我们在头文件中增加两个导出函数和两个全局。

#define MY_API __declspec(dllexport)
extern "C" MY_API VOID SetHookOn();
extern "C" MY_API VOID SetHookOff();
HHOOK g_Hook = NULL;		//钩子句柄
HINSTANCE g_Inst = NULL;	//DLL模块句柄

在DllMain中保存该DLL模块的句柄,以方便安装全局钩子。

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{

	//保存DLL模块句柄
	g_Inst = (HINSTANCE)hModule;
      return TRUE;
}

安装与卸载钩子的函数如下:

VOID SetHookOn()
{
	//安装钩子
	g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);
}

VOID SetHookOff()
{
	//卸载钩子
	UnhookWindowsHookEx(g_Hook);
}

钩子函数的实现如下:

//钩子函数
LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
	if(code < 0)
	{
		//如果code小于0,必须调用CallNextHookEx传递消息,不处理该消息,并返回CallNextHookEx的返回值。
		return CallNextHookEx(g_Hook, code, wParam, lParam);
	}

	if(code == HC_ACTION && lParam > 0)
	{
		//code等于HC_ACTION,表示消息中包含按键消息
		//如果为WM_KEYDOWN,则显示按键对应的文本
		char szBuf[MAXBYTE] = {0};
		GetKeyNameText(lParam, szBuf, MAXBYTE);
		MessageBox(NULL, szBuf, "提示", MB_OK);
	}

	return CallNextHookEx(g_Hook, code, wParam, lParam);

}

编译链接后产生我们需要的.dll和.lib文件,然后新建一个项目来导入动态库内容调用相关函数。

新建项目如下:



首先导入库:

#pragma comment (lib, "全局钩子.lib")

声明将要调用的函数(不声明链接时将报错):

extern "C" VOID SetHookOn();
extern "C" VOID SetHookOff();

在按钮事件中调用导出函数:

void CHookDebugDlg::OnHookon() 
{
	SetHookOn();
}

void CHookDebugDlg::OnHookoff() 
{
	SetHookOff();
}

执行结果如下:


3.2低级键盘钩子

数据防泄漏软件通常会精致PrintScreen键,防止通过截屏将数据保存为图片而导致数据泄密。下面我们也可以模仿一下,简单的实现该功能。这里需要注意的是,普通的键盘钩子(WH_KEYBOARD)是无法过滤一些系统按键的,得通过安装低级键盘钩子(WH_KEYBOARD_LL)来达到目的。

在低级键盘钩子的回调函数中,判断是否为PrintScreen键,如果是,则直接返回TRUE;如果不是,则传递给下一个钩子处理。

具体DLL中的实现代码如下:

BOOL SetHookOn()
{
	if(g_Hook != NULL)
	{
		return FALSE;
	}
	//安装钩子
	g_Hook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_Inst, 0);
	if(NULL == g_Hook)
	{
		MessageBox(NULL, "安装钩子出错!", "ERROR", MB_ICONSTOP);
		return FALSE;
	}

	return TRUE;
}

BOOL SetHookOff()
{
	if(g_Hook == NULL)
	{
		return FALSE;
	}
	//卸载钩子
	UnhookWindowsHookEx(g_Hook);
	g_Hook = NULL;
	return TRUE;
}

//钩子函数
LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
	KBDLLHOOKSTRUCT *Key_Info = (KBDLLHOOKSTRUCT *)lParam;

	if(HC_ACTION == code)
	{
		if(WM_KEYDOWN == wParam || WM_SYSKEYDOWN == wParam)
		{
			if(Key_Info->vkCode == VK_SNAPSHOT)
			{
				MessageBox(NULL, "该键已禁用!", "ERROR", MB_ICONSTOP);
				return TRUE;
			}
		}
	}

	return CallNextHookEx(g_Hook, code, wParam, lParam);

}

依然利用前面的小程序,执行后按下PrintScreen键,效果如下:




可能在编译时会报错,说WH_KEYBOARD_LL和KBDLLHOOKSTRUCT未定义,此时可以在文件开头加上如下代码:

#define WH_KEYBOARD_LL 13

typedef struct tagKBDLLHOOKSTRUCT {

DWORD vkCode;

DWORD scanCode;

DWORD flags;

DWORD time;

DWORD dwExtraInfo;

} KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;

其实在winuser.h中已有定义,但是可能是兼容的缘故用不了。

3.3钩子注入DLL

利用WH_GETMESSAGE钩子,可以方便地将DLL文件注入到所有基于消息机制的程序中。因为有时候可能需要DLL文件完成一些工作,但是工作时需要DLL在目标进程的空间中。这个时候,就可以将DLL注入目标进程来完成相关的功能。

主要的代码如下:

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
	//保存DLL模块句柄
	g_Inst = (HINSTANCE)hModule;

    switch (ul_reason_for_call)
	{
		case DLL_PROCESS_ATTACH:
			{
				DoSomething();
				break;
			}
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
    }
    return TRUE;
}
VOID SetHookOn()
{
	g_Hook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_Inst, 0);
}

VOID SetHookOff()
{
	UnhookWindowsHookEx(g_Hook);
}

LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
	return CallNextHookEx(g_Hook, code, wParam, lParam);
}

VOID DoSomething()
{
	MessageBox(NULL, "Hello,我被执行了!", "提示", MB_OK);
}

执行效果图:


 需要注意的是,此处执行的DoSomething并不是导出函数哦。




二、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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值