PE解释器之PE文件结构(二)

接下来的内容是对IMAGE_OPTIONAL_HEADER32中的最后一个成员DataDirectory,虽然他只是一个结构体数组,每个结构体的大小也不过是个字节,但是它却是PE文件中最重要的成员。PE装载器通过查看它才能准确的找到某个函数或某个资源。

一:IMAGE_DATA_DIRECTORY——数据目录结构

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;                   /**指向某个数据的相对虚拟地址   RAV  偏移0x00**/
  DWORD Size;                             /**某个数据块的大小                 偏移0x04**/
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

此数据目录表结构中有俩个成员VirtualAddress和Size,这俩成员的含义比较简单,前者指定了数据块的相对虚拟地址(RVA)Size则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该数据类型一个数据项的大小。这俩成员成为定位各种表的关键,所以一定要了解知道每个数组元素所指向的数据类型,请看下表:

//定位目录项的方法(以导出表为例):    所有操作都在FileBuffer状态下完成
 
//1、指向相关内容
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
 
//2、获取导出表的地址(目录项的第0个成员)
DWORD ExportDirectory_RAVAdd = pOptionalHeader->DataDirectory[0].VirtualAddress;
DWORD ExportDirectory_FOAAdd = 0;
//    (1)、判断导出表是否存在
if (ExportDirectory_RAVAdd == 0)
{
    printf("ExportDirectory 不存在!\n");
    return ret;
}
//    (2)、获取导出表的FOA地址    转换函数看上一章作业提示
ret = RVA_TO_FOA(FileAddress, ExportDirectory_RAVAdd, &ExportDirectory_FOAAdd);
if (ret != 0)
{
    printf("func RVA_TO_FOA() Error!\n");
    return ret;
}
 
//3、指向导出表
PIMAGE_EXPORT_DIRECTORY ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)FileAddress + ExportDirectory_FOAAdd);

二:IMAGE_EXPORT_DIRECTORY——导出表


typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;        //         未使用,总为0
    DWORD   TimeDateStamp;          //         文件创建时间戳
    WORD    MajorVersion;           //         未使用,总为0
    WORD    MinorVersion;           //         未使用,总为0
    DWORD   Name;                   // **重要   指向一个代表此 DLL名字的 ASCII字符串的 RVA
    DWORD   Base;                   // **重要   函数的起始序号
    DWORD   NumberOfFunctions;      // **重要   导出函数地址表的个数
    DWORD   NumberOfNames;          // **重要   以函数名字导出的函数个数
    DWORD   AddressOfFunctions;     // **重要   导出函数地址表RVA
    DWORD   AddressOfNames;         // **重要   导出函数名称表RVA
    DWORD   AddressOfNameOrdinals;  // **重要   导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

导出表由前面的目录表找到相对应的位置,其中导出表是什么?


导出表包含以下主要信息:
1. **导出函数的名称和地址: 列出了该可执行文件中导出的函数或符号的名称和相应的内存地址。
2. **导出函数的序号:每个导出函数分配的唯一序号,方便通过序号进行引用。
3. **导出函数的起始地址:指定导出函数的实际执行代码在内存中的起始地址。
4. **导出函数的外部名称:如果函数具有外部别名,该别名也会在导出表中记录。
导出表在动态链接库(DLL)中尤为重要,因为其他程序或模块可以通过导出表来动态链接到DLL中的函数,实现模块间的交互和共享代码。对于PE文件的分析和调试,导出表是一个关键的信息来源,可用于理解文件的功能和与其他模块的交互关系。

导出表我们需要注意标注的地方

AddressOfFunctions
  这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的地址表,这个地址表可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfFunctions进行限定,地址表中的成员也是一个RVA地址,在内存中加上ImageBase后才是函数真正的地址。
  AddressOfNames
  这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的名称表,这个名称表也可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员也是一个RVA地址,在FIleBuffer状态下需要进行RVA到FOA的转换才能真正找到函数名称。
  AddressOfNameOrdinals
  这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的序号表,这个序号表可以当作一个成员宽度为2的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。
  NumberOfFunctions
  注意,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions = 4 - 0 + 1 = 5个。

导出表结构图:

通过导出表查找函数地址的两种方法:

  1、通过函数名查找函数地址:

               (1)、首先定位函数名表,然后通过函数名表中的RVA地址定位函数名,通过比对函数名获取目标函数名的在函数名表中的索引。
    (2)、通过获取函数名表的索引获取函数序号表中对应索引中的函数序号。
    (3)、通过把该序号当作函数地址表的下标,就可以得到该下标中的函数地址。

  2、通过函数序号查找函数地址:

(1)首先计算出函数地址表的索引:index = 目标函数的函数序号 - 导出表的基地址(Base).

  (2)   通过计算出的索引就可以在函数地址表中获取到目标序号的函数地址

注意:相比于通过函数名字,通过序号获取函数地址不需要使用函数名称表和函数序号表就可以直接获取函数地址,实现上相对来说比较方便。

三:IMAGE_BASE_RELOCATION——重定位表

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;            重定位数据所在页的RVA
    DWORD   SizeOfBlock;               当前页中重定位数据块的大小
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

        重定位表简介:正如我们知道的,在程序运行时系统首先会给程序分配一个4gb的虚拟内存空间,低2g空间用于存放EXE文件和DLL文件,高2g空间则是用于取得程序使用。系统随后就会将EXE文件第一个贴入低2g空间占据文件指定的imageBase,所以EXE文件有时会木有重定位表。贴完EXE文件后接下来就会将大量程序使用的DLL文件贴入虚拟空间,然后这些dll文件和imagebase可能发生冲突,所以有些dll文件不能贴入指定的地址,但是为了让程序正常运行,需要重新给它分配一个地址,由此产生重定位表。

        重定位表就是记录这些需要修正的地址,在imagebase发生改变时就会就行修正重定位表

修正方法:

        需要重定位的地址 - 以前的基址 + 当前的基址

 VirtualAddress
  这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址。
  SizeOfBlock
  它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
  重定位项
  重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节,每一个重定位项分为两个部分:高4位和低12位。高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址),低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。

注:如果修改了EXE文件的ImageBase,就要手动修复它的重定位表,因为系统会判断程序载入地址和ImageBase是否一致,如果一致就不会自动修复重定位表,双击运行时就会报错。

重定位表结构:

通过重定位表找到需要修正的数据:

四:IMAGE_IMPORT_DESCRIPTOR——导入表

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;             //导入名称表(INT)的RVA地址
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                      //时间戳多数情况可忽略  如果是0xFFFFFFFF表示IAT表被绑定为函数地址
    DWORD   ForwarderChain;
    DWORD   Name;                               //导入DLL文件名的RVA地址
    DWORD   FirstThunk;                         //导入地址表(IAT)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

         

导入表简介:PE文件使用来自于其他DLL的代码或数据是,称作导入(或者输入)。当PE文件装入时,Windows装载器的工作之一就是定位所有被输入的函数和数据,并且让正在被装入的问渐渐可以使用这些地址。这个过程就是通过PE文件的导入表来完成的,导入表中保存的是函数名和其驻留的DLL名等动态链接所需的信息。

  OriginalFirstThunk
  这个值是一个4字节的RVA地址,这个地址指向了导入名称表(INT),INT是一个IMAGE_THUNK_DATA结构体数组,这个结构体的最后一个成员内容为0时数组结束。这个数组的每一个成员又指向了一个IMAGE_IMPORT_BY_NAME结构体,这个结构体包含了两个成员函数序号和函数名,不过这个序号一般没什么用,所以有的编译器会把函数序号置0。函数名可以当作一个以0结尾的字符串。(注:这个表不在目录项中。)

  Name
  DLL名字的指针,是一个RVA地址,指向了一个以0结尾的ASCII字符串。

  FirstThunk
  这个值是一个4字节的RVA地址,这个地址指向了导入地址表(IAT),这个IAT和INT一样,也是一个IMAGE_THUNK_DATA结构体数组,不过它在程序载入前和载入后由两种状态,在程序载入前它的结构和内容和INT表完全一样,但却是两个不同的表,指向了IMAGE_IMPORT_BY_NAME结构体。在程序载入后,他的结构和INT表一样,但内容就不一样了,里面存放的都是导入函数的地址。(注:这个表在目录项中,需要注意。)

EXE文件载入后对应的导入表结构图:

IMAGE_THUNK_DATA——INT、IAT的结构体定义如下:

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;
 
//注:这个结构体是联合类型的,每一个成员都是4字节,所以为了编程方便,完全可以用一个4字节的数组取代它。



IMAGE_IMPORT_BY_NAME 结构体定义如下:


typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
 
//注:这个结构体由两个成员组成,大致一看它的大小是3个字节,其实它的大小是不固定的,
//    因为无法判断函数名的长度,所以最后一个成员是一个以0结尾的字符串。

 EXE文件载入前对应的导入表结构图:

 五:IMAGE_BOUND_IMPORT_DESCRIPTOR——绑定导入表

IMAGE_BOUND_IMPORT_DESCRIPTOR的结构体定义如下:


typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD   TimeDateStamp;                    //时间戳
    WORD    OffsetModuleName;                 //DLL名的地址偏移
    WORD    NumberOfModuleForwarderRefs;      //该结构后IMAGE_BOUND_FORWARDER_REF数组的数量
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

 

绑定导入表简介:绑定导入是一个文件快速启动的技术,但是只能起到辅助的效果,它的存在只会影响到PE文件的加载过程,并不会影响PE文件的运行结果,这也就是说把绑定导入的信息从PE文件中清除后对这个PE文件的运行结果没有任何影响。从导入表部分我们可以知道,FirstThunk这个成员指向了IAT表,在程序加载时加载器会通过INT表来修复IAT表,使里面存放上对应函数的地址信息,但是如果导入的函数太多在加载过程中就会使程序启动变慢,绑定导入就是为了减少IAT表的修复时间。它会在程序加载前修复IAT表,然后在PE文件中声明绑定导入的数据信息,让操作系统知道这些事情已经提前完成。这就是绑定导入表的作用。

  TimeDateStamp
  这个时间戳相对来说还是比较重要的,因为这个值只有和导入DLL的IMAGE_FILE_HEADER中的TimeDateStamp值相同才能起到绑定导入的效果,如果不一致加载器就会重新计算IAT表中的函数地址。(由于DLL文件的版本不同或者DLL文件的ImageBase被重定位时,IAT绑定的函数的地址就会发生变化)

  OffsetModuleName
  这个偏移不是RVA页不是FOA,所以模块名的定位与之前的方法不同,它的定位方式是以第一个IMAGE_BOUND_IMPORT_DESCRIPTOR的地址为基址,加上OffsetModuleName的值就是模块名所在的地址了,这个模块名是以0结尾的ASCII字符串。

  NumberOfModuleForwarderRefs
  这个值是在IMAGE_BOUND_IMPORT_DESCRIPTOR结构后跟随的IMAGE_BOUND_FORWARDER_REF结构的数量。在每一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构后都会跟随着大于等于0个IMAGE_BOUND_FORWARDER_REF结构,然后在其后面又会跟上绑定表结构体,直至全部用0填充的绑定表结构。

IMAGE_BOUND_IMPORT_DESCRIPTOR的结构体定义如下:

typedef struct _IMAGE_BOUND_FORWARDER_REF {
    DWORD   TimeDateStamp;               //时间戳
    WORD    OffsetModuleName;            //DLL名的地址偏移
    WORD    Reserved;                    //保留
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
//注:
//    该结构中的成员和绑定导入表的成员含含义一致,所以不再过多叙述。
//    由于IMAGE_BOUND_IMPORT_DESCRIPTOR和IMAGE_BOUND_FORWARDER_REF的大小结构相同,所以可以相互转型,方便编程。

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

安捷伦AgilentE8363B网络分析仪

安捷伦AgilentE8363B网络分析仪 E8363B 是 Agilent 的 40 GHz 网络分析仪。网络分析仪是一种功能强大的仪器,可以以无与伦比的精度测量射频设备的线性特性。许多行业使用网络分析仪来测试设备、测量材料和监控信号的完整性 附加功能: 104 dB 的动态范围…

Redis--HyperLogLog的指令语法与使用场景举例(UV统计)

文章目录 前言HyperLogLog介绍HyperLogLog指令使用使用场景:UV统计 前言 Redis除了常见的五种数据类型之外,其实还有一些少见的数据结构,如Geo,HyperLogLog等。虽然它们少见,但是作用却不容小觑。本文将介绍HyperLogL…

使用KTO进行更好、更便宜、更快速的LLM对齐

KTO全称为Kahneman-Tversky Optimisation,这种对齐方法使在我们的数据上对大型语言模型(LLM)进行对齐变得前所未有地容易和便宜,而且不会损害性能。大型语言模型的成功在很大程度上得益于与人类反馈的对齐。如果ChatGPT曾经拒绝回…

三星刚刚将Google Gemini集成到Galaxy S24中

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版,欢迎购买。点击进入详情 AI手机的新时代即将到来。 三星刚刚将 Google Gemini 集成到 Galaxy S24 中! 准备好迎接智能手机吧,它不仅智能,而且具有灵性。…

Jvm相关知识(面试高级必备)

类的实例化顺序 先静态、先父后子 先静态:父静态>子静态 优先级:父类>子类 静态代码块>非静态代码块>构造函数 一个类的实例化过程: ①.父类的static代码块,当前类的static; ②.顺序执行…

线程池的简单介绍及使用

线程池 线程池的参数介绍拒绝策略 线程池的任务处理流程使用Executors创建常见的线程池 线程池的参数介绍 corePoolSize: (核心线程数)这是线程池中始终存在的线程数,即使这些线程处于空闲状态。maximumPoolSize:(最大线程数) 是线程池允许的最大线程数。keepAliveT…

【动态规划】【C++算法】741摘樱桃

作者推荐 【动态规划】【数学】【C算法】18赛车 涉及知识点 动态规划 LeetCode741 摘樱桃 给你一个 n x n 的网格 grid ,代表一块樱桃地,每个格子由以下三种数字的一种来表示: 0 表示这个格子是空的,所以你可以穿过它。 1 表…

EasyRecovery2024数据恢复大师最新版本下载

EasyRecovery可以从初始化的磁盘恢复损坏或删除的文件。该软有易于使用,即使是最缺乏经验的用户也可以轻松恢复数据。一款威力非常强大的硬盘数据恢复工具。能够帮你恢复丢失的数据以及重建文件系统。EasyRecovery 不会向你的原始驱动器写入任何东东,它主…

深入探索 Android 中的 Runtime

深入探索 Android 中的 Runtime 一、什么是 Runtime二、Android 中的 Runtime 类型2.1. Dalvik Runtime2.2. ART(Android Runtime) 三、Runtime 的作用和特点3.1. 应用程序执行环境3.2. 跨平台支持3.3. 性能优化3.4. 应用程序优化 四、与应用开发相关的重…

论rtp协议的重要性

rtp ps流工具 rtp 协议,实时传输协议,为什么这么重要,可以这么说,几乎所有的标准协议都是国外创造的,感叹一下,例如rtsp协议,sip协议,webrtc,都是以rtp协议为基础&#…

【react】创建react项目+项目结构

使用create-react-app快速搭建开发环境 create-react-app是一个快速创建React开发环境的工具,底层由Webpack构建,封装了配置细节 npx create-react-app react_hm执行命令后开始创建 创建好执行cd react_hm npm start 当看到webpack compiled successfu…

zookeeper window 安装

下载 Apache ZooKeeper 解压Zookeeper安装包到指定目录,注意目录不要有空格。 备份zoo_sample.cfg并改名zoo.cfg 注意:此处的路径一定要使用双斜杠" \\ " D:\\apache-zookeeper-3.8.3-bin\\data 新建环境变量:ZOOKEEPER_HOME D…

Kafka Console Client 的 Consumer Group

以往使用 kafka-console-consumer.sh 消费 Kafka 消息时并没有太在意过 Consumer Group,在命令行中也不会使用 --group 参数,本文针对 Kafka Console Client 命令行中的 Consumer Group 进行一次统一说明。 1. 如不设置 --group 参数会自动生成一个 Con…

亚马逊云科技 WAF 部署小指南(六)追踪 Amazon WAF Request ID,排查误杀原因

众所周知,中国是全球制造业的巨大力量,许多中国企业通过 2B 电商平台网站进行商品销售和采购。在这些电商平台上,Web 应用防火墙(WAF)成为不可或缺的安全工具。然而,WAF 也可能导致误杀问题。一旦误杀发生&…

CodeReview 小工具

大家开发中有没有遇到一个版本开发的非常杂,开发很多个项目,改动几周后甚至已经忘了自己改了些什么,领导要对代码review的时候,理不清楚自己改过的代码,只能将主要改动的大功能过一遍。这样就很容易造成review遗漏&…

从技术大会到面试舞台:程序猿的蜕变之旅!

在这个技术日新月异的时代,程序员们需要不断地学习和提升自己的技能。 参加技术大会,无疑是程序员们拓宽视野、提升技能的重要途径之一。然而,技术大会只是程序员成长的一部分,掌握面试技巧同样至关重要。只有将这两者完美结合&a…

ubuntu-20.04.6-live-server-amd64安装教程-完整版

简介 Ubuntu 20.04.6 Live Server AMD64 安装教程 - 完整版" 提供了详细的指南,旨在帮助用户在使用 AMD64 架构的服务器上安装 Ubuntu 20.04.6 Live Server 版本。该教程包含全面的步骤和详细说明,使用户能够顺利完成整个安装过程,建立…

【AI的未来 - AI Agent系列】【MetaGPT】5. 更复杂的Agent实战 - 实现技术文档助手

在 【AI的未来 - AI Agent系列】【MetaGPT】2. 实现自己的第一个Agent 中,我们已经实现了一个简单的Agent,实现的功能就是顺序打印数字。 文章目录 0. 本文实现内容1. 实现思路2. 完整代码及细节注释 0. 本文实现内容 今天我们来实现一个有实际意义的Ag…

test0120测试1

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 磁盘满的本质分析 专栏:《Linux从小白到大神》 | 系统学习Linux开发、VIM/GCC/GDB/Make工具…

项目实战————苍穹外卖(DAY11)

苍穹外卖-day11 课程内容 Apache ECharts 营业额统计 用户统计 订单统计 销量排名Top10 功能实现:数据统计 数据统计效果图: 1. Apache ECharts 1.1 介绍 Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观&#x…