HOOK API技术

HOOK API

HOOK API 是指截获特定进程或系统对某个API函数的调用,使得API的指定流程转向指定的代码。截获API使得用户有机会干预其他应用程序流程。

最常用的一种挂钩API的方法是改变目标进程中调用API函数的代码,使得它们对API的调用变为对用户自定义函数的调用。

实现原理:

1.在挂钩API之前,必须将一个可以替代API执行的函数的代码注入到目标进程中。一般称这个自定义函数为代理函数。之所以这样做,是因为Windows下应用程序有自己的地址空间,它们只能调用自己地址空间中的函。

2.注入代码到目标进程中是实现拦截API很重要的一步。我们可以把要注入的代码写到DLL中,然后让目标进程加载这个DLL,这就是所谓的DLL注入技术。一旦程序代码进入了另一个进程的地址空间,就可以毫无限制的做任何事情。

3.在这个要被注入到目标进程的DLL中写一个与感兴趣的API函数的签名完全相同的函数(代理函数)。当DLL执行初始化代码的时候,把目标进程对这个API的调用全部改为对代理函数的调用,即可实现拦截API函数。

 

使用钩子注入DLL

任何程序在接收键盘输入时都会先调用DLL中的KeyHookProc函数。

使用Windows钩子注入特定DLL到其他进程时一般都安装WH_GETMESSAGE钩子,而不是安装WH_KEYBOARD钩子。因为许多进程不接收键盘输入,所以Windows就不会将实现钩子函数的DLL加载到这些进程中,但是Windows下的应用程序大部分都需要调用GetMessage或PeekMessage函数从消息队列中获取消息,所以它们都会加载钩子函数所在的DLL。

安装WH_GETMESSAGE钩子的目的是让其它进程加载钩子函数所在的DLL,所以一般仅在钩子函数中调用CallNextHookEx函数,不做什么有用的工作。

 

HOOK过程

1.导入表的作用:

导入函数是被程序调用,但其实现代码却在其它模块中的函数,API函数全都是导入函数,它们的实现代码在Kernel32.dll、User32.dll等Win32子系统模块中。

导入表(Import Table):模块的导入函数名和这些函数驻留的DLL名等信息都保留在它的导入表中。导入表是一个IMAGE_IMPORT_DESCRIPTOR结构的数组,每个结构对应着一个导入模块。

作用:有了导入表之后,应用程序启动时,载入器根据PE文件的导入表记录的DLL名加载相应的DLL模块,在根据导入表的hint/name(函数序号/名称)记录的函数名取得函数地址,将这些地址保存到导入表的导入地址表(Import Address Table)(FirstThunk指向的数组)中。

应用程序在调用导入函数时,要先到导入表的IAT中找到这个函数的地址,然后再调用

了解了上面的知识之后,我们可以发现,只要修改模块的导入地址表,将导入地址表中的函数地址用一个自定义函数的地址覆盖掉,就可以实现HOOK API。

2.修改导入地址表: 定位导入表

为了修改导入地址表(IAT),必须先定位目标模块PE结构中的导入表的地址,这主要是对PE文件结构的分析。

PE文件结构:PE文件以64字节的DOS文件头(IMAGE_DOS_HESDER)开始,之后是一小段DOS程序,然后是248字节的NT文件头(IMAGE_NT_HEADERS)结构。NT文件头的偏移地址由IMAGE_DOS_HEADER结构的e_lfanew成员给出。NT文件头的前4个字节是文件签名("PE00"字符串),紧接着是20字节的IMAGE_FILE_HEADER结构。它的后面是224字节的IMAGE_OPTIONAL_HEADER结构。  IMAGE_OPTINAL_HEADER里面包含了许多重要的信息,有推荐的模块基地址、代码和数据的大小和基地址、线程堆栈和进程堆的配置、程序入口点的地址和我们最感兴趣的数据目录表指针。PE文件保留了16个数据目录,最常见的有导入表、导出表、资源和重定位表。

除了可以通过PE文件结构定位模块的导入表外,还可以使用ImageDirectoryEntryToData函数,这个函数知道模块基地址后直接返回指定数据目录表的首地址。为了调用这个API函数,必须包含ImageHlp.h 和 #pragma comment(lib, "ImageHlp")。

下面这个例子打印了此模块从其他模块导入的所有函数的名称和地址:

#include <Windows.h>
#include <stdio.h>

int main()
{
	//获得主模块的模块句柄
	HMODULE hMod = ::GetModuleHandleA(NULL);	
	//PE问价以DOS文件头开始
	IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER*)hMod;
	//获得PE文件中NT文件头中IMAGE_OPTIONAL_HEADER结构的指针
	IMAGE_OPTIONAL_HEADER * pOptHeader = 
		(IMAGE_OPTIONAL_HEADER*)((BYTE*)hMod + pDosHeader->e_lfanew + 24);
	//取得导入表中第一个IAMGE_IMPORT_DESCRIPTOR结构的指针
	IMAGE_IMPORT_DESCRIPTOR * pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)
		((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
	 
	//得到了导入表之后,就可以修改对应导入地址表中的地址了

	//IMAGE_IMPORT_DESCRIPTOR结构中的FirstThunk记录了导入函数的地址
	while(pImportDesc->FirstThunk){
		//获得模块的名称
		char * pszDllName = (char *)((BYTE*)hMod + pImportDesc->Name);
		printf("\n模块名称: %s\n", pszDllName);

		//一个IMAGE_THUNK_DATA结构就是一个双字,它指定了一个导入函数的地址
		IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA*)
			((BYTE*)hMod + pImportDesc->OriginalFirstThunk);

		int n = 0;
		while(pThunk->u1.Function){
			//获得导入函数的名称,hint/name表选项前2个字节是函数序号,后面才是函数名称字符串
			char * pszFunName = (char *)((BYTE *)hMod +
				(DWORD)pThunk->u1.AddressOfData + 2);
			//获得函数地址,IAT表就是一个DWORD类型的数组,每个成员记录一个函数的地址
			PDWORD lpAddr = (DWORD *)((BYTE*)hMod + 
				pImportDesc->FirstThunk) + n; 
			printf("  导出函数:%-25s>  ", pszFunName);
			printf("函数地址: %X\n", lpAddr);

			//使得指向下一个导入函数
			++n;	
			++pThunk;	
		}
		//使得指向下一个模块的导入表结构
		++pImportDesc;		
	}

}


HOOK API的实现

 

定位导入表之后即可定位导入地址表,为了截获API调用,只要用自定义函数的地址覆盖导入地址表中真是的API函数地址即可。

下面的一个例子是HOOK MessageBoxA函数的例子,在例子中使用自定义函数MyMessageBoxA取代了API函数MessageBoxA,使得主模块对MessageBoxA的调用都变为对自定义函数MyMessageBoxA的调用。

1.首先你得定义函数MyMessageBoxA

2.定位MessageBoxA所在模块在导入表中的位置

3.定位MessageBoxA在导入地址表中的位置,从而找到MessageBoxA地址,然后修该IAT表项,通过使用函数WriteProcessMemory

完整代码如下: (这个例子中只是挂钩本模块中对MessageBoxA的调用,对其他进程中调用MessageBoxA不起作用)

#include <Windows.h>
#include <stdio.h>

//挂钩指定模块hMod对MessageBoxA的调用
BOOL SetHook(HMODULE hMod);
//定义MessageBoxA的函数原型
typedef int (WINAPI *PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT uType);
//保存MessageBoxA的真是地址
PROC g_orgProc = (PROC)MessageBoxA;


int main()
{
	::MessageBoxA(NULL, "原函数", "Hook API test", 0);
	SetHook(::GetModuleHandleA(NULL));
	::MessageBoxA(NULL, "原函数", "Hook API test", 0);

}

//代理函数,代替函数 MessageBoxA
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	return ((PFNMESSAGEBOX)g_orgProc)(hWnd, "新函数", "Hook API test", uType);
}

//挂钩指定模块hMod对MessageBoxA的调用
BOOL SetHook(HMODULE hMod)
{
	//定位导入表
	IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER*)hMod;
	IMAGE_OPTIONAL_HEADER *pOptHeader = (IMAGE_OPTIONAL_HEADER*)
		((BYTE*)hMod + pDosHeader->e_lfanew + 24);
	IMAGE_IMPORT_DESCRIPTOR * pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)
		((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

	//在导入表中查找user32.dll模块,因为MessageBoxA函数从user32.dll模块导出
	while(pImportDesc->FirstThunk){
		//取得模块的名称
		char * pszDllName = (char *)((BYTE*)hMod + pImportDesc->Name);
		if(lstrcmpiA(pszDllName, "user32.dll") ==  0){
			break;
		}
		++pImportDesc;
	}

	//找到对应的模块之后,就要定位函数MessageBoxA的位置
	if(pImportDesc->FirstThunk){
		//一个IMAGE_THUNK_DATA结构就是一个双字(DWORD),它指定了一个导入函数
		IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA*)
			((BYTE*)hMod + pImportDesc->FirstThunk);
		
		//逐个取出函数地址来比较
		while(pThunk->u1.Function){
			//lpAddr指向的内存保存了函数的地址
			DWORD * lpAddr = (DWORD *)&(pThunk->u1.Function);
			
			if(*lpAddr == (DWORD)g_orgProc){	//看看是否找到MessageBoxA的地址
				//修改导入地址表(IAT),使其指向我们自定义的函数
				//相当于语句 *lpAddr = (DWORD)MyMessageBOxA;
				DWORD * lpNewProc = (DWORD *)MyMessageBoxA;

				//写入内存
				::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, 
					sizeof(DWORD), NULL);
				printf("找到了\n");
				return true;
			}
			//指向下一个函数地址
			++pThunk;
		}
	}
	return false;
}

 

注意:当利用WriteProcessMemory函数写内存时,如果在Debug版本下运行没有问题,但是在Release版本下程序对WriteProcessMemory的调用将会失败,原因是此时lpAddr指向的内存仅是可读的。 要想写这块内存,必须调用VirtualProtect函数改变内存地址所在页的页属性,将它改为可写。

				/*
					修改内存页的属性
				*/
				DWORD dwOldProtect;
				MEMORY_BASIC_INFORMATION mbi;
				VirtualQuery(lpAddr, &mbi, sizeof(mbi));
				VirtualProtect(lpAddr, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect);


				//写内存
				::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, 
					sizeof(DWORD), NULL);

				//恢复页的保护属性
				VirtualProtect(lpAddr, sizeof(DWORD), dwOldProtect, 0);


如果是挂钩其它进程中特定API的调用,就要将类似SetHook函数的代码写入DLL,在DLL初始化的时候调用它,然后将这个DLL注入到目标进程,这样的代码就会在目标进程中的地址空间执行,从而改变目标进程模块的导入地址表。

 

封装CAPIHook类 ---- 一个很好用的类

 

1.HOOK所有模块:HOOK一个进程对某个API的调用时,不仅要修改主模块导入表,还必须遍历此进程的所有模块,替换掉每个模块对目标API的调用。CAPIHook类提供了连个函数来完成这项工作, ReplaceIATEntryInOneMod 和 ReplaceIATEntryAllMod

 

2.防止程序在运行期间动态加载模块:在HOOK完目标进程当前的所有模块后,它还可以调用LoadLibrary函数加载新的模块。为了能够将今后目标进程动态加载的模块也HOOK掉,可以默认挂钩LoadLibrary之类的函数。在代理函数中首先调用原来的Loadlibrary函数,然后对新加载的模块调用ReplaceIATEntryInOneMod函数。

一个CAPIHook对象仅能挂钩一个API函数,为了挂钩多个API函数,用户可能申请了多个CAPIHook对象。将所有的CAPIHook对象连成一个链表,用一个静态变量记录下表头,在每个CAPIHook对象中在记录下表中下一个CAPIHook对象的地址。

 

3.防止程序在运行期间动态调用API函数:并不是只有经过导入表才能调用API函数,应用程序可以在运行期间调用GetProcAddress函数取得API函数的地址在调用它。所以也要默认挂钩GetProcAddress函数CAPIHook类的静态成员函数GetProcAddress将替换这个API。

 

下面介绍一个HOOK API 的实例: 进程保护器

每当系统内有进程调用了TerminateProcess函数,程序就会将他截获,在输出窗口显示调用进程主模块的镜像文件名和传递给TerminateProcess的两个参数。

为了HOOK掉所有进程对TerminateProcess的调用,我们要创建一个DLL。

#include "APIHook.h"
#include <Windows.h>

extern CAPIHook g_TerminateProcess;

//替代TerminateProcess的函数
BOOL WINAPI Hook_TerminateProcess(HANDLE hProcess, UINT uExitCode)
{
	typedef BOOL (WINAPI*PFNTERMINATEPROCESS)(HANDLE, UINT);
	//保存主模块的文件名称
	char szPathName[MAX_PATH];
	//取得主模块的文件名称
	::GetModuleFileNameA(NULL, szPathName, MAX_PATH);

	//构建发送给主窗口的字符串
	char sz[2048];
	wsprintf(sz, "\r\n 进程: (%d) %s\r\n\r\n 进程句柄: %x\r\n 退出代码:%d",
		::GetCurrentProcessId(), szPathName, hProcess, uExitCode);
	//发送字符串到主对话框
	COPYDATASTRUCT cds = {::GetCurrentProcessId(), strlen(sz) + 1, sz};
	if(::SendMessageA(::FindWindowA(NULL, "进程保护器"), WM_COPYDATA, 
		0, (LPARAM)&cds) != -1){
		//如果函数返回的不是-1,我们就允许API执行
		return ((PFNTERMINATEPROCESS)(PROC)g_TerminateProcess)(hProcess,uExitCode);
	}
	return true;
}

//挂钩TerminateProcess函数
CAPIHook g_TerminateProcess("kernel32.dll", "TerminateProcess", (PROC)Hook_TerminateProcess);

//定义一个数据段 YCIShared
#pragma data_seg("YCIShared")
HHOOK g_hHook = NULL;
#pragma data_seg()

//通过内存地址取得模块句柄  的帮助函数
static HMODULE ModuleFromAddress(PVOID pv)
{
	MEMORY_BASIC_INFORMATION mbi;
	if(::VirtualQuery(pv, &mbi, sizeof(mbi)) != 0){
		return (HMODULE)mbi.AllocationBase;
	}
	else{
		return NULL;
	}
}

static LRESULT WINAPI GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
	return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

//负责安装和卸载WH_GETMESSAGE类型的钩子
_declspec(dllexport) BOOL WINAPI SetSysHook(BOOL bInstall, DWORD dwThreadId)
{
	BOOL bOK;
	if(bInstall){
		g_hHook = ::SetWindowsHookExA(WH_GETMESSAGE, GetMsgProc,
			ModuleFromAddress(GetMsgProc), dwThreadId);
		bOK = (g_hHook != NULL);
	}
	else{
		bOK = ::UnhookWindowsHookEx(g_hHook);
		g_hHook = NULL;
	}
	return bOK;
}

利用上面的代码,生成了HookTermProcLib.dll,供另外一个测试程序调用。

WM_COPYDATA消息:

Hook_TerminateProcess函数采用了发送WM_COPYDATA消息的方式向主程序传递数据。这是系统定义的用于在进程间传递数据的消息。需要注意的是,直接在消息的参数中隔着进程传递指针是不行的,因为进程的地址空间是相互隔离的,接收方接收到的仅仅是一个指针的值,不可能接收到指针所指的内容。如果要传递的参数必须由指针来决定,就要使用WM_COPYDATA消息。但是接收方必须认为接收到的数据是只读的,不可以改变lpData指向的数据。如果使用内存映射文件的话没有这个限制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章