引入
Hook,被译作“钩子”或者“挂钩”。
我在另一篇博客中也简单提出这种机制vue2的生命周期函数
vue框架中的生命周期函数就是一种钩子函数,它是一种中断消息的机制。
通过钩子函数达到对特定事件的消息的响应和操作。
应用程序可以通过设置Hook对某个进程或窗口进行监视,即:对特定事件“挂钩”;一旦预定义特定事件发生,Windows操作系统即会向钩子hook发送通知消息,这时,应用程序可进行响应。
windows中的Hook技术被广泛应用于安全的多个领域,比如杀毒软件的主动防御功能,涉及到对一些敏感API的监控,就需要对这些API进行Hook;窃取密码的木马病毒,为了接收键盘的输入,需要Hook键盘消息;甚至是Windows系统及一些应用程序,在打补丁时也需要用到Hook技术。接下来,我们就来学习Hook技术的原理。
Hook的机制
这里借用别人的图来解释一下钩子的机制, 在Micrisoft Windows中, 每个进程都有自己的私有地址空间。当我们用指针来引用内存的时候,指针的值表示的是进程自己的自制空间的一个内存地址。进程不能创建一个指针来引用属于其他进程的内存。
独立的地址控件对开发人员和用户来说都是非常有利的。对开发人员来说,系统更有可能捕获错误的内存读\写。对用户而言, 操作系统变得更加健壮。当然这样的健壮性也是要付出代价的,因为它使我们很难编写能够与其他进程通信的应用程序或对其他进程进行操控的应用程序。
在《Windows 核心编程》第二十二章《DLL注入和API拦截》中讲解了多种机制,他们可以将一个DLL注入到另一个进程地址的空间中。一旦DLL代码进入另一个地址空间,那么我们就可以在那个进程中随心所欲了
(童鞋们Hook机制仅供学习,请勿它用)
简单的说就是在调用函数和被调用函数之间再加上一个Hook函数,这个函数就可以接受消息、处理消息、转发消息了,从而实现我们想要的自定义功能。
常见的Hook技术
在windows系统下,Hook技术的方法也比较多,使用比较灵活,可以inline内联到消息中称为注入Hook;还可以主动发消息称为消息Hook;也可以被动的接受消息称为调试Hook
消息Hook(简单的将事件消息拦截下来)
首先先来了解下常规的Windows消息流:
- 发生键盘输入事件时,WM_KEYDOWN消息被添加到系统消息队列中。
- OS判断哪个应用程序中发生了事件,然后从系统消息队列取出消息,添加到相应应用程序的应用程序消息队列中。
- 应用程序(如记事本)监视自身的应用程序消息队列,发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理。
详细的消息处理过程可以看我上一篇博客windows消息机制学习
看这个消息流我们可以在系统消息队列和应用程序队列之间安装钩子函数来获取消息。
Windows提供了一个官方函数SetWindowsHookEx()用于设置消息Hook,编程时只要调用该API就能简单地实现Hook。
#include <Windows.h>
// 钩子函数(键盘消息处理函数)
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szPath[MAX_PATH] = { 0 };
char* p = NULL;
if (nCode > 0)
{
//lParam第13位若为0,则表示KeyDown,反之为KeyUp
if (!(lParam & 0x80000000))
{
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
//比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序或下一个钩子
if (!_stricmp(p + 1, DEF_PROCESS_NAME))
return 1;
}
}
//若非notepad.exe , 则调用CallNextHookEx()函数, 将消息传递给应用程序或下一个钩子
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
extern "C"
{
_declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}
_declspec(dllexport) void HookStop()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
}
int main()
{
return 0;
}
该API在简单高效的同时也有一个弊端,就是它只能监视较少的消息,如:击键消息、鼠标移动消息、窗口消息。想要对系统更全面的进行Hook就要使用以下介绍的两种Hook方法。
调试Hook
该Hook方法的原理跟调试器的工作机制相似,核心思想都是让进程发生异常,然后自己捕获到该异常,对处于被调试状态下的进程才能进行恶意操作。
常规进程的异常事件处理:
若进程被另一个进程调试了,异常事件的处理工作将移交给调试者
PS:调试器无处理或不关心的调试事件最终由OS处理(像是一个责任链的结构)
//启动、附加事件处理函数
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
//获取WriteFile()地址
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
//设置被调试者的进程信息
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
//保存原来的第一字节到g_chOrgByte中
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
//将0xCC写入到第一字节中
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
}
注入Hook
Hook的核心思想就是修改API的代码,但是基本的Hook不能跨越不同进程访问的限制,达到修改API函数地址的目的。
比如我A进程要Hook一个B进程的CreateProcess函数,A是没有权限修改B内存中的代码的,怎么办?
我们知道系统函数都是以DLL封装起来的,应用程序应用到系统函数时会先把DLL加载到当前的进程空间中,这时候使用DLL注入技术就可以解决跨进程访问的问题,我们将Hook的代码写入一个DLL(或直接一个shellcode),将此DLL注入到B进程中,此时因为DLL在B进程的内存中,所以就有权限直接修改B内存中的代码了。
一般的Hook注入过程:
- 安装一个钩子(系统的dll注入被hook)
- 保存系统函数入口处的代码
- 替换掉进程中的系统函数入口指向我们的函数(直接修改地址空间的字节)
- 当系统函数被调用时立即跳转到我们的函数
- 函数处理
- 恢复系统函数入口的代码(处理后拖钩)
技术原理
这里介绍是是注入Hook中的一种IAT Hook方式;
IAT Hook顾名思义就是通过修改IAT里的函数地址对API进行Hook。
如下,左图红框内是IAT修改前的状态,指明
S
e
t
W
i
n
d
o
w
T
e
x
t
W
(
)
SetWindowTextW()
SetWindowTextW()的地址为
0
x
77
D
0960
E
0x77D0960E
0x77D0960E,所以
c
a
l
c
.
e
x
e
calc.exe
calc.exe执行
c
a
l
l
S
e
t
W
i
n
d
o
w
T
e
x
t
W
(
d
w
o
r
d
p
t
r
[
01001110
]
)
call SetWindowTextW(dword ptr[01001110])
callSetWindowTextW(dwordptr[01001110])实质上就是执行
c
a
l
l
0
x
77
D
0960
E
call 0x77D0960E
call0x77D0960E。
右图是被Hook后的状态,IAT中的
S
e
t
W
i
n
o
w
T
e
x
t
W
(
)
SetWinowTextW()
SetWinowTextW()的地址已被修改为
0
x
10001000
,
c
a
l
c
.
e
x
e
0x10001000,calc.exe
0x10001000,calc.exe执行
c
a
l
l
S
e
t
W
i
n
d
o
w
T
e
x
t
W
(
d
w
o
r
d
p
t
r
[
01001110
]
)
call SetWindowTextW(dword ptr[01001110])
callSetWindowTextW(dwordptr[01001110])实质变成了执行
c
a
l
l
0
x
10001000
call 0x10001000
call0x10001000(也就是恶意代码的起始地址),这时候就可以做我们想做的操作了。
代码实现
Hook IAT的代码实现,核心代码很少,大部分代码在计算IAT的位置。这里值得注意的是,我们把SetWindowTextW()替换为我们的恶意函数后,我们的恶意函数执行完后必须要调用回SetWindowTextW()(在Hook之前我们保存了SetWindowTextW()的地址),这样才能保证功能的完整性。
#include "ILHook.h"
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 )
{
// 保存该地址处5个字节的内容
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;
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;
}
#include "ILHook.h"
CILHook CreateProcessHook;
// 我们实现的Hook函数
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;
}
内联Hook
内联Hook也算是属于注入Hook的一种,它是通过注入一个DLL文件,在程序流程中直接进行嵌入jmp指令来改变流程的。
Inline Hook大致流程
- 构造跳转指令。
- 在内存中找到欲Hook函数地址,并保存欲Hook位置处的前5字节。
- 将构造的跳转指令写入需Hook的位置处。
- 当被Hook位置被执行时会转到自己的流程执行。
- 如果要执行原来的流程,那么取消Hook,也就是还原被修改的字节。
- 执行原来的流程。
- 继续Hook住原来的位置
导入地址表钩子-IAT HOOK
导入地址表是PE文件结构中的一个表结构。在可执行文件中使用其他DLL可执行文件的代码或数据,成为导入或者输入。当PE文件需要运行时,将被系统加载至内存中,此时windows加载器会定位所有的导入的函数或者数据将定位到的内容填写至可执行文件的某个位置供其使用。这个地位是需要借助于可执行文件的导入表来完成的。导入表中存放了所使用的DLL的模块名称及导入的函数名称或函数序号。
在加壳和脱壳的研究中,导入表是非常关键的部分。加壳要尽可能地隐藏或破坏原始的导入表。脱壳一定要找到或者还原或者重建原始的导入表,如果无法还原或修过脱壳后的导入表的话,那么可执行文件仍然是无法运行的。
来看我们Hook自定义的Add函数、首先我们创建一个AddFunc的dll工程, 这个dll只有一个导出函数:
i
n
t
W
I
N
A
P
I
a
d
d
(
i
n
t
a
,
i
n
t
b
)
;
int WINAPI add(int a, int b);
intWINAPIadd(inta,intb);
这个add函数就是我们稍后需要拦截的函数。有了dll后我们就能可以直接新建一个MFC工程调用Add函数 主要代码如下:
// HOOK 我的Add方法
voidCHookDemoDlg::OnBnClickedButton7()
{
//函数原型定义
typedefint(WINAPI*AddProc)(inta,intb);
AddProcadd;
staticHINSTANCEs_instadd=NULL;
s_instadd=LoadLibrary(s_path+_T("\\AddFunc.dll"));//加载dll文件
if(s_instadd==NULL)
{
AfxMessageBox(_T("no AddFunc.dll!"));
return;
}
add= (AddProc)::GetProcAddress(s_instadd,"add");//获取函数地址
intnRet=add(1,1);
CStringcstr;
cstr.Format(_T("%d + %d = %d"),1,1,nRet);
::MessageBoxW(NULL,cstr,NULL,MB_OK);
}
接下来, 我们来进行HOOK即使Hook我们AddFunc.dll中的add函数。新建一个win32的Dll工程HookDll。首先在头部声明如下变量:
//全局共享变量
#pragmadata_seg("MySec")
staticHINSTANCEg_hInstance=NULL;
staticHHOOKg_hook=NULL;
#pragmadata_seg()
#pragmacomment(linker,"/section:MySec,rws")
这两个变量表示能够在所有调用该dll的进程中共享。如果不加#pragma data_seg()来声明,g_hInstance 和g_hook将会在每个进程空间中都有一份独立的数据。编写鼠标钩子的安装卸载函数,注意两个函数导出。
//鼠标钩子过程,什么也不做,目录是注入dll到程序中
LRESULTCALLBACKMouseProc(intnCode,WPARAMwParam,LPARAMlParam)
{
returnCallNextHookEx(hhk,nCode,wParam,lParam);
}
//鼠标钩子安装函数
BOOLInstallHook()
{
hhk=::SetWindowsHookEx(WH_MOUSE,MouseProc,g_hInstance,0);
returnhhk!=NULL;
}
//鼠标钩子卸载脱钩函数
BOOLUninstallHook()
{
if(hhk!=NULL)
{
::UnhookWindowsHookEx(hhk);
hhk=NULL;
}
//HookMessageBoxW::HookOff();
//HookAddFuc::HookOff();
HookTextOutW::HookOff();
returnTRUE;
}
//在DLL的入口处DLL_PROCESS_ATTACH添加初始化变脸和进行注入。
BOOLAPIENTRYDllMain(HMODULEhModule,
DWORD ul_reason_for_call,
LPVOIDlpReserved
)
{
g_hInstance=(HINSTANCE)hModule;
switch(ul_reason_for_call)
{
caseDLL_PROCESS_ATTACH:
{
DWORDdwPid=::GetCurrentProcessId();
HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,0,dwPid);
HookAddFuc::Inject(hProcess);
break;
}
caseDLL_THREAD_ATTACH:
caseDLL_THREAD_DETACH:
caseDLL_PROCESS_DETACH:
{
HookAddFuc::HookOff();
break;
}
}
returnTRUE;
}
//编写HookAddFuc::Inject(hProcess)注入函数
voidHookAddFuc::Inject(HANDLEh)
{
hProcess=h;
if(!bInjectedAdd)
{
bInjectedAdd=true;
//获取add.dll中的add()函数
HMODULEhmod=::LoadLibrary(s_path);
add=(AddProc)::GetProcAddress(hmod,"add");
pfadd=(FARPROC)add;
if(pfadd==NULL)
{
MessageBoxW(NULL,L"cannot locate add()",NULL,MB_OK);
}
// 将add()中的入口代码保存入OldCode[]
_asm
{
leaedi,OldCode
movesi,pfadd
cld
movsd
movsb
}
NewCode[0]=0xe9;//实际上0xe9就相当于jmp指令
//获取Myadd()的相对地址
_asm
{
leaeax,Myadd
movebx,pfadd
subeax,ebx
subeax,5
movdwordptr[NewCode+1],eax
}
//填充完毕,现在NewCode[]里的指令相当于Jmp Myadd
HookOn();//可以开启钩子了
}
}
//恢复函数地址
voidHookAddFuc::HookOff()
{
DWORDdwTemp=0;
DWORDdwOldProtect;
VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect);
WriteProcessMemory(hProcess,pfadd,OldCode,5,0);
VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp);
}
//修改函数地址
voidHookAddFuc::HookOn()
{
DWORDdwTemp=0;
DWORDdwOldProtect;
//将内存保护模式改为可写,老模式保存入dwOldProtect
VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect);
//将所属进程中add()的前5个字节改为Jmp Myadd
WriteProcessMemory(hProcess,pfadd,NewCode,5,0);
//将内存保护模式改回为dwOldProtect
VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp);
}
//然后,写我们自己的Myadd()函数
intWINAPIMyadd(inta,intb)
{
//截获了对add()的调用,我们给a,b都加1
a=a+1;
b=b+1;
HookAddFuc::HookOff();//关掉Myadd()钩子防止死循环
intret;
ret=add(a,b);
HookAddFuc::HookOn();//开启Myadd()钩子
return ret;
}
总结
Windows Hook机制和技术仅供学习了解,不建议大家用于不良用途;