目录
一、导出表
1.1 导出表概述
1.2 说明与使用
二、导入表
2.1 导入表概述
2.2 说明与使用
一、导出表
1.1 导出表概述
(1)导出行为和导出表用途:PE文件能把自身的函数、变量或者类,提供给其他PE文件使用,这种行为就叫导出。导出表专门用来存放这些导出项目的信息。当一个PE文件要调用其他PE文件里的导出函数(变量、类)时,依靠导出表就能迅速找到它们在文件里的位置。一般来说,这些被导出的函数、变量、类,也叫做符号(Symbol)。
(2)导出项序号的特点:每一个被导出的函数(变量、类),都有一个独一无二的序号。在有些情况下,可能找不到对应的函数名(变量、类名),但函数(变量、类)的地址和序号是存在的,听说可以通过序号来调用这类函数。
(3)导出表的内容组成:导出表里面记录的内容,包含了函数(变量、类)的地址、序号,还有函数(变量、类)名。
(4)导出表的查找方法:数据目录表的第一个元素里有相对虚拟地址,利用前面讲过的相对虚拟地址转文件偏移的办法,就能找到导出表的位置(后面会给出具体代码) 。
导出表的数据结构如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD Characteristics; //1( 没用)保留值,恒为0DWORD TimeDateStamp; //2( 没用)和文件头中的时间一样的。WORD MajorVersion; //3(没 用)主版本号WORD MinorVersion; //4 ( 没用 )次版本号DWORD Name; //5(有用)本PE文件的名字,也就是谁导出的这些函数(变量,类)DWORD Base; //6(有用)序号基数DWORD NumberOfFunctions;//7 (重要) 函数数量DWORD NumberOfNames; //8 (重要) 函数名称数量DWORD AddressOfFunctions;//9(重要)函数地址表的相对虚拟地址//RVA from base of imageDWORD AddressOfNames; //10(重要)函数名称表的相对虚拟地址//RVA from base of imageDWORD AddressOfNameOrdinals;//11(重要)序号表的相对虚拟地址//RVA from base of image
}IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
1.2 说明与使用
说明
(1)导出表存储位置:导出表一般在.edata段,不过.edata段通常会合并到.rdata段。
(2)序号基数:有个序号基数,从序号表得到的序号得加上它,才是真实函数序号,这方便在函数地址、序号和函数名之间互相查找。
(3)表项地址定位:导出表最后三个成员是相对虚拟地址,转换成文件偏移后,能找到函数地址表、函数名称表和序号表。
(4)函数名称表:函数名称表存的是函数名的相对虚拟地址,得再转换成文件偏移才能用,解析时要注意。
(5)导出表各表关系:
- 序号顺序:序号不是按顺序排列的。
- 对应关系:序号和函数名一一对应,两个表相同位置元素对应,所以结构体没设序号数量,因为它和函数名数量一样。
- 序号中断:序号可能中断,比如缺1、5,但不代表没对应的函数地址。
- 数量关系:函数个数比函数名个数多,多出来的可能是序号导出函数,也可能是地址填0的无效函数。
- 表间关联:序号表元素值对应函数地址表位置,该位置的函数地址就是对应函数名的地址,靠这个把三个表联系起来。
- 序号导出函数:函数地址表有地址值,但序号表没对应序号,说明是序号导出函数,序号就是它在函数地址表中的位置,没函数名。
- 无效函数:函数地址表元素填0,就是无效函数,没序号和函数名对应。
看下面这张导出表图示,能更好理解导出表。
函数表5、7位置是无效函数,2、3、4、6、8位置是函数名导出,注意函数名位置和地址表地址位置不同,1、9是序号导出函数,序号就是本身位置。
使用
(1)导出表信息提取:想提取导出表所有信息并过滤无效地址,得用循环,循环次数是函数地址个数。根据函数地址位置,在序号表找对应值,这个值的位置就是函数名位置。
代码思路如下,假设目标文件已读入内存,首地址为pFile(void类型指针),且用到之前计算偏移的函数:
for(Ordinal=0;Ordinal<函数个数;dwOrdinal++){if(!导出函数数组[Ordinal])continue ;for(Index=0;Index<函数个数;dwIndex++){if(导出序号数组[Index]== Ordinal ){输出带函数名的导出函数信息}elseif(已经遍历完导出表){输出不带函数名的导出函数信息}}
}
在LoadPE中,点击导出表的“...”即可弹出导出表界面,序号代表结构体中成员的序号。
导出表偏移:这个偏移是用数据目录中的相对虚拟地址算出来的
特征:特征值恒0
函数地址:函数地址的相对虚拟地址
函数名地址:函数名称的相对虚拟地址
函数名顺序地址:序号表的相对虚拟地址
名称:名称的相对虚拟地址
字符串名称:根据名称的地址算出编移存储的名称
(2)根据函数名查找函数地址:导出表还有其他用途,比如给定一个函数名,查找其函数地址,这类似于GetProcAddress()函数。在编写壳程序等特殊场景中会用到此功能。该方法比上述代码更简单,直接根据函数名找到其位置,该位置序号的值即为所要查找函数的地址索引。若要查找已加载到内存后的dll中的函数地址,就不应再使用文件偏移,而应直接使用虚拟地址(VA)。
二、导入表
2.1 导入表概述
(1)导入行为和导入表用途:PE文件运行过程中,如果用到其他PE文件里的函数、变量或者类,这种行为就叫做导入。导入表专门记录这部分信息。
(2)导入表的查找方法:数据目录表的第二个元素能帮我们定位导入表,查找的办法和找导出表一样。
(3)导入表的存储内容:导入表会记录从其他PE文件导入的函数名和序号。当PE文件加载到内存后,导入表还会保存这些函数的实际内存地址。
(4)导入表的结构特点:一个PE文件往往需要多个其他PE文件提供支持,所以导入表通常有多个。从结构上来说,导入表是一个结构体数组,数组以一个全零元素作为结束标志,数组里每个元素,都对应着一个PE文件的导入信息 。
2.2 说明与使用
导入表相关数据结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR
union {DWORD Characteristics; DWORD OriginalFirstThunk;//1(重要)指向一个结构体数组的相对虚拟地址(RVA), 结构体数组叫输入名称表
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //2(有用)没用
DWORD ForwarderChain; //3(有用)转发机制用到,这里不探讨
DWORD Name; //4(有用)导入的PE文件的名字的相对虚拟地址RVA
DWORD FirstThunk; //5(重要)指向一个结构体数组的相对虚拟地址(RVA),结构体数组叫做输入地址表(IAT:Import Address Table)
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
说明
(1)指针指向特性:OriginalFirstThunk和FirstThunk这两个指针,都指向IMAGE_THUNK_DATA类型的结构体。
(2)磁盘与内存数据变化:在磁盘上的文件里,OriginalFirstThunk和FirstThunk所指向的数据是一样的。基于此,我们可以把输入名称表(INT)当作输入地址表(IAT)的备份。等到文件加载到内存后,加载器会把对应PE文件里函数的真实地址,填充到输入地址表中。这时,输入地址表才成了真正可用的输入地址表。
(3)输入名称表情况:有些文件的输入名称表内容全为零,处于空白状态。这意味着输入地址表有时没有备份。所以在解析输入表时,优先使用输入地址表;当然,也可以同时解析输入名称表和输入地址表,对比查看。
(4)解析终止条件:这两个指针指向的结构体数组,是以全零元素作为结尾的。我们可以利用这一特性,作为解析过程的结束标志。
(5)相关结构体介绍:
typedef struct _IMAGE_THUNK_DATA32 {union {DWORD ForwarderString; //转发用到DWORD Function; //导入函数的地址,在加载到内存后,这里才起作用DWORD Ordinal; //假如是序号导入的,用到这里DWORD AddressOfData; //假如是函数名导入的,用到这里,它指向一个PIMAGE_IMPORT_BY_NAME结构体}ul;
}IMAGE_THUNK_DATA32;
在磁盘文件中,起作用的只有后两个成员。该结构占4个字节,若最高位为1,则序号导入起作用,只需输出一个序号;若最高位为0,则最后一个成员起作用,指向一个PIMAGE_IMPORT_BY_NAME。可使用系统提供的宏IMAGE_SNAP_BY_ORDINAL32()判断最高位是否为1,参数为该结构体。
typedef struct _IMAGE_IMPORT_BY_NAME {WORD Hint;CHAR Name[1];
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
该结构包含序号和函数名。
使用:
//循环打印INT的内容
while(pINT->u1.Ordinal)
{//判断最高位是否不为1,是的话则打印其AddressOfData的内容if(!IMAGE_SNAP_BY_ORDINAL32(pINT->u1.Ordinal)){PIMAGE_IMPORT_BY_NAME pBN =获取ByName;printf("%04X %s\r\n",pBN->Hint,pBN->Name);}pINT++;continue;
}
//如果最高位为1,则直接打印Ordinal部分
printf("%04X %s\r\n",pINT->u1.Ordinal&0xFFFF,"(Null)");
pINT++;
原始 FirstThunk:导入名称表的RVA
FirstThunk:导入地址表的RVA
正向链:转发用
ThunkEVA:每-个Thunk统构的RVA(算的)
Thunk 偏移:Thunk结构的编移(算的)
Hint:函数的序号
API名称:函数的名称
计算每个Thunk结构的相对虚拟地址(RVA)时,只要知道第一个结构的RVA,后续结构的RVA依次加4就能得出。偏移计算方法也是一样,算出第一个偏移后,后续偏移每次加4。
LoadPE软件会对输入名称表(INT)和输入地址表(IAT)这两个数据域进行解析。软件默认展示INT的解析结果,如果点击软件下方“总是查看FirstThunk”复选框,软件就会显示IAT的解析结果。
到这里,关于导出表和导入表的解析说明就结束了。