网络安全 频道

反病毒引擎设计之绪论篇

1.2.3截获系统操作

截获系统操作是病毒惯用的伎俩。DOS时代如此,WINDOWS时代也不例外。在DOS下,病毒通过在中断向量表中修改INT21H的入口地址来截获DOS系统服务(DOS利用INT21H来提供系统调用,其中包括大量的文件操作)。而大部分引导区病毒会接挂INT13H(提供磁盘操作服务的BIOS中断)从而取得对磁盘访问的控制。WINDOWS下的病毒同样找到了钩挂系统服务的办法。比较典型的如CIH病毒就是利用了IFSMGR.VXD(可安装文件系统)提供的一个系统级文件钩子来截获系统中所有文件操作,我会在相关章节中详细讨论这个问题,因为WIN9X下的实时监控也主要利用这个服务。除此之外,还有别的方法。但效果没有这个系统级文件钩子好,主要是不够底层,会丢失一些文件操作。

其中一个方法是利用APIHOOK,钩挂API函数。其实系统中并没有现成的这种服务,有一个SetWindowsHookEx可以钩住鼠标消息,但对截获API函数则无能为力。我们能做的是自己构造这样的HOOK。方法其实很简单:比如你要截获Kernel32.dll导出的函数CreateFile,只须在其函数代码的开头(BFF7XXXX)加入一个跳转指令到你的钩子函数的入口,在你的函数执行完后再跳回来。如下图所示:

;; Target Function(要截获的目标函数)
  ……
  TargetFunction:(要截获的目标函数入口)
  jmp DetourFunction(跳到钩子函数,5个字节长的跳转指令)
  TargetFunction+5:
  push edi
  ……
  ;; Trampoline(你的钩子函数)
  ……
  TrampolineFunction:(你的钩子函数执行完后要返回原函数的地方)
  push ebp
  mov ebp,esp
  push ebx
  push esi(以上几行是原函数入口处的几条指令,共5个字节)
  jmp TargetFunction+5(跳回原函数)
  ……


但这种方法截获的仅仅是很小一部分文件打开操作。

在WIN9X下还有一个鲜为人知的截获文件操作的办法,说起来这应该算是WIN9X的一大后门。它就是Kernel32.dll中一个未公开的叫做VxdCall0的API函数。反汇编这个函数的代码如下:

mov eax,dword ptr [esp+00000004h] ;取得服务代号
pop dword ptr [esp] ;堆栈修正
call fword ptr cs:[BFFC9004] ;通过一个调用门调用3B段某处的代码


如果我们继续跟踪下去,则会看到:

003B:XXXXXXXX int 30h ;


这是个用以陷入VWIN32.VXD的保护模式回调

有关VxdCall的详细内容,请参看Matt Pietrek的《Windows 95 System Programming Secrets》。

当服务代号为0X002A0010时,保护模式回调会陷入VWIN32.VXD中一个叫做VWIN32_Int21Dispatch的服务。这正说明了WIN9X还在依赖于MSDos,尽管微软声称WIN9X不再依赖于MSDos。调用规范如下:

my_int21h:push ecx
  push eax ;类似DOS下INT21H的AX中传入的功能号
  push 002A0010h
  call dword ptr [ebp+a_VxDCall]
  ret


我们可以将上面VxdCall0函数的入口处第三条远调用指令访问的Kernel32.dll数据段中用户态可写地址BFFC9004Υ娲⒌?FWORD''六个字节改为指向我们自己钩子函数的地址,并在钩子中检查传入服务号和功能号来确定是否是请求VWIN32_Int21Dispatch中的某个文件服务。著名的HPS病毒就利用了这个技术在用户态下直接截获系统中的文件操作,但这种方法截获的也仅仅是一小部分文件操作。

1.2.4加密变形病毒

加密变形病毒是虚拟机一章的重点内容,将放到相关章节中介绍。

1.2.5反跟踪/反虚拟执行病毒

反跟踪/反虚拟执行病毒和虚拟机联系密切,所以也将放到相应的章节中介绍。

1.2.6直接API调用

直接API调用是当今WIN32病毒常用的手段,它指的是病毒在运行时直接定位API函数在内存中的入口地址然后调用之的一种技术。普通程序进行API调用时,编译器会将一个API调用语句编译为几个参数压栈指令后跟一条间接调用语句(这是指Microsoft编译器,Borland编译器使用JMP

DWORD PTR [XXXXXXXXh])形式如下:
  push arg1
  push arg2
  ……
  call dword ptr[XXXXXXXXh]


地址XXXXXXXXh在程序映象的导入(Import Section)段中,当程序被加载运行时,由装入器负责向里面添入API函数的地址,这就是所谓的动态链接机制。病毒由于为了避免感染一个可执行文件时在文件的导入段中构造病毒体代码中用到的API的链接信息,它选择运用自己在运行时直接定位API函数地址的代码。其实这些函数地址对于操作系统的某个版本是相对固定的,但病毒不能依赖于此。现在较为流行的做法是先定位包含API函数的动态连接库的装入基址,然后在其导出段(Export Section)中寻找到需要的API地址。后面一步几乎没有难度,只要你熟悉导出段结构即可。关键在于第一步--确定DLL装入地址。其实系统DLL装入基址对于操作系统的某个版本也是固定的,但病毒为确保其稳定性仍不能依赖这一点。目前病毒大都利用一个叫做结构化异常处理的技术来捕获病毒体引发的异常。这样一来病毒就可以在一定内存范围内搜索指定的DLL(DLL使用PE格式,头部有固定标志),而不必担心会因引发页面错误而被操作系统杀掉。

由于异常处理和后面的反虚拟执行技术密切相关,所以特将结构化异常处理简单解释如下:

共有两类异常处理:最终异常处理和每线程异常处理。

其一:最终异常处理

当你的进程中无论哪个线程发生了异常,操作系统将调用你在主线程中调用SetUnhandledExceptionFilter建立的异常处理函数。你也无须在退出时拆去你安装的处理代码,系统会为你自动清除。

PUSH OFFSET FINAL_HANDLER 
  CALL SetUnhandledExceptionFilter 
  ……
  CALL ExitProcess 
  ;************************************ 
  FINAL_HANDLER: 
  …… 
  ;(eax=-1 reload context and continue) 
  MOV EAX,1 
  RET ;program entry point 
  ……
  ;code covered by final handler 
  ……
  ;code to provide a polite exit 
  ……
  ;eax=1 stops display of closure box 
  ;eax=0 enables display of the box


其二:每线程异常处理

FS中的值是一个十六位的选择子,它指向包含线程重要信息的数据结构TIB,线程信息块。其的首双字节指向我们称为ERR的结构:

1st dword +0 pointer to next err structure
(下一个err结构的指针) 
2nd dword +4 pointer to own exception handler
(当前一级的异常处理函数的地址)


所以异常处理是呈练状的,如果你自己的处理函数捕捉并处理了这个异常,那么当你的程序发生了异常时,操作系统就不会调用它缺省的处理函数了,也就不会出现一个讨厌的执行了非法操作的红叉。

下面是cih的异常段:

MyVirusStart:
  push ebp
  lea eax, [esp-04h*2]
  xor ebx, ebx
  xchg eax, fs:[ebx] ;交换现在的err结构和前一个结构的地址
  ; eax=前一个结构的地址
  ; fs:[0]=现在的err结构指针(在堆栈上)
  call @0
  @0:
  pop ebx
  lea ecx, StopToRunVirusCode-@0[ebx] ;你的异常处理函数的偏移
  push ecx ;你的异常处理函数的偏移压栈
  push eax ;前一个err结构的地址压栈
  ;构造err结构,记这时候的esp(err结构指针)为esp0
  ……
  StopToRunVirusCode:
  @1 = StopToRunVirusCode
  xor ebx, ebx ;发生异常时系统在你的练前又加了一个err结构,
                       ;所以要先找到原来的结构地址
  mov eax, fs:[ebx] ; 取现在的err结构的地址eax
  mov esp, [eax] ; 取下个结构地址即eps0到esp
  RestoreSE: ;没有发生异常时顺利的回到这里,你这时的esp为本esp0
  pop dword ptr fs:[ebx] ;弹出原来的前一个结构的地址到fs:0
  pop eax ;弹出你的异常处理地址,平栈而已


1.2.7病毒隐藏

实现进程或模块隐藏应该是一个成功病毒所必须具备的特征。在WIN9X下Kernel32.dll有一个可以使进程从进程管理器进程列表中消失的导出函数RegisterServiceProcess ,但它不能使病毒逃离一些进程浏览工具的监视。但当你知道这些工具是如何来枚举进程后,你也会找到对付这些工具相应的办法。进程浏览工具在WIN9X下大都使用一个叫做ToolHelp32.dll的动态连接库中的Process32First和Process32Next两个函数来实现进程枚举的;而在WINNT/2000里也有PSAPI.DLL导出的EnumProcess可用以实现同样之功能。所以病毒就可以考虑修改这些公用函数的部分代码,使之不能返回特定进程的信息从而实现病毒的隐藏。

但事情远没有想象中那么简单,俗话说“道高一尺,魔高一丈”,此理不谬。由于现在很多逆项工程师的努力,微软力图隐藏的许多秘密已经逐步被人们所挖掘出来。当然其中就包括WINDOWS内核使用的管理进程和模块的内部数据结构和代码。比如WINNT/2000用由ntoskrnl.exe导出的内核变量PsInitialSystemProcess所指向的进程Eprocess块双向链表来描述系统中所有活动的进程。如果进程浏览工具直接在驱动程序的帮助下从系统内核空间中读出这些数据来枚举进程,那么任何病毒也无法从中逃脱。

有关Eprocess的具体结构和功能,请参看David A.Solomon和Mark E.Russinovich的《Inside Windows2000》第三版。

1.2.8病毒特殊感染法

对病毒稍微有些常识的人都知道,普通病毒是通过将自身附加到宿主尾部(如此一来,宿主的大小就会增加),并修改程序入口点来使病毒得到击活。但现在不少病毒通过使用特殊的感染技巧能够使宿主大小及宿主文件头上的入口点保持不变。

附加了病毒代码却使被感染文件大小不变听起来让人不可思议,其实它是利用了PE文件格式的特点:PE文件的每个节之间留有按簇大小对齐后的空洞,病毒体如果足够小则可以将自身分成几份并分别插入到每个节最后的空隙中,这样就不必额外增加一个节,因而文件大小保持不变。著名的CIH病毒正是运用这一技术的典型范例(它的大小只有1K左右)。

病毒在不修改文件头入口点的前提下要想获得控制权并非易事:入口点不变意味着程序是从原程序的入口代码处开始执行的,病毒必须要将原程序代码中的一处修改为导向病毒入口的跳转指令。原理就是这样,但其中还存在很多可讨论的地方,如在原程序代码的何处插入这条跳转指令。一些查毒工具扫描可执行文件头部的入口点域,如果发现它指向的地方不正常,即不在代码节而在资源节或重定位节中,则有理由怀疑文件感染了某种病毒。所以刚才讨论那种病毒界称之为EPO(入口点模糊)的技术可以很好的对付这样的扫描,同时它还是反虚拟执行的重要手段。

另外值得一提的是现在不少病毒已经支持对压缩文件的感染。如Win32.crypto病毒就可以感染ZIP,ARJ,RAR,ACE,CAB 等诸多类型的压缩文件。这些病毒的代码中含有对特定压缩文件类型解压并压缩的代码段,可以先把压缩文件中的内容解压出来,然后对合适的文件进行感染,最后再将感染后文件压缩回去并同时修改压缩文件头部的校验和。目前不少反病毒软件都支持查多种格式的压缩文件,但对有些染毒的压缩文件无法杀除。原因我想可能是怕由于某种缘故,如解压或压缩有误,校验和计算不对等,使得清除后压缩文件格式被破坏。病毒却不用对用户的文件损坏负责,所以不存在这种担心。【未完待续】

查阅下一篇:反病毒引擎设计之虚拟机查毒篇

主要参考文献

David A. Solomon, Mark Russinovich 《Inside Microsoft Windows 2000》September 2000

David A. Solomon 《Inside Windows NT》 May 1998

Prasad Dabak,Sandeep Phadke,Milind Borate 《Undocumented Windows NT》October 1999

Matt Pietrek 《Windows 95 System Programming Secrets》 March 1996

Walter Oney 《System Programming for Windows 95》 March 1996

Walter Oney 《Programming the Windows Driver Model》 1999

陆麟 《WINDOWS9X文件读写Internal》2001 http://www.hack58.net/Article/60/64/2006/7957.htm
0
相关文章