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病毒就利用了这个技术在用户态下直接截获系统中的文件操作,但这种方法截获的也仅仅是一小部分文件操作。