Hook(钩子)
简介
Hook:意为钩子。
在技术中用途多样化,但多数都用来改变原有程序执行顺序或拦截原程序指定信息、代码或数据。
- 这技术其实无处不在,比如我们所熟知的任意杀毒软件中。这些杀软在各种敏感的系统函数中,下了各种钩子。当有程序或病毒调用该函数时,首先会经过杀软的检测函数。如果正常则放行,如不正常则阻止并提示。(当然这只是部分应用,真实场景下对抗是很复杂的)
- 或在我们熟知的外挂中,通过各种hook技术达到无敌、锁头等功能实现。(外挂其实是在调用当前游戏自己的函数功能,而不是外挂作者自己去实现一个具体功能)
本篇文章复现及讲解几个主要的hook技术。 - IATHook(IAT表Hook)
- inlineHook(内联Hook)
- MessageHook(系统消息钩子)
IATHook
通过修改导入表中函数地址达到Hook目的,一般用来修改指定函数。
缺点就是局限性较高,只能修改对应一些函数地址。
本次目标在windows弹窗函数上下钩子:Messagebox
代码实现:
//初始化钩子(解析IAT表保存原函数地址)
void InitHook()
{
HMODULE exe_model = GetModuleHandle(NULL);//获得本模块句柄
char* base = (char*)exe_model;//该模块进程的首地址(相当于是ImageBase)
PIMAGE_DOS_HEADER dosHead = (PIMAGE_DOS_HEADER)base;//获得Dos头
PIMAGE_NT_HEADERS ntHead = (PIMAGE_NT_HEADERS)(dosHead->e_lfanew + base);//获得NT头
PIMAGE_OPTIONAL_HEADER optHead = &ntHead->OptionalHeader;//获得可选PE头
PIMAGE_DATA_DIRECTORY import_directory = &optHead->DataDirectory[1];//拿到IAT表的内存偏移(RVA)
//获得IAT表的首地址
PIMAGE_IMPORT_DESCRIPTOR import_table = (PIMAGE_IMPORT_DESCRIPTOR)(import_directory->VirtualAddress + base);
while (import_table->Name != 0)//循环表内所有模块的名字,以0结尾
{
PCHAR dllname = (PCHAR)(import_table->Name + base);//获得表内dll的名字的首地址
if (strcmp(dllname, "USER32.dll") == 0)//判断是否存在USER32这个dll
{
PDWORD iat = (PDWORD)(import_table->FirstThunk + base);//获得USER32内的函数名称地址
for (size_t i = 0; iat[i] != 0; i++)//循环,函数名称地址以0结尾
{
if (iat[i] == (DWORD)MessageBoxW)//判断是否是MessageBoxW这个函数
{
OldMessagebox = iat + i;//将MessageBoxW的地址保存下来
break;
}
}
break;
}
import_table++;
}
}
//重新实现一个自己的messagebox函数
int WINAPI MyMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
lpText = L"IATHook -by、my";//弹窗内容修改
lpCaption = L"提示";//修改弹窗的标题
OffHook(); //卸载一次钩子
int result = MessageBoxW(hWnd, lpText, lpCaption, uType);
SetHook(); //将钩子设置回去
return result; //让返回值正常返回
}
//设置钩子
void SetHook()
{
PUCHAR Address = (PUCHAR)OldMessagebox;//PUCHAR无符号char类型
DWORD OldProtect = 0;//定义一个属性,用来保存旧属性
VirtualProtect(Address, 5, PAGE_EXECUTE_READWRITE, &OldProtect);//将虚拟内存的属性修改为可读可写
*OldMessagebox = (DWORD)MyMessageBoxW;//将系统MessageBox的地址变成我自己的函数地址
VirtualProtect(Address, 5, OldProtect, &OldProtect);//将内存恢复回原来的属性
}
//卸载钩子
void OffHook()
{
DWORD OldProtect = 0;//修改保护属性
VirtualProtect((LPVOID)OldMessagebox, 5, PAGE_EXECUTE_READWRITE, &OldProtect);
*OldMessagebox = (DWORD)MessageBoxW;
//还原保护属性
VirtualProtect((LPVOID)OldMessagebox, 5, OldProtect, &OldProtect);
}
最终hook成功效果
hook前提示
:
hook后提示
:
解析
根据代码,我们做了三件事。
1、保存原来的系统函数(messagebox)地址
2、重写一个我们自己的messagebox函数
3、将我们自己的messagebox函数地址替换到IAT表中
分析一下看看具体:
inlineHook(内联钩子)
内联钩子,也称自定义钩子。
自由度高,可以在任何地址上下钩子。缺点就是会修改原程序字节,可能会被检查到。
步骤:
- 组装需要跳转的数据(JMP xxx)
- 计算偏移
- 修改目标地址内存属性
- 写入自己的代码
- 还原代码段属性
- 重新调用被覆盖的代码
这里还是以系统弹窗函数messagebox为例。将messagebox的头几个字节修改为跳转到我们自己的函数地址上
代码实现:
// 目标 钩住 MessageBoxW 这个函数
int WINAPI MyMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
// 修改了参数
lpText = L"被InlineHook了";
lpCaption = L"提示";
// 如果不还原原来的字节码,先还原原来的数据
OffHook();
// call MessageBoxW 还是跳回当前函数,就无限套娃了
int result = MessageBoxW(hWnd, lpText, lpCaption, uType);
// 继续加钩子,再吧 MessageBoxW 的前五个字节修改
OnHook();
return result;
}
// 初始化函数
// void InitHook(char* dllname,char* funname,int loc,int funadd)
void InitHook()
{
// 保存原来的函数地址
OldMessageBox = (DWORD)MessageBoxW;
// 计算偏移
*((PDWORD)Offset) = (DWORD)MyMessageBoxW - OldMessageBox - 5;
// 保存原来的数据
memcpy_s(OldOpcode, 5, (PUCHAR)OldMessageBox, 5);
}
// 安装钩子函数
void OnHook()
{
// 找到 MessageBoxW 修改前五个字节
PUCHAR Address = (PUCHAR)OldMessageBox;
// 修改成 jmp MyMessageBoxW
// 之后 MessageBox 的前五个字节就是 jmp MyMessageBoxW
// call MessageBoxW 跳转到我自己的函数中
// 修改保护属性
DWORD OldProtect = 0;
VirtualProtect(Address, 5, PAGE_EXECUTE_READWRITE, &OldProtect);
// 修改字节
Address[0] = 0xE9;
Address[1] = Offset[0];
Address[2] = Offset[1];
Address[3] = Offset[2];
Address[4] = Offset[3];
// 还原保护属性
VirtualProtect(Address, 5, OldProtect, &OldProtect);
}
// 卸载钩子函数
void OffHook()
{
// 修改保护属性
DWORD OldProtect = 0;
VirtualProtect((LPVOID)OldMessageBox, 5, PAGE_EXECUTE_READWRITE, &OldProtect);
memcpy_s((PUCHAR)OldMessageBox, 5, OldOpcode, 5);
// 还原保护属性
VirtualProtect((LPVOID)OldMessageBox, 5, OldProtect, &OldProtect);
}
Hook成功效果
Hook前提示
:
Hook后提示
:
内存解析
查看内存,在我们hook瞬间内存里发生了什么
未hook前地址指向正常的系统函数
:
hook后,我们修改了函数头几个字节。在正常调用messagebox函数时直接跳转到我们函数上
:
跟进一步查看
:
:
这样下来内联hook已经完成了
试想一下,我们既然可以破坏修改任意字节。那么如果这个函数是一个游戏里的扣血函数或是其他功能函数,我们是不是可以把扣除血量改为加血量。无敌效果是不是就实现了(当然不会这么简单)
MessageHook
系统提供的消息Hook机制
Windows操作系统是以事件驱动的。事件被包装成了消息发送给窗口,比如点击菜单,按钮,移动窗口,按下键盘,正常消息:
● 当按下键盘,产生一个消息,按键消息加入到系统消息队列
● 操作系统从消息队列取出消息,添加到相应的程序的消息队列中
● 应用程序使用消息泵从自身的消息队列中取出消息 WM_KEYDOWN,调用消息处理函数
● 我们可以在系统消息队列到程序消息队列之间添加消息钩子,从而使得在系统消息队列消息发给应用程序之前捕获到消息。
● 可以多次添加钩子,从而形成一个钩子链,可以依次调用函数。
● 消息钩子是windows操作系统提供的机制,SPY++截获窗口消息的功能就是基于这样的机制
这里我们实现一个监控键盘输入的程序
代码实现:
// 消息钩子的回调函数
LRESULT CALLBACK HookProc(int code, WPARAM wParam, LPARAM lParam)
{
// 判断是否wParam与lParam都有键盘消息,是的话则执行打印操作
if (code == HC_ACTION) {
// 将256个虚拟键的状态拷贝到指定的缓冲区中,如果成功则继续
BYTE KeyState[256] = { 0 };
if (GetKeyboardState(KeyState)) {
// 得到第16–23位,键盘虚拟码
LONG KeyInfo = lParam;
UINT keyCode = (KeyInfo >> 16) & 0x00ff;
WCHAR wKeyCode = 0;
ToAscii((UINT)wParam, keyCode, KeyState, (LPWORD)&wKeyCode, 0);
// 将其打印出来
WCHAR szInfo[512] = { 0 };
swprintf_s(szInfo, _countof(szInfo), L"Hook_%c", (char)wKeyCode);
OutputDebugString(szInfo);
return 0;
}
}
return CallNextHookEx(g_hook, code, wParam, lParam);
}
// 安装钩子的函数
BOOL InstallHook(DWORD threadid)
{
g_hook = SetWindowsHookEx(
WH_KEYBOARD, // 要钩取的消息类型
HookProc, // 消息钩子的回调函数
g_module, // 包含回调函数的模块句柄
threadid // 要钩取的线程ID,如果填 0 就是钩住所有线程
);
if (g_hook == NULL)
{
return FALSE;
}
else
{
return TRUE;
}
}
// 卸载钩子函数
BOOL UnInstallHook()
{
if (g_hook == NULL)
{
return FALSE;
}
else
{
return UnhookWindowsHookEx(g_hook);
}
}
Hook成功效果
利用debugview等系统消息捕捉工具,实现监控我们的键盘输入。
结尾
既然能攻击,那防守肯定是没问题的。解决问题的方法很多种
其中一种解决办法:如果我们把关键的代码去计算一个crc32值单独开一个线程不断去扫去校验。只要crc32值不对劲,立马终止程序。其实就解决了这个问题
技术是个双刃剑,怎么用取决自己。
- 本篇文章不鼓励任何破坏行为,仅做参考和学习