3

PE结构-PE文件的两种状态

 2 years ago
source link: https://antipassion.github.io/2021/11/16/PE%E7%BB%93%E6%9E%84-PE%E6%96%87%E4%BB%B6%E7%9A%84%E4%B8%A4%E7%A7%8D%E7%8A%B6%E6%80%81/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文的是学习复现自吾爱破解dalao lyl610abc的PE文件解析系列,非本人原创,属于学习笔记(大佬yyds!)。

PE文件的两种状态

PE文件处于磁盘中与处于内存中时,两者的结构会稍微发生改变。

一个PE文件可以分为两种状态:

运行态: 当一个PE文件被打开时,PE文件的相关数据将会被装载到内存中,根据文件对齐以及内存对齐中的区别,文件的大小以以及结构会发生相应的改变

非运行态: 当一个PE文件尚未运行时,其数据存储在磁盘中时的一种状态

思维脑图

lyl610abc师傅太强了

image-20211116145908655

PE文件整体结构表

结构 对应C数据结构 默认占用空间大小(单位字节) DOS MZ头 _IMAGE_DOS_HEADER 64 DOS Stub 仅在MS-DOS系统下有效,不作研究 不固定 PE文件头 _IMAGE_NT_HEADERS 4+20+224=248 (标志+标准头+扩展头) PE文件头标志 Signature 4 PE文件表头/标准PE头 _IMAGE_FILE_HEADER 20 PE文件表头可选部分/扩展PE头 _IMAGE_OPTIONAL_HEADER 224 块表/节表 _IMAGE_SECTION_HEADER 40 块/节 无 由块表/节表决定

判断文件类型是否为PE格式文件

判断流程如下:

  • 判断前两个字节是否为4D 5A(MZ)
  • 找到3Ch
  • 根据3Ch位置的2字节大小数据跳转偏移至其中,查看是否为50 45 00 00(PE..)
地址 长度(单位字节) 对应C的数据结构 说明 值 ASCII 0 2 _IMAGE_DOS_HEADER的第一个成员e_magic DOS MZ头的第一个成员 4D 5A MZ 3C 2 _IMAGE_DOS_HEADER的最后一个成员e_lfanew 指出PE头文件偏移位置 不定 不定 [3C] 4 Signature PE文件头标志 50 45 00 00 PE..

利用msf生成的exe马进行实例分析

使用十六进制编辑器打开,判断文件格式类型

image-20211116150624182

可以看到,00处起始位置为4D 5A,表示DOS头开头;3Ch处位置是1801 表示PE头文件偏移位置;利用Ctrl+G跳转至118,可以看到标准PE头50 45,此文件可以判断为是PE文件

对应表格数据为

地址 说明 值 ASCII 0 DOS MZ头的第一个成员 4D 5A MZ 3C 指出PE头文件偏移位置C 118

F0 PE文件头标志 50 45 00 00 PE..

由此,我们可以发现以PE文件头标志为开始的PE头

根据PE结构可知,整个PE头是:PE文件头标志+标准PE头+扩展PE头的标准,长度大小为4+20+224=248字节

根据3Ch处的118

后面所占的字节地址为:0x118+4+20+224=280+4+20+224=448=0xE0

由此可知整个PE头所占范围为0x118~0xE0

PE文件头标志和标准PE头:

​ 0x118+2+20=200+24=0x130

image-20211116170504766

扩展PE头

根据先前得到的标准PE头结束后,紧挨着的224个字节便是扩展PE头所占空间

标准pe头结束位置:0x130

扩展PE头所占空间:224字节

所以扩展PE头所占区域为: 0x130+224=304+224=528=0x210

由此可知,自0xE0开始到0x1C0结束,都是扩展PE头位置

image-20211116170637779

块表

先前拿到的扩展PE头地址后16个字节为空字节,跳过,拿到第一个块表地址

从先前拿到的块表头地址继续往后看40个字节(块表大小) 拿到第二个块表的首地址

第一个块表头地址: 0x210

块表大小为 40字节

0x210+40 = 0x238

第一块表范围: 0x210~0x238

从0x238开始便是第二个块表

第一个块表: .text

image-20211116170758247

第二个块表: .rdata

0x238+0x28=0x260 ,块表范围为0x238~0x260

image-20211116170913559

第三个块表: .data

0x260+0x28=0x288,块表范围为0x260~0x288

image-20211116170953884

第四块表 .sxdata

0x288+0x28=0x2B0,块表范围为0x288~0x2B0

image-20211116171048772

第五块表 .rsrc

0x2B0+0x28=0x2D8,块表范围为0x2B0~0x2D8

image-20211116171132556

第六块表 .reloc

0x2D8+0x28=0x300,块表范围为0x2D8~0x300.

image-20211116171308670

汇总块表

块名称 块地址 .text 0x210~0x238 .rdata 0x238~0x260 .data 0x260~0x288 .sxdata 0x288~0x2B0 .rsrc 0x2B0~0x2D8 .reloc 0x2D8~0x300

块表后的空隙

块表后跟着的按理应该是块,但在块表后和块之前,多出了一段空间

此处为300~400

image-20211116171539285

存储时不存在空隙的,被称为连续存储

但在块表以及块之间是可能存在空隙的,这个空隙里面一般被填充为编译器插入的数据(也可能不存在)

这段间隙的修改并不会导致程序的不可运行,因此可以拿来写入自己想要的代码来对程序进行修改

为什么会存在这段间隙呢?

  • 这段间隙是由于块表与块之间没有进行连续存储
  • 这段长度的存在与否以及长度,取决于块的起始位置
  • 而块的其实位置则根据扩展PE头中的成员决定
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

结构体中的SizeOfHeaders成员(DWORD类型占4个字节)

SizeOfHeaders的含义是3个头按文件对齐后的大小:(DOS头+PE头+块表)大小加起来后进行文件对齐后得到的大小

自DOS头开始至块表结束,头大小和为0x248,此处想要拿到SizeOfHeaders成员的值,需要了解文件对齐的概念并对其进行对齐处理。

什么是文件对齐?

文件对齐的标准通过扩展PE头中定义的FileAlignment(DWORD类型 占4字节)

文件对齐的要求就是SizeOfHeaders必须为FileAlignment的整数倍

通过以上概念,我们可以理解,由于扩展PE头FileAlignment成员的限制,要求SizeOfHeaders必须为它的整数倍(不足则填充);要想知道文件对齐的标准是多少,我们需要拿到FileAlignment的值

由于前面已知扩展PE头开始地址为:0xE0

FileAlignment(DWORD 4字节)地址为:0xE0+36 = 0x104

image-20211116164142391

FileAlignment为 00 02 00 00 即为 0x200

前面拿到的头大小和为0x248,显然并不是0x200的整数倍

其整数倍,也就是SizeOfHeader的大小应为: (0x248/200+1)*200=400

SizeOfHeaders大小

再通过扩展PE头成员值的方式验证计算是否正确

先前拿到的FileAlignment地址为0x104,再往后24字节,便是SizeOfHeaders存放地址

0x104+24 = 0x11C

SizeOfHeaders:na

image-20211116165632063

拿到00 04 00 00 即 0x400,是FileAlignment的整数倍

image-20211116164459021

可以看到,块表与块之间的间隙自0x400结束

为什么需要文件对齐?

跟内存对齐一样,都是为了使执行时的效率更高,方便内存与磁盘进行交换数据更有效率。

块结构

块的起始地址由块表中的PointerToRawData决定,第一个块的起始地址则有上面的SizeofHeaders决定

块部分存储的是数据,如何存储由块表进行决定

块表在C中的定义

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData; //块的大小
DWORD PointerToRawData; //块在磁盘中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

块的起始地址

找到结构体中的PointerToRawData成员(DWORD类型占4个字节)

PointerToRawData的含义为该块在磁盘文件中的偏移

前面拿到第一个块表的首地址0x210

从0x210开始往后找20字节,0x210+20=0x224,拿到PointerToRawData的地址,跳转过去拿到4字节的值(磁盘文件中的偏移)

00 04 00 00 即 0x400image-20211116172542500

与SizeOfHeaders得到的一致,前面说过,块的起始地址由块表中的PointerToRawData决定,而第一个块的首地址由SizeOfHeaders决定,此处一致,验证了上文的说法。

块的大小

SizeOfRawData为块的大小(文件对齐后)

SizeOfRawData(Dword 4字节)就在PointerToRawData前面

所以其在块表中的地址为PointerToRawData-4=0x224-4=0x220

SizeOfRawData:

00 72 09 00 即为 0x97200

image-20211116173302128

块的大小与前面三个头(DOS+PE+块表)的大小一样,都需要满足文件对齐

先前拿到的FileAlignment为0x200 块的大小SizeOfRawData大小为0x97200满足整数倍条件,满足文件对齐。

块的结束地址(下一个块的起始地址)

块的结束地址为 块的起始地址+块的大小

即 0x400+0x97200 = 0x97600

image-20211116173621872

可以看到,第一个块与第二个块之间存在空隙,这段空隙是由于在文件中补足文件对齐产生的。

在非运行态下:

  • DOS首部和PE文件头与块表连续存储,中间没有空隙
  • 块表与块之间存在文件对齐的影响,可能会存在间隙
  • 块与块之间也可能由于文件对其的影响产生间隙

相关数据结构成员

数据结构成员 所属数据结构 说明 SizeOfHeaders 扩展PE头 头大小(文件对齐后) FileAlignment 扩展PE头 文件对齐 PointerToRawData 块表 第一个块表的PointerToRawData由SizeOfHeaders决定,后面块表的PointerToRawData由前一个块表的PointerToRawData+SizeOfRawData决定 SizeOfRawData 块表 块表的大小(文件对齐后)

记录下各个结构的起始以及结束位置方便接下来与运行态比较

结构 起始地址 结束地址 大小 DOS部首 0 118 0x118=280 PE文件头 118 210 0xF8=248=4+20+224 块表 210 300 0xF0=240=6*40 前三个结构 0 400 0x400(文件对齐后) 第一个块 400 97600 0x97200(文件对齐后)

运行态,指PE文件装载入内存时,PE结构文件的状态

加载运行态PE文件

1.启动PE文件

image-20211116192050095

2.使用16进制编辑器 工具->打开主内存->选定PE程序

image-20211116192222818

分析运行态的PE文件

此时Dos头的偏移起始地址为00820000,结束地址为008200F0

image-20211116192334866

跟进偏移,到达PE头标志起始地址008200F0,可见头标志起始地址和标准PE头

PE头标志和标准头

起始为008200F0

结束为00820108

image-20211116193032725

紧随其后,便是扩展PE头,占据224字节单位

起始:00820108

结束:008201E8

image-20211116193755733

块表

image-20211116194147123

块表起始位置:008201E8

块表结束位置:008202AF

块表后的空隙

根据前面的信息来看,在块表前的结构在运行态以及废运行态出了起始地址不同之外,其他的并无不同

在内存中,块表与块的间隙大小与文件对齐FileAlignment无关,是由内存对齐所决定的,内存对齐的机制与文件对齐类似,都是需为SectionAlignment所规定的的值的整数倍。

内存对齐的属性是由_IMAGE_OPTIONAL_HEADER构造体中的成员SectionAlignment决定的,同时,此成员其实就在FileAlignment(文件对齐)成员的上面

扩展PE头在c中的定义

typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment; //<--- 内存对齐
DWORD FileAlignment; //<--- 文件对齐
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders; //<--- 决定块的起始位置
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

利用扩展PE头中的SectionAlignment值验证理论是否正确:

从扩展PE头首地址00820108开始数32(1word 2bytes 7dword =1 + 2 + 28)个字节,到820128处,拿到SectionAlignment值

00 10 00 00 即 0x1000

image-20211116195441400

即内存对齐的基数为0x1000

同时,在内存中的PE文件块结构,头地址与SizeofHeaders值无关,因为SizeOfHeaders是文件对齐(FileAlignMent)专用

在非运行态中,块的起始位置由PointerToRawData决定,且PointerToRawData必须为FileAlignment的整数倍

但在运行态中,块的起始位置则并不由PointerToRawData决定,PointerToRawData和SizeOfHeaders一样都为文件对齐专用

运行态块存储涉及点较多,此处只对第一个块的起始地址,结束地址以及大小做计算

块的起始地址

第一个块的起始地址,取决于(DOS头+PE头+块表)总大小的和进行内存对齐后的结果。

三大头加起来的地址为8202AF为止,根据SectionAlignment的值1000h计算,需要进行补齐操作,补齐至821000达到1000的整数倍

image-20211116200746213

可以看到第一个块的首地址已经发现:

首地址:821000

image-20211116200958454

块表的结束地址=块表首地址+块大小,而块大小通过块表当中的SizeOfRawData可以拿到,此处再放出c中的块表定义

#define IMAGE_SIZEOF_SHORT_NAME              8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
2
DWORD PointerToRawData; //<--- 块在磁盘文件中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

根据块表定义可以发现,SizeOfRawData在块表中可以通过块表首地址+16字节(8x1bytes + 2DWORD ) = 8201E8 + 16 = 8201F8

拿到SizeOfRawData大小 00 92 19 00 即 199200

image-20211116202106848

由此可知结束地址:821000 + 199200 = 9BA200 + E00(补0内存对齐1000)

image-20211116203313553

拿到块大小第二种方法

块的大小=块的结束地址-块的起始地址=0x59B000-0x401000=0x19A000(满足内存对齐)

image-20211116203344335

运行态时,块的大小满足内存对齐,而非先前的文件对齐

在运行态中:

  • DOS头、PE文件头、块表连续存储,中间没有空隙
  • 运行态中,块表与块之间存储可能会有空隙
  • 空隙的大小与SectionAlignMent有关,而非FileAlignMent
  • 块与块之间也有可能会因内存对齐而产生空隙

相关数据结构成员:

数据结构成员 所属数据结构 说明 SectionAlignment 扩展PE头 内存对齐

各结构的起始和结束位置:

结构 起始地址 结束地址 大小 DOS部首 00820000 008200F0 0xF0=240 PE文件头 008200F0 00820108 0xF8=244=224+40 块表 008201E8 008202AF 0x1E8=200=7*40 前三个结构 00820000 00821000 0x1000(内存对齐后) 第一个块 00821000 0x19A000 0x19A000(内存对齐后)

运行态与非运行态相同点

相同

无论是运行态,还是非运行态,DOS头、PE头、块表均为连续存储,中间没有填充间隙

第一个块表首地址都三个头的大小影响,紧随着对齐后的三个头

块与块之间也需要进行对齐

运行态与非运行态的起始地址不同

在非运行态中,块表与块之间、块与块之间的间隙由文件对齐(FileAlignment)产生

在运行态中,块表与块之间、块与块之间的间隙由内存对齐(SectionAlignment)产生

非运行态与运行态映射图

image-20211116204836412


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK