一、简介
文本章主要讲结构体IMAGE_DATA_DIRECTORY
数组。它制定了各种数据目录的地址与大;PE装载器
可以通过这些信息准确加载PE
文件所需的函数,资源等;此外,数据目录表也是设置钩子,注入等逆向的理论基础。所以学习这个结构体数组对于想学逆向的小伙伴是非常重要的(ps:头文件中所有地址均为RVA
);
二、结构体:IMAGE_DATA_DIRECTORY
IMAGE_DATA_DIRECTORY
结构体数组表示数据目录表(Data Directory Table
)。其数组实际大小由IMAGE_OPTIONAL_HEADER32
结构体中的变量NumberOfRvaAndSizes决
定;每个结构体都对应PE文件中的一个数据目录的地址和大小;
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小课堂)
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
(实际上0x19000
是RAV
值);
步骤二:查找地址所在节区范围,如图所示,在节区.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
格式的好东西,所以一起学习学习吧;它是如下结构体的数组;每一个元素值都指向一个结构体地址;
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.读取IID
的OriginalFirstThunk
成员,获取INT地扯。
4,逐一读取INT
中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
。
5·使用IMAGE-IMPORT-BY-NAME的Hint(ordinal)
或Name
项,获取相应函数的起始地址。 →GetProcAddress("GetCurrentThreadld")
。
6.读取IID
的FirstThunk(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进行讲解
)
typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD Characteristics; //标志,未用DWORD TimeDateStamp; //时间戳 WORD MajorVersion; //未用WORD MinorVersion; //未用DWORD Name; //指向该导出表的文件字符串DWORD Base; //导出函数的起始序号DWORD NumberOfFunctions; //所有的导出函数个数DWORD NumberOfNames; //以函数名导出的函数个数DWORD AddressOfFunctions; // 所有导出函数地址表RVADWORD AddressOfNames; // 函数名称地址表RVADWORD 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
表示一个指向导出函数名称的地址表;此表进包含了已名称方式导出的函数名地址;不包括已序号导出的函数地址;
导出函数名称表也有类似的结构图如下;单Hint
与Name
分别使用AddressOfNameOrdinals
与AddressOfNames
进行存储了;
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文件,以达到加壳,防止逆向等效果。学海无涯,且行且珍惜呀。