From:https://blog.csdn.net/adam001521/article/details/84658708
PE结构详解:https://www.cnblogs.com/zheh/p/4008268.html
PE格式解析-区段表及导入表结构详解:https://blog.csdn.net/qq_30145355/article/details/78859214
PE文件基本解析:https://www.52pojie.cn/thread-1023342-1-1.html
PE结构体中导出表/导入表解析——初阶:https://bbs.pediy.com/thread-224265.htm
PE 文件格式分析:https://blog.csdn.net/as14569852/article/details/78120335
小甲鱼 PE 结构 详解:https://www.bilibili.com/video/av6861026
木马、免杀、病毒 都需要了解 PE 结构
PE 文件结构超详细图:
图 1:
图 2:
图 3:
下表描述了贯穿于本文中的一些概念:
名称 | 描述 |
地址 | 是 “虚拟地址” 而不是“物理地址”。为什么不是“物理地址”呢?因为数据在内存的位置经常在变,这样可以节省内存开支、避开错误的内存位置等的优势。同时用户并不需要知道具体的“真实地址”,因为系统自己会为程序准备好内存空间的(只要内存足够大) |
镜像文件 | 包含以 EXE 文件为代表的 “可执行文件”、以DLL文件为代表的“动态链接库”。为什么用“镜像”?这是因为他们常常被直接“复制”到内存,有“镜像”的某种意思。 |
RVA | 英文全称 Relatively Virtual Address。偏移(又称“相对虚拟地址”)。相对镜像基址的偏移。 |
节 | 节 是 PE 文件中 代码 或 数据 的基本单元。原则上讲,节只分为 “代码节” 和 “数据节” 。 |
VA | 英文全称 Virtual Address。基址 |
PE( Portable Execute)文件是Windows下可执行文件的总称,常见的有 DLL,EXE,OCX,SYS 等。它是微软在 UNIX 平台的 COFF(通用对象文件格式)基础上制作而成。最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在 Windows 系列操作系统下。PE文件是指 32 位可执行文件,也称为PE32。64位的可执行文件称为 PE+ 或 PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64)。
事实上,一个文件是否是 PE 文件与其扩展名无关,PE文件可以是任何扩展名。那 Windows 是怎么区分可执行文件和非可执行文件的呢?我们调用 LoadLibrary 传递了一个文件名,系统是如何判断这个文件是一个合法的动态库呢?这就涉及到PE文件结构了。
PE文件的结构一般来说如下图所示:从起始位置开始依次是 DOS头,NT头,节表 以及 具体的节。
这四个步骤便是PE文件的执行顺序,具体细节读者可以参考相关文档。(以上四个步骤摘自《黑客破解精通》)
当一个 PE 文件被加载到内存中以后,我们称之为 " 映象 "(image),一般来说,PE文件在硬盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些 “空洞” 。
因为存在这种对齐,所以在 PE 结构内部,表示某个位置的地址采用了两种方式:
堆栈:堆栈里面的数据都是临时存储的数据,作为参数、局部变量、计算的中间值等临时的存储位置。使用数据时,用pop出栈使用数据,并清除堆栈上对应数据。
然而 CPU 的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址,以及编译后的汇编指令中肯定需要用到绝对地址而不是相对映象头的偏移,因此 PE 文件会建议操作系统将其加载到某个内存地址(这个叫基地址。段地址其实就是一种基地址,但基地址并不等于就是段地址。所谓基地址 ( 可以理解为汇编中全局变量 ),顾名思义就可以理解为基本地址,他是相对偏移量的计算基准,即参考位置。基地址和偏移地址的概念:https://blog.51cto.com/godben/1746144,基地址 可以理解为 内存中整个 PE 文件的头地址,文件最开始的位置),编译器便根据这个地址求出代码中一些 全局变量 和 函数的地址,并将这些地址用到对应的指令中。例如在 IDA 里看上去是这个样子:
这种表示方式叫做 虚拟地址(VA)。
https://www.bilibili.com/video/av28047648/?p=7
也许有人要问,既然有 VA 这么简单的表示方式为什么还要有前面的 RVA 呢?因为虽然PE文件虽然为自己指定加载的基地址,但是 windows 有茫茫多的 DLL,而且每个软件也有自己的 DLL,如果指定的地址已经被别的 DLL 占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在 PE 文件头中大部分是使用 RVA 来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后 VA 都失效了,那存在于代码中的那些 VA 怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么 NT 头不能依靠重定位采用 VA 表示地址呢(十万个为什么)?因为不是所有的 PE 都有重定位,早期的 EXE 就是没有重定位的。
我们都知道 PE 文件可以导出函数让其他的 PE 文件使用,也可以从其他 PE 文件导入函数,这些是如何做到的?PE 文件通过 导出表 指明自己导出那些函数,通过 导入表 指明需要从哪些模块导入哪些函数。导入表 和 导出表 的具体结构会在单独的文章中详细解释。
小甲鱼视频:https://www.bilibili.com/video/av28047648/?p=4
就是把 PE 文件 从 硬盘中 放到 内存中,然后 CPU 从 内存中读取指令并执行。
文件中使用偏移(offset),内存中使用 VA(Virtual Address,虚拟地址)来表示位置。
VA 指进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)是指从某基准位置(ImageBase)开始的相对地址。VA 与 RVA 满足下面的换算关系: RVA + ImageBase = VA
***************************************************************
PE 头内部信息大多是 RVA 形式存在。
原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的 PE文件(DLL)。
此时必须通过重定向(Relocation)将其加载到其他空白的位置,若 PE头信息使用的是 VA,则无法正常访问。
因此使用 RVA 来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。
***************************************************************
当 PE 文件被执行时,PE 装载器会为 进程 分配 4GB 的 虚拟地址空间( Virtual address spaces 官方文档:https://docs.microsoft.com/zh-tw/windows-hardware/drivers/gettingstarted/virtual-address-spaces ),然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到 虚拟地址空间 中的 0X400000的位置。
浅谈进程地址空间与虚拟存储空间:https://www.cnblogs.com/fengliu-/p/9243004.html
Linux内存管理 4---虚拟地址空间管理:https://www.cnblogs.com/smartjourneys/p/7196868.html
进程的虚拟地址空间分布:http://www.mamicode.com/info-detail-2487121.html
虚拟地址空间:https://blog.csdn.net/qq_33883085/article/details/88430087
Linux 虚拟地址空间:https://blog.csdn.net/Jocker_D/article/details/83659465
( 虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是 虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户模式空间(user mode space)两部分组成。虚拟地址会通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用,每个进程都有自己的页表。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页是会导致一个页错误(page fault)。其中内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。与此相反,用户模式空间的映射随进程切换的发生而不断变化。)
DOS 头(分 Header 和 DOS 存根)
Header 结构 (00000000 - 0000003F,共 64 个字节)
注意 Win+Intel 的电脑上大部分采用 ”小端法”,字节在内存中储存方式是倒过来的。
重要参数为 e_magic
和 e_lfanew
,已用不同颜色体现。
DOS存根(00000040 - 000000BF,共128字节)
DOS存根则是一段简单的DOS程序,主要用来输出类似“This program cannot be run in DOS mode.”的提示语句。即使没有DOS存根,程序也能正常执行。参考:DOS存根写入数据的想法
NT头(PE最重要的头,由 DOS 头中的 e_lfanew
决定 ,表示 DOS头之后的 NT头相对文件起始地址的偏移)
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; '// 每个CPU拥有唯一的Machine码 -> 4C 01 -> PE -> 兼容32位Intel X86芯片'
WORD NumberOfSections; '// 指文件中存在的节段(又称节区)数量,也就是节表中的项数 -> 00 04 -> 4
// 该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。'
DWORD TimeDateStamp; // PE文件的创建时间,一般有连接器填写 -> 38 D1 29 1E
DWORD PointerToSymbolTable; // COFF文件符号表在文件中的偏移 -> 00 00 00 00
DWORD NumberOfSymbols; // 符号表的数量 -> 00 00 00 00
WORD SizeOfOptionalHeader; '// 指出IMAGE_OPTIONAL_HEADER32结构体的长度。-> 00 E0 -> 224字节
// PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,
// 这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。'
WORD Characteristics; '// 标识文件的属性,二进制中每一位代表不同属性 -> 0F 01
// 属性参见https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.2'
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
图解:
小甲鱼视频地址 IMAGE_OPTIONAL_HEADER 讲解:bilibili.com/video/av28047648/?p=3
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; '// 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107'
BYTE MajorLinkerVersion; // 链接器的主版本号 -> 05
BYTE MinorLinkerVersion; // 链接器的次版本号 -> 0C
DWORD SizeOfCode; // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 40 00 04 00
DWORD SizeOfInitializedData; // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 40 00 0A 00
DWORD SizeOfUninitializedData; // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
DWORD AddressOfEntryPoint; '// 指出程序最先执行的代码起始地址(RVA) -> 00 00 10 00'
DWORD BaseOfCode; // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 10 00
DWORD BaseOfData; // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 20 00
// 在64位文件中此处被并入紧随其后的ImageBase中。
DWORD ImageBase; '// 当加载进内存时,镜像的第1个字节的首选地址。
// WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
// 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
// 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint'
'// PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。'
DWORD SectionAlignment; '// SectionAlignment指定了节段在内存中的最小单位, -> 00 00 10 00'
DWORD FileAlignment; '// FileAlignment指定了节段在磁盘文件中的最小单位,-> 00 00 02 00
// SectionAlignment必须大于或者等于FileAlignment'
WORD MajorOperatingSystemVersion;// 主系统的主版本号 -> 00 04
WORD MinorOperatingSystemVersion;// 主系统的次版本号 -> 00 00
WORD MajorImageVersion; // 镜像的主版本号 -> 00 00
WORD MinorImageVersion; // 镜像的次版本号 -> 00 00
WORD MajorSubsystemVersion; // 子系统的主版本号 -> 00 04
WORD MinorSubsystemVersion; // 子系统的次版本号 -> 00 00
DWORD Win32VersionValue; // 保留,必须为0 -> 00 00 00 00
DWORD SizeOfImage; '// 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
// 一般文件大小与加载到内存中的大小是不同的。 -> 00 00 50 00'
DWORD SizeOfHeaders; '// 所有头的总大小,向上舍入为FileAlignment的倍数。
// 可以以此值作为PE文件第一节的文件偏移量。-> 00 00 04 00'
DWORD CheckSum; // 镜像文件的校验和 -> 00 00 B4 99
WORD Subsystem; '// 运行此镜像所需的子系统 -> 00 02 -> 窗口应用程序
// 用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll),
// 参考:https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.3'
WORD DllCharacteristics; // DLL标识 -> 00 00
DWORD SizeOfStackReserve; // 最大栈大小。CPU的堆栈。默认是1MB。-> 00 10 00 00
DWORD SizeOfStackCommit; // 初始提交的堆栈大小。默认是4KB -> 00 00 10 00
DWORD SizeOfHeapReserve; // 最大堆大小。编译器分配的。默认是1MB ->00 10 00 00
DWORD SizeOfHeapCommit; // 初始提交的局部堆空间大小。默认是4K ->00 00 10 00
DWORD LoaderFlags; // 保留,必须为0 -> 00 00 00 00
DWORD NumberOfRvaAndSizes; '// 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10'
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; '// 数据目录数组。详见下文。'
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory[] 数据目录数组:数组每项都有被定义的值,不同项对应不同数据结构。重点关注的 IMPORT 和 EXPORT,它们是 PE 头中的非常重要的部分,其它部分不怎么重要,大致了解下即可。
'#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory '
'#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory '
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
在 PE 文件结构详解(一)基本概念里,解释了一些 PE 文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构。
了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格式,重要的信息都包含在头部,顺着头部的信息,可以引导系统解析整个文件。所以,我们先来认识一下PE文件的头部格式。还记得上篇里的那个图吗?
DOS头 和 NT头 就是 PE 文件中两个重要的文件头。
DOS头 的作用是兼容 MS-DOS 操作系统中的可执行文件,对于 32位PE文件来说,DOS 所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在 DOS 上没有干用户希望看到的工作而已,好吧,我承认这不是重点。但是,至少我们看一下这个头是如何定义的:
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;
我们只需要关注两个域:
顺着 DOS 头中的 e_lfanew,我们很容易可以找到 NT头,这个才是 32位PE文件中最有用的头,定义如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
下图是一张真实的 PE文件头结构 以及其 各个域的取值 :
块对齐:
类似于 DOS头中的 e_magic,其高16位是0,低16是0x4550,用字符表示是 'PE‘ 。
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:该文件的运行平台,是 x86、x64 还是 I64 等等,可以是下面值里的某一个。
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE
NumberOfSections: 该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp: PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable: COFF文件符号表在文件中的偏移。
NumberOfSymbols: 符号表的数量。
SizeOfOptionalHeader: 紧随其后的可选头的大小。
Characteristics: 可执行文件的属性,可以是下面这些值按位相或。
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
可以看出,PE 文件头定义了 PE 文件的一些基本信息和属性,这些属性会在PE加载器加载时用到,如果加载器发现PE文件头中定义的一些属性不满足当前的运行环境,将会终止加载该PE。
另一个重要的头就是 PE 可选头,别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。
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;
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
#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
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
DllCharacteristics:DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 // DLL can move.
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 // Code Integrity Image
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 // Image is NX compatible
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // Image understands isolation and doesn't want it
#define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // Image does not use SEH. No SE handler may reside in this image
#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // Do not bind this image.
// 0x1000 // Reserved.
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // Driver uses WDM model
// 0x4000 // Reserved.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这两个数有什么用呢 ?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory 是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
看到这么多的定义,大家估计要头疼了,好不容易要把 PE文件头学习完了,又 “从天而降” 一大波的结构。不用紧张,有了前面的知识,后面的部分就迎刃而解了。下一篇开始将沿着这个数据目录分解其余部分,继续关注哦~
小甲鱼 区块表 和 区块:https://www.bilibili.com/video/av28047648/?p=5
小甲鱼 区块描述及意义:https://www.bilibili.com/video/av28047648/?p=6
小甲鱼视频 - 导出表 :https://www.bilibili.com/video/av28047648/?p=10
导出表 是 用来描述 模块(dll)中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。导出表 就是一个 " 表格 "
扩展名为 .exe 不存在导出表,导出表 存在在 dll 文件中,就是 导出 函数给别人用。
上篇文章 PE文件结构详解(二)可执行文件头 的结尾出现了一个大数组,这个数组中的每一项都是一个特定的结构,通过函数获取数组中的项可以用RtlImageDirectoryEntryToData函数,DataDirectory中的每一项都可以用这个函数获取,函数原型如下:
PVOID NTAPI RtlImageDirectoryEntryToData(PVOID Base, BOOLEAN MappedAsImage, USHORT Directory, PULONG Size);
Base:模块基地址。
MappedAsImage:是否映射为映象。
Directory:数据目录项的索引。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
Size:对应数据目录项的大小,比如Directory为0,则表示导出表的大小。
返回值表示数据目录项的起始地址。
导出表 是 用来描述 模块(dll)中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。
函数导出的方式有两种:
这两种导出方式在导出表中的描述方式也不相同。
模块的导出函数可以通过Dependency walker工具来查看:
上图中红框位置显示的就是模块的导出函数,有时候显示的导出函数名字中有一些符号,像 ??0CP2PDownloadUIInterface@@QAE@ABV0@@Z,这种是导出了C++的函数名,编译器将名字进行了修饰。
下面看一下导出表的定义吧:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
导出表结构:
结构还算比较简单,具体每一项的含义如下:
第一次接触这个结构的童鞋被后面的5项搞晕了吧,理解这个结构比结构本身看上去要复杂一些,文字描述不管怎么说都显得晦涩,所谓一图胜千言,无图无真相,直接上图:
在上图中,AddressOfNames 指向一个数组,数组里保存着一组 RVA,每个RVA指向一个字符串,这个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的对应项。获取导出函数地址时,先在AddressOfNames中找到对应的名字,比如Func2,他在AddressOfNames中是第二项,然后从AddressOfNameOrdinals中取出第二项的值,这里是2,表示函数入口保存在AddressOfFunctions这个数组中下标为2的项里,即第三项,取出其中的值,加上模块基地址便是导出函数的地址。如果函数是以序号导出的,那么查找的时候直接用序号减去Base,得到的值就是函数在AddressOfFunctions中的下标。
用代码实现如下:
DWORD* CEAT::SearchEAT( const char* szName)
{
if (IS_VALID_PTR(m_pTable))
{
bool bByOrdinal = HIWORD(szName) == 0;
DWORD* pProcs = (DWORD*)((char*)RVA2VA(m_pTable->AddressOfFunctions));
if (bByOrdinal)
{
DWORD dwOrdinal = (DWORD)szName;
if (dwOrdinal < m_pTable->NumberOfFunctions && dwOrdinal >= m_pTable->Base)
{
return &pProcs[dwOrdinal-m_pTable->Base];
}
}
else
{
WORD* pOrdinals = (WORD*)((char*)RVA2VA(m_pTable->AddressOfNameOrdinals));
DWORD* pNames = (DWORD*)((char*)RVA2VA(m_pTable->AddressOfNames));
for (unsigned int i=0; iNumberOfNames; ++i)
{
char* pNameVA = (char*)RVA2VA(pNames[i]);
if (strcmp(szName, pNameVA) != 0)
{
continue;
}
return &pProcs[pOrdinals[i]];
}
}
}
return NULL;
}
小甲鱼视频 - 导入表:https://www.bilibili.com/video/av28047648/?p=8
https://www.bilibili.com/video/av28047648/?p=9
.exe 文件存在导入表,就是导入函数,然后自己使用。
导入表 在 PE 文件加载时,会根据这个表里的内容加载依赖的 DLL ( 模块 ),并填充所需函数的地址。
C++ 代码:
#include
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR szCmdLine,
int iCmdShow
)
{
MessageBox(
NULL,
TEXT("hello, welcome to fichc.com"),
TEXT("welcome"),
MB_OKCANCEL | MB_OK
);
return 0;
}
PE文件结构详解(二)可执行文件头的最后展示了一个数组,PE文件结构详解(三)PE导出表中解释了其中第一项的格式,本篇文章来揭示这个数组中的第二项:IMAGE_DIRECTORY_ENTRY_IMPORT,即导入表。
也许大家注意到过,在 IMAGE_DATA_DIRECTORY 中,有几项的名字都和导入表有关系,其中包括:IMAGE_DIRECTORY_ENTRY_IMPORT,IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT,IMAGE_DIRECTORY_ENTRY_IAT 和 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 这几个导入都是用来干什么的,他们之间又是什么关系呢?听我慢慢道来。
举个实际的例子,看一下下面这张图:
这个代码调用了一个 RegOpenKeyW 的导入函数,我们看到其 opcode 是 FF 15 00 00 19 30,其实 FF 15 表示这是一个间接调用,即 call dword ptr [30190000] ,这表示要调用的地址存放在 30190000 这个地址中,而 30190000 这个地址在导入地址表的范围内,当模块加载时,PE 加载器会根据导入表中描述的信息修正30190000这个内存中的内容。
那么导入表里到底记录了那些信息,如何根据这些信息修正 IAT ( 导入地址表 ) 呢?我们一起来看一下导入表的定义:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
使用 RtlImageDirectoryEntryToData 并将索引号传 1,会得到一个如上结构的指针,实际上指向一个上述结构的数组,每个导入的 DLL 都会成为数组中的一项,也就是说,一个这样的结构对应一个导入的 DLL。
既然 OriginalFirstThunk 与 FirstThunk 都指向一个 IMAGE_THUNK_DATA 数组,而且这两个域的名字都长得很像,他俩有什么区别呢?为了解答这个问题,先来认识一下 IMAGE_THUNK_DATA 结构:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
ForwarderString 是转发用的,暂时不用考虑,Function 表示函数地址,如果是按序号导入 Ordinal 就有用了,若是按名字导入AddressOfData 便指向名字信息。可以看出这个结构体就是一个大的union,大家都知道union虽包含多个域但是在不同时刻代表不同的意义那到底应该是名字还是序号,该如何区分呢?可以通过Ordinal判断,如果Ordinal的最高位是1,就是按序号导入的,这时候,低16位就是导入序号,如果最高位是0,则AddressOfData是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,用来保存名字信息,由于Ordinal和AddressOfData实际上是同一个内存空间,所以AddressOfData其实只有低31位可以表示RVA,但是一个PE文件不可能超过2G,所以最高位永远为0,这样设计很合理的利用了空间。实际编写代码的时候微软提供两个宏定义处理序号导入:IMAGE_SNAP_BY_ORDINAL 判断是否按序号导入,IMAGE_ORDINAL 用来获取导入序号。
这时我们可以回头看看 OriginalFirstThunk 与 FirstThunk,OriginalFirstThunk 指向的 IMAGE_THUNK_DATA 数组包含导入信息,在这个数组中只有 Ordinal 和 AddressOfData 是有用的,因此可以通过 OriginalFirstThunk 查找到函数的地址。FirstThunk则略有不同,在PE文件加载以前或者说在导入表未处理以前,他所指向的数组与 OriginalFirstThunk 中的数组虽不是同一个,但是内容却是相同的,都包含了导入信息,而在加载之后,FirstThunk 中的 Function 开始生效,他指向实际的函数地址,因为FirstThunk 实际上指向 IAT 中的一个位置,IAT 就充当了 IMAGE_THUNK_DATA 数组,加载完成后,这些 IAT 项就变成了实际的函数地址,即 Function 的意义。还是上个图对比一下:
上图是加载前。
上图是加载后。
最后总结一下:
导入表其实是一个 IMAGE_IMPORT_DESCRIPTOR 的数组,每个导入的 DLL 对应一个 IMAGE_IMPORT_DESCRIPTOR。
IMAGE_IMPORT_DESCRIPTOR 包含两个 IMAGE_THUNK_DATA 数组,数组中的每一项对应一个导入函数。
加载前 OriginalFirstThunk 与 FirstThunk 的数组都指向名字信息,加载后 FirstThunk 数组指向实际的函数地址。
PE文件结构详解(四)PE导入表讲了一般的PE导入表,这次我们来看一下另外一种导入表:延迟导入(Delay Import)。看名字就知道,这种导入机制导入其他DLL的时机比较“迟”,为什么要迟呢?因为有些导入函数可能使用的频率比较低,或者在某些特定的场合才会用到,而有些函数可能要在程序运行一段时间后才会用到,这些函数可以等到他实际使用的时候再去加载对应的DLL,而没必要再程序一装载就初始化好。
这个机制听起来很诱人,因为他可以加快启动速度,我们应该如何利用这项机制呢?VC有一个选项,可以让我们很方便的使用到这项特性,如下图所示:
在这一项后面填写需要延迟导入的DLL名称,连接器就会自动帮我们将这些DLL的导入变为延迟导入。
现在我们知道如何使用延迟导入了,那这个看上去很厉害的机制是如何实现的呢?接下来我们来探索一番。在IMAGE_DATA_DIRECTORY 中,有一项为 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT,这一项便延迟导入表,IMAGE_DATA_DIRECTORY.VirtualAddress 就指向延迟导入表的起始地址。既然是表,肯定又是一个数组,每一项都是一个ImgDelayDesc r结构体,和导入表一样,每一项都代表一个导入的DLL,来看看定义:
[cpp] view plaincopy
typedef struct ImgDelayDescr {
DWORD grAttrs; // attributes
RVA rvaDLLName; // RVA to dll name
RVA rvaHmod; // RVA of module handle
RVA rvaIAT; // RVA of the IAT
RVA rvaINT; // RVA of the INT
RVA rvaBoundIAT; // RVA of the optional bound IAT
RVA rvaUnloadIAT; // RVA of optional copy of original IAT
DWORD dwTimeStamp; // 0 if not bound,
// O.W. date/time stamp of DLL bound to (Old BIND)
} ImgDelayDescr, * PImgDelayDescr;
typedef const ImgDelayDescr * PCImgDelayDescr;
定义知道了,那他是怎么被处理的呢?前面提到了,在延迟导入函数指向的IAT里,默认保存的是一段代码的地址,当程序第一次调用到这个延迟导入函数时,流程会走到那段代码,这段代码用来干什么呢?请看一个真实的延迟导入函数的例子:
[cpp] view plaincopy
.text:75C7A363 __imp_load__InternetConnectA@32: ; InternetConnectA(x,x,x,x,x,x,x,x)
.text:75C7A363 mov eax, offset __imp__InternetConnectA@32
.text:75C7A368 jmp __tailMerge_WININET
这段代码其实只有两行汇编,第一行把导入函数IAT项的地址放到eax中,然后用一个jmp跳转走,那么他跳转到哪里了呢?我们继续跟踪:
[cpp] view plaincopy
__tailMerge_WININET proc near
.text:75C6BEF0 push ecx
.text:75C6BEF1 push edx
.text:75C6BEF2 push eax
.text:75C6BEF3 push offset __DELAY_IMPORT_DESCRIPTOR_WININET
.text:75C6BEF8 call __delayLoadHelper
.text:75C6BEFD pop edx
.text:75C6BEFE pop ecx
.text:75C6BEFF jmp eax
.text:75C6BEFF __tailMerge_WININET endp
其中最重要的是 push 了一个 __DELAY_IMPORT_DESCRIPTOR_WININET,这个就是上文中看到的 ImgDelayDescr 结构,他的 DLL 名字是 wininet.dll。之后,CALL了一个__delayLoadHelper,在这个函数里,执行了加载DLL,查找导出函数,填充导入表等一系列操作,函数结束时IAT中已经是真正的导入函数的地址,这个函数同时返回了导入函数的地址,因此之后的eax里保存的就是函数地址,最后的 jmp eax 就跳转到了真实的导入函数中。
这个过程很完美,也很灵巧,但是如果仔细观察就会发现什么地方有点不对劲,你发现了吗?__delayLoadHelper 的参数中只有IAT项的偏移和整个模块的延迟导入描述__DELAY_IMPORT_DESCRIPTOR_WININET,但是参数中并没有要导入函数的名字。也许你说,名字在 __DELAY_IMPORT_DESCRIPTOR_WININET 的名字表中,是的,那里确实有名字,但是别忘了,那是个表,里面存的是所有要从该模块导入的函数名字,而不是“当前”这个被调用函数的函数名。或许你觉得参数中应该有个索引号,用来表示名字列表中的第几项是即将被导入的那个函数的名字,不幸的是我们也没有看到参数中有这样的信息存在,那Windows执行到这里是如何得到名字的呢?MS在这里使用了一个巧妙的办法:__DELAY_IMPORT_DESCRIPTOR_WININET中有一项是rvaIAT,前面提到了,这里实际上就是指向了IAT,而且是该模块第一个导入函数的IAT的偏移,现在我们有两个偏移,即将导入的函数IAT项的偏移(记作RVA1)和要导入模块第一个函数IAT项的偏移(记作RVA0),(RVA1-RVA0)/4 = 导入函数IAT项在rvaIAT中的下标,rvaINT中的名字顺序与rvaIAT中的顺序是相同的,所以下标也相同,这样就能获取到导入函数的名字了。有了模块名和函数名,用 GetProcAddress 就可以获取到导入函数的地址了。
上述流程用一张图来总结一下:
最后还有两点要提醒大家:
延迟导入的加载只发生在函数第一次被调用的时候,之后IAT就填充为正确函数地址,不会再走 __delayLoadHelper了。
延迟导入一次只会导入一个函数,而不是一次导入整个模块的所有函数。
小甲鱼 - 基址重定位:https://www.bilibili.com/video/av28047648/?p=11
前面两篇 PE文件结构详解(四)PE导入表 和 PE文件结构详解(五)延迟导入表 介绍了PE文件中比较常用的两种导入方式,不知道大家有没有注意到,在调用导入函数时系统生成的代码是像下面这样的:
在这里,IE 的 iexplorer.exe 导入了 Kernel32.dll 的 GetCommandLineA 函数,可以看到这是个间接 call,00401004 这个地址的内存里保存了目的地址,根据图中显示的符号信息可知,00401004 这个地址是存在于 iexplorer.exe 模块中的,实际上也就是一项 IAT 的地址。这个是 IE6 的 exe 中的例子,当然在 dll 中如果导入其他 dll 中的函数,结果也是一样的。这样就有一个问题,代码里 call 的地址是一个模块内的地址,而且是一个 VA,那么如果模块基地址发生了变化,这个地址岂不是就无效了?这个问题如何解决?
答案是:Windows 使用重定位机制保证以上代码无论模块加载到哪个基址都能正确被调用。听起来很神奇,是怎么做到的呢?其实原理并不很复杂,这个过程分三步:
以上三步,前两部涉及到了编译和链接的知识,跟本文的关系不大,我们直接看第三步,这一步符合本系列的特征。
在查看重定位表的定义前,我们先了解一下他的存储方式,有助于后面的理解。按照常规思路,每个重定位项应该是一个DWORD,里面保存需要重定位的RVA,这样只需要简单操作便能找到需要重定位的项。然而,Windows并没有这样设计,原因是这样存放太占用空间了,试想一下,加入一个文件有n个重定位项,那么就需要占用4*n个字节。所以Windows采用了分组的方式,按照重定位项所在的页面分组,每组保存一个页面其实地址的RVA,页内的每项重定位项使用一个WORD保存重定位项在页内的偏移,这样就大大缩小了重定位表的大小。
有了上面的概念,我们现在可以来看一下基址重定位表的定义了:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
好了,有了以上知识,相信大家可以很容易的写出自己修正重定位表的代码,不如自己做个练习验证一下吧。
本文 by evil.eagle 转载的时候请注明出处。http://blog.csdn.net/evileagle/article/details/12886949
最后,还是总结一下,哪些项目需要被重定位呢?
小甲鱼资源详解 和 软件汉化:https://www.bilibili.com/video/av28047648/?p=12
https://www.bilibili.com/video/av28047648/?p=13
PE文件详解(七)--资源表:https://blog.csdn.net/lj94093/article/details/50504110
PE格式第九讲,资源表解析 及实战演练:https://www.cnblogs.com/iBinary/p/7712932.html
【PE结构】资源表:https://blog.csdn.net/chy_chenyang/article/details/80823775
resource_hacker 链接: https://pan.baidu.com/s/1HFUKvBvwHm_5oa0DxGcroA 提取码: ay6p
eXeScope 链接: https://pan.baidu.com/s/17HVVkGN8bKZ9acg3f9aDrw 提取码: exjf
IMAGE_RESOURCE_DIRECTORY 与 IMAGE_RESOURCE_DIRECTORY_ENTRY 结构体定义如下:
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //属性,一般为0
DWORD TimeDateStamp; //资源的产生时刻,一般为0
WORD MajorVersion; //主版本号,一般为0
WORD MinorVersion; //次版本号,一般为0
WORD NumberOfNamedEntries; //以名称(字符串)命名的资源数量
WORD NumberOfIdEntries; //以ID(整型数字)命名的资源数量
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
第一层起始于一个 IMAGE_RESOURCE_DIRECTORY 头,后面紧接着是 IMAGE_RESOURCE_DIRECTORY_ENTRY 数组。数组个数 = NumberOfNamedEntries + NumberOfIdEntries。
IMAGE_RESOURCE_DIRECTORY_ENTRY 使用的是 Name 与 OffsetToDirectory,分别代表了资源类型与第二层的数据偏移地址。Name 与资源类型的匹配如下:
OffsetToDirectory 数据偏移地址是相对整个资源结构来说的,也就是说首个第一层的起始偏移地址加上 OffsetToDirectory 就是第二层的偏移地址。
与第一层一样,第二层起始于一个IMAGE_RESOURCE_DIRECTORY头,后面紧接着是IMAGE_RESOURCE_DIRECTORY_ENTRY 数组。数组个数=NumberOfNamedEntries+NumberOfIdEntries。
IMAGE_RESOURCE_DIRECTORY_ENTRY使用的是NameIsString、NameOffset、Id与OffsetToDirectory。其中OffsetToDirectory与第一层一样,代表了第三层的数据偏移地址,同样是相对整个资源结构来说的。
如果NameIsString=1,说明该资源以名称(UNICODE编码的字符串)定义的,NameOffset是名称的相对整个资源结构的偏移地址。相反,如果NameIsString=0,说明该资源以ID(整型数字)定义的,ID号为Id。
NameOffset相对地址指向的是IMAGE_RESOURCE_DIR_STRING_U结构体,该结构体定义如下:
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; //字符串的长度
WCHAR NameString[ 1 ]; //UNICODE字符串,由于字符串是不定长的。由Length 制定长度
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
与前两层一样,第二层起始于一个IMAGE_RESOURCE_DIRECTORY头,后面紧接着是IMAGE_RESOURCE_DIRECTORY_ENTRY数组,但不同的是数组个数=1。
IMAGE_RESOURCE_DIRECTORY_ENTRY使用的是Name与OffsetToData,分别代表了资源语言类型与资源数据相对地址。Name是指语言内码,比如936代表简体中文。
OffsetToData是相对整个资源结构的偏移地址,指向一个IMAGE_RESOURCE_DATA_ENTRY结构体,该结构体定义如下:
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //资源数据的RVA
DWORD Size; //资源数据的长度
DWORD CodePage; //代码页, 一般为0
DWORD Reserved; //保留字段
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;