登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 21|回复: 0

WindowsPE文件格式入门04.导入表

[复制链接]

56

主题

-7

回帖

66

积分

网站编辑

积分
66
发表于 3 天前 | 显示全部楼层 |阅读模式

PE 内部保存了导入的dll 和 api信息,这些信息保存到一个表里面.称为导入表, 导入表就是 记住一个可执行文件导入了那些dll,以及导入了这些dll中的哪些函数

一个可执行文件会调用其他DLL里的函数或数据,当PE文件被加载时,Windows加载器会定位这些所有被输入的函数和数据,并让正在被载入的文件也可以使用那些地址(函数), 这个过程是通过PE的导入表(Import Table,简称IT,也称为导入表)完成的。

导入表保存了什么?

  • 输入表中保存加载动态链接库所需要的函数名和驻留DLL名等信息(LoadLibaray)。

为什么需要导入表

假设要调用一个WINAPI --> MessageBoxA,调用API最终是call某一个API的地址,尴尬的是API地址在DLL里,地址是不确定的,因此这行代码是不能编译的。

编译器有一个巧妙的办法,将地址保存在一个全局变量中,编译器生成的代码就是call一个全局变量地址:call dword ptr [xxxx],这样就可以编译成功。

接下来在软件运行的时候,有OS将对应API的地址填入到对应的全局变量的位置,程序就可以调用正确的API。

这种工作当靠编译器或者OS一方是无法完成的,编译器知道API的地址要放在什么地方,要拿哪个API,但是不知道API的地址到底是多少

而OS知道各个API的地址,但是不知道应该放在那里。

这时导入表就至关重要,编译器将它所知道的数据整理成数据表放在PE中,软件运行时OS读取,又OS填入正确的API地址,程序就可以正确调用API。

  • 整理OS需要的信息:API名字(name/order)、填充位置(address)、库名称(lib name)

导入表结构

一个可执行文件 和 dll 之间是 一对多 的关系 , 一个 dll 和dll中的导出函数也是一对多的关系,根据数据关系是多存一,因此正常的建表情况如下图

img

但是当我们想要知道那些dll 有哪些函数时 , 如果 dll 和 每个 dll 中的函数很多时, 这种效率很低 假设 dll 有20个 ,每个dll的导数函数都是30个 ,那么总共需要遍历 20 (20 30) = 12000 次

但是我们可以可以改变数据结构(不遵循数据关系原则)来提高速率

img

如图,把每个 dll 的导出函数用之中表存起来,然后 dll 后面保存 该表地址,这样我们就只需要遍历 20+ (20*60) = 620 次,可以极大的提高效率,这种防暑效率虽然很高,但是插入删除就会有问题,但是对于可执行文件来说,他没有增删改的需求,因此这个问题对于可执行文件来说不是问题

但是对于系统来说,拿到 函数的名称并没有用,主要是拿导入函数的地址,,因为我们调函数时,用的是函数的地址,并不是字符串,理论上来说,是哪个地方调用了函数,就把调用函数的对应地址填过去,但是,可能调用函数的地方很多,这样就需要把每个地方的地址填过去,这样没办法知道哪个地方调用了该函数,否则就需要建表保存,这样很麻烦,因此,编译器在运行时,就会准备一张空白表,当可执行文件加载时,就会把所有函数的地址 保存进去,当去掉api或者导出函数的时候,就会从这张表去拿导出函数的地址.

导入名称表(INT)

用OD 打开 winmine

img

img

img

img

可以看出,不同地方调用同一个函数,调式调用保存函数地址的地址,因此当系统调用函数时.只需要哪保存函数地址的值传填进去就可以了

img

  • 导入表定位:IMAGE_OPTIONAL_HEADER-> DIrSectons[1]
    • 导入表位于数据目录第二项。
    • 注意:数据目录里的地址全部是RVA
    • 为什么是RVA而不是FA呢?
      • 处理导入表的时候,DLL已经映射进入内存了,通过RVA更加的方便。
    • 数据目录结构体
    • typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //该项数据的RVA地址 DWORD Size; //所占的大小,大小可不要,可修改。 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
    • 数据在PE中的位置:Option.Header的最后一项

    • 从下图可以看出导入表的地址是内存偏移
    • img
    • 从节表可以看出,内存偏移2000的地方,对应得文件偏移是 600 ,因此内存偏移位 002010的地址对应的文件偏移为
    • 610
    • img
    • (一)_IMAGE_IMPORT_DESCRIPTOR

    • 导入目录表,描述了DLL和其API信息 大小 20 字节
    • img

导出表结构体 IMAGE_IMPORT_DESCRIPTOR

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    //INT
    union {
        DWORD   Characteristics;    // 不是给Windows使用


        // 一个PIMAGE_THUNK_DATA结构体 保存 INT信息
        DWORD   OriginalFirstThunk; //一个API指向一个名称/序号表的中的表项,包括了序号和API名称两个字段                              //可以理解为需要的API
    } DUMMYUNIONNAME;

    //下面2个字段没用
    DWORD   TimeDateStamp;          // 时间戳
    DWORD   ForwarderChain;         // -1 if no forwarders

    DWORD   Name;                   // 重要,指向库名称

    //IAT
    DWORD   FirstThunk;             // IAT(导入地址表),IMAGE_THUNK_DATA32,一样的格式,和OriginalFirstThunk
                                    //可以理解为API地址要填入的全局变量的地址
                                    //FirstThunk是该库需要的第一个API要填入的位置,
                                    //下一个偏移4的位置是第二个API要填入的位置
} IMAGE_IMPORT_DESCRIPTOR;

1. 导入目录表个数:

导入表指向的数组中,并没有指明导入表的个数。但它的最后一个单元(一个IDD结构体大小)是“NULL”。由此可计算出项数。

img

(二)IMAGE_THUNK_DATA

导入查找表,也叫导入地址表,描述API名称/序号 信息

IMAGE_THUNK_DATA

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

导入函数导入的时候可以用名称导入,也可以用序号导入,因此要进行区分

  • ul:
    • 如果高位位1,说明是序号导入,低WORD(序号最大是 65535) 位导入函数的序号值。
    • 如果最高位为 0,说明是名字导入,该DWORD 指向结构体 IMAGE_IMPORT_BY_NAME 的偏移值
  • 微软提供了一个宏来判断最高位有效值
    • 32位:#define IMAGE_ORDINAL_FLAG32 80000000h
    • 64位:#define IMAGE_ORDINAL_FLAG64 8000000000000000h

img

img

(三)MAGE_IMPORT_BY_NAME

导入名称/序号表,结构体

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;   //OS并不看序号,不同版本DLL的API的序号不一定一样,
    CHAR   Name[1]; //导入函数的名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

名称导入示例

img

1. 序号导入示例

OriginalFirstThunk 和 FirstThunk 分别指向本质相同的两个数组

在程序加载到进程之后,**FirstThunk 所指的 INT 值会被 改写成导入函数的地址 IAT。**

**jmp XXXXXXX** 该地址指的就是FirstThunk 的入口地址

img

(四)填充IAT的流程

加载器内部调用

  1. LoadLibary(_IMAGE_IMPORT_DESCRIPTOR.Name)
  2. GetProcAddress(_IMAGE_IMPORT_BY_NAME);
  3. 拿到IAT的地址,填入到对应的地址中
    1. 需要注意的时,FirstThunk是一个数组,有和OriginalFirstThunk一样多的项,一一对应
    2. 第一个API写到FirstThunk的地址
    3. 第二个API写道FirstThunk + 4的地址,依次类推

具体流程:

  1. PE装载器先搜索 OriginalFirstThunk ,如果找到。加载程序就会迭代搜索数组中的每个指针。找出IMAGE_IMPORT_BY_NAME 结构体所指的输入函数的地址。

然后加载器用函数真正的函数入口地址替代由FirstThunk 指向的数组中的一个入口。因此称为输入地址表 IAT

img

导入名称表

img

img

img

img

img

img

img

img

img

img

img

img

img

导入地址表(IAT)

img

img

img

再看看低6个 dll 的 IAT (000020A4) 导出名称表地址(2b1c)

先看导出的dll名

imgimg

在看导出函数,可以看出只有一个

img

img

img

img

从上面可以看出,编辑器遍历的时候 实现获取 dll ,再从 dll 对应的 INT表(导入名称表) 拿到地址 ,填入 对应的 IAT (导入地址表)中

小细节

名称导入

img

img

可以看到 程序未加载时 INT 个 IAT 指向的 值是一样的,但是程序加载是 IAT 指向的值就填成了 函数指向的地址

进程加载前

img

进程加载后

img

序号导入

序号导入一般 MFC 的比较多

找一个32位的MFC程序,必须在共享dll中使用 MFC

img

img

img

img

img

img

OD也无法看到对应的函数名称,还原要去写工具,要找到序号和地址的对应关系,这个对应关系要到 lib 中去提取,

lib中有名称,dll中没有,都是序号

导入表字段对文件的影响

img

这个结构体中,除了上图3个成员,其他2个是没用的

清掉IAT

img

程序可以正常运行

清掉NAME

清掉名字就想当于到了导出表的结尾项后面的函数

img

程序无法正常运行用OD查看,发现后面的dll都没办法加载

img

把第二个dll的名字清了,第一个dll的导出函数有了,但是后面的都没了

img

清调INT

img

跟清NAME的效果一样

img

清掉INT表的第一项

img

程序不能正常运行,而且第一个 dll 的函数无法加载,后面的正常加载,如果清dll 的 INT表 的非第一项 后面的项,程序正常运行

img

清 NAME 表的第一项

img

对应的dll的导出函数就不会加载, ,清后面的项,则dll中 该项前面的 函数可以正常加载, 这项开始

的函数无法正常加载img

清掉IAT表的第一项

img

对应 dll 的导数无法加载,只清该项后面的项,程序正常运行

img

总结

  1. 遍历函数的时候,如果 NAME 或者 IAT 为 0,则遍历结束
  2. 如果 NAME 或者 IAT 都不为0 情况下
  • 如果 INT 不存在,那么加载函数就会用  IAT 表
  • 如果 INT 存在,那么加载函数就会用  INT 表,但是会检查  IAT 表第一项是否为0, 0就结束

导入表遍历


导入表遍历顺序 = 导入表加载的顺序

  1. 检查Name 和FirstThunk ,如果任一为NULL,则停止遍历
  2. 取FirstThunk 的项(数组中的元素),如果为NULL, 就取OriginalFirstThunk 对应的项,如果为NULL,则遍历下一项
  3. 判断项的最高位,如果为1,则取低WORD为序号,如果为0,则作为RVA 取出IMAGE_IMPORT_BY_NAME 中的函数名
  4. 循环遍历下一项

while TRUE
{
  pItem = 取出导入表一项;

  //判断
  if(Name == NULL || FirstThunk(IAT) == NULL)
  {
    //遍历结束
    break;
  }

  //加载dll
  LoadLibrary(Name);

  //判断OriginalFirstThunk(INT)是否存在
  pINT = FirstThunk(IAT);
  if(OriginalFirstThunk != NULL)
  {
    pINT = OriginalFirstThunk(INT);
  }

  //遍历导入名称表
  while(*pINT != NULL)  
  {
    //判断是否是序号
    if(pINT 最高位为1)
    {
        //序号导入
        导入函数地址 = GetProcAddress(低字序号);
    }
    else 
    {
        导入函数地址 = GetProcAddress(字符串地址);
    }

    导入函数地址填入IAT表中相等下标索引位置。
  }

}
while(Name != NULL && FirstThunK != NULL)
{
    IMAGE_DATA_THUNK* pTmpThunk = OriginalFirstThunk;
    if(OriginalFirstThunk == NULL)
    {
        pTmpThunk = FirstThunk;
    }
    while(*pTmpThunk != NULL)
    {
        if(*pTmpThunk & 0x80000000)
        {
            WORD dwNumber = *pTmpThunk & 0xffff; //低字为序号
        }
        else
        {
            IMAGE_IMPORT_BY_NAME* pName = *pTmpThunk;//获取导入函数名称的RVA
        }
        pTmpThunk++;
    }
}

导入表注入


  • 导入表中数据:导入表中保存的是函数名和DLL等动态链接所需的信息。
  • 导入表的作用:系统读取导入表,加载对应的DLL 和把导入的地址填入IAT表中。
  • 导入表注入:在导入表中加一项导入信息,让系统加载新增的一项所对应的DLL和数据。

导入表注入是将自己的DLL,添加到导入目录表中。目的是让系统根据导入表加载自己的DLL。从而实现代码注入的目的。

如果原导入目录表后有足够多的位置,那么可以直接加一项。

如果原导入目录表后没有足够多的位置,可以采用增加最后一个节的大小的方式来增加节,然后将导入目录表移到增加的节上,修改PE头的选项头中的数据表 --> 导入表中保存的导入目录表的地址。

新增的导入目录项,要是OS可以加载它,必须包含的信息有库名、IAT,IAT中必须保存一个DLL导出函数的名称/序号表项

步骤:

  1. 定位导入表
  2. 挪动导入表,因为一般导入表后面都没有空位置。
  3. 新增导入表项
  4. 修改导入表项的函数地址等

演示

在 PE.exe中j注入一个dll

img

img

imgimg

因为如果 INT (导入名称表) 那么就会直接用导入地址表(IAT),如果有INT就会用INT,Name说明 INT 可有可无,为了方便,我们可以不要 INT 只要 NAME 和 IAT 2项的值就可以了

img

注意: 如果移动数据涉及到跨分页,要考虑 内存属性问题,是否可读写,而且移动数据 不要覆盖有用的数据

用代码给可执行程序注入一个dll

代码没办法像人一样用肉眼判断是否数据移动位置,因此可以通过添加 和 拓展节来实现

有附加数据的没法加

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-1-24 04:47 , Processed in 0.058029 second(s), 28 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表