-
-
[求助]PE结构之导入表和导出表
-
发表于: 2019-8-27 18:06 3734
-
PE结构
大佬们帮我看看有什么问题
PE结构整体图
导出表结构图
通过函数名导出函数
1.定位到模块的Dos头,基址(DOS头的RVA)+0X3C偏移定位到DOS头中的e_Ifanew字段,而e_Ifanew的值A就是当前dll文件的NT头的偏移,DOS头+A偏移就是NT头,在32位的dll中,NT头可分为三部分(DWORD Signature,IMAGE_FILE_HEADER FileHerder,IMAGE_OPTIONAL_HEADER32 OptionalHeader三个部分)。
2.NT头里有一个特征码,两个结构体
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //The bytes are "PE\0\0". IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
3.我们需要通过计算到达OpionalHeader这个结构体处,取第一个元素即Export Directory RVA(<font color=0099ff>这个字段存储的是导出表相对偏移</font>) 要到达这里DOS头的基址+78h的偏移到达并取值。得到导出表的RVA地址。
4.Export Table的结构是这样的:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; +0x10 DWORD Base; +0x14 DWORD NumberOfFunctions; +0x18 DWORD NumberOfNames; +0x1c DWORD AddressOfFunctions; //RVA from base of image +0x20 DWORD AddressOfNames; // RVA from base of image +0x24 DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
6.现在我们需要通过其中三个表来找到我们需要的函数地址
函数名称表,函数地址表,序号表。
7.先遍历名称表中的名称,注意名称表中存储的是名称字符串所在的rva而不是字符串。
由于名称表和序号表的下标是对应的,也就是说名称表的第i个表对应序号表的第i个表。找到对应名称后,我们记住那个下标。
然后根据下标去序号表里取数组中的值。
把取出的这个值当做函数地址表的下标。
从函数地址表中根据下标拿到rva。
导入表
导入函数是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,<font color=0099ff>在调用者程序中只保留一些函数信息,包括函数名以及主力的DLL名称的等。</font>
动态链接:对于存储在磁盘上的PE文件来说,他无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存的时候,windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。
动态链接是通过PE文件中定义的<font color=red>“导入表(Import Table)”</font>来完成的,导入表中的正是函数名和和其驻留的DLL名等动态链接所必需的信息。
IMAGE_IMPOORT_DESC
导入表是由一系列的<font color=blue>IMAGE_IMPOORT_DESCRIPTOR</font>结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个Dll文件。如果一个PE文件从10个不同的DLL文件中引入了函数,那么就存在10个IMAGE_IMPORT_DESCRIPTOR结构来描述这些DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束:
IMAGE_IMPORT_DESCRIPTOR union Characteristics dd ? OriginalFirstThunk dd ? // ends TimeDateStamp dd ? ForwarderChain dd ? Name1 dd ? FirstThunk dd ? IMAGE_IMPORT_DESCRIPTOR ENDS
结构体中的Name1字段(使用Name一词和MASM的关键字冲突)是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。
IMAGE_THUNK_DATA
OriginalFirstTunk字段和FirstTunk字段的含义当前是可以看成相同的(使用“当前“一词的含义马上就会见分晓),他们都有指向一个包含一系列<font color=blue>IMAGE_THUNK_DATA</font>结构的数组。
其实 你会发现这是一个联合体的结构,所以一个IMAGE_IMPORT_DESCRIPTOR的大小是4个字节(dd),之所以把它定义成结构,是因为他在不同的时刻有不同的含义。结构的定义如下:
IMAGE_THUNK_DATA STRUCT union u1 ForwarderString dd ? Function dd ? Ordinal dd ? AddressOfData dd ? ends IMAGE_THUNK_DATA ENDS
IMAGE_IMPORT_BY_NAME
那么一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?
当双子(就是指结构)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。(80000000h代表最高位为1,四个字节大小的数)
当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME 结构,此结构的定义如下:
IMAGE_IMPORT_BY_NAME STRUCT Hint dw ? Name1 db ? IMAGE_IMPORT_BY_NAME ENDS
结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name1字段定义了导入函数的名称的字符串,这是一个以0为结尾的字符串。
整个过程听起来有些复杂,其实用图来说明就很清楚了,图中示意了可执行文件导入Kernel32.dll中的ExitProcess.ReadFile,WriteFile和一个以序号为导入方式的函数(姑且称这个函数为ImportByNo)的情况,假设这四个函数的序号分别是02f6h,,0111h,002bh和0010h。
上面三个结构体的关系
现在来分析一下图中的示例,导入表中IMAGE_IMPORT_DESCRIPTOR结构
的Name1字段指向字符串"Kernel32.dll"表示当前要从Kernel32.dll文件中导入函数,OriginalFitstThunk和FirstThunk字段指向两个同样的<font color=red>IMAGE-THUNK_DATA</font>数组,由于要导入的是4个函数,所以数组总包含4个有效项目并以最后一个内容为0的项目作为结束。
第四个函数是以序号导入的,与其对应的IMAGE_THUNK_DATA结构的最高位等于1,和函数的序号0010h组合起来的数值就是80000010h,其余的三个函数采用的是以函数名导入的方式,所以IMAGE_THUNK_DATA结构的数值是一个RVA,分别指向3个IMAGE_IMPORT_BY_NAME结构,每个IMAGE_IMPORT_BY_NAME结构的第一个字段是函数的序号,后面就是函数的字符串名称了,一切就是这么简单。
内存中的导入表
为什么会需要两个一模一样的IMAGE_THUNK_DATA数组呢?
答案是PE文件被装入内存的时候,其中一个数组的值将被改作他用。
比如hello world程序中,windows装载器会将指令jmp dword ptr[xxxxxxx]指定的 xxxxxx处的RVA替换成真正的函数地址,其实xxxxx地址正是由FirstThunk字段指向的那个数组中的一员。
如下图
加载的时候就是改<font color=red>2076</font>这个值为调用函数的RVA地址。
实际上,当PE文件被装入内存后,内存中的映像就被Windows装载器修正成了下图的样子,其中由FirstThunk 字段指向的那个数组的每个双字都被替换成真正的函数入口地址地址,之所以在PE文件中使用两份IMAGE_THUNK-DATA数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。
导入地址表(IAT)
IMAGE_IMPORT_DESCRIPTOR 结构中 FirstThunk 字段指向的数组最后会被替换成导入函数的真正入口地址,暂且把这个数组称为导入地址数组。在PE文件中,所有DLL对应的导入地址数组在位置上是被排列在一起的。全部这些数组的组合也被称为导入地址表(Import Address Table,或者简称为IAT),导入表中的第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址。
为什么有时脱完壳需要修复导入表(IAT)
首先我们要清楚,Windows系统加载程序的时候,会帮我们做什么。
1.程序加载时,原本我们的OrdinaryFirstThunk 和 FirstThunk 都指向同一个IMAGE_IMPORT_BY_NAME表,形成一个完整的双桥结构,系统会帮我们把生成IAT表(即将FirstThunk 的值改成具体调用函数的地址。)。
2.程序退出时,系统会在退出之前将我们的IAT修复为原本指向的IMAGE_IMPORT_BY_NAME表,每次加载程序时,加载的dll地址都是不同的,所以要恢复,下次启动重新加载。)
为什么要修复IAT:
加壳的程序,系统是读不到我们原本程序的IAT的,系统只能读到壳代码的IAT,并不知道源程序的IAT在哪儿。
而程序跑起来后 ,壳代码会帮我们填充原程序的IAT表。
如果我们在此时直接dump下来源程序,源程序的IAT表还是指向的函数地址,双桥断裂。系统下次运行程序发现这不是一个完整的双桥结构,系统也不会加载dump下来的这个东西。
所以我们dump下来源程序后需要将源程序的IAT修复。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!