22. PE结构-PE详解之输入表(导入表)、屠龙刀W32Dasm(静态)、LordPE(动态)工具入门(查找dll、调用函数)

版权声明:墨痕诉清风 https://blog.csdn.net/u012206617/article/details/88175338

我们知道PE 文件中的数据被载入内存后根据不同页面属性被划分成很多区块(节),并有区块表(节表)的数据来描述这些区块。这里我们需要注意的问题是:一个区块中的数据仅仅只是由于属性相同而放在一起,并不一定是同一种用途的内容。例如接着要讲的输入表、输出表等就有可能和只读常量一起被放在同一个区块中,因为他们的属性都是可读不可写的。

其次,由于不同用途的数据有可能被放入同一个区块中,因此仅仅依靠区块表是无法确定和定位的。那要怎么办?对了,PE 文件头中 IMAGE_OPTIONAL_DEADER32 结构的数据目录表来指出他们的位置,我们可以由数据目录表来定位的数据包括输入表、输出表、资源、重定位表和TLS等15 种数据。(数据目录表,不要以为他在前边出现就不重要哦~)

这节课我们谈的是输入表,为什么需要输入表呢?因为我们从数据目录表得到的仅仅是一些指定数据的RVA 和数据块的尺寸,很明显,不同的数据块中的数据组织方式(结构)是显然不同的,例如输入表和资源数据块中的数据就完全是牛马不相及的两个东西。因此,我们想要深入了解PE 文件就必须了解这些数据的组织方式,以及了解系统是如何处理调用它们的。

输入函数

在代码分析或编程中经常遇到“输入函数(Import Functions,也称导入函数)”的概念。这里我们就来解释下,输入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于相关的DLL 文件中,在调用者程序中只保留相关的函数信息(如函数名、DLL 文件名等)就可以。对于磁盘上的PE 文件来说,它无法得知这些输入函数在内存中的地址,只有当PE 文件被装入内存后,Windows 加载器才将相关DLL 装入,并将调用输入函数的指令和函数实际所处的地址联系起来。这就是“动态链接”的概念。动态链接是通过PE 文件中定义的“输入表”来完成的,输入表中保存的正是函数名和其驻留的DLL 名等。


实例预演(视频中将演示,这里只能截图)

以上是咱这次实验的小青蛙哈~灰常简单的一个小程序,如图双击程序只显示一个对话窗口,然后就结束~试验用小程序,我们尽量的将内部的结构删减,调试起来才方便些。我们这次体验的目的就是想靠所学的知识,试图来找到MessageBox 在内存中的地址。
注:MessageBox 是来自于USER32.DLL 动态链接库里的一个函数,我们通过对PE 文件的静态反编译分析来观察hello.exe 这个试验品是如何定位和调用MessageBox 这个在“异乡”的函数哈。
(MessageBox 有两个版本,一个是MessageBoxA 还有一个是MessageBoxW 分别带便ASCII码形式和UNICODE)

体验开始:
1. 我们用曾经号称为屠龙刀的W32DAM 对hello.exe进行反编译,如图:

我们可以看到这个程序只有两个导入模块(Import Module),分别导入来自两个动态链接库(USER32.DLL和KERNEL32.DLL)的若干函数,我们还清晰可见,咱要跟踪的MessageBoxA 就在USER32.DLL 中,这里程序还自动给我们定位了它的虚拟地址:2A2DC,但我们不要用这个,因为我们说过这回我们是来探险的,凡事讲究人工……

我们通过W32DASM 的查找功能找出MessageBox 这个函数代码的位置,并试图查看他的汇编跳转~

push xxxx   push xxxx   push xxxx   push xxxx 之后再来一个Call xxxx ……
没错,这就是调用函数的一个标准形式。每个push 其实就是将所要调用的函数需要的参数入栈,为啥要入栈呢?这又要从地球的起源说起了……我这里就简单的说下吧:栈的发明,使得函数和子程序的出现成为可能!对于局部变量和参数,栈的特性最适合不过~不明白不急哈,这个是汇编语言和编译原理的范畴了,以后咱学习深入自然会碰到,到时候再来详细解决就可以。

int MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType // style of message box );
由定义可见MessageBox 函数共有四个参数,因此我们四次push xxxx 分别将参数按照STDCALL 的方式入栈之后,就可以CALL MessageBox 这个函数了。好,既然是CALL 我们的目标函数了,通过反汇编我们就可以观察到它的地址是:[0042A2AC],难道就这么简单?42A2AC 就是目标函数的地址?

那我们就直接把程序往下拉,试图找找这个 42A2AC 的地址吧~
可是…… ……

我们悲剧的发觉,程序压根还没到 42A2AC 这地方,到了421FF8 就结束了!!什么情况呢?
或许……或许……或许……在没详细讲输入表时我们还不能直接给大家答案……

好吧,这里提问一下,这是一个什么地址呢?偏移地址 or 虚拟地址?

恩,没错,这是一个VA 哈,那这个VA 上节课我们说过可以换算成存放在实际物理内存上的方法。具体就是将这个VA 与该程序的各个区块的VA 地址逐一对比,由于PE 头文件有记载每个区块的VA 地址也同时记载着它的实际物理地址,因此我们可以通过判断该VA 位于哪个区块内并求出与区块VA 的差值进而求出该VA 的实际物理地址。那既然前边直直的路子走不通,我们就试下把他转换为物理地址试试吧?!

我们看到,咱的42A2AC 地址加载咱的2A000 和 2B000 之间(改程序VC编写,映像基地址为400000打头哦~),因此我们可以将该地址定位到改程序位于.idata 区块内。该区块的VA 起始地址为42A000,因此42A2AC - 42A000 = 2AC,raw data offs 跟我们说该区块的物理地址是28000,因此42A2AC 这个VA 所对应的物理偏移地址就是 28000 + 2AC = 282AC。

咱用UE 打开看下282AC 这偏移地址上有啥东西……

282AC 这个地址上存放着 DCA20200 这个数据,翻译成ASCII 码也是莫名其妙的说~
但我们把DCA20200 当成一个DWORD 类型的数据来读的话我们得到数据 0002A2DC(还记得大端与小端吧)
慢着,是不是很熟悉,又是2A****开头,跟咱之前的地址是不是差不多?那好,我们又按照刚才的方法转化为偏移地址试试,转化后得到的偏移地址是:282DC,咱再看看282DC 里边有啥神秘的东西吧?

哈哈,看到奇迹了吗?从282DC 地址读起,ASCII 码对应的值是MessageBoxA.USER32.dll 

输入表结构

回顾一下,在 PE文件头的 IMAGE_OPTIONAL_HEADER 结构中的 DataDirectory(数据目录表) 的第二个成员就是指向输入表的。而输入表是以一个 IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。
IMAGE_IMPORT_DESCRIPTOR 结构定义如下:

IMAGE_IMPORT_DESCRIPTOR STRUCT 

    union 

        Characteristics              DWORD   ? 
        OriginalFirstThunk        DWORD   ? 

    ends 
    TimeDateStamp                     DWORD   ? 
    ForwarderChain                     DWORD   ? 
    Name                                    DWORD   ? 
    FirstThunk                            DWORD   ?

IMAGE_IMPORT_DESCRIPTOR ENDS 

成员介绍:

OriginalFirstThunk
它指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function name 的地址。

TimeDateStamp
该字段可以忽略。如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。在最近,它被设置为0xFFFFFFFF以表示绑定发生。

ForwarderChain
一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。

Name
它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL)。

FirstThunk
它包含由IMAGE_THUNK_DATA定义的 first thunk数组的虚地址,通过loader用函数虚地址初始化thunk。在Orignal First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。

这个OriginalFirstThunk 和 FirstThunk明显是亲家,两家伙首先名字就差不多。那他们有什么不可告人的秘密呢?来,我们看下面一张图(画的很辛苦,大家仔细看哈):

我们看到:OriginalFirstThunk 和 FirstThunk 他们都是两个类型为IMAGE_THUNK_DATA 的数组,它是一个指针大小的联合(union)类型。每一个IMAGE_THUNK_DATA 结构定义一个导入函数信息(即指向结构为IMAGE_IMPORT_BY_NAME 的家伙,这家伙稍后再议),然后数组最后以一个内容为0 的 IMAGE_THUNK_DATA 结构作为结束标志。

我们得到 IMAGE_THUNK_DATA 结构的定义如下:
IMAGE_THUNK_DATA STRUC

union u1

     ForwarderString       DWORD  ?           ; 指向一个转向者字符串的RVA
     Function                      DWORD  ?        ; 被输入的函数的内存地址
     Ordinal                       DWORD  ?         ; 被输入的API 的序数值
     AddressOfData         DWORD  ?           ; 指向 IMAGE_IMPORT_BY_NAME

ends

IMAGE_THUNK_DATA ENDS

我们可以看出由于是union结构,所以IMAGE_THUNK_DATA 事实上是一个双字大小。该结构在不同时候赋予不同的意义。

那我们怎么来区分何时是何意义呢?
规定如下:
当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。
当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。

好,那接着我们讨论下指向的这个 IMAGE_IMPORT_BY_NAME 结构。IMAGE_IMPORT_BY_NAME 结构仅仅只有一个字型数据的大小,存有一个输入函数的相关信息结构。其结构如下:

IMAGE_IMPORT_BY_NAME STRUCT

Hint        WORD    ? 
Name      BYTE      ?

IMAGE_IMPORT_BY_NAME ENDS

结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0,Name 字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

输入地址表(IAT)

为什么由两个并行的指针数组同时指向 IMAGE_IMPORT_BY_NAME 结构呢?第一个数组(由 OriginalFirstThunk 所指向)是单独的一项,而且不能被改写,我们前边称为 INT。第二个数组(由 FirstThunk 所指向)事实上是由 PE 装载器重写的。

好了,那么 PE 装载器的核心操作时如何的呢?这里就给大家揭秘啦~
PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,因此我们称为输入地址表(IAT)。所以,当我们的 PE 文件装载内存后准备执行时,刚刚的图就会转化为下图:

此时,输入表中其他部分就不重要了,程序依靠 IAT 提供的函数地址就可正常运行。

实践

静态

OriginalFirstThunk    INT

输入表地址为2A000

2A000为虚拟地址,28000为物理地址

 下图就是导入表的地址。

2A15C-2A000+28000=2815C

2A2DC-2A000+28000=282DC,找到MessageBox函数

FirstThunk  IAT

2A2AC-2A000+28000=282AC

FirstThunk  IAT

动态

OriginalFirstThunk    INT

用LordPE jump full出动态的exe

win7 64位和win10上lordPE不好使,建议虚拟机XP上运行。

剩下的与静态分析一样方法。

注意一点:动态的虚拟地址与物理地址是一样的。

猜你喜欢

转载自blog.csdn.net/u012206617/article/details/88175338