PE文件格式

PE文件格式是Windows操作系统中的核心内容,学习PE文件格式可以理解可执行文件如何储存,如何进入内存被执行。

PE文件定义

PE文件是windows操作系统下的可执行文件

其中32位的可执行文件称为PE或PE32,64位的被称为PE32+或PE+。

PE文件分为4种类型:

可执行系列:.EXE .SCR

库系列:.DLL .OCX .CPL .DRV

驱动程序系列:.SYS .VXD

对象文件系列:.OBJ(除了该项外,上面三种都可执行,

​ PE正式规范中OBJ文件也是PE文件)

PE文件结构

PE文件分为PE头PE体,又可细分为DOS头,DOS存根,NT头,节区头(分别为.text,.data和.rsrc)然后是节区(分别为.text,.data和.rsrc节区)。text为代码区,data为数据区存放数据结构,rsrc为资源区,windows特有,存放应用程序资源。

每个节区前后存在NULL填充,即用00填充在节区头与节区,节区与节区之间,令文件或内存中节区的起始位置在各文件/内存最小单位的倍数上(这个值与NT头中的成员有关)。

PE头:PE头的构成是许多结构体(DOS头,DOS存根,NT头,节区头)。以下介绍结构体中的部分重要成员


1)DOS头

  • (IMAGE_DOS_HEADER)(19个成员):
  • e_magic:dos签名:4D5A->MZ
  • e_lfanew(long):指向NT头的偏移(注意,这是long值,但是数据表明了从文件开头到NT头的偏移值,可以被用作指针.同时该值也是小端序储存)

2)DOS存根

​ dos存根不是结构体,是存在于DOS头下方的一段命令与数据的混合,如果在dos环境下运行程序,会执行这里的指令。


3)NT头

  • (IMAGE_NT_HEADERS)(三个成员):

  • Signature(dword):签名:50450000h->”PE”00

  • FileHeader(IMAGE_FILE_HEADER):文件头,是一个结构体

    • Machine:每种类型的值表明能在哪种cpu上编译运行:14ch=x86,8664h=x64等等,但是并不是说32位的只能在32位系统上运行,现在有很多方法可以兼容运行。

    • NumberOfSelection:节区数量,一定大于0

    • SizeOfOpionalHeader:指出NT头中最后一个成员 (IMAGE_OPTIONAL_HEADER32或64)的大小供系统查看

    • Characteristics(word):标识文件属性,每一位都代表一个属性,如0x0002表示这是可执行文件,0x1000表示是系统文件,0x2000表示这是dll文件

    • TimeDateStamp:标识编译时间

  • OptionalHeader(IMAGE_OPTIONAL_HEADER32OR64):可选头:

    • Magic:可选头为32位时为10B,64位时为20B

    • AddressOfEnterPoint:EP的rva值,指出起始代码地址

    • ImageBase:指出优先装入地址0~FFFFFFF,由于原位置可能已经加载了其它PE文件(主要是dll)

    • SectionAlignment,FileAlignment:指定节区在磁盘位置和内存储存的最小单位

    • SizeOfImage:指定image在虚拟内存中占的大小

    • SizeOfHeader:指出PE头的大小,应是SectionAlignment的整数倍

    • Subsystem:该值表示文件类型:1:驱动文件,2:窗口应用程序,3:控制台应用程序

    • NumberOfRvaAndSizes:该值指定下一个元素(DataDirectory)的元素个数,标准是16个(10h)

    • DataDirectory(结构体数组):重点是0和1和9,该项存了许多表


4)节区头

(节区头是由结构体组成的数组,每个结构体对应了一个节区):

  • IMAGE_SECTION_HEADER是结构体的类型,其中有部分重要成员:

    • VirtualAdress(无值,已被可选头内容定义)

    • VirtulSize:内存中节区大小(无值,已被可选头内容定义)

    • VirtulSize:表明内存中节区的大小

    • SizeOfRawData:磁盘文件中节区大小

      • VirtualSize与SizeOfRawData一般不同,说明磁盘文件中节区大小与加载到内存后的节区大小不同,若VirtualSize大于SizeOfRawData,节区剩余部分会被填充为0
    • PointerToRawData:磁盘文件中节区起始位置(必须为FileAlignment的整倍数)

    • Characteristics:属性,由位或而来

    • Name:可以填入任何值,所以仅供参考不一定叫.code的区是.code区


PE文件的映射

RVA(相对虚拟地址) to RAW(文件偏移)

RAW=RVA-VA+PointerToRawData

IAT和EAT

IAT:

导入地址表

IMAGE_IMPORT_DESCRIPTOR(又叫IMPORT Directory Table):

IMAGE_IMPORT_DESCRIPTOR

一个库就对应了一个IMAGE_IMPORT_DESCRIPTOR结构体,所有需要导入的库的所有结构体形成了数组,最后以NULL结构体结束。

其中的重要成员是:

1
2
3
4
5
OriginalFirstThunk:导入名称表(该表是指向函数名的指针数组)INT	  地址rva(注意该值和characteristics是一个联合)

Name:库名称字符串地址rva

FirsThunk:导入地址表(该表是指向函数地址的指针数组)IAT地址rva

作用方式:

编译时,INT被创建,每个库都把指向不同函数名的指针(指向字符串)写入INT表和IAT表,运行时,程序先查找绑定输入表,然后根据信息在INT地址找到函数名,把函数地址写入IAT,供后续使用。

此结构体数组被储存在PE体中,但PE头中IMAGE_OPTIONAL_HEADER32OR64.DataDirectory[1].VirtualAddress是该结构体数组的起始位置(RVA)(前4字节),后4字节是大小。

EAT:

导出地址表

IMAGE_EXPORT_DIRECTORY:

IMAGE_EXPORT_DIRECTORY

同上,每个模块(dll)都有一个结构体,一起构成数组,重要成员有:

1
2
3
4
5
6
7
8
9
10
11
12
13
NumberOfFuctions:实际导出函数个数

NumberOfNames:导出函数中具名的函数个数(显式命名)

AddressOfFunctions:导出函数地址数组,含有入口地址,个数为NumberOfFunctions

AddressOfNames:函数名称地址数组,有指向全部导出函数入口地址的rva,个数为NumberOfNames

AddressOfNameOrdinals:存该dll中的函数的序号(ordinal)

Name:是指向模块(dll)在内存中的相同名称地址的指针,地址为RVA

Base:序号的基准值,序号减base值来调用这个模块

作用方式:

1)通过名称:当程序发现一个函数调用不在内部时,会查找dll是否包含这个函数,如果包含,就会用名称先从AddressOfNames中比较寻找所需函数地址,用得到的索引再在AddressOfNameOrdinals中得到序号,用序号在AddressOfFunctions找到对应的地址进入。然后就会连接地址与这个函数。之所以这么麻烦是因为一个函数可能被多个程序使用,一个程序也可能使用多个动态链接库。查找名称也可以保证即使有新版本,旧编译的程序也可以调用这个函数。

该结构体数组存在在PE体中,RVA存在于DataDirectory[1].VirtualAddress(前4字节),后4字节是大小。

关于修改PE文件

添加节区:

添加节区首先要加节区头,使文件能识别到这个节区,由于一个节区头大小为40字节(

节表起始位置 = DOS头的e_lfanew字段值 + sizeof(IMAGE_NT_HEADERS)

节表大小 = 文件头的NumberOfSections字段值 * sizeof(IMAGE_SECTION_HEADER)

用这个公式计算节表后是否大于40字节,防止截断,如果可以放入一个节区头就放入,如果不行,一般有两种方法:一个是删掉dos存根,把后面的pe头部分前移,在后面的位置加节区头,这时要注意IMAGE_DOS_HEADER 的e_flanew要修改为NT头的偏移,因为NT头移动过,不过不用管对齐,因为并没有更改文件大小和节区位置,只要注意后面加的节区就可以了。第二个是在节表后面加数据,让它变长,这就要在SizeOfHeader里面增加字节,在多出来的地方写节区头,但是这样要把每个节区头里的偏移调到对应的偏移,因为节区头数据变多之后,节区的地址相应增加了那么多。但要注意增加的字节必须要对齐为最小单位的倍数。

还要把NumberOfSections+1,可选头中的sizeOfImage加对应的值

删除节区:

直接0填充对应节区头,就识别不到节区啦。然后要把NumberOfSections-1.

倒是可以这样,但是真的要删除节区的话,参考《逆核》17.2.2删除.reloc节区(位于整个文件的最后):

:这里使用的是exe文件,因为一般exe文件没用重定位节也可以正常运行
  1. 从节区头找到reloc节区起始偏移,跳转后删除(物理)
  2. 文件头中有Number of Section填入减1的值(删了几个就减几)
  3. 可选头中有整个记录映像大小的Size of Image,在节区头中找到.reloc节区的大小RawDataSize让映像大小的值减去这个值