【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
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