特邀体验者
- 帖子:2994
- 注册:
2007-06-02
- 来自:
|
发表于:
2009-06-11 15:20
|
显示全部
短消息
资料
回复: 反病毒引擎设计(作者不详)Part3
3.2病毒实时监控实现技术概论 正如上面提到的病毒实时监控其实就是一个文件监视器,它会在文件打开,关闭,清除,写入等操作时检查文件是否是病毒携带者,如果是则根据用户的决定选择不同的处理方案,如清除病毒,禁止访问该文件,删除该文件或简单地忽略。这样就可以有效地避免病毒在本地机器上的感染传播,因为可执行文件装入器在装入一个文件执行时首先会要求打开该文件,而这个请求又一定会被实时监控在第一时间截获到,它确保了每次执行的都是干净的不带毒的文件从而不给病毒以任何执行和发作的机会。以上说的仅是病毒实时监控一个粗略的工作过程,详细的说明将留到后面相应的章节中。病毒实时监控的设计主要存在以下几个难点:
其一是驱动程序的编写不同于普通用户态程序的写作,其难度很大。写用户态程序时你需要的仅仅就是调用一些熟知的API函数来完成特定的目的,比如打开文件你只需调用CreateFile就可以了;但在驱动程序中你将无法使用熟悉的CreateFile。在NT/2000下你可以使用ZwCreateFile或NtCreateFile(native API),但这些函数通常会要求运行在某个IRQL(中断请求级)上,如果你对如中断请求级,延迟/异步过程调用,非分页/分页内存等概念不是特别清楚,那么你写的驱动将很容易导致蓝屏死机(BSOD),Ring0下的异常将往往导致系统崩溃,因为它对于系统总是被信任的,所以没有相应处理代码去捕获这个异常。在NT下对KeBugCheckEx的调用将导致蓝屏的出现,接着系统将进行转储并随后重启。另外驱动程序的调试不如用户态程序那样方便,用象VC++那样的调试器是不行的,你必须使用系统级调试器,如softice,kd,trw等。
其二是驱动程序与ring3下客户程序的通信问题。这个问题的提出是很自然的,试想当驱动程序截获到某个文件打开请求时,它必须通知位于ring3下的查毒模块检查被打开的文件,随后查毒模块还需将查毒的结果通过某种方式传给ring0下的监控程序,最后驱动程序根据返回的结果决定请求是否被允许。这里面显然存在一个双向的通信过程。写过驱动程序的人都知道一个可以用来向驱动程序发送设备I/O控制信息的API调用DeviceIoControl,它的接口在MSDN中可以找到,但它是单向的,即ring3下客户程序可以通过调用DeviceIoControl将某些信息传给ring0下的监控程序但反过来不行。既然无法找到一个现成的函数实现从ring0下的监控程序到ring3下客户程序的通信,则我们必须采用迂回的办法来间接做到这一点。为此我们必须引入异步过程调用(APC)和事件对象的概念,它们就是实现特权级间唤醒的关键所在。现在先简单介绍一下这两个概念,具体的用法请参看后面的每子章中的技术实现细节。异步过程调用是一种系统用来当条件合适时在某个特定线程的上下文中执行一个过程的机制。当向一个线程的APC队列排队一个APC时,系统将发出一个软件中断,当下一次线程被调度时,APC函数将得以运行。APC分成两种:系统创建的APC称为内核模式APC,由应用程序创建的APC称为用户模式APC。另外只有当线程处于可报警(alertable)状态时才能运行一个APC。比如调用一个异步模式的ReadFileEx时可以指定一个用户自定义的回调函数FileIOCompletionRoutine,当异步的I/O操作完成或被取消并且线程处于可报警状态时函数被调用,这就是APC的典型用法。Kernel32.dll中导出的QueueUserAPC函数可以向指定线程的队列中增加一个APC对象,因为我们写的是驱动程序,这并不是我们要的那个函数。很幸运的是在Vwin32.vxd中导出了一个同名函数QueueUserAPC,监控程序拦截到一个文件打开请求后,它马上调用这个服务排队一个ring3下客户程序中需要被唤醒的函数的APC,这个函数将在不久客户程序被调度时被调用。这种APC唤醒法适用于WIN9X,在WINNT/2000下我们将使用全局共享的事件和信号量对象来解决互相唤醒问题。有关WINNT/2000下的对象组织结构我将在3.4.2节中详细说明。NT/2000版监控程序中我们将利用KeReleaseSemaphore来唤醒一个在ring3下客户程序中等待的线程。目前不少反病毒软件已将驱动使用的查毒模块移到ring0,即如其所宣传的“主动与操作系统无缝连接”,这样做省却了通信的消耗,但把查毒模块写成驱动形式也同时会带来一些麻烦,如不能调用大量熟知的API,不能与用户实时交互,所以我们还是选择剖析传统的反病毒软件的监控程序。
其三是驱动程序所占用资源问题。如果由于监控程序频繁地拦截文件操作而使系统性能下降过多,则这样的程序是没有其存在的价值的。本论文将对一个成功的反病毒软件的监控程序做彻底的剖析,其中就包含有分析其用以提高自身性能的技巧的部分,如设置历史记录,内置文件类型过滤,设置等待超时等。
3.3WIN9X下的病毒实时监控 3.3.1实现技术详解 WIN9X下病毒实时监控的实现主要依赖于虚拟设备驱动(VXD)编程,可安装文件系统钩挂(IFSHook),VXD与ring3下客户程序的通信(APC/EVENT)三项技术。
我们曾经提到过只有工作于系统核心态的驱动程序才具有有效地完成拦截系统范围文件操作的能力,VXD就是适用于WIN9X下的虚拟设备驱动程序,所以正可当此重任。当然,VXD的功能远不止由IFSMGR.vxd提供的拦截文件操作这一项,系统的VXDs几乎提供了所有的底层操作的接口--可以把VXD看成ring0下的DLL。虚拟机管理器本身就是一个VXD,它导出的底层操作接口一般称为VMM服务,而其他VXD的调用接口则称为VXD服务。
二者ring0调用方法均相同,即在INT20(CD 20)后面紧跟着一个服务识别码,VMM会利用服务识别码的前半部分设备标识--Device Id找到对应的VXD,然后再利用服务识别码的后半部分在VXD的服务表(Service Table)中定位服务函数的指针并调用之:
CD 20 INT 20H
01 00 0D 00 DD VKD_Define_HotKey
这条指令第一次执行后,VMM将以一个同样6字节间接调用指令替换之(并不都是修正为CALL指令,有时会利用JMP指令),从而省却了查询服务表的工作:
FF 15 XX XX XX XX CALL [$VKD_Define_HotKey]
必须注意,上述调用方法只适用于ring0,即只是一个从VXD中调用VXD/VMM服务的ring0接口。VXD还提供了V86(虚拟8086模式),Win16保护模式,Win32保护模式调用接口。其中V86和Win16保护模式的调用接口比较奇怪:
XOR DI DI MOV ES,DI MOV AX,1684 ;INT 2FH,AX = 1684H-->取得设备入口 MOV BX,002A ;002AH = VWIN32.VXD的设备标识 INT 2F MOV AX,ES ;现在ES:DI中应该包含着入口 OR AX,AX JE failure MOV AH,00 ;VWIN32 服务 0 = VWIN32_Get_Version PUSH DS MOV DS,WORD PTR CS:[0002] MOV WORD PTR [lpfnVMIN32],DI MOV WORD PTR [lpfnVMIN32+2],ES ;保存ES和DI CALL FAR [lpfnVMIN32] ;call gate(调用门) ES:DI指向了3B段的一个保护模式回调:
003B:000003D0 INT 30 ;#0028:C025DB52 VWIN32(04)+0742
INT30强迫CPU从ring3提升到ring0,然后WIN95的INT30处理函数先检查调用是否发自3B段,如是则利用引发回调的CS:IP索引一个保护模式回调表以求得一个ring0地址。本例中是0028:C025DB52 ,即所需服务VWIN32_Get_Version的入口地址。
VXD的Win32保护模式调用接口我们在前面已经提到过。一个是DeviceIoControl,我们的ring3客户程序利用它来和监控驱动进行单向通信;另一个是VxdCall,它是Kernel32.dll的一个未公开的调用,被系统频繁使用,对我们则没有多大用处。
你可以参看WIN95DDK的帮助,其中对每个系统VXD提供的调用接口均有详细说明,可按照需要选择相应的服务。
可安装文件系统钩挂(IFSHook)就源自IFSMGR.VXD提供的一个服务IFSMgr_InstallFileSystemApiHook,利用这个服务驱动程序可以向系统注册一个钩子函数。以后系统中所有文件操作都会经过这个钩子的过滤,WIN9X下文件读写具体流程如下:
在读写操作进行时,首先通过未公开函数EnterMustComplete来增加MUSTCOMPLETECOUNT变量的记数,告诉操作系统本操作必须完成。该函数设置了KERNEL32模块里的内部变量来显示现在有个关键操作正在进行。有句题外话,在VMM里同样有个函数,函数名也是EnterMustComplete。那个函数同样告诉VMM,有个关键操作正在进行。防止线程被杀掉或者被挂起。
接下来,WIN9X进行了一个_MapHandleWithContext(又是一个未公开函数)操作。该操作本身的具体意义尚不清楚,但是其操作却是得到HANDLE所指对象的指针,并且增加了引用计数。
随后,进行的乃是根本性的操作:KERNEL32发出了一个调用VWIN32_Int21Dispatch的VxdCall。陷入VWIN32后,其 检查调用是否是读写操作。若是,则根据文件句柄切换成一个FSD能识别的句柄,并调用IFSMgr_Ring0_FileIO。接下来任务就转到了IFS MANAGER。
IFS MANAGER生成一个IOREQ,并跳转到Ring0ReadWrite内部例程。Ring0ReadWrite检查句柄有效性,并且获取FSD在创建文件句柄时返回的CONTEXT,一起传入到CallIoFunc内部例程。CallIoFunc检查IFSHOOK的存在,如果不存在,IFS MANAGER生成一个缺省的IFS HOOK,并且调用相应的VFatReadFile/VFatWriteFile例程(因为目前 MS本身仅提供了VFAT驱动);如果IFSHOOK存在,则IFSHOOK函数得到控制权,而IFS MANAGER本身就脱离了文件读写处理。然后,调用被层层返回。KERNEL32调用未公开函数LeaveMustComplete,减少MUSTCOMPLETECOUNT计数,最终回到调用者。
由此可见通过IFSHook拦截本地文件操作是万无一失的,而通过ApiHook或VxdCall拦截文件则多有遗漏。著名的CIH病毒正是利用了这一技术,实现其驻留感染的,其中的代码片段如下:
lea eax, FileSystemApiHook-@6[edi] ;取得欲安装的钩子函数的地址 push eax int 20h ;调用IFSMgr_InstallFileSystemApiHook IFSMgr_InstallFileSystemApiHook = $ dd 00400067h mov dr0, eax ;保存前一个钩子的地址 pop eax 正如我们看到的,系统中安装的所有钩子函数呈链状排列。最后安装的钩子,最先被系统调用。我们在安装钩子的同时必须将调用返回的前一个钩子的地址暂存以便在完成处理后向下传递该请求:
mov eax, dr0 ;取得前一个钩子的地址
jmp [eax] ; 跳到那里继续执行
对于病毒实时监控来说,我们在安装钩子时同样需要保存前一个钩子的地址。如果文件操作的对象携带了病毒,则我们可以通过不调用前一个钩子来简单的取消该文件请求;反之,我们则需及时向下传递该请求,若在钩子中滞留的时间过长--用于等待ring3级查毒模块的处理反馈--则会使用户明显感觉系统变慢。
至于钩子函数入口参数结构和怎样从参数中取得操作类型(如IFSFN_OPEN)和文件名(以UNICODE形式存储)请参看相应的代码剖析部分。
我们所需的另一项技术--APC/EVENT也是源自一个VXD导出的服务,这便是著名的VWIN32.vxd。这个奇怪的VXD导出了许多与WIN32 API对应的服务:如_VWIN32_QueueUserApc,_VWIN32_WaitSingleObject,_VWIN32_ResetWin32Event,_VWIN32_Get_Thread_Context,_VWIN32_Set_Thread_Context 等。这个VXD叫虚拟WIN32,大概名称即是由此而来的。虽然服务的名称与WIN32 API一样,但调用规则却大相径庭,千万不可用错。_VWIN32_QueueUserApc用来注册一个用户态的APC,这里的APC函数当然是指我们在ring3下以可告警状态睡眠的待查毒线程。ring3客户程序首先通过IOCTL把待查毒线程的地址传给驱动程序,然后当钩子函数拦截到待查文件时调用此服务排队一个APC,当ring3客户程序下一次被调度时,APC例程得以执行。_VWIN32_WaitSingleObject则用来在某个对象上等待,从而使当前ring0线程暂时挂起。我们的ring3客户程序先调用WIN32 API--CreateEvent创建一组事件对象,然后通过一个未公开的API--OpenVxdHandle将事件句柄转化为VXD可辩识的句柄(其实应是指向对象的指针)并用IOCTL发给ring0端VXD,钩子函数在排队APC后调用_VWIN32_WaitSingleObject在事件的VXD句柄上等待查毒的完成,最后由ring3客户程序在查毒完毕后调用WIN32 API--SetEvent来解除钩子函数的等待。 当然,这里面存在着一个很可怕的问题:如果你按照的我说的那样去做,你会发现它会在一端时间内工作正常,但时间一长,系统就被挂起了。就连驱动编程大师Walter Oney在其著作《System Programming For Windows 95》的配套源码的说明中也称其APC例程在某些时候工作会不正常。而微软的工程师声称文件操作请求是不能被中断掉的,你不能在驱动中阻断文件操作并依赖于ring3的反馈来做出响应。网上关于这个问题也有一些讨论,意见不一:有人认为当系统DLL--KERNEL32在其调用ring0处理文件请求时拥有一个互斥量(MUTEX),而在某些情况下为了处理APC要拥有同样的互斥量,所以死锁发生了;还有人认为尽管在WIN9X下32位线程是抢先多任务的,但Win16子系统是以协作多任务来运行的。为了能平滑的运行老的16位程序,它引入了一个全局的互斥量--Win16Mutex。任何一个16位线程在其整个生命周期中都拥有Win16Mutex而32位线程当它转化成16位代码也要攫取此互斥量,因为WIN9X内核是16位的,如Knrl386.exe,gdi.exe。如果来自于拥有Win16Mutex的线程的文件请求被阻塞,系统将陷入死锁状态。这个问题的正确答案似乎在没有得到WIN9X源码的之前永远不可能被证实,但这是我们实时监控的关键,所以必须解决。
我通过跟踪WIN95文件操作的流程,并反复做实验验证,终于找到了一个比较好的解决办法:在拦截到文件请求还没有排队APC之前我们通过Get_Cur_Thread_Handle取得当前线程的ring0tcb,从中找到TDBX,再在TDBX中取得ring3tcb根据其结构,我们从偏移44H处得到Flags域值,我发现如果它等于10H和20H时容易导致死锁,这只是一个实验结果,理由我也说不清楚,大概是这样的文件请求多来自于拥有Win16Mutex的线程,所以不能阻塞;另外一个根本的解决方法是在调用_VWIN32_WaitSingleObject时指定超时,如果在指定时间里没有收到ring3的唤醒信号,则自动解除等待以防止死锁的发生。
以上对WIN9X下的实时监控的主要技术都做了详细的阐述。当然,还有一部分关于VXD的结构,编写和编译的方法由于篇幅的关系不可能在此一一说明。需要了解更详细内容的,请参看Walter Oney的著作《System Programming For Windows 95》,此书尚有台湾候俊杰翻译版《Windows 95系统程式设计》。
|