PE文件是Windows下的可执行文件格式,一般来说指的是32位的可执行文件,而64位的可执行文件称为PE+或PE32+。PE文件分为以下几种:1.可执行系列(.exe, .scr); 2. 库系列(.dll, .ocx, .cpl, .drv); 3.驱动程序系列(.sys, .vxd); 4.对象文件系列(.obj)。除了对象文件之外的其他PE文件都是可执行的,所以我们需要重点关注除了对象文件之外的其他文件,下面是PE文件的结构图
图1 PE文件结构图
上图结构有个印象就行,实际上我们害得用工具,直接下PE-bear的release,Release v0.6.7.3 · hasherezade/pe-bear · GitHub,打开书中的例子,notepad.exe大概是这样的:
图2 notepad.exe in PE-bear
1 PE头
这一节仅仅介绍PE文件在磁盘中的结构。
1.1 DOS头
80年代的主流文件格式是MZ MS-DOS格式,新的格式(PE format)被创建出来的时候考虑到对DOS环境的兼容性,所以在前面加了一个DOS头,这样一来,PE文件在加载到DOS的时候,实际的程序不会运行,而DOS stub的内容将被执行。如果没有DOS头,那么文件在被加载到DOS的时候会报错。虽然这个东西是为了兼容而存在的,但是里面还有一些东西在Windows有用。
1.2 DOS stub
这部分是可选项,是代码和数据的混合,在DOS环境下这部分内容会被执行。在介绍NT头之前,我们发现,如果用PE-bear打开notepad.exe,那么会发现在DOS stub和NT header之间还有一个Rich头,这部分不是PE规范的内容,如果用Microsoft Visual Studio toolset构建可执行程序,那么这部分就会被加进去
1.3 NT头
1993年Windows NT 3.1推出,MS从16位的NE格式迁移到NT格式,前面的两个部分是为了兼容存在的,所以这里开始是New Technology。
1.3.1 文件头
包含字段:1.CPU的机器码;2.节区的数量;3文件创建时间;4.指向符号表的指针(符号表用用来调试);5.符号表中符号的数量;6.可选头的大小;7.此文件的特征(比如说这个文件能不能执行啊,这个文件是一个系统文件啊等等,只是举个例子,实际上可能不会有这种,具体的可以在这里看到)。
1.3.2 可选头
有一些需要注意的字段:1.AddressOfEntryPoint(程序入口的RVA值);2.ImageBase(PE文件映射刀虚拟内存的优先映射地址);
1.4 节区头
用于指明节区的属性。
2 磁盘映射到内存
基本概念:
- 文件偏移(RAW, FileOffset):用于指明磁盘中某个数据相对于文件开头位置的偏移
- PointertoRawdata:这是节区头的字段,用于指明在磁盘中该节区的起始文件偏移
- 虚拟地址(Virtual Address):PE文件被装入内存的“实际”地址(因为还要做一次映射才到物理内存地址,所以打个引号)
- 相对虚拟地址(Relative Virtual Address):PE文件被装入内存后,相对于文件开头位置的偏移
两个公式:
- 虚拟地址=PE装入基址+相对虚拟地址
- 文件偏移-PointertoRawdata=虚拟地址-相对虚拟地址(式子成立基于一个事实:PE文件从磁盘映射到内存后,某个节区中的数据相对于该节区的起始位置是不变的,尽管映射后节区大小不同)
3 导入地址表(Import Address Table, IAT)
DLL显示链接:程序里面手动加载库,然后调用函数,调用完手动释放;
DLL隐式链接:程序开始时,一同加载DLL,程序结束再释放,程序中调用的时候就像已经定义过一样调用。
IAT记录了隐式链接时,程序正在使用哪些库的哪些函数。可选头里面的Import Directory指出了导入目录的相对虚拟地址,跳转到这个相对虚拟地址发现这是一片记录了若干个Image_Import_Descriptor的相对虚拟内存空间,我们把这些描述符的集合称为导入表。
既然我们知道导入表是若干个导入描述符组成的,那么我们只需要研究其中一个描述符,需要注意一个描述符对应的是一个导入的模块,在Notepad.exe中是一个DLL,一个描述符包括5个字段:
- OriginalFirstThunk:记录了一个相对虚拟地址,跳转到这个相对虚拟地址,发现存放的是调用这个DLL的所有函数的名称,我们把这个称为导入函数名称表,INT
- TimeDateStamp:不关注它
- Forwarder:目前没用到也不关注它
- NameRVA:存放了这个模块的名称还有一些其他的字符串,这些字符串暂时不知道用来干嘛
- FirstThunk:存放了一个相对虚拟地址,跳转后发现以这个相对虚拟地址指示了一片相对虚拟地址空间,这一片相对虚拟内存存放了若干个相对虚拟地址。如果以这些相对虚拟地址为元素,那么这篇空间存放了一张表,我们把这张表称为IAT,导入地址表。我们研究其中一个地址,发现这个地址实际上指的仍然是一片空间而不是简单的函数的虚拟地址,这片空间是一个结构体,其中的某个指针指向的仍然是函数的描述,这是怎么回事呢?原来是因为PE在加载进内存后会改写这部分内容,使其指向实际的虚拟地址。根据什么改写的呢?书里说是用IMAGE_IMPORT_BY_NAME(ORDINAL)获取函数实际的起始地址。
以上是用PE-bear打开notepad.exe的分析
4 导出地址表(Export Address Table, EAT)
可选头中有一个导出目录,导出目录里面存放了一张表,这张表记录了导出函数与符号的信息,我们称其为导出表,导出表其中一个字段指明了导出函数的所有相对虚拟地址,这些RVA组成的表叫做导出地址表,EAT,上面重写IAT就是通过EAT找到实际的函数入口地址的。
一些疑问
1.PE文件是如何加载进内存的?首先所有进程用的都是虚拟地址,操作系统会为每个进程都分配一个虚拟内存空间(一般4GB),然后由操作系统的内存管理单元(MMU)转换成物理地址,我们不关注转换的这个过程,只需要知道有这个事情就好了。所以PE文件加载进内存的这个过程其实就是一个从文件地址映射到虚拟地址的过程。
2.如果用ida打开notepad.exe会发现部分数据似乎与用PE-bear分析时不一样,这是怎么回事呢?通过对比PE-bear节区起始RVA与IDA对应节区的起始地址,发现两者一样,也就是说IDA虽然是静态分析,但是它已经将PE文件映射到了虚拟地址空间。
3.IDA分析文件时,有没有把该文件依赖的dll加载进物理内存?我想应该是没有的,又不是要执行程序,可能只是找到磁盘中的dll,把相应代码拷贝并展示给用户,来模拟程序运行时的虚拟内存环境,这个有待考究。
References
1.https://0xrick.github.io/win-internals/pe3/#dos-header
2.[PE结构分析] 8.输入表结构和输入地址表(IAT) - endlesstravel - 博客园 (cnblogs.com)