PE结构-导入表

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PE结构-导入表相关的知识,希望对你有一定的参考价值。

首先就是确定如何定位导入表的位置,导入表是位于数据目录项的第二项

前四个字节为相对虚拟地址(RVA),后四个字节为大小(这里做个参考,不依赖)

好了,既然是RVA,那么说的就是内存中情况,那么如何在文件中定位到导入表位置呢,这里我们则需要做的就是将RVA转换为FA,这里的话就大致来说明一下,不清楚的可以参考下其他博客

先来看一下节区的分布情况,有2个节,分别在0x1000处和0x2000处,大小都为0x1000

RVA=2020
1.查找RVA位于哪个节区内
    这里很明显位于.rdata内,该段起始为0x2000
2.计算节内偏移
    也就是减去该节起始地址(0x2000) = 0x20
3.加上该节的文件起始偏移
    0x20 + 0x400 = 0x420

所以我们来看一下文件偏移为0x420的地方

此时我选中的部分就是导入表的位置了。下面先不着急来分析字段,我们先来探讨一下为什么需要导入表?

在我们程序运行的时候,可能需要调用一些外部接口,比如说动态库,说到动态库,很容易想到两个API

LoadLibrary //加载动态库
GetProcAddress //获取模块内函数地址

是的,我们可以使用上面两个API来获取到模块中函数地址,并调用,那么问题来了,这个地址可以写死么?

答案是不可以的,因为该程序放到其他环境中运行时,该函数地址可能是会发生变化的,所以该这么办呢?

这里的话就涉及到了编译器与操作系统之间的交互了,编译器会生成间接调用的代码(留IAT接口),操作系统在装载应用程序时会填写该值,这样子就保证了正确的函数指针了。有点抽象,下面来看一下这段汇编代码

00401000 >/$  6A 00         push    0                                ; Style = MB_OK|MB_APPLMODAL
00401002  |.  68 1D204000   push    0040201D                         ; |Title = "PE"
00401007  |.  68 10204000   push    00402010                         ; |Text = "Hello World!"
0040100C  |.  6A 00         push    0                                ; |hOwner = NULL
0040100E  |.  E8 07000000   call    <jmp.&USER32.MessageBoxA>        ; 到 40101A处
00401013  |.  6A 00         push    0                                ; ExitCode = 0
00401015  \\.  E8 06000000   call    <jmp.&KERNEL32.ExitProcess>      ; 到 401020处
;          jmp     dword ptr [402008]  间接调用-编译器留的接口 402008,里面的值由操作系统填写
0040101A   $- FF25 08204000 jmp     dword ptr [<&USER32.MessageBoxA>>;  user32.MessageBoxA
;          jmp     dword ptr [402000]  与上同理
00401020   .- FF25 00204000 jmp     dword ptr [<&KERNEL32.ExitProces>;  kernel32.ExitProcess

上面汇编代码处的两个JMP,相当于就是编译器留下的接口,供操作系统填写,所以在不同的环境中,操作系统会负责该函数指针的正确性

其实这里就是IAT表了(导入地址表)。好了下面再来说一说操作系统在填写IAT表时,需要哪些信息呢?

通过上面的API可得,我们只需要知道哪个动态库,以及动态库中的函数名(或序号),这样子就能获取到函数地址了

那么我们会发现动态库和动态库中的函数名是一个一对多的关系,我们自己先来简易的设计一下

由于是一对多的关系,其实结构上我们可以设计成每个动态库后面挂一个对应的函数信息表的结构

这样子我们可以试想一下,我们遍历动态库信息表(外层循环),可以调用LoadLibrary来加载模块,然后在遍历其对应的函数信息表(内层循环),调用GetProcAddress来获取函数地址填入IAT中。这样子的的设计,相当于操作系统只要写个双层循环,就能将完成获取到这些函数地址。

好了,上面理解完后,其实导入表的大概框架结构也就是上图所示,下面我们来看一下大致的结构框架

IMAGE_DATA_DIRECTORY  //数据目录结构
    IMAGE_IMPORT_DESCRIPTOR //动态库信息 多个  以全零结构表示结尾
        IMAGE_THUNK_DATA //函数信息 多个 4字节 以全零表示结尾
            1.如果该值最高位为1,那么说明使用序号,取低WORD作为序号
            2.否则说明是名字导出 指向 IMAGE_IMPORT_BY_NAME结构

是不是和外面上面画的结构很像,首先数据目录结构会指向IMAGE_IMPORT_DESCRIPTOR,该结构会有多个(相当于表中有多条记录),以全零结构结束,所以上面说IMAGE_DATA_DIRECTORY里面的大小是供参考的。

IMAGE_IMPORT_DESCRIPTOR里的某个字段会指向IMAGE_THUNK_DATA,该结构就相当于函数信息,也会有多个,以全零结构结束。复杂的是该结构体其实只是一个四字节大小的数据,它分2种情况讨论,因为动态库中的函数导出也是有2种情况的,分为导出序号和导出函数名,正好相对应。

下面来看一下细节的结构

//导入描述结构-动态库结构
typedef struct _IMAGE_IMPORT_DESCRIPTOR 
    union 
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    ; //指向IMAGE_THUNK_DATA结构,也称INT表(导入名称表)
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;                           //指向动态库名称 RVA
    DWORD   FirstThunk;                     //指向IAT表,获取地址后对应填写到此处表中
 IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;


//函数信息描述表
typedef struct _IMAGE_THUNK_DATA32 
    union 
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // 如果是函数名,则指向 IMAGE_IMPORT_BY_NAME
     u1;
 IMAGE_THUNK_DATA32;


//函数名称信息
typedef struct _IMAGE_IMPORT_BY_NAME 
    WORD    Hint;  //通常填写编译时这个函数的序号,操作系统不做参考
    BYTE    Name[1];  //函数名称
 IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

具体的一些解释写在上面的字段后面了,有个别没写注释的,说明是不关键字段,可忽略。

下面,我们在文件中来跟着结构识别一个,首先上面可以定位到导入表了

我在每个结构之间使用了红色竖线进行划分,可以发现,该应用程序有两个动态库,最后一个全零表示结尾。

我们就来看一下第一个结构吧,首先先来看第二个红框字段,该字段表示指向动态库的名字,RVA 0x207A转FA也就是0x47A

可以看出来是User32.DLL。再来看第一个红框,表示指向INT,也就是函数信息结构0x2064转FA也就是0x464

可以发现INT表中只有一项(以零结尾),说明我们只使用了User32.DLL中一个函数

我们来看第一项,0x206c,最高位不为1,说明该函数以名字导出,所以该值又会指向IMAGE_IMPORT_BY_NAME,0x206c转FA就是0x46C

该结构的第二个结构(红框处)也就是名字了,说明我们程序使用了User32.DLL中的MessageBoxA函数。

好了,因为只有一个函数,所以该动态库已经分析差不多了,那么现在回到IMAGE_IMPORT_DESCRIPTOR结构,第三个字段就是指向IAT表,因为我们已经拿到了动态库名和函数名,那么自然就能获取到地址了。

我们来看一下这个IAT表的值为0x2008,加上ImageBase后就是0x402008,我们来回想一下上面的汇编调用代码,可以发现正好吻合上了,当获取函数指针后,操作系统就会负责填入其内。

;          jmp     dword ptr [402008]  间接调用-编译器留的接口 402008,里面的值由操作系统填写
0040101A   $- FF25 08204000 jmp     dword ptr [<&USER32.MessageBoxA>>;  user32.MessageBoxA

好了,一个动态库差不多就分析完了,可以继续分析下一个了,这里我就不继续分析了,第二个其实说明的是Kernel32中的ExitProcess函数。

下面我们来尝试一个修改导入表信息,首先,我们先来自己写一个DLL,功能就是弹一个对话框,用于替代原先的MessageBoxA

extern "C"
int __stdcall MyBoxA(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType)

    MessageBoxA(hWnd,"Hook",NULL,NULL);
    return MessageBoxA(hWnd, lpText, lpCaption, uType);

该函数的功能就是先弹一个Hook的提示对话框,然后在弹出原先的对话框。

将生成的DLL放入应用程序的同目录下,下面来修改下导入信息

双击运行后,你就能发现先弹出了Hook提示的对话框了

以上是关于PE结构-导入表的主要内容,如果未能解决你的问题,请参考以下文章

PE结构-导出表

PE结构导入表

PE结构导入表

PE 导入表

PE知识复习之PE的绑定导入表

PE格式:手工实现IAT导入表注入劫持