宝塔服务器面板,一键全能部署及管理,送你10850元礼包,点我领取

PE文件格式介绍

PE文件格式总结……………………………..
一、引入函数表与引出函数表
1.
引入函数表:

对于一个程序,又用到很多EXE/DLL,把使用他们的信息写成一个数组,成员就是每个EXE/DLL和里面使用的function函数信息。

2.
引出函数表:

对于一个DLL,为了方便别的函数调用每一个EXE/DLL内的function,,做了一个引出函数表,里面有相关信息。

3.
引入函数表和引出函数表的联系:

PE文件运行之前,加载器必须把需要调用分布在DLL里的function加载进来。首先进入引入表,依次处理每个数组:根据数组信息里面的DLL地址,和其中用到的function地址,查找对应DLL的引出表,确定最终位置。最后完成DLL加载。

二、加载执行PE文件的大概步骤(不精确,但可以这样思维):
1.
验证是不是有效的PE文件,主要看DOS MZ HEADER里标志是否为“MZ”,如果是(表示是DOS程序),则由e_lfanew的值跳到
PE HAEDER
2.
PE HAEDER中,察看signature 是否是PE/0/0,如果是(表明是有效的PE文件)

3.
PE HAEDER中,察看file header 中的 machine 是否准许在本机运行

4.
PE HAEDER中,根据characteristic 中的值,来控制下面操作。跳到optionalheader

5.
PE文件分配堆栈,同时确定PE文件在内存中的映像,程序入口点,各RVA

6.
一次处理数组datadirectory 内每一项,包括通过引入表,把DLL加载进来

7.
跳到OEP,执行

 

 

具体介绍:

一、一些概念:

1PE文件的结构图

DOS MZ Header

DOS Stub

PE Header

Section Table

Section

2)、虚拟地址的概念:由段地址:偏移量的形式表示32位的系统中可寻址范围为232=4G;(进程实际用到的只有2GOS用了2G OS为我们的进程分配了4G的线性地址空间,使得我们可以直接使用偏移量来直接表示而不需要段地址;至于线性地址跟实际的物理内存地址的转换就是由OS来管理的了。

3)、相对虚拟地址的概念:Relative VirtualAddressRVA),相对上面系统分配给进程的基址(虚拟地址)的偏移量,PE文件里许多都是以RVA来表示的

4)、Section的概念:有共同属性的数据/代码被分配到同一节当中,其中的Section名称只具有标识作用;以下是Section的名称和对应的作用(属性)

Section

属性

.arch

最初的构建信息Alpha Architecture Information)

.bss

未经初始化的数据

.CRT

C运行期只读数据

.data

已经初始化的数据

.debug

调试信息

.didata

延迟输入文件名表

.edata

导出文件名表

.idata

导入文件名表

.pdata

异常信息Exception Information)

.rdata

只读的初始化数据

.reloc

重定位表信息

.rsrc

资源

.text

.exe.dll文件的可执行代码

.tls

线程的本地存储器

.xdata

异常处理表

二、深入了解:

1)DOS MZ Header DOS Stub

DOS Stub是一段可执行的代码,通常是由编译器生成的一段简单的中断21H服务9来显示字符串“This program cannot run in DOS mode

如果在DOS中执行PE文件则会显示DOS Stub中的“This program cannot run in DOS mode,windows中执行时,则加载器会跟据DOS MZ Header中的e_lfnew跳转到PE Header

2)、PE Header

PE Header是一个IMAGE_NT_HEADERS结构,此结构包含在WINNT.H中;

以下是此结构图:

IMAGE_NT_HEADERS结构

字段名

作用

DWORD Signature

对于PE格式文件这个应该为一个ASCII码为PE/0/0的值

FileHeader结构

字段名

作用

Machine

该程序运行所要求的CPU;例如需要CPUIntel,则此值为14Ch

NumberOfSections

文件的节数目

TimeDateStamp

文件的创建时间和日期

PointerToSymbolTable

表示COFF文件的符号表的偏移量,只在调试时有用

NumberOfSymbols

COFF文件符号表中的符号数

SizeOfOptionalHeader

紧随本结构后OptionalHeader结构的大小

Characteristics

关于文件信息的标志,比如该文件是EXE,还是DLL

对我们来说有用的就MachineNumberOfSectionsCharacteristics

OptionHeader结构

字段名

作用

AdressOfEntryPoint

入口地址:PE装载器运行的第一个指令的RVA

ImageBase

PE文件的优先装载地址;也就是说在虚拟地址空间中,如果此地址没被其它模块占领;那么就把此PE文件装载到此地址中

SectionAlignment

内存中节对齐的粒度。例如,如果该值是4096 1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h402000h之间还有很多空间没被使用。

FileAlignment

表示文件中节对齐的粒度。同上面不一样这是在文件中,而上面则是在内存中。例如,如果该值是200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移量5121024之间还有很多空间没被使用/定义。

MajorSubSystemVersion

MinorSubSystemVersion

win32子系统版本。若PE文件是专门为Win32设计的,该子系统版本必定是4.0否则对话框不会有3维立体感。

MajorOperatingSystemVersion

MinorOperatingSystemVersion

 

表示使用该可执行文件所要求的操作系统最小版本。该域含义有点模棱两可,因为subsystem域(后面的一些域)页体现类似的目的。在大多数Win32文件中,该域为版本1.0

 

MajorImageVersion MinorImageVersion

表示一个用户自定义域。该域允许你具有一个EXE或一个DLL的不同版本。可用连接器的/VERSION开关来置该域的值,如LINK/VERSION2.0 myobj.obj

 

SizeOfImage

内存中整个PE文件映射的大小,它是所有节和头经过对齐后的大小

SizeOfHeader

所有头+节表的大小,也就是第一个节的文件偏移量

Subsystem

用来标识PE属于哪个子系统;对于大多数的WIN32程序来说,此值为Windows GUI Windows CUI控制台)

SizeOfStackReserve

表示为初始线程栈保留的虚拟内存量。然而,这些内存不是都要交付的(见后一个域)。该域默认为0x1000001MB)。如果你对CreateThread()指定一个0作为栈的大小,结果线程仍是得到一个域默认值相同的栈

 

SizeOfStackCommit

表示为初始线程栈首先交付的内存量。在微软连接器中,该域默认值是0x1000字节(1页),而TLINK默认为0x2000字节(2页)

DataDirectory

成员

作用

0

Export symbols

1

Import symbols

2

Resources

3

Exception

4

Security

5

Base relocation

6

Debug

7

Copyright string

8

Unknown

9

Thread local storage TLS)

10

Load configuration

11

Bound Import

12

Import Address Table IAT)

13

Delay Import

14

COM descriptor

15

 

此字段为IMAGE_DATA_DIRECTORY结构数组,16个成员;此结构为一个RVA和一个大小组成;

其它的不介绍了

 

 

3)Section Table

节表是一个结构数组,数组的每个结构对应PE文件的一个节;每一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等,都保存在这个结构里。

此结构如下

struct  IMAGE_SECTION_HEADER

{

BYTE Name[8];
union

{
DWORD PhysicalAddress;//
物理地址

DWORD VirtualSize;//
真实长度,这两个值是一个联合结构,可以使用其中的任何一个,
//
一般是节的数据大小
} Misc;
DWORD VirtualAddress;//RVA
DWORD SizeOfRawData;//
物理长度
DWORD PointerToRawData;//
节基于文件的偏移量
DWORD PointerToRelocations;//
重定位的偏移
DWORD PointerToLinenumbers;//
行号表的偏移
WORD NumberOfRelocations;//
重定位项数目
WORD NumberOfLinenumbers;//
行号表的数目
DWORD Characteristics;//
节属性 如可读,可写,可执行等
};

重要的是以下字段

字段名

作用

Name

这儿的节名长不超过8字节。记住节名仅仅是个标记而已,我们选择任何名字甚至空着也行,注意这里不用null结束。命名不是一个ASCIIZ字符串,所以不用null结尾。

VirtualAddress

本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h

SizeOfRawData

经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。

PointerToRawData

这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。

Characteristics

包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。

 

4)Import TableExport Table

在知道两者作用前,我们先介绍一下PE文件如何调用其它模块的函数的如(USER32.dllGetMessage);编译器并不是直接Call其它模块的函数的,而是先Call到一个存储JMP DWORD PTR [**]指令的地址,然后JMP DWORD PTR [**]跳到一个地址上去,其地址存在于.idata节中;这个地址存储着其它模块的函数的地址。现在再来说一下Import Table的作用,它的作用是存储该程序调用了哪些模块哪些函数,而Export Table的作用是存储了对应模块的函数的地址。

Import Table实际是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,每个结构包含引入的DLL信息,该数组以一个全0的结构结尾;

Struct IMAGE_IMPORT_DESCRIPTOR STRUCT

{
  union

{
    DWORD Characteristics;
    DWORD OriginalFirstThunk ; //
指向一个IMAGE_THUNK_DATA数组RVA

//每个IMAGE_THUNK_DATA对应于一个输入函数

//如果函数以名称输入,则IMAGE_THUNK_DATA对应于// 一个IMAGE_IMPORT_BY_NAME结构
 }
DWORD  TimeDateStamp;
DWORD  ForwarderChain;
DWORD Name1;
DWORD FirstThunk ;

};

Import TableExport Table的位置都由DataDirectory指定;

Export Table是一个IMAGE_EXPORT_DIRECTORY结构数组,该结构共有11个成员;主要成员如下表

字段名

作用

nName

模块的真实名称。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。

nBase

基数,加上序数就是函数地址数组的索引值了。

NumberOfFunctions

模块引出的函数/符号总数。

NumberOfNames

通过名字引出的函数/符号数目。该值不是模块引出的函数/符号总数,这是由上面的NumberOfFunctions给出。本域可以为0,表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号,那么数据目录中引出表的RVA0

AddressOfFunctions

模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。简言之,模块中所有函数的RVAs都保存在一个数组里,本域就指向这个数组的首地址。

AddressOfNames

类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA

AddressOfNameOrdinals

RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。