注:我测试的环境为Win7+VS2008+MFC
前言
本文主要介绍了如何实现替换Windows上的API函数,实现Windows
API Hook。
(当然,对于socket的Hook只是其中的一种特例)。这种Hook API技术被广泛的采用在一些领域中,如屏幕取词,个人防火墙等。这种API
Hook技术并不是很新,但是涉及的领域比较宽广,要想做好有一定的技术难度。本文是采集了不少达人的以前资料并结合自己的实验得出的心得体会,在这里进行总结发表,希望能够给广大的读者提供参考,达到抛砖引玉的结果。
问题
最近和同学讨论如何构建一个Windows上的简单的个人防火墙。后来讨论涉及到了如何让进程关联套接字端口,
替换windows API,屏幕取词等技术。其中主要的问题有:
1) 采用何种机制来截获socket的调用?
一般来说,实现截获socket的方法有很多很多,最基本的,可以写驱动,驱动也有很多种,TDI驱动, NDIS驱动,Mini
port驱动…
由于我使用的是Win2000系统,所以截获socket也可以用Windows SPI来进行。另外一种就是Windows
API Hook技术。
由于我没什么硬件基础,不会写驱动,所以第一种方法没有考虑,而用SPI相对比较简单。
但是后来觉得Windows API Hook适应面更广,而且觉得自己动手能学到不少东西,
就决定用Windows API Hook来尝试做socket Hook.
2) API Hook的实现方法?
实际上就是对系统函数的替换,当然实现替换的方法大概不下5,6种吧,可以参考《Windows核心编程》第22章。
不过我使用的方法与其不近相同,应该相对比较简单易懂。
原理
我们知道,系统函数都是以DLL封装起来的,应用程序应用到系统函数时,应首先把该DLL加载到当前的进程空间中,
调用的系统函数的入口地址,可以通过 GetProcAddress函数进行获取。当系统函数进行调用的时候,
首先把所必要的信息保存下来(包括参数和返回地址,等一些别的信息),然后就跳转到函数的入口地址,继续执行。
其实函数地址,就是系统函数“可执行代码”的开始地址。那么怎么才能让函数首先执行我们的函数呢?
呵呵,应该明白了吧,把开始的那段可执行代码替换为我们自己定制的一小段可执行代码,这样系统函数调用时,
不就按我们的意图乖乖行事了吗?其实,就这么简单。Very very简单。 :P
实际的说,就可以修改系统函数入口的地方,让他调转到我们的函数的入口点就行了。
采用汇编代码就能简单的实现Jmp XXXX, 其中XXXX就是要跳转的相对地址。
我们的做法是:把系统函数的入口地方的内容替换为一条Jmp指令,目的就是跳到我们的函数进行执行。
而Jmp后面要求的是相对偏移,也就是我们的函数入口地址到系统函数入口地址之间的差异,再减去我们这条指令的大小。
用公式表达如下:(1)int nDelta = UserFunAddr – SysFunAddr -
(我们定制的这条指令的大小);(2)Jmp nDleta;
为了保持原程序的健壮性,我们的函数里做完必要的处理后,要回调原来的系统函数,然后返回。
所以调用原来系统函数之前必须先把原来修改的系统函数入口地方给恢复,否则,
系统函数地方被我们改成了Jmp XXXX就会又跳到我们的函数里,死循环了。
那么说一下程序执行的过程。
我们的dll“注射”入被hook的进程 -> 保存系统函数入口处的代码
-> 替换掉进程中的系统函数入口指向我们的函数 -> 当系统函数被
调用,立即跳转到我们的函数 -> 我们函数进行处理 -> 恢复系统函数入口的代码 ->
调用原来的系统函数 -> 再修改系统函数入口指向
我们的函数(为了下次hook)-> 返回。于是,一次完整的Hook就完成了。
好,这个问题明白以后,讲一下下个问题,就是如何进行dll“注射”?即将我们的dll注射到要Hook的进程中去呢?
很简单哦,这里我们采用调用Windows提供给我们的一些现成的Hook来进行注射。举个例子,鼠标钩子,
键盘钩子大家都知道吧?我们可以给系统装一个鼠标钩子,然后所有响应到鼠标事件的进程,
就会“自动”(其实是系统处理了)载入我们的dll然后设置相应的钩子函数。其实我们的目的只是需要让被注射进程
载入我们的dll就可以了,我们可以再dll实例化的时候进行函数注射的,我们的这个鼠标钩子什么都不干的。
简单的例子OneAddOne
讲了上面的原理,现在我们应该实战一下了。先不要考虑windows系统那些繁杂的函数,
我们自己编写一个API函数来进行Hook与被Hook的练习吧,哈哈。
第一步,首先编写一个Add.dll,很简单,这个dll只输出一个API函数,就是add啦。
新建一个win32 dll工程,
dllmain.cpp的内容:
//千万别忘记声明WINAPI,否则调用的时候回产生声明错误哦! int WINAPI add(int a,int b) { return a+b; } BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { return TRUE; } |
然后别忘了在add.def里面输出函数add:
LIBRARY Add DESCRIPTION "ADD LA" EXPORTS add @1; |
建完工程后,你会发现没有Add.def文件,这时我们自己新建一个Add.def文件,然后添加到工程中即可,添加Add.def文件到工程后,我们还需要设置工程的属性,将Add.def添加到【项目】-->【Add属性】-->【链接器】-->【输入】-->【模块定义文件】,如下图所示,不这样设置的话,我们添加的Add.def文件是不起作用的哦。
设置好后,编译,ok,我们获得了Add.dll
得到Add.dll后,我们可以用一个小工具【dll函数查看器】来打开我们的Add.dll文件,如果函数导出成功的话,我们就可以
在里面看到导出的函数名字了,如下图所示:
该工具下载地址:http://download.csdn.net/detail/friendan/6347455
//dll函数查看器
有了dll文件后,接下来我们新建一个MFC对话框程序来调用该dll中导出的函数add,
程序界面即运行效果截图如下:
主要代码如下:
//调用dll函数 add(int a,int b) void CCallAddDlg::OnBnClickedBtnCallAdd() { HINSTANCE hAddDll=NULL; typedef int (WINAPI*AddProc)(int a,int b);//函数原型定义 AddProc add; if (hAddDll==NULL) { hAddDll=::LoadLibrary(_T("Add.dll"));//加载dll } add=(AddProc)::GetProcAddress(hAddDll,"add");//获取函数add地址 int a=1; int b=2; int c=add(a,b);//调用函数 CString tem; tem.Format(_T("%d+%d=%d"),a,b,c); AfxMessageBox(tem); } |
接下来我们进行HOOK,即HOOK我们的Add.dll文件中的函数int add(int a,int
b)
新建一个MFC的 dll工程,工程名为Hook,然后我们在Hook.cpp文件里面编写代码如下:
首先在头部声明如下变量:
//变量定义 //不同Instance共享的该变量 #pragma data_seg("SHARED") static HHOOK hhk=NULL; //鼠标钩子句柄 static HINSTANCE hinst=NULL; //本dll的实例句柄 (hook.dll) #pragma data_seg() #pragma comment(linker, "/section:SHARED,rws") //以上的变量共享哦! CString temp; //用于显示错误的临时变量 bool bHook=false; //是否Hook了函数 bool m_bInjected=false; //是否对API进行了Hook BYTE OldCode[5]; //老的系统API入口代码 BYTE NewCode[5]; //要跳转的API代码 (jmp xxxx) typedef int (WINAPI*AddProc)(int a,int b);//add.dll中的add函数定义 AddProc add; //add.dll中的add函数 HANDLE hProcess=NULL; //所处进程的句柄 FARPROC pfadd; //指向add函数的远指针 DWORD dwPid; //所处进程ID //end of 变量定义 |
编写鼠标钩子安装、卸载和处理函数:
//鼠标钩子过程,什么也不做,目的是注入dll到程序中 LRESULT CALLBACK MouseProc(int nCode,WPARAM wParam,LPARAM lParam) { return CallNextHookEx(hhk,nCode,wParam,lParam); } //鼠标钩子安装函数: BOOL InstallHook() { hhk=::SetWindowsHookEx(WH_MOUSE,MouseProc,hinst,0); return true; } //卸载鼠标钩子函数 void UninstallHook() { ::UnhookWindowsHookEx(hhk); } |
在dll实例化函数InitInstance()中,初始化变量和进行注入:
//在dll实例化中获得一些参数 BOOL CHookApp::InitInstance() { CWinApp::InitInstance(); //获得dll 实例,进程句柄 hinst=::AfxGetInstanceHandle(); DWORD dwPid=::GetCurrentProcessId(); hProcess=OpenProcess(PROCESS_ALL_ACCESS,0,dwPid); //调用注射函数 Inject(); return TRUE; } |
编写注射函数,即HOOK函数Inject()了:
//好,最重要的HOOK函数: void Inject() { if (m_bInjected==false) { //保证只调用1次 m_bInjected=true; //获取add.dll中的add()函数 HMODULE hmod=::LoadLibrary(_T("Add.dll")); add=(AddProc)::GetProcAddress(hmod,"add"); pfadd=(FARPROC)add; if (pfadd==NULL) { AfxMessageBox(L"cannot locate add()"); } // 将add()中的入口代码保存入OldCode[] _asm { lea edi,OldCode mov esi,pfadd cld movsd movsb } NewCode[0]=0xe9;//实际上0xe9就相当于jmp指令 //获取Myadd()的相对地址 _asm { lea eax,Myadd mov ebx,pfadd sub eax,ebx sub eax,5 mov dword ptr [NewCode+1],eax } //填充完毕,现在NewCode[]里的指令相当于Jmp Myadd HookOn(); //可以开启钩子了 } } |
编写HOOK开启和停止函数HookOn()和HookOff()
//开启钩子的函数 void HookOn() { ASSERT(hProcess!=NULL); DWORD dwTemp=0; DWORD dwOldProtect; //将内存保护模式改为可写,老模式保存入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); bHook=true; } //关闭钩子的函数 void HookOff()//将所属进程中add()的入口代码恢复 { ASSERT(hProcess!=NULL); DWORD dwTemp=0; DWORD dwOldProtect; VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect); WriteProcessMemory(hProcess,pfadd,OldCode,5,0); VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp); bHook=false; } |
编写我们自己的Myadd函数()
//然后,写我们自己的Myadd()函数 int WINAPI Myadd(int a,int b) { //截获了对add()的调用,我们给a,b都加1 a=a+1; b=b+1; HookOff();//关掉Myadd()钩子防止死循环 int ret; ret=add(a,b); HookOn();//开启Myadd()钩子 return ret; } |
然后别忘记在hook.def里面导出我们的两个函数 :
InstallHook
UninstallHook
接下来就可以进行HOOK的测试了,给前面的对话框程序,再添加两个按钮,一个用于安装钩子,另一个用于卸载钩子,
程序和运行效果截图如下:
//未HOOK之前
//HOOK之后
安装钩子和卸载钩子主要代码如下:
HINSTANCE hinst=NULL; //安装鼠标钩子,进行HOOK void CCallAddDlg::OnBnClickedBtnStartHook() { typedef BOOL (CALLBACK *inshook)(); //函数原型定义 inshook insthook; hinst=LoadLibrary(_T("Hook.dll"));//加载dll文件 if(hinst==NULL) { AfxMessageBox(_T("no Hook.dll!")); return; } insthook=::GetProcAddress(hinst,"InstallHook");//获取函数地址 if(insthook==NULL) { AfxMessageBox(_T("func not found!")); return; } insthook();//开始HOOK } //卸载鼠标钩子,停止HOOK void CCallAddDlg::OnBnClickedBtnStopHook() { if (hinst==NULL) { return; } typedef BOOL (CALLBACK *UnhookProc)(); //函数原型定义 UnhookProc UninstallHook; UninstallHook=::GetProcAddress(hinst,"UninstallHook");//获取函数地址 if(UninstallHook!=NULL) { UninstallHook(); } if (hinst!=NULL) { ::FreeLibrary(hinst); } } |
以上就是之前我看的那篇文章的主要内容了,关于HOOK系统API,我会在其它的文章里面进行说明。
这里再说一下原文的缺点,我认为其有两个缺点:
1.停止HOOK时,没有恢复被HOOK函数的入口。
2.没有处理dll退出事件,没有在dll退出事件中恢复被HOOK函数入口。
以上两个缺点,很容易导致程序的崩溃,因此在我的例子程序中,都对它们进行了处理:
//卸载鼠标钩子函数 void UninstallHook() { if (hhk!=NULL) { ::UnhookWindowsHookEx(hhk); } HookOff();//记得恢复原函数入口 } //dll退出时 int CHookApp::ExitInstance() { HookOff();//记得恢复原函数入口 return CWinApp::ExitInstance(); } |
以上我这个例子工程的下载地址:hook dll文件中的函数add.zip
http://download.csdn.net/detail/friendan/6348209
友情提示:我在Debug模式运行程序时,HOOK会失败,在Release模式运行程序则HOOK成功。
|