PE文件结构
PE文件结构
MS-DOS头部
1 |
|
PE文件头
1 |
|
64位PE文件头
1 |
|
IMAGE_FILE_HEADER
1 |
|
IMAGE_OPTIONAL_HEADER
1 |
|
64位下IMAGE_OPTIONAL_HEADER
1 |
|
IMAGE_DATA_DIRECTORY
1 |
|
数据目录的成员
序号 | 成员 | 结构 | 偏移量(PE/PE32+) |
---|---|---|---|
0 | Export Table | IMAGE_DIRECTORY_ENTRY_EXPORT | 78H/88H |
1 | Import Table | IMAGE_DIRECTORY_ENTRY_IMPORT | 80H/90H |
2 | Resources Table | IMAGE_DIRECTORY_ENTRY_RESOURCE | 88H/98H |
3 | Exception Table | IMAGE_DIRECTORY_ENTRY_EXCEPTION | 90H/A0H |
4 | Security Table | IMAGE_DIRECTORY_ENTRY_SECURITY | 98H/A8H |
5 | Base relocation Table | IMAGE_DIRECTORY_ENTRY_BASERELOC | A0H/B0H |
6 | Debug | IMAGE_DIRECTORY_ENTRY_DEBUG | A8H/B8H |
7 | Copyright | IMAGE_DIRECTORY_ENTRY_COPYRIGHT | B0H/C0H |
8 | Global Ptr | IMAGE_DIRECTORY_ENTRY_GLOBALPTR | D8H/C8H |
9 | Thread local storage(TLS) | IMAGE_DIRECTORY_ENTRY_TLS | C0H/D0H |
10 | Load configuration | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | C8H/D8H |
11 | Bound Import | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | D0H/E0H |
12 | Import Address Table(IAT) | IMAGE_DIRECTORY_ENTRY_IAT | D8H/E8H |
13 | Delay Import | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | E0H/F0H |
14 | COM descriptor | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | E8H/F8H |
15 | 保留,必须为0 | F0H/100H |
IMAGE_SECTION_HEADER
1 |
|
字段值 | 地址 | 用途 |
---|---|---|
IMAGE_SCN_SNT_CODE | 00000020H | 包含代码,常与100000000h一起设置 |
IMAGE_SCN_CNT_INITIALIZED_DATA | 00000040H | 该块包含已初始化的数据 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA | 00000080H | 该块包含未初始化的数据 |
IMAGE_SCN_MEM_DISCARDABLE | 02000000H | 该块可被丢弃,因为它一旦被载入,进程就不再需要它,常见的可丢弃块是.reloc(重定位块) |
IMAGE_SCN_MEM_SHARED | 10000000H | 该块为共享块 |
IMAGE_SCN_MEM_READ | 40000000H | 该块可读。可执行文件中的块总是被设置该标志 |
IMAGE_SCN_MEM_EXECUTE | 20000000H | 该块可以执行。通常当00000020H标志被设置时,该标志也被设置 |
IMAGE_SCN_MEM_WRITE | 80000000H | 该块可写。如果PE文件中没有设置该标志,装载程序就会将内存映像页标记为 |
常见区块
名称 | 描述 |
---|---|
.text | 代码区块,内容全是指令代码。链接器把所有目标文件的.text块链接成一个大的.text块。 |
.data | 默认的读/写数据区块。全局变量、静态变量一般放在这里 |
.rdata | 默认的只读区块,程序很少用到该块。有两种情况需要用到.rdata块。一是在Microsoft链接器产生的EXE文件中,用于存放调试目录;二是用于存放说明字符串。如果程序的DEF文件中指定了DESCRIPTION,字符串就会出现在.rdata块中。 |
.idata | 包含其他外来的DLL的函数及数据信息,即输入表。将.idata区块合并到另一个区块已成为惯例,典型的是.rdata区块。 |
.edata | 输出表。当创建一个输出API或数据的可执行文件时,链接器会创建一个.EXP文件,这个.EXP文件包含一个.edata区块,它会被加入最后的可执行文件中。idata区块也经常被合并 |
.rsrc | 资源。包含模块的全部资源,例如图标、菜单、位图等。这个区块是只读的,不能命名位.rsrc以外的名字,也不能被合并。 |
.bss | 未初始化数据。 |
.crt | 用于支持c++运行时(CRT)所添加的数据 |
.tls | TLS的意思是线程局部存储器,用于支持通过__declspec(thread)声明的线程局部存储变量的数据,既包括数据的初始化值,也包括运行时所需的额外变量 |
.reloc | 可执行文件的基址重定位。基址重定位一般只是DLL需要。在Release模式下,链接器不回给EXE文件加上基质重定位。 |
.sdata | IA-64的常规大小的全局变量放在这个区块。 |
.pdata | 异常表,包含一个CPU特定的IMAGE_RUNTIME_FUNCTION_ENTRY结构数组,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它。它用于异常处理,是基于表的体系结构,就像IA-64。唯一不使用基于表的以尝处理架构体系是x86 |
.debug$S | OBJ文件中Codeview格式的符号。这是一个变量长度的Codeview格式的符号记录流 |
.debug$T | OBJ文件中Codeview格式的类型记录。这是一个变量长度的Codeview格式的类型记录流 |
.debug$P | 当使用预编译的头时,可以在OBJ文件中找到它 |
.drectve | 包含链接器命令,只能在OBJ中找到它。命令是能被传递给练级额其命令行的字符串。例如:-defaultlib:LIBC,命令行用空格字符分开 |
.didat | 延迟载入的输入数据,只能在非Release模式的可执行文件中找到。在Release模式下,延迟载入的数据会被合并到另一个区块中 |
区块的对齐值
有两个对齐值,一种用于磁盘内,另一种用于内存中。PE文件头指出了这两个值。在PE文件头里,FileAlignment
定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始,但是区块的实际代码或数据的大小不一定刚好是这么多,不足用00H填充。被填充的空间被称为区块间隙。
在PE文件里,SectionAlignment定义了内存中区块的对齐值。当PE文件被映射到内u那种时,区块总是至少从一个页边界处开始。每个区块的第一个字节对应于某个内存页。在x86系统CPU中,内存页是按4KB(1000h)排列的;在x64中内存是按8KB(2000h)排列的。
文件偏移与虚拟地址地址的转换
PE文件为了减少题基,磁盘对齐值不是一个内存页1000h,而是200h。因此这类文件被映射到内存中后,同一数据相对于文件头的偏移量在内存中与在磁盘中不同。
输入表
可执行文件使用来自其他DLL的代码或数据的动作称为输入。当PE文件被载入后,Windows加载器的工作就是定位所有被输入的函数和数据,并让正在载入的文件可以使用那些地址。这个过程是通过PE文件的输入表(Import Table,也称导入表)完成的。输入表中保存的是函数名和其驻留的DLL名等动态链接所需要的信息。
输入函数的调用
输入函数就是被程序调用但其执行代码不在程序中的函数,这些函数的代码位于相关的DLL中,在调用者程序中只保留相关的函数信息,例如函数名、DLL文件名等。对磁盘上的PE文件来说,它无法得知这些输入函数在内存中的地址。只有当PE文件载入内存后,Windows加载器才将相关DLL载入,并将调用输入函数的指令和函数实际所处的地址联系起来。
- 隐式调用:当应用程序调用一个DLL的代码和数据时,程序就被隐式地连接到DLL,这个过程是由Windows加载器完成地。
- 显示调用:首先需要确定目标DLL已被加载,然后寻找API地地址,是通过LoadLibrary和GetProcAddress完成的。
PE文件内有一组数据结构,分别对应于被输入的DLL。每一个这样的结构都给出了被输入的DLL的名称并指向一组函数指针。这组函数指针被称为输入地址表(Import Address Table,IAT)。每一个被引入的API在IAT里都保留位置,在IAT里API可以被Windows加载器写入输入函数的地址
输入表结构
1 |
|
- OriginalFirstThunk(Characteritics):包含指向输入名称表(INT)的RVA。INT是一个IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构都指向IMAGE_IMPORT_BY_NAME结构,数组以一个内容为0的IMAGE_THUNK_DATA结构结束。
- TimeDateStamp:一个32位的时间标志
- ForwarderChain:这是第一个被转向的API的索引,一般为0,在程序引入一个DLL中的API,而这个API又在引用其他DLL的API时使用。
- Name:DLL名字的指针。是以\x00结尾的ASCII字符的RVA地址,该字符串包含输入的DLL名。
- FirstThunk:包含指向输入地址表(IAT)的RVA。IAT是一个IMAGE_THUNK_DATA结构的数组
1 |
|
当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时低31位被看成一个函数序号。当双字最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构
1 |
|
- Hint:本函数在其所驻留DLL的输入表中的序号。该域被PE装载器用来在DLL的输入表里快速查询函数。该值不是必需的,一些链接器将它设为0.
- Name:含有输入函数的函数名。函数名是一个ASCII字符串,以”NULL”结尾。
输入表地址
为什么会有两个并行的指针数组指向IMAGE_IMPORT_BY_NAME结构
- 第一个数组OriginalFirstThunk所指向的数组,为单独的一项,不可改写,称为INT。也称提示名表
- 第二个数组有FirstThunk所指向的数组,由PE装载器重写的。
- PE装载器先搜索OriginalFirst结构所指向的输入函数的地址。然后,加载器用函数真正的入口地址来代替由FirstThunk指向的IMAGE_THUNK_DATA数组里元素的值。
Jmp dword ptr [xxxxx]
语句中的[xxxxx]
是指FirstThunk数组的一个入口,因此被称为输入地址表(IAT Import Address Table)。此时输入表的其他部分就不太重要了,程序依靠IAT提供的函数地址就可以正常运行。程序依靠IAT提供的函数地址就可以正常运行。
- 另一种情况是程序OrignalFirstThunk的值为0。初始化时,系统根据FirstThunk的值找到指向函数名的地址串,根据地址串找到函数名,再根据函数名得到入口地址,然后用入口地址取代FirstThunk指向的地址串中的原值。
输入表实例分析
OrignalFirstThunk | TimeDateStamp | ForwardChain | Name | First Thunk |
---|---|---|---|---|
8C20 0000 | 0000 0000 | 0000 0000 | 7421 0000 | 1020 0000 |
7C20 0000 | 0000 0000 | 0000 0000 | B421 0000 | 0020 0000 |
0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 |
每个IID中的第4个字段是指向DLL名称的指针。OrignalFirstThunk中指向的是IMAGE_THUNK_DATA数组,它存储的是指向IMAGE_IMPORT_BY_NAME结构
IMAGE_THUNK_DATA数据
1021 0000 | 1c21 0000 | f420 0000 | e020 0000 |
---|---|---|---|
5021 0000 | 6421 0000 | 0221 0000 | ce20 0000 |
bc20 0000 | 2e21 0000 | 4221 0000 | 0000 000 |
同一IID结构中FirstThunk的字段值为2010h,即文件偏移为610h
其数据与PrignalFirstThunk字段所指的完全一样。
name字段所指向的不是刚好ASCII字符串,前面还有2个字节的空缺,这是作为函数名(Hint)引用的,可以为0.
API函数
提示名表(RVA) | 提示名表(File Offset) | Hint | ApiName |
---|---|---|---|
00002110h | 710h | 019bh | LoadIconA |
0000211ch | 71ch | 01DDH | PostQuitMessage |
000020f4h | 6f4h | 0128h | GetMessageA |
000020e0h | 6e0h | 0094h | DispatchMessageA |
00002150h | 750h | 072dh | TranslateMessage |
00002164h | 764h | 028bh | UpdateWindow |
00002102h | 702h | 0197h | LoadCursorA |
000020ceh | 6ceh | 0083h | DefWindowProcA |
000020bch | 72eh | 01efh | RegisterClassExA |
00002142h | 742h | 0265h | ShowWindow |
绑定输入
当PE装载器载入PE文件时,会检查输入表并将相关的DLL映射到进程地址空间,然后遍历IAT里的IMAGE_THUNK_DATA数组并用输入函数的真实地址替换它。如果程序员能正确预测函数地址,PE装载器就不用再每次载入PE文件时都去修改IMAGE_THUNK_DATA的值。
当一个可执行文件被绑定时,IAT中的IMAGE_THUNK_DATA结构被输入函数的实际地址改写了。在磁盘中可执行文件的IAT里,有的存放的是与DLL输出函数相关的实际内存地址。
在整个进程执行期间,Bind程序做了如下两个重要假设
- 当进程初始化时,需要的DLL实际上加载到了它们的首选基地址中。
- 自从绑定操作执行依赖,DLL输出表中引用的符号位置一直没有改变。
IMAGE_BOUND_IMPORT_DESCRIPTOR
1 |
|
- TimeDateStamp:一个汉字,包含一个被输入DLL的时间/日期戳。它允许加载器快速判断是否是新的。
- OffsetModuleName:一个字,包含一个被输入DLL的名称的偏移。这个字段是与第一个IBID结构之间的偏移(不是RVA)
- NumberOfModuleForwarderRes:一个字,包含紧跟该结构的IMAGE_BOUND_FORWARDER_REF结构的数目。
输出表
当一个DLL函数能被EXE或另一个DLL文件使用时,它就被输出。输出信息保存在输出表中,DLL文件通过输出表向系统提供输出函数名、序列号和入口地址等。
输出表结构
输出表的主要内容是一个表格,其中包括函数名称、输出序数。序数是指定DLL中某个函数的16位数字,在做指向的DLL独一无二。
输出表是数据目录表的第1个成员,指向IMAGE_EXPORT_DIRECTORY(IED)结构。
1 |
|
- Characteristics:输出属性的旗标。目前还没有定义,总是为0
- TimeDateStamp:输出表创建的时间(GMT时间)
- MajorVersion:输出表的主版本号。未使用,设置为0
- MinorVersion:输出表的次版本号。未使用,设置为0
- Name:指向一个ASCII字符串的RVA。这个字符串是与这些输出函数相关联的DLL的名字
- Base:这个字段包含用于这个PE文件输出表的起始数值(基数)。正常情况下这个数值为1.当通过序数查询一个输出函数时,这个值从序数里被减去,其结果将作为进入输出地址表(EAT)的索引
- NumberOfFunctions:EAT中的条目数量。当条目为0时,这个序数值表名没有代码或数据被输出
- NumberOfNames:输出函数名称表(ENT)里的条目数量。NumberOfNames的值总是小于或等于NumberOfFunctions的值,小于的情况发生在符号只通过序数输出的时候。另外,当被赋值的序数里有数字间距时也会是小于的情况,这个值也是输出序数表的长度。
- AddressOfFunctions:EAT的RVA。EAT是一个RVA数组,数组中的每一个非零的RVA都对应于一个被输出的符号。
- AddressOfNames:ENT的RVA。ENT是一个指向ASCII字符串的RVA数组。每一个ASCII字符串对应于一个通过名字输出的符号。因为这个表是要排序的,所以ASCII字符串也是按顺序排序的。
- AddressOfNameOrdinals:输出序数表的RVA。这个表是字的数组。这个表将ENT中的数组索引映射到相应的输出地址表条目。
基址重定位
在PE文件中,重定位表往往单独作为一块,用.reloc
表示
基址重定位的概念
PE格式不参考外部DLL或模块中的其他区块,而是把文件中所有可能需要修改的地址放在一个数组里。如果PE文件不在首选的地址载入,那么文件中的每一个定位都需要被修改。
基址重定位表的结构
基址重定位表位于一个.reloc区块内,找到它们得正确方式是通过数据目录表得IMAGE_DIRECTORY_ENTRY_BASERELOC条目查找。基址重定位数据采用类似按页分割的方法组织,是由许多重定位块串接成的,每个块中存放4KB(1000h)的重定位信息,每个重定位数据块的大小必须以DWORD(4字节)对齐。以IMAGE_BASE_RELOCATION结构开始
1 |
|
- VirtualAddress:这组重定位数据的开始RVA地址。各重定位项的地址加这个值才是该重定位项的完整RVA地址
- SizeOfBlock:当前重定位结构的大小。因为VirtualAddress和SizeOfBlock的大小都是固定的4字节,所以这个值减8就是TypeOffset数组的大小。
- TypeOffset:一个数组。数组每项大小为2字节,共16位。这16位分为高4位和低12位。高4位代表重定位类型;低12位是重定位地址,它与VirtualAddress相加就是指向PE映像中需要修改的地址数据的指针。
类型 | winnt.h里的预定义值 | 含义 |
---|---|---|
0 | IMAGE_REL_BASED_ABSOLUTE | 没有具体的含义,只是为了让每个段4字节对齐 |
3h | IMAGE_REL_BASED_HIGHLOW | 重定位指向的整个地址都需要修改 |
10h | IMAGE_REL_BASED_DIR4 | 出现在64位PE文件中,对指向的整个地址进行修正 |
重定位数据转换
项目 | 重定位数据1 | 重定位数据2 | 重定位数据3 | 重定位数据4 |
---|---|---|---|---|
原始数据 | 0F30h | 2330h | 0000 | 0000 |
TypeOffset值 | 300Fh | 3023h | - | - |
TypeOffset高4位(类型) | 3h | 3h | - | - |
TypeOffset低12位(地址) | 00fh | 023h | - | - |
低12位加VirtualAddress | 100fh(RVA) | 1023h(RVA) | - | - |
转换成文件偏移地址 | 60fh | 623h | - | - |
60fh和623h分别指向00402000h和00403030h处
资源
Windows程序的各种界面称为资源,包括加速键、位图、光标、对话框、图标、菜单、串表、工具栏和版本信息等。
资源结构
资源使用类型于磁盘目录结构的方式保存,目录通常包含三层。
- 第一层类似于文件系统的根目录,每个根目录下的条目总是在它自己权限下的一个目录。
- 第二层目录中的每一个都对应于一个资源类型。
- 每个第2层资源类型目录下是第3层目录
资源目录结构
- IMAGE_DIRECTORY_ENTRY_RESOURCE条目包含资源的RVA和大小
- IMAGE_RESOURCE_DIRECTORY和数个IMAGE_RESOURCE_DIRECTORY_ENTRY组成资源目录结构
IMAGE_RESOURCE_DIRECTORY结构长度为16字节
1 |
|
资源目录入口结构
1 |
|