PE文件格式介绍
PE文件格式总结……………………………..
一、引入函数表与引出函数表
1. 引入函数表:
对于一个程序,又用到很多EXE/DLL,把使用他们的信息写成一个数组,成员就是每个EXE/DLL和里面使用的function函数信息。
2. 引出函数表:
对于一个DLL,为了方便别的函数调用每一个EXE/DLL内的function,则,做了一个引出函数表,里面有相关信息。
3. 引入函数表和引出函数表的联系:
当PE文件运行之前,加载器必须把需要调用分布在DLL里的function加载进来。首先进入引入表,依次处理每个数组:根据数组信息里面的DLL地址,和其中用到的function地址,查找对应DLL的引出表,确定最终位置。最后完成DLL加载。
二、加载执行PE文件的大概步骤(不精确,但可以这样思维):
1. 验证是不是有效的PE文件,主要看DOS MZ HEADER里标志是否为“MZ”,如果是(表示是DOS程序),则由e_lfanew的值跳到PE HAEDER
2. 在PE HAEDER中,察看signature 是否是PE/0/0,如果是(表明是有效的PE文件)
3. 在PE HAEDER中,察看file header 中的 machine 是否准许在本机运行
4. 在PE HAEDER中,根据characteristic 中的值,来控制下面操作。跳到optionalheader,
5. 为PE文件分配堆栈,同时确定PE文件在内存中的映像,程序入口点,各RVA。
6. 一次处理数组datadirectory 内每一项,包括通过引入表,把DLL加载进来
7. 跳到OEP,执行
具体介绍:
一、一些概念:
(1)PE文件的结构图
DOS MZ Header |
DOS Stub |
PE Header |
Section Table |
Section |
(2)、虚拟地址的概念:由段地址:偏移量的形式表示;在32位的系统中可寻址范围为232=4G;(进程实际用到的只有2G,OS用了2G) OS为我们的进程分配了4G的线性地址空间,使得我们可以直接使用偏移量来直接表示而不需要段地址;至于线性地址跟实际的物理内存地址的转换就是由OS来管理的了。
(3)、相对虚拟地址的概念:Relative VirtualAddress(RVA),相对上面系统分配给进程的基址(虚拟地址)的偏移量,在PE文件里许多都是以RVA来表示的
(4)、Section的概念:有共同属性的数据/代码被分配到同一节当中,其中的Section名称只具有标识作用;以下是Section的名称和对应的作用(属性)
Section名 | 属性 |
.arch | 最初的构建信息(Alpha Architecture Information) |
.bss | 未经初始化的数据 |
.CRT | C运行期只读数据 |
.data | 已经初始化的数据 |
.debug | 调试信息 |
.didata | 延迟输入文件名表 |
.edata | 导出文件名表 |
.idata | 导入文件名表 |
.pdata | 异常信息(Exception Information) |
.rdata | 只读的初始化数据 |
.reloc | 重定位表信息 |
.rsrc | 资源 |
.text | .exe或.dll文件的可执行代码 |
.tls | 线程的本地存储器 |
.xdata | 异常处理表 |
二、深入了解:
(1)、DOS MZ Header和 DOS Stub
DOS Stub是一段可执行的代码,通常是由编译器生成的一段简单的中断21H服务9来显示字符串“This program cannot run in DOS mode”
如果在DOS中执行PE文件则会显示DOS Stub中的“This program cannot run in DOS mode”,在windows中执行时,则加载器会跟据DOS MZ Header中的e_lfnew跳转到PE Header。
(2)、PE Header
PE Header是一个IMAGE_NT_HEADERS结构,此结构包含在WINNT.H中;
以下是此结构图:
IMAGE_NT_HEADERS结构
字段名 | 作用 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
DWORD Signature | 对于PE格式文件这个应该为一个ASCII码为PE/0/0的值 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
FileHeader结构
| 对我们来说有用的就Machine、NumberOfSections和Characteristics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OptionHeader结构
|
|
(3)Section Table
节表是一个结构数组,数组的每个结构对应PE文件的一个节;每一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等,都保存在这个结构里。
此结构如下
struct IMAGE_SECTION_HEADER
{
BYTE Name[8];
union
{
DWORD PhysicalAddress;//物理地址
DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个,
//一般是节的数据大小
} Misc;
DWORD VirtualAddress;//RVA
DWORD SizeOfRawData;//物理长度
DWORD PointerToRawData;//节基于文件的偏移量
DWORD PointerToRelocations;//重定位的偏移
DWORD PointerToLinenumbers;//行号表的偏移
WORD NumberOfRelocations;//重定位项数目
WORD NumberOfLinenumbers;//行号表的数目
DWORD Characteristics;//节属性 如可读,可写,可执行等
};
重要的是以下字段
字段名 | 作用 |
Name | 这儿的节名长不超过8字节。记住节名仅仅是个标记而已,我们选择任何名字甚至空着也行,注意这里不用null结束。命名不是一个ASCIIZ字符串,所以不用null结尾。 |
VirtualAddress | 本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h。 |
SizeOfRawData | 经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。 |
PointerToRawData | 这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。 |
Characteristics | 包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。 |
(4)Import Table和Export Table
在知道两者作用前,我们先介绍一下PE文件如何调用其它模块的函数的如(USER32.dll的GetMessage);编译器并不是直接Call其它模块的函数的,而是先Call到一个存储JMP DWORD PTR [**]指令的地址,然后JMP DWORD PTR [**]跳到一个地址上去,其地址存在于.idata节中;这个地址存储着其它模块的函数的地址。现在再来说一下Import Table的作用,它的作用是存储该程序调用了哪些模块哪些函数,而Export Table的作用是存储了对应模块的函数的地址。
Import Table实际是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,每个结构包含引入的DLL信息,该数组以一个全0的结构结尾;
Struct IMAGE_IMPORT_DESCRIPTOR STRUCT
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk ; //指向一个IMAGE_THUNK_DATA数组的RVA,
//每个IMAGE_THUNK_DATA对应于一个输入函数
//如果函数以名称输入,则IMAGE_THUNK_DATA对应于// 一个IMAGE_IMPORT_BY_NAME结构
}
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name1;
DWORD FirstThunk ;
};
Import Table和Export Table的位置都由DataDirectory指定;
Export Table是一个IMAGE_EXPORT_DIRECTORY结构数组,该结构共有11个成员;主要成员如下表
字段名 | 作用 |
nName | 模块的真实名称。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。 |
nBase | 基数,加上序数就是函数地址数组的索引值了。 |
NumberOfFunctions | 模块引出的函数/符号总数。 |
NumberOfNames | 通过名字引出的函数/符号数目。该值不是模块引出的函数/符号总数,这是由上面的NumberOfFunctions给出。本域可以为0,表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号,那么数据目录中引出表的RVA为0。 |
AddressOfFunctions | 模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。简言之,模块中所有函数的RVAs都保存在一个数组里,本域就指向这个数组的首地址。 |
AddressOfNames | 类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA。 |
AddressOfNameOrdinals | RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。 |