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;        //         未使用,总为0DWORD   TimeDateStamp;          //         文件创建时间戳WORD    MajorVersion;           //         未使用,总为0WORD    MinorVersion;           //         未使用,总为0DWORD   Name;                   // **重要   指向一个代表此 DLL名字的 ASCII字符串的 RVADWORD   Base;                   // **重要   函数的起始序号DWORD   NumberOfFunctions;      // **重要   导出函数地址表的个数DWORD   NumberOfNames;          // **重要   以函数名字导出的函数个数DWORD   AddressOfFunctions;     // **重要   导出函数地址表RVADWORD   AddressOfNames;         // **重要   导出函数名称表RVADWORD   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;            重定位数据所在页的RVADWORD   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;      // PBYTEDWORD Function;             // PDWORDDWORD 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的大小结构相同,所以可以相互转型,方便编程。

 

 

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

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

相关文章

软件测试|sqlalchemy一对一关系详解

简介 SQLAlchemy 是一个强大的 Python ORM(对象关系映射)库,它允许我们将数据库表映射到 Python 对象,并提供了丰富的关系模型来处理不同类型的关系,包括一对一关系。在本文中,我们将深入探讨 SQLAlchemy …

Ubuntu使用QtCreator + CMake 开发C/C++程序

平台 OS: Ubuntu 20.04 cmake: 3.16.3 IDE: Qt Creator 4.11.1 Based on Qt 5.14.1 (GCC 5.3.1 20160406 (Red Hat 5.3.1-6), 64 bit) Built on Feb 5 2020 12:48:30 From revision b2ddeacfb5 Copyright 2008-2019 The Qt Company Ltd. All rights reserved. The program …

Hugging Face怎么通过国内镜像去进行模型下载(hf-mirror.com)

一、引言 Hugging Face 🤗是一家专注于自然语言处理(NLP)技术的公司,以其开源贡献和先进的机器学习模型而闻名。该公司最著名的产品是 Transformers 库,这是一个广泛使用的 Python 库,它提供了大量预训练模…

(Bean实例化的基本流程 )学习Spring的第六天

Bean实例化的基本流程 其实可以解释为三个过程: 1 . 有关Bean的信息被封装成一个map集合 : DeanDefinitionMap . key为Bean的名称 , value为有关<bena>标签的信息 2 . Spring框架对这个存储Bean信息的Map进行遍历 , 进行创建对象 , 把创建好的对象存储到另一个Map集合里…

深入理解 Kubernetes Ingress:路由流量、负载均衡和安全性配置

Kubernetes Ingress 是 Kubernetes 集群中外部流量管理的重要组件。它为用户提供了一种直观而强大的方式&#xff0c;通过定义规则和配置&#xff0c;来控制外部流量的路由和访问。 1. 什么是 Ingress&#xff1f; 在 Kubernetes 中&#xff0c;Ingress 是一种 API 资源&#…

取代房子,中国又一种资本在崛起(深度)

我一直有一个观点&#xff1a;经济形势好的时候&#xff0c;只要不是夕阳行业&#xff0c;做什么都能过得不错。经济形势差的时候&#xff0c;对于个人来说&#xff0c;拼的就是学习能力。 10年前&#xff0c;在市场上很吃香的是MBA&#xff0c;那时候企业需要高速发展&#x…

JVM工作原理与实战(二十):直接内存

专栏导航 JVM工作原理与实战 RabbitMQ入门指南 从零开始了解大数据 目录 专栏导航 前言 一、直接内存 1.直接内存作用 二、在直接内存上创建数据 总结 前言 JVM作为Java程序的运行环境&#xff0c;其负责解释和执行字节码&#xff0c;管理内存&#xff0c;确保安全&…

Spring Cloud详细入门使用

文章目录 Spring Cloud服务注册与发现EurekaServer 启动服务注册服务发现 NacosServer启动 (nacos2.2.0)服务注册服务发现服务集群命名空间配置中心集群搭建 负载均衡RPC远程过程调用Feign使用 网关路由断言工厂网关过滤工厂跨域处理全局跨域配置单个微服务跨域配置 限流 熔断 …

Codeforces Round 919 (Div. 2) A~E

A. Satisfying Constraints(模拟) 题意&#xff1a; 给出 n n n个限制条件&#xff0c;问有多少个数字 k k k同时满足这些限制条件。 限制条件分为以下三种&#xff1a; k k k必须大于等于给出的一些数字 x x x k k k必须小于等于给出的一些数字 x x x k k k不能与给出的…

定时器开发基础

1定时器的基本概述 通过滴漏和漏沙瓶这两个例子简单讲述定时器的基本工作原理。 STM32的常见的定时器资源&#xff1a; 系统嘀嗒定时器SysTick、看门狗定时器WatchDog、实时时钟RTC、基本定时器、通用定时器、高级定时器。 系统嘀嗒定时器SysTick &#xff1a;这是一个集成在C…

JavaScript 自定义分页组件

仿boostrap 前端分页组件的实现 一 写一个前端自定义分页组件&#xff0c;需要考虑以下问题 需要一个<ul id"pagination"></ul>标签 total; // 总数据的数量 pageSize; // 一页显示数量 pageIndex; // 当前页 二 实现细节 编写html文件 index.html…

蓝桥杯(C++ 整数删除 优先队列 )

优先队列&#xff1a; 优先队列具有队列的所有特性&#xff0c;包括队列的基本操作&#xff0c;只是在这基础上添加了内部的一个排序&#xff0c;它本质是一个堆实现的。 1.头文件&定义 #include <queue> #include <functional> //greater<>// 定义 p…

2023 年顶级前端工具

谁不喜欢一个好的前端工具&#xff1f;在本综述中&#xff0c;您将找到去年流行的有用的前端工具&#xff0c;它们将帮助您加快开发工作流程。让我们深入了解一下&#xff01; 在过去的 12 个月里&#xff0c;我在我的时事通讯 Web Tools Weekly 中分享了数百种工具。我为前端…

经典数据库练习题及答案

数据表介绍 --1.学生表 Student(SId,Sname,Sage,Ssex) --SId 学生编号,Sname 学生姓名,Sage 出生年月,Ssex 学生性别 --2.课程表 Course(CId,Cname,TId) --CId 课程编号,Cname 课程名称,TId 教师编号 --3.教师表 Teacher(TId,Tname) --TId 教师编号,Tname 教师姓名 --4.成绩…

JavaScript DOM可以做什么?

1、通过id获取标签元素 DOM是文档对象模型&#xff0c;它提供了一些属性和方法来方便我们操作document对象&#xff0c;比如getElementById()方法可以通过某个标签元素的id来获取这个标签元素 // 用法 window.document.getElementById(id); // 例子 <!DOCTYPE html> &l…

深度学习模型之yolov8实例分割模型TesorRT部署-python版本

1 模型转换 从github上下载官方yolov8版本&#xff0c;当前使用的版本是2023年9月份更新的版本&#xff0c;作者一直在更新。官网地址 2 加载模型 模型的训练和测试在官方文档上&#xff0c;有详细的说明&#xff0c;yolov8中文文档这里不做过多说明&#xff0c;v8现在训练是…

智能驾驶新浪潮:SSD与UFS存储技术如何破浪前行?-UFS篇

如果说SSD是赛道上的超级跑车&#xff0c;那UFS更像是专为智能汽车定制的高性能轻量化赛车。UFS采用串行接口技术&#xff0c;像是闪电侠一样&#xff0c;将数据传输的速度推向新高&#xff0c;大幅缩短了系统启动时间和应用程序加载时间&#xff0c;这对追求即时反应的ADAS系统…

从零开始的 dbt 入门教程 (dbt core 命令进阶篇)

引 根据第一篇文章的约定&#xff0c;我想通过接下来的几篇文章带大家进一步了解 dbt 的用法&#xff0c;原计划这篇文章我会介绍 dbt 命令的进阶用法&#xff0c;进一步认识 dbt 的配置以及如何创建增量表等等零零散散十几个方面的知识点&#xff0c;结果在我写完命令部分发现…

深度学习中Numpy的一些注意点(多维数组;数据类型转换、数组扁平化、np.where()、np.argmax()、图像拼接、生成同shape的图片)

文章目录 1多维数组压缩维度扩充维度 2numpy类型转换深度学习常见的float32类型。 3数组扁平化4np.where()的用法5np.argmax()6图像拼接7生成同shape的图片&#xff0c;指定数据类型 1多维数组 a.shape(3,2);既数组h3&#xff0c;w2 a.shape(2,3,2);这里第一个2表示axis0维度上…