目录
导入表描述符 IMAGE_IMPORT_DESCRIPTOR:
PE中的导入表
Windows 加载器在运行 PE 时会将导入表中声明的动态链接库一并加载到进程的地址空间,并修正指令代码中调用的函数地址。在数据目录中一共有四种类型的数据与导入表数据有关。
这四种数据依次为:
口导入表
口导入函数地址表
口绑定导入表
口 延迟加载导入表
导入表的概念:
Windows API 函数,这些代码存储在 DLL 文件,即动态链接库中。在动态链接库里存放的不是函数的源代码,而是编译链接以后生成的字节码。
看下面的两个调用语句:
invoke MessageBox,NULL,offtset szText,NULL,MB_OK
invoke ExitProcess,NULL
MessageBox 是一个从外界〈相对于 HelloWorld.asm 本身而言) 引入的函数,ExitProcess也是一个从外界引入的函数。Windows API 函数有所了解的人都知道,前者的指令字节码在user32.dll 动态链接库中,而后者则是在 kernel32.dll 中 。
当程序调用了动态链接库的相关函数,在进行编译和链接的时候,编译程序和链接程序就会将调用的相关信息写人最终生成的PE 文件中,以告诉操作系统这些函数的执行指令字节码从哪里能够获取。这些信息就是导和人表所要描述的内容。首先来看导入函数。
invoke 指令分解
在汇编语言中,程序一旦被编译,编译器会对 invoke 指令进行适当分解。分解后的指令中将会包含指向导入函数的地址的操作数。当 PE 文件被装载到内存中时,该操作数就会变成导入函数所在虚拟地址空间真实的 VA。
PE文件被装载到内存中的字节码:
将源代码中对两个导人国数 MessageBoxA 和 ExitProcess 的调用语句翻译成字节码分别为:
第一个调用语句被编译器解释为从地址 0x00401000 到 0x0040100B 的反汇编代码。根据地址 0x0040100B 处的调用关系来看,该语句还包含 0x00401018 处的跳转指令。第二个调用语句被编译器解释为从地址 0x00401010 到 0x00401012 的反汇编代码。根据地址0x00401012 处的调用关系来看,该语句还包含 0x0040101E 处的跳转指令。
(也就是说一个invoke指令会被分解为CALL和JMP指令)
PE文件在文件中的字节码:
回顾前面目录《解析 HelloWorld 程序的字节码》HelloWorld.exe 文件偏移 0x400 处是代码段,其中存放程序指令。上面显示的是 PE 被装载后在内存中的代码段,装载前文件中的代码段字节码如下所示:
可以看出,装载前文件中和装载后内存中的指令的字节码是完全一样的,这意味着,该程序在被装载以后加载的基址和 IMAGE_ OPTIONAL_HEADER32.ImageBase 是相等的,不存在重定位问题 〈如果两者不相等,则指令中涉及全局操作数的部分就会有不同)。
invoke指令分解三步骤:
从指令的反汇编代码中可以看出,在对源代码 HelloWorld.asm 进行编译时,编译器对 invoke指令实施了分解。以第一个调用语句为例,对 invoke 指令的分解操作包含以下三步:
步骤1 :压栈
即先将要调用的所有参数 push 到栈中。压栈时按照先推后参数,再推前参数的规则,即第一个推人栈的应该是调用的最后一个参数 MB_OK,最后一个推入栈的参数应该是调用中的第一个参数 NULL,尽管两者的值看起来都是 0。
步骤2 :段内调用
即通过指令 call 调用一个段内地址,即 call 00401018。
步骤3 :无条件转移
call 指令操作数 0x00401018 处的值是 : FF25 08204000,将该字节码反汇编,得到一个无条件跳转指令,跳转到了位置 00402008 处。
注意 :从位置 00402008(小端顺序) 处获取的值是导入函数 MessageBoxA 在内存中的 VA。
导入函数地址
导入函数是从动态链接库引入的函数,所以,导入函数的地址位于被加载的进程地址空间中的相应的动态链接库模块内。系统在执行用户程序对导入函数的调用语句时,会跳转到该地址处执行导入函数代码。
使用OD 打开 HelloWorld.exe,选择地址 0x0040101E 所在行,在其上单击鼠标右键,选择“数据窗口中跟随”|“内存地址”。OD 的3区就会显示内存从 00402000 开始的数据,如下所示:
深色加粗部分即为导人表数据(大小为3Ch 字节)。虽然两个jmp指令中的操作数 0x00402008 和 0x00402000 都不在该导入表(黑体部分) 的范围内,但导入表的数据结构描述中有一个字段是指向这个操作数所在位置的。从跳转指令的操作数所指向的位置 0x00402008 获取的值为760F39A0h (图中绿色方框里的值)。该值是 MessageBoxA这个导入函数在进程 HelloWorld.exe 中的 VA。
文件中和内存映像中的导入函数地址的差异与关系:
来对比一下磁盘文件和内存映像的导入函数的地址数据,看看是否存在差别:
以下是从文件中获取的导入表相关数据:
以下是从内存中获取的导入表相关数据:
可以看到 : 在同样功能的位置(文件 0x0608 处,内存 0x00402008 处),内存映像中的值为 760F39A0h,而文件中此处的值为 0000205ch。这意味着,文件被装载到内存后,这里的值发生了变化,真正标识 MessageBoxA 函数地址的应该是内存中此处的值。那么这两个值之间有什么联系呢?
研究文件中该值和内存中该值得关系:
先来看文件中该位置的值0000205ch,它是一个RVA.。 按照第 3 章中 RVA 地址与 FOA 的转换关系可以计算出该值在文件中的位置。
具体计算过程如下:
首先,通过小工具 PEInfo 获取 HelloWord.exe 中与节有关的信息(用PEinfo打开文件即可),如下所示:
其次,通过三步计算法计算出 RVA 对应的 FOA:
步骤 1:0000205ch 是落在节 .rdata 中,因为节 .rdata 的真实数据的范围是 00002000h 一00002092h,而提供的值恰好在这个范围内。
步骤2 :计算偏移 offset1=0000205ch-00002000h=005ch。
步骤3 :计算在文件中的偏移 RVA=0600h+005ch=065ch。
查看文件偏移 0x0676 处的值,发现是019Dh+ 字符串 "MessageBoxA" ,前者是一个编号 (hint),后者是调用的函数名 (name)。
最后调整一下思路:(是函数地址重新覆盖的问题)
在将 PE 文件装载进内存时,Windows 加载器会根据以下指令中的地址查找到 0x00401018 处:
Call 00401018
此处的指令是一个跳转指令,如下:
JMP DWORD PTR DS: [00402008]
加载器继续查找对应位置 0x00402008 得到值 0000205ch,然后从文件的 0x0000205c 处获取函数的名字 MessageBoxA 和函数在动态链接库里的编号。加载器会根据函数的 Hint/Name 从内存地址空间中查找到函数的 VA 为 760F39A0h,并将找到的函数地址重新覆盖内存的 0x00402008 这个位置。
当程序真正被装载进内存以后,0x00402008 这个位置就已经被杰换成为函数的正确的虚拟内存地址了。
导入函数宿主:
操作系统会在加载时根据导入表的描述将调用的函数指令字节码复制到进程地址空间中。事实上,操作系统总是会将该函数所处的动态链接库全部复制到进程地址空间,这些动态链接库便是导入函数的指令宿主。
系统还通过页面调度机制使两个进程同时访问一个动态链接库,
在内存映像中,从跳转指令的操作数 0x00402008 处取出的值为 760F39A0h。该值看起来离 HelloWorld 的线程空间很远,通过 OD 查看 760F39A0h 这个地址处的数据,根据OD 显示的种种蛛丝马迹,基本可以断定 760F39A0h 这个虚拟内存空间的地址附近装载的正是 user32.dll 链接库的 MessageBoxA 函数的源字节码。
如下所示:
而且在菜单“查看”|“内存”界面中截取的画面可以看出处列出的代码应该属于 user32.dll 模块:
从画面中可以看出虚拟地址空间的 760F39A0 处为 user32 的 PE 文件头 ;而 user32 的代码段范围是 0x76070000 一 0x7620A000,所以说跳转指令的地址 0x760F39A0h 恰好落在user32.dll 的代码段范围内。
还可以从静态文件分析中证实这个结果,首先使用PEInfo 查看文件 user32.dll 的信息:(这里是书的内容,与上面实际地址不一样的)
该链接库位于 C:\windows\system32 文件夹中,显示结果如下:
结果中可以看出,user32.dll 的默认加载地址为 0x77d10000,代码段 .text 在文件中的偏移为 0x400。有了代码段在文件中的起始偏移,我们就可以使用PEDump 来查看文件 user32.dll 的代码段的字节码,截取部分字节码如下所示;
使用OD 打开 HelloWorld.exe,在命令文本框中输入“d 77d11000”后回车,“HEX 数据”列显示了从该地址开始的指令字节码。从文件和内存〈见图 4-3) 中的进程显示的字节码来看,两者是一致的:
注意:由于每个程序的虚拟地址空间是独立的,所以一般情况下,user32.dll 的装载基地址是固定的,这个地址由字段 IMAGE_ OPTIONAL_HEADER32.ImageBase 来决定。从 PEInfo 运行的结果来看,HelloWorld.exe 被加载到内存以后,一起加载进内存的 user32.dll 所在进程地址空间的基地址并没有发生变化,还是 IMAGE_ OPTIONAL_ HEADER32.ImageBase 字段指定的 0x77D11000 这个值。
invoke指令总结:
现在做一个简单的总结: 编译程序在编译汇编语言源文件时,会把程序中的 invoke 语句分解成三部分:
口 将参数压栈部分
口 call 指令部分
口 jmp 指令部分
call 的操作数是 jmp 指令所在的地址 ; 而 jmp 指令的操作数则是该导入函数在导入表的地址。
在程序中所有的导入函数地址被排列在一起,组成 IAT,通过这样的分解操作配合导入表实现对外部函数的调用。
导入表定位:
导人表是数据目录中注册的数据类型之一,其描述信息位于数据目录的第 2个目录项中。IAT(导入函数地址表) 也是数据目录中注册的数据类型之一,其描述信息位于数据目录的第 13 个目录项中。
回顾一下PE头目录中的数据目录项 IMAGE_DATA_DIRECTORY:
查看一下HelloWorld.exe 的文件数据目录内容如下:
上面说明IAT导入函数地址表在导入表前面,使用小工具 PEInfo 获取的 chapter4\HelloWorld.exe 所有节的相关信息:
根据 RVA 与 FOA 的换算关系,可以得到:
口 IAT 数据所在文件的偏移地址 = 0x00000600
口 导入表数据所在文件的偏移地址 = 0x00000610
定位到 HelloWorld.exe 文件 0x00000600 位置处得到 IAT 和导入表数据如下:
其中绿色方框部分为导入表数据,共 60 个字节。红色方框部分为 IAT 数据,共 16 个字节。到目前为止,你只需要明确的是 IAT 实际是导入表数据组织中的一个重要的组成部分。
导入表描述符 IMAGE_IMPORT_DESCRIPTOR:
导入表数据的起始是一组导入表描述符结构。每组为 20 个字节,实例中 60 个字节的导人表数据被分成三个组。前两组均代表两个动态链接库,最后一组为全 0 结构,表示导人表描述已经结束。可以通过导入表起始地址和这个空结构计算出导入表中引用的动态链接库的个数。
Windows 在查找导入表的时候并不一定要求最后一组的 20 个字节都为0,只要其中的字段 Name1 是 0 就已经满足结束条件了。导入表的每一组都是一个结构,称为导入表摘述符 IMAGE_ IMPORT_DESCRIPTOR,该结构的具体定义如下:
1:IMAGE_IMPORT_DESCRIPTOR. OriginalFirstThunk(桥1)
+0000h,双字。因为它是指向另外数据结构的通路,因此简称为桥 1。该字段指向一个包含了一系列结构的数组。
指向的数组中的每个结构定义了一个导入函数的信息,最后以一个内容为全 0 的结构作为结束。指向的数组中每一项为一个结构,此结构名称是 IMAGE_THUNK_DATA。该结构实际上只是一个双字,但在不同的时刻却拥有不同的解释。
该字段有两种解释:
口 双字最高位为 0,表示导入符号是一个数值,该数值是一个 RVA。
口 双字最高位为 1,表示导和符号是一个名称。
2:IMAGE_IMPORT_DESCRIPTOR. TimeDateStamp
+0004h,双字。时间戳,一般不用,多为0。如果该导入表项被绑定,那么绑定后的这个时间戳就被设置为对应 DLL 文件的时间惟。操作系统在加载时,可以通过这个时间戳来判断绑定的信息是否过时。
3:IMAGE_IMPORT_DESCRIPTOR. ForwarderChain
+0008h,双字。链表的前一个结构。
4:IMAGE_IMPORT_DESCRIPTOR. Name1
+000ch,双字。这个字段的含义和名称并不一致,这里的 Name1 是一个 RVA,它指向该结构所对应的 DLL 文件的名称,而这个名称是以“\0”结尾的 Ansi 字符串。
5:IMAGE_IMPORT_DESCRIPTOR. FirstThunk(桥2)
+0010h,双字。与 OriginalFirstThunk 相同,它指向的链表定义了针对 Name1 这个动态链接库引入的所有导入函数,简称桥 2。
导入表的双桥结构:
桥 1 和桥 2最终通向了一个目的地,都指向了引入函数的“编号-名称”(Hin/Name)描述部分。而从桥 2到目的地的过程中,还经过了另外一个很重要的结构 IAT。
下图为引入了 ExitProcess 等 3 个函数的 kernel32.dll 的导入表描述符结构示意图:
下面是对 HelloWorld.exe 中的导入表数据的详细解释:
>> 54 20 00 00
桥1,最高位为0,这是一个RVA,表明函数是以字符串类型的函数名导入的。先将RVA 转换为 FOA,值为 0x00000654,从文件的该位置开始取双字,直到取出的双字为“0”结束。
IMAGE_THUNK_DATA字段:
取出的每一个双字都是结构IMAGE_THUNK_DATA 。该结构的详细定义如下:
连续取出的数分别为:
这组数中每一个都是一个RVA,不过这个 RVA 却指向了另外一个结构 IMAGE_IMPORT_BY_NAME。这个结构大小不确定,是桥 1 的最终目的地。结构的第一个为字,紧跟着的是函数的名字。
IMAGE_THUNK_DATA字段:
从文件偏移 0x0000065C 开始的数据是 (碰到“0”即结束):(0x0000065C 就是紧接着IMAGE_THUNK_DATA字段)
9D 01 4D 65 73 73 61 67 65 42 6F 78 41 00
这些值组成的数据结构就是 IMAGE_IMPORT_BY_NAME,详细摘述如下:
1:IMAGE_IMPORT_BY_NAME.Hint
+0000h,双字。函数的编号,在 DLL 中对每个函数都进行了编号(编号也叫值),访问函数时可以通过名称访问,也可以通过编号访问。
2:IMAGE_IMPORT_BY_NAME.Name1
+0004h,大小不确定。函数名字字符串的具体内容,以“\0”作为字符串结束标志。
其中 019dh 标识该函数在 user32.dll 中的编号,后面紧跟着函数名" MessageBoxA ”。
导入表双桥结构总结:
桥 1 指向的 INT 与桥 2 指向的 IAT 内容(数据值)完全一样,但 INT 和 IAT 却存储在文件的不同位置。
每一个结构 IMAGE_IMPORT_DESCRIPTOR 都对应一个唯一的动态链接库文件,以及引用了该动态链接库的多个函数,每个函数的最终“值 - 名称”描述均可以沿着桥 1 或者桥 2找到,这种导入表结构被称为双桥结构。
双桥结构的导入表在文件中存在两份内容完全相同的地址列表。一般情况下,桥 2 指向的地址列表被定义为 IAT,而桥 1 指向的地址列表则被定义为 INT (Import Name Table)。有的链接程序只为导入表存储一个桥,如 Borland 公司的 Tlink 只保留桥 2,这样的导入表我们称之为单桥结构的导人表。
注意:单桥结构的导入表是无法执行绑定导入操作的。