【逆向基础】十八、PE文件格式(三)

一、简介

文本章主要讲结构体IMAGE_DATA_DIRECTORY数组。它制定了各种数据目录的地址与大;PE装载器可以通过这些信息准确加载PE文件所需的函数,资源等;此外,数据目录表也是设置钩子,注入等逆向的理论基础。所以学习这个结构体数组对于想学逆向的小伙伴是非常重要的(ps:头文件中所有地址均为RVA);

二、结构体:IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY结构体数组表示数据目录表(Data Directory Table)。其数组实际大小由IMAGE_OPTIONAL_HEADER32结构体中的变量NumberOfRvaAndSizes决定;每个结构体都对应PE文件中的一个数据目录的地址和大小;

cpp 复制代码
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;
    DWORD Size;
}IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddress:

VirtualAddress:表示数据目录项在PE文件中的相对虚拟地址(Relative Virtual Address)。RVA是一个相对于PE文件加载到内存后的基地址的偏移量,用于定位数据在内存中的位置。

Size:

Size:表示数据目录对应表的大小。

三、结构体:IMAGE_IMPORT_DESCRIPTOR

每个IMAGE_IMPORT_DESCRIPTOR结构体占用20个字节;这些字节用于告诉操作系统当前导入dll何时被修改,去哪里调用函数,去哪找到函数名,是否被绑定等;每dll对应一个结构体,多个导入dll对应了结构体数组;PE文件中的导入表是逆向和病毒分析中比较重要的一个表,所以我们来一起学习学习;

(ps:dll相关内容可去这里瞧瞧:dll小课堂

cpp 复制代码
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;    //特征
        DWORD   OriginalFirstThunk; //输入名称表(INT)的RVA
    };
    DWORD   TimeDateStamp;          //时间戳
    DWORD   ForwarderChain;         //函数转发情况 
    DWORD   Name;                   //DLL名字的指针
    DWORD   FirstThunk;             //输入地址表(IAT)的 RVA
} IMAGE_IMPORT_DESCRIPTOR;

3.1 成员变量介绍

Characteristics/OriginalFirstThunk

Characteristics :特征值
OriginalFirstThunk :表示当前导入文件INT(Import Name Table)首地址;详情下文INT讲解;

TimeDateStamp

TimeDateStamp:表示时间戳,表示当前导入dll的被绑定时间;

ForwarderChain

ForwarderChain:表示函数转发的状态

Name

Name:表示当前文件的名称地址,执行导入函数所属的库名称,遇到0x00表示结束;;

FirstThunk

FirstThunk:表示当前导入文件IAT(Import Address Table)首地址;详情下文IAT讲解;

3.2、IAT(Import Address Table:导入函数地址表)

IAT表示导入地址表,此表用于存储从其它dll(Dynamic Link Library:动态链接库)中导入函数的实际入口地址; PE文件运行是就可以通过IAT表中的函数指针直接调用导入函数。导出地址表每个元素都存放了一个函数指针,遇到全零时表示结束;

找导入地址表过程如下

步骤一 :查找IAT表的地址;如图所示,VirtualAddress = 0x19000 (实际上0x19000RAV值);

步骤二 :查找地址所在节区范围,如图所示,在节区.idata的范围内,即PointerToRawData = 0x7000

步骤三:计算FOA(File Offset Address)的值

由于RVA = 0x19000,与导入地址表的地址偏差值为0,所以FOA = PointerToRawData = 0x7000;

步骤四:去文件找对应地址,如果所示即为导入地址表;遇到全零的地址表示结束;

如果所示即为某dll文件导入函数时的函数地址;

步骤五:PE加载器在加载PE文件时,会根据INT中的函数信息在对应的DLL中查询实际函数的地址,并更新IAT中的条目为这些函数的实际地址。也就是说IAT的初始值会在加载时被改变;

3.3、INT(Import Name Table:导入函数名称表)

数据目录数组中,单独的下标表示INT,但这是让我们更好理解PE格式的好东西,所以一起学习学习吧;它是如下结构体的数组;每一个元素值都指向一个结构体地址;

cpp 复制代码
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE*    Name;
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
3.3.1 成员变量介绍
Hint

Hint可以认为是函数在PE文件中的唯一标识ID;用两个字节表示;

Name

Name表示一个字符指针,遇到0x00表示字符结束;

找导入函数名称表过程如下:

IMAGE_DATA_DIRECTORY数组中无INT的下标,找到INT表需要通过导入表中的指针OriginalFirstThunk来确定对应导入dll中的函数名称表地址;
步骤一 :找到任意导入文件的导入表地址

步骤二 :选取任意一张导入表中的OriginalFirstThunk来确定对应导入dll中的函数名称表地址;取第一个OriginalFirstThunk = 0x00019250;根据值判断在那个节区范围内;如果说是,导入名称表的地址在节区.idata的范围内;

步骤三 :计算FOA(File Offset Address)的值

由于VA = 0x19000;PointerToRawData = 0x7000;RVA = 0x19250;

所以RVA - 0x19000 = FOA - PointerToRawData;

FOA = 19250 - 19000 + 7000;
FOA = 0x7250;

步骤四 :去文件找对应地址,如果所示即为导入函数名称表;遇到全零的地址表示结束;

步骤五 :根据INT表中每个元素的值,找到对应导入函数名称结构体位置;例如找到第一个结构体IMAGE_IMPORT_BY_NAME的地址是0x00019392;通过RVA计算FOA得到
FOA = 0x7392(步骤与前面一样,此处省略);得到对应的Hint = 0x0031;name = " __vcrt_LoadLibraryExW"

步骤六 :从步骤四开始遍历INT表,可得到当前导入dll文件中的所有导入函数及其函数ID

3.4、INT 与 IAT 的关联

INT(Import Name Table)IAT(Import Address Table)均使用数组存储dll的信息;

数组的特点如下:

1、均未指出数组大小,且都已Null表示结束(指针为null);

2、INT的数组大小应该与IAT数组大小相同;

3、IAT表的数组值会在文件加载时,根据INT的信息更新为函数的实际地址。

通过INT 输入IAT的值步骤如下:

1.读取IID的Name成员,获取库名称字符串("kernel32.dll")。

2.装载相应库。 →LoadLibrary("kernel32.dll")

3.读取IIDOriginalFirstThunk成员,获取INT地扯。

4,逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)

5·使用IMAGE-IMPORT-BY-NAME的Hint(ordinal)Name项,获取相应函数的起始地址。 →GetProcAddress("GetCurrentThreadld")

6.读取IIDFirstThunk(IAT)成员,获得IAT地址。

7.将上面获得的函数地址输入相应IAT数组值。

8,重复以上步骤4-7,直到INT结束(遇到NULL时)。

四、结构体:_IMAGE_EXPORT_DIRECTORY

IMAGE_EXPORT_DIRECTORY结构体定义了导出表的结构,它包含了DLL文件中导出的函数和变量的相关信息。导出表的主要作用是将PE文件中存在的函数引出到外部,以便其他程序或模块可以使用这些函数,实现代码的重用。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便PE加载器通过这些信息来完成动态链接的整个过程,且PE文件中仅有一个IMAGE_EXPORT_DIRECTORY结构体即可描述导出信息。其定义如下

ps:因为exe文件导出函数表为空,所以使用自己编写的dll进行讲解

cpp 复制代码
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics; 	    //标志,未用
    DWORD   TimeDateStamp; 		    //时间戳 
    WORD    MajorVersion; 		    //未用
    WORD    MinorVersion;		    //未用
    DWORD   Name;					//指向该导出表的文件字符串
    DWORD   Base;					//导出函数的起始序号
    DWORD   NumberOfFunctions;	    //所有的导出函数个数
    DWORD   NumberOfNames;		    //以函数名导出的函数个数
    DWORD   AddressOfFunctions;     // 所有导出函数地址表RVA
    DWORD   AddressOfNames;         // 函数名称地址表RVA
    DWORD   AddressOfNameOrdinals;  // 函数序号地扯表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

4.1 成员变量介绍

TimeDateStamp

TimeDateStamp表示dll文件的时间信息;值用秒来表示;

Name

Name表示库文件名称地址

NumberOfNames

NumberOfNames表示当前文件包含以函数名称方式导出的函数总数;(ps:不包括以序号方式导出的函数);

NumberOfFunctions

NumberOfFunctions表示当前文件包含以函数名称和序号两种方式导出的函数总数;即所有导出函数的总数;

AddressOfFunctions

AddressOfFunctions表示一个指向所有导出函数地址的地址表;此表包含了所有导出函数(函数名和序号的方式)的入口点地址;

AddressOfNames

AddressOfNames表示一个指向导出函数名称地址的地址表;

AddressOfNameOrdinals

AddressOfNameOrdinals表示一个指向导出函数名称序号地址的地址表;与AddressOfNames表相对应;

4.2、EAT(Export Address Table:导出函数地址表)

EAT(Export Address Table)表示导出函数地址表,对应属性中的AddressOfFunctions;实际上是一个四个字节类型的指针数组;指针指向了实际的导出函数地址(RVA:Relate Virtual Address);

4.3、ENT(Export Name Table:导出函数名称表)

AddressOfNames表示一个指向导出函数名称的地址表;此表进包含了已名称方式导出的函数名地址;不包括已序号导出的函数地址;

导出函数名称表也有类似的结构图如下;单HintName分别使用AddressOfNameOrdinalsAddressOfNames进行存储了;

cpp 复制代码
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE*    Name;
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

4.4、ENT与EAT的关联

当一个程序需要动态加载并调用某个DLL中的函数时,它首先会查找该DLL的导出表。然后根据函数名在AddressOfNames指向的列表中进行查找匹配的函数名,使用AddressOfNameOrdinals中对应的序号在AddressOfFunctions指向的列表中查找函数的入口地址。最后,程序使用这个入口地址来调用所需的函数。

找导出对应函数地址过程如下

举例:我们要找出导出函数myAdd的地址,步骤如下
步骤一找导出函数表地址 ;即找到IMAGE_DATA_DIRECTORY数组中下标为0的元素;其VirtualAddress元素对应的值即导出函数表的RVA(Relate Virtual Address);根据导出函数结构体_IMAGE_EXPORT_DIRECTORY解析对应信息;


步骤二找所有导出函数名字符串 ;属性AddressOfNames=0x17D90的值为部分函数名称地址表地址;属性NumberOfNames的值为2,表示此表有两个导出函数名;

取两个值分别对应两个函数的名称地址值RVA_1 = 0x17D90,RVA_2= 0x17D94;根据计算得出RAW_1 = 0x6B90,RAW_2 = 0x6B94;

这处地址的值即存储函数名的实际地址值RVA_1 = 0x17DA8,RVA_2 = 0x17DAE;根据计算得出RAW_1 = 0x6BA8,RAW_2 = 0x6BAE;

根据函数名地址,找到如下两个函数名字符串(遇到0x00表示结束);

(ps:此处函数索引是从0开始自增1)

步骤三找指定函数名称地址下标

例如:myAdd,就需要先找到函数在AddressOfFunctions数组中的下标ordinal;

如果我们要找myAdd的导出函数地址,则需要先找到它对应的ordinal;

属性AddressOfNameOrdinals=0x17d98的值为函数名称序号地址表地址;属性NumberOfNames的值为2,表示此表有两个导出函数名序号;

取两个值分别对应两个函数的名称序号地址RVA_1 = 0x17D98,RVA_2 = 0x17D9A;根据计算得出RAW_1 = 0x6B98,RAW_2 = 0x6B9A;

根据函数名序号地址,找到如下两个函数名序号值对应ordinals_1 = 0x0000;ordinals_2 = 0x0002

(ps:此处函数索引是从0开始自增1)

步骤四:找到所有函数地址表 ,属性NumberOfFunctions = 0x17D88的值为所有函数名称地址表地址;属性NumberOfFunctions的值为所有函数名称地址表地址;属性NumberOfNames的值为2,表示此表有两个导出函数名;

取两个值分别对应两个函数的名称地址值RVA_1 = 0x17D88,RVA_2= 0x17D8C;根据计算得出RAW_1 = 0x6B88,RAW_2 = 0x6B8C;

这处地址的值即存储函数名的实际地址值RVA_1 = 0x11127,RVA_2 = 0x11159;根据计算得出RAW_1 = 0x0527,RAW_2 = 0x0559;

根据函数名地址,找到EAT数组如下
FunctionAddress[0] = 0x000444E9;
FunctionAddress[1] = 0x000452E9;

(ps:此处函数索引是从0开始自增1)

步骤5根据寻找指定函数的下标,来确认对应函数的地址;

若我们查询myAdd函数的地址,则由表1可知函数myAdd索引为0,对应表2索引0的ordinal的值为0x0000;

去表3查找下标为0x0000的地址即为myAdd函数的地址,即0x0x000444E9;

综上所述,我们已经获取了三张表;

表1:AddressOfNames指向的以函数名称方式导出的函数名称表;大小为NumberOfNames

表2:AddressOfNameOrdinals指向的所有函数名称序号表;大小为NumberOfFunctions

表3:AddressOfFunctions指向的所有函数地址表;大小为NumberOfFunctions; 其中

FunctionAddress[0] = 0x000444E9;
FunctionAddress[1] = 0x000452E9;

五、小结

文章主要讲解了讲结构体IMAGE_DATA_DIRECTORY数组中15种数据目录中的导出表和导出表;这两种表是我们深入理解PE文件格式的关键,有助于我们想更深入的逆向分析前进;假以时日,我相信理解完所有表格的我们,也会像很多前辈一样,可以修改或者保护我们的PE文件,以达到加壳,防止逆向等效果。学海无涯,且行且珍惜呀。