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

https://bpsend.net/thread-307-1-1.html

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

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

有附加数据的没法加

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/909736.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TODO 数据结构

先进先出(队列) 滑动窗口最大值(单调队列) 最长递增子序列(动态规划)(贪心+二分) 4.数组逆序对统计(归并排序) 快速获取集合重复值(哈希表) 6.快速获取集合最值(二叉堆) 7.无向图判环(并查集) 8.图的连通性问题(dfs+bfs)9.单源最短路dijkstra 10.多源最短路(…

CX9261S线控增益问题

简述 想将9361增益控制的逻辑移植到9261s上,先验证线控功能,配置后在接收单音信号测试中观测不到增益的变化。 配置修改 根据寄存器列表和手册修改配置的寄存器值,如图1所示。同时,修改了增益步进值为5dB。图1 手册中的控制逻辑 测试中,接收单音信号,并根据接收时钟MCLK产…

青岛oj集训5

Floyd,全源最短路,传递闭包,灾后重建,无向图的最小问题,Redistributing Gifts S,重新分发礼物 S,oj集训Floyd算法——全源最短路 cerr:标准输出错误流:不会输出到freopen制定的out文件中,而是会输出到错误文件中。 提交上去无论加不加freopen,哪怕是提交到洛谷,也只…

20244214 实验二《Python程序设计》实验报告

20244214 2024-2025-2 《Python程序设计》实验二报告 课程:《Python程序设计》 班级: 2442 姓名: 张家乐 学号:20244214 实验教师:王志强 实验日期:2025年4月1日 必修/选修: 公选课 1.实验内容 (1)设计并完成一个完整的应用程序,完成加减乘除模等运算,功能多多益善。…

langchain0.3教程:聊天机器人进阶之方法调用

大语言模型只能聊天吗?本篇文章将会介绍OpenAI的Function calling原理,以及在Langchain中对应的Tools Calling如何使用,最后将工具调用集成到gradio实现可视化聊天界面。我们思考一个问题:大语言模型是否能帮我们做更多的事情,比如帮我们发送邮件。默认情况下让大模型帮我…

云锵投资 2025 年 3 月简报

季报摘要加密货币高频量化策略,研发中…… 本季度量化基金策略业绩:4.98%,良,全国排名:3975/12416;平均 Beta:1.00; 本季度量化股票策略业绩:5.04%,良,全国排名:3935/12416;平均 Beta:1.46; 本季度量化期权策略业绩:8.69%(中性策略,不参与全国股基排名);(…

【攻防世界】flag_universe

⭕、知识点 流量分析/ftp协议/图片LSB隐写/zsteg 一、题目给出一个流量包二、解题 1、追踪ftp流量 2、查看后发现一些可能有用的信息3、flag.txt里的内容是一串base64编码后的数据,解密后发现是假flag4、根据题目名称以及剩下的两个文件,盲猜答案就隐藏在两张图片中13号流是旧…

20242317 2024-2025-2 《Python程序设计》实验二报告

20242317 2024-2025-2 《Python程序设计》实验二报告 课程:《Python程序设计》 班级:2423 姓名:林楚皓 学号:20242317 实验教师:王志强 实验日期:2025年3月26日 必修/选修:公选课 一、实验内容 1.设计并完成一个完整的应用程序,完成加减乘除模等运算,功能多多益善; …

# **DeepSeek 深度解析 PasteForm:一个让管理端开发爽到飞起的全栈解决方案**

🤖 DeepSeek 深度解析 PasteForm:一个让管理端开发爽到飞起的全栈解决方案 各位开发者注意啦!今天我要带大家全方位解剖 PasteForm 这个神奇框架——不仅介绍核心思想,更要重点展示它强大的配套工具链!(那些被其他教程忽略的精华部分都在这里了!) 先上镇楼图,这是 De…

利用 AWS Signature:REST API 认证的安全指南

随着云计算领域的不断发展,保护 API 访问的安全性变得愈加重要。AWS Signature 提供了一种强大的机制,用于通过 REST API 认证请求到 AWS 服务。本文讨论了 AWS Signature 的重要性,解释了它是什么,提供了 Java 和 Go 中的实现示例,并介绍了用于测试的工具,包括 APIPost,…

【ABP】项目示例(8)——数据迁移

数据迁移 在上一章节中,已经展示了数据播种的用途之一,即单元测试中进行数据初始化,在这一章节中,实现数据播种的另一重要用途,即数据迁移 该项目使用的是代码优先的开发模式,需要将领域模型迁移到数据库中的数据模型 EF数据迁移 在程序包管理控制台选中General.Backend.…

一个测试工程师的实战笔记:我是如何在Postman和Apipost之间做出选择的?

作为一家金融科技公司的测试负责人,我每天要处理数十个需要加密验签的接口。从最开始的Postman,到后来的Apipost,让我重新思考:我们需要的究竟是一个代码编辑器,还是一个真正懂测试者的智能工具? 一、当加密需求被Postman的脚本支配 1、密码字段MD5加密 去年接手支付系统…