Windows下API Hook 技术
1 前言
在Micrisoft Windows中, 每个进程都有自己的私有地址空间。当我们用指针来引用内存的时候,指针的值表示的是进程自己的自制空间的一个内存地址。进程不能创建一个指针来引用属于其他进程的内存。
独立的地址控件对开发人员和用户来说都是非常有利的。对开发人员来说,系统更有可能捕获错误的内存读\写。对用户而言, 操作系统变得更加健壮。当然这样的健壮性也是要付出代价的,因为它使我们很难编写能够与其他进程通信的应用程序或对其他进程进行操控的应用程序。
在《Windows 核心编程》第二十二章《DLL注入和API拦截》中讲解了多种机制,他们可以将一个DLL注入到另一个进程地址的空间中。一旦DLL代码进入另一个地址空间,那么我们就可以在那个进程中随心所欲了。
本文主要介绍了如何实现替换Windows上的API函数, 实现Windows API HOOK。API HOOK的实现方法大概不下五六种。本位主要介绍了其中的一种,即如何使用WIndows挂钩来注入DLL。
2 使用Windows挂钩来注入DLL
2.1 简单windows消息HOOK
2.1.1 SetWindowsHookEx
这个是安装狗子的函数声明如下:
HHOOK WINAPI SetWindowsHookEx(
__in int idHook, \\钩子类型
__in HOOKPROC lpfn, \\回调函数地址
__in HINSTANCE hMod, \\实例句柄
__in DWORD dwThreadId); \\线程ID,0表示所有
让我们通过一个例子来学些下这个API。进程A(一个游戏改建工具,需要接获键盘消息),安装了WH_KEYBOARD_LL挂钩,如下所示:
HOOK hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hInstDll, 0); 第一个参数表示挂钩类型, 是一个低级键盘钩子。第二个参数是一个函数地址(在我们的地址空间内),在窗口即将有键盘消息的时候,系统应该调用这个函数。第三个参数hInstDll标识一个DLL, 这个DLL中包含了LowLevelKeyboardProc函数。在windows中, hInstDll的值就是就是DLL被映射到的虚拟内存地址。最后一个参数表示要给哪个线程安装钩子,0表示给所有GUI线程安装。
现在让我们看一看接下来会发生什么。
1)进程B中的一个线程准备向一个窗口发送一个键盘消息
2)系统检查该线程是否已经安装了WH_KEYBOARD_LL钩子。
3)系统检查LowLevelKeyboardProc所在的dll是否已经被映射到进程B的地址空间中。
4)如果DLL尚未被映射,那么系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计数器(lock count)递增。
5)由于DLL的hInstDll是在进程B中映射的, 因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同。如果hInstDll相同,那么在两个进程地址空间中,LowLevelKeyboardProc函数位于相同的位置,在这种情况下, 系统可以直接在进程A的地址空间中调用LowLevelKeyboardProc。如果hInstDll不同,那么系统必须确定LowLevelKeyboardProc函数在进程B地址空间中的虚拟地址,这个地址通过下面的公式得出
LowLevelKeyboardProc B = hInstDll B + (LowLevelKeyboardProc A - hInstDll A);
通过把LowLevelKeyboardProc A减去hInstDll A, 我们得到LowLevelKeyboardProc函数的偏移量,以字节为单位, 再把这个偏移量於hInstDll B相加就得到LowLevelKeyboardProc在B地址空间中位置。
6)系统在进程B中递增该DLL的锁计数器(为何再次递增? 核心编程中这样写的,不是很明白)
7)系统在B的地址空间中调用LowLevelKeyboardProc函数
8)当LowLevelKeyboardProc返回的时候,系统减去DLL在进程B中的锁计数器。
注意, 当系统把挂钩过滤函数所在的DLL注入或映射到地址空间中时,会映射整个DLL,而不仅仅是挂钩过滤函数。这意味着,DLL内所有函数存在于进程B中,能够为进程B中所有线程所调用。
此处参考两个简单消息钩子的demo, 一个是类似於改建的键盘钩子demo,这个demo展示了如何简单的做一个游戏改建; 一个是在MFC中使用钩子勾取菜单的相关消息, MFC的菜单不继承CWnd, 所以菜单相关的创建、销毁、绘制等消息我们都捕获不到,所以重绘的时候要改变菜单的一些属性就需要用到钩子,。
2.2 API Hook
2.2.1 原理
api hook并不是什么特别不同的hook,它也需要通过基本的hook提高自己的权限,跨越不同进程间访问的限制,达到修改api函数地址的目的。对于自身进程空间下使用到的api函数地址的修改,是不需要用到api hook技术就可以实现的。
我们知道,系统函数都是以DLL封装起来的,应用程序应用到系统函数时,应首先把该DLL加载到当前的进程空间中,调用的系统函数的入口地址,可以通过 GetProcAddress函数进行获取。当系统函数进行调用的时候,首先把所必要的信息保存下来(包括参数和返回地址,等一些别的信息),然后就跳转到函数的入口地址,继续执行。其实函数地址,就是系统函数“可执行代码”的开始地址。那么怎么才能让函数首先执行我们的函数呢?实际上就是把开始的那段可执行代码替换为我们自己定制的一小段可执行代码,这样系统函数调用时,不就按我们的意图乖乖行事了吗? 简单的说,就可以修改系统函数入口的地方,让他调转到我们的函数的入口点就行了。采用汇编代码就能简单的实现Jmp XXXX, 其中XXXX就是要跳转的相对地址。而Jmp后面要求的是相对偏移,也就是我们的函数入口地址到系统函数入口地址之间的差异,再减去我们这条指令的大小。用公式表达如下:
(1)int nDelta = UserFunAddr – SysFunAddr - (我们定制的这条指令的大小);
(2)Jmp nDleta;
为了保持原程序的健壮性,我们的函数里做完必要的处理后,要回调原来的系统函数,然后返回。 否则会发生自己调用自己的死循环。
那么说下程序执行过程。
1) 我们的dll“注射”入被hook的进程 (这一步只需要安装一个钩子就能实现)
2)保存系统函数入口处的代码
3)替换掉进程中的系统函数入口指向我们的函数
4)当系统函数被调用,立即跳转到我们的函数
5)我们函数处理
6)恢复系统函数入口的代码
7)调用原来的系统函数
8)再修改系统函数入口指向我们的函数(为了下次hook)-> 返回
来看我们HooK自定义的Add函数、
首先我们创建一个AddFunc的dll工程, 这个dll只有一个导出函数:
int WINAPI add(int a, int b);
这个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()钩子
returnret;
}