【pker / CVC.GB】 
1、声明 
------- 
本文仅仅是一篇讲述病毒原理的理论性文章,任何人如果通过本文中讲述的技术或利用本文 
中的代码写出恶性病毒,造成的任何影响均与作者无关。 
2、前言 
------- 
病毒是什么?病毒就是一个具有一定生物病毒特性,可以进行传播、感染的程序。病毒同样 
是一个程序,只不过它经常做着一些正常程序不常做的事情而已,仅此而已。在这篇文章中 
我们将揭开病毒的神秘面纱,动手写一个病毒(当然这个病毒是不具有破坏力的,仅仅是一 
个良性病毒)。 
在网上有很多病毒方面的入门文章,但大部分都很泛泛,并不适合真正的初学者。真正的高 
手没有时间也不屑于写这样一篇详细的入门文章,所以我便萌发了写这样一篇文章的冲动, 
一来是对自己的学习进行一下总结,二来也是想让像我一样的初学者能少走一些弯路。如果 
你有一定的病毒编写基础,那么就此打住,这是一篇为对病毒编程完全没有概念的读者编写 
的,是一篇超级入门的文章 :P 
3、对读者的假设 
--------------- 
没错,这是一篇完整、详细的入门文章,但是如果读者对编程还没有什么认识我想也不可能 
顺利地读下去。本文要求读者: 
1)  有基本的C/C++语言知识。因为文章中的很多结构的定义我使用的是C/C++的语法。 
2)  有一定的汇编基础。在这篇文章中我们将使用FASM编译器,这个编译器对很多读者来说 
    可能很陌生,不过没关系,让我们一起来熟悉它 :P 
3)  有文件格式的概念,知道一个可执行文件可以有ELF、MZ、LE、PE之分。 
好了,让我们开始我们的病毒之旅吧!!! 
4、PE文件结构 
------------- 
DOS下,可执行文件分为两种,一种是从CP/M继承来的COM小程序,另一种是EXE可执行文件, 
我们称之为MZ文件。而Win32下,一种新的可执行文件可是取代了MZ文件,就是我们这一节 
的主角 -- PE文件。 
PE(Portable Executable File Format)称为可移植执行文件格式,我们可以用如下的表 
来描述一个PE文件: 
+-----------------------------+     -------------------------------------------- 
|         DOS MZ文件头        |                                         ^ 
+-----------------------------+                                      DOS部分 
|            DOS块            |                                         v 
+-----------------------------+     -------------------------------------------- 
|           PE\0\0            |                                         ^ 
+-----------------------------+                                         | 
|    IMAGE_FILE_HEADER结构    |                                      PE文件头 
+-----------------------------+                                         | 
| IMAGE_OPTIONAL_HEADER32结构 |                                         v 
+-----------------------------+     -------------------------------------------- 
|                             |-----+                                   ^ 
|                             |-----+-----+                             | 
|  n*IMAGE_SECTION_HEADER结构 |-----+-----+-----+                     节表 
|                             |-----+-----+-----+-----+                 | 
|                             |-----+-----+-----+-----+-----+           v 
+-----------------------------+     |     |     |     |     |     -------------- 
|           .text节           |<----+     |     |     |     |           ^ 
+-----------------------------+           |     |     |     |           | 
|           .data节           |<----------+     |     |     |           | 
+-----------------------------+                 |     |     |           | 
|           .idata节          |<----------------+     |     |        节数据 
+-----------------------------+                       |     |           | 
|           .reloc节          |<----------------------+     |           | 
+-----------------------------+                             |           | 
|             ...             |<----------------------------+           v 
+-----------------------------+     -------------------------------------------- 
好了,各位读者请准备好,我们要对PE格式进行一次超高速洗礼,嘿嘿。 
PE文件的头部是一个DOS MZ文件头,这是为了可执行文件的向下兼容性设计的。PE文件的DOS 
部分分为两部分,一个是MZ文件头,另一部分是DOS块,这里面存放的是可执行代码部分。还 
记得在DOS下运行一个PE文件时的情景么:“This program cannot be run in DOS mode.”。 
没错,这就是DOS块(DOS Stub)完成的工作。下面我们先来看看MZ文件头的定义: 
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header 
    WORD   e_magic;                     // Magic number 
    WORD   e_cblp;                      // Bytes on last page of file 
    WORD   e_cp;                        // Pages in file 
    WORD   e_crlc;                      // Relocations 
    WORD   e_cparhdr;                   // Size of header in paragraphs 
    WORD   e_minalloc;                  // Minimum extra paragraphs needed 
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed 
    WORD   e_ss;                        // Initial (relative) SS value 
    WORD   e_sp;                        // Initial SP value 
    WORD   e_csum;                      // Checksum 
    WORD   e_ip;                        // Initial IP value 
    WORD   e_cs;                        // Initial (relative) CS value 
    WORD   e_lfarlc;                    // File address of relocation table 
    WORD   e_ovno;                      // Overlay number 
    WORD   e_res[4];                    // Reserved words 
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo) 
    WORD   e_oeminfo;                   // OEM information; e_oemid specific 
    WORD   e_res2[10];                  // Reserved words 
    LONG   e_lfanew;                    // File address of new exe header 
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; 
其中e_magic就是鼎鼎大名的‘MZ’,这个我们并不陌生。后面的字段指明了入口地址、堆 
栈位置和重定位表位置等。我们还要关心的一个字段是e_lfanew字段,它指定了真正的PE文 
件头,这个地址总是经过8字节对齐的。 
下面让我们来真正地走进PE文件,下面是PE文件头的定义: 
typedef struct _IMAGE_NT_HEADERS { 
    DWORD Signature; 
    IMAGE_FILE_HEADER FileHeader; 
    IMAGE_OPTIONAL_HEADER32 OptionalHeader; 
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 
PE文件头的第一个双字是00004550h,即字符P、E和两个0。后面还有两个结构: 
typedef struct _IMAGE_FILE_HEADER { 
    WORD    Machine; 
    WORD    NumberOfSections; 
    DWORD   TimeDateStamp; 
    DWORD   PointerToSymbolTable; 
    DWORD   NumberOfSymbols; 
    WORD    SizeOfOptionalHeader; 
    WORD    Characteristics; 
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; 
typedef struct _IMAGE_OPTIONAL_HEADER { 
    // 
    // Standard fields. 
    // 
    WORD    Magic; 
    BYTE    MajorLinkerVersion; 
    BYTE    MinorLinkerVersion; 
    DWORD   SizeOfCode; 
    DWORD   SizeOfInitializedData; 
    DWORD   SizeOfUninitializedData; 
    DWORD   AddressOfEntryPoint; 
    DWORD   BaseOfCode; 
    DWORD   BaseOfData; 
    // 
    // NT additional fields. 
    // 
    DWORD   ImageBase; 
    DWORD   SectionAlignment; 
    DWORD   FileAlignment; 
    WORD    MajorOperatingSystemVersion; 
    WORD    MinorOperatingSystemVersion; 
    WORD    MajorImageVersion; 
    WORD    MinorImageVersion; 
    WORD    MajorSubsystemVersion; 
    WORD    MinorSubsystemVersion; 
    DWORD   Win32VersionValue; 
    DWORD   SizeOfImage; 
    DWORD   SizeOfHeaders; 
    DWORD   CheckSum; 
    WORD    Subsystem; 
    WORD    DllCharacteristics; 
    DWORD   SizeOfStackReserve; 
    DWORD   SizeOfStackCommit; 
    DWORD   SizeOfHeapReserve; 
    DWORD   SizeOfHeapCommit; 
    DWORD   LoaderFlags; 
    DWORD   NumberOfRvaAndSizes; 
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; 
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; 
我们先来看看IMAGE_FILE_HEADER。Machine字段指定了程序的运行平台。 
NumberOfSections指定了文件中节(有关节的概念后面会有介绍)的数量。 
TimeDataStamp是编译次文件的时间,它是从1969年12月31日下午4:00开始到创建为止的总 
秒数。 
PointerToSymbolTable指向调试符号表。NumberOfSymbols是调试符号的个数。这两个字段 
我们不需要关心。 
SizeOfOptionalHeader指定了紧跟在后面的IMAGE_OPTIONAL_HEADER结构的大小,它总等于 
0e0h。 
Characteristics是一个很重要的字段,它描述了文件的属性,它决定了系统对这个文件的 
装载方式。下面是这个字段每个位的含义(略去了一些我们不需要关心的字段): 
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // 文件中不存在重定位信息 
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // 文件是可执行的 
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // 程序可以触及大于2G的地址 
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // 小尾方式 
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32位机器 
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // 不可在可移动介质上运行 
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // 不可在网络上运行 
#define IMAGE_FILE_SYSTEM                    0x1000  // 系统文件 
#define IMAGE_FILE_DLL                       0x2000  // 文件是一个DLL 
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // 只能在单处理器计算机上运行 
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // 大尾方式 
下面我们再来看一下IMAGE_OPTIONAL_HEADER32结构,从字面上看好象这个结构是可选的, 
其实则不然,它是每个PE文件不可缺少的部分。我们分别对每个字段进行讲解,同样我们仍 
省略了一些我们不太关心的字段。 
Magic字段可能是两个值:107h表示是一个ROM映像,10bh表示是一个EXE映像。 
SizeOfCode表示代码节的总大小。 
SizeOfInitializedData指定了已初始化数据节的大小,SizeOfUninitializedData包含未初 
始化数据节的大小。 
AddressOfEntryPoint是程序入口的RVA(关于RVA的概念将在后面介绍,这是PE文件中的一个 
非常重要又非常容易混淆的概念)。如果我们要改变程序的执行入口则可以改变这个值 :P 
BaseOfCode和BaseOfData分别是代码节和数据节的起始RVA。 
ImageBase是程序建议的装载地址。如果可能的话系统将文件加载到ImageBase指定的地址, 
如果这个地址被占用文件才被加载到其他地址上。由于每个程序的虚拟地址空间是独立的, 
所以对于优先装入的EXE文件而言,其地址空间不可能被占用;而对于DLL,其装入的地址空 
间要依具体程序的地址空间的使用状态而定,所以可能每次装载的地址是不同的。这还引出 
了另一个问题就是,一般的EXE文件不需要定位表,而DLL文件必须要有一个重定位表。 
SectionAligment和FileAligment分别是内存中和文件中的对齐粒度,正是由于程序在内存 
中和文件中的对齐粒度不同才产生了RVA概念,后面提到。 
SizeOfImage是内存中整个PE的大小。 
SizeOfHeaders是所有头加节表的大小。 
CheckSum是文件的校验和,对于一般的PE文件系统并不检查这个值。而对于系统文件,如驱 
动等,系统会严格检查这个值,如果这个值不正确系统则不予以加载。 
Subsystem指定文件的子系统。关于各个取值的定义如下: 
#define IMAGE_SUBSYSTEM_UNKNOWN              0   // 未知子系统 
#define IMAGE_SUBSYSTEM_NATIVE               1   // 不需要子系统 
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Windows图形界面 
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Windows控制台界面 
#define IMAGE_SUBSYSTEM_OS2_CUI              5   // OS/2控制台界面 
#define IMAGE_SUBSYSTEM_POSIX_CUI            7   // Posiz控制台界面 
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // Win9x驱动程序,不需要子系统 
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // Windows CE子系统 
NumberOfRvaAndSizes指定了数据目录结构的数量,这个数量一般总为16。 
DataDirectory为数据目录。 
下面是数据目录的定义: 
typedef struct _IMAGE_DATA_DIRECTORY { 
    DWORD   VirtualAddress; 
    DWORD   Size; 
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 
VirtualAddress为数据的起始RVA,Size为数据块的长度。下面是数据目录列表的含义: 
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出表 
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // 引入表 
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 资源 
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // 异常 
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // 安全 
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // 重定位表 
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // 调试信息 
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // 版权信息 
...... 
看到这里大家是不是很混乱呢?没办法,只能硬着头皮“啃”下去,把上面的内容再重新读 
一遍... 下面我们继续,做好准备了么?我们开始啦!! 
紧接着IMAGE_NT_HEADERS结构的是节表。什么是节表呢?别着急,我们先要清楚一下什么是 
节。PE文件是按照节的方式组织的,比如:数据节、代码节、重定位节等。每个节有着自己 
的属性,如:只读、只写、可读可写、可执行、可丢弃等。其实在执行一个PE文件的时候, 
Windows并不是把整个PE文件一下读入内存,而是采用内存映射的机制。当程序执行到某个 
内存页中的指令或者访问到某个内存页中的数据时,如果这个页在内存中那么就执行或访问, 
如果这个页不在内存中而是在磁盘中,这时会引发一个缺页故障,系统会自动把这个页从交 
换文件中提交的物理内存并重新执行故障指令。由于这时这个内存页已经提交到了物理内存 
则程序可以继续执行。这样的机制使得文件装入的速度和文件的大小不成比例关系。 
节表就是描述每个节属性的表,文件中有多少个节就有多少个节表。下面我们来看一下节表 
的结构: 
#define IMAGE_SIZEOF_SHORT_NAME              8 
typedef struct _IMAGE_SECTION_HEADER { 
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; 
    union { 
            DWORD   PhysicalAddress; 
            DWORD   VirtualSize; 
    } Misc; 
    DWORD   VirtualAddress; 
    DWORD   SizeOfRawData; 
    DWORD   PointerToRawData; 
    DWORD   PointerToRelocations; 
    DWORD   PointerToLinenumbers; 
    WORD    NumberOfRelocations; 
    WORD    NumberOfLinenumbers; 
    DWORD   Characteristics; 
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 
Name为一个8个字节的数组。定义了节的名字,如:.text等。习惯上我们把代码节称为.text, 
把数据节称为.data,把重定位节称为.reloc,把资源节称为.rsrc等。但注意:这些名字不 
是一定的,可一任意命名,千万不要通过节的名字来定位一个节。 
Misc是一个联合。通常是VirtualSize有效。它指定了节的大小。这是节在没有进行对齐前的 
大小。 
VirtualAddress指定了这个节在被映射到内存中后的偏移地址,是一个RVA地址。这个地址是 
经过对齐的,以SectionAlignment为对齐粒度。 
PointerToRawData指定了节在磁盘文件中的偏移,注意不要与RVA混淆。 
SizeOfRawData指定了节在文件中对齐后的大小,即VirtualSize的值根据FileAlignment粒度 
对齐后的大小。 
Characteristics同样又是一个很重要的字段。它指定了节的属性。下面是部分属性的定义: 
#define IMAGE_SCN_CNT_CODE                   0x00000020  // 节中包含代码 
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // 节中包含已初始化数据 
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // 节中包含未初始化数据 
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  // 是一个可丢弃的节,即 
                                                         // 节中的数据在进程开始 
                                                         // 后将被丢弃 
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000  // 节中数据不经过缓存 
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000  // 节中数据不被交换出内存 
#define IMAGE_SCN_MEM_SHARED                 0x10000000  // 节中数据可共享 
#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // 可执行节 
#define IMAGE_SCN_MEM_READ                   0x40000000  // 可读节 
#define IMAGE_SCN_MEM_WRITE                  0x80000000  // 可写节 
好了,是时候跟大家介绍RVA的概念了。这是一个大多数初学者经常搞不清楚的容易混淆的概 
念。RVA是Relative Virtual Address的缩写,即相对虚拟地址。那么RVA到底代表什么呢? 
简单的说就是,RVA是内存中相对装载基址的偏移。假设一个进程的装载地址为00400000h, 
一个数据的地址为00401234h,那么这个数据的RVA为00401234h-00400000h=1234h。 
好累啊... 不知道我的描述是否清楚呢?我想多数读者读到这里一定又是一头雾水吧?为什 
么要将这么多关于PE文件的知识呢?(废什么话?这样的问题也拿出来问。呵呵,我好象听 
到有人这么说了 :P)因为Win32下的可执行文件、DLL和驱动等都是PE格式的,我们的病毒 
要感染它们,所以必须要把整个PE格式烂熟于心。 
其实关于PE文件我们还有导入表、导出表、重定位表、资源等很多内容没有讲。但是为了让 
读者能够减轻一些负担,所以把这些内容穿插在后面的小节中,直到涉及到相关知识时我们 
再进行讲解。 
下面我们准备进入下一节,在进入下一节之前我建议读者把前面的内容再巩固一遍,在后面 
的一节中我们要向大家介绍一款相当优秀的编译器 ---- FASM(Flat Assembler)。为什么 
我要推荐它呢?一会儿你就会知道 :P
posted on 2008-10-13 22:42  一个人的天空@  阅读(636)  评论(0编辑  收藏  举报