一、浅谈 HOOK API 都是一些很老很基础的东西(ZZ)
HOOK API 是一个永恒的话题,如果没有 HOOK,许多技术将很难实现,也许根本不能实现。这里所说的 API,是广义上的 API,它包括 DOS 下的中断,WINDOWS 里的 API、中断服务、IFS 和 NDIS 过滤等。比如大家熟悉的即时翻译软件,就是靠 HOOK TextOut() 或 ExtTextOut() 这两个函数实现的,在操作系统用这两个函数输出文本之前,就把相应的英文替换成中文而达到即时翻译;IFS 和 NDIS 过滤也是如此,在读写磁盘和收发数据之前,系统会调用第三方提供的回调函数来判断操作是否可以放行,它与普通 HOOK 不同,它是操作系统允许的,由操作系统提供接口来安装回调函数。
甚至如果没有 HOOK,就没有病毒,因为不管是 DOS 下的病毒或 WINDOWS 里的病毒,都是靠 HOOK 系统服务来实现自己的功能的:DOS 下的病毒靠 HOOK INT 21 来感染文件(文件型病毒),靠 HOOK INT 13 来感染引导扇区(引导型病毒);WINDOWS 下的病毒靠 HOOK 系统API(包括 RING0 层的和 RING3 层的),或者安装 IFS 来感染文件。因此可以说“没有 HOOK,就没有今天多姿多彩的软件世界”。
由于涉及到专利和知识产权,或者是商业机密,微软一直不提倡大家 HOOK 它的系统 API,提供 IFS 和 NDIS 等其他过滤接口,也是为了适应杀毒软件和防火墙的需要才开放的。所以在大多数时候,HOOK API 要靠自己的力量来完成。
HOOK API 有一个原则,这个原则就是:被 HOOK 的 API 的原有功能不能受到任何影响。就象医生救人,如果把病人身体里的病毒杀死了,病人也死了,那么这个“救人”就没有任何意义了。如果你 HOOK API 之后,你的目的达到了,但 API 的原有功能失效了,这样不是 HOOK,而是 REPLACE,操作系统的正常功能就会受到影响,甚至会崩溃。
HOOK API 的技术,说起来也不复杂,就是改变程序流程的技术。在 CPU 的指令里,有几条指令可以改变程序的流程:JMP、CALL、INT、RET、RETF、IRET 等指令。理论上只要改变 API 入口和出口的任何机器码,都可以 HOOK,但是实际实现起来要复杂很多,因为要处理好以下问题:
- CPU指令长度问题
在 32 位系统里,一条 JMP/CALL 指令的长度是 5 个字节,因此你只有替换 API 里超过 5 个字节长度的机器码(或者替换几条指令长度加起来是 5 字节的指令),否则会影响被更改的小于 5 个字节的机器码后面的数条指令,甚至程序流程会被打乱,产生不可预料的后果;
-
参数问题
为了访问原API的参数,你要通过EBP或ESP来引用参数,因此你要非常清楚你的HOOK代码里此时的EBP/ESP的值是多少;
-
时机的问题
有些 HOOK 必须在 API 的开头,有些必须在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那么此时你就不能写文件,甚至不能访问文件;HOOK RECV(),如果你在API头HOOK,此时还没有收到数据,你就去查看RECV()的接收缓冲区,里面当然没有你想要的数据,必须等RECV()正常执行后,在RECV()的尾部HOOK,此时去查看RECV()的缓冲区,里面才有想要的数据;
-
上下文的问题
有些HOOK代码不能执行某些操作,否则会破坏原API的上下文,原API就失效了;
-
同步问题
在HOOK代码里尽量不使用全局变量,而使用局部变量,这样也是模块化程序的需要;
- 最后要注意的是,被替换的CPU指令的原有功能一定要在HOOK代码的某个地方模拟实现。
下面以 ws2_32.dll
里的 send() 为例子来说明如何 HOOK 这个函数:
Exported fn(): send - Ord:0013h
地址 机器码 汇编代码
:71A21AF4 55 push ebp //将被HOOK的机器码(第1种方法)
:71A21AF5 8BEC mov ebp, esp //将被HOOK的机器码(第2种方法)
:71A21AF7 83EC10 sub esp, 00000010
:71A21AFA 56 push esi
:71A21AFB 57 push edi
:71A21AFC 33FF xor edi, edi
:71A21AFE 813D1C20A371931CA271 cmp dword ptr [71A3201C], 71A21C93 //将被HOOK的机器码(第4种方法)
:71A21B08 0F84853D0000 je 71A25893
:71A21B0E 8D45F8 lea eax, dword ptr [ebp-08]
:71A21B11 50 push eax
:71A21B12 E869F7FFFF call 71A21280
:71A21B17 3BC7 cmp eax, edi
:71A21B19 8945FC mov dword ptr [ebp-04], eax
:71A21B1C 0F85C4940000 jne 71A2AFE6
:71A21B22 FF7508 push [ebp+08]
:71A21B25 E826F7FFFF call 71A21250
:71A21B2A 8BF0 mov esi, eax
:71A21B2C 3BF7 cmp esi, edi
:71A21B2E 0F84AB940000 je 71A2AFDF
:71A21B34 8B4510 mov eax, dword ptr [ebp+10]
:71A21B37 53 push ebx
:71A21B38 8D4DFC lea ecx, dword ptr [ebp-04]
:71A21B3B 51 push ecx
:71A21B3C FF75F8 push [ebp-08]
:71A21B3F 8D4D08 lea ecx, dword ptr [ebp+08]
:71A21B42 57 push edi
:71A21B43 57 push edi
:71A21B44 FF7514 push [ebp+14]
:71A21B47 8945F0 mov dword ptr [ebp-10], eax
:71A21B4A 8B450C mov eax, dword ptr [ebp+0C]
:71A21B4D 51 push ecx
:71A21B4E 6A01 push 00000001
:71A21B50 8D4DF0 lea ecx, dword ptr [ebp-10]
:71A21B53 51 push ecx
:71A21B54 FF7508 push [ebp+08]
:71A21B57 8945F4 mov dword ptr [ebp-0C], eax
:71A21B5A 8B460C mov eax, dword ptr [esi+0C]
:71A21B5D FF5064 call [eax+64]
:71A21B60 8BCE mov ecx, esi
:71A21B62 8BD8 mov ebx, eax
:71A21B64 E8C7F6FFFF call 71A21230 //将被HOOK的机器码(第3种方法)
:71A21B69 3BDF cmp ebx, edi
:71A21B6B 5B pop ebx
:71A21B6C 0F855F940000 jne 71A2AFD1
:71A21B72 8B4508 mov eax, dword ptr [ebp+08]
:71A21B75 5F pop edi
:71A21B76 5E pop esi
:71A21B77 C9 leave
:71A21B78 C21000 ret 0010
下面用4种方法来HOOK这个API:
- 把API入口的第一条指令是PUSH EBP指令(机器码0x55)替换成INT 3(机器码0xcc),然后用WINDOWS提供的调试函数来执行自己的代码,这中方法被SOFT ICE等DEBUGER广泛采用,它就是通过BPX在相应的地方设一条INT 3指令来下断点的。但是不提倡用这种方法,因为它会与WINDOWS或调试工具产生冲突,而汇编代码基本都要调试;
-
把第二条mov ebp,esp指令(机器码8BEC,2字节)替换为INT F0指令(机器码CDF0),然后在IDT里设置一个中断门,指向我们的代码。我这里给出一个HOOK代码:
lea ebp,[esp+12] //模拟原指令mov ebp,esp的功能 pushfd //保存现场 pushad //保存现场 //在这里做你想做的事情 popad //恢复现场 popfd //恢复现场 iretd //返回原指令的下一条指令继续执行原函数(71A21AF7地址处)
这种方法很好,但缺点是要在IDT设置一个中断门,也就是要进RING0。
-
更改CALL指令的相对地址(CALL分别在71A21B12、71A21B25、71A21B64,但前面2条CALL之前有一个条件跳转指令,有可能不被执行到,因此我们要HOOK 71A21B64处的CALL指令)。为什么要找CALL指令下手?因为它们都是5字节的指令,而且都是CALL指令,只要保持操作码0xE8不变,改变后面的相对地址就可以转到我们的HOOK代码去执行了,在我们的HOOK代码后面再转到目标地址去执行。
假设我们的HOOK代码在71A20400处,那么我们把71A21B64处的CALL指令改为CALL 71A20400(原指令是这样的:CALL 71A21230)
而71A20400处的HOOK代码是这样的:71A20400: pushad //在这里做你想做的事情 popad jmp 71A21230 //跳转到原CALL指令的目标地址,原指令是这样的:call 71A21230
这种方法隐蔽性很好,但是比较难找这条5字节的CALL指令,计算相对地址也复杂。
-
替换71A21AFE地址上的cmp dword ptr [71A3201C], 71A21C93指令(机器码:813D1C20A371931CA271,10字节)成为
call 71A20400 nop nop nop nop nop (机器码:E8 XX XX XX XX 90 90 90 90 90,10字节) 在71A20400的HOOK代码是: pushad mov edx,71A3201Ch //模拟原指令cmp dword ptr [71A3201C], 71A21C93 cmp dword ptr [edx],71A21C93h //模拟原指令cmp dword ptr [71A3201C], 71A21C93 pushfd //在这里做你想做的事 popfd popad ret
这种方法隐蔽性最好,但不是每个API都有这样的指令,要具体情况具体操作。
以上几种方法是常用的方法,值得一提的是很多人都是改 API 开头的 5 个字节,但是现在很多杀毒软件用这样的方法检查 API 是否被 HOOK,或其他病毒木马在你之后又改了前 5 个字节,这样就会互相覆盖,最后一个 HOOK API 的操作才是有效的,所以提倡用第 3 和第 4 种方法。
Reference
文章作者:MGF
信息来源:www.eviloctal.com
二、Windows API Hook
注:测试的环境为Win7+VS2008+MFC
原文出处,好像是这篇:http://blog.csdn.net/glliuxueke/article/details/2702608
前言
本文主要介绍了如何实现替换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的内容:
1.//千万别忘记声明WINAPI,否则调用的时候回产生声明错误哦!
2.int WINAPI add(int a,int b)
3.{
4. return a+b;
5.}
6.
7.BOOL APIENTRY DllMain( HANDLE hModule,
8. DWORD ul_reason_for_call,
9. LPVOID lpReserved
10. )
11.{
12. return TRUE;
13.}
然后别忘了在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文件,如果函数导出成功的话,我们就可以在里面看到导出的函数名字了,如下图所示:
该工具下载地址: dll函数查看器
有了dll文件后,接下来我们新建一个MFC对话框程序来调用该dll中导出的函数add,程序界面即运行效果截图如下:
主要代码如下:
1.//调用dll函数 add(int a,int b)
2.void CCallAddDlg::OnBnClickedBtnCallAdd()
3.{
4. HINSTANCE hAddDll=NULL;
5. typedef int (WINAPI*AddProc)(int a,int b);//函数原型定义
6. AddProc add;
7. if (hAddDll==NULL)
8. {
9. hAddDll=::LoadLibrary(_T("Add.dll"));//加载dll
10. }
11. add=(AddProc)::GetProcAddress(hAddDll,"add");//获取函数add地址
12.
13. int a=1;
14. int b=2;
15. int c=add(a,b);//调用函数
16. CString tem;
17. tem.Format(_T("%d+%d=%d"),a,b,c);
18. AfxMessageBox(tem);
19.}
接下来我们进行HOOK,即HOOK我们的Add.dll文件中的函数int add(int a,int b)。新建一个MFC的 dll工程,工程名为Hook,然后我们在Hook.cpp文件里面编写代码如下:
首先在头部声明如下变量:
1.//变量定义
2.//不同Instance共享的该变量
3.#pragma data_seg("SHARED")
4.static HHOOK hhk=NULL; //鼠标钩子句柄
5.static HINSTANCE hinst=NULL; //本dll的实例句柄 (hook.dll)
6.#pragma data_seg()
7.#pragma comment(linker, "/section:SHARED,rws")
8.//以上的变量共享哦!
9.
10.CString temp; //用于显示错误的临时变量
11.bool bHook=false; //是否Hook了函数
12.bool m_bInjected=false; //是否对API进行了Hook
13.BYTE OldCode[5]; //老的系统API入口代码
14.BYTE NewCode[5]; //要跳转的API代码 (jmp xxxx)
15.typedef int (WINAPI*AddProc)(int a,int b);//add.dll中的add函数定义
16.AddProc add; //add.dll中的add函数
17.HANDLE hProcess=NULL; //所处进程的句柄
18.FARPROC pfadd; //指向add函数的远指针
19.DWORD dwPid; //所处进程ID
20.//end of 变量定义
编写鼠标钩子安装、卸载和处理函数:
1.//鼠标钩子过程,什么也不做,目的是注入dll到程序中
2.LRESULT CALLBACK MouseProc(int nCode,WPARAM wParam,LPARAM lParam)
3.{
4. return CallNextHookEx(hhk,nCode,wParam,lParam);
5.}
6.
7.//鼠标钩子安装函数:
8.BOOL InstallHook()
9.{
10.
11. hhk=::SetWindowsHookEx(WH_MOUSE,MouseProc,hinst,0);
12.
13. return true;
14.}
15.
16.//卸载鼠标钩子函数
17.void UninstallHook()
18.{
19. ::UnhookWindowsHookEx(hhk);
20.}
在dll实例化函数InitInstance()中,初始化变量和进行注入:
1.//在dll实例化中获得一些参数
2.BOOL CHookApp::InitInstance()
3.{
4. CWinApp::InitInstance();
5.
6. //获得dll 实例,进程句柄
7. hinst=::AfxGetInstanceHandle();
8. DWORD dwPid=::GetCurrentProcessId();
9. hProcess=OpenProcess(PROCESS_ALL_ACCESS,0,dwPid);
10. //调用注射函数
11. Inject();
12. return TRUE;
13.}
编写注射函数,即HOOK函数Inject()了:
1.//好,最重要的HOOK函数:
2.void Inject()
3.{
4.
5. if (m_bInjected==false)
6. { //保证只调用1次
7. m_bInjected=true;
8.
9. //获取add.dll中的add()函数
10. HMODULE hmod=::LoadLibrary(_T("Add.dll"));
11. add=(AddProc)::GetProcAddress(hmod,"add");
12. pfadd=(FARPROC)add;
13.
14. if (pfadd==NULL)
15. {
16. AfxMessageBox(L"cannot locate add()");
17. }
18.
19. // 将add()中的入口代码保存入OldCode[]
20. _asm
21. {
22. lea edi,OldCode
23. mov esi,pfadd
24. cld
25. movsd
26. movsb
27. }
28.
29. NewCode[0]=0xe9;//实际上0xe9就相当于jmp指令
30. //获取Myadd()的相对地址
31. _asm
32. {
33. lea eax,Myadd
34. mov ebx,pfadd
35. sub eax,ebx
36. sub eax,5
37. mov dword ptr [NewCode+1],eax
38. }
39. //填充完毕,现在NewCode[]里的指令相当于Jmp Myadd
40. HookOn(); //可以开启钩子了
41. }
42.}
编写HOOK开启和停止函数HookOn()和HookOff()
1.//开启钩子的函数
2.void HookOn()
3.{
4. ASSERT(hProcess!=NULL);
5.
6. DWORD dwTemp=0;
7. DWORD dwOldProtect;
8.
9. //将内存保护模式改为可写,老模式保存入dwOldProtect
10. VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect);
11. //将所属进程中add()的前5个字节改为Jmp Myadd
12. WriteProcessMemory(hProcess,pfadd,NewCode,5,0);
13. //将内存保护模式改回为dwOldProtect
14. VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp);
15.
16. bHook=true;
17.}
18.//关闭钩子的函数
19.void HookOff()//将所属进程中add()的入口代码恢复
20.{
21. ASSERT(hProcess!=NULL);
22.
23. DWORD dwTemp=0;
24. DWORD dwOldProtect;
25.
26. VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect);
27. WriteProcessMemory(hProcess,pfadd,OldCode,5,0);
28. VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp);
29. bHook=false;
30.}
编写我们自己的Myadd函数()
1.//然后,写我们自己的Myadd()函数
2.int WINAPI Myadd(int a,int b)
3.{
4. //截获了对add()的调用,我们给a,b都加1
5. a=a+1;
6. b=b+1;
7.
8. HookOff();//关掉Myadd()钩子防止死循环
9.
10. int ret;
11. ret=add(a,b);
12.
13. HookOn();//开启Myadd()钩子
14.
15. return ret;
16.}
然后别忘记在hook.def里面导出我们的两个函数 :
– InstallHook
– UninstallHook
<--  -->
接下来就可以进行HOOK的测试了,给前面的对话框程序,再添加两个按钮,一个用于安装钩子,另一个用于卸载钩子,程序和运行效果截图如下:
//未HOOK之前
//HOOK之后
安装钩子和卸载钩子主要代码如下:
1.HINSTANCE hinst=NULL;
2.//安装鼠标钩子,进行HOOK
3.void CCallAddDlg::OnBnClickedBtnStartHook()
4.{
5. typedef BOOL (CALLBACK *inshook)(); //函数原型定义
6. inshook insthook;
7.
8. hinst=LoadLibrary(_T("Hook.dll"));//加载dll文件
9. if(hinst==NULL)
10. {
11. AfxMessageBox(_T("no Hook.dll!"));
12. return;
13. }
14. insthook=::GetProcAddress(hinst,"InstallHook");//获取函数地址
15. if(insthook==NULL)
16. {
17. AfxMessageBox(_T("func not found!"));
18. return;
19. }
20. insthook();//开始HOOK
21.}
22.
23.//卸载鼠标钩子,停止HOOK
24.void CCallAddDlg::OnBnClickedBtnStopHook()
25.{
26. if (hinst==NULL)
27. {
28. return;
29. }
30. typedef BOOL (CALLBACK *UnhookProc)(); //函数原型定义
31. UnhookProc UninstallHook;
32.
33. UninstallHook=::GetProcAddress(hinst,"UninstallHook");//获取函数地址
34. if(UninstallHook!=NULL)
35. {
36. UninstallHook();
37. }
38. if (hinst!=NULL)
39. {
40. ::FreeLibrary(hinst);
41. }
42.}
以上就是之前我看的那篇文章的主要内容了,关于HOOK系统API,我会在其它的文章里面进行说明。这里再说一下原文的缺点,我认为其有两个缺点:
- 停止HOOK时,没有恢复被HOOK函数的入口。
- 没有处理dll退出事件,没有在dll退出事件中恢复被HOOK函数入口。
以上两个缺点,很容易导致程序的崩溃,因此在我的例子程序中,都对它们进行了处理:
1.//卸载鼠标钩子函数
2.void UninstallHook()
3.{
4. if (hhk!=NULL)
5. {
6. ::UnhookWindowsHookEx(hhk);
7. }
8. HookOff();//记得恢复原函数入口
9.}
10.
11.//dll退出时
12.int CHookApp::ExitInstance()
13.{
14. HookOff();//记得恢复原函数入口
15. return CWinApp::ExitInstance();
16.}
以上我这个例子工程的下载地址:hook dll文件中的函数add.zip
友情提示:我在 Debug 模式运行程序时,HOOK 会失败,在 Release 模式运行程序则 HOOK 成功。