逆向工程核心原理学习 PE文件格式

介绍

PE文件是Windows系统下的可执行文件格式。

PE文件格式

打开记事本进行简单说明。

学习PE结构就是学习PE头的结构体。

基本结构

从dos头到节区头是pe头部分,其下的节区合称PE体。文件中使用偏移(offset),内存中使用VA来表示位置。文件加载到内存时,情况就会发生变化。

各节区头定义了各节区在文件或内存中的大小,位置,属性等。

PE头与各节区的尾部存在一个区域,称为NULL填充。计算机中,为了提高处理文件、内存、网络包的效率,使用最小基本单位这一概念,PE文件中也类似。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数位置上,空白区域将用NULL填充。

VR&RVA

VA指的是进程虚拟内存的绝对地址,RVA指从某个基准位置开始的相对地址。VA与RVA满足下面的换算关系。

RAV + ImageBass = AV

PE头内部信息大多以RVA形式存在。原因在于,PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件(DLL)。此时必须通过重定位将其加载到其他空白位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来定位信息,即使发生了重定位,只要相对于基准的相对地址没发生变化,就能正常访问到指定信息。

PE头

pe头由许多结构体组成。

dos头

微软创建pe文件格式时,人们正广泛使用dos文件,所以微软充分考虑了pe文件对dos文件的兼容性。其结果是在pe头的最前面添加了一个IMAGE_DOS_HEADERIMAGE_DOS_HEADER结构体,用来扩展已有的dos exe头。

该结构体大小为40个字节,必须要知道的两个重要成员:

e_magic与e_lfanew。

e_magic:dos签名(4d5a)MZ

e_lfanew:指示NT头的偏移

所有PE文件在开始部分都有dos签名(mz).e_lfanew指向NT头所在的位置。

DOS存根

dos存根在dos头下,是个可选项,且大小不固定。dos存根由代码与数据混合而成。

文件偏移40-4d区域为16位的汇编指令。32位windows os中不会运行该命令。在dos环境中运行该文件,或者使用dos调试器运行它,可使其执行该代码。

该代码非常简单,在画面中输出字符串“This program cannot be run in dos mode”后就退出。

NT头

IMGAE_NT)HEADER结构体由3个成员组成,第一个成员为签名结构体,其值为PE00,另外两个成员分别是文件头与可选头结构体:

NT头:文件头

文件头是表现文件大致属性的IMAGE_FILE_HEADER结构体

有以下四种重要成员

1.Machine

每个cpu都拥有唯一的machine码,兼容32位intel x86芯片的machine码为14c。以下是定义在winnt.h文件中的machine码。

numberofSections

前面提到过,pe文件把代码、数据、资源等依据属性分类到各节区中存储。

用来值出文件中存在的节区的数量。该值一定要大于0,且当定义的节区数量与实际节区不同时,将发生运行错误。

SizeofOptionHeader

用来指出IMAGE_OPTIONAL_HEADER32结构体的长度。IMAGE_OPTIONAL_HEADER32结构体由c语言编写而成,故其大小以及确定。但是windows的PE装载器需要查看IMAGE_FILE_HEADER的sizeofOptionalHeader值,从而识别出IMAGE_OPTIONAL_HEADER32结构体的大小。

Characteristics

该字段用于标识文件的属性,文件是否是可运行的形态,是否为dll文件的信息,以bit OR形式组合起来。

另外,PE文件中Characteristics的值有可能不是002h吗,是的,确实存在这种可能。比如类似*.obj的object文件及resource dll文件等。l

最后讲一下IMAGE_FILE_HEADER的timedatestamp成员。该成员的值不影响文件运行,用来记录编译器创建此文件的日期。在hex中查看IMAGE_FILE_HEADER

 

以上就是文件头的结构体对应的内容

NT头:可选头

IMAGE_OPTIONAL_HEADER32是PE结构体中最大的

在IMAGE_OPTIONAL_HEADER32结构体中需要关注以下成员

#1.Magic

为IMAGE_OPTIONAL_HEADER32结构体时,Magic码为10b;weiIMAGE_OPTIONAL_HEADER64结构体时,Magic码为20b

#2.AddressOfEntryPoint

指出该程序最先执行的代码起始地址,相当重要

#3.ImageBase

进程虚拟内存的范围是0-ffffffff(32位系统).PE文件被加载到如此大的内存中时,ImageBase指出文件的优先装入地址.exe,dll文件被装载到用户内存的0-7fffffff中,sys文件被载入内核的80000000-ffffffff.一般而言exe文件的ImageBase为400000,dll的ImageBase为10000000,执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把eip寄存器的值设置为ImageBase+AddressOfEntryPoint

#4.SectionAlignment,FileAlignment

PE文件的Body部分划分为若干节区,这些节区存储着不同类别的数据.FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAliignment指定了节区在内存中的最小单位,FileAlignment与SectionAlignment的值可能相同,也可能不同),磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment值的整数倍.

#5.SizeofImage

加载PE文件到内存时,SizeofImage指定了PE Image在虚拟内存中所占空间的大小.一般而言,文件本身的大小与加载到内存中的大小是不相同的.

#6.SizeofHeader

SizeofHeader用来指出整个PE头的大小.该值也必须是FileAlignment的整数倍.第一节区所在位置与SizeofHeader距文件开始偏移量相同.

#7.Subsystem

该值用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe,*.dll).Subsystem

#8.NumberOfRvaAndSizes

用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体中最后一个成员)数组的个数.虽然结构体定义中明确指出了数组个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16),但是PE装载器通过查看NumberOfRvaAndSizes值来识别数组大小,换言之,数组大小也可能不是16

#9.DataDirectory

是由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都有被定义的值.

使用hex查看这个结构体:

节区头

节区头中定义了各节区属性,每个节区可以设置不同的特性,访问权限等.

IMAGE_SECTION_HEADER

节区头是有IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区.

项目 含义
VirtualSize 内存中节区所占大小
VirtualAddress 内存中节区起始地址(RVA)
SizeOfRawData 磁盘文件中节区所占大小
PointerToRawData 磁盘文件中节区起始位置
Characteristics 节区属性 

 

RVA to RAW

pe文件加载到内存时,每个节区都要准确完成内存地址与文件偏移之间的映射.

(1) 查找RVA所在节区

(2)使用简单的公式计算文件偏移(RAW)

也就是偏移量相同原理

IAT

简而言之:IAT就是一种表格,用来记录程序正在使用那些库中的那些函数

DLL

动态链接库,加载dll的方式实际只有两种:一种是"显式链接",程序使用dll时加载,使用完毕后释放内存;另一种是"隐式链接"程序开始时即一同加载dll,程序终止时再释放内存.IAT提供的机制与隐式链接有关.

调用CreateFileW()函数时并非直接调用,而是通过获取01001104地址处的值来实现.

地址01001104是notepa.exe中text节区的内存区域.地址的值为7c8107f0,是CreateFileW函数的地址.

那么为啥不直接调用而是间接调用呢?

事实上,程序开发者并不知道程序要运行在哪种windows下.不同的环境,kernel32.dll的版本各不相同,CerateFileW函数的位置也不相同.为了确保在所有环境中都能正常调用CreateFileW函数,编译器准备了保存CreateFileW函数实际地址的位置,并记下了call 01001104的指令,在执行文件时,PE装载器将CreateFileW函数的地址写到01001104位置.

另一个原因在于dll重定位.dll的IMageBase值一般为10000000.比如某个程序使用a.dll与b.dll时,PE装载器先把a.dll装载到内存10000000,然后尝试将b.dll装载进去.但是由于已经被占用了,就只能装载到其他位置了.

这就是所谓的dll重定位,它使我们无法对实际地址硬编码.另一个原因在于,PE头中表示地址时不使用VA,而是RVA.

IMAGE_IMPORT_DESCRIPTOR

IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入那些库文件.

IMPORT:导入,向库提供服务

Export:导出,从库向其他PE文件提供服务.

执行一个普通程序时往往需要导入多个库,导入多少库就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成了数组,且结构体数组最后以NULL结构体结束.

IMAGE_IMPORT_DESCRIPTOR

项目 含义
OrignalFirstThunk NT的地址
Name 库名称字符串的地址
FirstThunk IAT的地址

 

图中,INT与IAT的各元素同时指向相同地址,但也有很多情况下他们不是一致的.

使用notepad.exe练习

IMAGE_IMPORT_DESXRIPTOR结构体不在PE头而在PE体中,但查找其位置信息在PE头中,IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址,也被称为IMPORT Directory Table

IMAGE_OPTIONAL_HEADER32.DataDirectory[1]结构体的值如图所示:

RVA是7604,故文件偏移=7604-1000(VirutalAddress)+400 = 6a04

查看6A04

下面看一下第一个结构体元素的各个成员

1.库名称(name):name是一个字符串指针,它指向导入函数所属的库文件名称

查看6eac

可以看到comdlg32.dll

2.Original FirsThunk-INT

INT是一个包含导入函数信息的结构体指针数组,只有获取了这些信息,才能在加载到进程内存的库中准确求得相应函数的起始地址.

查看6d90

由地址数组形式组成(数组尾部以NULL结束).每个地址分别指向IMAGE_IMPORT_BY_NAME结构体,进行第一个结构体地址7a7a

RAW =  7a7a-1000+400=6e7a

3.IMAGE_IMPORT_BY_NAME 

RVA :7a7a,即为RAW:6e7a.

文件偏移6e7a最初的两个字节(000f)为Ordinal,是库中函数的固有编号.Ordinal的后面为函数名称字符串PageSetupDlgW.

4.FirstThunk -IAT(Import Address Table)

RAW= 12c4-1000+400 = 6C4

IAT 的第一个元素被硬编码为76324906,该值无实际意义,notepad.exe文件加载到内存时,准确的地址会取代该值.

下面用ollydbg查看IAT

notepad.exe的ImageBase值为01000000.所以comdlg32.dll!Page Setup DlgW函数的起始位置.

以上是对IAT的基本讲解,都是一些初学者不易理解的概念.

ETA

windows操作系统中,"库"是为了方便其他程序调用而集中包含相关函数的文件(dll/sys).win32PAI是最具代表性的库,其中的kernel32.dll文件被称为最核心的库文件.

EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数.也就是说,只有通过EAT才能准确求得从相应库中导出函数的起始地址.与前面讲解的IAT一样,PE文件内的特定结构(IMAGE_EXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY结构体.

可以在PE文件的PE头中查到IMAGE_IMPORT_DESCRIPTOR结构体的位置.IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress值即是IMAGE_EXPORT_DIRECTORY结构体数组的起始地址.

IMAGE_EXPORT_DIRECTORY

结构体中的重要成员

项目 含义
NumberofFunctions 实际Export函数的个数
NumberOfname Export函数中具名的函数个数
AddressOfFunctions Export函数地址数组
AddressOfname 函数名称地址数组
AddressOfNameOrdinal Ordinal地址数组

从库中获得函数地址的API为GetProcAddress函数.该API引用EAT来获取指定PAI的地址.

GetProcAddressAPI拥有函数名称,下面讲解一下

(1).利用AddressOfname成员转到"函数名称数组"

(2)通过比较字符串,查找指定函数名称

(3)利用AddressOfnameordinal成员,转到Ordinal数组

(4)在ordinal数组中通过name_index查找到相应的ordinal值.

(5)利用AddressOfFunctions成员转到"函数地址数组"EAT

(6)在函数地址数组中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址

使用kernel32.dll练习

1.函数名称数组

AddrsssOfName成员的值为RVA=353c,即RAW=293c.使用Hex Editor查看该地址

2.查找指定函数名称

第二个值为4bb3,raw  = 4bb3-1000+400 = 3fb3,查看字符串值.

可以看到AddAtomW.

3.Ordinal数组

下面查找AddAtomW函数的Ordinal值.去AddressOfNameordinal数组的地址3824查看.

4.ordinal

这里的索引为1 的地方ordinal[1] = 0005

5.函数地址数组_EAT

最后查找Add Atom W 的实际地址.AddressofFunctions成员的地址为1a54,查看

这里的索引为5,也就是第6个地址就是函数的地址,000154ec,kernel.dll的ImageBase为7c800000,因此函数的地址为,7c8154ec.验证一下

暂时有问题........

高级PE

PEView.exe

PE头按不同结构体分类组织起来,非常方便查看,也能非常容易地在RVA与文件偏移间转换.

Patched PE

事实上,只要文件符合PE规范就是PE文件.Patched PE指的就是这样的PE文件,这些PE文件仍然符合PE规范,但附带的PE头非常具有创意.

 

 

posted @ 2024-06-20 15:30  robot__i  阅读(84)  评论(0)    收藏  举报