一些必要的知识
EXE和DLL都是PE文件,他们的区别完全是语义上的,因为他们使用的是完全相同的PE格式,唯一的区别就是用一个字段来标记该文件是EXE还是DLL。
64位Windows只是对PE格式做了一些简单的修饰,新格式叫做PE32+,并没有加新的结构进去,只是把32位的字段扩展为64位。
PE格式定义的地方位于头文件winnt.h,在这个头文件中几乎能找到任何关于头文件的定义。
PE的基本概念
PE文件使用的是一个平面的结构,文件内容被分成不同的区块,区块中包含代码或数据,各个区块按照页边界对齐,每一个区块在内存中都有一套属性,例如是否包含代码是否可写等。
PE文件不是作为单一的映射文件载入到内存中。PE装载器遍历PE文件并决定将哪一部分映射到内存中去。这种映射是把较高的偏移位置映射到较高的内存中去,这样磁盘上的文件布局就和内存中的布局是一致的,但是数据之间的相对位置可能会改变,某项的偏移地址可能区别于原来的偏移位置。

基地址
PE文件被加载器加载进内存中,在内存中的版本被称为模块(Module),映射模块的起始地址被称为模块的句柄(hModule),可以通过模块句柄访问内存中的其他数据。这个起始的内存地址也叫做基地址(ImageBase)。所以在Windows NT架构中,获取模块的句柄也就是相当于获取了模块的基地址。但是对于Windows CE并不是这样的。
模块句柄可以用这个函数来获取
GetModuleHandle(lpModuleName)参数是模块名,返回的就是模块句柄,若没有参数(NULL),返回的则是调用进程本身的句柄。
基地址是文件本身设定的,如果按照默认的设定,使用VC++建立的EXE文件的默认基地址是400000h,DLL是10000000h,这个地址是可以被改变的。
虚拟地址
PE文件被加载进内存所拥有虚拟空间的内存地址就是虚拟地址(VA)。
相对虚拟地址
PE文件虽然存在一个首选载入地址,但是它可以载入任何地方(基地址),所以不能依赖PE的载入点,因此引用了相对虚拟地址(RVA),即相对于基地址的偏移量。例如一个虚拟地址为410000h,则其基地址为400000h,RVA为10000h。
文件偏移地址
当PE文件存储在磁盘中时,某个数据的位置相对于文件头的偏移量就是文件偏移地址(File Offset)或者叫做物理地址(Raw Offset)。从文件的第一个字节开始计数,从0开始,使用十六进制工具打开PE文件所显示的地址就是文件偏移地址。下图左边的地址就是010Editor中显示的文件偏移地址。

MS-DOS头
PE文件始于一个传统的MS-DOS头部,被称作IMAGE_DOS-HEADER,其结构如下
IMAGE_DOS_HEADER STRUCT
{
+0h WORD e_magic // Magic DOS signature MZ(4Dh 5Ah) DOS可执行文件标记
+2h WORD e_cblp // Bytes on last page of file
+4h WORD e_cp // Pages in file
+6h WORD e_crlc // Relocations
+8h WORD e_cparhdr // Size of header in paragraphs
+0ah WORD e_minalloc // Minimun extra paragraphs needs
+0ch WORD e_maxalloc // Maximun extra paragraphs needs
+0eh WORD e_ss // intial(relative)SS value DOS代码的初始化堆栈SS
+10h WORD e_sp // intial SP value DOS代码的初始化堆栈指针SP
+12h WORD e_csum // Checksum
+14h WORD e_ip // intial IP value DOS代码的初始化指令入口[指针IP]
+16h WORD e_cs // intial(relative)CS value DOS代码的初始堆栈入口
+18h WORD e_lfarlc // File Address of relocation table
+1ah WORD e_ovno // Overlay number
+1ch WORD e_res[4] // Reserved words
+24h WORD e_oemid // OEM identifier(for e_oeminfo)
+26h WORD e_oeminfo // OEM information;e_oemid specific
+29h WORD e_res2[10] // Reserved words
+3ch DWORD e_lfanew // Offset to start of PE header 指向PE文件头
} IMAGE_DOS_HEADER ENDS
其中左边是偏移量。magic是DOS可执行文件标记MZ(4D5A),是固定的,最后的lfnew是PE头指针,指向真正的PE头部。

其后面紧跟着的是DOS stub(DOS块),其实际上是一个有效的exe,在不支持PE格式的操作系统中会显示一个简单的错误提示,类似于字符串“This program cannot be run in DOS mode”,在大多数情况下这段结构是由编译器自动生成的。一般通常把DOS MZ头与DOS stub合称为DOS文件头。
PE文件头(IMAGE_NT_HEADER)
PE文件头紧挨着DOS stub,PE header是相关结构NT映像头的简称(IMAGE_NT_HEADERS),其中包含了许多PE装载器能用得到的重要字段。在ODS头中的ifanew字段加上基地址就是PE头的指针。PE头实际上有32位和64位两个版本(PE32和PE32+),但实际上这两个版本没有任何区别。
IMAGE_NT_HEADERS STRUCT
{
+0h DWORD Signature //
+4h IMAGE_FILE_HEADER FileHeader //
+18hIMAGE_OPTIONAL_HEADER32OptionalHeader //
} IMAGE_NT_HEADERS ENDS
其中Signature结构是PE的标志,PE00,是PE文件头的开始。
IMAGE_FILE_HEADER
IMAGE_FILE_HEADER(映像文件头)结构包含PE文件的一些基本信息,其结构如下
typedef struct _IMAGE_FILE_HEADER {
+04h WORD Machine; // 运行平台
+06h WORD NumberOfSections; // 文件的区块数目
+08h DWORD TimeDateStamp; // 文件创建日期和时间
+0Ch DWORD PointerToSymbolTable; // 指向符号表(主要用于调试)
+10h DWORD NumberOfSymbols; // 符号表中符号个数(同上)
+14h WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 结构大小
+16h WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine是可执行文件的目标CPU类型,其中典型的机器类标志如下
| 机器类型 | 标志 |
|---|---|
| Intel i386 | 14Ch |
| MIPS R3000 | 162H |
| MIPS R4000 | 166H |
| Alpha AXP | 184H |
| Power PC | 1F0H |
| MIPS R10000 | 0x0168 |
| AMD64 | 0x8664 |
| ALPHA64 | 0x0284 |
| IA64 | 0x0200 |
NumberOfSections是文件的区块数目,TimeDateStamp是文件创建的时间,这个值是从1970年1月1日以来用格林尼治时间计算的秒数。
PointerToSymbolTable是COFF符号表的偏移地址,主要用于调试,若不存在,这一项可设置为0,NumberOfSymbols代表符号表的符号个数。
SizeOfOptionalHeader表示IMAGE_OPTIONAL_HEADER32 结构大小,其最小值对于PE32结构是00E0H,对于PE32+是00F0H。
Characteristics表示文件属性,通过几个值运算得到。这些值定义与winnt.h的IMAGE_FILE_XXX中。
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 重定位信息被移除,文件必须加载先前的基地址
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可执行
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 行号被移除
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符号被移除
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 程序能处理大于2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位机器
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // .dbg文件的调试信息被移除
#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 // 处理机的高位字节是相反的
其值是通过上述的值或运算得到,上述定义的值都是2^n,所以运算后对于二进制就是不同的位的一。

IMAGE_OPTIONAL_HEADER结构
尽管IMAGE_OPTIONAL_HEADER是一个可选结构,但是IMAGE_FILE_HEADERvu足以包含所有的属性,所以这个结构中给出了更多的选项,其结构如下
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+18h WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号
+1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h DWORD AddressOfEntryPoint; // 程序执行入口RVA
+2Ch DWORD BaseOfCode; // 代码的区块的起始RVA
+30h DWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+34h DWORD ImageBase; // 程序的首选装载地址
+38h DWORD SectionAlignment; // 内存中的区块的对齐大小
+3Ch DWORD FileAlignment; // 文件中的区块的对齐大小
+40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h WORD MajorImageVersion; // 可运行于操作系统的主版本号
+46h WORD MinorImageVersion; // 可运行于操作系统的次版本号
+48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58h DWORD CheckSum; // 映像的校检和
+5Ch WORD Subsystem; // 可执行文件期望的子系统
+5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags; // 与调试有关,默认为 0
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic是一个标记字,用于标记文件是ROM映像(0107h)还是可执行文件的映像(010Bh),如果是PE32+,则是020Bh。
定义如下:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10B
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20B
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
MajorLinkerVersion 连接程序的主版本号 如vc6.0的为06h
MinorLinkerVersion 连接程序的次版本号 如vc6.0的为00h
SizeOfCode是image_scn_cnt_code属性区块的总大小,即代码段的大小。多数文件只有一个Code块,所以这个字段和.text块的大小相匹配。
SizeOfInitializedData 所有含已初始化数据的块的大小,即在编译时所构成块的大小(不包含代码段),一般在.data段中,但这个数据不太准确。
SizeOfUninitializedData 所有含未初始化数据的块的大小,装在程序要在虚拟地址控件中给这些数据约定空间,这些块在磁盘中并不占地方,在程序开始时并没有指定值,一般在.bss段中.
AddressOfEntryPoint字段,指出文件被执行时的入口地址(OEP),这是一个RVA地址,对于dll,这个入口点在进程初始化和关闭时及线程创建和毁灭时调用。在多数可执行文件中,这个值并不指向Main、WinMain或DllMain函数,而是指向运行需要的库代码并由其调用上述函数,对于exe文件,这里是启动代码;对于dll文件,这里是libMain()的地址.。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。
BaseOfData 数据段基地址,数据段一般出现在内存的末尾,这个字段在PE32+结构是没有的,下图中可以看到PE64(即PE32+)没有此字段。

BaseOfCode 代码段基地址,代码段通常在PE头和数据段之间,微软的连接程序生成的程序一般把这个值置为1000h。
ImageBase pe文件默认的装入地址,这是一个理想地址,如果有可能的话(即地址当前没有被占用且是一个合法地址),加载器会试图在这个地址装在PE文件。当文件被装载进这个地址中时,将不进行重定位操作,斗则会进行过重定位,速度会慢一点。对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一,因此,在前面介绍的IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。windows9x中exe文件默认为400000h,dll文件为10000000h.
SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。对运行在Windows 9x、Me下的用户模式可执行文件,最小的对其尺寸是每页1000h(4kb)。
FileAlignment字段指定了节存储在磁盘文件中时的对齐单位,对于x86可执行文件,这个值通常是200h或1000h,这是为了保证块总是从磁盘的扇区开始,使用不同版本的Microsoft连接器,这个值会被改变,但必须是2的幂,最小为200h。而且如果SectionAlignment小于CPU的页尺寸,这个值就必须与SectionAlignment匹配。
由于在磁盘中的页大小和在内存中的页大小不同,所以文件在被装载进内存中与在磁盘中不是完全一样的,中间的差距用0填充,这在上面的图上可以表现出来。
MajorOperatingSystemVersion、MinorOperatingSystemVersion是指运行这个pe文件所需的操作系统的最低版本号(分别是主版本号和次版本号)
MajorImageVersion、MinorImageVersion,是指用户自定义的pe文件的版本号(分别是主版本号和次版本号).可以通过连接程序来设置
MajorSubsystemVersion、MinorSubsystemVersion要求最低子系统版本的主、次版本号
Win32VersionValue 总是0
SizeOfImage pe文件装入内存后映像的总大小,从ImageBase到最后一个块的总大小,四舍五入到文件对齐值的倍数。如果SectionAlignment域和FileAlignment域相等,那么这个值也是pe文件在硬盘上的大小.
SizeOfHeader是MS-DOS头、PE头和区块表的总尺寸,在PE文件代码块或数据块之前
CheckSum pe文件的CRC校验和,IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算该值,一般的EXE文件该值可以为0h。
Subsystem pe文件的用户界面使用的子系统(UI)类型,这个值只对EXE重要。如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。
| 值 | 子系统 |
|---|---|
| 0 | 未知 |
| 1 | 不需要子系统(例如驱动程序) |
| 2 | 图形接口子系统(GUI) |
| 3 | 字符子系统(CUI) |
| 5 | OS/2字符子系统 |
| 7 | POSIX字符子系统 |
| 8 | 保留 |
| 9 | WinCE图形界面 |
DllCharacteristics Dll文件特性,与Characteristics相类似,采用相同的计算方式,其定义如下
// IMAGE_LIBRARY_PROCESS_INIT 0x0001 // 保留
// IMAGE_LIBRARY_PROCESS_TERM 0x0002 // 保留
// IMAGE_LIBRARY_THREAD_INIT 0x0004 // 保留
// IMAGE_LIBRARY_THREAD_TERM 0x0008 // 保留
#define IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA 0x0020 // Image can handle a high entropy 64-bit virtual address space.
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 // DLL 可重定位。
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 // Code Integrity Image
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 // 该映像与 NX 兼容。
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // 该映像理解隔离,但不需要隔离。
#define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // 该映像不使用 SEH。 该映像中不能有任何 SE 处理程序。
#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // 请勿绑定此映像。
#define IMAGE_DLLCHARACTERISTICS_APPCONTAINER 0x1000 // 映像必须在 AppContainer 中运行。
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // 驱动程序使用 WDM 模型。
#define IMAGE_DLLCHARACTERISTICS_GUARD_CF 0x4000 // Image supports Control Flow Guard.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000 //该映像支持终端服务器感知。
SizeOfStackReserve 为线程的栈初始保留的虚拟内存的大小,默认为00100000h.如果在调用CreateThread函数时指定堆栈的大小为0,被创建的线程的堆栈的初始大小就与这个值相同.
SizeOfStackCommit 为线程的栈初始提交的虚拟内存的大小.微软的连接程序把这个值置为 1000h.
SizeOfHeapReserve 为进程的堆保留的虚拟内存的大小.默认值为 00100000h.
SizeOfHeapCommit 为进程的堆初始提交的虚拟内存的大小.微软的连接程序把这个值置为1000h.
LoadFlags 与调试有关,默认为0
NumberOfRvaAndSizes 数据目录结构数组的项数,即下面数据目录的项数,总为 00000010h
DataDirectory 数据目录表,是一个数组,由16个相同的IMAGE_DATA_DIRECTORY结构组成,IMAGE_DATA_DIRECTORY结构如下
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;//数据起始RVA
DWORD Size;//数据块长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
数据目录成员如下表
| 结构 | 序号 | 成员 | 偏移量(PE/PE32+) |
|---|---|---|---|
| IMAGE_DIRECTORY_ENTRY_EXPORT | 0 | 导出表 | 78h/88h |
| IMAGE_DIRECTORY_ENTRY_IMPORT | 1 | 导入表 | 90h/90h |
| IMAGE_DIRECTORY_ENTRY_RESOURCE | 2 | 资源 | 88h/98h |
| IMAGE_DIRECTORY_ENTRY_EXCEPTION | 3 | 异常 | 90h/A0h |
| IMAGE_DIRECTORY_ENTRY_SECURITY | 4 | 安全 | 98h/A8h |
| IMAGE_DIRECTORY_ENTRY_BASERELOC | 5 | 重定位表 | A0h/B0h |
| IMAGE_DIRECTORY_ENTRY_DEBUG | 6 | 调试信息 | A8h/B8h |
| IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 7 | 版权信息 | B0h/C0h |
| IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 8 | RVA of GP | B8h/C8h |
| IMAGE_DIRECTORY_ENTRY_TLS | 9 | TLS Directory | C0h/D0h |
| IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 10 | Load Configuration Directory | C8h/D8h |
| IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 11 | Bound Import Directory in headers | D0h/E0h |
| IMAGE_DIRECTORY_ENTRY_IAT | 12 | 导入函数地址表 | D8h/E8h |
| IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 13 | Delay Load Import Descriptors | E0h/F0h |
| IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 14 | COM Runtime descriptor | E8h/F8h |
| 15 | 保留,必须为0 | F0h/100h | |
| PE文件在定义输出表、输入表等重要数据时,就是从这里开始的。 |



