通常来讲exe文件只有导入表而没有导出表,而dll文件既有导入表也有导出表
代码重用机制提供了重用代码的动态链接库, 它会向调用者说明库里的哪些函数是可以被别人使用的, 而这些说明的信息便组成了导出表
简单来说,PE文件提供一些函数给其他PE文件调用,这些函数都记录在导出表里面
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;
导出表的地址和大小是由PE扩展头的最后一个成员NumberOfRvaAndSizes
所决定的,这个成员是个结构体数组,结构体类型为_IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //导出表的起始地址
DWORD Size; //导出表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
将程序拖入CFFE工具查看,特别要注意的是这个导出表的地址是RVA,需要转换的FOA才能在文件状态下寻找到导出表的起始地址
这里就不转换成FOA了,工具已经为我们转换好了。切换至输出目录, 即导入表
Characteristics成员的偏移值就是导出表的FOA, 此处导出表的起始地址为1BD20
导出表里有三张表,分别是导出函数地址表、导出函数名称表、导出函数序号表
这三张表有着紧密的联系,如下图所示,导出表有三个导出函数,分别是Add、Sub、Div
若某个PE文件想调用这个导出表的Add函数,会通过查询名称表找到对应的下标;随后通过下标在序号表中找到对应的序号;再通过序号来对应地址表里的下标,便可查询到函数的地址
将程序拖入CFFE工具里,这三张表之间的对应关系工具已经为我们排列好了
任何PE文件还会调用哪些PE文件都会记录在导入表里。
因为PE文件可能要依赖多个模块,所以通常一个PE文件会有多张导入表。
//导入表的结构体代码
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME; //RVA,指向INT表(导入函数地址表),是个IMAGE_THUNK_DATA结构体数组
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向所依赖的dll(模块)的名字
DWORD FirstThunk; //指向IAT(导入函数地址表)的指针,也是个IMAGE_THUNK_DATA结构体数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // RVA,指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可为空,若不为空则为函数在导出表的索引
CHAR Name[1]; //函数名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
结合上述导入表的结构体成员,下图我只列举三个重要的成员,分别是OriginalFirstThunk、Name、FirstThunk
OriginalFirstThunk指向了INT(import Name Table)表, 里面存放着IMAGE_THUNK_DATA结构的成员
IMAGE_THUNK_DATA结构里的最后一个成员AddressOfData指向IMAGE_IMPORT_BY_NAME
IMAGE_IMPORT_BY_NAME结构存放函数的名称,由此可确定导入表里所用到的函数
如下图所示,call的是一个内存地址而不是绝对地址,这样的call称为间接调用,意思是间接调用其他dll里的messagebox函数
下图是PE加载后的导入表结构图, PE文件加载后IAT表不再存放IMAGE_THUNK_DATA, 而是存放函数的地址
若程序的导入地址表被加壳工具给加密了从而导致函数地址表损坏,则可以通过INT表查询到函数的名称,随后调用GetProcessAddress获取到函数地址,随后再将函数地址写入IAT表,这种技术常用于加壳与脱壳
使用CFEE工具打开dll文件,切换到导入目录,这里记录着所有导入表以及其属性
单击其中一个模块, 即可查询此模块提供的函数名以及地址