前言
前两天遇到了和PE文件格式有些关系的问题,有点儿蒙,发现以前看的有点些忘了,于是就拿出书又看了一遍,简单的写了点笔记。
PE文件简述
PE文件是指Windows系统下32位的可执行文件,又叫PE32。Windows系统下64位的可执行文件称为PE+或PE32+。
PE文件是由UNIX平台的COFF为基础制作的。
PE文件的种类(后缀)有:
-
可执行文件
- .exe
- .scr
-
驱动文件
- .sys
- .vxd
-
库文件
- .dll
- .ocx
- .cpl
- .drv
-
对象文件
- .obj(PE文件中只有它文件本身不能以任何形式执行)
PE文件的基本结构
| DOS头 |
|---|
| DOS存根 |
| NT头 |
| 节区头 |
| NULL |
| 节区 |
| NULL |
| 节区 |
| NULL |
| ……(一直是 节区、NULL ……) |
NULL为NULL填充。
PE头
从DOS头到节区头的合称。
PE头由许多结构体组成,储存了可执行文件运行所需要的所有信息。
以下各结构体中只列出要讲解的成员。
DOS头
用于对DOS文件保持兼容性。
typedef struct _IMAGE_DOS_HEADER {
WOED e_magic; //DOS签名
……
LONG e_lfanew;//NT文件头的偏移
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
IMAGE_DOS_HEADER:
此结构体的大小为64字节。
e_magic:
所有PE文件在开始部分都有DOS签名,且DOS签名固定为 4D5A (即ASCII值 MZ ),取自微软设计了DOS可执行文件的开发人员名字的首字母(有兴趣的可以看看这个故事)。
e_lfanew:
此成员的值指向NT头所在位置,NT头的名称为 IMAGE_NT_HEADERS 。
DOS存根
为可选项,且大小不固定。由代码和数据混合而成,用于在其他环境(非32位环境)下输出字符串提示或执行其他操作。
NT头
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //签名
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_0PTIONAL_HEADER32 OptionalHeader; //可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
_IMAGE_NT_HEADERS:
大小为 F8 。
Signature:
值为 50450000h (即ASCII值 PE 00)。
FileHeader
文件头,结构体,用于表现文件大致属性。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //CPU的Machine码
WORD NumberOfSections; //文件中存在的节区数量
DWORD TimeDateStamp; //记录编译器创建此文件的时间
……
WORD SizeOfOptionalHeader; //IMAGE_0PTIONAL_HEADER32结构体的大小
WORD Characteristics; //用于表示文件的属性,文件是否可运行,是否为DLL文件等信息
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine:
每个CPU都有唯一的Machine码,例:IA64的Machine码为 0x0200 。
NumberOfSections:
用来指出文件中存在的节区数量,该值一定要大于零,且当定义的节区数量与实际节区不同时,将发生运行错误。
TimeDateStamp:
用于记录编译器创建此文件的时间。
SizeOfOptionalHeader:
用于指出IMAGE_0PTIONAL_HEADER32结构体的大小。
另外,PE+格式的文件中使用的是IMAGE_0PTIONAL_HEADER64结构体。两个结构体尺寸不同,需要在 SizeOfOptionalHeader 成员中明确指出结构体的大小。
Characteristics:
用于表示文件的属性,文件是否可运行,是否为DLL文件等信息,以bin OR形式组合。
OptionalHeader
可选头,由结构体、预定义等组成,其中IMAGE_OPTIONAL_HEADER32是PE头结构体中最大的。
……
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
……
DWORD AddressOfEntryPoint; //EP的RVA值
……
DWORD ImageBace; //基准地址
DWORD SectionAlignment; //指定节区在内存中的最小单位
DWORD FileAlignment; //指定节区在磁盘文件中的最小单位
……
DWORD SizeOfImage; //PE Image在虚拟内存中所占空间的大小
DWORD SizeOfHeaders;//PE头的大小
……
WORD Subsystem; //用来区分系统驱动文件与普通的可执行文件
……
DWORD NumberOfRvaAndSizes; //用来指定DataDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic:
IMAGE_OPTIONAL_HEADER32结构体的Magic码为10B;
IMAGE_OPTIONAL_HEADER64结构体的Magic码为20B。
AddressOfEntryPoint:
EP的RVA值,该值指出程序最先执行的代码起始地址(入口点)。
ImageBace:
PE文件被加载到内存时,指出文件的优先装入地址。
SectionAlignment:
指定了节区在内存中的最小单位。
FileAlignment:
指定了节区在磁盘文件中的最小单位。一个文件的SectionAlignment和FileAlignment值可能相同,也可能不同。磁盘文件或内存的节区大小必定为值得整数倍。
SizeOfImage:
加载PE文件到内存时,指定了PE Image在虚拟内存中所占空间的大小。
SizeOfHeaders:
用来指出整个PE头的大小,该值必须是 FileAlignment 的整数倍。
Subsystem:
用来区分系统驱动文件(.sys)与普通的可执行文件(.exe、.dll)。
| 值 | 含义 | 备注 |
|---|---|---|
| 1 | Driver | 系统驱动(.sys) |
| 2 | GUI文件 | 窗口应用程序(.exe) |
| 3 | CUI文件 | 控制台应用程序(.exe) |
NumberOfRvaAndSizes:
用来指定 DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] 数组的个数。
DataDirectory:
由IMAGE_DATA_DIRECTORY结构体组成的数组。
DataDirectory[0] = EXPORT Directory //EAT结构体数组的起始地址(RVA值)
DataDirectory[1] = IMPORT Directory //IAT结构体数组的起始地址(RVA值)
DataDirectory[2] = RESOURCE Directory
……
DataDirectory[9] = TLS Directory
……
节区头
各节区头定义了各节区在文件或内存中的大小、位置、属性等。将PE文件创建成多个节区结构的好处是,可以保证程序的安全性(比如缓冲区溢出攻击)。
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。
……
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节区的名字
union {
……
DWORD VirtualSize; //内存中节区所占大小
} Misc;
DWORD VirtualAddress; //内存中节区起始地址
DWORD SizeOfRawData; //磁盘文件中节区所占大小
DWORD PointerToRawData; //磁盘文件中节区起始位置
……
DWORD Characteristics; //节区属性(bin OR)
}IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name:
PE未明确规定节区的名字,所以可以向其中放入任何值,甚至可以填充空值,所以节区的名字(例:.text)仅供参考,不能百分之百确定被用作某种信息。
VirtualSize:
内存中节区所占大小。
SizeOfRawData:
磁盘文件中节区所占大小,与 VirtualSize 一般具有不同的值,即磁盘文件中节区的大小与加载到内存中的节区大小是不同的。
VirtualAddress:
内存中节区起始地址,即 RVA。不带有任何值,由 IMAGE_OPTIONAL_HEADER32 中的 SectionAlignment 确定。
PointerToRawData:
磁盘文件中节区起始位置。不带有任何值,由 IMAGE_OPTIONAL_HEADER32 中的 FileAlignment 确定。
PE体
各节区的合称。
PE文件格式把可执行文件分成若干个节区,不同的资源(例:字符串、菜单、图标、字体、位图……)被存放在不同的节中, 标准PE文件通常包含的节名及含义如下:
-
.text(代码)
由编译器产生,存放着二进制的机器代码,也是反汇编和调试的对象。
访问权限:执行、读取。
-
.data(数据)
初始化的数据块,如宏定义、全局变量、静态变量等。
访问权限:非执行、读写。
-
.idata
可执行文件所使用的动态链接库等外来函数与文件的信息。
-
.rsrc(资源)
存放程序的资源,如图标、菜单等。
访问权限:非执行、读取。
-
……
节区的名字只是为了方便人的记忆与使用,节名可以自己定义,另外,如果可执行文件经过加壳处理,节信息就会变得非常奇怪。
IAT
一种表格(其实是长整型数组),用来记录程序正在使用哪些库中的哪些函数,未规定大小。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
……
DWORD OriginalFirstThunk; //INT的地址(RVA值)
};
……
DWORD Name; //库名称字符串的地址(RVA值)
DWORD FirstThunk; //IAT的地址(RVA值)
} IMAGE_IMPORT_DESCRIPTOR;
……
Name:
字符串指针,指向导入函数所属的库文件名称。
IMAGE_IMPORT_DESCRIPTOR 结构体记录PE文件要导入哪些库文件。
导入多少库就存在多少个 IMAGE_IMPORT_DESCRIPTOR 结构体,这些结构体形成数组,且结构体数组最后以NULL结构体结束。
IAT在PE体中,但查找其位置的信息在PE头中。
INT
记录导入函数信息的长整型结构体数组,元素为IMAGE_IMPORT_BY_NAME结构体,以NULL结构体结束,大小与IAT相同。
标准的PE文件中,INT与IAT的各元素同时指向相同地址。
EAT
不同的应用程序通过EAT来得到从相应库中导出函数的起始地址。 PE文件内用结构体来保存导出信息。
typedef struct _IMAGE_EXPORT_DIRECTORY {
……
DWORD NumberOfFunctions; //实际Export函数的个数
DWORD NumberOfNames; //Export函数中具名的函数个数
DWORD AddressOfFunctions; //Export函数地址数组(数组元素个数 = NumberOfFunctions)
DWORD AddressOfNames; //函数名称地址数组(数组元素个数 = NumberOfNames)
DWORD AddressOfNameOrdinals;//Ordinal地址数组(数组元素个数 = NumberOfNames)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
文件到内存的映射
文件中使用偏移,内存中使用VA来分别表示位置,文件加载到内存时,节区的大小,位置等会发生变化。
VA与RVA的换算:
RVA + Image Base = VA
即
相对虚拟内存地址 + 基准地址 = 虚拟内存地址
PE头内部信息大多以RVA形式存在,PE文件加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件,因此必须通过重定位加载到其他空白位置,使用RVA来定位信息,即使发生了重定位,只要相对于基准地址的相对地址没有发生变化,就能正常访问到指定信息。
32位的Windows OS中,各进程分配有4GB的虚拟内存,进程中VA的值范围是00000000~FFFFFFFF(关于Windows OS中的内存管理和资源分配机制,可以通过学习操作系统的相关知识,比如银行家算法来深入了解) 。EXE、DLL文件被装载到用户内存的 0~7FFFFFFF 中,SYS文件被载入到内核内存的 80000000~FFFFFFFF 中。
基准地址可以通过修改编译选项更改,在默认情况下,EXE文件在内存中的基准地址是0x00400000,DLL文件在内存中的基准地址是0x10000000。
PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBace + AddressOfEntryPoint。
一般而言,文件的大小与加载到内存中的大小是不同的。其原因是文件数据存放的基本单位和内存数据存放的基本单位不同,按照磁盘或内存数据标准存放时,不足一个基本单位的数据会被0x00填充,超出的将分配给下一个,这种由存储单位差异引起的节基址差叫做节偏移。
RVA与RAW的换算:
《逆向工程核心原理》的13.4节 RVA & RAW 中的公式:
RAW = RVA - VirtualAddress + PointerToRawData
把公式变形得
RAW = (VA - Image Base) - VirtualAddress + PointerToRawData
???似乎有哪里不对??? : — )
这里的 VirtualAddress 为 RVA所在节区的 VA - Image Base 得到的相对虚拟偏移量RVA,PointerToRawData为文件偏移量,(- VirtualAddress + PointerToRawData)即为节偏移。
即
文件偏移地址 = 相对虚拟地址 - 内存中节区的起始地址 + 磁盘文件中节区的起始位置
= RVA - 节偏移
涉及到的术语解释
PE
Portable Executable,Windows系统下使用的可执行文件格式。
COFF
Common Object File Format,通用对象文件格式,UNIX平台的文件格式。
PE header
PE文件的头部分。
Section header
节区头。
DOS header
DOS头。
VA
Virtual Address,虚拟地址,进程虚拟内存的绝对地址,也是PE文件中的指令被装入内存后的位置 。
RVA
Relative Virtual Address,相对虚拟地址,从某个基准位置开始的相对地址,内存地址相对于映射基址的偏移量 。
Image Base
基准地址,也叫装载地址,PE装入内存时的基地址 。
NULL padding
NULL填充。
Relocation
重定位。
RAW
File offset,文件偏移地址,文件在磁盘上存放时相对于文件开头的偏移 。
Image
映像,PE文件装入到内存中的形态。
IAT
Import Address Table,导入地址表。
EAT
Export Address Table,导出地址表。
DLL
Dynamic Link Library,动态链接库。
Import
导入,向库提供服务(函数)。
Export
导出,从库向其他PE文件提供服务(函数)。
Table
PE头中提到的Table指数组。
INT
Import Name Table,导入名称表。
IMPORT Directory Table
IMAGE_IMPORT_DESCRIPTOR 结构体数组的别称。
学习与参考
《逆向工程核心原理》
《0day安全:软件漏洞分析技术(第2版)》
4862

被折叠的 条评论
为什么被折叠?



