本环境是蛇矛实验室基于”火天网演攻防演训靶场”进行搭建,通过火天网演中的环境构建模块,可以灵活的对目标网络进行设计和配置,并且可以快速进行场景搭建和复现验证工作。
前言
Hook中文译为“钩子”或“挂钩”,这很容易联想到钓东西,好比钓鱼,但将其比作“网”更合适,在安全开发的过程中,Hook技术主要用于对程序的运行流程进行控制和拦截,对特定的消息或动作进行过滤。
Hook原理
在真正执行原始API之前,对程序流程进行拦截,使其先执行自定义的代码后,再执行原始API调用流程。
Hook分类
Hook根据其作用的权限,可分为应用层(R3)钩子和内核(R0)钩子,本文主要讲解应用层钩子。
从代码实现角度,可将R3 Hook分为以下几类:
基于地址修改,比如IAT Hook。
基于代码修改,比如Inline Hook。
基于异常或调试,比如VEH Hook。
由于篇幅有限,本文只包含部分Hook技术。
IAT Hook
IAT(import address table,导入地址表)是指PE文件格式中的一个表结构,说到IAT就离不开导入表,在实际的开发过程中,难免会使用到Windows API,这些API的代码保存在Windows提供的不同的DLL(动态链接库)文件中,DLL将这些API导出,在可执行程序中使用到其他DLL的代码或数据时,编译器会将这些导入的信息填充到可执行程序的导入表中。当可执行程序运行时,系统会将可执行程序和其依赖的DLL加载到内存中,其中windows加载器会定位所有导入函数的地址并将定位到的地址填充到IAT中供其使用,Windows加载器定位这些函数的地址需要依赖PE文件中的导入表,其中导入表存放了所使用到的DLL文件和导入的函数名称和序号信息。
实现原理
通过替换IAT表中函数的原始地址从而实现Hook。
实现步骤
以Hook User32!MessageBoxA为例:
1. 定义基于User32!MessageBoxA的函数原型的函数指针;
2. 获取User32!MessageBoxA的函数地址并保存;
3. 创建HookedMessageBoxA函数(函数原型同User32!MessageBoxA一样),以拦截程序对User32!MessageBoxA的调用:
先执行自定义的代码;
再执行原始User32!MessageBoxA函数。
4. 解析导入表,并在IAT中定位User32!MessageBoxA的位置;
5. 使用HookedMessageBoxA函数的地址替换IAT中User32!MessageBoxA的地址。
实现代码
#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <dbghelp.h>
#pragma comment (lib, "dbghelp.lib")
// 1. 定义基于User32!MessageBoxA的函数原型的函数指针
using MessageBoxT = int (WINAPI*)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
// 2. 获取User32!MessageBoxA的函数地址并保存;
MessageBoxT OriginalMessageBox = MessageBoxA;
// 3. 创建HookedMessageBoxA函数(函数原型同User32!MessageBoxA一样),以拦截程序对User32!MessageBoxA的调用:
int WINAPI HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
// 3.1 先执行自定义的代码
MessageBoxW(0, L"HookedMessageBox() called", L"IAT Hook", 0);
// 3.2 再执行原始User32!MessageBoxA函数
return OriginalMessageBox(hWnd, lpText, lpCaption, uType);
}
/*
4. 解析导入表,并在IAT中定位User32!MessageBoxA的位置;
5. 使用HookedMessageBoxA函数的地址替换IAT中User32!MessageBoxA的地址。
*/
bool SetHook(std::string dllName, std::string origFunc, PROC hookingFunc)
{
ULONG size;
DWORD i;
LPCSTR importDllName = NULL;
HMODULE importDllImageBase = NULL;
// 获取主模块句柄
HMODULE imageBase = GetModuleHandle(NULL);
// 定位主模块的导入表
PIMAGE_IMPORT_DESCRIPTOR importDescTab = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToDataEx(imageBase, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &size, NULL);
// 寻找目标DLL
bool found = false;
for (i = 0; i < size; i++)
{
importDllName = (LPCSTR)importDescTab[i].Name + (DWORD_PTR)imageBase; // 获取导入DLL名称
if (_stricmp(dllName.c_str(), importDllName) == 0)
{
found = true;
break;
}
}
// 没找到目标DLL,返回
if (!found)
return false;
// 找到目标dll
importDllImageBase = GetModuleHandleA(importDllName);
if (!importDllImageBase) return false;
PIMAGE_THUNK_DATA originalFirstThunk = (PIMAGE_THUNK_DATA)((ULONG_PTR)imageBase + importDescTab[i].OriginalFirstThunk); // 定位导入名称表INT
PIMAGE_THUNK_DATA firstThunk = (PIMAGE_THUNK_DATA)((ULONG_PTR)imageBase + importDescTab[i].FirstThunk); // 定位导入地址表IAT
// 寻找Hook的API
while (originalFirstThunk->u1.AddressOfData)
{
PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)imageBase + originalFirstThunk->u1.AddressOfData);
if (_stricmp(origFunc.c_str(), functionName->Name) == 0)
{
// 确保内存可写
DWORD oldProtect = 0;
VirtualProtect((LPVOID)(&firstThunk->u1.Function), 4096, PAGE_READWRITE, &oldProtect);
// 替换IAT中的函数地址
firstThunk->u1.Function = (ULONG_PTR)hookingFunc;
// 恢复内存属性
VirtualProtect((LPVOID)(&firstThunk->u1.Function), 4096, oldProtect, &oldProtect);
}
++originalFirstThunk;
++firstThunk;
}
return true;
}
int main()
{
// Hook前
MessageBoxA(0, "Before Hooking", "IAT HOOKS", 0);
// 进行IAT Hook
SetHook("user32.dll", "MessageBoxA", (PROC)HookedMessageBox);
// Hook后
MessageBoxA(0, "After Hooking", "IAT Hook", 0);
return 0;
}
上述代码第一次调用MessageBoxA时,正常弹出,为了之后在调用MessageBoxA时,先执行自定义的函数代码(HookedMessageBox),首先在进程的IAT中定位到MessageBoxA的地址,这个过程是先通过进程的导入表找到MessageBoxA所在的DLL模块(user32.dll),找到之后,通过INT(导入名称表)得到MessageBoxA函数地址在IAT中的下标,此时使用自定义函数的地址替换掉IAT中MessageBoxA函数的地址,即可达到IAT Hook的效果,Hook之后在调用MessageBoxA时,程序会先执行HookedMessageBox函数,在执行原始的MessageBoxA函数(需要提前获取MessageBoxA的地址)。
效果
Inline Hook
Inline Hook实际上是一种通过修改机器码的方式来实现Hook的技术。
实现原理
通过直接修改API函数在内存中对应的二进制代码,通过跳转指令将其代码的执行执行流程改变从而执行用户编写的代码进而进行Inline Hook。
实现步骤
以Hook User32!MessageBoxA为例:
1. 在指定进程中内存中找到MessageBoxA函数地址,并保存函数头部若干字节(用于后续unpatch);
2. 创建HookedMessageBoxA函数(函数原型同User32!MessageBoxA一样),以拦截程序对User32!MessageBoxA的调用:
先执行自定义的代码;
恢复先前保存的MessageBoxA原始字节;
执行原函数
再次对MessageBoxA进行Hook;
3. 构造跳转指令,用于后续替换MessageBoxA函数代码头部字节;
4. 修改MessageBoxA函数首地址代码为跳转指令。
实现代码
#include <iostream>
#include <Windows.h>
#if defined(_WIN64)
#define ORIG_BYTES_SIZE 14
#else
#define ORIG_BYTES_SIZE 7
#endif
BYTE OriginalBytes[ORIG_BYTES_SIZE]{}; // 用于保存MessageBoxA的部分原始代码字节
BYTE PatchBytes[ORIG_BYTES_SIZE]{}; // 构造的跳转指令
using MessageBoxAT = int (WINAPI*)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
MessageBoxAT OriginalMessageBox = nullptr;
int WINAPI HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
// 执行自定义的代码
SIZE_T bytesOut = 0;
MessageBoxW(0, L"HookedMessageBox() called", L"Inline Hook", 0);
// unpatch MessageBoxA
WriteProcessMemory(GetCurrentProcess(), (LPVOID)OriginalMessageBox, OriginalBytes, sizeof(OriginalBytes), &bytesOut);
// 调用原来的MessageBoxA
int result = MessageBoxA(NULL, lpText, lpCaption, uType);
// 再次patch MessageBoxA
WriteProcessMemory(GetCurrentProcess(), OriginalMessageBox, PatchBytes, sizeof(PatchBytes), &bytesOut);
return result;
}
bool SetHook(std::string dllName, std::string origFunc, FARPROC hookingFunc)
{
SIZE_T bytesIn = 0;
SIZE_T bytesOut = 0;
// 保存MessageBoxA原始地址
OriginalMessageBox = (MessageBoxAT)GetProcAddress(GetModuleHandleA(dllName.c_str()), origFunc.c_str());
// 保存MessageBoxA的部分原始代码字节
ReadProcessMemory(GetCurrentProcess(), OriginalMessageBox, OriginalBytes, ORIG_BYTES_SIZE, &bytesIn);
memset(PatchBytes, 0, sizeof(PatchBytes));
#if defined(_WIN64)
/*
JMP [RIP+0];
xFFx25x00x00x00x00
x00x11x22x33x44 x55x66x77
*/
memcpy(PatchBytes, "xFFx25", 2);
memcpy(PatchBytes + 6, &hookingFunc, 8);
#else
/*
mov eax, &hookingFunc
jmp eax
*/
memcpy(PatchBytes, "xB8", 1);
memcpy(PatchBytes + 1, &hookingFunc, sizeof(ULONG_PTR));
memcpy(PatchBytes + 5, "xFFxE0", 2);
#endif
// patch the MessageBoxA
WriteProcessMemory(GetCurrentProcess(), OriginalMessageBox, PatchBytes, sizeof(PatchBytes), &bytesOut);
return true;
}
int main()
{
// Hook前
MessageBoxA(0, "Before Hooking", "Inline Hook", 0);
// 进行Inline Hook
SetHook("user32.dll", "MessageBoxA", (FARPROC)HookedMessageBox);
// Hook后
MessageBoxA(0, "After Hooking", "Inline Hook", 0);
return 0;
}
程序中调用了2次MessageBoxA,第一次调用时未被挂钩,之后Inline Hook方式使用对MessageBoxA进行挂钩,当之后再次调用MessageBoxA时,程序会首先进入自写函数(HookedMessageBox)中,在该函数中,自定义的代码部分使用MessageBoxW弹出内容,随后修复MessageBoxA被修改的字节代码后,开始执行原始MessageBoxA代码。
效果
VEH Hook
VEH Hook是一种基于异常处理的Hook手段,通过主动触发异常从在获取程序控制权来达到Hook的手段。其中VEH(Vectored Exception Handler,向量化异常处理)是Windows中处理异常的一种方式。
实现原理
由于VEH的异常处理发生在之前,所以通过`主动抛出异常,使程序触发异常,进而使控制权交给异常处理例程的这一系列操作来实现Hook。
实现步骤
以Hook User32!MessageBoxA为例:
1. 获取MessageBoxA地址,并保存;
2. 安装VEH异常处理程序,编写VEHHandler(VEH的异常处理函数);
3. 设置钩子:人为在Hook点构造异常(比如修改目标函数第一个字节位0xCC等),并保存触发异常的地址等信息;
4. 在VEHHandler函数内部修改目标函数原始流程,并在执行完毕后主动修复异常。
实现代码
#include <Windows.h>
#include <iostream>
// 获取目标函数的地址
ULONG_PTR OriginalMessageBox = NULL;
struct EXCEPTION_HOOK
{
ULONG_PTR address; // 用来记录异常产生的地址,后面将用来确保是我们人为构造的异常
BYTE originalBytes; // 用来记录原始目标函数的第一个字节
};
EXCEPTION_HOOK HookInfo;
// 异常处理函数
// 用来修改目标函数原始流程,并在执行完我们功能后修复异常
LONG NTAPI VEHHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT && // 异常类型为断点异常
(ULONG_PTR)ExceptionInfo->ExceptionRecord->ExceptionAddress == HookInfo.address) // 发生异常的地址为我们主动构造的异常地址
{
// 在这里编写自定义的代码,或者修改Hook API的相关参数
MessageBoxW(0, L"VEHHandler() called", L"VEH Hook", 0);
// 解除钩子
DWORD oldProtect = 0;
VirtualProtect((LPVOID)HookInfo.address, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)HookInfo.address = HookInfo.originalBytes;
VirtualProtect((LPVOID)HookInfo.address, 1, oldProtect, &oldProtect);
return EXCEPTION_CONTINUE_EXECUTION; // 回到异常发生的地方,由于已经修复了异常问题,所以之后能够正确执行
}
return EXCEPTION_CONTINUE_SEARCH; // 向上继续寻找异常处理程序
}
void SetHook(ULONG_PTR address)
{
AddVectoredExceptionHandler(1, VEHHandler); // 添加VEH的异常处理函数
HookInfo.address = address; // 保存目标函数发生异常的地址
HookInfo.originalBytes = *(BYTE*)address; // 保存目标函数原始的第一个字节
DWORD oldProtect = 0;
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(UCHAR*)address = 0xCC; // 人为构造异常,将目标函数代码处的第一个字节改为0xCC
VirtualProtect((LPVOID)address, 1, oldProtect, &oldProtect);
}
int main()
{
// 保存MessageBoxA原始地址
OriginalMessageBox = (ULONG_PTR)GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
MessageBoxA(0, "Before Hooking", "VEH Hook", 0);
SetHook(OriginalMessageBox);// 安装钩子用以触发异常
MessageBoxA(0, "After Hooking", "VEH Hook", 0);
return 0;
}
在上述代码中,先保存了MessageBoxA函数在当前进程中的地址,之后第一次调用MessageBoxA,此时该函数还没有被Hook,随后为了人为构造异常,将MessageBoxA函数代码的第一个字节修改为0xCC,当之后再次调用MessageBoxA后,程序将会触发0xCC异常,由于程序中添加了VEH异常处理,那么程序将跳转到VEHHandler(自写的异常处理函数中),在该函数代码中,首先过滤得到主动触发的异常,满足的条件下,开始执行自定义的代码,这里为了说明,使用MessageBoxW弹出对话框,由于异常被程序接管,所以在异常处理函数中,执行完自定义的代码后,需要修复异常,进而返回原始触发异常的位置继续执行。
效果
PS:通常来说,我们将Hook的功能代码编写进一个DLL文件中,在将该DLL文件通过进程注入的方式注入到需要改变程序流程的进程中来达到目的。
丈八网安蛇矛实验室成立于2020年,致力于安全研究、攻防解决方案、靶场对标场景仿真复现及技战法设计与输出等相关方向。团队核心成员均由从事安全行业10余年经验的安全专家组成,团队目前成员涉及红蓝对抗、渗透测试、逆向破解、病毒分析、工控安全以及免杀等相关领域。
原文始发于微信公众号(蛇矛实验室):安全开发之应用层Hook技术