1   1  /  1  页   跳转

[分享] 反病毒引擎设计(作者不详)Part4

反病毒引擎设计(作者不详)Part4

3.3.3.2取得当前进程名称代码 C0000870 push ebx C0000871 push esi C0000872 push edi C0000873 call VWIN32_GetCurrentProcessHandle ;在eax中返回ring0 PDB(进程数据库) C0000878 mov eax, [eax+38h] ;HTASK W16TDB ;偏移38h处是Win16任务数据库选择子 C000087B push 0 ;DWORD Flags C000087D or al, C000087F push eax ;DWORD Selector C0000880 call Get_Sys_VM_Handle@0 C0000885 push eax ;取得系统VM的句柄 VMHANDLE hVM C0000886 call _SelectorMapFlat ;将选择子基址映射为平坦模式的线形地址 C000088B add esp, 0Ch C000088E cmp eax, 0FFFFFFFFh ;映射错误 C0000891 jnz short loc_C0000899 ...... C0000899 lea edi, [eax+0F2h] ;从偏移0F2h取得模块名称 ;char TDB_ModName[8] 3.3.3.3通信部分代码 hooksys.vxd中代码: C00011BC push ecx ;客户程序的ring0线程句柄 C00011BD push ebx ;传入APC的参数 C00011BE push edx ;ring3级APC函数的平坦模式地址 C00011BF call _VWIN32_QueueUserApc ;排队APC C00011C4 mov eax, [ebp+0Ch] ;事件对象的ring0句柄 C00011C7 push eax C00011C8 call _VWIN32_ResetWin32Event;设置事件对象为无信号态 ...... C00011E7 mov eax, [ebp+0Ch] C00011EA push 3E8h ;超时设置 C00011EF push eax ;事件对象的ring0句柄 C00011F0 call _VWIN32_WaitSingleObject ;等待ring3查毒的完成 guidll.dll中代码: APC函数入口: 10001AD1 mov eax, hDevice ;取得设备句柄 10001AD6 lea ecx, [esp+4] 10001ADA push 0 10001ADC push ecx ;返回字节数 10001ADD lea edx, [esp+8] 10001AE1 push 4 ;输出缓冲区大小 10001AE3 push edx ;输出缓冲区指针 10001AE4 push 0 ;输入缓冲区大小 10001AE6 push 0 ;输入缓冲区指针 10001AE8 push 83003C07h ;IO控制代码 10001AED push eax ;设备句柄 10001AEE call ds:DeviceIoControl 10001AF4 test eax, eax 10001AF6 jz short loc_10001B05 10001AF8 mov ecx, [esp+0] ;得到打开文件链表头元素 10001AFC push ecx 10001AFD call ScanOpenFile ;调用查毒函数 ScanOpenFile函数中: 1000185D call ds:fnScanOneFile ;调用真正查毒库导出函数 10001863 mov edx, hMutex 10001869 add esp, 8 1000186C mov esi, eax ;查毒结果 1000186E push edx 1000186F call ds:ReleaseMutex 10001875 test esi, esi ;检查结果 10001877 jnz short OpenFileIsVirus ;如发现病毒则跳到OpenFileIsViru进一步处理 10001879 mov eax, [ebp+10h] ;事件对象的ring3句柄 1000187C mov byte ptr [ebp+16h], 0 ;设置元素中的结果位为无病毒 10001880 push eax 10001881 call ds:SetEvent ;设置事件对象为有信号态唤醒钩子函数    3.4WINNT/2000下的病毒实时监控 3.4.1实现技术详解 WINNT/2000下病毒实时监控的实现主要依赖于NT内核模式驱动编程,拦截IRP,驱动与ring3下客户程序的通信(命名的事件与信号量对象)三项技术。程序的设计思路和大体流程与前面介绍的WIN9X下病毒实时监控非常相似,只是在实现技术由于运行环境的不同将呈现很大的区别。 WINNT/2000下不再支持VXD,我将在后面剖析的hooksys.sys其实是一种称为NT内核模式设备驱动的驱动程序。这种驱动程序无论从其结构还是工作方式都与VXD有很大不同。比较而言,NT内核模式设备驱动的编写比VXD难度更大:因为它要求编程者熟悉WINNT/2000的整体架构和运行机制,NT/2000是纯32位微内核操作系统,与WIN9X有很大区别;能灵活使用内核数据结构,如驱动程序对象,设备对象,文件对象,IO请求包,执行体进程/线程块,系统服务调度表等。另外编程者在编程时还需注意许多重要事项,如当前系统运行的IO请求级,分页/非分页内存等。 这里首先介绍几个重要的内核数据结构,它们在NT内核模式设备驱动的编程中经常被用到,包括文件对象,驱动程序对象,设备对象,IO请求包(IRP),IO堆栈单元(IO_STACK_LOCATION): 文件明显符合NT中的对象标准:它们是两个或两个以上用户态进程的线程可以共享的系统资源;它们可以有名称;它们被基于对象的安全性所保护;并且它们支持同步。对于用户态受保护的子系统,文件对象通常代表一个文件,设备目录,或卷的打开实例;而对于设备和中间型驱动,文件对象通常代表一个设备。文件对象结构中的域大部分是透明的驱动可以访问的域包括: PDEVICE_OBJECT DeviceObject:指向文件于其上被打开的设备对象的指针。 UNICODE_STRING FileName:在设备上被打开的文件的名字,如果当由DeviceObject代表的设备被打开时此串长度(FileName.Length)为0。 驱动程序对象代表可装载的内核模式驱动的映象,当驱动被加载至系统中时,有I/O管理器负责创建。指向驱动程序对象的指针将作为一个输入参数传送到驱动的初始化例程(DriverEntry),再初始化例程(Reinitialize routines)和卸载例程(Unload routine)。驱动程序对象结构中的域大部分是透明的,驱动可以访问的域包括: PDEVICE_OBJECT DeviceObject:指向驱动创建的设备对象的指针。当在初始化例程中成功调用IoCreateDevice后这个域将被自动更新。当驱动卸载时,它的卸载例程将使用此域和设备对象中NextDevice域调用IoDeleteDevice来清除驱动创建的每个设备对象。 PDRIVER_INITIALIZE DriverInit:由I/O管理器设置的初始化例程(DriverEntry)入口地址。该例程负责创建驱动程序操作的每个设备的设备对象,需要的话还可以在设备名称和设备对用户态可见名称间创建符号链接。同时它还把驱动程序各例程入口点填入驱动程序对象相应的域中。 PDRIVER_UNLOAD DriverUnload:驱动程序的卸载例程入口地址。 PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION+1]:一个或多个驱动程序调度例程入口地址数组。每个驱动必须在此数组中为驱动处理的IRP_MJ_XXX请求集设置至少一个调度入口,这样所有的IRP_MJ_XXX请求都会 被I/O管理器导入同一个调度例程。当然,驱动程序也可以为每个IRP_MJ_XXX请求设置独立的调度入口。 当然,驱动程序中可能包含的例程将远不止以上列出的。比如启动I/O例程,中断服务例程(ISR),中断服务DPC例程,一个或多个完成例程,取消I/O例程,系统关闭通知例程,错误记录例程。只不过我们将要剖析的hooksys.sys中只用到例程中很少一部分,故其余的不予详细介绍。 设备对象代表已装载的驱动程序为之处理I/O请求的一个逻辑,虚拟或物理设备。每个NT内核模式驱动程序必须在它的初始化例程中一次或多次调用IoCreateDevice来创建它支持的设备对象。例如tcpip.sys在其DriverEntry中就创建了3个共用此驱动的设备对象:Tcp,Udp,Ip。目前有一种比较流行的称为WDM(Windows Driver Model)的驱动程序,在大多数情况下,其二进制映像可以兼容WIN98和WIN2000(32位版本)。WDM与NT内核模式驱动程序的主要区别在于如何创建设备:在WDM驱动程序中,即插即用(PnP)管理器告知何时向系统中添加一个设备,或者从系统中删除设备。WDM驱动程序有一个特殊的AddDevice例程,PnP管理器为共用该驱动的每个设备实例调用该函数;而NT内核模式驱动程序需要做大量额外的工作,它们必须探测自己的硬件,为硬件创建设备对象(通常在DriverEntry中),配置并初始化硬件使其正常工作。设备程序对象结构中的域大部分是透明的,驱动可以访问的域包括: PDRIVER_OBJECT DriverObject:指向代表驱动程序装载映象的驱动程序对象的指针。 所有I/O都是通过I/O请求包(IRP)驱动的。所谓IRP驱动,是指I/O管理器负责在系统的非分页内存中分配一定的空间,当接受用户发出的命令或由事件引发后,将工作指令按一定的数据结构置于其中并传递到驱动程序的服务例程。换言之,IRP中包含了驱动程序的服务例程所需的信息指令。IRP有两部分组成:固定部分(称为标题)和一个或多个堆栈单元。固定部分信息包括:请求的类型和大小,是同步请求还是异步请求,用于缓冲I/O的指向缓冲区的指针和由于请求的进展而变化的状态信息。 PMDL MdlAddress:指向一个内存描述符表(MDL),该表描述了一个与该请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为DO_DIRECT_IO,则I/O管理器为IRP_MJ_READ或IRP_MJ_WRITE请求创建这个MDL。如果一个IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则I/O管理器为该请求使用的输出缓冲区创建一个MDL。MDL本身用于描述用户模式虚拟缓冲区,但它同时也含有该缓冲区锁定内存页的物理地址。 PVOID AssociatedIrp.SystemBuffer:SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中于IRP_MJ_READ和IRP_MJ_WRITE操作,如果顶级设备指定DO_BUFFERED_IO标志I/O管理器就创建这个数据缓冲区。对于IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能代码指出需要缓冲区,则I/O管理器就创建这个数据缓冲区。I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,这也是创建IRP过程的一部分。这些数据可以是与WriteFile调用有关的数据,或者是DeviceIoControl调用中所谓的输入数据。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。对于指定了METHOD_BUFFERED的I/O控制操作,驱动程序把所谓的输出数据放到这个缓冲区, 然后I/O管理器再把数据复制到用户模式的输出缓冲区。 IO_STATUS_BLOCK IoStatus:IoStatus(IO_STATUS_BLOCK)是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。IoStatus.Status域将收到一个NTSTATUS代码。 PVOID UserBuffer:对于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。 任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的IO_STACK_LOCATION结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,另外还有一个堆栈单元供IRP的创建者使用。堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。 UCHAR MajorFunction:该IRP的主功能码。这个代码应该为类似IRP_MJ_READ一样的值,并与驱动程序对象中MajorFunction表的某个派遣函数指针相对应。 UCHAR MinorFunction:该IRP的副功能码。它进一步指出该IRP属于哪个主功能类。 PDEVICE_OBJECT DeviceObject:与该堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。 PFILE_OBJECT FileObject:内核文件对象的地址,IRP的目标就是这个文件对象。 下面简要介绍一下WINNT/2000下I/O请求处理流程。先看对单层驱动程序的同步的I/O请求:I/O请求经过子系统DLL子系统DLL调用I/O管理器中相应的服务。I/O管理器以IRP的形式给设备驱动程序发送请求。驱动程序启动I/O操作。在设备完成了操作并且中断CPU时,设备驱动程序服务于中断。最后I/O管理器完成I/O请求。以上六步只是一个非常粗略的描述,其中的中断处理和I/O完成阶段比较复杂。 当设备完成了I/O操作后,它将发出中断请求服务。设备中断发生时,处理器将控制权交给内核陷阱处理程序,内核陷阱处理程序将在它的中断调度表(IDT)中定位用于设备的ISR。驱动程序的ISR例程获得控制权后,它通常只在设备IRQL上停留获得设备状态所必需的一段时间,然后停止设备中断,接着它排队一个DPC并清除中断退出操作。IRQL降低至Dispatch/DPC级之前,所有中间优先级中断因而可以得到服务。当DPC例程得到控制时,它将启动设备队列中下一个I/O请求,然后完成中断服务。

用户系统信息:Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; InfoPath.2)
本帖被评分 1 次
分享到:
gototop
 

回复: 反病毒引擎设计(作者不详)Part4

当驱动的DPC例程执行完后,在I/O请求可以考虑结束之前还有一些工作要做。如某些情况下,I/O系统必须将存储在系统内存中的数据复制到调用者的虚拟地址空间中,如将操作结果记录在调用者提供的I/O状态块中或执行缓冲I/O的服务将数据返回给调用线程。这样当DPC例程调用I/O管理器完成原始I/O请求后,I/O管理器会为调用线程调用线程排队一个核心态APC。当线程被调度执行时,挂起的APC被交付。它将把数据和返回状态复制到调用者的地址空间,释放代表I/O操作的IRP,并将调用者的文件句柄或调用者提供的事件或I/O完成端口设置为有信号状态。如果调用者用异步I/O函数ReadFileEx和WriteFileEx指定了用户态APC,则此时还需要将用户态APC排队。最后可以考虑完成I/O。在文件或其它对象句柄上等待的线程将被释放。 基于文件系统设备的I/O请求处理过程与此是基本相同的,主要区别在于增加一个或多个附加的处理层。例如读文件操作,用户应用程序调用子系统库Kernel32.dll中的API函数ReadFile,ReadFile接着调用系统库Ntdll.dll中的NtReadFile,NtReadFile通过一个陷入指令(INT2E)将处理器模式提升至ring0。然后Ntoskrnl.exe中的系统服务调度程序KiSystemService将在系统服务调度表中定位Ntoskrnl.exe中的NtWReadFile并调用之,同时解除中断。此服务例程是I/O管理器的一部分。它首先检查传递给它们的参数以保护系统安全或防止用户模式程序非法存取数据,然后创建一个主功能代码为IRP_MJ_READ的IRP,并将之送到文件系统驱动程序的入口点。以下的工作会由文件系统驱动程序与磁盘驱动程序分层来完成。文件系统驱动程序可以重用一个IRP或是针对单一的I/O请求创建一组并行工作的关联(associated)IRP。执行IRP的磁盘驱动程序最后可能会访问硬件。对于PIO方式的设备,一个IRP_MJ_READ操作将导致直接读取设备的端口或者是设备实现的内存寄存器。尽管运行在内核模式中的驱动程序可以直接与其硬件会话,但它们通常都使用硬件抽象层(HAL)访问硬件:读操作最终会调用Hal.dll中的READ_PORT_UCHAR例程来从某个I/O口读取单字节数据。 WINNT/2000下设备和驱动程序的有着明显堆栈式层次结构:处于堆栈最底层的设备对象称为物理设备对象,或简称为PDO,与其对应的驱动程序称为总线驱动程序。在设备对象堆栈的中间某处有一个对象称为功能设备对象,或简称FDO,其对应的驱动程序称为功能驱动程序。在FDO的上面和下面还会有一些过滤器设备对象。位于FDO上面的过滤器设备对象称为上层过滤器,其对应的驱动程序称为上层过滤器驱动程序;位于FDO下面(但仍在PDO之上)的过滤器设备对象称为下层过滤器,其对应的驱动程序称为下层过滤器驱动程序。这种栈式结构可以使I/O请求过程更加明了。每个影响到设备的操作都使用IRP。通常IRP先被送到设备堆栈的最上层驱动程序,然后逐渐过滤到下面的驱动程序。每一层驱动程序都可以决定如何处理IRP。有时,驱动程序不做任何事,仅仅是向下层传递该IRP。有时,驱动程序直接处理完该IRP,不再向下传递。还有时,驱动程序既处理了IRP,又把IRP传递下去。这取决于设备以及IRP所携带的内容。 通过上面的介绍可得知:如果我们想拦截系统的文件操作,就必须拦截I/O管理器发向文件系统驱动程序的IRP。而拦截IRP最简单的方法莫过于创建一个上层过滤器设备对象并将之加入文件系统设备所在的设备堆栈中。具体方法如下:首先通过IoCreateDevice创建自己的设备对象,然后调用IoGetDeviceObjectPointer来得到文件系统设备(Ntfs,Fastfat,Rdr或Mrxsmb,Cdfs)对象的指针,最后通过IoAttachDeviceToDeviceStack将自己的设备放到设备堆栈上成为一个过滤器。 这是拦截IRP最常用也是最保险的方法,Art Baker的《Windows NT设备驱动程序设计指南》中有详细介绍,但用它实现病毒实时监控却存在两个问题:其一这种方法是将过滤器放到堆栈的最上层,当存在其它上层过滤器时就不能保证过滤器正好在文件系统设备之上;其二由于过滤器设备需要表现的和文件系统设备一样,这样其所有特性都需从文件系统设备中复制。另外文件系统驱动对象中调度例程过滤器驱动必须都支持,这就意味着我们无法使过滤器驱动中的调度例程供自己的ring3级客户程序所专用,因为原本发往文件系统驱动调度例程的IRP现在都会先从过滤器驱动的调度例程中经过。 所以Hooksys.sys没有使用上述方法。它的方法更简单且更为直接:它先通过ObReferenceObjectByName得到文件系统驱动对象的指针。然后将驱动对象中MajorFunction数组中的打开,关闭,清除,设置文件信息,和写入调度例程入口地址改为Hooksys.sys中相应钩子函数的入口地址来达到拦截IRP的目的。具体操作细节请参看代码剖析一节。 下面介绍驱动与ring3下客户程序的通信技术。与WIN9X下驱动与ring3下客户程序通信技术相同,NT/2000仍然支持使用DeviceIoControl实现从ring3到ring0的单向通信,但从ring0通过排队APC来唤醒ring3线程的方法却无法使用了。原因是我没有找到一个公开的函数来实现(Walter Oney的书中说存在一个未公开的函数实现从ring0排队APC)。其实不通过APC我们也可以通过命名的事件/信号量对象来实现双向唤醒,而且这可能比APC更为可靠些。 对象管理器在Windows NT/2000内核中占了极其重要的位置,其一个最主要职能是组织管理系统内核对象。在Windows NT/2000中,内核对象管理器大量引入了C++面向对象的思想,即所有内核对象都封装在对象管理器内部,除对象管理器自己以外,对其他所有想引用内核对象结构成员的子系统都是不透明的,也即都需通过对象管理器访问这些结构。Microsoft极力推荐内核驱动代码遵循这一原则(用户态代码根本不能直接访问这些数据),它提供了一系列以Ob开头的例程供我们使用。 内核已命名对象存于系统全局命名内核区,与传统的DOS目录和文件组织方式相似,对象管理器也采用树状结构管理这些对象,这样可以快速检索内核对象。当然使用这种树状结构组织内核已命名对象,还有另一个优点,那就是使所有已命名对象组织的十分有条理,如设备对象处于\Device下,而对象类型名称处于\ObjectTypes下等等。再者这样也能达到使用户态进程仅能访问\??与\BaseNamedObjects下的对象,而内核态代码则没有任何限制的目的。至于系统内部如何组织管理这些已命名对象,其实Windows NT/2000内部由内核变量ObpRootDirectoryObject指向的Directory对象代表根目录,使用哈希表(HashTable)来组织管理这些命名内核对象。 Hooksys.sys中使用命名的信号量来唤醒ring3级线程。具体做法如下:首先在guidll.dll中调用CreateSemaphore创建一个命名信号量Hookopen并设为无信号状态,同时调用CreateThread创建一个线程。线程代码的入口处通过调用WaitForSingleObject在此信号量上等待被ring0钩子函数唤醒查毒。驱动程序这边则在初始化过程中通过未公开的例程ObReferenceObjectByName(\BaseNamedObjects\Hookopen)得到命名信号量对象Hookopen的指针,当它拦截到文件打开请求时调用KeReleaseSemaphore将Hookopen置为有信号状态唤醒ring3级等待检查打开文件的线程。其实guidll.dll共创建了两个命名信号量,还有一个Hookclose用于唤醒ring3级等待检查关闭文件的线程。 guidll.dll中使用命名的事件来唤醒暂时挂起等待查毒完毕的ring0钩子函数。具体做法如下:Hooksys.sys在其初始化过程中通过ZwCreateEvent函数创建一组命名事件对象(此处必须合理设置安全描述符,否则ring3线程将无法使用事件句柄)并得到其句柄,同时通过ObReferenceObjectByHandle得到句柄引用的事件对象的指针。然后Hooksys.sys将这一组事件句柄和指针对以及事件名保存在备用链表的每个元素中:ring3使用句柄,ring0使用指针。当钩子函数拦截到文件请求时它首先唤醒ring3查毒线程,然后马上调用KeWaitForSingleObject在一个事件\BaseNamedObjects\Hookxxxx上等待查毒的完成。而被唤醒的ring3查毒线程通过OpenEventA函数由事件名字得到其句柄,在结束查毒后发出一个SetEvent调用将事件置为有信号状态从而唤醒ring0挂起的钩子函数。当然,以上讨论仅限于打开文件操作,钩子函数在拦截到其它文件请求时并不调用KeWaitForSingleObject等待查毒的完成,而是唤醒ring3查毒线程后直接返回;相应的ring3查毒线程也就不必在查毒完成后调用SetEvent进行远程唤醒。 另外在编写NT内核模式驱动程序时还必须注意一些事项。首先是中断请求级(IRQL),这是在进行NT驱动编程时特别值得注意的问题。每个内核例程都要求在一定的IRQL上运行,如果在调用时不能确定当前IRQL在哪个级别,则可调用KeGetCurrentIrql获取当前的IRQL值并进行判断。例如欲获得指向当前进程Eprocess的指针可以考虑先判断当前的IRQL,如大于等于DISPATCH_LEVEL时可调用IoGetCurrentProcess;而当IRQL小于调度/延迟过程调用级别时(DISPATCH_LEVEL/DPC)则可使用PsGetCurrentProcessId和PsLookupProcessByProcessId。其次要注意的问题是分页/非分页内存。由于执行在提升的IRQL级上时系统将不能处理页故障,因为系统在APC级处理页故障,因而这里总的原则是:执行在高于或等于DISPATCH_LEVEL级上的代码绝对不能造成页故障。这也意味着执行在高于或等于DISPATCH_LEVEL级上的代码必须存在于非分页内存中。此外,所有这些代码要访问的数据也必须存在于非分页内存中。最后是同步互斥问题,这对于如病毒实时监控等系统范围共享的驱动程序尤显重要。虽然在Hooksys中没有创建多线程(PsCreateSystemThread),但由于它挂接了系统文件钩子,系统中所有线程的文件请求都会从Hooksys中经过。当一个线程的文件请求被处理过程中Hooksys会去访问一些全局共享的数据,如过滤器,历史记录等,有可能在访问进行到一半时该线程由于某种原因被抢占了,结果是其它线程的文件请求经过时Hooksys访问的共享数据将是错误的。为此驱动程序必须合理使用自旋锁,互斥量,资源等内核同步对象对共享全局数据的所有线程进行同步。 3.4.2程序结构与流程 以下的程序结构与流程分析来自一著名反病毒软件的WINNT/2000实时监控NT内核模式设备驱动程序Hooksys.sys: 1.初始化例程(DriverEntry):调用_GetProcessNameOffset取得进程名在Eprocess中的偏移。初始化备用,打开文件等待操作,关闭文件,历史记录5个双向循环链表及用于链表操作互斥的4把自旋锁和1个快速互斥量。将全局变量_IrqCount(IRP记数)设置为0。创建卸载保护用事件对象。为文件名过滤数组初始化同步用资源变量。在系统全局命名内核区中检索Hookopen和Hookclose两个命名信号量( _CreateSemaphore)。为备用(_AllocateBuffer)和历史记录(_AllocatHistoryBuf)链表在系统非分页池中分配空间,同时创建一组命名事件对象Hookxxxx并保存至备用链表的每个元素中(_CreateOneEvent)。创建设备,设置驱动例程入口,为设备建立符号连接。创建磁盘驱动器设备对象指针(_QuerySymbolicLink)和文件系统驱动程序对象指针(_HookSys)列表。 2.打开例程(IRP_MJ_CREATE):将备用链表用系统非分页内存(首地址保存在_SysBufAddr中)映射到用户空间中(保存在_UserBufAddr)以便从用户态可以直接访问这段内存(_MapMemory)。
gototop
 

回复: 反病毒引擎设计(作者不详)Part4

3.设备控制例程(IRP_MJ_DEVICE_CONTROL):它会从入口IRP当前堆栈单元中取得用户程序利用DeviceIoControl传送进来的IO控制代码(IoControlCode),以此判断用户程序的意图。和Hooksys.sys协同工作的ring3级客户程序guidll.dll会依次向Hooksys.sys发送IO控制请求来完成一系列工作,具体次序和代码含义如下: 83003C2F:将guidll取得的驱动器类型值传给驱动(保存在DriverType变量中),根据此变量值的不同,设置不同的等待(KeWaitForSingleObject)超时值,因为非固定驱动器的读写时间会稍长些。 83003C0F:保存guidll传送的用户指定的拦截文件的类型,其实这个类型过滤器在查毒模块中已存在,这里再设置显然是为了提高处理效率:它确保不会将非指定类型文件送到ring3级查毒模块,节省了通信的开销。经过解析的各文件类型过滤块指针将保存在_gaFileNameFilterArra数组中,同时更新过滤项个数_gNumOfFilters变量的值。 83003C13:修改文件系统驱动程序对象调度例程入口,启动拦截文件操作的钩子函数的工作。 83003C17:恢复文件系统驱动程序原调度例程入口,停止拦截文件操作的钩子函数工作。 以上列出的IO控制代码的发出是固定,而当钩子函数启动后,还会发出一些随机的控制代码: 83003C07:驱动将打开文件链表的头元素即最先的请求打开的文件删除并插入到等待链表尾部,同时将元素的用户空间地址传送至ring3级等待查杀打开文件的线程中处理。 83003C0B:驱动将关闭文件链表的头元素即最先的请求关闭的文件删除并插入到备用链表尾部,同时将元素中的文件名串传送至ring3级等待查杀关闭文件的线程中处理 83003C1F:当查得关闭文件是病毒时,更新历史记录链表。 下面介绍钩子函数_HookCreateDispatch和guidll中等待查杀打开文件的线程协同工作流程,而关闭,清除,设置文件信息,和写入操作的处理与此大同小异: 当文件请求进入钩子函数_HookCreateDispatch后,它首先从入口IRP中定位当前的堆栈单元并从中取得代表此次请求的文件对象。然后判断当前进程是否为我们自己,若是则必须放过去,因为查毒模块中要频繁的进行文件操作,所以拦截来自ravmon的文件请求将导致严重的系统死锁。接下来利用堆栈单元中的文件对象取得完整的文件路径名并确保文件不是:\PIPE\,\IPC。之后查找历史记录链表以确定该文件是否最近曾被检查并记录过,若在历史记录链表中找到关于该文件的记录并且记录未失效即其时间戳和当前系统时间之差不得大于1F4h,则可直接从记录中读取查毒结果。如历史链表中没有该文件的记录则利用保存的文件类型过滤阵列检查文件是否在被拦截的文件类型之列。至此才进入真正的检查打开文件函数_RAVCheckOpenFile,此函数入口处先从备用,等待或关闭链表头部摘得一空闲元素(_GetFreeEntry)并填充之,如文件路径名域等。接着将空闲元素加入打开文件链表尾部并释放Hookopen信号量唤醒ring3下等待检查打开文件的线程。然后调用KeWaitForSingleObject在空闲元素中保存的一个事件对象上等待ring3查毒的完成。当钩子函数挂起后,ring3查毒线程得到执行:它会向驱动发出一IO控制码为83003C07的请求以取得打开文件链表头元素即保存最先提交而未决的文件请求,驱动会将元素映射到用户空间中的偏移地址直接传给它。接着它调用RsEngine.dll中的fnScanOneFile函数进行查毒并在元素中设置查毒结果位,完毕后再对元素中保存的事件对象调用SetEvent唤醒在此事件上等待的钩子函数。被唤醒的钩子函数检查被ring3查毒代码设置的结果位以此决定该文件请求是被采纳即调用保存的原调度例程还是被取消即调用IofCompleteRequest直接返回,同时增加历史记录。 以上只是钩子函数与ring3线程流程的一个简单介绍,其中省略了诸如判断固定驱动器,超时等内容,具体细节请参看guidll.dll和hooksys.sys的反汇编代码注释。 4.关闭例程(IRP_MJ_CLOSE):停止钩子函数工作,恢复文件系统驱动程序原调度入口(_StopFilter)。解除到用户空间的内存映射。 5.卸载例程(DriverUnload):停止钩子函数工作,恢复文件系统驱动程序原调度入口。删除设备和符号连接。删除初始化时创建的一组命名事件对象Hookxxxx,包括解除指针引用,关闭打开的句柄。释放为MDL(_pMdl),备用链表(_SysBufAddr),历史记录链表(_HistoryBuf)和过滤器分配的内存空间。删除为文件名过滤数组访问同步设置的资源变量(_FilterResource)。解除对系统全局命名内核区中Hookopen和Hookclose两个命名信号量的指针引用。 3.4.3HOOKSYS.SYS逆向工程代码剖析 3.4.3.1取得当前进程名称代码 初始化例程中取得进程名在Eprocess中偏移 00011889 call ds:__imp__IoGetCurrentProcess@0 ;得到当前进程System的Eprocess指针 0001188F mov edi, eax ;Eprocess基地址 00011891 xor esi, esi ;初始化偏移为0 00011893 lea eax, [esi+edi] ;扫描指针 00011896 push 6 ;进程名长度 00011898 push eax ;扫描指针 00011899 push offset $SG8452 ; "System" ;进程名串 0001189E call ds:__imp__strncmp ;比较扫描指针处是否为进程名 000118A4 add esp, 0Ch ;恢复堆栈 000118A7 test eax, eax ;测试比较结果 000118A9 jz short loc_118B9 ;找到则跳出循环 000118AB inc esi ;增加偏移量 000118AC cmp esi, 3000h ;在12K范围中扫描 000118B2 jb short loc_11893 ;在范围之内则继续比较 钩子函数开始处取得当前进程名 00010D1E call ds:__imp__IoGetCurrentProcess@0 ;得到当前进程System的Eprocess指针 00010D24 mov ecx, _ProcessNameOffset ;取得保存的进程名偏移量 00010D2A add eax, ecx ;得到指向进程名的指针 3.4.3.2启动钩子函数工作代码 000114F4 push 4 ;预先将文件系统驱动对象个数压栈 000114F6 mov esi, offset FsDriverObjectPtrList ;取得文件系统驱动对象指针列表偏移地址 000114FB pop edi ;用EDI做记数器,初始值为4 000114FC mov eax, [esi] ;取得第一个驱动对象的指针 000114FE test eax, eax ;测试是否合法 00011500 jz short loc_11548 ;不合法则继续下一个修改驱动对象 00011502 mov edx, offset _HookCreateDispatch@8 ;取得自己的钩子函数的偏移地址 00011507 lea ecx, [eax+38h] ;取得对象中打开调度例程(IRP_MJ_CREATE)偏移 0001150A call @InterlockedExchange@8 ;原子操作,替换驱动对象中打开调度例程的入口为钩子函数的偏移地址 0001150F mov [esi-10h], eax ;保存原打开调度例程的入口    3.4.3.3映射系统内存至用户空间代码 0001068E push esi ;系统内存大小 0001068F push _SysBufAddr ;系统内存基地址 00010695 call ds:__imp__MmSizeOfMdl@8 ;计算描述系统内存所需内存描述符表(MDL)大小 0001069B push 206B6444h ;调试用标签 000106A0 push eax ;MDL大小 000106A1 push 0 ;在系统非分页内存池中分配 000106A3 call ds:__imp__ExAllocatePoolWithTag@12 ;为MDL分配内存 000106A9 push esi ;系统内存大小 000106AA mov _pMdl, eax ;保存MDL指针 000106AF push _SysBufAddr ;系统内存基地址 000106B5 push eax ;MDL指针 000106B6 call ds:__imp__MmCreateMdl@12 ;初始化MDL 000106BC push eax ;MDL指针 000106BD mov _pMdl, eax ;保存MDL指针 000106C2 call ds:__imp__MmBuildMdlForNonPagedPool@4 ;填写MDL后物理页面数组 000106C8 push 1 ;访问模式 000106CA push _pMdl ;MDL指针 000106D0 call ds:__imp__MmMapLockedPages@8 ;映射MDL描述的物理内存页面 ...... 000106DB mov _UserBufAddr, eax ;保存映射后的用户空间地址 _UserBufAddr 和_SysBufAddr映射到相同的物理地址。    结 论 至此本论文已告撰写完毕。本论文在介绍了诸多目前较为流行的病毒技术后着重讨论了当今两大反病毒技术:虚拟机和实时监控。 我参与开发的w32encode是一个功能完备且结构复杂的商用虚拟机,它属于32位自含指令式虚拟机,与其它搜索清除模块合并在一起组成了一个功能强大的反病毒引擎。虽然目前它还不能支持所有的386+指令集,但从其查杀毒的运行效果来看结果还是非常令人满意的:普通的加密变形病毒可以在虚拟机默认的处理常式中查杀;特殊的,如hps,marburg等复杂加密变形病毒则可通过向虚拟机中添加少量的病毒特定处理代码来完成查杀。由于反虚拟执行技术的出现,所以今后对此虚拟机源代码的更新--向其中添加更多的对操作系统机制的支持--或者重写--成为真正的虚拟机器而非虚拟CPU--将是不可避免的。 同时,我通过逆向工程某反病毒软件的实时监控程序,在系统原理和驱动编程上又有了新的认识,并且它大大增强了我的反汇编功力。今后我会将注释的反汇编代码编写成C语言版源代码,并把病毒扫描模块移到系统核心态下工作,从而使整个工程变为“主动的与内核无缝连接”式监控。 总之当今反病毒技术的主流发展方向是屏弃传统的特征码扫描,创建智能的监控与行为分析引擎,这就必然要求更加先进的虚拟机和实时监控技术。
gototop
 

回复:反病毒引擎设计(作者不详)Part4

完。
gototop
 

回复:反病毒引擎设计(作者不详)Part4

菜鸟啊,看昏了,还是看不明白!
杀毒是一种游戏;杀毒是一种境界;杀毒是一种宽容;杀毒也是一种谅解!
杀杀杀!杀尽天下毒马!饿饿饿!饿死天下毒猪!气气气!气死天下毒狗!
不要以为机子无毒就不杀毒;也不要以为机子有毒就乱杀毒!
瑞星是我家,爱它等于爱大家!
gototop
 
1   1  /  1  页   跳转
页面顶部
Powered by Discuz!NT