首页
社区
课程
招聘
[原创]《加密与解密》读书笔记 PE文件格式-区块和输入表
发表于: 2020-10-12 14:36 3024

[原创]《加密与解密》读书笔记 PE文件格式-区块和输入表

2020-10-12 14:36
3024

在原始数据和原始数据之间存在一个区块表,其中包含着每个块在内存中的信息,分别指向不同区块的实体

区块表是一个IMAGE_SECTION_HEADER结构数组,数组中的每个结构包含了其所关联的区块信息。区块的数目由上面介绍过的NumberOfSections字段给出。其结构定义如下

一个PE文件的部分区块表如下图

Name 区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会在载入的时候被合并,在合并之后的区块中,他们是按照“$” 后边的字符的字母顺序进行合并的。
区块名称是唯一的,在一个PE文件中不可重复,而且区块名称仅仅是一个标记,和区块内容没有任何关系,这也就意味着在把代码块命名成.data也是合法的,所以判断区块内容可以结合 IMAGE_OPTIONAL_HEADER32结构中的字段结合判断。

Virtual Size 对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。如果这个字段的值大于SizeOfRawData的值,那么相差的字节用0填充。

Virtual Address 该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个块的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。

SizeOfRawData 该区块在磁盘中所占的大小。在可执行文件中,该字段是已经被FileAlignment 处理过的长度。例如,FileAlignment为200字节,Virtual Size中指定的长度为192h字节,该块应保存为200h字节。

PointerToRawData 该区块在磁盘中的偏移。这个数值是从文件头开始算起的偏移量。如果是自装载PE文件或者COFF文件,则必须使用完全线性的方式装载文件,而不是Virtual Address字段中的RVA地址。

PointerToRelocations 这个字段在EXE中没有任何意义,在OBJ 文件中,表示本区块重定位信息的偏移值。(在OBJ 文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组)

PointerToLinenumbers 行号表在文件中的偏移值,是文件的调试信息。

NumberOfRelocations 在EXE文件中没有意义,在OBJ 文件中,是本区块在重定位表中的重定位数目

NumberOfLinenumbers 该区块在行号表中的行号数目。

Characteristics 块属性,是一组指出块属性的标志,与之前介绍过的属性字段相类似

更多定义可以查阅winnt.h头文件

在执行一个PE文件的时候,windows 并不在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制。也就是说,windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
但是要注意的是,系统装载可执行文件的方法又不完全等同于内存映射文件。当使用内存映射文件的时候,如果将磁盘文件和内存映像比较的话,可以发现不管是数据本身还是数据之间的相对位置都是完全相同的。
而我们知道,在装载可执行文件的时候,有些数据在装入前会被预处理,如重定位等,正因此,装入以后,数据之间的相对位置可能发生微妙的变化。
而我们知道,在装载可执行文件的时候,有些数据在装入前会被预处理,如重定位等,正因此,装入以后,数据之间的相对位置可能发生微妙的变化。
Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。

一般来说,一个PE文件会包含至少代码块和数据块两个区块,诶个区块都有特定的名字用于区别区块的用途,区块在映像中是按照RVA排列的。EXE和OBJ文件一些常见的区块表如下图
file
file
用户还可以自己创建和命名自己的区块,在vc++中用#pragma来声明,告诉编译器将数据插入一个区块,代码如下
#pragma data_seg("MY_DATA")
这样,vc++处理的数据都将放进一个叫MY_DATA的区块内,而不是默认的.data区块。
如果两个区块拥有相似或相同的属性,那么他们在链接时就能够合并成一个区块。合并区块的优点是节省磁盘和内存空间。合并区块没有什么硬性规定,但是不应该把.rsrc、.reloc或.pdata合并到其他区块里。因为部分输入数据是在载入内存时由Windows加载器写入的,所以对于那些只读区块,系统会临时修改那些包含输入数据的页属性为可读可写,初始化完成后恢复成原来的属性。

在PE文件头里,FileAlignment定义磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。
在PE文件头中,SectionAlignment定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。

在磁盘对齐值和内存对齐值都是1000h的程序中,文件在磁盘中的偏移地址与在内存中的便宜地址是相同的,不需要计算。但是对于这两个对齐值不同的,就需要一定的转换,这里有一种方法。
如果如果有一个VA,需要先找到其所属的区块(头部在映像里和磁盘上完全相同),然后用区块表中的VirtualAddress字段和PointerOfRawData字段相减得到RVA与FOA的差值,再用VA-ImageBase-差值即得到FOA。其原理实际上就是计算出了在内存中比在磁盘中多了多少填充,再减掉多余的填充,就得到了文件中的偏移地址。
亦或者也可以使用地址转换器,LoadPE就有这样的功能。

可执行文件使用来自其他Dll的代码或数据的动作称作是输入。当PE文件被WIndows加载器载入时,其用作之一就是定位所有被输入的函数和数据,并且让正在载入的文件可以使用那些地址,这个过程就是通过PE文件的输入表(Import Table,导入表)来完成的。表中保存的信息就是函数名和其驻留Dll名等动态链接所需要的信息。

输入函数就是被程序调用但是其执行代码不在程序中的函数,这些函数的代码在Dll中。
程序链接DLL有隐式链接和显式链接两种方式,其中隐式链接是完全由Windows装载器完成,显示链接则需要通过调用LoadLibrary和GetProcAddress来使DLL被加载。不过在隐式链接中,类似于LoadLibrary和GetProcAddress的代码在Windows装载器中执行,且要确保PE文件所需的任何DLL都被载入。
在PE文件中有一组结构,分别存储了输入的DLL名称并且指向一组函数指针,这样的一组结构叫做输入地址表(Import Adress Table,IAT),每一个被引入的API在IAT中都有位置,将被Windows装载器写入输入函数的地址。一旦模块被载入,IAT中将会包含所要调用函数的地址。
在调用输入函数时,有两种情况。假设输入函数在00402010h中,位于IAT中,则高效的情况将会这样调用
CALL DWORD PTR [00402010]
但是对于一个低效的调用则是下面这种情况的
CALL 00401164
……
:00401164:
Jmp dword ptr [00402010] ;指向USER32.LoadIconA函数
这种情况下call指令把控制权交给了一个子程序,子程序中的jmp指令跳转到IAT中,使用了而外的代码并且耗费了额外的时间。这样的原因是因为编译器无法区分输入函数调用和普通函数调用,对于每一个调用函数编译器会使用相同的CALL XXXXXXX指令,这里的xxxxx实际上是函数的实际地址,而不是一个指针,但是在程序中输入函数的实际地址并不存在,所以链接器产生了jmp来取代xxxxx。这里的jmp指令来自为输入函数准备的输入库。
如果想要使用高效的方式,可以使用declspec(dllimport)来告诉编译器这个函数来自另一个Dll此时编译器会给函数加上一个“__imp”前缀,然后把函数送给链接器,这样就可以直接把函数送入到IAT中,程序就可以直接调用了。

这里的函数修饰符除了可以让输入函数的调用效率变高,还可以更好的处理DLL中的静态变量。加入外部dll库的类中存在一个静态变量,使用_declspec(dllimport)修饰符才可以使用这个静态变量。

注意:想要dll成为外部库,需要使用_declspec(dllexport)修饰符,这样可以使dll中的代码暴露出来为其他应用程 序使用。

输入表以一个IMAGE_IMPORT_DESCRIPTOR(IID)数组开始,每一个被PE文件隐式链接的DLL 都有一个IID。在这个结构中并没有指出该结构数组的项数,但是数组最后一个单元是NULL,由此可以计算出数组的项数。输入表的结构如下

Characteristics、OriginalFirstThunk 包含指向输入名称表的RVA。输入名称表是一个IMAGE_THUNK_DATA结构的数组,数组中每一个结构都指向IMAGE_IMPORT_BY_NAME结构,拥有 Hint 和 Function name 的地址,数组以一个为0的结构结束。

TimeDateStamp 一个32位时间戳,可以忽略。

ForwarderChain 第一个被转向的API的索引,一般为0,当程序引用一个DLL中的API,而这个API又在引用其他DLL的API时使用,这样的情况很少出现,一般可以忽略。

Name DLL名字的RVA,是一个以00为结尾的ASCII字符串的RVA地址,其存储的是完整的DLL文件名,包含.DLL后缀。

FirstThunk 他指向输入地址表(IAT),IAT是一个IMAGE_THUNK_DATA结构的数组。

OriginalFirstThunk和FirstThunk相似,分别指向两个本质上相同的数组,IMAGE_THUNK_DATA结构,这些数组最常见的教法就是IAT和INT。每个IMAGE_THUNK_DATA元素对应于一个从可执行文件输入的函数。

为什么会有两个一模一样的数组呢?
OriginalFirstThunk 指向的数组也叫做叫做 hint-name table,即 HNT ,他在 PE 加载到内存中时被保留了下来且永远不会被修改。但是在 Windows 加载过 PE 到内存之后,Windows 会通过INT索引输入函数的地址,然后重写 FirstThunk 所指向的数组元素中的内容,使得数组中每个 IMAGE_THUNK_DATA 不再表示指向带有函数描述的 IMAGE_THUNK_DATA 元素,而是直接指向了函数地址。此时,FirstThunk 所指向的数组就称之为输入地址表(Import Address Table ,即经常说的 IAT)。
重写前:
file
重写后:
file

IMAGE_THUNK_DATA是一个双字,不同时刻有着不同的含义。其结构如下

当这个结构的高位为1的时候,表示该函数以序号的形式输入,这时低31位会被视作一个函数序号,当高位为0的时候,表示函数以字符串类型的函数名方式输入,这是其值是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构。对于IMAGE_IMPORT_BY_NAME,其结构如下

HInt是本函数在其所驻留的DLL中的的序号。PE装在其可以用来在DLL的输出表里快速查询函数。
Name是函数名的ASCII字符串,实际上是一个变长结构,以NULL为结尾。

typedef struct _IMAGE_SECTION_HEADER
{
        BYTE Name[IMAGE_SIZEOF_SHORT_NAME];   // 八字节长度的区块名称,如“.text”
        //IMAGE_SIZEOF_SHORT_NAME=8
        union   //区块尺寸
         {
                DWORD PhysicalAddress; // 物理地址
                DWORD VirtualSize;     // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个
        } Misc;
        DWORD VirtualAddress;          // 节区的 RVA 地址
        DWORD SizeOfRawData;           // 在文件中对齐后的尺寸
        DWORD PointerToRawData;        // 在文件中的偏移量
        DWORD PointerToRelocations;    // 在OBJ文件中使用,重定位的偏移
        DWORD PointerToLinenumbers;    // 行号表的偏移(供调试使用地)
        WORD NumberOfRelocations;      // 在OBJ文件中使用,重定位项数目
        WORD NumberOfLinenumbers;      // 行号表中行号的数目
        DWORD Characteristics;         // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
typedef struct _IMAGE_SECTION_HEADER
{
        BYTE Name[IMAGE_SIZEOF_SHORT_NAME];   // 八字节长度的区块名称,如“.text”
        //IMAGE_SIZEOF_SHORT_NAME=8
        union   //区块尺寸
         {
                DWORD PhysicalAddress; // 物理地址
                DWORD VirtualSize;     // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个
        } Misc;
        DWORD VirtualAddress;          // 节区的 RVA 地址
        DWORD SizeOfRawData;           // 在文件中对齐后的尺寸
        DWORD PointerToRawData;        // 在文件中的偏移量
        DWORD PointerToRelocations;    // 在OBJ文件中使用,重定位的偏移
        DWORD PointerToLinenumbers;    // 行号表的偏移(供调试使用地)
        WORD NumberOfRelocations;      // 在OBJ文件中使用,重定位项数目
        WORD NumberOfLinenumbers;      // 行号表中行号的数目
        DWORD Characteristics;         // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//