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中的导出函数也是一对多的关系,根据数据关系是多存一,因此正常的建表情况如下图
但是当我们想要知道那些dll 有哪些函数时 , 如果 dll 和 每个 dll 中的函数很多时, 这种效率很低 假设 dll 有20个 ,每个dll的导数函数都是30个 ,那么总共需要遍历 20 (20 30) = 12000 次
但是我们可以可以改变数据结构(不遵循数据关系原则)来提高速率
如图,把每个 dll 的导出函数用之中表存起来,然后 dll 后面保存 该表地址,这样我们就只需要遍历 20+ (20*60) = 620 次,可以极大的提高效率,这种防暑效率虽然很高,但是插入删除就会有问题,但是对于可执行文件来说,他没有增删改的需求,因此这个问题对于可执行文件来说不是问题
但是对于系统来说,拿到 函数的名称并没有用,主要是拿导入函数的地址,,因为我们调函数时,用的是函数的地址,并不是字符串,理论上来说,是哪个地方调用了函数,就把调用函数的对应地址填过去,但是,可能调用函数的地方很多,这样就需要把每个地方的地址填过去,这样没办法知道哪个地方调用了该函数,否则就需要建表保存,这样很麻烦,因此,编译器在运行时,就会准备一张空白表,当可执行文件加载时,就会把所有函数的地址 保存进去,当去掉api或者导出函数的时候,就会从这张表去拿导出函数的地址.
导入名称表(INT)
用OD 打开 winmine
可以看出,不同地方调用同一个函数,调式调用保存函数地址的地址,因此当系统调用函数时.只需要哪保存函数地址的值传填进去就可以了
- 导入表定位: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的最后一项
- 从下图可以看出导入表的地址是内存偏移
- 从节表可以看出,内存偏移2000的地方,对应得文件偏移是 600 ,因此内存偏移位 002010的地址对应的文件偏移为
- 610
(一)_IMAGE_IMPORT_DESCRIPTOR
- 导入目录表,描述了DLL和其API信息 大小 20 字节
导出表结构体 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”。由此可计算出项数。
(二)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
(三)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;
名称导入示例
1. 序号导入示例
OriginalFirstThunk 和 FirstThunk 分别指向本质相同的两个数组
在程序加载到进程之后,**FirstThunk 所指的 INT 值会被 改写成导入函数的地址 IAT。**
**jmp XXXXXXX**
该地址指的就是FirstThunk 的入口地址
(四)填充IAT的流程
加载器内部调用
- LoadLibary(_IMAGE_IMPORT_DESCRIPTOR.Name)
- GetProcAddress(_IMAGE_IMPORT_BY_NAME);
- 拿到IAT的地址,填入到对应的地址中
- 需要注意的时,FirstThunk是一个数组,有和OriginalFirstThunk一样多的项,一一对应
- 第一个API写到FirstThunk的地址
- 第二个API写道FirstThunk + 4的地址,依次类推
具体流程:
- PE装载器先搜索 OriginalFirstThunk ,如果找到。加载程序就会迭代搜索数组中的每个指针。找出IMAGE_IMPORT_BY_NAME 结构体所指的输入函数的地址。
然后加载器用函数真正的函数入口地址替代由FirstThunk 指向的数组中的一个入口。因此称为输入地址表 IAT
导入名称表
导入地址表(IAT)
再看看低6个 dll 的 IAT (000020A4) 导出名称表地址(2b1c)
先看导出的dll名
在看导出函数,可以看出只有一个
从上面可以看出,编辑器遍历的时候 实现获取 dll ,再从 dll 对应的 INT表(导入名称表) 拿到地址 ,填入 对应的 IAT (导入地址表)中
小细节
名称导入
可以看到 程序未加载时 INT 个 IAT 指向的 值是一样的,但是程序加载是 IAT 指向的值就填成了 函数指向的地址
进程加载前
进程加载后
序号导入
序号导入一般 MFC 的比较多
找一个32位的MFC程序,必须在共享dll中使用 MFC
OD也无法看到对应的函数名称,还原要去写工具,要找到序号和地址的对应关系,这个对应关系要到 lib 中去提取,
lib中有名称,dll中没有,都是序号
导入表字段对文件的影响
这个结构体中,除了上图3个成员,其他2个是没用的
清掉IAT
程序可以正常运行
清掉NAME
清掉名字就想当于到了导出表的结尾项后面的函数
程序无法正常运行用OD查看,发现后面的dll都没办法加载
把第二个dll的名字清了,第一个dll的导出函数有了,但是后面的都没了
清调INT
跟清NAME的效果一样
清掉INT表的第一项
程序不能正常运行,而且第一个 dll 的函数无法加载,后面的正常加载,如果清dll 的 INT表 的非第一项 后面的项,程序正常运行
清 NAME 表的第一项
对应的dll的导出函数就不会加载, ,清后面的项,则dll中 该项前面的 函数可以正常加载, 这项开始
的函数无法正常加载
清掉IAT表的第一项
对应 dll 的导数无法加载,只清该项后面的项,程序正常运行
总结
- 遍历函数的时候,如果 NAME 或者 IAT 为 0,则遍历结束
- 如果 NAME 或者 IAT 都不为0 情况下
如果 INT 不存在,那么加载函数就会用 IAT 表
如果 INT 存在,那么加载函数就会用 INT 表,但是会检查 IAT 表第一项是否为0, 0就结束
导入表遍历
导入表遍历顺序 = 导入表加载的顺序
- 检查Name 和FirstThunk ,如果任一为NULL,则停止遍历
- 取FirstThunk 的项(数组中的元素),如果为NULL, 就取OriginalFirstThunk 对应的项,如果为NULL,则遍历下一项
- 判断项的最高位,如果为1,则取低WORD为序号,如果为0,则作为RVA 取出IMAGE_IMPORT_BY_NAME 中的函数名
- 循环遍历下一项
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导出函数的名称/序号表项
步骤:
- 定位导入表
- 挪动导入表,因为一般导入表后面都没有空位置。
- 新增导入表项
- 修改导入表项的函数地址等
演示
在 PE.exe中j注入一个dll
因为如果 INT (导入名称表) 那么就会直接用导入地址表(IAT),如果有INT就会用INT,Name说明 INT 可有可无,为了方便,我们可以不要 INT 只要 NAME 和 IAT 2项的值就可以了
注意: 如果移动数据涉及到跨分页,要考虑 内存属性问题,是否可读写,而且移动数据 不要覆盖有用的数据
用代码给可执行程序注入一个dll
代码没办法像人一样用肉眼判断是否数据移动位置,因此可以通过添加 和 拓展节来实现
有附加数据的没法加
WindowsPE文件格式入门04.导入表的更多相关文章
- 小甲鱼PE详解之输入表(导入表)详解(PE详解07)
捷径并不是把弯路改直了,而是帮你把岔道堵上! 走得弯路跟成长的速度是成正比的!不要害怕走上弯路,弯路会让你懂得更多,最终还是会在终点交汇! 岔路会将你引入万劫不复的深渊,并越走越深…… 在开始讲解输入 ...
- 利用PE数据目录的导入表获取函数名及其地址
PE文件是以64字节的DOS文件头开始的(IMAGE_DOS_HEADER),接着是一段小DOS程序,然后是248字节的 NT文件头(IMAGE_NT_HEADERS),NT的文件头位置由IMAGE_ ...
- 逆向-PE导入表
导入表 动态链接库需要导入表 结构 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for ...
- 手动添加导入表修改EXE功能
目标: 改动PE导入表,手工给HelloWorld增加一个功能,就是启动的时候写入一条开机启动项,C:\cmd0000000000000000000000000000.exe 实现方法: 直接在注册相 ...
- EXP/IMP迁移案例,IMP遭遇导入表的表空间归属问题
生产环境: 源数据库:Windows Server + Oracle 11.2.0.1 目标数据库:SunOS + Oracle 11.2.0.3 1.确认迁移需求:源数据库cssf 用户所有表和数据 ...
- oracle exp imp 导入 正在跳过表 plsql 导入表 成功终止 数据 被导入
http://blog.csdn.net/agileclipse/article/details/12968011 .导入过程中,所有表导入都出现提示, 正在跳过表...某某表名 最后提示成功终止导入 ...
- 无废话ExtJs 入门教程四[表单:FormPanel]
无废话ExtJs 入门教程四[表单:FormPanel] extjs技术交流,欢迎加群(201926085) 继上一节内容,我们在窗体里加了个表单.如下所示代码区的第28行位置,items:form. ...
- Sqoop2入门之导入关系型数据库数据到HDFS上(sqoop2-1.99.4版本)
sqoop2-1.99.4和sqoop2-1.99.3版本操作略有不同:新版本中使用link代替了老版本的connection,其他使用类似. sqoop2-1.99.4环境搭建参见:Sqoop2环境 ...
- PE文件结构详解(五)延迟导入表
PE文件结构详解(四)PE导入表讲 了一般的PE导入表,这次我们来看一下另外一种导入表:延迟导入(Delay Import).看名字就知道,这种导入机制导入其他DLL的时机比较“迟”,为什么要迟呢?因 ...
- PE文件结构详解(四)PE导入表
PE文件结构详解(二)可执行文件头的最后展示了一个数组,PE文件结构详解(三)PE导出表中解释了其中第一项的格式,本篇文章来揭示这个数组中的第二项:IMAGE_DIRECTORY_ENTRY_IMPO ...
随机推荐
- DeepSeek满血版测试
技术背景 很多厂商以次充好,用蒸馏版DeepSeek冒充满血版.本文提供一些收集的问题集,可以用于测试是否满血DeepSeek.经过实际测试,国内厂商中只有满血版DeepSeek可以全对.但是各厂商后 ...
- 【资源分享】Latex mathematical symbol
https://files.cnblogs.com/files/blogs/705982/symbols.zip?t=1660463874 上面这个是pdf版,把这个压缩包下载之后,后缀改为pdf即可 ...
- [vue系列]-vue+vue-i18n+elementUI 国际化
前言 vue+vue-i18n实现多语言 本文主要内容 安装 多语言配置 element 内置语言国际化 踩到的坑以及解决方案 安装 npm install vue-i18n 配置 1.i18n.js ...
- 添加xxx.so到环境变量里
点击查看代码 libxxx.so 文件位于 /usr/local/lib 目录下,你可以按照以下步骤操作: 创建配置文件: echo "/usr/local/lib" | sudo ...
- mysql查询指定表所有的字段信息 columns
show columns from 表名: desc 表名;(全写:describe 表名) show create table 表名;
- oracle服务 linux启动命令
一.Linux下启动Oracle Linux下启动Oracle分为两步: 1)启动监听: 2)启动数据库实例: 1.登录服务器,切换到oracle用户,或者以oracle用户登录 [admin@dat ...
- FFT & NTT & FWT 基础
FFT Part 公式 欧拉公式: \(e^{i\theta}=\cos{\theta}+i\sin{\theta}\) 特殊形式:\(e^{i\pi}=-1\) Begin 一个多项式 \(F=x^ ...
- 一个基于 .NET 开源免费的异地组网和内网穿透工具
前言 今天大姚给大家分享一个基于 .NET 开源免费的异地组网和内网穿透工具:linker. 工具介绍 linker是一个基于 .NET8 开源免费(GPL-2.0 license)的异地组网和内网穿 ...
- 【SpringCloud】SpringCloud Bus消息总线
SpringCloud Bus消息总线 概述 上一讲解的加深和扩充,一言以蔽之 分布式自动刷新配置功能 Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态 ...
- 【Docker】本地镜像发布到阿里云
本地镜像发布到阿里云 本地镜像发布到阿里云流程 镜像的生成方法 1. 前面的DockerFile 2. 从容器创建一个新的镜像 docker commit [OPTIONS] 容器ID [REPOSI ...