PE格式解析-区段表及导入表结构详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_30145355/article/details/78859214

必须亲自动手,才能了解

目录
一、区段名及其含义
二、区段类别及分布
三、区段表解析
四、导入表解析

一、区段名及其含义

.text
默认的代码区块,它的内容全是指令代码,链接器把所有目标文件的text块连接成一个大的.text块,使用Borland C++,编译器产生的代码存放在CODE的区域里
.data
默认的读/写数据块,全局变量,静态变量一般放在这个区段
.rdata
默认只读数据区块,但程序中很少用到该块中的数据,一般两种情况用到,一是MS 的链接器产生EXE文件中用于存放调试目录,二是用于存放说明字符串,如果程序的DEF文件中指定了DESCRIPTION,字符串就会出现在rdata中
.idata
包含其他外来的DLL的函数及数据信息,即输入表,将.idata区块合并成另一个区块已成为一种惯例,典型的是.rdata区块,默认的,链接器只在创建一个Release模式的可执行文件时才能将idata合并到另外一个区块中
.edata
输出表,当创建一个输出API或数据的可执行文件时,连接器会创建一个.EXP文件,这个.EXP文件包含一个.edata区块,其会被加载到可执行文件中,经常被合并到.text或.rdata 区块中
.rsrc
资源,包括模块的全部资源,如图标,菜单,位图等,这个区块是只读的,无论如何不应该把它命名为.rsrc以外的名字,也不能合并到其他的区块里
.bss
未初始化的数据,很少在用,取而代之的是执行文件的.data区块的的VirtualSize被扩展大的空间里用来装未初始化的数据.
.crt
用于C++ 运行时(CRT)所添加的数据
.tls
TLS的意思是线程局部存储器,用于支持通过_declspec(thread)声明的线程局部存储变量的数据,这包括数据的初始化值,也包括运行时所需要的额外变量
.reloc
可执行文件的基址重定位,基址重定位一般仅Dll需要的
.sdata
相对于全局指针的可被定位的 短的读写数据
.pdata
异常表,包含CPU特定的IAMGE_RUNTIME_FUNTION_ENTRY结构数组,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它.
.didat
延迟装入输入数据,在非Release模式下可以找到

二、区段类别及分布

1、Stud_PE 加载的区段示例(图一)
区段
Name:区段的名称
VirtualSize:区段在内存中的大小
VirtualOffset:区段在内存中的相对虚拟地址RVA
RawSize:区段在文件中的大小
RawOffset:区段在文件中的偏移地址
Characteristics:特征值(下图中我们可以看到.text特征值600000020)

从区段表不难发现:.text段的RVA为1000,大小为4B48,也就是说.text区段在内存中的位置是从1000~5B48。其他区段也不难得出


2、通过区段图,得出程序加载到内存后的分布以及文件在硬盘的分布(图二)
地址
程序加载到内存之后,都会分配4GB的内存地址空间(注:并不是内存大小)
在区段与区段之间并不是紧挨着的,因为内存会进行0x1000的内存对齐(不懂的可以参见该篇文章:PE格式解析-NT头与地址换算),空出来的空间进行填充0字节的数据。例如5B4B~6000的空间都是填充的0字节数据


3、如何正确识别一个区段为代码段
注:代码段的真正识别并不是根据.text名来判断,后面我们会知道,将.text修改后是不会影响到我们程序的真正运行的
方法一:通过Image_Option_Header中代码块起始RVA(BaseOfCode)中的地址
方法二:通过区段的特征值标志(属性)MEM_EXECUTE(可执行)、MEM_READ(可读),判断这个是代码段
例如 .text 区段(通过右击区段行,选择Edit Header即可查看):
text
注:并没有勾选MEN_WRITE标志,因为程序运行的一般情况下是不允许代码进行变化的。当然,我们也可以进行改变属性为可写,此时可称之为SMC(Self Modify Code 可变代码),程序在运行的时候,可能会对自身的代码进行修改,实现一些功能上的变化,比如产生偏移量的变化。当然如果实现自变形的功能,必须添加MEM_WRITE属性,否则会程序的崩溃。

三、区段表解析

1、Section Table(区段表) 中,每0x28个字节保存一个区段的信息
区段表示意图(另存图片浏览更清晰):
示意图
如下:从黑色标识的开始的每0x28字节表示一个区段的信息
区段表

内存中块大小(VirtualSize):0x4B48(大小端的转换)
内存中块RVA值(VirtualAddress):0x1000
文件中块大小(SizeOfRawSize):0x4C00
文件中块偏移(PointerToRawRelocations):0x400
块属性(Characteristics):0x60000020
格式中的块名(0x08字节):表示的一个字符串(区段的名称),可以修改!
例如(修改 .text 为 .zhangy):
zy
修改之后是依旧可以正常运行的,再用Stud_PE打开exe文件:
zy
不难发现,此时 .text 变成了 .zhangy,所以判断一个区段是不是代码段,不是根据名称来判断,而是根据区段的属性来判断的。

2、手动解析的时候如何判断一个区段表是否正确?区段表的数量
区段表的数量在上篇关于NT头中有两个字节(NumberOfSection 块数目)表示的就是区段的数量。在校验区段时,我们可以根据VA,RVA值以及文件偏移值换算的计算方式进行校验。

跳转到文件的0x400后,前面有一大片的0,这是因为要实现文件对齐而填充的0。同样,在区段与区段之间,你也会发现填充0的部分,这也是为了实现文件对齐。

四、导入表解析

1、本示例中的程序是没有输出表的(此文就只讲解输入表),往往在Dll文件(具有导出函数)中才有输出表。
导入表
前四个字节表示输入输出表的RVA值,后四个字节表示输入输出表的大小
解析出输入表的八个字节
RVA:0x000091B8
大小:0x00000050

我们接下来提到的输入表,你可以理解为程序调用的一个Dll中的一些函数所形成的列表,PE文件分别为每一个所调用的Dll都添加了一个输入表。
相反的,输出表就是所需要导出的函数所形成的表。
注:输入表中可能是系统的kernel32.dll中的API,也可能是自己书写的Dll中的函数,所以输入表中不一定是API,接下来对输入表的形容我会尽量用函数来替代,而不是API

2、将输入表的RVA转换成文件的偏移地址

  • 0x91BB对应的区段为.idata(0x9000~0x9A7D)
  • 列表内容
  • .idata对应的文件偏移位置为0x7200
    输入表的文件偏移:0x91B8-0x9000+0x7200=0x73B8(以下就不写计算过程了)

输入表

3、OS是如何解析输入表中的这些字节数据的呢?
一个输入表的平面结构示意图
XXX
注:这里讲到的输入表
输入表中,每20个字节(一个Image_Import_Directory)对应一个动态链接库Dll的调用数据
例如:我们要调用kernel32里面的的10个API,此时这10个API的数据都会保存在这20个字节中。但是是怎么保存这10个API的呢?其实它根据一个数组的指针来保存的所要调用的API(下面会讲到)

4、了解了输入表的结构,现在来分析下输入表
第一个输入表示意图(红色部分 20字节):
这里写图片描述
1)Dll名字符串的RVA值(Name):0x9452->对应的文件偏移:0x7652
此时我们就能在文件偏移0x7652的地方找到Dll的名字
这里写图片描述

2)Import Thunk Data数组的RVA值(Original First Thunk):0x92A0->对应的文件偏移:74A0
这里写图片描述
每四个字节代表了一个导入的函数,所以24个字节正好对应VCRUNTIME140D.Dll中的6个函数(24个字节后为四个字节的0,所以判断到此结束)
其实这四个字节也是一个RVA值,我们可以再看一下输入表的示意图
0x9420是上图的第一个函数RVA,0x93C0是上图的最后一个函数的RVA
这里写图片描述
0x9420的文件偏移:0x7620
这里写图片描述
此时就得到了我们调用VCRUNTIME140D.Dll中的第一个函数的名字__vcrt_GetModuleHandleW,和Stud_PE中解析的相同

0x93C0的文件偏移:0x75C0
这里写图片描述
此时就得到了我们调用VCRUNTIME140D.Dll中的最后一个函数的名字__std_type_info_destroy_list,和Stud_PE中解析的相同

综上小结:PE文件里面,通过前四个字节一个多级的数组指针,指向一个数组HNT,OS通过HNT中保存的RVA值就能找到对应API的地址

3)IAT(Import address table):导入表函数地址表(保存API实际地址的数组),每四个字节表示一个函数的地址。当我们应用程序加载起来的时候,OS的PE文件加载器就会将我们所需要导入的动态链接库(Dll)的函数(不一定是API)在内存中的实际地址填写到IAT中。

我们来看下IAT的RVA:0x9098,对应的文件偏移:0x7298
这里写图片描述
此时IAT中的这些数据并没有什么作用,主要是起到了占位的作用。因为我们要调用的函数(Dll中的函数)在内存中的实际地址是在程序运行的时候才进行填写到IAT的。也就是说,没有运行的时候,IAT这个数组中的数据并没有用,运行时OS才会把API在内存中真正的地址填写(说填写也不是很准确,应该说是修改了IAT中的数据)到IAT中,此时我们通过Import Thunk Data(数组RVA值)调用的API才是正在的API…就解释到这里吧,越说越绕

4)现在我们来验证下IAT的加载(OD加载我们的应用程序):
这里写图片描述
(不知道大家忘没有,在0x002F9098中,前面的002F表示的是程序加载到内存的基址VA,后面的9098是相对于基址的偏移,也就是相对虚拟地址RVA)
可以看到,此时RVA0x9098位置处的值并不是文件偏移0x7298的值,这些值都是在程序运行的时候进行修改的,修改成了函数在内存中真正的地址。这6个地址,就是我们调用的VCRUNTIME140D.Dll中的6个函数的真正地址!
此时我们就可以跟进第一个函数(__vcrt_GetModuleHandleW)在内存中的地址0x72A69CE0,可以看到该函数的真正实现
这里写图片描述
还有一个方法,通过OD右键->长型->地址来查看,效果图
这里写图片描述

小结下IAT的作用:如果没有IAT,或者IAT中没有保存我们所需要调用的dll中的函数时,OS是无法进行相关函数调用的,可见IAT这个数据结构是很重要的。其实我们每重启操作系统的时候,API的地址都会发生变化,但是IAT的这种运行时加载真正函数地址的行为保证了地址变化也能正常运行。
因为IAT的重要,所以往往一些壳都会在对IAT进行一些修改。

本文难免有所错误,如有问题欢迎留言

展开阅读全文

没有更多推荐了,返回首页