特地把Optional header放一篇文章,是因为它比较复杂与庞大,也因为它比较重要。
老样子,先是IMAGE_OPTIONAL_HEADER结构:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // 又是Magic
BYTE MajorLinkerVersion; // 链接器主要版本
BYTE MinorLinkerVersion; // 链接器次要版本
DWORD SizeOfCode; // 所有代码区段的长度之和
DWORD SizeOfInitializedData; // 同上
DWORD SizeOfUninitializedData; // 同上
// 代码起始执行处,对DLL可选,没有是0
DWORD AddressOfEntryPoint;
DWORD BaseOfCode; // 载入内存后代码的起始地址,相对ImageBase而言
DWORD BaseOfData; // 载入内存后代码的起始地址,相对ImageBase而言
DWORD ImageBase; // 期望的内存中映像载入地址
DWORD SectionAlignment; // 内存中区段排列基数
DWORD FileAlignment; // 磁盘上区段排列基数
// OS最低主要版本号
WORD MajorOperatingSystemVersion;
// OS最低次要版本号
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion; // 映像文件的主要版本号
WORD MinorImageVersion; // 映像文件的次要版本号
WORD MajorSubsystemVersion; // 子系统的主要版本号
WORD MinorSubsystemVersion; // 子系统的次要版本号
DWORD Win32VersionValue; // 由编译器定义,PE规定这里是保留位置
// 加载到内存中的映像文件所占内存的大小,一定是SectionAlignment的整数倍
DWORD SizeOfImage;
DWORD SizeOfHeaders; // 所有头与区段表的长度之和
// 映像文件校验和,仅仅针对内核模式驱动和一些系统DLL
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;
为了方便比对,我把前面那张PE截图再贴一下:
PICTURE MISSING
_IMAGE_OPTIONAL_HEADER占用从0F0h~14Fh,也就是图中第三行开始总共六行。
同样,下面挑选一些有用的结构成员讲一下。
Magic。位置F0~F1,值为010Bh。Winnt.h 为我们定义了如下三个常数:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
也就是符合第一个,表示32位程序。最后一个是ROM,指刷在ROM里面的程序,反正我从来没见过。
MajorLinkerVersion。也就是接下去的一个字节,图中是09h,说明我们的连接器版本是9.0(VC9),MinorLinkerVersion同理。
SizeOfCode。值为00 00 36 00,即13824字节,指所有代码区段的长度和。
SizeOfInitializedData。值为00 00 42 00,即16896字节,是所有已初始化数据的长度和。
AddressOfEntryPoint。截图中的位置,100h~103h,就是右边有个x开始的那里,别找不到咯~值为00 01 10 78,程序将从地址为00011078h处的指令开始执行,也就是俗称的程序入口点。经过加壳的EXE通常入口点处先是一段解密程序,完了才会跳到原始入口点(OEP)真正执行。
BaseOfCode与BaseOfData分别指代码区段与数据区段的基地址,自己看看是哪里?
ImageBase。指最希望把PE文件载入到地址空间中的ImageBase地址开始的地方,如果这个地址已经被占用,那只好换地方了,但99%的情况下,ImageBase指向的地址肯定是空闲的。
SectionAlignment与FileAlignment。位置为110~117,值分别为00 00 10 00和00 00 02 00。指的是任意区段的起始地址必须是SectionAlignment(或FileAlignment)中值的整数倍,不同的是,前者指在内存中,后者指在文件中。在内存中起始地址是1000h,即4096的整数倍,是因为Windows中一个虚拟内存页面的大小就是4096字节,这样就可以保证,一个新的区段一定会被分配到一个新的页面中,而不会出现两个区段在一个页面中的情况。同样,在磁盘上必须是200h,即512的整数倍,是因为磁盘一个扇区的大小就是512字节。
SizeOfImage。PE文件被载入内存后所占所有内存空间的总和(包括各个区段)。位置在128~12B,值为00 01 B0 00,即110592字节,合108KB。告诉你,这个程序文件只有正好30KB大小,那又怎么会在内存中变大了呢?原因就是上面说的,在内存中区段地址是按4096的整数倍分配的,而在磁盘上是按512的整数倍,这样就必定会造成很多未使用的内存空间浪费,使占用变大。但是,这也是出于性能优化的角度考虑,没什么好说的。
SizeOfHeaders。所有的头的长度总和,像什么DOS header,Optional header等等,再加上区段表(Section Table,最先的结构图里有)的长度,就是这个值。这里是1024字节,正好1K。注意:这里的Size指文件中(或磁盘中的)Size,而上面一个指内存中的,别混了。
Subsystem。位置134h~135h,值为00 03,对着下面的常数,发现是Windows控制台程序(Windows CUI)。
#define IMAGE_SUBSYSTEM_UNKNOWN 0
#define IMAGE_SUBSYSTEM_NATIVE 1
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3
#define IMAGE_SUBSYSTEM_OS2_CUI 5 /* Not in PECOFF v8 spec */
#define IMAGE_SUBSYSTEM_POSIX_CUI 7
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 /* Not in PECOFF v8 spec */
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX 14
DllCharacteristics。DLL文件特征,只对作为DLL时有效。值为81 40,计算方法仍然是使用二进制位来标记。这里原文中在胡扯,后来查了MSDN,如下
#define IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE 0x0040 // 允许重定位
#define IMAGE_DLL_CHARACTERISTICS_FORCE_INTEGRITY 0x0080 // 强制完整性检查
#define IMAGE_DLL_CHARACTERISTICS_NX_COMPAT 0x0100 // 开启数据执行保护DEP
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // 不被孤立
#define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // 关闭结构化异常处理
#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // 不被绑定
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // 这是个WDM Driver
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000 //此映像能得知终端服务
由此,我们的这个程序是IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE | IMAGE_DLL_CHARACTERISTICS_NX_COMPAT | IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE,虽然这个参数没什么意义。
DataDirectory。它是一个IMAGE_DATA_DIRECTORY类型的数组,共有IMAGE_NUMBEROF_DIRECTORY_ENTRIES个元素,一般IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16。每一个元素都指向PE文件内的一个很重要的数据结构,具体IMAGE_DATA_DIRECTORY的结构,我们下回讨论。