总览图:
前置知识
虚拟地址 WIndows系统中,PE文件被系统加载器映射到内存中,每个程序都有自己的虚拟空间,该虚拟空间中的内存地址称为虚拟地址(VA)。
相对虚拟地址 PE文件虽然有一个首选的载入地址,但是其可在进程空间的任何地方载入,故需要有一种方式方式来指定内存中的具体地址。为避免PE文件中出现绝对地址故引入了相对虚拟地址(RVA),其是相对于PE文件载入地址的一个偏移量,计算公式为目标地址-载入地址=RVA。
文件偏移地址 PE文件储存在磁盘里时某个数据相对于文件头的偏移量。
DOS头 PE文件都是从一个DOS程序开始的,程序在DOS下执行时系统能识别这是一个有效的执行体并运行紧随MZ header的DOS stub中的内容,即显示一个简单的错误提示。程序员也可以自行在此处实现完整的DOS程序代码。
MS-DOS头部完整结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER
其中e_magic字段的值需要设置为0x5A4D ,即为字符”MZ”,是PE文件的一个标志。
而e_lfanew字段是真正的PE文件头的相对偏移,其指出真正PE头的文件偏移位置,位于文件开始偏移0x3C 位置处。
PE头 紧随DOS stub之后的是 PE头(PE header),其包含许多PE装载器用得到的字段。
IMAGE_NT_HEADER实际有两个版本,分别对应32位和64位,但是差别很小,此处将其忽略。
1 2 3 4 5 typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature字段 其被设置为0x00004550,对应字符”PE\0\0”。此处为PE文件头的开始,也是PE文件的一个重要标志。
PE文件头 此处包含一些PE文件的基本信息,并指出了可选头的大小。
1 2 3 4 5 6 7 8 9 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;
Machine 其记录可执行文件的目标CPU类型,PE文件可在多种机器上使用,不同平台上指令的机器码不同。在intel i386上标志为0x14C ,在x86_64上为0x8664 。
NumberOfSections 其记录区块(Section)的数目。
TimeDateStamp 表示文件的创建时间。可用_ctime或gmtime函数将此值翻译为易读的字符串.
PointerToSymbolTable COFF符号表的文件偏移位置。
NumberOfSymbols 若有COFF符号表,其表示其中的符号数目。
SizeOfOptionalHeader 表示可选头的大小,其大小于文件32/64bit有关,32位PE文件此处的值一般为0x00E0,64位则为0x00F0。
Characteristics 文件属性,有选择地通过几个值地运算得到(通过标志位形式存储)。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #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 #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 #define IMAGE_FILE_32BIT_MACHINE 0x0100 #define IMAGE_FILE_DEBUG_STRIPPED 0x0200 #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 #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000
可选头 IMAGE_OPTIONAL_HEADER64与IMAGE_OPTIONAL_HEADER结构差别不大,一下以PE32为例并标注区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; 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;
可使用study_PE、LordPE等工具查看这些字段信息
区块表(节表) IMAGE_SECTION_HEADERS结构包含了其所关联的区块的信息,例如位置、长度、属性等,该数组的数目由IMAGE_SECTION_HEADERS.FileHeader.NumberOfSections指出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #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;
Characteristics指出块属性的标志,具体标志位代表的含义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #define IMAGE_SCN_TYPE_NO_PAD 0x00000008 #define IMAGE_SCN_CNT_CODE 0x00000020 #define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 #define IMAGE_SCN_LNK_OTHER 0x00000100 #define IMAGE_SCN_LNK_INFO 0x00000200 #define IMAGE_SCN_LNK_REMOVE 0x00000800 #define IMAGE_SCN_LNK_COMDAT 0x00001000 #define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 #define IMAGE_SCN_GPREL 0x00008000 #define IMAGE_SCN_MEM_FARDATA 0x00008000 #define IMAGE_SCN_MEM_PURGEABLE 0x00020000 #define IMAGE_SCN_MEM_16BIT 0x00020000 #define IMAGE_SCN_MEM_LOCKED 0x00040000 #define IMAGE_SCN_MEM_PRELOAD 0x00080000 #define IMAGE_SCN_ALIGN_1BYTES 0x00100000 #define IMAGE_SCN_ALIGN_2BYTES 0x00200000 #define IMAGE_SCN_ALIGN_4BYTES 0x00300000 #define IMAGE_SCN_ALIGN_8BYTES 0x00400000 #define IMAGE_SCN_ALIGN_16BYTES 0x00500000 #define IMAGE_SCN_ALIGN_32BYTES 0x00600000 #define IMAGE_SCN_ALIGN_64BYTES 0x00700000 #define IMAGE_SCN_ALIGN_128BYTES 0x00800000 #define IMAGE_SCN_ALIGN_256BYTES 0x00900000 #define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 #define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 #define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 #define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 #define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 #define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 #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
常见的区段介绍
区块的大小是要对齐 的。有两种对齐值,一种用于磁盘文件内,另一种用于内存中。SectionAlignment定义了内存中区块的对齐值。x86系列CPU上,内存页是按4KB(0x1000)排列的,x64上是按8KB(0x2000)排列的。
而一些PE文件为了减小体积,磁盘对齐值常常是0x200等值,当其载入内存时需要将进行转换。 文件被映射到内存中时,DOS头、PE文件头和块表的偏移位置与大小均没有变化,而当各区块被映射到内存中后,其偏移位置就发生了变化。
如图,磁盘文件中.text 块起始端与文件头的偏移量为add1,映射到内存后,.text 块起始端与文件头(基地址)的偏移量为add2。同 时,.text 块与块表之间形成了一大段空隙,这部分数据全是以0填充的。在这里add1的值就是文件偏移地址(File Offset),add2的值就是相对虚拟地址(RVA)。假设它们的差值为△k,则文件偏移地址与虚拟地址的关系如下File Offset = RVA - △k或File Offset = VA - ImageBase - △k
在同一区块中,各地址的偏移量是相等的,但是不同区块的差值是不同的,这是因为各区块在内存中是以一个页边界开始的,从第1个区块的结束到第2个区块的开始(1000h对齐处)全以数据0填充,所以不同区块在磁盘与内存中的差值不同。
导入表 in .idata 导入函数是程序中调用但不存在于程序中的函数,其在程序载入内存后才从DLL文件中载入。
在PE文件中存在一组数据结构,其分别对应于被输入的DLL。每一个这样的结构都给出了被输入的DLL名称并指向一组函数指针,其被称为输入地址表IAT(Import Address Table) 。
由于编译器无法区分输入函数调用和普通函数调用,其使用同样形式的CALL指令,其将控制权交给一个子程序,然后通过子程序的JMP跳转到IAT对应的地址,这也正是IDA中会出现粉色的导入表函数 的原因,同时函数将被加上__imp__的前缀。
导入表的结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
Characteristics和OriginalFirstThunk 一个联合体,如果是数组的最后一项 Characteristics 为 0,否则 OriginalFirstThunk 保存一个 RVA,指向一个 IMAGE_THUNK_DATA 的数组(INT输入名称表),这个数组中的每一项表示一个导入函数。
TimeDateStamp 映象绑定前,这个值是0,绑定后是导入模块的一个32位时间戳。
ForwarderChain 转发链,在被程序引用的DLL里的API又引用了其他DLL的API时使用,一般为0。如果没有转发器,这个值是-1。
Name 一个 RVA,指向导入模块的名字(以00结尾的ASCII字符串),所以一个 IMAGE_IMPORT_DESCRIPTOR 描述一个导入的DLL。
FirstThunk 也是一个 RVA,也指向一个 IMAGE_THUNK_DATA 数组(IAT)。
IMAGE_THUNK_DATA的结构
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
而INT和IAT内容相同的原因如下:
INT不可改写,而IAT是由PE装载器重写的,加载程序迭代搜索数组中的每个指针并用函数真正的入口替代FirstThunk指向的IMAGE_THUNK_DATA内容,这也正是IAT输入地址表名称的来源。当PE文件装载内存后准备执行时,IAT已转换为如下图所示的状态,此时输入表中的其他部分就无用了,直接依靠IAT中的函数地址就可使程序正常运行。
为了简化这一替换过程提升效率,引入了绑定输入。
PE文件载入时的绑定输入过程是Windows加载器优化DLL加载的机制。加载器首先检查导入描述符的时间戳:0表示未绑定需完整解析,-1表示新式绑定需查绑定目录,其他值为旧式绑定时间戳。接着验证绑定有效性,检查DLL时间戳(TimeDateStamp)是否匹配及基址(ASLR下常跳过),失败则回退常规解析。处理绑定导入表时直接使用预计算的函数地址填充IAT并处理转发引用。该机制优势在于避免逐个调用GetProcAddress和减少基址冲突,但现代系统因ASLR常忽略基址验证,绑定主要作用变为减少加载时间。
输出表 与导入表进行类比学习,有的DLL既有导入表又有导出表。
导出表的结构如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
在导出表中,函数既可以通过名字导出也可以通过编号导出。导出方式如下:
按照名称导出
在AddressOfNames指向的字符串数组中,线性搜索FuncName。假设找到索引i。
从AddressOfNameOrdinals[i]获取序号值ord。
从AddressOfFunctions[ord]获取函数地址RVA。
按照编号导出 直接使用序号Ordinal,则函数地址RVA = AddressOfFunctions[Ordinal - Base]
基址重定位 基址重定位是Windows加载PE文件时的重要机制,用于解决DLL加载地址冲突问题。当DLL无法加载到其预设的基址(ImageBase)时,系统会触发重定位过程:
加载器检查DLL是否需要重定位(IMAGE_FILE_RELOCS_STRIPPED标志)
定位.reloc段,其中包含需要修正的地址偏移列表
计算实际加载地址与预设基址的差值(Delta值)
遍历重定位表,对每个标记的地址加上Delta值进行修正
现代系统因ASLR(地址空间布局随机化)强制启用重定位,每次加载地址都不同。重定位主要影响代码中的绝对地址引用,对相对地址(如RVA)无影响。此过程确保程序能正确运行,但会增加加载开销并影响共享DLL的内存效率。