⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 反病毒引擎设计之实时监控篇.txt

📁 学习(编程技巧_编程知识_程序代码),是学习编程不可多得的学习精验
💻 TXT
📖 第 1 页 / 共 5 页
字号:
由此可见通过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系统程式设计》。 

3.3.2程序结构与流程 

以下的程序结构与流程分析来自一著名反病毒软件的WIN9X实时监控虚拟设备驱动程序Hooksys.vxd: 

1.当VXD收到来自VMM的ON_SYS_DYNAMIC_DEVICE_INIT消息--需要注意这是个动态VXD,它不会收到系统虚拟机初始化时发送的Sys_Critical_Init, Device_Init和Init_Complete控制消息--时,它开始初始化一些全局变量和数据结构,包括在堆上分配内存(HeapAllocate),创建备用,历史记录,打开文件,等待操作,关闭文件5个双向循环链表及用于链表操作互斥的5个信号量(调用Create_Semaphore),同时将全局变量_gNumOfFilters即文件名过滤项个数设置为0。 

2.当VXD收到来自VMM的ON_W32_DEVICEIOCONTROL消息时,它会从入口参数中取得用户程序利用DeviceIoControl传送进来的IO控制代码(IOCtlCode),以此判断用户程序的意图。和Hooksys.vxd协同工作的ring3级客户程序guidll.dll会依次向Hooksys.vxd发送IO控制请求来完成一系列工作,具体次序和代码含义如下: 

83003C2B:将guidll取得的操作系统版本传给驱动(保存在iOSversion变量中),根据此变量值的不同,从ring0tcb结构中提取某些域时将采用不同的偏移,因为操作系统版本不同会影响内核数据结构。 

83003C1B:初始化后备链表,将guidll传入的用OpenVxdHandle转换过的一组事件指针保存在每个链表元素中。 

83003C2F:将guidll取得的驱动器类型值传给驱动(保存在DriverType变量中),根据此变量值的不同,调用VWIN32_WaitSingleObject设置不同的等待超时值,因为非固定驱动器的读写时间可能会稍长些。 

83003C0F:保存guidll传送的用户指定的拦截文件的类型,其实这个类型过滤器在查毒模块中已存在,这里再设置显然是为了提高处理效率:它确保不会将非指定类型文件送到ring3级查毒模块,节省了通信的开销。经过解析的各文件类型过滤块指针将保存在_gaFileNameFilterArra数组中,同时更新过滤项个数_gNumOfFilters 变量的值。 

83003C23:保存guidll中等待查杀打开文件的APC函数地址和当前线程KTHREAD指针。 

83003C13:安装系统文件钩子,启动拦截文件操作的钩子函数FilemonHookProc的工作。 

83003C27:保存guidll中等待查杀关闭文件的APC函数地址和当前线程KTHREAD指针。 

83003C17:卸载系统文件钩子,停止拦截文件操作的钩子函数FilemonHookProc的工作。 

以上列出的IO控制代码的发出是固定,而当钩子函数启动后,还会发出一些随机的控制代码: 

83003C07:驱动将打开文件链表的头元素即最先的请求打开的文件删除并插入到等待链表尾部,同时将元素的用户空间地址传送至ring3级等待查杀打开文件的APC函数中处理。 

83003C0B:驱动将关闭文件链表的头元素即最先的请求关闭的文件删除并插入到备用链表尾部,同时将元素中的文件名串传送至ring3级等待查杀关闭文件的APC函数中处理 

83003C1F:当查得关闭文件是病毒时,更新历史记录链表。 

下面介绍钩子函数和guidll中等待查杀打开文件的APC函数协同工作流程,写文件和关闭文件的处理与之类似: 

当文件请求进入钩子函数FilemonHookProc后,它先从入口参数中取得被执行的函数的代号并判断其是否为打开操作(IFSFN_OPEN 24H),若非则马上将这个IRQ向下传递,即构造入口参数并调用保存在PrevIFSHookProc中前一个钩子函数;若是则程序流程转向打开文件请求的处理分支。分支入口处首先要判断当前进程是否是我们自己,若是则必须放过去,因为查毒模块中要频繁的进行文件操作,所以拦截来自自身的文件请求将导致严重的系统死锁。接下来是从堆栈参数中取得完整的文件路径名并通过保存的文件类型过滤阵列检查其是否在拦截类型之列,如通过则进一步检查文件是否是以下几个须放过的文件之一:SYSTEM.DAT,USER.DAT,\PIPE\。然后查找历史记录链表以确定该文件是否最近曾被检查并记录过,若在历史记录链表中找到关于该文件的记录并且记录未失效即其时间戳和当前系统时间之差不得大于1F4h,则可直接从记录中读取查毒结果。至此才进入真正的检查打开文件函数_RAVCheckOpenFile,此函数入口处先从备用,等待或关闭链表头部摘得一空闲元素(_GetFreeEntry)并填充之(文件路径名域等)。接着通过一内核未公开的数据结构中的值(ring3tcb->Flags)判断可否对该文件请求排队APC。如可则将空闲元素加入打开文件链表尾部并排队一个ring3级检查打开文件函数的APC。然后调用_VWIN32_WaitSingleObject在空闲元素中保存的一个事件对象上等待ring3查毒的完成。当钩子函数挂起不久后,ring3的APC函数得到执行:它会向驱动发出一IO控制码为83003C07的请求以取得打开文件链表头元素即保存最先提交而未决的文件请求,驱动可以将内核空间中元素的虚拟地址直接传给它而不必考虑将之重新映射。实际上由于WIN9X内核空间没有页保护因而ring3级程序可以直接读写之。接着它调用RsEngine.dll中的fnScanOneFile函数进行查毒并在元素中设置查毒结果位,完毕后再对元素中保存的事件对象调用SetEvent唤醒在此事件上等待的钩子函数。被唤醒的钩子函数检查被ring3查毒代码设置的结果位以此决定该文件请求是被采纳即继续向下传递还是被取消即在EAX中放入-1后直接返回,同时增加历史记录。 

以上只是钩子函数与APC函数流程的一个简单介绍,其中省略了诸如判断固定驱动器,超时等内容,具体细节请参看guidll.dll和hooksys.vxd的反汇编代码注释。 

3.当VXD收到来自VMM的ON_SYS_DYNAMIC_DEVICE_EXIT消息时,它释放初始化时分配的堆内存(HeapFree),并清除5个用于互斥的信号量(Destroy_Semaphore)。 

3.3.3HOOKSYS.VXD逆向工程代码剖析 

在剖析代码之前有必要介绍一下逆向工程的概念。逆向工程(Reverse Engineering)是指在没有源代码的情况下对可执行文件进行反汇编试图理解机器码本身的含义。逆向工程的用途很多,如摘掉软件保护,窥视其设计和编写技术,发掘操作系统内部奥秘等。本文中我们用到的不少未公开数据结构和服务就是利用逆向的方法得到的。逆向工程的难度可想而知:一个1K大小的exe文件反汇编后就有1000行左右,而我们要逆向的3个文件加起来有80多K,总代码量是8万多行。所以必须掌握一定的逆向技巧,否则工作起来将是非常困难的。 

首先要完成逆向工作,必须选择优秀的反汇编及调试跟踪工具。IDA(The Interactive Disassembler)是一款功能强大的反汇编工具:它以交互能力强而著称,允许使用者增加标签,注释及定义变量,函数名称;另外不少反汇编工具对于特殊处理的反逆向文件,如导入节损坏等显得无能为力,但IDA仍可胜任之。当文件被加过壳或插入了干扰指令时 就需要使用调试工具进行动态跟踪。Numega公司的Softice是调试工具中的佼佼者:它支持所有类型的可执行文件,包括vxd和sys驱动程序,能够用热键实时呼出,可对代码执行,内存和端口访问设置断点,总之功能非常之强大以至于连微软总裁比尔盖茨对此都惊叹不已。 

其次需要对编译器常用的编译结构有一定了解,这样有助于我们理解代码的含义。 

如下代码是MS编译器常用的一种编译高级语言函数的形式: 

0001224A push ebp ;保存基址寄存器
0001224B mov ebp, esp
0001224D sub esp, 5Ch ;在堆栈留出局部变量空间
00012250 push ebx
00012251 push esi
00012252 push edi
......
0001225B lea edi, [ebp-34h] ;引用局部变量
......
0001238D mov esi, [ebp+08h] ;引用参数
......
00012424 pop edi
00012425 pop esi
00012426 pop ebx
00012427 leave
00012428 retn 8 ;函数返回
如下代码是MS编译器常用的一种编译高级语言取串长度的形式:
0001170D lea edi, [eax+1Ch] ;串首地址指针 
00011710 or ecx, 0FFFFFFFFh ;将ecx置为-1
00011713 xor eax, eax ;扫描串结束符号(NULL) 
00011715 push offset 00012C04h ;编译器优化 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -