本篇文章学习PE导入导出表、重定位表以及函数转发的概念。本人水平有限,如有不足的地方,欢迎指出。(文章后面有一段长空白,不是内容缺失,我也不知道为啥会多出空白)
导出表的作用就不赘述了,直接贴chatgpt的回答(狗头)


由于一般exe都没有导出表,我们随便找个dll
记得之前的文章讲过IMAGE_OPTIONAL_HEADER里的DataDirectory字段保存的是PE使用的各种表的信息,DataDirectory的结构如下
我们可以在010中找到对应的导出表项

说明导出表的RAV是16500H,大小是834H,我们还可以看到导出表的结构名称是IMAGE_DIRECTORY_ENTRY_EXPORT,其结构如下
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 未使用
WORD MinorVersion; // 未使用
DWORD Name; // 指向该导出表的文件名字符串RVA
DWORD Base; // 导出函数的起始序号
DWORD NumberOfFunctions; // 所有导出函数的个数
DWORD NumberOfNames; // 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表RVA
DWORD AddressOfNames; // 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
由于保存的是RAV,我们肯定不能直接到文件16500H处找导出表,需要将FOA和RAV转化,很麻烦,有两种方便找到导出表的方法。第一种是直接在内存中找导出表,第二种是利用CFF Explorer。先说第一种
我们可以在x64dbg中查看dll的进程布局(x64dbg可以手动装载dll),但是dll不是exe,不能单独调试,我们需要先选一个exe进行调试

然后在符号界面右键点击注入模块,选择你要查看的dll

就可以在内存中找到了
第二种方法是使用CFF Explorer,CFF Explorer会把各种表解析出来

其中的Offset是FOA,这样就可以直接静态找到导出表了,比第一种方法方便
根据上图,IMAGE_EXPORT_DIRECTORY的文件偏移量是15900H

我们找到name字段,其偏移量是0x0c

不过这个值是一个RVA,看来还是得在内存中查找

我们找到dll的基址是00007FF9C1EF0000H,加上000167EEH,应该是00007FF9C1F067EEH

说明该导出表的名称是VCRUNTIME140.dll
接着看Base、NumberOfFunctions、NumberOfNames,调用函数可以通过函数名称也可以通过函数序号,所以导出函数也会有名称和序号

说明导出函数有0x47个,并且全部都以名称导出
最后看AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals
AddressOfFunctions很好理解,函数不可能保存在导出表中,所以这里保存的是一个RVA,指向的是每个元素大小为4字节的数组,元素个数由NumberOfFunctions提供,每项元素为导出函数地址的RVA,是的没看错,指向的仍然是RVA,相当于是间接在间接,我们在内存空间中看看

首先AddressOfFunctions的值为0x16528

找到0x00007FF9C1F06528,之后每四个字节为一个RVA,共0x47个
再来看AddressOfNames,也是指向元素大小为4字节的数组,元素个数由 NumberOfNames提供,也是每项元素都是RVA,指向真正的函数名字符串

AddressOfNames的偏移量是0x16644

找到对应地址,同理,每四个一组,每组都是一个RVA,我们随便找一组吧,就找第一个,0x000167ff

我们找到的函数名与工具解析的是一致的

其实后面一大串都是函数名,AddressOfNameOrdinals,该成员相当于指向元素大小为2字节的数组,元素个数由 NumberOfNames提供,元素值加上Base为函数导出序号,不再详细分析
接下来需要知道这三个字段之间的联系

可以很直观的看到,AddressOfNameOrdinals和AddressOfName其实是按数组下标一一对应的,AddressOfNameOrdinals的值则是对应AddressOfFunctions的下标,那么就有如下两句话
1.通过函数名称调用函数时,先遍历导出函数名称表中的数组,如果函数名称表中查找到对应函数名,将函数名称表中索引值作为下标去函数序号表中查找对应值,然后将函数序号表中的值作为函数地址表中的下标即可得到导出函数地址
2.通过函数序号调用函数时,先用函数序号减去Base(函数序号起始值),再将得到的值作为函数地址表中的下标即可得到导出函数地址
根据上图我们知道,函数_CreateFrameInfo在AddressOfName中的下标是0,那么对应AddressOfNameOrdinals中下标为0的值

对应的值是0,那我们就找AddressOfFunctions下标为0的元素

说明_CreateFrameInfo函数的RVA是0x0001030

与工具解析是一致的,看来三个字段的对应关系没有问题,顺带提一嘴,导出表一般位于 .edata 节(export data 的缩写),也可以在.rdata中
导入表会比导出表复杂一些,解析导入表可以知道程序依赖的函数,能从一定程度上了解程序是否恶意,在对抗杀软时混淆导入表也是常用的方法,所以相对来说我们会更加关注导入表的情况
导入几个模块就有几张导入表,每个表记录该模块的信息,每个导入表的大小是20个字节,从起始位置划分每20字节一组,直到出现一组20字节全部为0即代表结束,其实只要Name字段是0就满足结束条件了
导入表大部分PE文件都有,可以选择Project3.exe来调试

RVA是3AF4H,大小是0xB4,确实有多张导入表,导入表的名称是IMAGE_IMPORT_DESCRIPTOR,结构如下
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名称表(INT)的地址RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳多数情况可忽略.如果是0则表明IAT没有绑定,如果是-1(0xFFFFFFFF),则表示IAT为绑定导入
DWORD ForwarderChain; //链表的前一个结构
DWORD Name; //导入DLL文件名的地址RVA
DWORD FirstThunk; //导入地址表(IAT)的地址RVA
} IMAGE_IMPORT_DESCRIPTOR;
DUMMYUNIONNAME一般都是表示OriginalFirstThunk,ForwarderChain在早期的绑定导入中使用,用于表示当前 DLL 的导出是否是被转发到其他 DLL 的函数,后面会提到,ForwarderChain基本不用
我们先找到导入表

每20个字节为一张导入表,直到全为0的部分

共8张表

与工具解析的是一样的,找到其中一张表看看name字段


仍然是一致的,其它字段涉及IAT、INT,结构如下
typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // 如果是转发,指向转发字符串
ULONGLONG Function; // IAT 表中被填充后的函数地址
ULONGLONG Ordinal; // 按序号导入时,高位为1,低位为序号
ULONGLONG AddressOfData; // 指向 IMAGE_IMPORT_BY_NAME(INT使用)
} u1;
[培训]科锐逆向工程师培训第53期2025年7月8日开班!
最后于 2025-5-25 14:28
被kanxue编辑
,原因: 修正格式