PE文件详解(七)

本文转载自小甲鱼PE文件讲解系列原文传送门 这次主要说明导出表,导出表一般记录着文件中函数的地址等相关信息,供其他程序调用,常见的.exe文件中一般不存在导出表,导出表更多的是存在于dll文件中。一般在dll中保存函数名称以及它的地址,当某个程序需要调用dll中的函数时,如果这个dll在内存中,则直接找到对应函数在内存中的位置,并映射到对应的虚拟地址空间中,如果在内存中没有对应的dll,则会先通过PE加载器加载到内存,然后再进行映射

导出表结构

导出表(Export Table)中的主要成分是一个表格,内含函数名称、输出序数等。 序数是指定DLL 中某个函数的16位数字,在所指向的DLL 文件中是独一无二的。 在此我们不提倡仅仅通过序数来索引函数的方法,这样会给DLL 文件的维护带来问题。 例如当DLL 文件一旦升级或修改就可能导致调用改DLL 的程序无法加载到需要的函数。

数据目录表的第一个成员指向导出表,是一个IMAGE_EXPORT_DIRECTORY(以后简称IED)结构,IED 结构的定义如下:

IMAGE_EXPORT_DIRECTORY STRUCT
    Characteristics     DWORD   ?   ; 未使用,总是定义为0
    TimeDateStamp       DWORD   ?       ; 文件生成时间
    MajorVersion        WORD    ?   ; 未使用,总是定义为0
    MinorVersion        WORD    ?   ; 未使用,总是定义为0
    Name            DWORD   ?   ; 模块的真实名称
    Base                DWORD   ?   ; 基数,加上序数就是函数地址数组的索引值
    NumberOfFunctions   DWORD   ?   ; 导出函数的总数
    NumberOfNames       DWORD   ?   ; 以名称方式导出的函数的总数
    AddressOfFunctions  DWORD   ?   ; 指向输出函数地址的RVA
    AddressOfNames      DWORD   ?   ; 指向输出函数名字的RVA
    AddressOfNameOrdinals   DWORD   ?   ; 指向输出函数序号的RVA
IMAGE_EXPORT_DIRECTORY ENDS

Name: 一个RVA 值,指向一个定义了模块名称的字符串。如即使Kernel32.dll 文件被改名为”Ker.dll”。 仍然可以从这个字符串中的值得知其在编译时的文件名是”Kernel32.dll”。

NumberOfFunctions: 文件中包含的导出函数的总数。

NumberOfNames: 被定义函数名称的导出函数的总数,显然只有这个数量的函数既可以用函数名方式导出。 也可以用序号方式导出,剩下的NumberOfFunctions 减去NumberOfNames 数量的函数只能用序号方式导出。 该字段的值只会小于或者等于 NumberOfFunctions 字段的值,如果这个值是0,表示所有的函数都是以序号方式导出的。

AddressOfFunctions: 一个RVA 值,指向包含全部导出函数入口地址的双字数组。数组中的每一项是一个RVA 值,数组的项数等于NumberOfFunctions 字段的值。 Base:导出函数序号的起始值,将AddressOfFunctions 字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出 序号。 假如Base 字段的值为x,那么入口地址表指定的第1个导出函数的序号就是x;第2个导出函数的序号就是x+1。 总之,一个导出函数的导出序号等 于Base 字段的值加上其在入口地址表中的位置索引值。 这个只是一个导出序号导出给外部进行使用的,当我们在分析PE文件进行相关函数的定址时,不使用这个序号,表中也没有存储函数的导出序号 AddressOfNames 和 AddressOfNameOrdinals: 均为RVA 值。前者指向函数名字符串地址表。 这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA。 数组的项数等于NumberOfNames 字段的值,所有有名称的导出函数的名称字符串都定义在这个表中;后者指向另一个word 类型的数组(注意不是双字数组)。 数组项目与文件名地址表中的项目一一对应,项目值代表函数入口地址表的索引,这样函 数名称与函数入口地址关联起来。 (举个例子说,加入函数名称字符串地址表的第n 项指向一个字符串“MyFunction”。 那么可以去查找 AddressOfNameOrdinals 指向的数组的第n 项,假如第n 项中存放的值是x,则表示AddressOfFunctions 字段描述的地址表中的第x 项函数入口地址对应的名称就是“MyFunction” 他们的关系如图所示:

一般在分析定位函数地址的时候采用的是通过函数名称来定位 在定位时可以使用序号的方式,也可以使用函数名的方式来定位,使用序号需要提前知道这个函数对应的序号,这个非常困难,还要一种方式是采用函数名找到对应函数的序号,然后再通过序号定位,一般在进行定位时都是使用函数名进行定位 1. 从序号查找函数入口地址 定位到PE 文件头 从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA 从导出表的 Base 字段得到起始序号 将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引 检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址

  1. 从函数名称查找入口地址

如果已知函数的名称,如何得到函数的入口地址呢?与使用序号来获取入口地址相比,这个过程要相对复杂一点! Windows 装载器的工作步骤如下: 最初的步骤是一样的,那就是首先得到导出表的地址 从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环 从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一 个函数名是符合的,表示文件中没有指定名称的函数 如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x 最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址 一帮情况下病毒程序就是通过函数名称查找入口地址的,因为病毒程序作为一段额外的代码被附加到可执行文件中的。 如果病毒代码中用到某些 API 的话,这些 API 的地址不可能在宿主文件的导出表中为病毒代码准备好。 因此只能通过在内存中动态查找的方法来实现获取API 的地址。 接下来就是来实际分析一个PE文件。 通过之前的知识,发现这个导出表的RVA = 0x00002060,表所在节区为.rdat,节区在内存中的RVA = 0x00002000,节区在文件中的偏移 = 0x00000600。通过之前的计算公式得到导出表在文件中的偏移为0x00000660. 定位到这个地方发现这个表中的内容如下: 通过解析知道 Name = 0x0000209c ==>0x0000069c Base = 0x00000002 NumberOfFunctions = 0x02 NumberOfNames = 0x02 AddressOfFunctions = 0x2088 ==>0x688 AddressOfNames = 0x2090==>0x690 AddressOfNameOrdinals = 0x2098 ⇒ 0x698 对于保存的是RVA的变量,后面都是通过换算得到的其值在内存中的偏移 对于AddressOfNames来说,它指向的是一个保存了函数名的RVA,我们在对应偏移位置得到它的值为0x20A8 ==> 0x6a8,从文件中的内容来看,这个位置保存到额正好是两个导出函数的值。

两个函数名分别为: _DecCount ==>0 _IncCount ==> 1 后面的是它们在这个位置的编号,等会需要这个编号,中的它们在函数地址表中对应的索引 接下来根据AddressOfNameOrdinals中的值,00 01,发现它们在函数地址表中的索引分别为0 1 最后再AddressOfFunctions中得到它们分别为0x1046和0x1023 也就是_DecCount = 0x1046 _IncCount = 0x1023 我们通过反汇编工具W32Dasm,查看这个dll的反汇编代码:

这个dll加载到内存中后它的基地址为0x10000000,这样得到两个函数在内存中的地址为: _DecCount =0x10001046 _IncCount =0x10001023 在它的反汇编中找到函数的地址发现正好是这两个值

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小灰灰

Java 线程学习

Java 线程相关 如何创建线程(两种方式,区别,使用场景) 线程状态调度 多线程数据共享(会有什么问题,如何实现共享,多线程操作同一个变量会有什么问题,如果不...

2439
来自专栏木宛城主

Unity应用架构设计(7)——IoC工厂理念先行

一谈到 『IoC』,有经验的程序员马上会联想到控制反转,将创建对象的责任反转给工厂。IoC是依赖注入 『DI』 的核心,大名鼎鼎的Spring框架就是一个非常...

2877
来自专栏专注 Java 基础分享

虚拟机类加载机制

虚拟机把字节码文件从磁盘加载进内存的这个过程,我们可以粗糙的称之为「类加载」,因为「类加载」不仅仅是读取一段字节码文件那么简单,虚拟机还要进行必要的「验证」、「...

4717
来自专栏玩转JavaEE

MongoDB数据类型

上篇文章我们介绍了MongoDB的最基本的增删改查操作,也介绍了一些基础的概念,MongoDB中每条记录称作一个文档,这个文档和我们平时用的JSON有点像,但也...

3405
来自专栏WindCoder

《Linux内核分析》之计算机是如何工作的 实验总结

马马虎虎学完了Python课程,一直想学下linux,看到里面有个linux的就选上了。当初没细看,如今听完第一节课有点傻眼,竟然糊里糊涂给自己找了一科汇编语言...

1271
来自专栏李航的专栏

Shell 主要逻辑源码级分析:SHELL 运行流程 (1)

分享一下在学校的时候分析shell源码的一些收获,帮助大家了解shell的一个工作流程,从软件设计的角度,看看shell这样一个历史悠久的软件的一些设计优点和缺...

2.1K0
来自专栏纯洁的微笑

jvm系列(一):java类的加载机制

1、什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Cl...

4016
来自专栏皮皮之路

【JVM】浅谈双亲委派和破坏双亲委派

笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自己读的有多粗糙,也体会到只有实践...

2642
来自专栏生信宝典

Bash概论 - Linux系列教程补充篇

本篇是我最开始学习Linux命令时看的一篇帖子,最早见于ChinaUnix (这次查找其出处时发现2002年就有这篇)。学习过程中,遇到问题就查一下。这次看到,...

1997
来自专栏枕边书

搭建自己的PHP框架心得(二)

续言 对于本次更新,我想说: 本框架由本人挑时间完善,而我还不是PHP大神级的人物,所以框架漏洞难免,求大神们指出。 本框架的知识点应用都会写在博客里,大家有什...

2618

扫码关注云+社区

领取腾讯云代金券