二、防火墙原理及实现 (2)

 

2.2.1 应用层实现

2.2.1.1 HOOK API

在应用层我们可以使用HOOK API的方法,拦截windows中与网络通信相关的Socket API。由于一般的应用软件都是通过调用Windows socket API进行数据通信的。我们只考虑Winsock2,也就是说这些socket操作的API都被封装到ws2_32.dll中。关于HOOK API也有众多实现方法。

 

 

2.2.1.1.1跳转大法(我这么叫它)

    我们知道Windows系统API都是被封装到DLL中,在某个应用程序要调用一个API函数的时候,如果这个函数所在的DLL没有被加载到本进程中则加载它,然后保存当前环境当前值(各个寄存器和函数调用完后的返回地址等)。接着程序会跳转到这个API的入口地址去执行此处的指令。我们想在调用真正的API之前先调用我们的函数,那么可以修改这个API函数的入口处的代码,使他调用我们的函数,然后在我们的函数最后再调用原来的API函数。下面以拦截WS2_32.dll中的recv函数为例说明拦截的主要过程。首先可以使用普通的HOOK把自己编写的DLL挂接到系统当前运行的所有进程中(要排除一些Windows系统自身的进程,否则会出现问题,影响系统正常工作),也可以使用列举系统进程然后用远程线程注入的方法,但是后者只适用于Win2000以上的操作系统。

 

 

当我们的DLL被所有目标进程加载后,我们就可以进行真正的工作了。首先使用Tool Help库的相关函数列举目标进程加载的所有模块,看看是否有ws2_32.dll,如果没有,说明这个进程没有使用Winsock提供的函数,那么我们就不用再给这个进程添乱了。如果找到ws2_32.dll模块,那么OK,我们可以开工了。先是用GetProcAddress函数获得进程中ws2_32.dll模块的recv函数的地址。刚才说过,我们想把recv函数起始位置加入一条跳转指令,让它先跳转到我们的函数中运行。跳转指令可以用0xE9来表示,后面还有4个字节的我们函数的相对地址。也就是我们要修改recv函数前5个字节。其中 相对地址 = 我们函数的地址 - API的地址 - 5(我们这条指令的长度)。好了,先让我们读取稍后要被覆盖的recv函数入口处的5个字节的内容,把它保存起来留着以后恢复时使用。

 

 

然后向recv函数入口地址的5字节改写为跳转指令(在这里我们使用CALL指令)。这样每当recv被进程中的代码调用,那么都会先运行在入口处的执行跳转指令而跳转到我们的函数。在我们的函数中,你可以“为所欲为”了,但是在我们的函数里应该能够使用recv函数呀,所以在我们自己的函数中要调用recv函数之前先要恢复recv函数入口处的5个字节,然后调用它,调用完成后还要把它改写成跳转指令。慢着,你一定发现问题了吧?当我们为了调用原来的recv函数而刚刚把recv入口处的5个字节恢复,这时系统中的其他线程调用了recv函数,而这个调用将会成为漏网之鱼而不会进入到我们的函数中来。简单的解决办法是使用临界对象CriticalSection来保证同时只能有一个线程对recv函数入口处5个字节进行读写操作。最后记得在你想要停止拦截的时候恢复所有你修改过的进程和这些进程中被修改的API的前5个字节。其实原理讲着容易,在实现的时候会遇到各种各样的问题,如98下这些系统的DLL被加载到系统内存区供应用程序共享,所以这些内存是受保护的,不能随意修改,还有nt/2000下权限问题,还要考虑到不要拦截某些系统进程,否则会带来灾难性的后果。这些都是在实践当中遇到的实际问题。

 

 

    下面结合代码给大家讲解一下吧,首先我们要实现HOOK模块,我们给它起个名字叫做MainHookDll.DLL。在此模块中,主要要实现一个CHookApi的类,这个类完成主要的拦截功能,也是整个项目的技术核心和难点,后面将具体介绍它。而且,MainHookDll模块就是将来要注入到系统其它进程的模块,而远程调用函数是非常困难的事情,所以我们设计此模块的时候应让其被加载后自动执行拦截的初始化等工作。这样,我们只需要让远程的进程加载HOOK,然后MainHookDll.dll就能够自动执行其它操作从而HOOK该进程的相关API

 

 

MainHookDll模块中的CHookApi类拥有2个向外部提供的主要的方法,HookAllAPI,表示拦截指定进程中的指定APIUnhookAllAPI,表示取消拦截指定进程中的指定API。进行具体设计的时候,会遇到一个问题。大家看到,上文所说的开始将原始API的前5个字节写成CALL XXXX,而在我们的替换函数中要恢复保存的API原始的5个字节,在调用完成后又要把API5个字节改为CALL XXXX。如果我们拦截多个API要在每个替换函数中按照如上的方法进行设置,这样虽然我们自己明白,但是可能您只是实现HOOKAPI部分,而别人实现调用,这样会使代码看起来很难维护,在别人写的替换函数中加上这些莫名奇妙的语句看来不是一个好主意,而且为了实现防火墙的功能,我们将需要拦截多个感兴趣的API函数,那样的话将会在每一个要拦截的函数里都有这些莫名其妙的代码将会是件很恶心得事情。而且对于CALL XXXX中的地址,要对于不同的API设置不同的替换函数地址。那么能不能把这些所有的函数归纳为一个函数,所有的API函数前5字节都改为CALL到这个函数的地址,这个函数先恢复API的前5字节,然后调用用户真正的替换函数,然后再设置API函数的前5字节,这样可以使真正的替换函数只做自己应该做的事情,而跟HOOK API相关的操作都由我们的通用函数来干。

 

 

这样的想法是好的,但是有一个突出问题,因为替换函数的函数声明与原API一致,所以对于要拦截的不同的API,它们的的参数和返回值是不一样的。那我们怎样通过一个函数获得用户传递给API的参数,然后使用这些参数调用替换函数,最后把替换函数的返回值再返回给调用API的客户?要想实现这个功能,我们需要了解一个知识,也就是C++究竟是怎样调用一个函数的。我们以ws2_32.dll中提供的recv函数为例进行说明,recv函数的声明如下:

        int recv(

            SOCKET s,

            char* buf,

        int len,

            int flags

            );

可以看出它具有4个参数,返回值类型是int。我们作如下调用:

        recv(s,buf,buflen,0);

    那么在调用recv前,这四个参数将按照从右向左的顺序压到栈中,然后用Call指令跳转到recv函数的地址继续执行。recv可以从栈中取出参数并执行其他功能,最后返回时返回值将被保存在寄存器EAX中。最后还要说明一点的是,在汇编语言看来这些参数和返回值都是以DWORD类型表示的,所以如果是大于4字节的值,就用这4个字节表示值所在的地址。

 

 

有了这些知识我们就可以想到,如果用户调用recv函数并被拦截跳转到我们的函数中运行,但是我们并不知道有多少个参数和返回值,那么我们可以从栈中取出参数,但是参数的个数需要提供,当然我们可以在前面为每个API函数指定相应的参数个数,然后运行真正的替换函数,最后在返回前把替换函数的返回值放到寄存器EAX中,这样就解决了不知道参数和返回值个数的问题。那么我们的函数应该是看起来无参数无返回值的。

 

 

        基本原理我们大家都清楚了,但是继续之前我还是想讲一讲几个汇编的知识,如果没有这些知识那么看下面的代码就好像天书一样。

        1 关于参数

        我们讲过,在调用一个子函数前要把参数按顺序压栈,而子函数会从栈中取出参数。对于栈操作,我们一般使用EBPESP寄存器,而ESP是堆栈指针寄存器,所以多数情况下使用EBP寄存器对堆栈进行暂时操作。还是用调用recv函数为例,假设调用前ESP指向0x00000100处(程序运行时ESP是不可能为这个值的,此处只是为了举例说明问题)。先将参数一次压栈

        push 0          // flags入栈

        lea eax, [len]

        push eax        // len入栈

        lea eax, [buf]

        push eax        // buf入栈

        lea eax, [s]

        push eax        // s入栈

        下面使用call调用真正的recv函数,

        call dword ptr [recv] // 调用recv

        call指令先将返回地址压入栈中,返回地址就是CALL指令的下一条指令的地址,然后跳转到recv入口地址处继续执行。进入recv后,recv使用EBP临时访问堆栈之前,要保存EBP的当前内容,以便以后再使用(在 关于调用函数时保存各个寄存器的值 部分将详细讨论)。所以位于recv函数开始可能是这样的

        push ebp    // 保存ebp的当前值

        mov ebp,esp // 使把esp负给ebp

堆栈指针

[ESP]

堆栈的内容

堆栈内容的含义

0x00000100

flags

参数

0x000000fc

len

参数

0x 000000f8

buf

参数

0x 000000f4

s

参数

0x 000000f0

RetAddress

返回的地址

0x00000ec

OldEBP

保存EBP的当前值

 

 

        到此,我们可以知道,如果现在要想通过EBP获得最后一个入栈的参数,那么需要用EBP+8来获得,因为最后一个入栈的参数被保存在返回地址和EBP原始值的上面(一定记住,栈是由高地址到低地址的)。而返回地址被放在EBP+4处,EBP的原始值放在EBP+0处。

 

 

    2 关于调用函数时保存各个寄存器的值

        当我们要调用其它函数的时候,程序应该先保存各个寄存器的值,然后转去调用其它函数,最后会恢复各个寄存器的值使它们恢复成调用其它函数之前的状态。当然我们使用高级语言写程序的时候,编译器为我们做了这些事情。使用vc调试程序,打开反汇编窗口。运行一个简单的程序,该程序调用一个我们自己写的简单函数,在这个简单函数中设置断点,可以看到,编译器生成的汇编代码使用堆栈保存各个寄存器的值,上面提到当执行一个函数的时候,首先保存的是EBP的值,然后依次压入栈中保存的寄存器为EBXESIEDI,我们在恢复这些寄存器的值的时候将逆向出栈来完成。

    3 关于函数调用的返回

        调用子函数前ESP指针会因为压栈参数而改变,然后压入返回地址等,子函数中会使用ret指令从栈中取出返回地址并跳转到返回地址,而在子函数返回到CALL的下一条指令时栈中还保存着参数,所以我们需要手工的将栈中的参数所占用的空间释放,如在调用完成一个4个参数的子函数后,我们应该将ESP指针上移4*4个字节,如

        add esp,16

    这个操作在调用API的时候是不需要的,因为,windows API在函数中自己将参数弹出堆栈了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值