本文将通过一个实例说明PE结构中的import table及import address table(IAT).
在data directory中有两项:IMAGE_DIRECTORY_ENTRY_IMPORT(1)和IMAGE_DIRECTORY_ENTRY_IAT(12),IMAGE_DIRECTORY_ENTRY_IMPORT指向了该PE文件中所有的输入信息,而IMAGE_DIRECTORY_ENTRY_IAT指向了该PE文件中的导入地址表。现在不必关心这两个表的关系,在之后的分析中将渐渐明朗。
如果用C/C++程序来分析import table或IAT的结构不是很合适,因为CRT会插入太多的代码到最终的PE文件中,所以我们选择用汇编代码作为例子,好处就是所有的导入函数都是自己的代码中确确实实使用到的,一目了然。
我们使用现成的一个例子的分析:(摘自Win32ASM Programming 3rd Edition)
通过stud_pe(一个很好的学习PE的工具)我们清楚的看到在这个例子中我们使用了如下导入函数:
user32.dll:
DispatchMessageA
DrawTextA
EndPaint
GetClientRect
GetMessageA
DestroyWindow
PostQuitMessage
RegisterClassExA
ShowWindow
TranslateMessage
UpdateWindow
DefWindowProcA
CreateWindowExA
LoadCursorA
BeginPaint
kernel32.dll:
GetModuleHandleA
ExitProcess
RtlZeroMemory
现在我们从Data Directory开始分析。(以下截图均来自Stud_PE)
IMAGE_DIRECTORY_ENTRY_IMPORT:
IMAGE_DIRECTORY_ENTRY_IAT:
在继续之前我们需要了解两个东西:
1. Data Directory的每一项前四字节表示虚拟地址,后四字节表示大小,本文中不关心。不熟悉的读书可以查阅PE结构获取更多信息。
2. 这里的地址是虚拟地址,也就是载入内存以后的地址。我们如何在文件中也就是物理硬盘上找到对应的内容呢?其实通过Optional Header中的FileAlignment和SectionAlignment可以实现虚拟地址和物理地址的转化。有兴趣的读书可以查阅PE结构获取更多信息,本文将使用stud_pe提供的转化工具完成相应工作。
接下来开始我们逐步分析:
Import table的地址:00002090 通过stud_pe找到物理地址:00000690
IAT的地址:00002000 通过stud_pe找到物理地址:00000600
那么00000690和00000600分别在文件的哪个section呢?通过查看该PE的section table我们发现所有的相关信息都存放在.rdata段了。说到这里简单提一下,data directory中指向的内容都是存在各个section中的,至于存在哪个section主要看section的读写属性,当然也可以另起一个section单独存放某个data entry的内容,比如.reloc就作为一个单独的段存放了所有的重定位信息。
为了方便,我们把整个.rdata段拿出来分析:
在继续之前,我们先大概了解一下import table的知识:
这个图再清晰不过了,不过要彻底搞明白还是要结合例子分析:
输入表是一个 IMAGE_IMPORT_DESCRIPTOR数据结构的数组,这个结构定义如下:
这个结构总共20个字节,最重要的有三个:OriginalFirstThunk、Name、FirstThunk。一个IMAGE_IMPORT_DESCRIPTOR包含了从一个dll中导入的信息,在这个例子中我们有两个dll:user32.dll和kernel32.dll,最后用一个全0的IMAGE_IMPORT_DESCRIPTOR表示结束。我们只提取我们关心的东西:
OriginalFirstThunk1: 000020DC --> 000006DC(物理地址)
Name1: 0000220E --> 0000080E
FirstThunk1: 00002010 --> 00000610
OriginalFirstThunk2: 000020CC --> 000006CC(物理地址)
Name2: 0000224C --> 0000084C
FirstThunk2: 00002000 --> 00000600
我们先从简单的开始分析:Name
(前面的地址如00000200不是真实的物理地址,在stud_pe中此地址表示到.rdata的起始地址的偏移,也就是00000600+00000200,因此我们看到user32.dll的物理地址其实是0000080E,也就是Name1的值。kernel32.dll同理)
再继续往下之前,我们先要回到之前PE的import table结构中另外两个结构的定义:
我们看到IMAGE_THUNK_DATA是一个4字节的联合,在本例中其实就是一个IMAGE_IMPORT_BY_NAME的虚拟地址。如果导入函数不是用函数名导入的而是用序号导入的,情况就不一样了,具体是按名字导入还是按序号导入是看IMAGE_THUNK_DATA字段的最高位是否为1决定的。具体可查看PE文件结构。
接下来我们结合例子来看OriginalFirstThunk和FirstThunk两个字段表示的意义(以kernel32.dll为例):
OriginalFirstThunk: 000020CC --> 000006CC
内容如下:(28 22 00 00 1A 22 00 00 3C 22 00 00 00 00 00 00)
FirstThunk: 00002000 --> 00000600
内容如下:(28 22 00 00 1A 22 00 00 3C 22 00 00 00 00 00 00)
我们发现了两个地方:
1. 000006CC和00000600指向的内容是一样的,但是确存放了两个拷贝。
2. 00000600好熟悉啊。。。回头看看原来是data directory中IMAGE_DIRECTORY_ENTRY_IAT的地址!
OriginalFirstThunk和FirstThunk其实是指向了两个不同的表INT和IAT,但是这两个表其实内容是相同的。INT/IAT中的内容在本例中都是虚拟地址,至于该虚拟地址中的数据究竟表示了什么含义,就涉及IMAGE_IMPORT_BY_NAME这个结构体了。我们就取一个例子分析:1A 22 00 00 --> 0000081A(9B 00 45 78 69 74 50 72 6F 63 65 73 73 00)
Hint: 00 9B,这个是用来优化的,具体不展开,在本例中直接可以忽略。
Name: 45 78 69 74 50 72 6F 63 65 73 73 00 == ExitProcess
至于为什么需要用两个相同的表INT/IAT,原因比较复杂,简单的说是为了加快加载速度,能够进行链接时绑定。具体就不展开了,因为跟本文主题没有密切的联系,有兴趣的读者自己查阅相关资料。
对import table的分析基本全部完成了,现在我们再回头来看一下IAT的内容:
除了之前kernel32对应的FirstThunk,还有user32.dll对应的FirstThunk(00000610),也就是说IAT其实包含了该PE文件中所有导入的函数,以00 00 00 00作为一个dll的结尾。IAT对应的data directory的size告诉我们IAT的大小,在本例中50h表示这个IAT总共包含了80字节(20项)。
说到这里其实关于import table跟IAT相关的分析已经结束了,但是既然话题是import相关的,那我们也来分析一下在代码中调用一个输入函数是如何处理的:
比如:invoke ExitProcess,NULL
我们在OD中可以看到对应的机器/汇编指令:E8 5B 00 00 00/CALL 004011CA
004011CA这个地址是做什么用的呢?在OD中我们看到004011CA地址对应的指令如下:
JMP DWORD PTR DS:[402004]
那么402004是什么呢???噢!其实就是文件中对应的物理地址604!402004-400000(exe加载地址)== 2004 --> 604,也就是IAT表中的第二项,那么是1A 22 00 00么?不是!为什么叫IAT(image address table)?因为这个表的内容在加载时会用实在的导入函数地址填充。所以这个时候其实是真正的ExitProcess的地址了,我们可以用OD看到,运行时这个地址是76 0E 73 4E!
其实这个就是调用外部函数通常的做法,先用CALL指令转向一个跳转表:
而这个跳转表其实就是用JMP指令跳转到了IAT中的每一项。但是这个跳转表不是必需的,又是也可以直接用CALL DWORD PTR DS:[XXX]这样的语句实现外部函数的调用,XXX正是IAT中的某一项的地址。具体什么时候系统会创建这个跳转表是个疑问,暂时没有找到规律。