1.PE header:
如何加载到内存、从何处开始运行、运行中需要的DLL文件有哪些、需要多大的栈/堆内存等,大量信息以结构体形式存储在PE头中。
PE文件格式如下:
DOS头+DOS存根+节区头=PE头
1.DOS头
在PE头最前面的是IMAGE_DOS_HEADER结构体,用来扩展已知的的DOS EXE头,该结构体共64个字节,我们必须要知道其中两个重要成员,分别是:
- e_magic
代表DOS签名,其16进制值为4D5A(ASCII值为“MZ”) - e_flanew(最后四字节)
指示NT头的偏移(NT头开始的地址,根据不同的文件拥有可变值)
Intel系列的CUP使用逆序存储数据(小端序)
2.DOS存根
DOS存根是可选项,大小不固定,即使没有DOS存根文件也能正常运行。由代码和数据混合而成,但是其中代码被忽略不会执行。
3.NT头(块表)
NT头结构体是IMAGE_NT_HEADERAS,其由三个成员组成,分别是:Signature结构体、File Header、OptionalHeader结构体。
- Signature结构体(PE标识)
签名结构体,其值为50450000h("PE"00) - FIle Header(标准PE头、文件头)
文件头是表现文件大致属性的IMAGE_FILE_HEADER结构体,其总共20个字节。
以下四种成员变量十分重要,若设置不正确将导致文件无法正常运行
1.Machine
每个CPU都有唯一的Machine码,可以在winnt.h中查看定义的Machine码。
2.NumberOfSectons
用来指出文件中存在的节区数量。该值一定要大于0,且定义的节区数量与实际节区不同时,将发生运行错误。
3.SizeOfOptionalHeader
其用来指出可选PE头(IMAGE_OPTIONAL_HEADER32)的长度。
PE32+格式的文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,而不是IMAGE_OPTIONAL_HEADER32结构体,两者的尺寸是不同的,所以需要在SizeOfOptionalHeader成员中明确指出结构体的大小。
4.Characteristics
该字段用来标识文件的属性,文件是否是可运行的形态、是否为DLL文件等信息,若该值为0x0002,代表文件是可执行的,若值为0x2000,则代表该文件是DLL文件。 - Optional Header(扩展PE头)(32位扩展PE头一般来说是224个字节)
IMAGE_OPTIONAL_HEADER32结构体很大,在其中我们需要关注以下成员:
1.Magic
为IMAGE_OPTIONAL_HEADER32结构体时,Magic码为10B,为IMAGE_OPTIONAL_HEADER64时,Magic码为20B。
2.AddressOfEntryPoint
持有EP的RVA(相对虚拟地址)值。该值指出程序最先执行的代码起始地址,相当重要。
3.ImageBase
指出文件的优先装入地址。执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint(对于操作系统来说都是虚拟地址)
4.SectionAlignment,FileAlignment
FileAlignment指定了节区在磁盘文件中的最小单位,SectionAlignment指定了节区在内存中的最小单位。磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment值的整数倍。
5.SIzeOfImage
指定了PE Image在虚拟内存中所占空间的大小。文件的大小与加载到内存中的大小一般是不同的。
6.SizeOfHeader
指出整个PE头的大小。
7.Subsystem
用来区分系统驱动文件(.sys)与普通可执行文件(.exe,*.dll)。
8.NumberOfRvaAndSizes
用来指定DataDirectory数组的个数
#9.DataDirectory
是由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都有被定义的值。这每项都指向一个表,其中重要的有导入表、导出表、重定位表、资源表。
这每个数组成员都是差不多的结构,拿EXPORT Directory来说,它其中记录了导出表的地址,结构如下:
IMAGE_DIRECTORY_ENTRY_EXPORT
struct_IMAGE_DATA_DIRECTORY{
0x00 DWORD VirtualAddress;//导出表的位置
0x04 DWARD Size;//导出表的大小,这个大小包括导出表以及其中的子表所占用的空间}
4.节区头
节区头中定义了各节区的属性。
PE文件中的code、data、resource等按照属性分类存储在不同节区,节区头记录各节区属性,包括阔文件/内存的起始位置、大小、访问权限等。
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。每个结构体占40个字节,这里的union结构总共占4个字节。所以加起来总共是40字节。
RAW(文件偏移)=RVA–节.VirtualAddress+节.PointerToRawData
首先要找到数据在内存中的哪个节区中,得到数据相对于节区的偏移值,再在文件中找到相应节区,节区的相对偏移即是该数据存放的地方。
5.导入表
导入表的地址在扩展PE头的最后一个成员结构体数组中的第二个成员中,其中有第一个导入表的起始地址和总共的大小,地址4字节,大小4字节。
依赖多少个模块就有多少个导入表
- 不要把库包含到程序中,单独组成DLL文件,需要时调用即可。
- 内存映射技术使加载后的DLL代码、资源在多个进程中实现共享。
- 更新库时只要替换相关的DLL文件即可,简便易行。
加载DLL的方式:
- 显式链接(Explicit Linking):程序使用DLL时加载,使用完毕后释放内存。
- 隐式链接(Implicit Linking):程序开始时即一同加载DLL,程序终止时再释放占用的内存。
5.1导入表结构
每一个导入表有20个字节,多个导入表是连续存放的,并且以20个字节的0结尾,所以判断导入表有多少个就看什么时候出现了20个0。
OriginalFirstThunk指向INT(导入名称表),FirstThunk指向IAT(导入地址表)。这两个表的内容是一样的(在PE文件未加载到内存中时是一样的,加载到内存后IAT表中存储的是函数地址)。
INT表每个成员都是一个结构体,都是IMAGE_THUNK_DATA结构体,其结构如下:每个结构体最长四个字节。这个union代表联合体,相当于给这四个字节起了四个不同的名字。
每个INT表中FirsThunk或OriginalFirstThunk指向的IAT或INT存在着多少个IMAGE_THUNK_DATA结构体,就说明当前pe文件依赖此dll中多少个函数,INT和IAT表的结束也是以0为结束符,若出现连续4字节的0则说明IAT或INT结束。
INT:
对于每一个IMAGE_THUNK_DATA结构体,首先判断最高为是否为1,如果是,那么除去最高位的值就是函数的导出序号,如果不是,那么这个值是一个RVA,指向IMAGE_IMPORT_BY_NAME。这个结构体结构如下:
Hint不是函数序号,是函数在函数地址导出表中的索引,可能为空。Name只有一个字节,它只存储了函数名称的第一个字节,因为函数名长度是不确定的,所以后面的也是函数名的一部分,直到找到0为止。这就是当前函数的名字。
分析每一个导入表的INT中的所有IMAGE_THUNK_DATA结构体,我们就能直到当前pe文件依赖哪个dll文件的哪些函数(一个PE文件中有多个导入表,对应多个依赖的dll文件,一个导入表中有一个INT,中有多个IMAGE_THUNK_DATA结构体,对应dll文件的函数)。
**找到函数地址:**根据函数名称,使用getaddress函数得到函数地址,再将函数地址放入IAT中,所以在PE文件加载到内存后,IAT表中存储的是函数的地址。
6.导出表
导出表结构
pe文件(例如dll文件)提供一些函数给别人用,导出表中就记录了函数的名称、地址等信息。
1970.0开始的秒数为时间戳
函数可以以函数名称和序号的形式导出
导出表中最重要的是最后三个DWORD,分别指向导出函数地址表、导出函数名称表、导出函数序号表。
导出函数地址表记录了各个函数的地址RVA,每个四字节
导出函数名称表,记录了指向函数名称的指针RVA,每个四字节
导出函数序号表,每个序号占两字节。
当根据函数名称寻找函数时,首先查找函数名称表,找到对应函数名称的索引,又去找函数序号表中相同索引的序号,再根据序号,将序号当作索引在函数地址表中查找对应函数的地址。
当直接通过序号来查找函数地址时,首先将序号–base(导出函数起始序号),作为索引再去查导出函数地址表。得到函数的地址。
7.重定位表
为了使进程空间中的pe镜像基址不用固定,因为可能在编译时不是所有地方都是用的RVA,万一有的地方会生成固定地址,那在该模块没有得到其正确imagebase时要能够修改其生成的固定地址。
扩展PE头的数据目录项的第六个结构就记录了第一个重定位表的位置,其结构如下:
SizeOfBlock是以字节为单位的,它代表了当前重定位表的大小,每一个重定位表的大小可不是以上结构的8个字节,而是SizeOfBlock个字节。这样一直往下找,直到找到8个字节的0,则代表重定位表数组结束了。
一个物理页(4KB大小)创建一个重定位表,为了使重定位表的大小更小,我们将这一个页的基址取出来作为VirtualAddress,它为4字节,那么那些需要修改的地方的地址就可以用偏移地址的方式保存,用比如说2字节来存储足矣,这样的话就可以节省内存空间。但是实际上2字节中只用到了低12位,所以规定只有当高四位为0011时,VirtualAddress+低12位才是修改的地址。因为有的可能是为了内存对齐而产生的垃圾。