HOOK技术正如其名,就像是代码中放下的一个“钩子”,它在静静地等待捕获系统中的某个消息或动作。在编程技术中,钩子技术在DOS时代就已经存在了。在windows下,钩子按照实现技术的不同和挂钩位置的不同,其种类也是越来越多,但是设置钩子的本质却是始终不变的。
那么钩子究竟有什么用?它能干的事非常多,例如输入监控、API拦截、消息捕获、改变程序执行流程等。杀毒软件会用HOOK技术钩住一些API函数,比如注册表读写函数,从而防止病毒对注册表的操作;病毒使用HOOK技术有针对性的捕获键盘的输入,从而记录用户的密码;文件系统通过HOOK技术在不改变用户操作的情况下对用户的文件进行透明加密。这些都属于HOOK的技术范畴。
内联HOOK的原理
API函数都保存在操作系统提供的DLL文件中。当在程序中调用某个API函数并运行程序后,程序会隐式地将API函数所在的DLL文件加载入进程中。这样,程序就可以像调用自己的函数一样调用API。大体过程如下图所示:
CreateProcessW()是API 函数,API函数也是程序员的代码编译而成,也有其对应的二进制代码。既然是代码,就可以被修改。通过一种强制性的手段修改API函数在内存中的映像,从而对API 函数进行hook。使用的方法是,直接使用汇编指令的jmp指令将其的代码执行流程改变,进而执行自己的代码。执行完自己的代码后可以选择性地执行其它的代码。
在Windows下,大部分应用程序都是由Explorer.exe进程来创建的。那么只要把Explorer.exe中创建进程的函数CreateProcessW() hook住,就可以自主控制是否让它创建摸个特定进程。执行流程如下:
由于这种方法是直接在程序流程中嵌入jmp指令来改变流程的,所以把它叫做Inline Hook。
在二进制文件中,代码部分都是CPU可以用来执行的机器码,机器码和汇编指令是一一对应的。使用 :jmp 目的地址,该汇编指令的长度为5个字节。具体可以用OD打开任意程序,修改某条指令为jmp格式的指令即可看到效果。其中jmp指令后跟着的是从当前地址到目的地址的偏移量。如下图所示:
准备修改00402390地质处的反汇编代码为jmp指令:
修改后的反汇编代码:
下一个指令的末尾是95,由此可见该jmp命令的长度是5字节。
jmp后的偏移量 = 目标地址 – 原地址 – jcc的指令长度。
下图是,目标地址 – 原地址后的值,由于本次我们使用的jmp指令长度是5,所以在减去5,最后 偏移量 = 11F432E3,正是上面所给出的值(主机使用的是小端字节序)。
注:
小端字节序:低字节存于内存低地址;高字节存于内存高地址。
大端字节序:高字节存于内存低地址;低字节存于内存高地址。
网络字节序:就是大端字节序。规定不同系统间通信一律采用网络字节序。
总结一下,InlineHook的流程大致如下:
1. 构造跳转指令
2. 在内存中找到要HOOK的函数地址,并保存要HOOK位置处的前指令长度字节数内容。
3. 将构造的跳转指令写入需要HOOK 的位置处。
4. 当被HOOK位置被执行时会转到自己的流程中执行。
5. 如果要执行原来的流程,那么取消HOOK,也就是还原被修改的字节。
6. 执行原来的流程。
7. 继续HOOK原来的位置。
下面是一个简单的例子:
首先,新建一个win32下的DLL工程。在工程中添加一个类,用于操作钩子。
.h文件中的主要代码如下:
#include<Windows.h>
class CILHOOK
{
public:
CILHOOK(); //构造
~CILHOOK(); //析构
//HOOK函数
BOOL Hook(LPSTR pszModuleName,
LPSTR pszFuncName,
PROC pfnHookFunc);
//取消HOOK函数
VOID UnHook();
//重新进行HOOK
BOOL ReHook();
private:
PROC m_pfnOrig; //函数地址
BYTE m_bOldBytes[5]; //函数入口代码
BYTE m_bNewBytes[5]; //Inline代码
};
.cpp文件中的主要代码如下:
CILHOOK::CILHOOK()
{
m_pfnOrig = NULL;
ZeroMemory(m_bOldBytes, 5);
ZeroMemory(m_bNewBytes, 5);
}
CILHOOK::~CILHOOK()
{
//取消HOOK
UnHook();
m_pfnOrig = NULL;
ZeroMemory(m_bOldBytes, 5);
ZeroMemory(m_bNewBytes, 5);
}
/*
函数名:Hook
函数功能:对指定模块的函数进行挂钩
参数说明:
pszModuleName:模块名称
pszFuncName:函数名称
pfnHookFunc:钩子函数
*/
BOOL CILHOOK::Hook(LPSTR pszModuleName,
LPSTR pszFuncName,
PROC pfnHookFunc)
{
BOOL bRet = FALSE;
//获取指定模块中函数的地址
m_pfnOrig = (PROC)GetProcAddress(GetModuleHandle(pszModuleName), pszFuncName);
if(m_pfnOrig != NULL)
{
DWORD dwNum = 0;
//将原来的数据存起来
ReadProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bOldBytes, 5, &dwNum);
//构造jmp指令
m_bNewBytes[0] = '\xe9'; //jmp Opcode
//pfnHookFunc是HOOK后的目标地址
//m_pfnOrig是原来的地址
//5是指令长度
*(DWORD *)(m_bNewBytes + 1) = (DWORD)pfnHookFunc - (DWORD)m_pfnOrig - 5;
//将构造好的地址写入该地址处
WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bNewBytes, 5, &dwNum);
bRet = TRUE;
}
return bRet;
}
/*
函数名称:UnHook
函数功能:取消函数的挂钩
*/
VOID CILHOOK::UnHook()
{
if(m_pfnOrig != 0)
{
DWORD dwNum = 0;
//将原来的内容写回hook地址
WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bOldBytes, 5, &dwNum);
}
}
/*
函数名称:ReHook
函数功能:重新对函数进行挂钩
*/
BOOL CILHOOK::ReHook()
{
BOOL bRet = FALSE;
if(m_pfnOrig != 0)
{
DWORD dwNum = 0;
//写入挂钩地址
WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bNewBytes, 5, &dwNum);
bRet = TRUE;
}
return bRet;
}
DLL中的cpp主要代码:
//全局HOOK对象
CILHOOK CreateProcessHook;
BOOL WINAPI MyCreateProcessW(LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation)
{
BOOL bRet = FALSE;
//弹出被创建的进程名
if(MessageBoxW(NULL, lpApplicationName, lpCommandLine, MB_YESNO) == IDYES)
{
//自己调用函数前先去掉钩子,否则会进入死循环
CreateProcessHook.UnHook();
bRet = CreateProcessW( lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
CreateProcessHook.ReHook();
}else{
MessageBox(NULL, "您启动的程序被拦截", "提示", MB_OK);
}
return bRet;
}
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
//HOOK CreateProcessW()函数
CreateProcessHook.Hook("kernel32.dll", "CreateProcessW", (PROC)MyCreateProcessW);
break;
}
case DLL_PROCESS_DETACH:
{
CreateProcessHook.UnHook();
break;
}
}
return TRUE;
}
接下来用我前面文章中用到的DLL注入器进行注入相应的DLL即可。
下面是运行时的界面:
注入explorer.exe进程,然后点击打开IE,此时会hook到我们指定的函数。选择是,可以打开IE;选择否,将不打开IE程序。选否时如下图所示: