PE文件格式
@date: 2016/11/24
@author: dlive
PE (portable executable) ,它是微软在Unix平台的COFF(Common Object File Format 通用对象文集格式)的基础上制成的。
PE文件是指32位的可执行文件,也称PE32,64位的可执行文件称为PE+或者PE32+
PE文件的种类
种类 | 主扩展名 |
---|---|
可执行系列 | exe ,scr |
驱动程序系列 | sys, vxd |
库系列 | dll, dcx, cpl, drv |
对象文件系列 | obj |
VA 虚拟地址 RVA 相对虚拟地址
32位的Windows OS中,各进程分配有4GB的虚拟内存,因此进程中VA值的范围是00000000-FFFFFFFF
此处还有一个重要的图:p92 图13-2
以下格式推荐使用notepad.exe为例,结合010editor中的EXE Template模板解析结果学习。
0x01 PE头
1.DOS头
微软考虑PE对DOS文件的兼容性,在PE头最前面添加了IMAGE_DOS_HEADER结构体,用来扩展已有的DOS头。
# IMAGE_DOC_HEADER
WORD e_magic; // DOS签名 MZ
...
...
LONG e_lfanew; // NT Header offset
2.DOS存根(Stub)
没有DOS存根,文件也能正常运行,DOS存根由代码和数据混合而成,大小不固定,其中的代码和数据是为了兼容MS-DOS,可以在MS-DOS下运行,输出“This program cannot be run in DOS mode”
3.NT头
# IMAGE_NT_HEADERS
DWORD Signature; //PE签名 50450000 =》 'PE'NULLNULL
IMAGE_FILE_HEADER FileHeader; // 文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //可选头,PE为IMAGE_OPTIONAL_HEADER32,PE32+ //为IMAGE_OPTIONAL_HEADER64
3.1 IMAGE_FILE_HEADER
# IMAGE_FILE_HEADER
WORD Machine; //CPU架构,Intel x86芯片的Machine码为14C
WORD NumberOfSections; //节区数量
DWORD TimeDataStamp; //记录编译器创建文件的时间,有些开发工具可设置该值,有些不可设置
...
WORD SizeOfOptionalHeader;//optional header大小,PE和PE32+的optional header大小不同
WORD Characteristics; //标识文件是否是可执行,是否为dll等信息
3.2 IMAGE_OPTIONAL_HEADER32
PE32+ 为IMAGE_OPTIONAL_HEADER64
# IMAGE_OPTIONAL_HEADER32
WORD Magic; //IMAGE_OPTIONAL_HEADER32为10B,IMAGE_OPTIONAL_HEADER64为20B
...
DWORD AddressOfEntryPoint; //EP的RVA值
...
DWORD ImageBase; //指示文件装入虚拟内存时的优先装入地址(VA)
//执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint
DWORD SectionAlignment; //节区在内存中的最小单位, alignment(n.准线)
DWORD FileAlignment; //节区在磁盘文件中的最小单位
//磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment的整数倍
...
DWORD SizeOfImage; //PE Image在虚拟内存中所占空间的大小,一般而言文件大小和加载到内存中的大小是不同的
DWORD SizeOfHeader; //整个PE头的大小,该值必须是FileAlignment的整数倍
WORD Subsystem; //区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll)
DWORD NumbersOfRvaAndSizes; //用来指定DataDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//后面几章会单独讲解其中重要的几项:
------------------------ DataDirectory[] ------------------------
DataDirectory[0] = EXPORT Directory
DataDirectory[1] = IMPORT Directory
DataDirectory[2] = RESOURCE Directory
...
...
DataDirectory[9] = TLS Directory
...
...
4.节区头
节区头中定义了各节区属性。
PE文件中的code, data, resource等按照属性分类存储在不同节区。
PE文件格式的设计者把具有相似属性的数据统一保存砸一个称为节区的地方,然后需要把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置,大小,访问权限等)
类别 | 访问权限 |
---|---|
code | r-x |
data | rw- |
resource | r-- |
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节区名,如.text,.code,但是PE规范未明确规定节区Name,可以 //向其中放入任何值,所以Name的值仅供参考
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //内存中节区所占大小
} Misc;
DOWRD VirtualAddress;//内存中节区起始地址(RVA)
DOWRD SizeOfRawData; //磁盘文件中节区所占大小,和VirtualSize一般具有不同的值,即磁盘中和内存中节区 //大小不同
DOWRD PointerToRawData; //磁盘文件中节区起始位置
...
DWORD Charateristics; //节区属性,是否包含代码,是否包含初始化/未初始化数据,可读/可写/可执行
0x02 RVA to RAW
通过RVA计算RAW
-
查找RVA所在节区
-
使用公式计算文件偏移(RAW)
RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
由于VirtualSize和SizeOfRawData值不同,所以会引起奇怪的事。。。
0x03 IAT(Import Address Table)
IAT(导入地址表)是一种表格,用来记录程序正在使用哪些库中的哪些函数。IAT存储在PE文件的.text段节区
1.DLL(Dynamic Linked Library)
16位DOS时代不存在DLL的概念,当程序用到库函数时编译器会先从C库中读取函数的二进制代码,然后插入到应用程序中。
但是在之后的Windows OS设计中,为了节约内存/磁盘空间,OS的设计者引入DLL概念:
- 不要把库包含到程序中,单独组成DLL文件,需要时调用即可
- 内存映射技术使加载后的DLL代码,资源在多个进程中实现共享
- 更新库时只要替换相关DLL文件即可,简便易行
加载DLL的方式有两种:
- 显示链接(Explicit Linking),程序使用DLL时加载,使用完毕后释放内存
- 隐式链接(Implicit Linking),程序开始时即一同加载DLL,程序终止时再释放占用内存
IAT提供的机制与隐式链接有关。
notepad.exe调用CreateFileW()时使用call dword ptr ds:[01001104]而非call 7c8107f0:
- kernel32.dll版本不同,CreateFileW()函数的位置也不同
- DLL重定位
2.IMAGE_IMPORT_DESCRIPTOR
IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入哪些库文件
执行一个普通程序时往往需要导入多个库,导入多少库就存在多少IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体组成了数组,且结构体数组最后以NULL结构体结束。
#struct IMAGE_IMPORT_DESCRIPTOR
union {
DWORD Charateristics;
DWORD OriginalFirstThunk; //INT(Import Name Table)的地址(RVA),INT与IAT是DWORD数组,以NULL结束,INT中各元
//素的值为IMAGE_IMPORT_BY_NAME结构体指针
}
...
DWORD Name; //库名称字符串的地址(RVA)
...
DWORD FirstThunk; //IAT的地址(RVA)
#struct IMAGE_IMPORT_BY_NAME
WORD Hint; //序号
BYTE Name[1]; //函数名称字符串
IMAGE_IMPORT_DESCRIPTOR不在PE头,而在PE体中,IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址(RVA),如果要在文件中找到对应的地址需要将RVA转换为RAW。
PE装载器把导入函数输入至IAT的顺序: 见书p108
0x04 EAT(Export Address Table)
与IAT一样,PE文件内的特定结构体(IMAGE_EXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个用来说明EAT的结构体(IMAGE_EXPORT_DIRECTORY)
IAT的IMAGE_EXPORT_DIRECTORY结构体以数组的形式存在,且拥有多个成员,这是因为PE文件可以同时导入多个库文件
在PE文件头中IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress的值即是IMAGE_EXPORT_DIRECTORY的起始位置(RVA)
#IMAGE_EXPORT_DIRECTORY
...
DWORD Base; //ordinal base
DWORD NumberOfFunctions; //实际Export函数的个数
DWORD NumberOfNames; //Export函数中具名的函数个数
DWORD AddressOfFunctions; //address of functions start address array
DWORD AddressOfNames; // address of funcion name string array
DWORD AddressOfNameOrdinals; //address of ordinal(序号) array
从库中获得函数的地址的API为GetProcAddress()。该API引用EAT来获取指定API的地址。
GetProcAddress()的操作原理:见书p114
0x05 高级PE
IAT/EAT的内容是运行时压缩器(Runtime Packer),反调试,DLL注入,API钩取等多种中高级逆向主题的基础知识。
PE规范只是一个建议性质的书面标准,查看结构体内部会发现,其实有很多成员并未被使用。只要文件符合PE规范就是PE文件,利用这一点可以制作出一些脱离常识的PE文件。
Patched PE指的就是这样的PE文件,这些PE文件仍然符合PE规范,但附带的PE头非常具有创意。