6.1.4 案例分析
1.引导型病毒编制技术
学习本节内容前建议先学习硬盘主引导区结构相关知识,掌握主引导程序以及DOS操作系统的中断知识。
(1)引导型病毒编制原理
20世纪90年代中期之前,引导型病毒一直是最流行的病毒类型。但是2010年3月由金山安全反病毒专家发现了Windows系统下引导型病毒“鬼影”,这彻底颠覆了人们的传统认识——Windows下不会再有引导型病毒。
引导型病毒首先感染软盘的引导区,随后再蔓延至硬盘,并感染硬盘的主引导记录(MBR)。一旦MBR被病毒感染,病毒就试图感染软驱中的软盘引导区。引导型病毒是这样工作的:由于病毒隐藏在软盘的第一扇区,使它可以在系统文件装入内存之前,先进入内存,从而使它获得对操作系统的完全控制,这就使它得以传播并造成危害。引导型病毒常常使用自身的程序替代MBR中的程序,并移动扇区到硬盘的其他存储区。由于PC开机后,将先执行主引导区的代码,因此病毒可以获得第一控制权,在引导操作系统之前,完成以下工作。
1)减少系统可用最大内存量,以供自己需要。
2)修改必要的中断向量,以便传播。
3)读入病毒的其他部分,进行病毒的拼装。病毒首先从已标记簇的某扇区读入病毒的其他部分,这些簇往往被标记为坏簇(但是文件型病毒则不必如此,两者混合型也不必如此)。然后,再读入原引导目录到0000:7COOH处,跳转执行。引导型病毒的代码如下:
4)读入原主引导分区,转去执行操作系统的引导工作。这部分工作可以参照硬盘引导程序。
2.16位可执行文件病毒编制技术
(1)16位可执行文件结构及运行原理
文件型病毒是病毒中的大家族,顾名思义,该病毒主要是感染文件(包括COM、EXE、DRV、BIN、OVL和SYS等扩展名的文件)。当它们激活时,感染文件又把自身复制到其他干净文件中,并能在存储介质中保存很长时间,直到病毒又被激活。由于技术的原因,文件型病毒的活力远比引导型病毒强。目前存在数千种文件型病毒,它们不但活动在DOS 16位环境中,而且在Windows 32位系统中依然非常活跃,同时,有些文件型病毒能很成功地感染OS2、Linux、UNIX和Macintosh环境中的文件。编制文件型病毒的关键是分析操作系统中的文件结构及其执行原理。本节主要介绍16位系统中常见的文件结构及其运行原理,为以后章节学习做准备。
1)COM格式
最简单的可执行文件就是DOS下的COM文件。由于当时计算机64 k B内存的限制,就产生了COM文件。COM格式文件最大为64 kB,内含16位程序的二进制代码映像,没有重定位信息。COM文件包含程序二进制代码的一个绝对映像,也就是说,为了运行程序准确的处理器指令和内存中的数据,DOS通过直接把该映像从文件复制到内存来加载COM程序,系统不需要做重定位工作。
为加载一个COM程序,DOS试图分配内存,因为COM程序必须位于一个64 kB的段中,所以COM文件的大小不能超过65 024 B(64k B减去用于PSP的256 B和用于一个起始堆栈的至少256 B,如果DOS不能为程序、一个程序段前缀(Program Segment Prefix,PSP)和一个起始堆栈分配足够内存,则分配尝试失败。否则,DOS分配尽可能多的内存(直至所有保留内存),即使COM程序本身不能大于64 kB。在试图运行另一个程序或分配另外的内存之前,大部分COM程序释放任何不需要的内存。在分配内存后,DOS在该内存的头256 B建立一个PSP。结构如下:
如果PSP中的第一个FCB含有一个有效驱动器标识符,则置AL为00 H,否则为0FFH。DOS还置AH为00 H或0FFH,这依赖于第二个FCB是否含有一个有效驱动器标识符。在创建PSP后,DOS在PSP后立即开始(偏移100 H)加载COM文件,它置SS、DS和ES为PSP的段地址,接着创建一个堆栈。为了创建这个堆栈,DOS置SP为0000H。如果没有分配64 k B内存,则要求置寄存器大小是所分配的字节总数加2的值。最后,它把0000 H推进栈中,这是为了保证与早期DOS版本上设计的程序的兼容性。
DOS通过控制传递偏移100 H处的指令而启动程序。程序设计者必须保证COM文件的第一条指令是程序的入口点。因为程序是在偏移100H处加载,所以所有代码和数据偏移也必须相对于100 H。汇编语言程序设计者可通过设置程序的初值为100 H保证这一点(如通过在汇编代码的开始处使用语句org 100H)。
2)MZ格式
COM发展下去就是MZ格式的可执行文件,这是DOS中具有重定位功能的可执行文件格式。MZ可执行文件内含16位代码,在这些代码之前加上一个文件头,文件头中包括各种说明数据,如第一句可执行代码执行指令时所需要的文件入口点、堆栈的位置、重定位表等。操作系统根据文件头的信息将代码部分装入内存,然后根据重定位表修正代码,最后在设置好堆栈后从文件头中指定的入口开始执行。因此DOS可以把MZ格式的程序放在任何它想要的地方。图6.10为MZ格式的可执行文件的简单结构示意图。
图6.10 MZ格式可执行文件结构示意图
MZ格式可执行程序文件头的代码如下:
3)NE格式
为了保持对DOS的兼容性并满足Windows的需要,Windows 3.x中出现的NE格式的可执行文件中保留了MZ格式的头,同时NE文件又加了一个自己的头,之后才是可执行文件的可执行代码。NE类型包括了EXE、DLL、DRV和FON共4种类型的文件。NE格式的关键特性是:它把程序代码、数据及资源隔离在不同的可加载区中,借由符号输入和输出,实现所谓的运行时动态链接。
16位的NE格式文件装载程序(NELoader)读取部分磁盘文件,并生成一个完全不同的数据结构,在内存中建立模块。当代码或数据需要装入时,装载程序必须从全局内存中分配出一块,查找原始数据在文件中的位置,找到位置后再读取原始的数据,最后再进行一些修正。另外,每一个16位的模块(Module)要负责记住现在使用的所有段选择符,该选择符表示该段是否已经被抛弃等信息。图6.11是NE格式的可执行文件的结构示意图。
图6.11 NE格式可执行文件结构示意图
NE格式可执行程序文件头的代码如下:
(2)COM文件病毒原理
COM文件是一种单段执行结构的文件,其执行文件代码和执行时内存映像完全相同,起始执行偏移地址为100H,对应于文件的偏移00H(文件头)。感染COM文件的病毒典型做法如下:
病毒要感染COM文件,先将开始的三个字节保存在orgcode中,并将这三个字节更改为OE9H和COM文件的实际大小的二进制编码。然后,将resume开始的三个字节改为0E9H和表达式(当前地址—COM文件的实际大小—病毒代码大小)的二进制编码,以便在执行完病毒后转向执行程序。最后,将病毒写入源COM文件的末尾。
此外,完整的病毒感染代码还需要感染标记判断、文件大小判断等。
3.32位可执行文件病毒编制技术
在学习本节内容前,建议先学习并掌握PE可执行文件的结构及运行原理。推荐参考罗云彬编著的《Windows环境下32位汇编语言程序设计》第2版一书。
尽管基于16位架构的病毒依然存在,尽管有些病毒创作者还沉浸在获得16位架构特权的喜悦中,但32位架构才代表当今的潮流。常言道“知己知彼,百战不殆”,尽管本教材的编写目的是传授恶意代码防范技术,但学习并精通32位操作系统下的病毒制作理论是当今病毒防范的重要基础。
(1)PE文件结构及其运行原理
PE是Win32环境自身所带的可执行文件格式。它的一些特性继承自UNIX的COFF(Common Object File Format)文件格式。可移植的执行体意味着此文件格式是跨Win32平台的,即使Windows运行在非Intel的CPU上,任何Win32平台的PE装载器都能识别和使用该文件格式。当然,移植到不同的CPU上PE执行体必然得有一些改变。除Vx D和16位的DLL外,所有Win32执行文件都使用PE文件格式。因此,研究PE文件格式是我们洞悉Windows结构的良机。
(2)PE文件型病毒关键技术
在Win32下编写Ring3[1]级别的病毒不是一件非常困难的事情,但是,在Win32下的系统功能调用不是直接通过中断来实现的,而是通过DLL导出的。因此,在病毒中得到API入口是一项关键任务。虽然,Ring3给我们带来了很多不方便的限制,但这个级别的病毒有很好的兼容性,能同时适用于Windows 9x和Windows 2000环境。编写Ring3组病毒,有6个重要问题需要解决,分别是病毒的重定位、获取API函数、文件搜索、内存映射文件、病毒如何感染其他文件和如何返回到宿主程序。
1)病毒的重定位
我们编写正常程序的时候根本不用去关心变量(常量)的位置,因为源程序在编译时所在内存中的位置都被计算好了。在程序装入内存时,系统不会为它重定位。在编程时需要用到变量(常量)的时候,直接用它们的名称访问(编译后就是通过偏移地址访问)即可。
病毒不可避免地也要用到变量(常量),当病毒感染宿主程序后,由于其依附到宿主程序中的位置各有不同,它随着宿主程序载入内存后,病毒中的各个变量(常量)在内存中的位置自然也会随之改变。如果病毒直接引用变量就不再准确,势必导致病毒无法正常运行。由此,病毒必须对所有病毒代码中的变量进行重新定位。病毒重定位代码如下:
当pop语句执行完之后,ebp中存放的是病毒程序中标号delta在内存中的真正地址。如果病毒程序中有一个变量varl,那么该变量实际在内存中的地址应该是ebp+(offset varl—offset delta)。由此可知,参照量delta在内存中的地址加上变量varl与参照量之间的距离就等于变量varl在内存中的真正地址。
下面用一个简单的例子来说明这个问题。假设有一段简单的汇编代码:
执行这段代码后,eax存放的就是dw Var的运行时刻的地址。如果还不好理解,可以假设这段代码在编译运行时有一个固定起始装载地址(这有点像DOS时代的COM文件)。不失一般性,可以令这个固定起始装载地址为00401000 H。这段代码编译后的可执行代码在内存中的映像为:
如果理解了这个固定起始地址的装载过程,动态的装载过程就很容易理解了。将可执行程序动态地加载到内存中的过程如下:
2)获取API函数
Win32 PE病毒和普通Win32 PE程序一样需要调用API函数,但是普通的Win32 PE程序里面有一个引入函数表,读函数表对应了代码段中所用到的API函数在动态链接库中的真实地址。这样,在调用API函数时就可以通过该引入表找到相应API函数的真正执行地址。但是,对于Win32 PE病毒来说,它只有一个代码段,并不存在引入表。既然如此,病毒就无法像普通程序那样直接调用相关API函数,而应该先找出这些API函数在相应动态链接库中的地址。
如何获取API函数地址一直是病毒技术的一个非常重要的话题。要得到API函数地址,首先需要获得相应的动态链接库的基地址。在实际编写病毒的过程中,经常用到的动态链接库有Kernel32.dll和user32.dll等。具体需要搜索哪个链接库的基地址,就要看病毒要用的函数在哪个库中了。不失一般性,下面以获得Kernel32基地址为例,介绍几种方法。
①利用程序的返回地址,在其附近搜索Kernel32的基地址。大家知道,当系统打开一个可执行文件的时候,会调用Kernel32.dll中的CreateProcess()函数。当CreateProcess()函数在完成装载工作后,它先将一个返回地址压入到堆栈顶端,然后转向执行刚才装载的应用程序。当该应用程序结束后,会将堆栈顶端数据弹出放到(E)IP中,并且继续执行。刚才堆栈顶端保存的数据是什么呢?仔细想想,不难明白,这个数据其实就是CreateProcess()函数在Kernel32.dll中的返回地址。其实这个过程和call指令调用子程序类似。
可以看出,这个返回地址在Kernel32.dll模块中。另外PE文件被装入内存时是按内存页对齐的,只要从返回地址按照页对齐的边界一页一页地往低地址搜索,就必然可以找到Kernel32.dll的文件头地址,即Kernel32的基地址。其搜索代码如下:
②对相应操作系统分别给出固定的Kernel32模块的基地址。对于不同的Windows操作系统来说,Kernel32模块的地址是固定的,甚至一些API函数的大概位置都是固定的。例如,Windows 98为BFF70000,Windows 2000为77E80000,Windows XP为77E60000。
在得到了Kernel32的模块地址以后,就可以在该模块中搜索所需要的API地址了。对于给定的API,可以通过直接搜索Kernel32.dll导出表的方法来获得其地址,同样也可以先搜索出GetProc Address()和Load Library()两个API函数的地址,然后利用这两个API函数得到所需要的API函数地址。在已知API函数序列号或函数名的情况下,如何在导出表中搜索API函数地址的过程请参考PE文件结构一节。具体代码如下:
(3)文件搜索
文件搜索是病毒寻找目标文件的非常重要的功能。在Win32汇编中,通常采用API函数进行文件搜索。关键的函数和数据结构如下。
1)Find FirstFile():该函数根据文件名查找文件。
2)Find NextFile():该函数根据调用Find First File()函数时指定的一个文件名查找下一个文件。
3)Find Close():该函数用来关闭由Find First File()函数创建的一个搜索句柄。
4)WIN32_FIND_DATA:该结构中存放找到文件的详细信息。
文件搜索一般采用递归算法进行,也可以采用非递归搜索方法,这里仅介绍第一种算法的搜索过程。
Find File Proe
1)指定找到的目录为当前工作日录;
2)开始搜索文件(*.*);
3)该目录搜索完毕?是则返回,否则继续;
4)找到文件还是目录?是目录则调用自身函数FindFile(),否则继续;
5)是文件,如符合感染条件,则调用感染模块,否则继续;
6)搜索下一个文件(Find Next File),转到第3步继续。
FindFileEndp
(4)内存映射文件
内存映射文件提供了一组独立的函数,这些函数使应用程序能够像访问内存一样对磁盘上的文件进行访问。这组内存映射文件函数将磁盘上的文件全部或者部分映射到进程虚拟地址空间的某个位置,以后对文件内容的访问就如同在该地址区域内直接对内存访问一样简单。这样,对文件中数据的操作便是直接对内存进行操作,大大提高了访问的速度,这对于计算机病毒减少资源占有是非常重要的。在计算机病毒中,通常采用如下几个步骤进行内存映射。
1)调用CreateFile()函数打开想要映射的宿主程序,返回文件句柄hFile。
2)调用CreateFile Mapping()函数生成一个建立基于宿主文件句柄h File的内存映射对象,返回内存映射对象句柄h Map。
3)调用Map Vicw Of File()函数将整个文件(一般还要加上病毒体的大小)映射到内存中,得到指向映射到内存的第一个字节的指针(p Mem)。
4)用刚才得到的指针p Mem对整个宿主文件进行操作,对宿主程序进行病毒感染。
5)调用Unmap View File()函数解除文件映射,传入参数是p Mem。
6)调用Close Handle来关闭内存映射文件,传入参数是h Map。
7)调用Close Handle来关闭宿主文件,传人参数是h File。
(5)病毒如何感染其他文件
PE病毒感染其他文件的常见方法是在文件中添加一个新的节,然后,把病毒代码和病毒执行后返回宿主程序的代码写入新添加的节中,同时修改PE文件头中入口点(AddressOf EntryPoint),使其指向新添加的病毒代码入口。这样,当程序运行时,首先执行病毒代码,当病毒代码执行完成后才转向执行宿主程序。下面来具体分析病毒感染其他文件的步骤。
1)判断目标文件开始的两个字节是否为MZ。
2)判断PE文件标记PE。
3)判断感染标记。如果已被感染过则跳出,继续执行宿主程序,否则继续。
4)获得Data Directory(数据目录)的个数(每个数据目录信息占8个字节)。
5)得到节表起始位置(数据目录的偏移地址+数据目录占用的字节数=节表起始位置)。
6)得到节表的末尾偏移(紧接其后用于写入一个新的病毒节信息,节表起始位置+节的个数*每个节表占用的字节数28 H=节表的末尾偏移)。
7)开始写入节表。
写入节表操作又分为以下11个步骤。
①写入节名(8字节)。
②写入节的实际字节数(4字节)。
③写入新节在内存中的开始偏移地址(4字节),同时可以计算出病毒入口位置。上一个节在内存中的开始偏移地址+(上一个节的大小/节对齐+1)*节对齐=本节在内存中的开始偏移地址。
④写入本节(即病毒节)在文件中对齐后的大小。
⑤写入本节在文件中的开始位置。上节在文件中的开始位置+上节对齐后的大小=本节(即病毒节)在文件中的开始位置。
⑥修改映像文件头中的节表数目。
⑦修改AddressOf EntryPoint(即程序入口点指向病毒入口位置),同时保存旧的AddressOf Entry Point,以便返回宿主并继续执行。
⑧更新SizeOfImage(内存中整个PE映像尺寸=原SizeOfImage+病毒节经过内存节对齐后的大小)。
⑨写入感染标记(后面例子中是放在PE头中)。
⑩在新添加的节中写入病毒代码。
ECX=病毒长度
ESI=病毒代码位置(并不一定等于病毒执行代码开始位置)
EDI=病毒节写入位置
⑪将当前文件位置设为文件末尾。
(6)如何返回到宿主程序
为了提高自己的生存能力,病毒不应该破坏宿主程序的原有功能。因此,病毒应该在执行完毕后,立刻将控制权交给宿主程序。病毒如何做到这一点呢?返回宿主程序相对来说比较简单,病毒在修改被感染文件代码开始执行位置(AddressOf EntryPoint)时,会保存原来的值,这样,病毒在执行完病毒代码之后用一个跳转语句跳到这段代码处继续执行即可。
在这里,病毒会先做出一个“现在执行程序是否为病毒启动程序”的判断,如果不是启动程序,病毒才会返回宿主程序,否则继续执行程序其他部分。对于启动程序来说,它是没有病毒标志的。
上述几点都是病毒编制不可缺少的技术,这里的介绍比较简单,如果想进一步了解相关技术可以参考Billy Belceb的Win32病毒编制技术以及中国病毒公社(CVC)杂志。
(7)从Ring3到Ring0的简述
Windows操作系统运行在保护模式,保护模式将指令执行分为4个特权级,即众所周知的Ring0、Ring1、Ring2和Ring3。Ring0意味着更多的权利,可以直接执行诸如访问端口等操作,通常应用程序运行于Ring3,这样可以很好地保护系统安全。然而当我们需要Ring0的时候(如跟踪、反跟踪和写病毒等),麻烦就来了。如果想进入Ring0,一般要写Vx D或WDM驱动程序,然而这项技术对一般人来说并不那么简单。由于Windows 9x未对IDT(Interrupt Descriptor Table)、GDT(Global Descriptor Table)和LDT(Locale Descriptor Table)加以保护,因此可以利用这一漏洞来进入Ring0。由于Windows 9x肯定会被淘汰,又由于有太多的人已经详细介绍了这些技术,这里不打算再多做介绍。用SHE(Structure Handle Exception)、IDT、GDT和LDT等方法进入Ring0的例子请参考CVC杂志以及已公开的病毒源码和相关论坛等。
在Windows NT/Windows 2000/Windows XP下进入Ring0是一件较困难的事情,因此,大多数感染Windows NT/Windows 2000/Windows XP系统的病毒都是Ring3级别的。最近网上流传着一篇由webcrazy编写的Windows 2000下进入Ring0的C教程,这篇文章非常值得研究Ring0病毒的技术人员参考。另外,大家也可以参考《未公开的NT核心》一书,该书详细介绍了添加用户中断服务的方法。目前已经有病毒利用了这个漏洞,但是相关病毒源码却很少见。
需要说明的是,由于Windows 2000已经有了比较多的安全审核机制,即使掌握了这种技术,如果想在Windows 2000下进入Ring0还必须具有Administrator权限。如果系统存在某种漏洞,如缓冲区溢出等,还是有可能获得Administrator权限的。因此,必须同时具备病毒编制技术和黑客技术才能进入Windows 2000的Ring0,由此可以看出当前的病毒编制技术越来越需要二者结合的能力。