逆向工程核心原理学习 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头非常具有创意.

浙公网安备 33010602011771号