Windows上的Hook学习
一、概述
Hook的作用:对目标函数的执行内容进行拦截、复制、修改和记录。
Hook的本质:插入特定的代码,然后干扰程序原本的执行流程。
Hook的实现方式:
- Address Hook:修改数据(如函数地址表)实现hook;
- inline Hook:直接修改函数内部的指令实现hook。
PS:Address Hook是原子操作不用担心多线程执行目标函数因修改数据引发的错误,但是inline Hook存在该问题,所以修改前最好挂起所有其他的线程。
二、Windows中常指的Hook--系统钩子
全局钩子,大多需要借助一个dll(钩子的安装和卸载以及钩子处理函数),因为不同的进程间不能随意相互访问内存空间,所以通常借助dll。如果只是hook本进程的消息,可以不用dll。通过函数SetWindowsHookEx来向操作系统申请安装钩子,函数UnhookWindowsHookEx来卸载。关于二次hook的函数地址问题,所有Hook依次调用它Hook前的Address,形成了一个调用链。
代码如下:
// dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "stdafx.h" #include <Windows.h> #include <fstream> #include <TlHelp32.h> using namespace std; extern "C" _declspec(dllexport) void InstallHook(); extern "C" _declspec(dllexport) void UnInstallHook(); HHOOK hHook = NULL; HMODULE g_hModule = NULL; bool KillProcessEx(char* processName) { HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe; pe.dwSize = sizeof(PROCESSENTRY32); if (!Process32First(hSnapShot, &pe)) { return false; } while (Process32Next(hSnapShot, &pe)) { char strTemp[25]={0}; int Length = WideCharToMultiByte(CP_ACP, 0, pe.szExeFile, -1, NULL, 0, NULL, NULL); WideCharToMultiByte(CP_ACP, 0, pe.szExeFile, -1, strTemp, Length, NULL, NULL); bool bIn = false; if (strcmp(processName, strTemp)==0) { bIn = true; } if (bIn) { DWORD dwProcessID = pe.th32ProcessID; HANDLE hProcess = ::OpenProcess(PROCESS_TERMINATE, FALSE, dwProcessID); ::TerminateProcess(hProcess, 0); CloseHandle(hProcess); } } return true; } LRESULT CALLBACK KeyboardProc( _In_ int code, _In_ WPARAM wParam, _In_ LPARAM lParam ) { if(code == HC_ACTION && lParam & 0x80000000) { std::ofstream ofile; //定义输出文件 ofile.open("D:\\keybroad.txt",std::ios::app); if(wParam>=0x30 && wParam<=0x5A) ofile << char(wParam)<<' ';//可显示的键值 else ofile <<std::hex<<"0x"<<wParam<<' '; ofile.close(); } return CallNextHookEx(hHook, code, wParam, lParam); } void InstallHook() { hHook = SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,g_hModule,0); if(hHook) MessageBox(NULL, TEXT("成功安装hook!"), TEXT("提示"), MB_OK); } VOID UnInstallHook() { MessageBox(NULL, TEXT("即将卸载hook!"), TEXT("提示"), MB_OK); if(hHook!=NULL) { if(UnhookWindowsHookEx(hHook)) MessageBox(NULL, TEXT("成功卸载hook!"), TEXT("提示"), MB_OK); } CloseHandle(hHook); //KillProcessEx("Test.exe"); TerminateProcess(GetCurrentProcess(),-1); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: g_hModule = hModule; break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
main.cpp的代码如下:
//Test.cpp #include <Windows.h> int main() { HMODULE hModule = LoadLibraryA("TestDll.dll"); FARPROC pfInstallHook = GetProcAddress(hModule,"InstallHook"); FARPROC pfUninstallHook = GetProcAddress(hModule,"UnInstallHook"); pfInstallHook(); pfUninstallHook(); return 0; }
PS:内核地址空间是唯一的,所以内核中的所有hook操作对所有的进程都有效。
三、Windows中的函数Hook
1.Address Hook
修改各种表:
- IAT(输入表):IAT具体指某个dll模块中的IAT,存放的是函数的地址,必须以静态调用的方式才会被hook,动态调用不受影响。
- EAT(输出表):它存放的是函数地址的偏移,使用时需要加上模块的基址。在hook后,所有试图通过EAT获取目标函数地址的操作都会受到影响。同样仅对静态调用有效。
- IDT(系统的中断描述符表):是操作系统用于处理中断机制,当中断发生操作系统应该交给谁处理。该表的基址存放在idtr寄存器中,表内项目数存放在idtl寄存器中。
- SSDT(系统服务描述符表):程序调用API转入内核处理前首先要用到。
- C++虚函数表:父类中定义了虚函数就会有一个虚函数指针指向虚函数表,若子类中也定义了虚函数,则它也有一个自己的虚函数表。(简而言之,每个使用虚函数的类都被赋予自己的虚表。)该表仅是编译器在编译时设置的静态数组。一个虚拟表为该类的对象可以调用的每个虚拟函数包含一个条目。该表中的每个条目都只是一个函数指针,指向该类可访问的最衍生函数。创建类实例时,编译器会自动设置* __ vptr指针,以便它指向该类的虚拟表。
IAT hook原理:通过获取IAT所在内存页的可写权限,修改想要hook的函数在IAT中的函数地址为自己定义的函数地址,从而实现调用自己定义函数的目的。
#include <Windows.h>
#include <stdio.h>
#include <Dbghelp.h>
#pragma comment(lib,"imagehlp.lib") //以MessageBoxA的原型定义一个函数指针类型 typedef int (WINAPI *PFN_MessageBoxA)( HWND hWnd, // handle of owner window LPCTSTR lpText, // address of text in message box LPCTSTR lpCaption, // address of title of message box UINT uType // style of message box ); PFN_MessageBoxA OldMessageBox=NULL; //指向IAT中pThunk的地址 PULONG_PTR g_PointerToIATThunk = NULL; int WINAPI MyMessageBoxA( HWND hWnd, // handle of owner window LPCTSTR lpText, // address of text in message box LPCTSTR lpCaption, // address of title of message box UINT uType // style of message box ) { //在这里,你可以对原始参数进行任意操作 int ret; char newText[1024]={0}; char newCaption[256]="pediy.com"; //为防止原函数提供的缓冲区不够,这里复制到我们自己的一个缓冲区中再进行操作 lstrcpy(newText,lpText); lstrcat(newText,"\n\tMessageBox Hacked by pediy.com!");//篡改消息框内容 uType|=MB_ICONERROR;//增加一个错误图标 ret = OldMessageBox(hWnd,newText,newCaption,uType);//调用原MessageBox,并保存返回值 //调用原函数之后,可以继续对OUT(输出类)参数进行干涉,比如网络函数的recv,可以干涉返回的内容 return ret; } BOOL InstallHook( HMODULE hModToHook, //待Hook的模块基址 char* szModuleName, //目标DLL名字 char* szFuncName, //目标函数名字 PVOID DetourFunc, //Detour函数 PULONG_PTR* pThunkPointer, ULONG_PTR* pOriginalFuncAddr ) { PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor; PIMAGE_THUNK_DATA pThunkData; ULONG ulSize; HMODULE hModule = 0; ULONG_PTR TargetFunAddr; PULONG_PTR lpAddr; char* szModName; BOOL result = FALSE; BOOL bRetn = FALSE; hModule = LoadLibrary(szModuleName); TargetFunAddr = (ULONG_PTR)GetProcAddress(hModule, szFuncName); pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(hModToHook, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
while(pImportDescriptor->FirstThunk) { szModName = (char*)((PBYTE)hModToHook + pImportDescriptor->Name); if (stricmp(szModName, szModuleName) != 0) { pImportDescriptor++; continue; } pThunkData = (PIMAGE_THUNK_DATA)((BYTE*)hModToHook + pImportDescriptor->FirstThunk); while (pThunkData->u1.Function) { lpAddr = (ULONG_PTR*)pThunkData; if ((*lpAddr) == TargetFunAddr) { DWORD dwOldProtect; MEMORY_BASIC_INFORMATION mbi; VirtualQuery(lpAddr,&mbi,sizeof(mbi)); bRetn = VirtualProtect(mbi.BaseAddress,mbi.RegionSize,PAGE_EXECUTE_READWRITE,&dwOldProtect); if (bRetn) { if (pThunkPointer != NULL) { *pThunkPointer = lpAddr; } if (pOriginalFuncAddr != NULL) { *pOriginalFuncAddr = *lpAddr; } *lpAddr = (ULONG_PTR)DetourFunc; result = TRUE; VirtualProtect(mbi.BaseAddress, mbi.RegionSize, dwOldProtect, 0); } break; } pThunkData++; } pImportDescriptor++; } FreeLibrary(hModule); return result; } void IAT_InstallHook() { BOOL bResult = FALSE; HMODULE currectExe = GetModuleHandle(NULL); PULONG_PTR pt; ULONG_PTR OrginalAddr; OldMessageBox = MessageBoxA; bResult = InstallHook(currectExe,"user32.dll","MessageBoxA",MyMessageBoxA,&pt,&OrginalAddr); } VOID IAT_UnInstallHook() { DWORD dwOLD; MEMORY_BASIC_INFORMATION mbi; if (g_PointerToIATThunk) { //查询并修改内存页的属性 VirtualQuery((LPCVOID)g_PointerToIATThunk,&mbi,sizeof(mbi)); VirtualProtect(mbi.BaseAddress,mbi.RegionSize,PAGE_EXECUTE_READWRITE,&dwOLD); //将原始的MessageBoxA地址填入IAT中 *g_PointerToIATThunk = (ULONG)OldMessageBox; //恢复内存页的属性 VirtualProtect(mbi.BaseAddress,mbi.RegionSize,dwOLD,0); } } int main() { MessageBoxA(NULL,"Before install","IAT",MB_OK); IAT_InstallHook(); MessageBoxA(NULL,"Correct!","IAT",MB_OK); IAT_UnInstallHook(); MessageBoxA(NULL,"After uninstall","IAT",MB_OK); return 0; }
2.Inline Hook
原理:通过获取目标函数的地址,修改开头的指令为jmp指令,跳向我们事先定义好的目标函数Detour的地址,在完成我们想要的操作后调用函数Trampoline(该函数的作用是跳回原函数并执行原本修改掉的指令,完成原函数功能。)。
#include <Windows.h> #include <iostream> using namespace std; ULONG64 g_JmpAddr; int WINAPI OrignalMessageBoxA( _In_opt_ HWND hWnd, _In_opt_ LPCTSTR lpText, _In_opt_ LPCTSTR lpCaption, _In_ UINT uType ) { //这里我nop掉了要修改掉的指令,因为参数都保存在栈,这几条指令是修改ebp的,若执行会导致参数丢失。 __asm { nop nop nop jmp g_JmpAddr } return 0; } int WINAPI My_MessageBoxA( _In_opt_ HWND hWnd, _In_opt_ LPCTSTR lpText, _In_opt_ LPCTSTR lpCaption, _In_ UINT uType ) { cout<<"hack success!"<<endl; lpText = "Hacker!"; OrignalMessageBoxA(hWnd,lpText,lpCaption,uType); return 0; } int main() { PBYTE AddrMesssageBox = (PBYTE)GetProcAddress(GetModuleHandle("user32.dll"),"MessageBoxA"); g_JmpAddr = (ULONG)AddrMesssageBox+5; BYTE newEntry[5]={0}; newEntry[0] = 0xe9; //这里的5是指jmp指令占的5个字节,求的是偏移,在当前位置向上或下偏移多少位 *(ULONG*)(newEntry+1) = (ULONG)My_MessageBoxA - (ULONG)AddrMesssageBox -5; DWORD OldProtect; MEMORY_BASIC_INFORMATION MBI = {0}; VirtualQuery((LPCVOID)AddrMesssageBox,&MBI,sizeof(MEMORY_BASIC_INFORMATION)); VirtualProtect(MBI.BaseAddress,5,PAGE_EXECUTE_READWRITE,&OldProtect); memcpy(AddrMesssageBox,newEntry,5); VirtualProtect(MBI.BaseAddress,5,OldProtect,&OldProtect); MessageBoxA(0,"OK","Tip",MB_OK); return 0; }
安装hook的过程:构造好jmp指令后,调用memcpy函数复制到目标函数中,这里需要使用VirrtualProtect函数修改该地址区域的权限,必须具备可写权限。
PS:jmp指令和call指令占5个字节。