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

这篇具有很好参考价值的文章主要介绍了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则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该数据类型一个数据项的大小。这俩成员成为定位各种表的关键,所以一定要了解知道每个数组元素所指向的数据类型,请看下表:

PE解释器之PE文件结构(二),算法,数据结构

PE解释器之PE文件结构(二),算法,数据结构

//定位目录项的方法(以导出表为例):    所有操作都在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个。

导出表结构图:

PE解释器之PE文件结构(二),算法,数据结构

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

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

PE解释器之PE文件结构(二),算法,数据结构

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

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

PE解释器之PE文件结构(二),算法,数据结构

(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是否一致,如果一致就不会自动修复重定位表,双击运行时就会报错。

重定位表结构:

PE解释器之PE文件结构(二),算法,数据结构

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

PE解释器之PE文件结构(二),算法,数据结构

四: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文件载入后对应的导入表结构图:

PE解释器之PE文件结构(二),算法,数据结构

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文件载入前对应的导入表结构图:PE解释器之PE文件结构(二),算法,数据结构

 五: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;

 文章来源地址https://www.toymoban.com/news/detail-808442.html

绑定导入表简介:绑定导入是一个文件快速启动的技术,但是只能起到辅助的效果,它的存在只会影响到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的大小结构相同,所以可以相互转型,方便编程。

 PE解释器之PE文件结构(二),算法,数据结构

 

到了这里,关于PE解释器之PE文件结构(二)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】 /bin/bash^M: 坏的解释器: 没有那个文件或目录[已解决]

    Windows 10 系统下编辑了一个shell脚本文件,然后copy到了远程的Linux服务器。 运行 xxx.sh 文件时报错 这个文件在Windows下编辑过,在Windows下每一行结尾是nr,而Linux下则是n,所以才会有多出来的r。 使用指令 会把 xxx.sh 中的r 替换成空白。

    2024年02月12日
    浏览(35)
  • Linux 报错 bash: /usr/bin/pip: 解释器错误:没有那个文件或目录

            今天在linux安装python库时,使用conda安装太慢,换了镜像源也没用,于是使用pip安装出现了解释器错误:没有那个文件或目录的问题,记录一下,或许对你有帮助。                 进入home/你的用户名/anaconda3/envs/虚拟环境名字/bin/pip,不能直接用cd进去,可以使用na

    2024年01月18日
    浏览(78)
  • 『pycharm 』解决无法添加解释器,无法新建项目问题(是无法添加解释器,不是没有添加解释器)

    这个问题和大家常见的: 没有Python解释器 这种简单问题不同。请看问题描述。👇👇 如图,解释器列表空空如也,即使点击“ 添加本地解释器 ”也没有反应(那个展开小三角也是空的)。 所有项目都这样了,那里变成空的了。而且这些都是我自己的项目,之前都好好的。 删

    2024年02月11日
    浏览(46)
  • Pycharm 重命名SSH的conda解释器后,提示该解释器不可用

    目前已知是BUG,截至投稿日未修复 已经提交给youtrack.jetbrains.com,如下图:

    2024年01月17日
    浏览(44)
  • 解锁Spring Boot中的设计模式—02.解释器模式:探索【解释器模式】的奥秘与应用实践!

    解释器模式(Interpreter Pattern)是一种行为设计模式,它用于定义语言的文法,并且解释语言中的表达式。在Java中,解释器模式可以用于构建解释器以解析特定的语言或表达式,如数学表达式、查询语言等。 优点: 灵活性: 解释器模式可以 灵活地添加新的表达式和规则 ,因

    2024年02月19日
    浏览(39)
  • 解释器设计模式

    解释器设计模式(Interpreter Pattern)是一种行为型设计模式,它定义了一种语言的文法,并建立一个解释器来解释该语言中的句子。这种模式通常用于需要解释或执行一种特定类型的语言的场景,例如编程语言的编译器或解释器、规则引擎系统等。 关键组成部分 抽象表达式(

    2024年02月21日
    浏览(38)
  • 解释器模式(Interpreter)

    解释器模式是一种行为设计模式,可以解释语言的语法或表达式。给定一个语言,定义它的文法的一种表示,然后定义一个解释器,使用该文法来解释语言中的句子。解释器模式提供了评估语言的语法或表达式的方式。 解释器模式包含如下角色: Context,上下文,包含解释器

    2024年02月14日
    浏览(29)
  • 解释器模式简介

    解释器模式( Interpreter Pattern )是一种行为型设计模式,它用于定义语言的文法,并解析和执行给定语言中的表达式。该模式将每个表达式表示为一个类,并提供了一种方式来组合这些表达式以实现复杂的语句。 定义了一种简单、可扩展的语法规则,使得可以灵活地处理不同

    2024年02月07日
    浏览(32)
  • 什么是解释器模式

    程序员必知!解释器模式的实战应用与案例分析 - 知乎 上边这篇文章写的比较好,有定义,有例子,还划了重点,引用一下重点: 解释器模式在日常Java开发过程中使用的非常多,它最大的优点在于灵活性:能动态地解释和执行代码,这在处理复杂逻辑或多变需求时特别有用

    2024年01月19日
    浏览(32)
  • 设计模式(十五)解释器

    定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种行为型模式。 解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它主要用于描述如何使用面向对象语言构成一个简单的语言

    2024年02月05日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包