分享
 
 
 

反病毒引擎设计全解

王朝other·作者佚名  2008-05-18
窄屏简体版  字體: |||超大  

本文将对当今先进的病毒/反病毒技术做全面而细致的介绍,重点当然放在了反病毒上,特别是虚拟机和

实时监控技术。文中首先介绍几种当今较为流行的病毒技术,包括获取系统核心态特权级,驻留,截获系

统操作,变形和加密等。然后分五节详细讨论虚拟机技术:第一节简单介绍一下虚拟机的概论;第二节介

绍加密变形病毒,作者会分析两个著名变形病毒的解密子;第三节是虚拟机实现技术详解,其中会对两种

不同方案进行比较,同时将剖析一个查毒用虚拟机的总体控制结构;第四节主要是对特定指令处理函数的

分析;最后在第五节中列出了一些反虚拟执行技术做为今后改进的参照。论文的第三章主要介绍实时监控

技术,由于win9x和winnt/2000系统机制和驱动模型不同,所以会分成两个操作系统进行讨论。其中涉及

的技术很广泛:包括驱动编程技术,文件钩挂,特权级间通信等等。本文介绍的技术涉及操作系统底层机

制,难度较大。所提供的代码,包括一个虚拟机C语言源代码和两个病毒实时监控驱动程序反汇编代码,

具有一定的研究和实用价值。

关键字:病毒,虚拟机,实时监控

文档内容目录

1.绪 论

1. 1课题背景

1.2当今病毒技术的发展状况

1.2.1系统核心态病毒

1.2.2驻留病毒

1.2.3截获系统操作

1.2.4加密变形病毒

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

1.2.6直接API调用

1.2.7病毒隐藏

1.2.8病毒特殊感染法

2.虚拟机查毒

2.1虚拟机概论

2. 2加密变形病毒

2.3虚拟机实现技术详解

2.4虚拟机代码剖析

2.4.1不依赖标志寄存器指令模拟函数的分析

2.4.2依赖标志寄存器指令模拟函数的分析

2.5反虚拟机技术

3.病毒实时监控

3.1实时监控概论

3.2病毒实时监控实现技术概论

3.3WIN9X下的病毒实时监控

3.3.1实现技术详解

3.3.2程序结构与流程

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

3.4WINNT/2000下的病毒实时监控

3.4.1实现技术详解

3.4.2程序结构与流程

3.4.3HOOKSYS.SYS逆向工程代码剖析

结论

致谢

主要参考文献

1.绪 论

本论文研究的主要内容正如其题目所示是设计并编写一个先进的反病毒引擎。首先需要对这“先进”二字

做一个解释,何为“先进”?众所周知,传统的反病毒软件使用的是基于特征码的静态扫描技术,即在文

件中寻找特定十六进制串,如果找到,就可判定文件感染了某种病毒。但这种方法在当今病毒技术迅猛发

展的形势下已经起不到很好的作用了。原因我会在以下的章节中具体描述。因此本论文将不对杀毒引擎中

的特征码扫描和病毒代码清除模块做分析。我们要讨论的是为应付先进的病毒技术而必需的两大反病毒技

术--虚拟机和实时监控技术。具体什么是虚拟机,什么是实时监控,我会在相应的章节中做详尽的介绍。

这里我要说明的一点是,这两项技术虽然在前人的工作中已有所体现(被一些国内外先进的反病毒厂家所

使用),但出于商业目的,这些技术并没有被完全公开,所以你无论从书本文献还是网路上的资料中都无

法找到关于这些技术的内幕。而我会在相关的章节中剖析大量的程序源码(主要是2.4节中的一个完整的

虚拟机源码)或是逆向工程代码(3.3.3节和3.4.3节中三个我逆向工程的某著名反病毒软件的实时监控驱

动程序及客户程序的反汇编代码),并同时公布一些我个人挖掘的操作系统内部未公开的机制和数据结构

。另外我在文中会大量地提到或引用一些关于系统底层奥秘的大师级经典图书,这算是给喜爱系统级编程

但又苦于找不到合适教材的朋友开了一份书单。下面就开始进入论文的正题。

1.1课题背景

本论文涉及的两个主要技术,也是当今反病毒界使用的最为先进的技术中的两个,究竟是作何而用的呢?

首先说说虚拟机技术,它主要是为查杀加密变形病毒而设计的。简单地来说,所谓虚拟机并不是个虚拟的

机器,说得更合适一些应该是个虚拟CPU(用软件实现的CPU),只不过病毒界都这么叫而已。它的作用主

要是模拟INTEL X86 CPU的工作过程来解释执行可执行代码,与真正的CPU一样能够取指,译码并执行相应

机器指令规定的操作。当然什么是加密变形病毒,它们为什么需要被虚拟执行以及怎样虚拟执行等问题会

在合适的章节中得到解答。再说另一个重头戏--实时监控技术,它的用处更为广泛,不仅局限于查杀病毒

。被实时监控的对象也很多,如中断(Intmon),页面错误(Pfmon),磁盘访问(Diskmon)等等。用于

杀毒的监控主要是针对文件访问,在你要对一个文件进行访问时,实时监控会先检查文件是否为带毒文件

,若是,则由用户选择是清除病毒还是取消此次操作请求。这样就给了用户一个相对安全的执行环境。但

同时,实时监控会使系统性能有所下降,不少杀毒软件的用户都抱怨他们的实时监控让系统变得奇慢无比

而且不稳定。这就给我们的设计提出了更高的要求,即怎样在保证准确拦截文件操作的同时,让实时监控

占用的系统资源更少。我会在病毒实时监控一节中专门讨论这个问题。这两项技术在国内外先进的反病毒

厂家的产品中都有使用,虽然它们的源代码没有公开,但我们还是可以通过逆向工程的方法来窥视一下它

们的设计思路。其实你用一个十六进制编辑器来打开它们的可执行文件,也许就会看到一些没有剥掉的调

试符号、变量名字或输出信息,这些蛛丝马迹对于理解代码的意图大有裨益。同时,在反病毒软件的安装

目录中后缀为.VXD或.SYS就是执行实时监控的驱动程序,可以拿来逆向一下(参看我在后面分析驱动源代

码中的讨论)。相信至此,我们对这两项技术有了一个大体的了解。后面我们将深入到技术的细节中去。

1.2当今病毒技术的发展状况

要讨论怎样反病毒,就必须从病毒技术本身的讨论开始。正是所谓“知己知彼,百战不殆”。其实,我认

为目前规定研究病毒技术属于违法行为存在着很大的弊端。很难想象一个毫无病毒写作经验的人会成为杀

毒高手。据我了解,目前国内一些著名反病毒软件公司的研发队伍中不乏病毒写作高手。只不过他们将同

样的技术用到了正道上,以‘毒’攻‘毒’。所以我希望这篇论文能起到抛砖引玉的作用,期待着有更多

的人会将病毒技术介绍给大众。当今的病毒与DOS和WIN3.1时代下的从技术角度上看有很多不同。我认为

最大的转变是:引导区病毒减少了,而脚本型病毒开始泛滥。原因是在当今的操作系统下直接改写磁盘的

引导区会有一定的难度(DOS则没有保护,允许调用INT13直接写盘),而且引导区的改动很容易被发现,

所以很少有人再写了;而脚本病毒以其传播效率高且容易编写而深得病毒作者的青睐。当然由于这两种病

毒用我上面说过的基于特征码的静态扫描技术就可以查杀,所以不在我们的讨论之列。我要讨论的技术主

要来自于二进制外壳型病毒(感染文件的病毒),并且这些技术大都和操作系统底层机制或386以上CPU的

保护模式相关,所以值得研究。大家都知道DOS下的外壳型病毒主要感染16位的COM或EXE文件,由于DOS没

有保护,它们能够轻松地进行驻留,减少可用内存(通过修改MCB链),修改系统代码,拦截系统服务或

中断。而到了WIN9X和WINNT/2000时代,想写个运行其上的32位WINDOWS病毒绝非易事。由于页面保护,你

不可能修改系统的代码页。由于I/O许可位图中的规定,你也不能进行直接端口访问。在WINDOWS中你不可

能象在DOS中那样通过截获INT21H来拦截所有文件操作。总之,你以一个用户态程序运行,你的行为将受

到操作系统严格的控制,不可能再象DOS下那样为所欲为了。另外值得一提的是,WINDOWS下采用的可执行

文件格式和DOS下的EXE截然不同(普通程序采用PE格式,驱动程序采用LE),所以病毒的感染文件的难度

增大了(PE和LE比较复杂,中间分了若干个节,如果感染错了,将导致文件不能继续执行)。因为当今病

毒的新技术太多,我不可能将它们逐一详细讨论,于是就选取了一些重要并具有代表性的在本章的各小节

中进行讨论。

1.2.1系统核心态病毒

在介绍什么是系统核心态病毒之前,有必要讨论一下核心态与用户态的概念。其实只要随便翻开一本关于

386保护模式汇编程序设计的教科书,都可以找到对这两个概念的讲述。386及以上的CPU实现了4个特权级

模式(WINDOWS只用到了其中两个),其中特权级0(Ring0)是留给操作系统代码,设备驱动程序代码使

用的,它们工作于系统核心态;而特权极3(Ring3)则给普通的用户程序使用,它们工作在用户态。运行

于处理器核心态的代码不受任何的限制,可以自由地访问任何有效地址,进行直接端口访问。而运行于用

户态的代码则要受到处理器的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访

问页面的虚拟地址,且只能对任务状态段(TSS)中I/O许可位图(I/O Permission Bitmap)中规定的可

访问端口进行直接访问(此时处理器状态和控制标志寄存器EFLAGS中的IOPL通常为0,指明当前可以进行

直接I/O的最低特权级别是Ring0)。以上的讨论只限于保护模式操作系统,象DOS这种实模式操作系统则

没有这些概念,其中的所有代码都可被看作运行在核心态。既然运行在核心态有如此之多的优势,那么病

毒当然没有理由不想得到Ring0。处理器模式从Ring3向Ring0的切换发生在控制权转移时,有以下两种情

况:访问调用门的长转移指令CALL,访问中断门或陷阱门的INT指令。具体的转移细节由于涉及复杂的保

护检查和堆栈切换,不再赘述,请参阅相关资料。现代的操作系统通常使用中断门来提供系统服务,通过

执行一条陷入指令来完成模式切换,在INTEL X86上这条指令是INT,如在WIN9X下是INT30(保护模式回调

),在LINUX下是INT80,在WINNT/2000下是INT2E。用户模式的服务程序(如系统DLL)通过执行一个

INTXX来请求系统服务,然后处理器模式将切换到核心态,工作于核心态的相应的系统代码将服务于此次

请求并将结果传给用户程序。下面就举例子说明病毒进入系统核心态的方法。

在WIN9X下进程虚拟地址空间中映射共享系统代码的部分(3G--4G)中除了最上面4M页表有页面保护外其

它地方可由用户程序读写。如果你用Softice(系统级调试器)的PAGE命令查看这些地址的页属性,则你

会惊奇地发现U RW位,这说明这些地址可从用户态直接读出或写入。这意味着任何一个用户程序都能够在

其运行过程中恶意或无意地破坏操作系统代码页。由此病毒就可以在GDT(全局描述符表),LDT(局部描

述符表)中随意构造门描述符并借此进入核心态。当然,也不一定要借助门描述,还有许多方法可以得到

Ring0。据我所知的方法就不下10余种之多,如通过调用门(Callgate),中断门(Intgate),陷阱门

(Trapgate),异常门(Fault),中断请求(IRQs),端口(Ports),虚拟机管理器(VMM),回调

(Callback),形式转换(Thunks),设备IO控制(DeviceIOControl),API函数(SetThreadContext)

,中断2E服务(NTKERN.VxD)。由于篇幅的限制我不可能将所有的方法逐一描述清楚,这里我仅选取最具

有代表性的CIH病毒1.5版开头的一段代码。

人们常说CIH病毒运用了VXD(虚拟设备驱动)技术,其实它本身并不是VXD。只不过它利用WIN9X上述漏洞

,在IDT(中断描述符表)中构造了一个DPL(段特权级)为3的中断门(意味着可以从Ring3下执行访问该

中断门的INT指令),并使描述符指向自己私有地址空间中的一个需要工作在Ring0下的函数地址。这样一

来CIH就可以通过简单的执行一条INTXX指令(CIH选择使用INT3,是为了使同样接挂INT3的系统调试器

Softice无法正常工作以达到反跟踪的目的)进入系统核心态,从而调用系统的VMM和VXD服务。以下是我

注释的一段CIH1.5的源代码:

; *************************************

; * 修改IDT以求得核心态特权级 *

; *************************************

push eax

sidt [esp-02h] ;取得IDT表基地址

pop ebx

add ebx, HookExceptionNumber*08h+04h ;ZF = 0

cli ;读取修改系统数据时先禁止中断

mov ebp, [ebx]

mov bp, [ebx-04h] ;取得原来的中断入口地址

lea esi, MyExceptionHook-@1[ecx] ;取得需要工作在Ring0的函数的偏移地址

push esi

mov [ebx-04h], si

shr esi, 16

mov [ebx+02h], si ;设置为新的中断入口地址

pop esi

; *************************************

; * 产生一个异常来进入Ring0 *

; *************************************

int HookExceptionNumber ;产生一个异常

当然,后面还有恢复原来中断入口地址和异常处理帧的代码。

刚才所讨论的技术仅限于WIN9X,想在WINNT/2000下进入Ring0则没有这么容易。主要的原因是WINNT/2000

没有上述的漏洞,它们的系统代码页面(2G--4G)有很好的页保护。大于0x80000000的虚拟地址对于用户

程序是不可见的。如果你用Softice的PAGE命令查看这些地址的页属性,你会发现S位,这说明这些地址仅

可从核心态访问。所以想在IDT,GDT随意构造描述符,运行时修改内核是根本做不到的。所能做的仅是通

过加载一个驱动程序,使用它来做你在Ring3下做不到的事情。病毒可以在它们加载的驱动中修改内核代

码,或为病毒本身创建调用门(利用NT由Ntoskrnl.exe导出的未公开的系统服务

KeI386AllocateGdtSelectors,KeI386SetGdtSelector,KeI386ReleaseGdtSelectors)。如Funlove病毒

就利用驱动来修改系统文件(Ntoskrnl.exe,Ntldr)以绕过安全检查。但这里面有两个问题,其一是驱

动程序从哪里来,现代病毒普遍使用一个称为“Drop”的技术,即在病毒体本身包含驱动程序二进制码(

可以进行压缩或动态构造文件头),在病毒需要使用时,动态生成驱动程序并将它们扔到磁盘上,然后马

上通过在SCM(服务控制管理器)注册并最终调用StartService来使驱动程序得以运行;其二是加载一个

驱动程序需要管理员身份,普通帐号在调用上述的加载函数时会返回失败(安全子系统要检查用户的访问

令牌(Token)中有无SeLoadDriverPrivilege特权),但多数用户在大多时候登录时会选择管理员身份,

否则连病毒实时监控驱动也同样无法加载,所以留给病毒的机会还是很多的。

1.2.2驻留病毒

驻留病毒是指那些在内存中寻找合适的页面并将病毒自身拷贝到其中且在系统运行期间能够始终保持病毒

代码的存在。驻留病毒比那些直接感染(Direct-action)型病毒更具隐蔽性,它通常要截获某些系统操

作来达到感染传播的目的。进入了核心态的病毒可以利用系统服务来达到此目的,如CIH病毒通过调用一

个由VMM导出的服务VMMCALL _PageAllocate在大于0xC0000000的地址上分配一块页面空间。而处于用户态

的程序要想在程序退出后仍驻留代码的部分于内存中似乎是不可能的,因为无论用户程序分配何种内存都

将作为进程占用资源的一部分,一旦进程结束,所占资源将立即被释放。所以我们要做的是分配一块进程

退出后仍可保持的内存。

病毒写作小组29A的成员GriYo 运用的一个技术很有创意:他通过CreateFileMappingA 和MapViewOfFile

创建了一个区域对象并映射它的一个视口到自己的地址空间中去,并把病毒体搬到那里,由于文件映射所

在的虚拟地址处于共享区域(能够被所有进程看到,即所有进程用于映射共享区内虚拟地址的页表项全都

指向相同的物理页面),所以下一步他通过向Explorer.exe中注入一段代码(利用WriteProcessMemory来

向其它进程的地址空间写入数据),而这段代码会从Explorer.exe的地址空间中再次申请打开这个文件映

射。如此一来,即便病毒退出,但由于Explorer.exe还对映射页面保持引用,所以一份病毒体代码就一直

保持在可以影响所有进程的内存页面中直至Explorer.exe退出。

另外还可以通过修改系统动态连接模块(DLL)来进行驻留。WIN9X下系统DLL(如Kernel32.dll 映射至

BFF70000)处于系统共享区域(2G-3G),如果在其代码段空隙中写入一小段病毒代码则可以影响其它所

有进程。但Kernel32.dll的代码段在用户态是只能读不能写的。所以必须先通过特殊手段修改其页保护属

性;而在WINNT/2000下系统DLL所在页面被映射到进程的私有空间(如Kernel32.dll 映射至77ED0000)中

,并具有写时拷贝属性,即没有进程试图写入该页面时,所有进程共享这个页面;而当一个进程试图写入

该页面时,系统的页面错误处理代码将收到处理器的异常,并检查到该异常并非访问违例,同时分配给引

发异常的进程一个新页面,并拷贝原页面内容于其上且更新进程的页表以指向新分配的页。这种共享内存

的优化给病毒的写作带来了一定的麻烦,病毒不能象在WIN9X下那样仅修改Kernel32.dll一处代码便可一

劳永逸。它需要利用WriteProcessMemory来向每个进程映射Kernel32.dll的地址写入病毒代码,这样每个

进程都会得到病毒体的一个副本,这在病毒界被称为多进程驻留或每进程驻留(Muti-Process Residence

or Per-Process Residence )。

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 csBFFC9004] ;通过一个调用门调用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_int21hush 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, fsebx] ;交换现在的err结构和前一个结构的地址

; eax=前一个结构的地址

; fs0]=现在的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, fsebx] ; 取现在的err结构的地址eax

mov esp, [eax] ; 取下个结构地址即eps0到esp

RestoreSE: ;没有发生异常时顺利的回到这里,你这时的esp为本esp0

pop dword ptr fsebx] ;弹出原来的前一个结构的地址到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 等诸多类型的压缩文件。这些病毒的代码中含有对特定压缩文件类型解压并压缩的代码段

,可以先把压缩文件中的内容解压出来,然后对合适的文件进行感染,最后再将感染后文件压缩回去并同

时修改压缩文件头部的校验和。目前不少反病毒软件都支持查多种格式的压缩文件,但对有些染毒的压缩

文件无法杀除。原因我想可能是怕由于某种缘故,如解压或压缩有误,校验和计算不对等,使得清除后压

缩文件格式被破坏。病毒却不用对用户的文件损坏负责,所以不存在这种担心。

2.虚拟机查毒

2.1虚拟机概论

近些年,虚拟机,在反病毒界也被称为通用解密器,已经成为反病毒软件中最引人注目的部分,尽管反病

毒者对于它的运用还远没有达到一个完美的程度,但虚拟机以其诸如"病毒指令码模拟器"和"Stryker"等

多变的名称为反病毒产品的市场销售带来了光明的前景。以下的讨论将把我们带入一个精彩的虚拟技术的

世界中。

首先要谈及的是虚拟机的概念和它与诸如Vmware(美国VMWARE公司生产的一款虚拟机,它支持在

WINNT/2000环境下运行如Linux等其它操作系统)和WIN9X下的VDM(DOS虚拟机,它用来在32位保护模式环

境中运行16实模式代码)的区别。其实这些虚拟机的设计思想是有渊源可寻的,早在上个世纪60年代IBM

就开发了一套名为VM/370的操作系统。VM/370在不同的程序之间提供抢先式多任务,作法是在单一实际的

硬件上模式出多部虚拟机器。典型的VM/370会话,使用者坐在电缆连接的远程终端前,经由控制程序的一

个IPL命令,模拟真实机器的初始化程序装载操作,于是 一套完整的操作系统被载入虚拟机器中,并开始

为使用者着手创建一个会话。这套模拟系统是如此的完备,系统程序员甚至可以运行它的一个虚拟副本,

来对新版本进行除错。Vmware与此非常相似,它作为原操作系统下的一个应用程序可以为运行于其上的目

标操作系统创建出一部虚拟的机器,目标操作系统就象运行在单独一台真正机器上,丝毫察觉不到自己处

于Vmware的控制之下。当在Vmware中按下电源键(Power On)时,窗口里出现了机器自检画面,接着是操

作系统的载入,一切都和真的一样。而WIN9X为了让多个程序共享CPU和其它硬件资源决定使用VMs(所有

Win32应用程序运行在一部系统虚拟机上;而每个16位DOS程序拥有一部DOS虚拟机)。VM是一个完全由软

件虚构出来的东西,以和真实电脑完全相同的方式来回应应用程序所提出的需求。从某种角度来看,你可

以将一部标准的PC的结构视为一套API。这套API的元素包括硬件I/O系统,和以中断为基础的BIOS和MS-

DOS。WIN9X常常以它自己的软件来代理这些传统的API元素,以便能够对珍贵的硬件多重发讯。在VM上运

行的应用程序认为自己独占整个机器,它们相信自己是从真正的键盘和鼠标获得输入,并从真正的屏幕上

输出。稍被加一点限制,它们甚至可以认为自己完全拥有CPU和全部内存。实现虚拟技术关键在于软件虚

拟化和硬件虚拟化,下面简要介绍WIN9X下的DOS虚拟机的实现。

当Windows移往保护模式后,保护模式程序无法直接调用实模式的MS-DOS处理例程,也不能直接调用实模

式的BIOS。软件虚拟化就是用来描述保护模式Windows部件是如何能够和实模式MS-DOS和BIOS彼此互动。

软件虚拟化要求操作系统能够拦截企图跨越保护模式和实模式边界的调用,并且调整适当的参数寄存器后

,改变CPU模式。WIN9X使用虚拟设备驱动(VXD)拦截来自保护模式的中断,通过实模式中断向量表(IVT

),将之转换为实模式中断调用。做为转换的一部分,VXD必须使用置于保护模式扩展内存中的参数,生

成出适当的参数,并将之放在实模式(V86)操作系统可以存取的地方。服务结束后,VXD在把结果交给扩

展内存中保护模式调用端。16位DOS程序中大量的21H和13H中断调用就此解决,但其中还存在不少直接端

口I/O操作,这就需要引入硬件虚拟化来解决。虚拟硬件的出现是为了在硬件中断请求线上产生中断请求

,为了回应IN和OUT指令,改变特殊内存映射位置等原因。硬件虚拟化依赖于Intel 80386+的几个特性。

其中一个是I/O许可掩码,使操作系统可能诱捕(Trap)对任何一个端口的所有IN/OUT指令。另一个特性

是:由硬件辅助的分页机制,使操作系统能够提供虚拟内存,并拦截对内存地址的存取操作,将Video

RAM虚拟化是此很好的例证。最后一个必要的特性是CPU的虚拟8086(V86)模式 ,让DOS程序象在实模式

中那样地执行。

我们下面讨论用于查毒的虚拟机并不是象某些人想象的:如Vmware一样为待查可执行程序创建一个虚拟的

执行环境,提供它可能用到的一切元素,包括硬盘,端口等,让它在其上自由发挥,最后根据其行为来判

定是否为病毒。当然这是个不错的构想,但考虑到其设计难度过大(需模拟元素过多且行为分析要借助人

工智能理论),因而只能作为以后发展的方向。我设计的虚拟机严格的说不能称之为虚拟机器,而叫做虚

拟CPU,通用解密器等更为合适一些,但由于反病毒界习惯称之为虚拟机,所以在下面的讨论中我还将延

续这个名称。查毒的虚拟机是一个软件模拟的CPU,它可以象真正CPU一样取指,译码,执行,它可以模拟

一段代码在真正CPU上运行得到的结果。给定一组机器码序列,虚拟机会自动从中取出第一条指令操作码

部分,判断操作码类型和寻址方式以确定该指令长度,然后在相应的函数中执行该指令,并根据执行后的

结果确定下条指令的位置,如此循环反复直到某个特定情况发生以结束工作,这就是虚拟机的基本工作原

理和简单流程。设计虚拟机查毒的目的是为了对付加密变形病毒,虚拟机首先从文件中确定并读取病毒入

口处代码,然后以上述工作步骤解释执行病毒头部的解密段(decryptor),最后在执行完的结果(解密

后的病毒体明文)中查找病毒的特征码。这里所谓的“虚拟”,并非是创建了什么虚拟环境,而是指染毒

文件并没有实际执行,只不过是虚拟机模拟了其真实执行时的效果。这就是虚拟机查毒基本原理,具体介

绍请参看后面的相关章节。

当然,虚拟执行技术使用范围远不止自动脱壳(虚拟机查毒实际上是自动跟踪病毒入口的解密子将加密的

病毒体按其解密算法进行解密),它还可以应用在跨平台高级语言解释器,恶意代码分析,调试器。如刘

涛涛设计的国产调试器Trdos就是完全利用虚拟技术解释执行被调试程序的每条指令,这种调试器比较起

传统的断点式调试器(Debug,Softice等)具有诸多优势,如不易被被调试者察觉,断点个数没有限制等

2.2加密变形病毒

前面提到过设计虚拟机查毒的目的是为了对付加密变形病毒。这一章就重点介绍加密变形技术。

早期病毒没有使用任何复杂的反检测技术,如果拿反汇编工具打开病毒体代码看到的将是真正的机器码。

因而可以由病毒体内某处一段机器代码和此处距离病毒入口(注意不是文件头)偏移值来唯一确定一种病

毒。查毒时只需简单的确定病毒入口并在指定偏移处扫描特定代码串。这种静态扫描技术对付普通病毒是

万无一失的。

随着病毒技术的发展,出现了一类加密病毒。这类病毒的特点是:其入口处具有解密子(decryptor),

而病毒主体代码被加了密。运行时首先得到控制权的解密代码将对病毒主体进行循环解密,完成后将控制

交给病毒主体运行,病毒主体感染文件时会将解密子,用随机密钥加密过的病毒主体,和保存在病毒体内

或嵌入解密子中的密钥一同写入被感染文件。由于同一种病毒的不同传染实例的病毒主体是用不同的密钥

进行加密,因而不可能在其中找到唯一的一段代码串和偏移来代表此病毒的特征,似乎静态扫描技术对此

即将失效。但仔细想想,不同传染实例的解密子仍保持不变机器码明文(从理论上讲任何加密程序中都存

在未加密的机器码,否则程序无法执行),所以将特征码选于此处虽然会冒一定的误报风险(解密子中代

码缺少病毒特性,同样的特征码也会出现在正常程序中),但仍不失为一种有效的方法。

由于加密病毒还没有能够完全逃脱静态特征码扫描,所以病毒写作者在加密病毒的基础之上进行改进,使

解密子的代码对不同传染实例呈现出多样性,这就出现了加密变形病毒。它和加密病毒非常类似,唯一的

改进在于病毒主体在感染不同文件会构造出一个功能相同但代码不同的解密子,也就是不同传染实例的解

密子具有相同的解密功能但代码却截然不同。比如原本一条指令完全可以拆成几条来完成,中间可能会被

插入无用的垃圾代码。这样,由于无法找到不变的特征码,静态扫描技术就彻底失效了。下面先举两个例

子说明加密变形病毒解密子构造,然后再讨论怎样用虚拟执行技术检测加密变形病毒。

著名多形病毒Marburg的变形解密子:

00401020: movsx edi,si ;病毒入口

00401023: movsx edx,bp

00401026: jmp 00408a99

......

00407400: ;病毒体入口

加密的病毒主体

00408a94: ;解密指针初始值

......

00408a99: mov dl,f7

00408a9b: movsx edx,bx

00408a9e: mov ecx,cf4b9b4f

00408aa3: call 00408ac4

......

00408ac4: pop ebx

00408ac5: jmp 00408ade

......

00408ade: mov cx,di

00408ae1: add ebx,9fdbd22d

00408ae7: jmp 00408b08

......

00408b08: add ecx,80c1fbc1

00408b0e: mov ebp,7fcdeff3 ;循环解密记数器初值

00408b13: sub cl,39

00408b16: movsx esi,si

00408b19: add dword ptr[ebx+60242dbf],9ef42073 ;解密语句,9ef42073是密钥

00408b23: mov edx,6fd1d4cf

00408b28: mov di,dx

00408b2b: inc ebp

00408b2c: xor dl,a3

00408b2f: mov cx,si

00408b32: sub ebx,00000004 ;移动解密偏移指针,逆向解密

00408b38: mov ecx,86425df9

00408b3d: cmp ebp,7fcdf599 ;判断解密结束与否

00408b43: jnz 00408b16

00408b49: jmp 00408b62

......

00408b62: mov di,bp

00408b65: jmp 00407400 ;将控制权交给解密后的病毒体入口

著名多形病毒Hps的变形解密子:

005365b8: ;解密指针初始值和病毒体入口

加密的病毒主体

......

005379cd: call 005379e2

......

005379e2: pop ebx

005379e3: sub ebx,0000141a ;设置解密指针初值

005379e9: ret

......

005379f0: dec edx ;减少循环记数值

005379f1: ret

......

00537a00: xor dword ptr[ebx],10e7ed59 ;解密语句,10e7ed59是密钥

00537a06: ret

......

00537a1a: sub ebx,ffffffff

00537a20: sub ebx,fffffffd ;移动解密指针,正向解密

00537a26: ret

......

00537a30: mov edx,74d9cb97 ;设置循环记数初值

00537a35: ret

......

00537a3f: call 005379cd ;病毒入口

00537a44: call 00537a30

00537a49: call 00537a00

00537a4e: call 00537a1a

00537a53: call 005379f0

00537a58: mov esi,edx

00537a5a: cmp esi,74d9c696 ;判断解密结束与否

00537a60: jnz 00537a49

00537a66: jmp 005365b8 ;将控制权交给解密后的病毒体入口

以上的代码看上去绝对不会是用编译器编译出来,或是编程者手工写出来的,因为其中充斥了大量的乱数

和垃圾。代码中没有注释部分均可认为是垃圾代码,有用部分完成的功能仅是循环向加密过的病毒体的每

个双字加上或异或一个固定值。这只是变形病毒传染实例的其中一个,别的实例的解密子和病毒体将不会

如此,极度变形以至让人无法辩识。至于变形病毒的实现技术由于涉及复杂的算法和控制,因此不在我们

讨论范围内。

这种加密变形病毒的检测用传统的静态特征码扫描技术显然已经不行了。为此我们采取的方法是动态特征

码扫描技术,所谓“动态特征码扫描”指先在虚拟机的配合下对病毒进行解密,接着在解密后病毒体明文

中寻找特征码。我们知道解密后病毒体明文是稳定不变的,只要能够得到解密后的病毒体就可以使用特征

码扫描了。要得到病毒体明文首先必须利用虚拟机对病毒的解密子进行解释执行,当跟踪并确定其循环解

密完成或达到规定次数后,整个病毒体明文或部分已被保存到一个内部缓冲区中了。虚拟机之所以又被称

为通用解密器在于它不用事先知道病毒体的加密算法,而是通过跟踪病毒自身的解密过程来对其进行解密

。至于虚拟机怎样解释指令执行,怎样确定可执行代码有无循环解密段等细节将在下一节中介绍。

2.3虚拟机实现技术详解

有了前面关于加密变形病毒的介绍,现在我们知道动态特征码扫描技术的关键就在于必须得到病毒体解密

后的明文,而得到明文产生的时机就是病毒自身解密代码解密的完毕。目前有两种方法可以跟踪控制病毒

的每一步执行,并能够在病毒循环解密结束后从内存中读出病毒体明文。一种是单步和断点跟踪法,和目

前一些程序调试器相类似;另一种方法当然就是虚拟执行法。下面分别分析单步和断点跟踪法和虚拟执行

法的技术细节。

单步跟踪和断点是实现传统调试器的最根本技术。单步的工作原理很简单:当CPU在执行一条指令之前会

先检查标志寄存器,如果发现其中的陷阱标志被设置则会在指令执行结束后引发一个单步陷阱INT1H。至

于断点的设置有软硬之分,软件断点是指调试器用一个通常是单字节的断点指令(CC,即INT3H)替换掉

欲触发指令的首字节,当程序执行至断点指令处,默认的调试异常处理代码将被调用,此时保存在栈中的

段/偏移地址就是断点指令后一字节的地址;而硬件断点的设置则利用了处理器本身的调试支持,在调试

寄存器(DR0--DR4)中设置触发指令的线形地址并设置调试控制寄存器(DR7)中相关的控制位,CPU会在

预设指令执行时自动引发调试异常。而Windows本身又提供了一套调试API,使得调试跟踪一个程序变得非

常简单:调试器本身不用接挂默认的调试异常处理代码,而只须调用WaitForDebugEvent等待系统发来的

调试事件;调试器可利用GetThreadContext挂起被调试线程获取其上下文,并设置上下文中的标志寄存器

中的陷阱标志位,最后通过SetThreadContext使设置生效来进行单步调试;调试器还可通过调用两个功能

强大的调试API--ReadProcessMemory和WriteProcessMemory来向被调试线程的地址空间中注入断点指令。

根据我逆向后的分析结果,VC++的调试器就是直接利用这套调试API写成的。使用以上的调试技术既然可

以写出像VC++那样功能齐全的调试器,那么没有理由不能将之运用于病毒代码的自动解密上。最简单的最

法:创建待查可执行文件为调试器的调试子进程,然后用上述方法对其进行单步跟踪,每当收到具有

EXCEPTION_SINGLE_STEP异常代码的事件时就可以分析该条以单步模式执行的指令,最后当判断病毒的整

个解密过程结束后即可调用ReadProcessMemory读出病毒体明文。

用单步和断点跟踪法的唯一一点好处就在于它不用处理每条指令的执行--这意味着它无需编写大量的特定

指令处理函数,因为所有的解密代码都交由CPU去执行,调试器不过是在代码被单步中断的间隙得到控制

权而已。但这种方法的缺点也是相当明显的:其一容易被病毒觉察到,病毒只须进行简单的堆栈检查,或

直接调用IsDebugerPresent就可确定自己正处于被调试状态;其二由于没有相应的机器码分析模块,指令

的译码,执行完全依赖于CPU,所以将导致无法准确地获取指令执行细节并对其进行有效的控制。;其三

单步和断点跟踪法要求待查可执行文件真实执行,即其将做为系统中一个真实的进程在自己的地址空间中

运行,这当然是病毒扫描所不能允许的。很显然,单步和断点跟踪法可以应用在调试器,自动脱壳等方面

,但对于查毒却是不合适的。

而使用虚拟执行法的唯一一点缺点就在于它必须在内部处理所有指令的执行--这意味着它需要编写大量的

特定指令处理函数来模拟每种指令的执行效果,这里根本不存在何时得到控制权的问题,因为控制权将永

远掌握在虚拟机手中。用软件方法模拟CPU并非易事,需要对其机制有足够的了解,否则模拟效果将与真

实执行相去甚远。举两个例子:一个是病毒常用的乘法后ASCII调整指令AAM,这条指令因为存在未公开的

行为从而常常被病毒用来考验虚拟机设计的优劣。通常情况下AAM是双字节指令,操作码为D4 0A(其实0A

隐含代表了操作数10);但也可作为单字节指令明确地指定第二字节除数为任意8位立即数,此时操作码

仅为D4。虚拟机必需考虑到后一种指定除数的情况来保证模拟结果的正确性;还有一个例子是关于处理器

响应中断的方式,即CPU在刚打开中断后将不会马上响应中断,而必须隔一个指令周期。如果虚拟机没有

考虑到该机制则很可能虚拟执行流程会与真实情况不符。但虚拟执行的优点也是很明显的,同时它正好填

补了单步和断点跟踪法所力不能及的方面:首先是不可能被病毒觉察到,因为虚拟机将在其内部缓冲区中

为被虚拟执行代码设立专用的堆栈,所以堆栈检查结果与实际执行无二(不会向堆栈中压入单步和断点中

断时的返回地址);其次由于虚拟机自身完成指令的解码和地址的计算,所以能够获取每条指令的执行细

节并加以控制;最后,最为关键的一条在于虚拟执行确实做到了“虚拟”执行,系统中不会产生代表被执

行者的进程,因为被执行者的寄存器组和堆栈等执行要素均在虚拟机内部实现,因而可以认为它在虚拟机

地址空间中执行。鉴于虚拟执行法诸多的优点,所以将其运用于通用病毒体解密上是再好不过的了。

通常,虚拟机的设计方案可以采取以下三种之一:自含代码虚拟机(SCCE),缓冲代码虚拟机(BCE),

有限代码虚拟机(LCE)。

自含代码虚拟机工作起来象一个真正的CPU。一条指令取自内存,由SCCE解码,并被传送到相应的模拟这

条指令的例程,下一条指令则继续这个循环。虚拟机会包含一个例程来对内存/寄存器寻址操作数进行解

码,然后还会包括一个用于模拟每个可能在CPU上执行的指令的例程集。正如你所想到的,SCCE的代码会

变的无比的巨大而且速度也会很慢。然而SCCE对于一个先进的反病毒软件是很有用的。所有指令都在内部

被处理,虚拟机可以对每条指令的动作做出非常详细的报告,这些报告和启发式数据以及通用清除模块将

相互参照形成一个有效的反毒系统。同时,反病毒程序能够最精确地控制内存和端口的访问,因为它自己

处理地址的解码和计算。

缓冲代码虚拟机是SCCE的一个缩略版,因为相对于SCCE它具有较小的尺寸和更快的执行速度。在BCE中,一

条指令是从内存中取得的,并和一个特殊指令表相比较。如果不是特殊指令,则它被进行简单的解码以求

得指令的长度,随后所有这样的指令会被导入到一个可以通用地模拟所有非特殊指令的小过程中。而特殊

指令,只占整个指令集的一小部分,则在特定的小处理程序中进行模拟。BCE通过将所有非特殊指令用一

个小的通用的处理程序模拟来减少它必须特殊处理的指令条数,这样一来它削减了自身的大小并提高了执

行速度。但这意味着它将不能真正限制对某个内存区域,端口或其他类似东西的访问,同时它也不可能生

成如SCCE提供的同样全面的报告。

有限代码虚拟机有点象用于通用解密的虚拟系统所处的级别。LCE实际上并非一个虚拟机,因为它并不真

正的模拟指令,它只简单地跟踪一段代码的寄存器内容,也许会提供一个小的被改动的内存地址表,或是

调用过的中断之类的东西。选择使用LCE而非更大更复杂的系统的原因,在于即使只对极少数指令的支持

便可以在解密原始加密病毒的路上走很远,因为病毒仅仅使用了INTEL指令集的一小部分来加密其主体。

使用LCE,原本处理整个INTEL指令集时的大量花费没有了,带来的是速度的巨大增长。当然,这是以不能

处理复杂解密程序段为代价的。当需要进行快速文件扫描时LCE就变的有用起来,因为一个小型但象样的

LCE可以用来快速检查执行文件的可疑行为,反之对每个文件都使用SCCE算法将会导致无法忍受的缓慢。

当然,如果一个文件看起来可疑,LCE还可以启动某个SCCE代码对文件进行全面检查。

下面开始介绍32位自含代码虚拟机w32encode(w32encode.cpp,Tw32asm.h,Tw32asm.cpp做为查毒引擎的

一部分和其它搜索清除模块联编为Rsengine.dll)的程序结构和流程。由于这是一个设计完备且复杂的大

型商用虚拟机,其中不可避免地包含了对某些特定病毒的特定处理,为了使虚拟机模型的结构清晰脉络分

明,分析时我将做适当的简化。

w32encode的工作原理很简单:它首先设置模拟寄存器组(用一个DWORD全局变量模拟真实CPU内部的一个

寄存器,如ENEAX)的初始值,初始化执行堆栈指针(虚拟机用内部的一个数组static int STACK[0x20]

来模拟堆栈)。然后进入一个循环,解释执行指令缓冲区ProgBuffer中的头256条指令,如果循环退出时

仍未发现病毒的解密循环则可由此判定非加密变形病毒,若发现了解密循环则调用EncodeInst函数重复执

行循环解密过程,将病毒体明文解密到DataSeg1或DataSeg2中。相关部分代码如下:

W32Encode0中总体流程控制部分代码:

for (i=0;i<0x100;i++) //首先虚拟执行256条指令试图发现病毒循环解密子

{

if (InstLoc>=0x280)

return(0);

if (InstLoc+ProgSeekOff>=ProgEndOff)

return(0); //以上两条判断语句检查指令位置的合法性

saveinstloc(); //存储当前指令在指令缓冲区中的偏移

HasAddNewInst=0;

if (!(j=parse())) //虚拟执行指令缓冲区中的一条指令

return(0); //遇到不认识的指令时退出循环

if (j==2) //返回值为2说明发现了解密循环

break;

}

if (i==0x100) //执行过256条指令后仍未发现循环则退出

return(0);

PreParse=0;

ProcessInst();

if (!EncodeInst()) //调用解密函数重复执行循环解密过程

return(0);

jmp中判定循环出现部分代码:

if ((loc>=0)&&(loc<InstLoc)) //若转移后指令指针小于当前指令指针则可能出现循环

if (!isinstloc(loc)) //在保存的指令指针数组InstLocArray中查找转移后指

...... //令指针值,如发现则可判定循环出现

else

{

......

return(2); //返回值2代表发现了解密循环

}

parse中虚拟执行每条指令的过程较复杂一些:通常parse会从取得指令缓冲区ProgBuffer中取得当前指令

的头两个字节(包括了全部操作码)并根据它们的值调用相应的指令处理函数。例如当第一个字节等于0F

并且第二个字节位与BE后等于BE时,可判定此指令为movszx并同时调用movszx进行处理。当执行进入特定

指令的处理函数中时,首先要通过判断寻址方式(调用modregrm或modregrm1)确定指令长度并将控制权

交给saveinst函数。saveinst在保存该指令的相关信息后会调用真正指令执行函数W32ExecuteInst。这个

函数和parse非常相似,它从SaveInstBuf1中取得当前指令的头两个字节并根据它们的值调用相应的指令

模拟函数以完成一条指令的执行。相关部分代码如下:

W32ExecuteInst中指令分遣部分代码:

if ((c&0xf0)==0x50)

{if (ExecutePushPop1) //模拟push和pop

return(gotonext());

return(0);

}

if (c==0x9c)

{if (ExecutePushf()) //模拟pushf

return(gotonext());

return(0);

}

if (c==(char)0x9d)

{if (ExecutePopf()) //模拟popf

return(gotonext());

return(0);

}

if ((c==0xf)&&((c2&0xbe)==0xbe))

{if (i=ExecuteMovszx(0)) //模拟movszx

return(gotonext());

return(0);

}

2.4虚拟机代码剖析

总体流程控制和分遣部分的相关代码,在上一章中都已分析过了。下面分析具体的特定指令模拟函数,这

才是虚拟机的精华之所在。我将指令分成不依赖标志寄存器和依赖标志寄存器两大类分别介绍:

2.4.1不依赖标志寄存器指令模拟函数的分析

push和pop指令的模拟:

static int ExecutePushPop1(int c)

{

if (c<=0x57)

{if (StackP<0) //入栈前检查堆栈缓冲指针的合法性

return(0);

}

else

if (StackP>=0x40) //出栈前检查堆栈缓冲指针的合法性

return(0);

if (c<=0x57) {

StackP--;

ENESP-=4; //如果是入栈指令则在入栈前减少堆栈指针

}

switch

{case 0x50:STACK[StackP]=ENEAX; //模拟push eax

break;

......

case 0x5f:ENEDI=STACK[StackP]; //模拟push edi

break;

}

if (c>=0x58) {

StackP++;

ENESP+=4; //如果是出栈指令则在出栈后增加堆栈指针

}

return(1);

}

2.4.2依赖标志寄存器指令模拟函数的分析

CW32Asm类中cmp指令的模拟:

void CW32Asm:: cmpw(int c1,int c2)

{

char FlgReg;

__asm {

mov eax,c1 //取得第一个操作数

mov ecx,c2 //取得第二个操作数

cmp eax,ecx //比较

lahf //将比较后的标志结果装入ah

mov FlgReg,ah //保存结果在局部变量FlgReg中

}

FlagReg=FlgReg; //保存结果在全局变量FlagReg中

}

CW32Asm类中jnz指令的模拟:

int CW32Asm::JNE()

{int i;

char FlgReg=FlagReg; //用保存的FlagReg初始化局部变量FlgReg

__asm

{

mov ah,FlgReg //设置ah为保存的模拟标志寄存器值

pushf //保存虚拟机自身当前标志寄存器

sahf //将模拟标志寄存器值装入真实标志寄存器中

mov eax,1

jne l //执行jnz

popf //恢复虚拟机自身标志寄存器

xor eax,eax

l:

popf //恢复虚拟机自身标志寄存器

mov i,eax

}

return; //返回值为1代表需要跳转

}

2.5反虚拟机技术

任何一个事物都不是尽善尽美,无懈可击的,虚拟机也不例外。由于反虚拟执行技术的出现,使得虚拟机

查毒受到了一定的挑战。这里介绍几个比较典型的反虚拟执行技术:

首先是插入特殊指令技术,即在病毒的解密代码部分人为插入诸如浮点,3DNOW,MMX等特殊指令以达到反

虚拟执行的目的。尽管虚拟机使用软件技术模拟真正CPU的工作过程,它毕竟不是真正的CPU,由于精力有

限,虚拟机的编码者可能实现对整个Intel指令集的支持,因而当虚拟机遇到其不认识的指令时将会立刻

停止工作。但通过对这类病毒代码的分析和统计,我们发现通常这些特殊指令对于病毒的解密本身没有发

生任何影响,它们的插入仅仅是为了干扰虚拟机的工作,换句话说就是病毒根本不会利用这条随机的垃圾

指令的运算结果。这样一来,我们可以仅构造一张所有特殊指令对应于不同寻址方式的指令长度表,而不

必为每个特殊指令编写一个专用的模拟函数。有了这张表后,当虚拟机遇到不认识的指令时可以用指令的

操作码索引表格以求得指令的长度,然后将当前模拟的指令指针(EIP)加上指令长度来跳过这条垃圾指

令。当然,还有一个更为保险的办法那就是:得到指令长度后,可以将这条我们不认识的指令放到一个充

满空操作指令(NOP)的缓冲区中,接着我们将跳到缓冲区中去执行,这等于让真正的CPU帮我们来执行这

条指令,最后一步当然是将执行后真实寄存器中的结果放回我们的模拟寄存器中。这虚拟执行和真实执行

参半方法的好处在于:即便在特殊指令对于病毒是有意义的,即病毒依赖其返回结果的情况下,虚拟机仍

可保证虚拟执行结果的正确。

其次是结构化异常处理技术,即病毒的解密代码首先设置自己的异常处理函数,然后故意引发一个异常而

使程序流程转向预先设立的异常处理函数。这种流程转移是CPU和操作系统相互配合的结果,并且在很大

程度上,操作系统在其中起了很大的作用。由于目前的虚拟机仅仅模拟了没有保护检查的CPU的工作过程

,而对于系统机制没有进行处理。所以面对引发异常的指令会有两种结果:其一是某些设计有缺陷的虚拟

机无法判断被模拟指令的合法性,所以模拟这样的指令将使虚拟机自身执行非法操作而退出;其二虚拟机

判断出被模拟指令属于非法指令,如试图向只读页面写入的指令,则立刻停止虚拟执行。通常病毒使用该

技术的目的在于将真正循环解密代码放到异常处理函数后,如此虚拟机将在进入异常处理函数前就停止了

工作,从而使解密子有机会逃避虚拟执行。因而一个好的虚拟机应该具备发现和记录病毒安装异常过滤函

数的操作并在其引发异常时自动将控制转向异常处理函数的能力。

再次是入口点模糊(EPO)技术,即病毒在不修改宿主原入口点的前提下,通过在宿主代码体内某处插入

跳转指令来使病毒获得控制权。通过前面的分析,我们知道虚拟机扫描病毒时出于效率考虑不可能虚拟执

行待查文件的所有代码,通常的做法是:扫描待查文件代码入口,假如在规定步数中没有发现解密循环,

则由此判定该文件没有携带加密变形病毒。这种技术之所以能起到反虚拟执行的作用在于它正好利用了虚

拟机的这个假设:由于病毒是从宿主执行到一半时获得控制权的,所以虚拟机首先解释执行的是宿主入口

的正常程序,当然在规定步数中不可能发现解密循环,因而产生了漏报。如果虚拟机能增加规定步数的大

小,则很有可能随着病毒插入的跳转指令跟踪进入病毒的解密子,但确定规定步数大小实在是件难事:太

大则将无谓增加正常程序的检测时间;太小则容易产生漏报。但我们对此也不必过于担心,这类病毒由于

其编写技术难度较大所以为数不多。在没有反汇编和虚拟执行引擎的帮助下,病毒很难在宿主体内定位一

条完整指令的开始处来插入跳转,同时很难保证插入的跳转指令的深度大于虚拟机的规定步数,并且没有

把握插入的跳转指令一定会被执行到。

另外还有多线程技术,即病毒在解密部分入口主线程中又启动了额外的工作线程,并且将真正的循环解密

代码放置于工作线程中运行。由于多线程间切换调度由操作系统负责管理,所以我们的虚拟机只能在假定

被执行线程独占处理器时间,即保证永远不被抢先,的前提下进行。如此一来,虚拟机对于模拟启用多线

程工作的代码将很难做到与真实效果一致。多线程和结构化异常处理两种技术都利用了特定的操作系统机

制来达到反虚拟执行的目的,所以在虚拟CPU中加入对特定操作系统机制的支持将是我们今后改进的目标

最后是元多形技术(MetaPolymorphy),即病毒中并非是多形的解密子加加密的病毒体结构,而整体均采

用变形技术。这种病毒整体都在变,没有所谓“病毒体明文”。当然,其编写难度是很大的。如果说前几

种反虚拟机技术是利用了虚拟机设计上的缺陷,可以通过代码改进来弥补的话,那么这种元多形技术却使

虚拟机配合的动态特征码扫描法彻底失效了,我们必须寻求如行为分析等更先进的方法来解决。

3.病毒实时监控

3.1实时监控概论

实时监控技术其实并非什么新技术,早在DOS编程时代就有之。只不过那时人们没有给这项技术冠以这样

专业的名字而已。早期在各大专院校机房中普遍使用的硬盘写保护软件正是利用了实时监控技术。硬盘写

保护软件一般会将自身写入硬盘零磁头开始的几个扇区(由0磁头0柱面1扇最开始的64个扇区是保留的,

DOS访问不到)并修改原来的主引导记录以使启动时硬盘写保护程序可以取得控制权。引导时取得控制权

的硬盘写保护程序会修改INT13H的中断向量指向自身已驻留于内存中的钩子代码以便随时拦截所有对磁盘

的操作。钩子代码的作用当然是很明显的,它主要负责由判断中断入口参数,包括功能号,磁盘目标地址

等来决定该类型操作是否被允许,这样就可以实现对某一特定区域的写操作保护。后来又诞生了在此基础

之上进行改进了的磁盘恢复卡之类的产品,其利用将写操作重定向至目标区域外的临时分区并保存磁盘先

前状态等技术来实现允许写入并可随时恢复之功能。不管怎么改进,这类产品的核心技术还是对磁盘操作

的实时监控。对此有兴趣的朋友可参看高云庆著《硬盘保护技术手册》。DOS下还有许多通过驻留并截获

一些有用的中断来实现某种特定目的的程序,我们通常称之为TSR(终止并等待驻留terminate-and-stay

-resident,此种程序不容易编好,需要大量的关于硬件和Dos中断的知识,还要解决Dos重入,tsr程序重

入等问题,搞不好就会当机)。在WINDOWS下要实现实时监控决非易事,普通用户态程序是不可能监控系

统的活动的,这也是出于系统安全的考虑。HPS病毒能在用户态下直接监控系统中的文件操作其实是由于

WIN9X在设计上存在漏洞。而我们下面要讨论的两个病毒实时监控(For WIN9X&WINNT/2000)都使用了驱

动编程技术,让工作于系统核心态的驱动程序去拦截所有的文件访问。当然由于工作系统的不同,这两个

驱动程序无论从结构还是工作原理都不尽相同的,当然程序写法和编译环境更是千差万别了,所以我们决

定将其各自分成独立的一节来详细地加以讨论。上面提到的病毒实时监控其实就是对文件的监控,说成是

文件监控应该更为合理一些。除了文件监控外,还有各种各样的实时监控工具,它们也都具有各自不同的

特点和功用。这里向大家推荐一个关于WINDOWS系统内核编程的站点:www.sysinternals.com。在其上可以

找到很多实时监控小工具,比如能够监视注册表访问的Regmon(通过修改系统调用表中注册表相关服务入

口),可以实时地观察TCP和UDP活动的Tdimon(通过hook系统协议驱动Tcpip.sys中的dispatch函数来截

获tdi clinet向其发送的请求),这些工具对于了解系统内部运作细节是很有裨益的。介绍完有关的背景

情况后,我们来看看关于病毒 实时监控的具体实现技术的情况。

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 CS0002]

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系统程式设计》。

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万多行。所以必须掌握

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有