在解决了起始目录的问题之后,就可以从这些起始目录开始使用FindFirstFile和FindNextFile开始遍历其下以及其子目录下的所有文件和目录了,遍历方法可采用深度优先或广度优先搜索算法,较常用的还是深度优先算法。具体实现方式可采用递归搜索或非递归搜索两种实现方式。递归搜索需要占用栈空间,有可能造成栈空间耗竭而产生异常,不过在现实应用中这种情况很少出现,而非递归搜索则不存在此问题,但代码实现略复杂。在现实应用中,应用最多的还是递归遍历搜索。搜索时,可指定FindFirstFile的第一形参为*.*以搜索所有文件,根据搜索结果WIN32_FIND_DATA结构的dwFileAttributes成员判断是否为目录,若为目录则需要继续遍历该子目录,根据WIN32_FIND_DATA的cFileName中的文件名成员判断是否具有要感染的文件后缀以采取修改感染动作,以下代码实现了递归搜索某个目录及其下所有子目录的功能:
void enum_path(char *cpath){
WIN32_FIND_DATA wfd;
HANDLE hfd;
char cdir[MAX_PATH];
char subdir[MAX_PATH];
int r;
GetCurrentDirectory(MAX_PATH,cdir);
SetCurrentDirectory(cpath);
hfd = FindFirstFile("*.*",&wfd);
if(hfd!=INVALID_HANDLE_VALUE) {
do{
if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if(wfd.cFileName[0] != '.') {
// 合成完整路径名
sprintf(subdir,"%s\\%s",cpath,wfd.cFileName);
// 递归枚举子目录
enum_path(subdir);
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根据后缀名判断是
// 否要感染相应的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
SetCurrentDirectory(cdir);
}
短短20 多行C 代码就实现了文件遍历的功能,Win32 API的强大功能不仅为开发者提供了便利,同时也为病毒敞开了方便之门。用汇编实现则稍微复杂一些,感兴趣的读者可参阅Elkern 中的enum_path部分,原理是一样的,限于篇幅这里不再给出相应的汇编代码。
非递归搜索不使用堆栈存储相关的信息,而使用显式分配的链表或栈等结构存储相关的信息,应用一个迭代循环完成递归遍历同样的功能,下面是使用链表以栈方式处理子目录列表的一个简单实现:
void nr_enum_path(char *cpath){
list
string cdir,subdir;
WIN32_FIND_DATA wfd;
HANDLE hfd;
int r;
dir_list.push_back(string(cpath));
while(dir_list.size()) {
cdir = dir_list.back();
dir_list.pop_back();
SetCurrentDirectory(cdir.c_str());
hfd = FindFirstFile("*.*",&wfd);
if(hfd!=INVALID_HANDLE_VALUE) {
do{
if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if(wfd.cFileName[0] != '.') {
// 合成完整路径名
subdir=cdir+"\\"+wfd.cFileName;
cout<<"push subdir: "<
dir_list.push_back(string(subdir));
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根据后缀名判断
// 是否要感染相应的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
}//end while
}
在以汇编语言实现时,需要自己管理链表以及分配和释放相应的结构,因此较为烦琐,代码量也稍大,因此病毒多采用递归的方式进行搜索。值得注意的是搜索深层次的目录是很费时的,因此大部分病毒为避免CPU占用率过高,搜索一定数量的文件之后,都会调用Sleep 休眠一会,以避免被敏感的用户发觉。文件搜索和感染模块通常是以单独的线程运行的,在病毒获得控制权后,创建相应的搜索和感染线程,而将主现成的控制权交给原程序。
PE 文件的修改和感染策略
既然已经能够搜索磁盘及网络共享文件中的所有文件,要实现寄生,那么自然下一步就是对搜索到的PE文件进行感染了。感染PE的很重要的一个考虑就是将病毒代码写入到PE 文件的哪个位置。读写文件一般利用Win32 API CreateFile、CreateFileMapping、MapViewOfFile等API以内存映射文件的方式进行,这样可以避免自己管理缓冲的麻烦,因而为较多病毒所采用。为了能够读写具有只读属性的文
件,病毒在操作前首先利用GetFileAttributes 获取其属性并保存,然后用SetFileAttributes将文件的属性修改为可写,在
感染完毕后再恢复其属性值。
一般说来,有如下几种感染PE文件的方案供选择:
a)添加一个新的节。将病毒代码写入到新的节中,相应修改节表,文件头中文件大小等属性值。由于在PE尾部增加了一个节,因此较容易被用户察觉。在某些情况下,由于原PE头部没有足够的空间存放新增节的节表信息,因此还要对其它数据进行搬移等操作。鉴于上述问
题,PE 病毒使用该方法的并不多。
b)附加在最后一个节上。修改最后一个节节表的大小和属性以及文件头中文件大小等属性值。由于越来越多的杀毒软件采用了一种尾部扫描的方式,因此很多病毒还要在病毒代码之后附加随机数据以逃避该种扫描。现代PE 病毒大量使用该种方式。
c)写入到PE文件头部未用空间各个节所保留的空隙之中。PE 头部大小一般为1024 字节,有5-6 个节的普通PE文件实际被占用部分一般仅为600 字节左右,尚有400 多个字节的剩余空间可以利用。PE文件各个节之间一般都是按照512 字节对齐的,但节中的实际数据常常未完全使用全部的512字节,PE文件的对齐设计本来是出于效率的考虑,但其留下的空隙却给病毒留下了栖身之地。这种感染方式感染后原PE 文的总长度可能并不会增加,因此自CIH 病毒首次使用该技术以来,备受病毒作者的青睐。
d)覆盖某些非常用数据。如一般exe文件的重定位表,由于exe一般不需要重定位,因此可以覆盖重定位数据而不会造成问题,为保险起见可将文件头中指示重定位项的DataDirectory 数组中的相应项清空,这种方式一般也不会造成被感染文件长度的增加。因此很多病毒也广泛使用该种方法。
e)压缩某些数据或代码以节约出存放病毒代码的空间,然后将病毒代码写入这些空间,在程序代码运行前病毒首先解压缩相应的数据或代码,然后再将控制权交给原程序。该种方式一般不会增加被感染文件的大小,但需考虑的因素较多,实现起来难度也比较大。用的还不多。
不论何种方式,都涉及到对PE头部相关信息以及节表的相关操作,我们首先研究一下PE的修改,即如何在添加了病毒代码后使得PE文件仍然是合法的PE文件,仍然能够被系统加载器加载执行。PE文件的每个节的属性都是由节表中的一个表项描述的,节表紧跟在IMAGE_NT_HEADERS后面,因此从文件偏移0x3C 处的双字找到IMAGE_NT_HEADERS 的起始偏移,再加上IMAGE_NT_HEADERS的大小(248字节)就定位了节表的起始位置,每个表项是一个IMAGE_SECTION_HEADER结构:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
// 节的名字
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
// 字节计算的实际大小
} Misc;
DWORD VirtualAddress;
// 节的起始虚拟地址
DWORD SizeOfRawData;
// 按照文件头FileAlignment
// 对齐后的大小
DWORD PointerToRawData;
// 文件中指向该节起始的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
// 节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
节表项的数目由IMAGE_NT_HEADERS的NumberOfSections成员确定。由节表中的起始虚拟地址以及该节在文件中的位置就可以换算加载后内存虚拟地址和文件中地址之间的映射关系。添加一个节则需要修改该节表数组, 在其中增加一个表项, 然后相应修改
NumberOfSections 的数目。值得注意的是,某些PE文件现存节表后面可能紧跟着其它数据,如bound import 数据,这时就不能简单地增加一个节表项,需要先移动这些数据并修改相应的结构后才能增加节,否则PE文件将不能正常执行。由于很多病毒是自我修改的,因此节属性通常设置为E000XXXX,表示该节可读写执行,否则就需要在病毒的开始处调用VirtualProtect之类的API动态修改内存页的属性了。
由上述节表的定义还可以看到每个节的实际数据都是按照文件头中FileAlignment 对齐的,这个大小一般是512,因此每个节可能有不超过512字节的未用空间(SizeOfRawData-VirtualSize),这恰好给病毒以可乘之机,著名的CIH病毒首先采用了这种技术,不过问题是每个节的空隙大小是不定的,因此就需要将病毒代码分成若干部分存放,运行时再通过一段代码组合起来,优点是如果病毒代码较小则无需增加PE的大小,隐蔽性较强。如果所有节的未用空间仍不足以容纳病毒代码,则可新增节或附加到最后一个节上。
附加到最后一个节上是比较简单的,只要修改节表中最后一个节的VirtualSize 以及按FileAlignment 对齐后的SizeOfRawData成员即可。当然在上述所有修改节的情况中,如果改变了文件的大小,都要修正文件头中SizeOfImage这个值的大小,该值是所有节和头按照SectionAlignment 对齐后的大小。
这里有两个问题值得注意,第一问题就是对WFP(Windows File Protection)文件的处理,WFP机制是从Windows 2000 开始新增的保护系统文件的机制,若系统发现重要的系统文件被改变,则弹出一个对话框警告用户该文件已被替换。当然有多种方法绕过WFP 保护,但对病毒而言,更简单的方法就是不感染在WFP 列表中的系统文件。
可使用sfc.dll的导出函数SfcIsFileProtected判断一个文件是否在该列表中,该API 的第一个问 匦胛?,第二个参数是要判断的文件名,若在列表中返回非0值,否则返回0。另外一个问题就是关于PE文件的校验。大部分PE文件都不使用文件头中的CheckSum域的校验和值,不过有些PE文件,如关键的系统服务程序文件以及驱动程序文件则该值必须正确,否则系统加载器将拒绝加载。PE 头部的CheckSum 可以使用Imagehlp.dll的导出函数 CheckSumMappedFile计算,也可以在将该域清0后按照如下简单的等价算法计算:
如果PE文件大小是奇数字节,则以0补足,使之按偶数字节。将PE文件头的CheckSum 域清0,然后以两个字节为单位进行adc运算,最后和将该累加和同文件实际大小进行adc运算即得到校验和的值。下面的cal_checksum过程假设esi 已经指向PE文件头,文件头部CheckSum域已经被清0,CF 标志位已经被复位:
;调用示例:
;clc
;push pe_fileseize
;call cal_checksum cal_checksum:
adc bp,word [esi] ;初始esi指向文件头,ebx 中保
存的是文件大小
inc esi
inc esi
loop cal_checksum
mov ebx,[esp+4]
adc ebp,ebx ;ebp 中存放的就是PE 的校验和
ret 4
除了PE头部的校验和之外,很多程序自身也有校验模块,如Winzip 和Winrar 的自解压文件,如果被感染,将造成无法正常解压缩。因此对于类似的PE文件,病毒应尽量不予感染。
Elkern 中感染文件修改文件相关的代码在infect.asm中,该病毒首先尽可能地利用PE 的头部和节的间隙存储自身代码,若所有间隙仍不足以存放病毒代码,则附加到最后一个节上,限于篇幅相关代码从略,感兴趣的读者请自行参阅。
事实上,除了在上边提到的病毒重定位、API地址的获取、文件搜索、修改感染PE等基本技术之外,关于病毒技术还有很重要的几个方面没有提及:病毒的内存驻留感染技术、内核模式病毒技术、抗分析以及隐藏技术(EPO、多态和变形技术等)。
内存驻留感染是前述主动全盘搜索技术的变形,病毒代码驻留内存被动地等待用户事件或等待程序代码执行到指定的路径被唤醒以执行感染操作。内核态病毒,也称ring0病毒,是指那些运行在ring0特权级内核模式下的病毒,这类病毒相当特殊,需要调用内核驱动接口实现感染和传播等操作,由于NT内核的复杂性,这类病毒非常难于编写,另外由于不同版本的NT系统之间内核的差异,欲令病毒稳定运行编写者需要付出额外的努力,最后的结果可能还是会由于测试的不充分而很快因蓝屏事故而被发现,这将严重影响病毒的传播速度和传播范围。ring0 病毒比较少,其中最著名的ring0 病毒当非CIH莫属了,但由于其巨大的破坏性,格外引人注目。由于ring0病毒数量不多而且非常复
杂,本文篇幅所限,不做深入介绍。对抗杀毒软件、抗分析以及病毒自身的隐藏技术可以说是病毒技术近年来除利用社会工程学借助网络快速传播之外的又一个重要发展方向,其目的在于对抗或逃避杀毒软件的扫描,最大限度地延长其生存期,主要包括EPO(入口点模糊)技术、加密技术、多态和变形技术等。作者将在后续的文章中陆续向读者进行介绍。
内存驻留感染技术
如果读者曾经使用过MS-DOS的话,对驻留内存、截获中断以执行特定操作的程序(TSR)一定不会陌生。在MS-DOS时代,不仅正常的应用程序大量使用TSR技术,病毒同样也利用TSR 技术驻留内存,监视文件读写操作并伺机进行感染。在Windows NT下,各个进程的地址空间被隔离了,不同进程之间不能自由地相互访问内存,而且对于用户态代码有了访问限制:ring3程序代码只能读写其进程空间中应用专属的部分(在进程空间为4GB 的情况下,通常是低2GB),对系统内核部分占用的空间是没有读写权限的。这使得内存驻留感染变得困难,不过类似的想法和技术仍然是可能实现的,需要做的不过是一点变通:既然每个进程有其专属的进程空间,尽管不能做到永久驻留,但病毒代码至少在进程的生命期内仍然是可以驻留的;既然Windows下仍然有作用和DOS下中断相同的API,那么病毒自然可以截获API,从而监视文件读写,伺机进行感染。
截获API 的技术通常被称为Hook 技术,实现起来也比较简单:修改API 的入口点代码,将其修改为指向病毒代码的跳转指令,在病毒代码开始处保存传递给API的参数,待病毒代码执行完毕后再恢复API 的入口代码和保存的参数,重新跳转到API 的入口使得程序继续执行。HOOK API还有其它的几种变形,比如修改Import 表的API地址指针;修改调用API点的CALL指令;或者是为了防止API Hook检查修改API 函数的尾部或中间部分的某条指令而获取控制权,思路都是类似的。
既然前面介绍了全盘搜索感染技术,那么为什么病毒还大量使用内存驻留感染技术呢?
让我们回头思考一下,随着存储媒质价格的降低,用户配备的可存储媒质的容量也越来越大,但是磁盘读写速度并没有大幅提高,普通的全磁盘搜索是很耗时的。试想一下用户在双击了某个程序后,10分钟之后才出现界面的情况,即使是初次使用计算机的用户也会暗生疑窦,这对于病毒的隐藏和传播是极端不利的。因此那些流行的病毒并不在获得控制权后直接进行全盘搜索感染,而是采用了如下的变通:
A) 仅搜索当前目录并对其中的PE 文件进行感染,然后迅速将控制权交给宿主程序。
B) 创建单独执行的线程后,马上把控制权交给宿主程序。在用户进行操作的同时进行当前目录或全盘的搜索感染。
C) 采用Hook 技术监视文件或目录相关的API操作,对用户操作的文件或其所在的目录下的可执行文件进行感染。
搜索当前目录的技术已经在前面进行了讨论。下面主要讨论B和C中涉及到线程技术和内存驻留Hook感染技术。
1.创建独立的线程
如果读者熟悉Win32 API 程序设计,对CreateThread这个API一定不会陌生。每个线程都是一个独立的执行单元,也是Windows内核进行调度以及时间片分配的最基本的单位,同一进程内所有线程共享同一地址空间内的资源。病毒可将真正的功能部分放到该线程部分去执行。病毒代码可以在宿主程序运行的同时得以执行,普通用户通常难以察觉。
2.内存驻留Hook感染技术
上面提到的线程模型还是有缺陷的,试想一下,如果宿主程序一执行马上就退出了怎么办呢?病毒的感染线程可能还没有开始运行呢。值得注意的是:Win32线程模型中如果一个线程执行了ExitProcess调用,其它正在运行的线程不会自动得到通知。病毒代码可能还尚未执行完成就退出了,试想一下感染线程在感染某个PE文件时写入自身代码时只写入了一半的情况。尽管类似的情况在现实中极少发生,但要想提高病毒的传染能力和隐蔽性这却又是必须要考虑的问题。此外,用单独线程进行全盘搜索和感染,仍然会造成计算机CPU 资源被大量占用,用户可能会注意到程序运行缓慢、硬盘指示灯狂闪等现象。当然,可以采用感染少量文件后休眠一会继续感染或者仅在计算机空闲时进行感染来解决。
让我们换个角度重新思考一下:真的有必要搜索全盘然后感染所有的PE 文件吗?或者,全盘搜索感染模型效率高吗?事实上,用户计算机硬盘上的PE 文件有很大一部分是很少有机会执行的,除系统程序外,经常运行的程序只有很少的几个:可能是几个游戏程序,也可能是用户的业务程序。如果占用资源去感染那些很少有机会被执行的PE 文件,除演示病毒的感染性的概念之外意义是不大的。相反,如果优先感染那些用户经常执行的程序,即使每次只感染少量的文件,病毒的传播速度也会得到大幅提升。鉴于此,现代病毒作者经常使用一条简单的启发式规则:优先感染那些经常被用户或用户程序访问的PE 文件或经常访问的文件夹下的PE 文件。这样病毒就从被感染文件开始,逐渐扩散感染与该程序相关或在逻辑位置(目录层次)比较接近的PE文件,最后直至整个磁盘上的文件都被感染。
无论是想要在进程结束时得到通知,还是实现上述的文件感染模型都可借助于Hook 技术来解决。通过Hook 文件或目录操作相关的API,病毒就能获取正在操作的PE文件路径或目录,从而对正在操作的PE文件或正在操作的目录下的PE文件进行感染。有关文件或目录操作的相关API包括:
CopyFile CopyFileEx CreateFile FindFirstFile FindFirstFileEx
FindNextFile GetCurrentDirectory SetCurrentDirectory GetFileAttributes
SetFileAttributes GetFileSize GetFileType GetFullPathName LockFile
LockFileEx MoveFile MoveFileEx SearchPath UnlockFile UnlockFileEx
如果想在进程或线程结束时获得通知或进行感染操作可Hook 如下API:
ExitProcess TerminateProcess ExitThread TerminateThread
如果不考虑9X 系统,Hook API 还可以考虑Hook Native API。Windows系统内核代码运行在CPU 保护模式下的ring0特权级,普通应用程序运行在ring3特权级,普通应用程序执行IO 操作、访问内存资源等都受到严格的限制,加上Windows NT系统严格的用户权限审查机制,不同用户对资源的访问权限不同,使得病毒在运行时要考虑的因素越来越多。但反观ring0 特权级的程序,执行时没有任何限制,因此ring0 病毒对病毒编写者有着独特的吸引力。Windows 9X 下普通的Ring3应用程序切入ring0 模式非常容易,但在Windows NT系统下则困难得多,但也不是不可能的:最通用的方法就是采用内核模式驱动的加载机制将病毒自身代码写入驱动文件然后加载;另外的一些技巧还包括感染
Windows 内核驱动文件或修改NTLDR 的IDT、GDT表项的访问限制位使得在ring3 可以访问从而使得病毒代码切入ring0;除此之外还可以利用\Device\PhysicalMemory对象的漏洞切入ring0。
最成功的ring0 病毒就是大名鼎鼎的CIH,不过该病毒只能运行在Win9X 系统下。但在Windows NT系统下,尽管仍然可能切入ring0,但很少有大规模流行的ring0 病毒,这是由于NT系统内核处理流程非常复杂,编写一个在各个版本的NT系统上都能稳定运行的ring0病毒难度不小,需要更加高超的编程技巧并经过大量的测试。因此ring0病毒数量较少,鉴于此本文将不再作深入介绍。 病毒抗分析技术
当今反病毒已经成为一个产业,病毒在被发现之后,为数众多的杀毒厂商会迅速分析该病毒并升级其病毒库或推出专杀工具,颇有“老鼠过街,人人喊打”之势。但这一切都是建立在对病毒代码和病毒行为分析的基础上的。
从病毒的角度来讲,对抗病毒分析和动态静态查杀还是有意义的,尽管再狡猾的狐狸也难逃有经验猎手的追捕,但抗分析和查杀技术至少可以在一定程度上延缓反病毒厂商推出杀毒方案的时间,从而延长病毒的生命周期、扩大传播范围。