学习PE文件可以用到的工具
LordPE 一款PE文件编辑工具,支持文件结构的编辑、内存转储、PE文件重建等功能
C32Asm:一款十六进制编辑器及反汇编工具
OD:Win32下强大的应用层调式工具
WinHex:强大的16进制编辑器,可以进行内存编辑,也可以进行文件编辑
PEView: PE文件查看器
RadASM:一款汇编语言的集成开发环境,支持编译和1连接
前置知识
可执行文件
可执行文件(Executable File)是指可以由操作系统直接加载执行的文件,在Windows操作系统中可执行文件就是PE文件结构,在Linux下则是ELF文件,我们这里只讨论Windows下的PE文件,要了解PE文件,首先要知道PE格式,那么什么是PE格式呢,既然是一个格式,那肯定是我们都需要遵循的定理,下面这张图就是PE文件格式的图片(来自论坛),非常大一张图片,其实PE格式就是各种结构体的结合,Windows下PE文件的各种结构体在WinNT.h这个头文件中,可以在VS中查询。
偏移
相对于某个地址的偏移量,如
A地址为0000100A
B地址为0000100F
则他们的偏移为 0000100F-0000100A =00000006(相对偏移了00000006h)
我理解的为两个地址的距离
我们可以经常看到两个概念:
文件偏移:在磁盘中两个地址的偏移
内存偏移:加载到内存中两个地址的偏移
内存映射文件和PE内存映像
内存映射文件
将硬盘上的文件不做修改地装载到内存中取。这样,文件中字节与字节之间就是顺序排列的了
解释:
在硬盘上,文件被分割成若干簇,这些簇不一定会按照文件内容顺序排列在一起,当我们访问磁盘上的文件时,需要计算机首先将不同位置的内容读取到内存。有了内存映射文件,访问就会变得更轻松和快捷,由于读取磁盘的操作集中到一起
执行,读写效率会提高很多。被一次性读取到内存的文件字节按线性排列,访问相对简单,速度也提升了不少。
PE内存映像
是指将PE文件按照一定的规则装载到内存中,装入后的整个文件头内容不会发送变化,但PE文件的某一部分如节的内容会按照字段中的
对齐方式在内存中对齐,从而使得内存中的PE映像与装载前的PE文件不同。
为什么PE内存映像不能和一般的内存映射文件一样呢?
因为PE文件是由操作性装载进内存的,其目的是为了运行。为了配合操作系统的运行,方便调度,提高运行效率,PE映像必须按照一定的格式对齐,所以内存中的PE映像和原来硬盘上的文件时不同的,当然与内存映射文件也就不同。
磁盘到内存的映射
对齐方式
数据在内存中的对齐
由于Windows操作系统对内存属性结构的设置以页为单位,所以通常情况下,节在内存中的对齐单位必须至少是一个页的大小。对32位的Windows XP操作系统来说,这个值是4KB(1000h);而对于64位操作系统来说,这个值就是8KB(2000h)
数据在文件中的对齐
为了提高磁盘利用率,通常情况下,定义的节在文件中的对齐单位要远小于内存对齐单位;通常会以一个物理扇区的大小作为对齐粒度的值,即512字节,十六进制是200h
处于节约资源考虑,操作系统允许节在内存和文件中的对齐尺度不一致。这就直接造成PE在文件中和在内存中的大小也会不一致。通常情况下,PE在内存中的尺寸要比在文件中的尺寸要大。用户可以自定义这些对齐的值。
注意:
如果内存对齐被定义为小于操作系统页的大小,则文件对齐和内存对齐的值必须一致
资源文件中资源数据的对齐
大端序和小端序
字节存储顺序主要分为大端序(Big-endian)和小端序(Little-endian),区别如下
Big-endian: 高位字节存入低地址,低位字节存入高地址
Little-endian:低位字节存入低地址,高位字节存入高地址
例如,将12345678h写入1000h开始的内存中,以大端序和小端序模式存放结果如下
一般来说,x86系列CPU都是Little-endian字节序,PowerPC通常是Big-endian字节序。
在PE结构中使用的就是小端序所以地址要从后往前看
如图读取该地址的顺序应该为:00 00 01 08
PE文件前置知识
PE的解释
PE(Portable Executeable File Format,可移植的执行体文件格式),使用该格式的目标是使连接生成的EXE能在不同的CPU工作指令下工作。
可执行文件的格式是操作系统工作方式的真实写照。Windows操作系统中可执行程序有好多种,比如COM,PIF,SCR,EXE等,这些文件的格式大部分都继承自PE。其中,EXE是最常见的PE 文件,动态链接库(大部分以dll为扩展名的文件)也是PE文件
PE文件是指32位的可执行文件,也称为PE32。64位的可执行文件称为PE+或pe32+,是PE(pe32)文件的一种扩展形式。
PE文件种类
可执行系列,主要扩展名有exe,scr
库系列,主要扩展名有dll,ocx,cpl,drv
驱动程序系列,主要扩展名有sys,vxd
对象文件系列,主要扩展名有obj
严格来说,OBJ文件之外的所有文件都是可执行的,DLL,SYS等文件虽然不能直接在Shell中运行,但是可以使用其他方式(调试器,服务等)执行
提示:根据pe正式规范,编译结果OJB文件也视为pe文件,男生ojb文件本身不能以任何形式执行,在代码逆向分析中几乎不需要关注它。
PE指纹
为了更加直观的描述我们用16进制编辑器直接将一个exe文件载入,分析其结构,首先我们需要清楚的概念是PE指纹,也就是判断一个文件是否是PE文件的依据,首先是根据文件的前两个字节是否为4D 5A,也就是’MZ’,然后看第四排四个字节指向的地址00 00 00 f8是否为50 45,也就是’PE’,满足这两个条件也就满足了PE文件的格式,简称PE指纹,在后面制作解析器的时候会通过它来判断是否为一个有效的PE文件。
PE文件格式示意图
接下来开始PE文件的正式学习
PE结构详解
PE结构的组成
标准的PE文件一般由四大部分组成:
1.DOS头
2.PE头(IMAGE_NT_HEADERS)
3.节表(多个IMAGE_SECTION_HEADERS结构)
4.节内容
详述:
DOS头的话,分为DOS MZ头 和DOS Stub
PE头(IMAGE_NT_HEADERS)包括了4字节的标识符号(Signature),20个字节的基本头信息(IMAGE_FILE_HEADER),216个字节的扩展头信息(IMAGE_OPTIONAL_HEADER32)
(即PE头=Signature+IMAGE_FILE_HEADER+IMAGE_OPTIONAL_HEADER32)
PE文件头部=DOS头+PE头 +节表(PE头和PE文件头部不是一个意思,别混淆。)
PE文件身体 = 节内容
磁盘里面的内容
DOS头结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
IMAGE_DOS_EADER STRUCT
e_magic WORD ? ;
0000h
-
EXE标志,“MZ”
e_cblp WORD ? ;
0002h
-
最后(部分页中的字节数)
c_cp WORD ? ;
0004h
-
文件中的全部和部分页数
e_crlc WORD ? ;
0006h
-
重定位表中的指针
e_cparhdr WORD ? ;
0008h
-
头部尺寸,以段落为单位
e_minalloc WORD ? ;
000ah
-
所需的最小附加段
e_maxalloc WORD ? ;
000ch
-
所需的最大附加段
e_ss WORD ? ;
000eh
-
初始的SS值(相对偏移)
e_sp WORD ? ;
0010h
-
初始的SP值
e_csum WORD ? ;
0012h
-
补码校验值
e_ip WORD ? ;
0014h
-
初始的IP值
e_cs WORD ? ;
0016h
-
初始的CS值
e_lfarlc WORD ? ;
0018h
-
重定位表的字节偏移量
e_ovmo WORD ? ;
001ah
-
覆盖号
e_res WORD
4
dup(?);
001ch
-
保留字
e_oemid WORD ? ;
0024h
-
OEM标识符
e_oeminfo WORD ? ;
0026h
-
OEM信息
e_res2 WORD
10
dup(?);
0028h
-
保留字
e_lfanew WORD ? ;
003ch
-
PE头相对于文件的偏移地址
|
DOS部分我们需要熟悉的是e_magic(4D5A=>ASCII值“MZ”,一个名叫Mark Zbikowski的开发人员在微软设计了dos,MZ就取自其名字的首字母)成员和e_lfanew(指示NT头的偏移,这里用的是小端序标识法,从后面往前的4个字节)成员,前者是标识PE指纹的一部分,后者则是寻找PE文件头的部分,除了这两个成员,其他成员全部用0填充都不会影响程序正常运行,所以我们不需要过多的对其他部分深究。
我们可以看到e_lfanew指向PE文件头,我们可以通过它来寻找PE文件头,而DOS块的部分自然就是PE文件头和DOS MZ文件头中间的部分,这部分是由链接器所写入的,可以随意进行修改,并不影响程序的运行:
注意:
注释后的偏移是基于IMAGE_DOS_HEADER头的
DOS Stub
DOS MZ 头的下面是DOS Stub。整个DOS Stub是一个字节块,其内容随着链接时使用的链接器不同而不同,PE中并没有与之对应的相关结构,所以不需要纠结。
这个结构是广义上的PE头,在标准的PE文件中其大小为456字节。它是Signature,IMAGE_FILE_HEADER,IMAGE_OPTIONAL_HEADER32这三个数据结构的组合。
该结构的详细定义如下:
1
2
3
4
|
IMAGE_NT_HEADERS STRUCT
Signature DOWRD ?;
-
PE文件标识,
"PE\0\0"
FileHeader IMAGE_FILE_HEADER <>;
0004h
-
PE标准头
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>;
0018h
-
PE扩展头
|
数据结构字段详解
+0000h,双字。PE文件标识,被定义为00004550h。也就是“P”,“E”加上两个0,0x45就是“E”,0x50就是“P”,这也是PE这个称呼的由来.如果更改其中的任何一个字节,操作系统就无法把该文件识别为正确的PE文件。通过修改这个字段,会导致PE文件在32位系统中加载失败,但由于文件的其他部分(特别是DOS头)并没有破坏,系统还是可以识别出其为DOS系统下的可执行程序,并通过调用纯DOS环境来运行DOS Stub中的程序代码。
确认操作系统中的某个PE文件携带病毒,并且开机后会被加载进内存运行,最简单的处理办法是通过Windows PE盘启动系统,在系统中找到病毒文件,使用记事本简单地修改其中任何一个字符,保存文件,重新开机启动后即可防止病毒文件被加载
注意:
此PE非彼PE,windows PE是一个操作系统,其全称为:Windows PreInstallation Environment,即Windows的预安装环境。该操作系统区别于Windows XP/2000/Vista等,可以从光盘引导。
+0004,结构。该结构指向IMAGE_FILE_HEADER,由于PE扩展自通用COFF规范,所以,该字段在官方文档中被称为标准COFF头
+0018h,结构。该结构指向IMAGE_OPTIONAL_HEADER32。在符合COFF规范的“.obj”目标文件中该部分并不存在,所以被称为OptionalHeader(“可选头”)
可选头又分为两部分,前10个字段原属于COFF,用来加载和执行一个可执行文件;后21个字段则是通过链接器追加的。作为PE扩展部分,用于描述可执行文件的一些信息,供PE加载器加载使用。
1
2
3
4
5
6
7
8
|
IMAGE_FILE_HEADER STRUCT
Machine WORD ?;
0004h
-
运行平台
NumberOfSections WORD ?;
0006h
-
PE中节的数量
TimeDateStamp DWORD ?;
0008h
-
文件创建日期和时间
PointerToSymbolTable DWORD ?;
000ch
-
指向符号表(用于调试)
NumberOfSymbols DWORD ?;
0010h
-
符号表中的符号数量
SizeOfOptionalHeader WORD ?;
0014h
-
扩展头结构的长度
Characteristics WORD ?;
0016h
-
文件属性
|
注释后的偏移是基于IMAGE_NT_HEADERS头的
+0004,单字。用来指定PE文件运行的平台。由于Windows最初被设计为可以运行在Intel,Sun,Dec,IBM等多种硬件平台上,或者能模拟这些平台的软件环境中,而不同的硬件平台其指令的机器码不相同,因此为不同平台编译的EXE是无法通用的。下面列出常见值:
+0006h,单字。文件中存在的节的总数。Windows XP中,可以有0个节,但数值不能小于1,也不能超过96。如果将该值设置为0,则操作系统装载时会提示不是有效的win32程序。如果想在PE中增加或删除节,必须变更此处的值。
另外,这个值既不能比实际内存中存在的节多,也不能比它少,否则装载时会发生错误,提示不是有效的Win32应用程序
+0008h,双字。编译器创建此文件时的时间戳。低32为存放的值是字1970年1月1日00:00时开始到创建时间为止的总秒数。
该数值可以随意修改而不会影响程序运行。所以,有的链接器在这里填入固定的值,有的则随意写入任何值,这对用户创建的文件并没有实际的意义。另外,这个时间值与操作系统文件属性可以看到的三个时间(创建时间,修改时间,访问时间)也没用任何联系
+000Ch,双字。COFF符号表的偏移。如果不存在COFF符号表,此值为0。对于映像文件来说,此值为0,因为微软已经不赞成在PE中使用COFF调试信息。
IMAGE_FILE_HEADER.NumberOfSymbols
+0010h,双字。符号表中元素的数目。由于字符串表紧跟在符号表后,所以可以利用这个值来定位字符串表。对于映像文件来说,此值为0,主要用于调试。
+0014h。单字。指定结构IMAGE_OPTIONAL_HEADER32的长度,默认情况下这个值等于00e0h;如果是64位PE文件,该结构的默认大小为00F0h。
用户可以自己定义这个值的大小,不过需要注意两点:
(1)更改完以后,需要自行将文件中IMAGE_OPTIONAL_HEADER32的大小扩充为你指定的值(一般以0补足)
(2)扩充完以后,要维持文件中的对齐特性(比如在HelloWorld.exe中,此处增加了8个字节后,一定要在后面相应删除8个字节,以保证.text节起始位置处于0400h)
+0016h,单字。文件属性标志字段,它的不同数据位定义了不同的文件属性
解释:
当位13为1时,这表示是一个DLL文件,那么系统将使用调用DLL入口函数的方式执行文件入口函数;当位13为0时,表示这是一个普通的可执行文件,系统直接跳到入口处执行。对于普通的可执行PE文件来说,这个字段的值一般是0fh,而对于DLL文件来说,这个字段的值一般是210eh。
当第0位为1时,表明此文件不包含基址重定位信息,因此必须将其加载到文件头中指定的基地址字段位置。如果进程空间此处的基地址被占用,加载器会报错。在程序运行前如果发现文件中存在可重定位信息,链接器会执行移出可执行文件中的重定位信息的操作。
当第1位为1时,表明此映像文件是合法的,可以运行。如果未设置此标志,表明出现链接器错误
当第7位为1时,表明文件是小尾方式,即内存中,最低有效位LSB位于最高有效位MSB的前面,与第15位的大尾方式(MSB在前,LSB在后)一样,都不赞成使用该标志,最好将其设置为0
当第10位为1时,如果此映像文件在可移动存储介质上,那么加载器将完全加载它并把它复制到内存交换文件中
当第11位为1时,如果此映像文件在网络上,那么加载器也将完全加载它并把它复制到内存交换文件中
当第13位为1时,表明此映像文件是动态链接库(DLL)。这样的文件总被认为是可执行文件,尽管它们并不能直接运行
可执行文件的标志设置为010fh,即第0,1,2,3,8位分别设置为1,表明该文件为可执行文件,不含重定位信息,不含符号和行号信息,文件只在32位平台运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ?;
0018h
-
魔术字
107h
=
ROM Image
10Bh
=
exe Image
MajorLinkerVersion BYTE ?;
001ah
-
链接器版本号
MinorLinkerVersion BYTE ?;
001bh
-
SizeOfCode DWORD ?;
001ch
-
所有含代码的节的总大小
SizeOfInitializedData DWORD ?;
0020h
-
所有含已初始化数据的节的总大小
SizeOfUninitializedData DWORD ?;
0024h
-
所有含未初始化数据的节的大小
AddressOfEntryPoint DWORD ?;
0028h
-
程序执行入口RVA
BaseOfCode DWORD ?;
002ch
-
代码的节的起始RVA
BaseOfData DWORD ?;
0030h
-
数据的节的起始RVA
ImageBase DWORD ?;
0034h
-
程序的建议装载地址
SectionAlignment DWORD ?;
0038h
-
内存中的节的对齐粒度
FileAlignment DWORD ?;
003ch
-
文件中的节的对齐粒度
MajorOperatingSystemVersion WORD ?;
0040h
-
操作系统版本号
MinorOperatingSystemVersion WORD ?;
0042h
-
MajorImageVersion WORD ?;
0044h
-
该PE的版本号
MinorImageVersion WORD ?;
0046h
-
MajorSubsystemVersion WORD ?;
0048h
-
所需子系统的版本号
MinorSubsystemVersion WORD ?;
004ah
-
Win32VersionValue DWORD ;
004ch
-
未用
SizeOfImage DWORD ;
0050h
-
内存中的整个PE映像尺寸
SizeOfHeaders DWORD ;
0054h
-
所有头
+
节表的大小
CheckSum DWORD ;
0058h
-
校验和
Subsystem WORD ?;
005ch
-
文件的子系统
DllCharacteristics WORD ?;
005eh
-
DLL文件特性
SizeOfStackReserve DWORD ?;
0060h
-
初始化时的栈大小
SizeOfStackCommit DWORD ?;
0064h
-
初始化时实际提交的栈大小
SizeOfHeapReserve DWORD ?;
0068h
-
初始化时保留的堆大小
SizeOfHeapCommit DWORD ?;
006ch
-
初始化时实际提交的堆大小
LoaderFlags DWORD ?;
0070h
-
与调试有关
NumberOfRvaAndSizes DWORD ?;
0074h
-
下面的数据目标结构的项目数量
DataDirectory IMAGE_DATA_DIRECTORY
16dup
(<>) ?;
0078h
-
IMAGE_OPTIONAL_HEADER32 ENDS
|
+0018h,单字。魔术字,说明文件的类型,如果为010BH,则表示该文件为PE32;如果为0107h,则表示文件为ROM映像;如果为020BH,则表示该文件为PE32+,即64位下的PE文件
+001ah,单字。这两个字段都是字节型,指定链接器版本号,对执行没有任何影响
+001h,双字。所有代码节的总和(以字节计算),该大小是基于文件对齐后的大小,而非内存对齐后的大小。稍后还会介绍一个字段SizeOfmage,它是基于内存对齐后的大小。需要注意一点:判断某个节是否包含代码的方法不是根据节的属性中是否含有IMAGE_SCN_MEM_EXECUTE标志,而是根据节的属性是否含有IMAGE_SCN_CNT_CODE标志
+0020h,双字。所有包含已经初始化的数据的节的总大小。
+0024h,双字。所有包含为初始化的数据的节的总大小。这些数据被定为未初始化,在文件中不占用空间;但在内加载到内存以后,PE加载程序应该为这些数据分配适当大小虚拟地址空间
+0028h,双字。在Windows中,可执行程序运行在虚拟地址空间中,由于4GB空间对于程序是唯一的,所以这里的虚拟空间可以简单地理解为真实的地址。该字段的值是一个RVA,它记录了启动代码距离该PE加载后的起始位置到底有多少个字节。
如果在一个可执行文件中附加了一段自己的代码,并且想让这段代码首先被执行,一般都要修改这里的值使之指向自己的代码位置。对于一般程序映像来说,它就是启动地址;对于设备驱动程序来说,它是初始化函数的地址。入口点对于DLL来说是可选的,如果不存在入口点,这个字段必须设置为0。
+002Ch,双字。代码节的起始RVA,表示映像被加载内存时代码节的开头相对于映像基址的偏移地址。一般情况下,代码节紧跟在PE头部后面,节的名称通常为“.text”
+0030h,双字。数据节的起始RVA,表示映像被加载进内存时数据节的开头相对于映像基地址的偏移地址。一般情况下,数据节位于文件末尾,节的名称通常为“.data”
+0034h,双字。该字段指出了PE映像的优先装入地址。也就是在IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint中说的程序被加载到内存后的起始VA。那么为什么要设置这个地址呢?因为链接器在产生可执行文件的时候,是对应这个地址来生成机器码的。如果操作系统也是按照这个地址加载机器码到内存中,那么指令中的许多重定位信息就不需要修改了,这样运行速度就会更快一些。
前面说过,对于EXE文件来说,每个文件使用的都是独立的虚拟地址空间,所以,优先装入的地址通常不会被其他模块占据。也就是说,EXE文件总是能按照这个地址装入,这就意味着装入后的EXE文件不需要进行重定位了
在链接的时候,可以使用参数“-base”来指定优先装入的地址,如果不确定,那么链接器默认装入EXE的地址就是0x00400000。而相对于DLL文件来说,它默认优先装入地址则是0x10000000。如果一个进程用到了多个DLL文件,其装入地址可能会发生冲突。PE加载器会调整其中的地址,使所有的DLL文件都能被正确装入。所以,不要错误地认为内存中动态链接库的基地址和其文件头字段IMAGE_OPTIONAL_HEADER32.ImageBase指定的完全一样。
你可以自己定义这个值,但取值有限制:第一,取值不能超出边界,即取的值必须在进程地址空间中;第二,该值必须是64KB的整数倍
+0038h,双字。内存中节的对齐粒度,该字段指定了节被装入内存后的对齐单位。
解释:
为什么16位汇编里取数时要从偶地址开始?(取一个字从偶地址开始,只需要一个CPU周期就可以取到;而从奇地址取一个字,则需要两个CPU周期)其实对齐和它一个道理,内存中的数据存取以页面为单位。
win32的页面大小是4KB,所以Win32 PE中节的内存对齐粒度一般都选择4KB大小。十六进制表示为01000h,
SectionAlignment必须大于或等于FileAlignment,当它小于系统页面大小时,必须保证SectionAlignment和FileAlignment
+003ch,双字。文件中节的对齐粒度。文件中的节对齐并不是提高本身代码的执行效率,同样也是为了提高文件从磁盘加载的效率。Windows XP同来组织硬盘的所有文件系统都是基于簇(分配单元)的,每个簇包含几个物理扇区。扇区是磁盘物理存取的最小单位。簇越大,硬盘存储信息的容量就越大,但存取所花费的时间也越长。通常情况下,Windows会选择使用(200h)512字节的簇大小(1个物理扇区的大小)来格式化分区,最大可以达到4KB。
+0040h,23和24标准的两个字段都为单字,共计为双字。标识操作系统的版本号,分为主版本号和次版本号两部分
+0044h,双字。本PE文件映像的版本号
+0048h,双字。运行所需要的子系统的版本号。
+004ch,双字。子系统版本的值,暂时保留未用,必须设置为0,比如将此处的值更改为696C6971h,程序运行将失败。错误如下:
+0054h,双字。所有头+节表按照文件对齐粒度对齐后的大小(即含补足的0)。在PE文件中,该部分数据是严格按照200h对齐的,如果不对齐,系统在加载时会提示出错
+0058h,双字。校验和,在大多数的PE文件中,该值是0,但在一些内核模式的驱动程序和系统DLL中,该值则是必须存在且正确的,比如kernel32.dll中PE的检验和是0011E97Eh。Windows系统目录下有一个动态链接库IMAGEHELP.DLL,它是Win32中专门用来操作PE文件的函数库,这里面的函数CheckSumMappedFile就是用来计算文件头检验和的,对于整个PE文件也有一个检验和函数MapFileAndCheckSum。该动态链接库中还包括其他一些常用的函数。
+005Ch,单字。指定使用界面的子系统,取值如下表。这个字段决定了系统如何为程序建立初始的界面,链接时使用的参数-subsystem:xxx选项指定的就是这个字段的值,如果将子系统指定为Windows命令行用户交互模式(Command User Interface,CUI),那么系统会自动为程序建立一个控制台窗口;如果指定为Windows GUI,窗口程序代码必须由用户自己建立。
MASM32的link程序的链接开关-subsystem的常见选项如下图
+005eh,单字。DLL文件属性,它是一个标志集,不是针对DLL文件,而是针对所有的PE文件的。
这个字段定义了PE文件装载时的一些特性
+0060h,双字,初始化时保留的栈大小。该字段表示为初始线程的栈而保留的虚拟内存数量,然而并不是留出的所有虚拟内存都可以用栈(真正的栈大小由下一个字段SizeOfStackCommit决定)。该字段的默认值为0x100000(1MB),如果调用API函数CreatThread时,把NULL当做传入的参数,那么创建出来的栈大小也会是1MB
IMAGE_OPTIONAL_HEADER32.SizeOfStackCommit
+0064h,双字,初始化时实际提交的栈大小。保证初始线程的栈实际占用内存空间的大小,它是被系统提交的,这些提交的栈不存在于交换文件里,而是存在于内存里。对于Microsoft的链接器来说,这个域的初始值为0x1000字节(1页),对于TLINK32,则为2页。
+0068h,双字,初始化时保留的堆大小。用来保留给初始进程堆使用的虚拟内存,这个堆的句柄可以通过调用GetProcessHeap函数获得。每一个进程至少会有一个默认的进程堆,该堆在进程启动的时候被创建,而且说进程的生命期中永远不会被删除。默认值为1MB,我们可以通过链接器的“-heap”参数指定起始的保留堆内存大小和实际提交的堆大小。
+006Ch,双字。初始化时实际提交的堆大小,在进程初始化时设定的堆所占用的内存空间,默认值为1页。
+0070h,双字。加载标志
IMAGE_OPTIONAL_HEADER32.NumberOfRvaAndSize
+0074h,双字。定义数据目录结构的数量,一般为00000010h,即16个。该值由字段SizeOfOptionalHeaders决定,实际应用中可以取2~16的值
+0078h,结构。由16个IMAGE_DATA_DIRECTORY结构线性排列而成,用于定义PE中16种不同类别的数据所在的位置和大小。以下是对这16数据的说明:
导出数据所在的节通常被命名为.edata,它包含一些可被其他EXE程序访问的符号的相关信息,比如导出函数和资源等。这些符号通常出现在DLL中,但DLL也可以包含导入符号,而且在某些EXE中也可以有导出符号。
导入数据所在的节通常被命名为.idata,它包含了PE映像中所有导入的符号。导入信息在EXE和DLL中几乎都存在
异常表数据所在的节通常被命名为.pdata。该节是由用于异常处理的函数表项组成的数组。可选文件头中的ExceptionTable(异常表)字段指向它。在将他们放进最终的映像文件之前,这些表项必须按函数地址进行排序,并且这些函数表项的描述必须符合特定的目标平台。该部分的数据主要用于基于表的异常处理,适用于除X86之外的所有CPU
资源数据所在的节通常被命名为.rsrc。该节是一个多层的二叉排序树,该树的节点指向PE中各种类型的资源,如图标,对话框,菜单等。树的深度可达231层,但是PE中经常使用的只有3层:类型层,名称层,语言代码层。
属性证书数据的作用类似PE文件的校验和或者MD5码,通过这种属性证书方式可以验证一个PE文件是否被非法修改过,为PE文件添加属性证书表可以使该PE与属性证书相关联。属性证书表是由一组连续的按八进制(从任意字节边界开始的16个连续字节)边界对齐的属性证书表项组成,每个属性证书表项指向WIN_CERTIFICATE结构。此结构可以在WinTrust.H文件中找到,结构定义如下:
1
2
3
4
5
6
|
WIN_CERTIFICATE STRUCT
dwLength DWORD ?;
0000h
wRevision WORD ?;
0004h
wCertificateType WORD ?;
0006h
bCertificate byte ?;
0008h
WIN_CERTIFICATE ENDS
|
注意:
该数据并不作为映像的一部分被映射到内存,因此,DataDirectory.Certificate_VirtualAddress字段是文件偏移,而不是RVA。
DataDirectory.Certificate_VirtualAddress字段给出了属性证书表中第一个属性证书表项在文件中的偏移,与后续的属性证书表项,可以通过当前属性证书表项的文件便宜加上WIN_CERTIFICATE.dwLength字段的值,并将结果向上舍入为8个字节的倍数来访问。后续的属性证书表项可以一直以这种方式访问,直到这些WIN_CERTIFICATE.dwLength字段(已经向上舍入为8字节的倍数)的和等于可选文件头中的DataDirectory.Certificate_isize的值。如果上述的值最后不等于isize字段的值,要么是属性证书表被破坏了,要是isize域被修改了。
基址重定位信息所处的节通常被命名为.reloc,基址重定位表包含了映像中所有需要重定位的内容。它被划分别许多块,每一块表示一个4KB页面范围内的基址重定位信息,它必须从32位边界开始。一般情况下,Windows加载器是不需要处理由链接器解析的基址重定位信息,除非该映像不能被如约地加载到IMAGE_OPTIONAL_HEADER32.ImageBase指定的位置。
调试数据所处的节通常被命名为.debug,它指向IMAGE_DEBUG_DIRECTORY结构数组。其中的每个元素都描述了PE中的一些调试信息。要获得IMAGE_DEBUG_DIRECTORY结构的数目,可以用isize字段除以IMAGE_DEBUG_DIRECTORY结构的大小
注意:
在默认情况下,调试信息并不会映射到映像到虚拟地址空间中。调试目录可以位于一个可丢弃的.debug节(如果存在)中,或者位于PE文件的其他节中,或者不任何节中。所以,它可能被加载到虚拟内存中,而大部分情况下是被丢弃的。
Global Ptr数据描述的是被存在全局指针寄存器中的一个值
线程本地存储数据所处的节,通常命名为.tls。线程本地存储(TLS)是Windows支持的一种特殊存储类别,其中的数据对象不是栈变量,是对应于运行相应代码的单个线程。因此,每个线程都可以为使用TLS定义的变量来维护一个不同有其它线程的值
当创建线程时,PE加载器通过线程环境块(TEB)的地址放入FS寄存器来传递线程的TLS数组地址,距TEB开头0x2C的位置处有一个指针指向该TLS数据。线程本地存储技术是特定于intel x86平台。
加载配置信息用于包含保留SEH技术,该技术基于x86的32位系统,它提供了一个安全的结构化异常处理程序列表,操作系统在进行异常处理时要用到这些异常处理程序
绑定导入数据的存在主要是为了优化导入信息,提高PE的加载效率。当PE文件被加载到内存时,加载器会先检查导入表,然后把需要加载的DLL载入到地址空间中。加载器还有一项比较重要的工作是根据导入信息的描述使用动态链接库里输入函数的实际地址取替换IAT表的内容,这个步骤会花去一部分时间。但是,如果程序员(或者链接器)可以完全知道函数的地址,就可以直接把数据中的元素替换为地址,这能节省想当多的时间,这种方法称为绑定。简单来说,绑定是由程序员或链接器代替Windows PE加载器完成了一部分对导入表的处理工作(在加载时)
IAT是导入地址表的英文缩写。准确地讲,它是导入表的一部分,这个双字数组里定义了所有导入函数的VA,程序可以直接通过跳转指令跳转到该VA处执行
延迟导入数据也和动态链接库调用有关,这种数据的存在是为了给“应用程序直到首次调用某个DLL中的函数或数据时才加载这个DLL(即延迟加载)”这种行为提供一种统一的访问机制
CLR数据所处的节通常被命名为.cormeta,该信息是.NET框架的一个重要组成部分,所有基于.NET框架开发的程序,其初始化部分都是通过访问这部分定义而实现的。PE加载时将通过该结构加载代码托管机制需要的所有动态链接库文件,并完成与CLR有关的一些其他操作。
数据目录项 IMAGE_DATA_DIRECTORY结构详细分析
结构定义如下:
1
2
3
|
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ?;
0000h
-
数据的起始RVA
isize DWORD ?;
0004h
-
数据块的长度
|
总的数据目录一共由16个相同的
IMAGE_DATA_DIRECTORY结构连续排列在一起组成,示意图如下:
这16个元组的数据每一项均代表PE中的某一个类型的数据,数据类型如下:
证书表条目指向属性证书表。 这些证书不会作为映像的一部分加载到内存中。 因此,此条目的第一个字段(通常是 RVA)是文件指针。
节表
pe文件中的code,data,resourse等按照属性分类存储在不同的节区。
把pe文件创建成多个节区结构的好处是:这样可以保证程序的安全性。
假如向字符data写数据时,由于某个原因导致溢出(输入超过缓冲区大小),那么其下的code指令就会被覆盖,应用程序就会被崩溃。
需要在各节区属性记录在节区头中(属性有文件/内存的起始位置、大小、访问权限等)
节表的结构如下,整体为40个字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
/
/
ASCII字符串 可自定义 只截取
8
个字节
union {
/
/
该节在没有对齐之前的真实尺寸,该值可以不准确
DWORD PhysicalAddress;
DWORD VirtualSize;
/
/
内存中节区的大小
} Misc;
DWORD VirtualAddress;
/
/
内存中的偏移地址(RVA)
DWORD SizeOfRawData;
/
/
节在文件中对齐的尺寸(磁盘文件中节区所占大小)
DWORD PointerToRawData;
/
/
节区在文件中的偏移(磁盘文件中节区起始位置)
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
/
/
节的属性
} IMAGE_SECTION_HEADER,
*
PIMAGE_SECTION_HEADER;
|
值得注意的是扩展PE头中的 FileAlignment 以及 SizeOfHeaders 这两个成员,SizeOfHeaders 表示所有的头加上节表文件对齐之后的值,对齐的大小参考的就是 FileAlignment 成员,如果所有的头加上节表的大小为320,FileAlignment 为 200,那么 SizeOfHeaders 大小就为 400,因为是根据FileAlignment 对齐的,这种对齐虽然牺牲了空间,但是可以提高程序运行效率,下图中的前面部分0x00100000就是程序在内存中对齐的大小,也就是程序运行起来时对齐的大小,0x00000400是程序在文件中的对齐大小,也就是没有运行时对齐的大小,需要清楚的是,PE程序在运行时内存中的对齐值和没有运行时的对齐值可能是截然不同的。
+0000h,8字节,该字段一共就8节,一般情况下是一个以“\0”结尾的ASCII码字符串来标识节的名称,内容可以自行定义
该名称并不遵循Ansi字符串必须以“\0”结尾的规则,如果不以“\0”结尾,系统依然会认为它是一个字符串,但会根据8个字节的长度对其进行截断处理
+0008h,双字。该字段是一个union型的数据,这是节的数据在没有对齐前的真实尺寸,步过很多PE文件该值并不准确
+000ch,双字。节区的RVA地址
+0010h,双字。节咋文件中对齐后的尺寸。一般512(200h)字节。
+0014h,双字。节区起始数据在文件中的偏移。
+0018h,双字。在“.obj”文件中使用,指向在重定位表的指针
+001ch,双字,行号表的位置(供调试用)
+0020h,单字。重定位表的个数(在OBJ文件中使用)
+0022h,单字。行号表中行号的数量
+0024h,双字。节的属性。这个字段很重要,这是节的属性标志字段,其中不同的数据为代表不同的属性,如下:
代码节的属性一般为60000020h,也就是可执行,可读和“节中包含代码”;数据节的属性一般为c0000040h,也就是可读,可读和“包含已初始化数据”;而常量节(对应源代码中的.const段)的属性为40000040h,也就是可读和“包含已初始化数据”;资源节的属性和常量节的属性一般是相同的。
节属性的定义不一定必须是这些值。比如,PE文件被压缩工具压缩以后,包含代码的节往往被同时设置成具有可执行,可读和可写属性,因为解压部分需要将解压后的代码回写到代码段中
PE文件与内存映射之间的一些转换
需要了解的概念
虚拟内存地址(VA)PE文件中的指令被装入内存后的地址
文件偏移地址(File Offset):数据在PE文件中的地址叫文件偏移地址,个人认为叫做文件地址更加准确。这是文件在磁盘上存放时相对于文件开头的偏移。
装载基址((Image Base) PE文件中的指令被装入内存后的地址。
相对虚拟地址(Relative Virtual Address,RVA)相对虚拟地址是内存地址相对于映射基址的偏移量。
VA= Image Base+ RVA
文件偏移地址与虚拟内存地址之间的换算关系可以用下面的公式来计算。文件偏移地址=虚拟内存地址(VA)-装载基址(Image Base)-节偏移=RVA-节偏移
映射:PE文件加载到内存时,文件不会原封不动的加载,而是根据节区头中定义的节区起始位置、节区大小等加载。因此磁盘文件中的PE与内存中的PE具有不同形态。
RVA to RAW
(1)查找RVA所在的节区
(2)使用简单的计算公式计算文件偏移(RAW)
根据IMAGE_SECTION_HEADER结构体,换算公式如下:
内存偏移 - 该段起始的RVA(VirtualAddress) = 文件偏移 - 该段的PointerToRawData
RAW - PointerToRawData = RVA - VirtualAddress
PAW = RVA - VirtualAddress+PointerToRawData
如上图,以.text节为例,左边400h - 0h = 400h是Roffset(.text节在文件中的偏移量),右边401000h - 400000h = 1000h是Voffset(.text节在内存中的偏移量)。
现在,对于上图应用程序,给定一个内存地址401125h,求对应的文件偏移地址。
根据上图知,该内存地址在.text节,则根据文件偏移的计算方法有:
相对内存偏移(RVA) = 401125h - 400000h = 1125h
.text节偏移 = 1000h - 400h = c00h
文件偏移 = 1125h - c00h = 525h
更直观一点,就好比2个人比赛跑步,跑了一样的距离125h,但起跑线不同(一个从左边400h开始跑,另一个从右边1000h开始跑)。节偏移就是起跑线的差距。