家里不能上网,实在无聊,翻遍了机子里所有的东东。无意中找到一些以前 没弄懂的、后来一直没有去弄的代码,呵呵~~看来是该做个了断的时候了。API Hook就 是其中之一。 下面就说说API Hook的原理、实现以及自己的分析和实际运用中的问题。 一、什么是API Hook 见下图所示,API Hook就是对API的正常调用起一个拦截或中间层的作用,这样可以 在调用正常的API之前得到控制权,执行自己的代码。其中Module指映射到内存中的可执 行文件或DLL。 module0 module1 | | CALL module1!API001 --------------------------------->| API001 |<-------------------------------------------| | | API215 |<----------------------------------CALL module0!API215 |------------------------------------------->| | | * * vs. module0 Hooooks.dll module1 | | | CALL module1!API001 -------->API001>----------------->| API001 |<-----------------<HOOOOK<------------------| | | | API215 |<-----------------<API215<---------CALL module0!API215 |------------------>HOOOOK>----------------->| | | | * * * 二、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 -------------------- /-->---------------- ---------- ---------------- <--/ | OriginalFirstThunk |--/ |IMAGE_THUNK_DATA|-->|01| 函数1 |<--|IMAGE_THUNK_DATA| | |--------------------| |----------------| |----------| |----------------| | | TimeDateStamp | |IMAGE_THUNK_DATA|-->|02| 函数2 |<--|IMAGE_THUNK_DATA| | |--------------------| |----------------| |----------| |----------------| | | ForwarderChain | | ... |-->| n| ... |<--| ... | | |--------------------| ---------------- ---------- ---------------- | | Name |------>"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调用去了,完成了拦截。不拦截时,再改写回来就是了。 三、实现前的准备 两种拦截方法,最终目的都是使程序对欲拦截API的调用跳转到自己的API。所以我们的API代码对 欲拦截进程必须是可见的,即我们的代码要映射到欲拦截进程的地址空间中。 在《隐藏进程》一文中我介绍了远程线程注入代码的技术,这里我们可以采用这种方法向欲拦截进 程中注入我们的API代码。同样有两种注入方式,一种是,直接将代码WriteProcessMemory到欲拦截进 程中,写入的代码包括我们的API代码和远程线程的入口函数代码。这种的缺点是有一些细节问题要解 决,如参数传递、写入代码大小的确定等并且由于多了一个远程线程效率不是很高,如果要拦截所有的 进程,即必须在每个进程中注入代码、插入线程。另一种是,注入DLL,远程线程入口函数为LoadLirary, 当然也存在效率的问题,但免去了一些麻烦。 这里我们主要介绍通过设置钩子(Hook)来自动注入DLL到欲拦截进程。先简单说明一下钩子是怎么回事。 Hook指出了系统消息处理机制。利用Hook,可以在应用程序中安装子程序监视系统和进程之间的消息 传递,这个监视过程是在消息到达目的窗口过程之前。系统支持很多不同类型的Hooks,不同的hook提供不 同的消息处理机制。比如,应用程序可以使用WH_MOUSE_hook来监视鼠标消息的传递。系统为不同类型的 Hook提供单独的Hook链。Hook链是一个指针列表,这个列表的指针指向指定的,应用程序定义的,被hook 过程调用的回调函数。当与指定的Hook类型关联的消息发生时,系统就把这个消息传递到Hook过程。一些 Hook过程可以只监视消息,或者修改消息,或者停止消息的前进,避免这些消息传递到下一个Hook过程或 者目的窗口。 为了利用特殊的Hook类型,可由开发者提供了Hook过程,使用SetWindowsHookEx函数来把Hook过程安 装到关联的Hook链。Hook过程必须按照以下的语法: LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam); HookProc是应用程序定义的名字,nCode参数是Hook代码,Hook过程使用这个参数来确定任务。这个参 数的值依赖于Hook类型,每一种Hook都有自己的Hook代码特征字符集。wParam和lParam参数的值依赖于 Hook代码,但是它们的典型值是包含了关于发送或者接收消息的信息。 SetWindowsHookEx函数总是在Hook链的开头安装Hook过程。当指定类型的Hook监视的事件发生时,系统 就调用与这个Hook关联的Hook链的开头的Hook过程。每一个Hook链中的Hook过程都决定是否把这个事件传递 到下一个Hook过程。Hook过程传递事件到下一个Hook过程需要调用CallNextHookEx函数。有些类型Hook的 Hook过程只能监视消息,不管是否调用了CallNextHookEx函数,系统都把消息传递到每一个Hook过程。全局 hook监视同一桌面的所有线程。而特定线程的Hook只能监视单独的线程。全局Hook过程可以被同一桌面的任 何应用程序调用,就象调用线程一样,所以这个过程必须和DLL模块分开。特定线程Hook过程只可以被相关 线程调用。只有在有调试目的的时候才使用全局Hook,应该避免使用,全局Hook损害了系统性能。 本文使用全局的WH_GETMESSAGE Hook,它也是经常用到的Hook,应用程序使用WH_GETMESSAGE Hook来监 视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入, 以及其他发送到消息队列中的消息。关于Hook的详细信息请参考MSDN。 使用SetWindowsHookEx设置全局的WH_GETMESSAGE Hook,传入DLL的映射到内存时的模块句柄(HANDLE) 和Hook过程,这样系统不但会将此DLL映射到当前所有进程的地址空间,并调用DllMain函数,而且也会将 此DLL映射到新创建的进程的地址空间了。也就是自动完成了代码的注入工作,省了很多力气,调用 UnhookWindowsHookEx卸载钩子。 四、具体实现 两种实现模式:一是由一个第三方进程负责钩子的设置和卸载,DLL导出设置和卸载函数;二是由一个 第三方进程向某一个进程插入远程线程、注入DLL,然后由DLL负责钩子的设置和卸载,第三方进程退出。 两种模型的DLL实现差别不大,封装了钩子设置和卸载函数,自己的API的函数等。 先说改写IAT方法: 定义一个保存拦截信息的结构APIHOOK32_ENTRY: typedef struct _APIHOOK32_ENTRY { LPCTSTR pszAPIName; //欲拦截API的函数名 LPCTSTR pszCalleeModuleName;//API所在模块的模块名 PROC pfnOriginApiAddress;//欲拦截API的函数地址 PROC pfnDummyFuncAddress;//我们自己的API的函数地址 HMODULE hModCallerModule; //调用此API的模块名 }APIHOOK32_ENTRY, *PAPIHOOK32_ENTRY; / #include "stdafx.h" #include "apihook32.h" HMODULE hModDLL; HHOOK hHook; APIHOOK32_ENTRY hkA; //钩子过程,直接调用CallNextHookEx,而不做任何处理 //因为我们只是利用设置钩子来映射DLL LRESULT CALLBACK GetMsgProc(int code,WPARAM wParam,LPARAM lParam) { return CallNextHookEx(hHook,code,wParam,lParam); } //我们自己的API函数 int WINAPI MyMessageBoxA(HWND hwnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType) { return MessageBoxA(hwnd,"It's coming from MyMessageBoxA",lpCaption,uType); } //设置全局钩子,自动映射DLL到当前所有进程和新创建的进程 HHOOK InsertDll () { hHook = SetWindowsHookEx(WH_GETMESSAGE,&GetMsgProc,hModDLL,0); return hHook; } BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved { hModDLL = (HMODULE)hModule;//在32位windows系统中,DLL的hModule和hHandle是一回事 hkA.hModCallerModule = NULL; hkA.pszAPIName = "MessageBoxA"; //拦截user32.dll中的MessageBoxA函数 hkA.pszCalleeModuleName = "user32.dll"; hkA.pfnDummyFuncAddress = (PROC) & MyMessageBoxA; hkA.pfnOriginApiAddress = GetProcAddress(GetModuleHandle("user32.dll","MessageBoxA"; switch(ul_reason_for_call) { case DLL_PROCESS_ATTACH: // InsertDll(); SetWindowsAPIHook(&hkW); SetWindowsAPIHook(&hkA); break; case DLL_THREAD_ATTACH: break; case DLL_PROCESS_DETACH: UnhookWindowsAPIHooks(hkW); UnhookWindowsAPIHooks(hkA); // UnhookWindowsHookEx(hHook); break; case DLL_THREAD_DETACH: break; } return TRUE; } / //有了一上对PE文件输入表的分析,我们将可以很好的理解下面的代码 void _SetApiHookUp(PAPIHOOK32_ENTRY phk) { ULONG size; //获取指向PE文件中的Import表中IMAGE_DIRECTORY_DESCRIPTOR数组的指针 PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(phk->hModCallerModule,TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&size); if (pImportDesc == NULL) return; //查找记录,看看有没有我们想要的DLL //pImportDesc->Name为空说明IID数组结束 for (;pImportDesc->Name;pImportDesc++) { //pImportDesc->Name是DLL名字字符串的RVA,加上Module的基址获得有效指针 LPSTR pszDllName = (LPSTR)((PBYTE)phk->hModCallerModule+pImportDesc->Name); if (lstrcmpiA(pszDllName,phk->pszCalleeModuleName) == 0) break; } if (pImportDesc->Name == NULL) { return; } //寻找我们想要的函数 //首先获得IID的FirstThunk指向的IAT PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE)phk->hModCallerModule+pImportDesc->FirstThunk); for (;pThunk->u1.Function;pThunk++) { //ppfn记录了与IAT表项相应的函数的地址 PROC * ppfn= (PROC *)&pThunk->u1.Function; if (*ppfn == phk->pfnOriginApiAddress) { //如果地址相同,也就是找到了我们想要的函数,进行改写,将其指向我们所定义的函数 WriteProcessMemory(GetCurrentProcess(),ppfn,&(phk->pfnDummyFuncAddress),sizeof(phk->pfnDummyFuncAddress),NULL); return; } } } //***************************************************************************************// // SetWindowsAPIHook 挂接WindowsAPI函数 当phk->hModCallerModule == NULL // // 会在整个系统内挂接函数 // // 仿照SetWindowsHookEx 建立 // //***************************************************************************************// BOOL SetWindowsAPIHook(PAPIHOOK32_ENTRY phk) { if (phk->pszAPIName == NULL) { return FALSE; } if (phk->pszCalleeModuleName == NULL) { return FALSE; } if (phk->pfnOriginApiAddress == NULL) { return FALSE; } if (phk->hModCallerModule == NULL) { MEMORY_BASIC_INFORMATION mInfo; HMODULE hModHookDLL; HANDLE hSnapshot; MODULEENTRY32 me = {sizeof(MODULEENTRY32)}; //根据_SetApiHookUp函数在内存中的位置,确定DLL的映射基址,即hModule VirtualQuery(_SetApiHookUp,&mInfo,sizeof(mInfo)); hModHookDLL=(HMODULE)mInfo.AllocationBase; //遍历本进程中的所有Module,除了本DLL模块 hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,0); BOOL bOk = Module32First(hSnapshot,&me); while (bOk) { if (me.hModule!=hModHookDLL) { phk->hModCallerModule = me.hModule; _SetApiHookUp(phk); } bOk = Module32Next(hSnapshot,&me); } return TRUE; } else { _SetApiHookUp(phk); return TRUE; } return FALSE; } //拦截某个某块的API调用 void SetMyHooksHere(APIHOOK32_ENTRY hk,HMODULE hMod) { hk.hModCallerModule = hMod; _SetApiHookUp(&hk); } //卸载API Hook,就是往IAT中写入原地址 BOOL UnhookWindowsAPIHooks(APIHOOK32_ENTRY & hk) { PROC temp; temp = hk.pfnOriginApiAddress; hk.pfnOriginApiAddress = hk.pfnDummyFuncAddress; hk.pfnDummyFuncAddress = temp; return SetWindowsAPIHook(&hk); } /// 第三方进程调用InsertDll注入DLL(设置钩子),调用UnhookWindowsHookEx取消拦截。 下面简单介绍jmp xxxxxxxx方法拦截API: 拦截user32.dll中的MessageBoxA函数 FARPROC g_pfMessageBoxA = NULL; BYTE g_OldMessageBoxACode[5] = {0}, g_NewMessageBoxACode[5] = {0}; DWORD g_dwNewProcessId = 0, g_dwOldProcessId = 0; BOOL g_bHook = FALSE; HMODULE g_hDll = FALSE; HHOOK g_hHook = NULL; BOOL WINAPI Initialize(); BOOL WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType); void WINAPI HookOn(); void WINAPI HookOff(); LRESULT WINAPI Hook(int nCode, WPARAM wParam, LPARAM lParam); BOOL WINAPI InstallHook(); BOOL WINAPI UnInstallHook(); BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved { //char szProcessID[64]; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // _itoa(GetCurrentProcessId(), szProcessID, 10); // MessageBox(NULL, szProcessID, "Remote Dll", MB_OK); if(!g_bHook) { g_hDll = (HMODULE)hModule; InstallHook(); Initialize(); // MessageBox(NULL, "Succeeded!", "Hook On", MB_OK); } // MessageBox(NULL, "Process Attach", "Remote Dll", MB_OK); break; case DLL_THREAD_ATTACH: // _itoa(GetCurrentProcessId(), szProcessID, 10); // MessageBox(NULL, szProcessID, "Remote Dll", MB_OK); break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: // if(g_bHook) // { // HookOff(); // MessageBox(NULL, "Off!", "Hook Off", MB_OK); // UnInstallHook(); // } // break; MessageBox(NULL, "Process Detach", "Remote Dll", MB_OK); break; } return TRUE; } //获得MessageBoxA的地址,然后保存代码起始处的5个字节,并生成跳转代码jmp xxxxxxxx BOOL WINAPI Initialize() { HMODULE hDll = LoadLibrary("user32.dll"; g_pfMessageBoxA = GetProcAddress(hDll, "MessageBoxA";//获得MessageBoxA地址 if(g_pfMessageBoxA == NULL) return FALSE; _asm { lea edi, g_OldMessageBoxACode mov esi, g_pfMessageBoxA cld movsd //将MessageBoxA地址起始的4个字节(dword)写入g_OldMessageBoxACode movsb //将MessageBoxA+4地址起始处的1个字节(byte)写入g_OldMessageBoxACode+4 } //jmp xxxxxxxx的机器码为e9xxxxxxxx,其中e9后的xxxxxxxx为相对跳转偏移,共5个字节 g_NewMessageBoxACode[0] = 0xe9; _asm { lea eax, MyMessageBoxA mov ebx, g_pfMessageBoxA sub eax, ebx sub eax, 5 //获得相对跳转偏移 mov dword ptr [g_NewMessageBoxACode + 1], eax } g_dwNewProcessId = GetCurrentProcessId(); g_dwOldProcessId = g_dwNewProcessId; HookOn(); return TRUE; } int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) { int nRet = 0; char szText[128]; strcpy(szText, lpText); strcat(szText, "/nYou have been Hooked!"; HookOff(); nRet = MessageBoxA(hWnd, szText, lpCaption, uType); HookOn(); return nRet; } void WINAPI HookOn() { g_dwOldProcessId = g_dwNewProcessId; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, g_dwOldProcessId); if(hProcess == NULL) return ; //申请MessageBoxA地址处的写权限,然后写入跳转代码,然后恢复权限 VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, PAGE_READWRITE, &g_dwOldProcessId); WriteProcessMemory(hProcess, g_pfMessageBoxA, g_NewMessageBoxACode, 5, NULL); VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, g_dwOldProcessId, &g_dwOldProcessId); g_bHook = TRUE; } void WINAPI HookOff() { g_dwOldProcessId = g_dwNewProcessId; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, g_dwOldProcessId); if(hProcess == NULL) return ; //写入原MessageBoxA的5个字节代码 VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, PAGE_READWRITE, &g_dwOldProcessId); WriteProcessMemory(hProcess, g_pfMessageBoxA, g_OldMessageBoxACode, 5, NULL); VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, g_dwOldProcessId, &g_dwOldProcessId); g_bHook = FALSE; } LRESULT WINAPI Hook(int nCode, WPARAM wParam, LPARAM lParam) { return CallNextHookEx(g_hHook, nCode, wParam, lParam); } BOOL WINAPI InstallHook() { g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)Hook, g_hDll, 0); if(!g_hHook) { MessageBoxA(NULL, "Set Error!", "ERROR", MB_OK); return FALSE; } return TRUE; } BOOL WINAPI UnInstallHook() { return UnhookWindowsHookEx(g_hHook); } 如果让DLL来负责钩子的设置和卸载,就必须设置映射到所有进程的DLL共享的数据段,因为DLL中的 全局数据,每个映射的DLL副本都有自己的副本,互不干扰。为了同步钩子的设置和卸载工作,我们 可以在DLL中设置一个共享段,如下: #pragma data_seg("shared" //定义段名为shared BOOL g_bHooked = FALSE; DWORD g_dwParentProcessID = 0; //…… #pragma data_seg() #pragma commnet(lib, "/Section:shared, rws";//设置段属性为read,write and shared 这个共享段在所有的DLL映射副本中共享,完成同步工作。 第三方进程向某个进程中注入远程线程,远程线程注入DLL,然后第三方进程退出。同步代码如下: BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved { DWORD dwProcessId = GetCurrentProcessId(); switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: if(!g_bHooked) { g_bHooked = TRUE; g_dw_ParentProcessID = dwProcessId; ...//在这里设置钩子,hMoudle即为DLL的Handle } ...//在这里完成API拦截工作 break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: ...//在这里完成API的恢复工作 if(g_bHooked && (g_dwParentProcessID == dwProcessId)) { g_bHooked = FALSE; g_dwParentProcessID = 0; ...//在这里卸载钩子 } break; } return TRUE; } 以上关于远程线程注入技术请参考我的另一篇《隐藏进程》的文章,以上代码在Win2k Professional+SP2 +Visual C++6.0上测试通过,API的具体参数请参考MSDN。 上面的代码大多是自己机子上的一些零碎代码,加上自己的分析和实际的应用调试,一直找不到方法 卸载DLL,无论是用设置钩子注入DLL,还是用注入远程线程的方法注入DLL。 写玩了,自我感觉写得好烂,也懒得修改,就此收笔。 //结束
|