PE中的import table/IAT 分析

来源:互联网 发布:希特勒我的奋斗 知乎 编辑:程序博客网 时间:2024/06/01 18:52

本文将通过一个实例说明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中的某一项的地址。具体什么时候系统会创建这个跳转表是个疑问,暂时没有找到规律。

原创粉丝点击