PE文件相关内容

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 HeaderOptionalHeader结构体

  • 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位才是修改的地址。因为有的可能是为了内存对齐而产生的垃圾。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值