HOOK API是一个永恒的话题。如果没有HOOK,很多技术将很难实现,或许根本无法实现。这里说的API是广义的API,包括DOS中的中断、WINDOWS中的API、中断服务、IFS和NDIS滤波等。比如大家熟悉的即时翻译软件,就是通过两个函数实现的,HOOK TextOut或者ExtTextOut。在操作系统输出具有这两个功能的文本之前,用中文替换对应的英文,实现即时翻译;中频和NDIS滤波也是如此。在读写磁盘和收发数据之前,系统会调用第三方提供的回调函数来确定是否可以释放操作。它不同于普通的钩子,它是被操作系统允许的,操作系统提供了一个安装回调函数的接口。
即使没有HOOK也不会有病毒,因为无论是DOS下的病毒还是WINDOWS下的病毒,都是依靠HOOK系统服务来实现其功能的:DOS下的病毒通过HOOK INT 21感染文件(文件型病毒),通过HOOK INT 13感染引导扇区(引导型病毒);WINDOWS下的病毒依靠HOOK API(包括RING0层和RING3层)或者安装IFS(CIH病毒使用的方法)感染文件。因此,可以说“没有HOOK,就没有今天多姿多彩的软件世界”。
因为涉及专利和知识产权,或者商业机密,微软一直不主张挂钩其系统API,提供IFS、NDIS等其他过滤接口,这些接口也是为了满足杀毒软件和防火墙的需要而开放的。所以很多时候HOOK API都要自己做。
HOOK API有一个原则,就是不能以任何方式影响被挂接API的原有功能。就像医生治病救人一样,如果患者体内的病毒被杀死,患者死亡,那么这个“治病救人”就毫无意义。如果挂钩了API,你的目的达到了,但是API原来的功能失效了,这样就不是挂钩,而是替换,操作系统的正常功能会受到影响甚至崩溃。
HOOK API技术,说起来并不复杂,就是改变程序流程的技术。在CPU指令中,有几个指令可以改变程序的进程:JMP、CALL、INT、RET、RETF、IRET等。理论上只要在API的入口和出口修改任意机器码就可以挂接,但实际实现要复杂得多,因为要处理以下问题:
1.CPU指令长度问题。在32位系统中,一条JMP/CALL指令的长度是5个字节,所以你只需要在API中替换掉长度超过5个字节的机器码(或者替换掉几条长度为5个字节的指令),否则会影响到长度小于5个字节的已更改机器码后面的几条指令,甚至会打乱程序流程,造成不可预知的后果;
2.参数问题。为了访问原始API的参数,你不得不通过EBP或者ESP来引用参数,所以这个时候你应该非常清楚你的钩子代码中EBP/ESP的值;
3.这是个时机问题。有些钩子必须在API的开头,有些必须在API的结尾,比如HOOK CreateFilaA。如果你在API的末尾挂接API,那么这个时候你就不能写文件,甚至不能访问文件;钩子RECV,如果在API头钩子,此时还没有收到数据,可以检查RECV的接收缓冲区。当然,里面没有你想要的数据。在RECV正常执行后,必须在RECV的末尾进行钩子操作。这时检查RECV的缓冲区,只有你想要的数据。
4.上下文的问题,有些钩子代码不能执行某些操作,否则会破坏原API的上下文,原API失效;
5.同步,钩子代码尽量不要用全局变量,要用局部变量,这也是模块化程序的需要;
6.最后需要注意的是,被替换的CPU指令的原函数必须在钩子代码的某个地方模拟。
以ws2_32.dll中的send为例,说明如何挂接这个函数:
出口fn:发送- Ord:0013h
地址机器代码汇编代码
:71A21AF4 55推送ebp //要挂钩的机器代码(方法1)
:71a21af5 8becamov ebp,esp//要挂钩的机器代码(方法2)
:71A21AF7 83EC10子esp,00000010
:71A21AFA 56推动esi
:71A21AFB 57推送edi
:71A21AFC 33FF xor edi,edi
:71a 21 AFE 813d 1c 20a 371931 ca 271 CMP dword ptr[71a 3201 c],71a 21 c 93//要挂钩的机器代码(方法4)
:71A21B08 0F84853D0000 je 71A25893
:71A21B0E 8D45F8 lea eax,dword ptr [ebp-08]
:71A21B11 50推动eax
:71A21B12 E869F7FFFF电话71A21280
:71A21B17 3BC7 cmp eax,edi
:71a 21 b 19 8945 fc mov dword ptr[ebp-04],eax
:71a 21 B1 c 0 f85c 4940000 jne 71a 2 AFE 6
:71A21B22 FF7508推送[ebp+08]
:71A21B25 E826F7FFFF电话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推送ebx
:71A21B38 8D4DFC lea ecx,dword ptr [ebp-04]
:71A21B3B 51推送ecx
:71A21B3C FF75F8按钮[ebp-08]
:71A21B3F 8D4D08 lea ecx,dword ptr [ebp+08]
:71A21B42 57推送edi
:71A21B43 57推送edi
:71A21B44 FF7514 push [ebp+14]
:71a 21 b 47 8945 f 0 mov dword ptr[ebp-10],eax
:71A21B4A 8B450C mov eax,dword ptr [ebp+0C]
:71A21B4D 51推送ecx
:71A21B4E 6A01推送0000001
:71A21B50 8D4DF0 lea ecx,dword ptr [ebp-10]
:71A21B53 51推送ecx
:71A21B54 FF7508推送[ebp+08]
:71a 21 b 57 8945 F4 mov dword ptr[ebp-0C],eax
:71A21B5A 8B460C mov eax,dword ptr [esi+0C]
:71A21B5D FF5064调用[eax+64]
:71A21B60 8BCE mov ecx,esi
:71A21B62 8BD8 mov ebx,eax
:71a 21 b 64 e 8 c 7 f 6 ffcall 7a 121230//要挂钩的机器代码(方法3)
:71A21B69 3BDF cmp ebx,edi
:71A21B6B 5B pop ebx
:71a 21 b 6 c 0 f 855 f 940000 jne 71 a2 AFD 1
:71A21B72 8B4508 mov eax,dword ptr [ebp+08]
:71A21B75 5F pop edi
:71A21B76 5E pop esi
:71A21B77 C9离开
:71A21B78 C21000 ret 0010
有四种方法可以挂钩这个API:
1.将API入口的第一条指令,PUSH EBP指令(机器码0x55),替换为INT 3(机器码0xcc),然后利用WINDOWS提供的调试功能,执行自己的代码。这种方法被软冰等DEBUGER广泛使用,通过BPX在相应的地方设置一条INT 3指令来中断断点。但是不推荐这种方法,因为会和WINDOWS或者调试工具冲突,汇编代码基本都要调试;
2.将第二条mov ebp,esp指令(机器码8BEC,2字节)替换为INT F0指令(机器码CDF0),然后在IDT设置一个中断门指向我们的代码。我在这里给出一个钩子代码:
Lea ebp,[esp+12] //模拟原指令mov ebp,esp的功能
Pushfd //保存站点
Pushad //保存场景
//在这里做你想做的
Popad //恢复站点
Popfd //恢复站点
irtd//返回原始指令的下一条指令继续执行原始函数(在地址71A21AF7)
这种方法很好,但缺点是在IDT中设置了一个中断门,即进入RING0。
3.改变调用指令的相对地址(调用分别在71A21B12,71A21B25,71A21B64,但是前两次调用之前有一个条件跳转指令,可能不会执行,所以我们要钩子71A21B64的调用指令)。为什么要找CALL命令?因为都是5字节指令,都是调用指令,只要操作码0xE8不变,我们就可以把后面的相对地址改成我们的钩子代码来执行,然后转到我们钩子代码后面的目标地址来执行。
假设我们的钩子代码在71A20400,那么我们把71A21B64的调用指令改为调用71A20400(原来的指令是这样的:调用71A21230)
而71A20400处的钩子代码是这样的:
71A20400:
保存现场
//在这里做你想做的
恢复寄存器
jm1a 21230//跳转到原调用指令的目标地址,是这样的:调用71A21230
这种方法具有很好的隐蔽性,但是很难找到这个5字节的调用指令,而且计算相对地址比较复杂。
4.替换71A21AFE地址的CMP DWORD PTR [71a3201C],71a21C93指令(机器码:813D1C20A371931CA271,10字节)变成
致电71A20400
not otherwise provided 除非另有规定
not otherwise provided 除非另有规定
not otherwise provided 除非另有规定
not otherwise provided 除非另有规定
not otherwise provided 除非另有规定
(机器代码:E8XX XX XX 90 90 90 90 90,10字节)
71A20400中的挂钩代码是:
保存现场
Mov edx,71A3201Ch //模拟原指令cmp dword ptr [71A3201C],71A21C93
Cmp双字ptr [edx],71A21C93h //模拟原始指令cmp双字ptr [71A3201C],71A21C93
推进功能
//在这里做你想做的
popfd
恢复寄存器
浸水使柔软
这种方法隐蔽性最好,但不是每个API都有这样的说明,要根据具体情况操作。
以上方法都是常用的。值得一提的是,很多人都是改API的前5个字节,但是现在很多杀毒软件都是用这种方法来检查API是否被勾过,或者其他病毒木马在你之后改了前5个字节,会互相覆盖,最后一个勾API的操作是有效的。所以提倡第三种、第四种方法。