一、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;
}
//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;
}
注:内核地址空间是唯一的,所以内核中的所有hook操作对所有的进程都有效。
Hook的作用:对目标函数的执行内容进行拦截、复制、修改和记录。
Hook的本质:插入特定的代码,然后干扰程序原本的执行流程。
Hook的实现方式:
- Address Hook:修改数据实现hook;
- inline Hook:直接修改函数内部的指令实现hook。
注:Address Hook是原子操作不用担心多线程执行目标函数因修改数据引发的错误,但是inline Hook存在该问题,所以修改前最好挂起所有其他的线程。
二、注入Hook
(一) 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; }
(二)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
)
{
//这里我省略了修改掉的指令,因为参数都保存在栈,这几条指令是修改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函数修改该地址区域的权限,必须具备可写权限。
注:jmp指令和call指令占5个字节。
