• 9.3 挂钩API技术(HOOK API)


    HOOK API

    HOOK API是指截获特定进程或系统对某个API函数的调用,使得API的执行流程转向指定的代码。例如,在挂钩了系统对User32.dll模块中MessageBoxA函数的调用以后,每当有应用程序调用MessageBoxA函数,调用线程都会执行用户提供的代码,而不去执行真正的MessageBoxA API函数。

    Windows下的应用程序都建立在API函数之上,所以截获API是一项相当有用的技术,它使得用户有机会干预其他应用程序的程序流程。

    9.3.1 实现原理

    Windows下应用程序有自己的地址空间,它们只能调用自己地址空间中的函数,所以在挂钩API之前,必须将一个可以代替API执行的函数的执行代码注入到目标进程,然后再想办法将目标进程对该API的调用改为对注入到目标进程中自定义函数的调用。一般称这个自定义函数为代理函数。在代理函数中,可以去调用原来的API,也可以做其他事情。

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

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

    还可以趁着DLL在目标进程中初始化的机会去创建新的线程。这个时候创建的线程运行在目标进程的地址空间中,所以它对目标进程有着完全的访问权限。例如,可以将它视为守护线程,在接收到通知时,访问目标进程中的资源,也可以通过这种方式隐藏自己,创建没有“进程”的线程。

    9.3.2 使用钩子注入DLL

    在HOOK键盘输入的实例中,任何程序在接收到键盘输入时都会先调用DLL中的KeyHookProc函数。进程的地址空间是相互隔离的,可是这个接收键盘输入的进程却可以调用KeyHookProc函数,执行其中的代码,访问其中的变量,这说明KeyHookProc函数的实现代码已经被映射到每个能够接收键盘输入的进程中了。

    在成功调用SetWindowsHookEx函数安装系统范围内的键盘钩子之后,Windows在内部自动对每个接收键盘输入的进程调用LoadLibrary函数,强迫它们加载包含钩子函数执行代码的模块09KeyHookLib.dll。这就是这些进程能够访问钩子函数的原因。

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

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

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

    如果要将DLL注入到特定进程中,一般是将该进程中主线程的线程ID传递给SetWindowsHookEx函数;而如果要将DLL注入到所有进程中,安装一个系统范围内的钩子即可(将0作为线程ID传递给SetWindowsHookEx函数)。

    9.3.3 HOOK过程

    1.导入表的作用
    导入函数是被程序调用,但其实现代码却在其他模块中的函数。API函数全都是导入函数,它们的实现代码在Kernel32.dll、User32.dll等Win32子系统模块(详见11.1节)中。

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

    typedef struct _IMAGE_IMPORT_DESCRIPTOR {
        union {
            DWORD   Characteristics;            // 0 for terminating null import descriptor
            DWORD   OriginalFirstThunk;         // hint/name(函数序号/名称)表的偏移量,记录导入函数名称 
        } DUMMYUNIONNAME;
        DWORD   TimeDateStamp;                  // 0 if not bound,
                                                // -1 if bound, and real date\time stamp
                                                //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                                // O.W. date/time stamp of DLL bound to (Old BIND)
    
        DWORD   ForwarderChain;                 // -1 if no forwarders
        DWORD   Name;                           //导入模块名称字符串的偏移量
        DWORD   FirstThunk;                     //  IAT(Import Address Table,导入地址表)的偏移量,记录导入函数地址
    } IMAGE_IMPORT_DESCRIPTOR;
    

    IMAGE_IMPORT_DESCRIPTOR结构的第一个域包含到hint/name(函数序号/名称)表的偏移量,最后一个域包含到导入地址表(Import Address Table,IAT)的偏移量。这两个表的大小相同,一个成员对应一个导入函数。

    应用程序启动时,载入器根据PE文件的导入表记录的DLL名(Name域)加载相应DLL模块,再根据导入表的hint/name表(OriginalFirstThunk指向的数组)记录的函数名取得函数的地址,将这些地址保存到导入表的IAT(FirstThunk指向的数组)中。

    应用程序在调用导入函数时,要先到导入表的IAT中找到这个函数的地址,然后再调用。例如,调用User32.dll模块中MessageBoxA函数的代码最终会被汇编成如下代码。

    call dword ptr [__imp__MessageBoxA@16 (0042428c)] // 函数的真实地址记录在0042428c地址处
    

    模块的IAT(导入地址表)仅仅是一个DWORD数组,数组的每个成员记录着一个导入函数的地址。地址0042428c是导入地址表中MessageBoxA函数对应成员的地址,这个地址处的内容是MessageBoxA在User32模块的真实地址。可见,调用API函数时,程序先要转向PE文件的导入地址表取得API函数的真实地址,然后再转向API函数的执行代码。

    一种非常常用的HOOK API的方法就是修改模块的导入表。还以MessageBoxA函数为例,如果将0042428c地址处的内容用一个自定义函数的地址覆盖掉,那么以后这个模块对MessageBoxA的调用实际上就成了对该自定义函数的调用,程序的执行流程转向了自定义函数,而不是真实的API函数。但是,为了保持堆栈的平衡,自定义函数使用的调用规则和参数的个数必须与它所替代的API函数完全相同。

    这种HOOK API的方法是最稳定的一种,而且实现起来也不算复杂。

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

    如8.1.4小节所述,PE文件以64字节的DOS文件头开始(IMAGE_DOS_HEADER),接着是一小段DOS程序,然后是248字节的NT文件头(IMAGE_NT_HEADERS)。NT文件头相对文件开始位置的偏移量可以有IMAGE_DOS_HEADER结构的e_lfanew给出。

    NT文件头的前4个字节是文件签名("PE00"字符串),紧接着是20字节的IMAGE_FILE_HEADER结构,它的后面是224字节的IMAGE_OPTIONAL_HEADER结构。

    下面的代码取得了一个指向IMAGE_OPTIONAL_HEADER结构的指针(以主模块为例)。

    HMODULE hMod = ::GetModuleHandle(NULL);
    IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hMod;
    IMAGE_OPTIONAL_HEADER * pOptHeader =
    	(IMAGE_OPTIONAL_HEADER *)((BYTE*)hMod + pDosHeader->e_lfanew + 24);
    

    事实上,IMAGE_OPTIONAL_HEADER绝对不是可选的(optional),它里面包含了许多重要的信息,有推荐的模块基地址、代码和数据的大小和基地址、线程堆栈和进程堆的配置、程序入口点的地址、和我们最感兴趣的数据目录表指针。PE文件保留了16个数据目录。最常见的有导入表、导出表、资源和重定位表。这里要用到的是导入表,它是一个IMAGE_IMPORT_DESCRIPTOR结构的数组,每个结构对应着一个导入模块。下面的代码取得导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的指针(导入表首地址)。

    IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)
    		((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
    

    除了可以通过PE文件结构定位模块的导入表外,还可以使用ImageDirectoryEntryToData函数。这个函数知道模块基地址后直接返回指定数据目录表的首地址,用法如下。

    PVOID ImageDirectoryEntryToData(  
      PVOID Base, // 模块基地址  
      BOOLEAN MappedAsImage,// 如果此参数是TRUE,文件被系统当作镜像映射,否则,将当作数据文件映射  
      USHORT DirectoryEntry, // 指定IMAGE_DIRECTORY_ENTRY_IMPORT说明要取得导入表首地址  
      PULONG Size // 返回表项的大小 
    ); // 为了调用此API,请添加代码“#include ”和“#pragma comment(lib, "ImageHlp")”
    

    IMAGE_IMPORT_DESCRIPTOR结构包含了hint/name(函数序号/名称)表和IAT(导入地址表)的偏移量。这两个表的大小相同,一个成员对应一个导入函数,分别记录了导入函数的名称和地址。下面代码打印出了此模块从其他模块导入的所有函数的名称和地址。
    09ImportTable工程

    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表前两个字节是函数的序号,后4个字节是函数名称字符串的地址
    		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++;
    }
    

    内部循环列出了从一个模块中导入的所有函数的函数名称和地址,外部循环处理下一个导入模块。可以看到,模块的导入地址表仅仅是一个DWORD数组。
    在这里插入图片描述
    3.HOOK API的实现
    定位导入表之后即可定位导入地址表。为了截获API调用,只要用自定义函数的地址覆盖掉导入地址表中真实的API函数地址即可。

    下面是挂钩MessageBoxA函数的例子(09HookDemo工程)。这个例子用自定义函数MyMessageBoxA取代了API函数MessageBoxA,使得主模块中对MessageBoxA的调用都变成了对自定义函数MyMessageBoxA的调用。具体代码如下。

    // 09HookDemo.cpp文件
    #include 
    #include 
    
    // 挂钩指定模块hMod对MessageBoxA的调用
    BOOL SetHook(HMODULE hMod);
    // 定义MessageBoxA函数原型
    typedef int (WINAPI *PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT uType);
    // 保存MessageBoxA函数的真实地址
    PROC g_orgProc = (PROC)MessageBoxA;
    
    void main()
    {
    	// 调用原API函数
    	::MessageBox(NULL, "原函数", "09HookDemo", 0);
    	// 挂钩后再调用
    	SetHook(::GetModuleHandle(NULL));
    	::MessageBox(NULL, "原函数", "09HookDemo", 0);
    }
    
    // 用于替换MessageBoxA的自定义函数
    int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
    {
    	return ((PFNMESSAGEBOX)g_orgProc)(hWnd, "新函数", "09HookDemo", uType);
    }
    
    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++;
    	}
    
    	if(pImportDesc->FirstThunk)
    	{
    		
    		// 一个IMAGE_THUNK_DATA就是一个双字,它指定了一个导入函数
    		// 调入地址表其实是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)
    			{	
    				// 修改IAT表项,使其指向我们自定义的函数,相当于“*lpAddr = (DWORD)MyMessageBoxA;”
    				DWORD* lpNewProc = (DWORD*)MyMessageBoxA;
    				::WriteProcessMemory(::GetCurrentProcess(), 
    						lpAddr, &lpNewProc, sizeof(DWORD), NULL);
    				return TRUE;
    			}
    			
    			pThunk++;
    		}
    	}
    	return FALSE;
    }
    

    运行这个程序,在SetHook函数执行前后,弹出的对话框是不相同的。执行前调用的是真实的MessageBoxA函数,而执行后调用的是程序自定义的MyMessageBoxA函数。原因是SetHook函数修改了记录MessageBoxA地址的导入地址表项。

    if(*lpAddr == (DWORD)g_orgProc)
    {	
    	// 修改IAT表项,使其指向我们自定义的函数,相当于“*lpAddr = (DWORD)MyMessageBoxA;”
    	DWORD* lpNewProc = (DWORD*)MyMessageBoxA;
    	::WriteProcessMemory(::GetCurrentProcess(), 
    			lpAddr, &lpNewProc, sizeof(DWORD), NULL);
    	return TRUE;
    }
    

    事实上,这样的代码在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注入到目标进程,这样的代码就会在目标进程的地址空间执行,从而改变目标进程模块的导入地址表。
    在这里插入图片描述
    上例仅仅是实现HOOK的最核心的内容。一个真正的HOOK系统还要考虑很多事情。

    9.3.4 封装CAPIHook类

    这个类最初是由Jeffrey Richter设计的,笔者仅仅做了一些改动以使它更好用。实际上由于接口成员简单,所有基于修改导入表设计的HOOK API的类都大同小异。下面是在设计过程中需要注意的几个问题。

    1.HOOK所有模块
    HOOK一个进程对某个API调用时,不仅要修改主模块的导入表,还必须遍历此进程的所有模块,替换掉每个模块对目标API的调用。CAPIHook类通过下面两个静态函数来完成这项工作。

    void CAPIHook::ReplaceIATEntryInOneMod(LPSTR pszExportMod, 
    				   PROC pfnCurrent, PROC pfnNew, HMODULE hModCaller)
    {
    	// 取得模块的导入表(import descriptor)首地址。ImageDirectoryEntryToData函数可以直接返回导入表地址
    	ULONG ulSize;
    	PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
    				::ImageDirectoryEntryToData(hModCaller, TRUE, 
    					IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
    	if(pImportDesc == NULL)	// 这个模块没有导入节表
    	{
    		return;
    	}
    
    	// 查找包含pszExportMod模块中函数导入信息的导入表项
    	while(pImportDesc->Name != 0)
    	{
    		LPSTR pszMod = (LPSTR)((DWORD)hModCaller + pImportDesc->Name);
    		if(lstrcmpiA(pszMod, pszExportMod) == 0) // 找到
    			break;
    
    		pImportDesc++;
    	}
    	if(pImportDesc->Name == 0) // hModCaller模块没有从pszExportMod模块导入任何函数
    	{
    		return;
    	}
    
    	// 取得调用者的导入地址表(import address table, IAT)
    	PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(pImportDesc->FirstThunk + (DWORD)hModCaller);
    
    	// 查找我们要HOOK的函数,将它的地址用新函数的地址替换掉
    	while(pThunk->u1.Function)
    	{
    		// lpAddr指向的内存保存了函数的地址
    		PDWORD lpAddr = (PDWORD)&(pThunk->u1.Function);
    		if(*lpAddr == (DWORD)pfnCurrent)
    		{
    			// 修改页的保护属性
    			DWORD dwOldProtect;
    			MEMORY_BASIC_INFORMATION mbi;
    			::VirtualQuery(lpAddr, &mbi, sizeof(mbi));
    			::VirtualProtect(lpAddr, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect);
    
    			// 修改内存地址  相当于“*lpAddr = (DWORD)pfnNew;”
    			::WriteProcessMemory(::GetCurrentProcess(), 
    						lpAddr, &pfnNew, sizeof(DWORD), NULL);
    
    			::VirtualProtect(lpAddr, sizeof(DWORD), dwOldProtect, 0);
    			break;
    		}
    		pThunk++;
    	}
    }
    
    void CAPIHook::ReplaceIATEntryInAllMods(LPSTR pszExportMod, 
    					PROC pfnCurrent, PROC pfnNew, BOOL bExcludeAPIHookMod)
    {
    	// 取得当前模块的句柄
    	HMODULE hModThis = NULL;
    	if(bExcludeAPIHookMod)
    	{
    		MEMORY_BASIC_INFORMATION mbi;
    		if(::VirtualQuery(ReplaceIATEntryInAllMods, &mbi, sizeof(mbi)) != 0)
    			hModThis = (HMODULE)mbi.AllocationBase;
    	}
    
    	// 取得本进程的模块列表
    	HANDLE hSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ::GetCurrentProcessId());
    
    	// 遍历所有模块,分别对它们调用ReplaceIATEntryInOneMod函数,修改导入地址表
    	MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
    	BOOL bOK = ::Module32First(hSnap, &me);
    	while(bOK)
    	{
    		// 注意:我们不HOOK当前模块的函数
    		if(me.hModule != hModThis)
    			ReplaceIATEntryInOneMod(pszExportMod, pfnCurrent, pfnNew, me.hModule);
    
    		bOK = ::Module32Next(hSnap, &me);
    	}
    	::CloseHandle(hSnap);
    }
    

    ReplaceIATEntryInOneMod函数修改hModCaller模块的IAT,将所有对pfnCurrent函数的调用改为对pfnNew函数的调用,参数pszExportMod是目标API所在模块。例如,为了用自定义函数MyMessageBoxA替换当前模块中的MessageBoxA函数,可以如下调用这个函数。

    CAPIHook::ReplaceIATEntryInOneMod("User32.dll",  (PROC)MyMessageBoxA, (PROC)MessageBoxA, ::GetModuleHandle(NULL)); 
    

    ReplaceIATEntryInAllMods函数修改进程内所有模块的IAT,挂钩用户指定的API函数。最后一个参数bExcludeAPIHookMod指定是否将负责HOOK API的模块排除在外。

    CAPIHook类工作在一个单独的DLL里,为了在此模块中方便地调用原来的API函数,一般选择不HOOK当前模块的函数,即将bExcludeAPIHookMod参数设为TURE。

    2.防止程序在运行期间动态加载模块

    在HOOK完目标进程当前所有模块之后,他还可以调用LoadLibrary函数加载新的模块。为了能够将今后目标进程动态加载的模块也HOOK掉,可以默认挂钩LoadLibrary之类的函数。在代理函数中首先调用原来的LoadLibrary函数,然后再对新加载的模块调用ReplaceIATEntryInOneMod函数。

    比如替换LoadLibrary API的自定义函数是HookNewlyLoadedModule,怎么实现它呢?一个CAPIHook对象仅能够挂钩一个API函数,为了挂钩多个API,用户很可能申请了多个CAPIHook对象,所以在自定义函数HookNewlyLoadedModule中,必须为每个CAPIHook对象都调用ReplaceIATEntryInOneMod函数才能确保新模块中相应的IAT项都被修改。这就需要记录用户申请的所有CAPIHook对象的指针。

    比较简单的方法是将所有的CAPIHook对象连成一个链表,用一个静态变量记录下表头地址,在每个CAPIHook对象中再记录下表中下一个CAPIHook对象的地址。

    class CAPIHook 
    { 
         ……  
         // 这两个指针用来将当前模块中所有的CAPIHook对象连在一起  
         static CAPIHook *sm_pHeader;  
         CAPIHook *m_pNext; 
    } 
    

    静态函数HookNewlyLoadedModule的实现代码如下。

    void WINAPI CAPIHook::HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags)
    {
    	// 如果一个新的模块被加载,挂钩各CAPIHook对象要求的API函数
    	if((hModule != NULL) && ((dwFlags&LOAD_LIBRARY_AS_DATAFILE) == 0))
    	{
    		CAPIHook *p = sm_pHeader;
    		while(p != NULL)
    		{
    			ReplaceIATEntryInOneMod(p->m_pszModName, p->m_pfnOrig, p->m_pfnHook, hModule);
    			p = p->m_pNext;
    		}
    	}
    }
    

    dwFlags是LoadLibraryEx函数的一个附加参数,它指定了加载时采取的行动,只要它的值中不包含LOAD_LIBRARY_AS_DATAFILE标记,就说明文件要以镜像方式映射到内存。

    3.防止程序在运行期间动态调用API函数

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

    FARPROC WINAPI CAPIHook::GetProcAddress(HMODULE hModule, PCSTR pszProcName)
    {
    	// 得到这个函数的真实地址
    	FARPROC pfn = ::GetProcAddress(hModule, pszProcName);
    
    	// 看它是不是我们要hook的函数
    	CAPIHook *p = sm_pHeader;
    	while(p != NULL)
    	{
    		if(p->m_pfnOrig == pfn)
    		{
    			pfn = p->m_pfnHook;
    			break;
    		}
    
    		p = p->m_pNext;
    	}
    
    	return pfn;
    }
    

    下面是CAPIHook类的完整实现,09HookTermProLib工程下找到源程序代码。

    // APIHook.h文件
    
    #ifndef __APIHOOK_H__
    #define __APIHOOK_H__
    
    #include 
    
    class CAPIHook  
    {
    public:
    	CAPIHook(LPSTR pszModName, 
    		LPSTR pszFuncName, PROC pfnHook, BOOL bExcludeAPIHookMod = TRUE);
    	virtual ~CAPIHook();
    	operator PROC() { return m_pfnOrig; }
    
    // 实现
    private:
    	LPSTR m_pszModName;		// 导出要HOOK函数的模块的名字
    	LPSTR m_pszFuncName;		// 要HOOK的函数的名字
    	PROC m_pfnOrig;			// 原API函数地址
    	PROC m_pfnHook;			// HOOK后函数的地址
    	BOOL m_bExcludeAPIHookMod;	// 是否将HOOK API的模块排除在外
    
    private:
    	static void ReplaceIATEntryInAllMods(LPSTR pszExportMod, PROC pfnCurrent, 
    				PROC pfnNew, BOOL bExcludeAPIHookMod);
    	static void ReplaceIATEntryInOneMod(LPSTR pszExportMod, 
    				PROC pfnCurrent, PROC pfnNew, HMODULE hModCaller);
    
    
    // 下面的代码用来解决其它模块动态加载DLL的问题
    private:
    	// 这两个指针用来将所有的CAPIHook对象连在一起
    	static CAPIHook *sm_pHeader;
    	CAPIHook *m_pNext;
    
    private:
    	// 当一个新的DLL被加载时,调用此函数
    	static void WINAPI HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags);
    
    	// 用来跟踪当前进程加载新的DLL
    	static HMODULE WINAPI LoadLibraryA(PCSTR  pszModulePath);
    	static HMODULE WINAPI LoadLibraryW(PCWSTR pszModulePath);
    	static HMODULE WINAPI LoadLibraryExA(PCSTR  pszModulePath, HANDLE hFile, DWORD dwFlags);
    	static HMODULE WINAPI LoadLibraryExW(PCWSTR pszModulePath, HANDLE hFile, DWORD dwFlags);
    	
    	// 如果请求已HOOK的API函数,则返回用户自定义函数的地址
    	static FARPROC WINAPI GetProcAddress(HMODULE hModule, PCSTR pszProcName);
    private:
    	// 自动对这些函数进行挂钩
    	static CAPIHook sm_LoadLibraryA;
    	static CAPIHook sm_LoadLibraryW;
    	static CAPIHook sm_LoadLibraryExA;
    	static CAPIHook sm_LoadLibraryExW;
    	static CAPIHook sm_GetProcAddress;
    };
    
    #endif // __APIHOOK_H__
    
    --------------------------------------------------------------------------
    // APIHook.cpp文件
    
    #include "APIHook.h"
    #include "Tlhelp32.h"
    
    #include 	// 为了调用ImageDirectoryEntryToData函数
    #pragma comment(lib, "ImageHlp")
    
    
    // CAPIHook对象链表的头指针
    CAPIHook* CAPIHook::sm_pHeader = NULL;
    
    CAPIHook::CAPIHook(LPSTR pszModName, LPSTR pszFuncName, PROC pfnHook, BOOL bExcludeAPIHookMod)
    {
    	// 保存这个Hook函数的信息
    	m_bExcludeAPIHookMod = bExcludeAPIHookMod;
    	m_pszModName = pszModName;
    	m_pszFuncName = pszFuncName;
    	m_pfnHook = pfnHook;
    	m_pfnOrig = ::GetProcAddress(::GetModuleHandle(pszModName), pszFuncName);
    
    	// 将此对象添加到链表中
    	m_pNext = sm_pHeader;
    	sm_pHeader = this;
    	
    	// 在所有当前已加载的模块中HOOK这个函数
    	ReplaceIATEntryInAllMods(m_pszModName, m_pfnOrig, m_pfnHook, bExcludeAPIHookMod); 
    }
    
    CAPIHook::~CAPIHook()
    {
    	// 取消对所有模块中函数的HOOK
    	ReplaceIATEntryInAllMods(m_pszModName, m_pfnHook, m_pfnOrig, m_bExcludeAPIHookMod);
    
    	CAPIHook *p = sm_pHeader;
    
    	// 从链表中移除此对象
    	if(p == this)
    	{
    		sm_pHeader = p->m_pNext;
    	}
    	else
    	{
    		while(p != NULL)
    		{
    			if(p->m_pNext == this)
    			{
    				p->m_pNext = this->m_pNext;
    				break;
    			}
    			p = p->m_pNext;
    		}
    	}
    
    }
    
    void CAPIHook::ReplaceIATEntryInOneMod(LPSTR pszExportMod, 
    				   PROC pfnCurrent, PROC pfnNew, HMODULE hModCaller)
    {
    	// 取得模块的导入表(import descriptor)首地址。ImageDirectoryEntryToData函数可以直接返回导入表地址
    	ULONG ulSize;
    	PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
    				::ImageDirectoryEntryToData(hModCaller, TRUE, 
    					IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
    	if(pImportDesc == NULL)	// 这个模块没有导入节表
    	{
    		return;
    	}
    
    	// 查找包含pszExportMod模块中函数导入信息的导入表项
    	while(pImportDesc->Name != 0)
    	{
    		LPSTR pszMod = (LPSTR)((DWORD)hModCaller + pImportDesc->Name);
    		if(lstrcmpiA(pszMod, pszExportMod) == 0) // 找到
    			break;
    
    		pImportDesc++;
    	}
    	if(pImportDesc->Name == 0) // hModCaller模块没有从pszExportMod模块导入任何函数
    	{
    		return;
    	}
    
    	// 取得调用者的导入地址表(import address table, IAT)
    	PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(pImportDesc->FirstThunk + (DWORD)hModCaller);
    
    	// 查找我们要HOOK的函数,将它的地址用新函数的地址替换掉
    	while(pThunk->u1.Function)
    	{
    		// lpAddr指向的内存保存了函数的地址
    		PDWORD lpAddr = (PDWORD)&(pThunk->u1.Function);
    		if(*lpAddr == (DWORD)pfnCurrent)
    		{
    			// 修改页的保护属性
    			DWORD dwOldProtect;
    			MEMORY_BASIC_INFORMATION mbi;
    			::VirtualQuery(lpAddr, &mbi, sizeof(mbi));
    			::VirtualProtect(lpAddr, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect);
    
    			// 修改内存地址  相当于“*lpAddr = (DWORD)pfnNew;”
    			::WriteProcessMemory(::GetCurrentProcess(), 
    						lpAddr, &pfnNew, sizeof(DWORD), NULL);
    
    			::VirtualProtect(lpAddr, sizeof(DWORD), dwOldProtect, 0);
    			break;
    		}
    		pThunk++;
    	}
    }
    
    void CAPIHook::ReplaceIATEntryInAllMods(LPSTR pszExportMod, 
    					PROC pfnCurrent, PROC pfnNew, BOOL bExcludeAPIHookMod)
    {
    	// 取得当前模块的句柄
    	HMODULE hModThis = NULL;
    	if(bExcludeAPIHookMod)
    	{
    		MEMORY_BASIC_INFORMATION mbi;
    		if(::VirtualQuery(ReplaceIATEntryInAllMods, &mbi, sizeof(mbi)) != 0)
    			hModThis = (HMODULE)mbi.AllocationBase;
    	}
    
    	// 取得本进程的模块列表
    	HANDLE hSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ::GetCurrentProcessId());
    
    	// 遍历所有模块,分别对它们调用ReplaceIATEntryInOneMod函数,修改导入地址表
    	MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
    	BOOL bOK = ::Module32First(hSnap, &me);
    	while(bOK)
    	{
    		// 注意:我们不HOOK当前模块的函数
    		if(me.hModule != hModThis)
    			ReplaceIATEntryInOneMod(pszExportMod, pfnCurrent, pfnNew, me.hModule);
    
    		bOK = ::Module32Next(hSnap, &me);
    	}
    	::CloseHandle(hSnap);
    }
    
    
    // 挂钩LoadLibrary和GetProcAddress函数,以便在这些函数被调用以后,挂钩的函数也能够被正确的处理
    
    CAPIHook CAPIHook::sm_LoadLibraryA("Kernel32.dll", "LoadLibraryA",   
    					(PROC)CAPIHook::LoadLibraryA, TRUE);
    
    CAPIHook CAPIHook::sm_LoadLibraryW("Kernel32.dll", "LoadLibraryW",   
    					(PROC)CAPIHook::LoadLibraryW, TRUE);
    
    CAPIHook CAPIHook::sm_LoadLibraryExA("Kernel32.dll", "LoadLibraryExA", 
    					(PROC)CAPIHook::LoadLibraryExA, TRUE);
    
    CAPIHook CAPIHook::sm_LoadLibraryExW("Kernel32.dll", "LoadLibraryExW", 
    					(PROC)CAPIHook::LoadLibraryExW, TRUE);
    
    CAPIHook CAPIHook::sm_GetProcAddress("Kernel32.dll", "GetProcAddress", 
    					(PROC)CAPIHook::GetProcAddress, TRUE);
    
    
    
    void WINAPI CAPIHook::HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags)
    {
    	// 如果一个新的模块被加载,挂钩各CAPIHook对象要求的API函数
    	if((hModule != NULL) && ((dwFlags&LOAD_LIBRARY_AS_DATAFILE) == 0))
    	{
    		CAPIHook *p = sm_pHeader;
    		while(p != NULL)
    		{
    			ReplaceIATEntryInOneMod(p->m_pszModName, p->m_pfnOrig, p->m_pfnHook, hModule);
    			p = p->m_pNext;
    		}
    	}
    }
    
    
    HMODULE WINAPI CAPIHook::LoadLibraryA(PCSTR pszModulePath) 
    {
    	HMODULE hModule = ::LoadLibraryA(pszModulePath);
    	HookNewlyLoadedModule(hModule, 0);
    	return(hModule);
    }
    
    HMODULE WINAPI CAPIHook::LoadLibraryW(PCWSTR pszModulePath) 
    {
    	HMODULE hModule = ::LoadLibraryW(pszModulePath);
    	HookNewlyLoadedModule(hModule, 0);
    	return(hModule);
    }
    
    HMODULE WINAPI CAPIHook::LoadLibraryExA(PCSTR pszModulePath, HANDLE hFile, DWORD dwFlags) 
    {
    	HMODULE hModule = ::LoadLibraryExA(pszModulePath, hFile, dwFlags);
    	HookNewlyLoadedModule(hModule, dwFlags);
    	return(hModule);
    }
    
    HMODULE WINAPI CAPIHook::LoadLibraryExW(PCWSTR pszModulePath, HANDLE hFile, DWORD dwFlags) 
    {
    	HMODULE hModule = ::LoadLibraryExW(pszModulePath, hFile, dwFlags);
    	HookNewlyLoadedModule(hModule, dwFlags);
    	return(hModule);
    }
    
    FARPROC WINAPI CAPIHook::GetProcAddress(HMODULE hModule, PCSTR pszProcName)
    {
    	// 得到这个函数的真实地址
    	FARPROC pfn = ::GetProcAddress(hModule, pszProcName);
    
    	// 看它是不是我们要hook的函数
    	CAPIHook *p = sm_pHeader;
    	while(p != NULL)
    	{
    		if(p->m_pfnOrig == pfn)
    		{
    			pfn = p->m_pfnHook;
    			break;
    		}
    
    		p = p->m_pNext;
    	}
    
    	return pfn;
    }
    
    -------------------------------------------------------
    // HookTermProLib.cpp文件
    
    #include 
    #include "APIHook.h"
    
    extern CAPIHook g_TerminateProcess;
    
    // 自定义TerminateProcess函数
    BOOL WINAPI Hook_TerminateProcess(HANDLE hProcess, UINT uExitCode)
    {
    	typedef BOOL (WINAPI *PFNTERMINATEPROCESS)(HANDLE, UINT);
    
    	// 取得主模块的文件名称
    	char szPathName[MAX_PATH];
    	::GetModuleFileName(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(::SendMessage(::FindWindow(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);
    
    
    ///
    
    #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);
    }
    
    BOOL WINAPI SetSysHook(BOOL bInstall, DWORD dwThreadId)
    {
    	BOOL bOk;
    	if(bInstall) 
    	{
    		g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, 
    						ModuleFromAddress(GetMsgProc), dwThreadId);
    		bOk = (g_hHook != NULL);
    	} 
    	else 
    	{
    		bOk = ::UnhookWindowsHookEx(g_hHook);
    		g_hHook = NULL;
    	}
    	return bOk;
    }
    

    9.3.5 HOOK实例——进程保护

    每当系统内有进程调用了TerminateProcess函数,程序就将它捕获,在输出窗口显示出调用进程主模块的镜像文件名和传递给TerminateProcess的两个参数。如果用户选中了禁止执行复选框,替换TerminateProcess的自定义函数(代理函数)仅仅返回TRUE,而不调用原API函数,从而达到保护进程不被非法关闭的目的;如果没有选中,代理函数将会调用原来的API,允许函数执行。

    程序的核心实现在09HookTermProLib工程中。这个DLL工程除了包含上述CAPIHook类的实现文件外,还包含HookTermProLib.cpp和HookTermProLib.def两个文件。

    // HookTermProLib.cpp文件
    
    #include 
    #include "APIHook.h"
    
    extern CAPIHook g_TerminateProcess;
    
    // 自定义TerminateProcess函数
    BOOL WINAPI Hook_TerminateProcess(HANDLE hProcess, UINT uExitCode)
    {
    	typedef BOOL (WINAPI *PFNTERMINATEPROCESS)(HANDLE, UINT);
    
    	// 取得主模块的文件名称
    	char szPathName[MAX_PATH];
    	::GetModuleFileName(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(::SendMessage(::FindWindow(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);
    
    
    ///
    
    #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);
    }
    
    BOOL WINAPI SetSysHook(BOOL bInstall, DWORD dwThreadId)
    {
    	BOOL bOk;
    	if(bInstall) 
    	{
    		g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, 
    						ModuleFromAddress(GetMsgProc), dwThreadId);
    		bOk = (g_hHook != NULL);
    	} 
    	else 
    	{
    		bOk = ::UnhookWindowsHookEx(g_hHook);
    		g_hHook = NULL;
    	}
    	return bOk;
    }
    
    //------------------------------------------------ HookTermProLib.def文件-------------------------------------------------// 
    EXPORTS
       SetSysHook
    
    SECTIONS
    	YCIShared   Read Write Shared
    

    SetSysHook函数负责安装或者卸载WH_GETMESSAGE类型的钩子。钩子成功安装之后,包含钩子函数GetMsgProc的模块(这里为09HookTermProLib.dll模块)将会被注入到系统内每个接收Windows消息的进程中。

    由于定义了CAPIHook类型的全局变量g_TerminateProcess,此DLL在执行初始化代码的时候将会遍历进程的所有模块,将所有对TerminateProcess函数的调用改为对自定义函数Hook_TerminateProcess的调用。

    1.WM_COPYDATA消息
    Hook_TerminateProcess函数采用了发送WM_COPYDATA消息的方式向主程序传递数据。这是系统定义的用于在进程间传递数据的消息。wParam参数是发送此消息的窗口句柄,lParam参数是指向COPYDATASTRUCT结构的指针,所要传递的数据都包含在该结构中。

    typedef struct tagCOPYDATASTRUCT 
    {  
        ULONG_PTR dwData; // 传递给对方的自定义数据  
        DWORD cbData; // 以字节为单位,表示lpData所指内存数据块的大小  
        PVOID lpData; // 指向数据内存块的指针 
    } COPYDATASTRUCT, *PCOPYDATASTRUCT; 
    

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

    接收进程在消息映射表中添加一个消息映射项:

    ON_WM_COPYDATA() 
    

    然后重载OnCopyData函数即可处理WM_COPYDATA消息。

    BOOL CMainDialog::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) 
    { 
        GetDlgItem(IDC_HOOKINFO)->SetWindowText((char*)pCopyDataStruct->lpData);  // 检查是否禁止执行  
        BOOL bForbid = ((CButton*)GetDlgItem(IDC_FORBIDEXE))->GetCheck();  
        if(bForbid)  
           return -1;  
        return TRUE; 
    }
    

    这是09HookTermProApp程序处理这个消息的代码。它将接收到的数据显示给用户,再通过用户的选择决定是否禁止函数的执行。

    2.动态调用DLL导出函数
    09HookTermProApp程序没有使用.lib文件,而是在运行期加载09HookTermProLib.dll模块,然后动态调用09HookTermProLib.dll模块的导出函数SetSysHook安装或者卸载钩子。

    09HookTermProApp工程中的SetSysHook函数封装了整个调用过程,实现代码如下。

    BOOL WINAPI SetSysHook(BOOL bInstall, DWORD dwThreadId = 0)
    {
    	typedef (WINAPI *PFNSETSYSHOOK)(BOOL, DWORD);
    
    	// 调试的时候可以这样设置szDll[] = "..//09HookTermProLib//debug//09HookTermProLib.dll";
    	char szDll[] = "09HookTermProLib.dll";
    
    	// 加载09HookTermProLib.dll模块
    	BOOL bNeedFree = FALSE;
    	HMODULE hModule = ::GetModuleHandle(szDll);
    	if(hModule == NULL)
    	{
    		hModule = ::LoadLibrary(szDll);
    		bNeedFree = TRUE;
    	}
    
    	// 获取SetSysHook函数的地址
    	PFNSETSYSHOOK mSetSysHook = (PFNSETSYSHOOK)::GetProcAddress(hModule, "SetSysHook");
    	if(mSetSysHook == NULL) // 文件不正确?
    	{
    		if(bNeedFree)
    			::FreeLibrary(hModule);
    		return FALSE;
    	}
    
    	// 调用SetSysHook函数
    	BOOL bRet = mSetSysHook(bInstall, dwThreadId);
    
    	// 如果有必要,释放上面加载的模块
    	if(bNeedFree)
    		::FreeLibrary(hModule);
    
    	return bRet;
    }
    

    09HookTermProApp程序在弹出主对话框前后安装、卸载钩子,如下面代码所示。

    BOOL CMyApp::InitInstance()
    {
    	// 安装钩子
    	if(!SetSysHook(TRUE, 0))
    		::MessageBox(NULL, "安装钩子出错!", "09HookTermProApp", 0);
    	// 显示对话框
    	CMainDialog dlg;
    	m_pMainWnd = &dlg;
    	dlg.DoModal();
    	// 卸载钩子
    	SetSysHook(FALSE);
    
    	return FALSE;
    }
    
  • 相关阅读:
    【入门-04】中断系统
    GO语言篇之交叉编译
    Java进阶(十一)缓冲流
    3、组件和容器
    TouchGFX界面开发 | 按钮控件应用示例
    百度智能云千帆大模型平台再升级,SDK版本开源发布!
    【qml学习笔记】QML与C++的交互
    STM8S-----选项字节
    文心一言,通营销之学,成一家之言,百度人工智能AI大数据模型文心一言Python3.10接入
    排序算法的总结
  • 原文地址:https://blog.csdn.net/qq_36314864/article/details/127109556