ELF&PE 文件结构分析

源代码如下:

永利酒店赌场 1

导出表:

ELF&PE 文件结构分析

说简单点,ELF 对应于UNIX 下的文件,而PE 则是Windows
的可执行文件,分析ELF 和 PE
的文件结构,是逆向工程,或者是做调试,甚至是开发所应具备的基本能力。在进行逆向工程的开端,我们拿到ELF
文件,或者是PE
文件,首先要做的就是分析文件头,了解信息,进而逆向文件。不说废话,开始分析:

ELF和PE 文件都是基于Unix 的 COFF(Common Object File Format)
改造而来,更加具体的来说,他是来源于当时著名的 DEC(Digital Equipment
Corporation) 的VAX/VMS 上的COFF文件格式。我们从ELF 说起。

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

image.png

  • 上篇文章,我们学习了各种头,Dos,NT,节表头,我们知道,OptionalHeader指向的DataDirectory[]数组一共有16个:

ELF

ELF 文件标准里把系统中采用ELF 格式的文件归类为四种:

  • 可重定位文件,Relocatable File
    ,这类文件包含代码和数据,可用来连接成可执行文件或共享目标文件,静态链接库归为此类,对应于Linux
    中的.o ,Windows 的 .obj.
  • 可执行文件,Executable File
    ,这类文件包含了可以直接执行的程序,它的代表就是ELF
    可执行文件,他们一般没有扩展名。比如/bin/bash ,Windows 下的 .exe
  • 共享目标文件,Shared Object File
    ,这种文件包含代码和数据,链接器可以使用这种文件跟其他可重定位文件的共享目标文件链接,产生新的目标文件。另外是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像来运行。对应于Linux
    中的 .so,Windows 中的 DLL
  • 核心转储文件,Core Dump
    File,当进程意外终止,系统可以将该进程地址空间的内容及终止时的一些信息转存到核心转储文件。
    对应 Linux 下的core dump。

ELF 文件的总体结构大概是这样的:

ELF Header
.text
.data
.bss
… other section
Section header table
String Tables, Symbol Tables,..
  • ELF
    文件头位于最前端,它包含了整个文件的基本属性,如文件版本,目标机器型号,程序入口等等。
  • .text
    为代码段,也是反汇编处理的部分,他们是以机器码的形式存储,没有反汇编的过程基本不会有人读懂这些二进制代码的。
  • .data
    数据段,保存的那些已经初始化了的全局静态变量局部静态变量
  • .bss
    段,存放的是未初始化的全局变量局部静态变量,这个很容易理解,因为在未初始化的情况下,我们单独用一个段来保存,可以不在一开始就分配空间,而是在最终连接成可执行文件的时候,再在.bss
    段分配空间。
  • 其他段,还有一些可选的段,比如.rodata 表示这里存储只读数据, .debug
    表示调试信息等等,具体遇到可以查看相关文档。
  • 自定义段,这一块是为了实现用户特殊功能而存在的段,方便扩展,比如我们使用全局变量或者函数之前加上
    **attribute(section(‘name’))** 就可以吧变量或者函数放到以name
    作为段名的段中。
  • 段表,Section Header Table ,是一个重要的部分,它描述了ELF
    文件包含的所有段的信息,比如每个段的段名,段长度,在文件中的偏移,读写权限和一些段的其他属性。

重定位表是一个数组,这个数组的大小记载在 _IMAGE_OPTIONAL_HEADER 的

Dos头:
  • Dos头的前两个字节恒为4D5A(只是作为判断PE文件的第一个标志,并不能通过它就能判断是否是PE文件)

  • Dos头的最后四个字节是指向NT头的偏移量
    只有前两个字节和后面四个字节关系到PE文件是否正常运行

  • NT头:

![](https://upload-images.jianshu.io/upload_images/5676193-1017ee68ff187eb6.png)

image.png
  • 前面四个字节恒为0x4550,用于判断是否为PE文件的第二个标志

  • Nt头后面就是各个区段信息

永利酒店赌场 2

ELF Header

ELF 文件信息的查看利器在Linux 下是是objdump, readelf,
相关命令较多,可查。下面我们从ELF 文件头说起。

文件头包含的内容很多,我们在Ubuntu 系统下使用 readelf 命令来查看ELF
文件头:

永利酒店赌场 3

我们以bash 这个可执行文件为例,我们可以看到ELF 文件头定义了ELF
魔数,文件机器字节长度,数据存储方式,版本,运行平台,ABI版本,ELF
重定位类型,硬件平台,硬件平台版本,入口地址,程序头入口和长度,段表的位置和长度,段的数量。

ELF 文件头的结构和相关常数一般定义在了 /usr/include/elf.h
中,我们可以进去查看一下:

永利酒店赌场 4

除了第一个,其他都是一一对应的,第一个是一个对应了Magic number, Class,
Data, Version, OS/ABI, ABI version.

出现在最开始的ELF Magic number, 16字节是用来标识ELF
文件的平台属性,比如字长,字节序,ELF
文件版本。在加载的时候,首先会确认魔数的正确性,不正确的话就拒绝加载。

另一个重要的东西是段表(Section Header Table)
,保存了各种各样段的基本属性,比如段名,段长度,文件中的偏移,读写权限,段的其他属性。而段表自己在ELF
文件中的位置是在ELF 头文件 e_shoff 决定的。

我们可以使用 objdump -h 的指令来查看ELF 文件中包含哪些段,以bash
这个可执行为例,其实除了我们之前说的哪些基本结构,他包含很多其他的结构:

永利酒店赌场 5

同样的,我们使用readelf -S 的指令也可以进行查看。

下面我们来看一下结构,还是到elf.h 中去查看,他的结构体名字叫
Elf32_Shdr,64位对应Elf64_Shdr,结构如下:

永利酒店赌场 6

以上结构中,分别对应于:

  • 段名
  • 段类型
  • 段标志位
  • 段虚拟地址
  • 段偏移
  • 段长度
  • 段链接
  • 段对齐
  • 项,一些大小固定的项,如符号表等。

这些项目,在使用readelf -S 指令时一一对应。

另外还有一个重要的表,叫重定位表,一般段名叫.rel.text,
在上边没有出现,链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,就是代码段和数据段中那些对绝对地址引用的位置,这个时候就需要使用重定位表了。

.DataDirect‌​ory[IMAGE_DIRECTORY_E‌​NTRY_BASERELOC].Size 成员中

文件头

永利酒店赌场 7

image.png

  • 文件头大小0x14个字节(由图可知:它是Nt头的第二个元素)

  • 扩展头的大小就在里面

  • 节的数量也在里面

  • 文件头里面保存了PE文件的一些属性(这里只列举了部分):
    1.是否是dll(0x0210),exe(0x010F)
    2.是否可执行

  • 我们接下来要学习的有:

字符串表

为什么会有字符串表呢?其实这个也是在不断发展改进中找到的解决办法,在ELF
文件中,会用到很多的字符串,段名,变量名等等,但是字符串其本身又长度不固定,如果使用固定结构来表示,就会带来空间上的麻烦。所以,构造一个字符串表,将使用的字符串统一放在那里,然后通过偏移量来引用字符串,岂不美哉。

需要使用的时候,只需要给一个偏移量,然后就到字符串该位置找字符串,遇到
就停止。

字符串在ELF 文件中,也是以段的形式保存的,常见的段名 .strtab, .shstrtab
两个字符串分别为字符串表和段表字符串,前者用来保存普通的字符串,后者保存段名。

在我们使用readelf -h 的时候,我们看到最后一个成员,section header string
table index ,实际上他指的就是字符串表的下标,bash
对应的字符串表下标为27,在使用objdump
的时候,实际上忽略了字符串表,我们使用readelf
,就可以看到第27位即字符串表:

永利酒店赌场 8


下面我们回顾一下,这个ELF 构造的精妙之处,当一个ELF
文件到来的时候,系统自然的找到他的开头,拿到文件头,首先看魔数,识别基本信息,看是不是正确的,或者是可识别的文件,然后加载他的基本信息,包括CPU
平台,版本号,段表的位置在哪,还可以拿到字符串表在哪,以及整个程序的入口地址。这一系列初始化信息拿到之后,程序可以通过字符串表定位,找到段名的字符串,通过段表的初始位置,确认每个段的位置,段名,长度等等信息,进而到达入口地址,准备执行。

当然,这只是最初始的内容,其后还要考虑链接,Import,Export
等等内容,留待以后完善。

结构图如下,图片中 0 和 000 都表示16进制数,转换到二进制是  0000 和 0000
0000 0000:

扩展头

永利酒店赌场 9

image.png

永利酒店赌场 10

image.png

  1. IMAGE_DIRECTORY_ENTRY_IMPORT 导入表
  2. IMAGE_DIRECTORY_ENTRY_BASERELOC 基址重定位表
  3. IMAGE_DIRECTORY_ENTRY_EXPORT 导出表
  4. IMAGE_DIRECTORY_ENTRY_RESOURCE 资源表

PE 文件

下面我们去看看更为常见的PE 文件格式,实际上PE 与 ELF
文件基本相同,也是采用了基于段的格式,同时PE
也允许程序员将变量或者函数放在自定义的段中, GCC
**attribute(section(‘name’))** 扩展属性。

PE 文件的前身是COFF,所以分析PE 文件,先来看看COFF
的文件格式,他保存在WinNT.h 文件中。

COFF 的文件格式和ELF 几乎一毛一样:

Image Header
SectionTable Image_SECTION_HEADER
.text
data
.drectve
.debug$S
… other sections
Symbol Table

文件头定义在WinNT.h 中,我们打开来看一下:

永利酒店赌场 11

我们可以看到,它这个文件头和ELF
实际上是一样的,也在文件头中定义了段数,符号表的位置,Optional Header
的大小,这个Optional Header 后边就看到了,他就是PE
可执行文件的文件头的部分,以及段的属性等。

跟在文件头后边的是COFF 文件的段表,结构体名叫 IMAGE_SECTION_HEADER :

永利酒店赌场 12

属性包括这些,和ELF 没差:

  • 段名
  • 物理地址 PhysicalAddress
  • 虚拟地址 VirtualAddress
  • 原始数据大小 Sizeof raw data
  • 段在文件中的位置 File pointer to raw data
  • 该段的重定位表在文件中的位置 File pointer to relocation table
  • 该段的行号表在文件中的位置 File pointer to line number
  • 标志位,包括段的类型,对齐方式,读取权限等标志。

永利酒店赌场 13

扩展头详解:
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;                          //表示这是一个什么类型的PE文件,32位一般是0x010B,64位的文件一般是0x020B
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;                  //所有代码区段(节)的总大小(基于文件对齐后的大小)
    DWORD   SizeOfInitializedData;            //已经初始化的数据的总大小
    DWORD   SizeOfUninitializedData;        //未初始化的数据的大小
    DWORD   AddressOfEntryPoint;            //程序开始执行的相对虚拟地址,即OEP,这是一个RVA,要想得到VA,则必须要加上ImageBase(下面有介绍!!!!!)
    DWORD   BaseOfCode;              //起始代码的相对虚拟地址(RVA),就是.text段的RVA
    DWORD   BaseOfData;              //  其实数据的相对虚拟地址(RVA),就是.data段的RVA

    //
    // NT additional fields.
    //

    DWORD   ImageBase;                //默认加载地址(如果没有这个基址会发生重定位)
    DWORD   SectionAlignment;          //块对齐数,一般是0x1000
    DWORD   FileAlignment;                    //文件对齐数,一般是0x200
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;                      //把文件加载进内存,所需要的内存大小,是进行了内存对齐之后的大小
    DWORD   SizeOfHeaders;                //所有头部大小(这是按照文件对齐后的大小),也是文件主体相对文件起始的偏移,是所有头+节表的大小
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;                    //文件(包括exe和dll文件)特征标志(见下面一张图)
    DWORD   SizeOfStackReserve;                   //表示进程中栈可以增长到的最大值,一般1M
    DWORD   SizeOfStackCommit;                    //进程中栈的初始值,据说也是栈每次分配增长的值,一般4KB
    DWORD   SizeOfHeapReserve;                      //表示进程中堆可以增长到的最大值,一般1M
    DWORD   SizeOfHeapCommit;                        //进程堆的初始值
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;      //数据目录的个数,也就是下面那个数组中元素的个数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表,比较重要!
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

永利酒店赌场 14

image.png

扩展头里面比较重要的在上面已经做出注释

  • 今天我们学习导出表:

DOS 头

在我们分析PE 的之前,还有另外一个头要了解一下,DOS
头,不得不说,微软事儿还是挺多的。

微软在创建PE 文件格式时,人们正在广泛使用DOS
文件,所以微软为了考虑兼容性的问题,所以在PE 头的最前边还添加了一个
IMAGE_DOS_HEADER 结构体,用来扩展已有的DOS EXE。在WinNTFS.h
里可以看到他的身影。

永利酒店赌场 15

DOS
头结构体的大小是40字节,这里边有两个重要的成员,需要知道,一个是e_magic
又见魔数,一个是e_lfanew,它只是了NT 头的偏移。

对于PE 文件来说,这个e_magic,也就是DOS 签名都是MZ,据说是一个叫 Mark
Zbikowski 的开发人员在微软设计了这种ODS 可执行文件,所以…

我们以Windows 下的notepad++
的可执行文件为例,在二进制编辑软件中打开,此类软件比较多,Heditor 打开:

永利酒店赌场 16

开始的两个字节是4D5A,e_lfanew 为00000108 注意存储顺序,小端。

你以为开头加上了DOS 头就完事了么,就可以跟着接PE 头了么。为了兼容DOS
当然不是这么简单了,紧接着DOS 头,跟的是DOS 存根,DOS
stub。这一块就是为DOS 而准备的,对于PE 文件,即使没有它也可以正常运行。

永利酒店赌场 17

旁边的ASCII 是读不懂的,因为他是机器码,是汇编,为了在DOS
下执行,对于notepad++ 来说,这里是执行了一句,this program cannot be run
in DOS mode 然后退出。逗我= =,有新的人,可以在DOS
中创造一个程序,做一些小动作。

每个元素的大小都记载在 SizeOfBlock 中,这个元素是由 一个
_IMAGE_BASE_RELOCATION 结构体和一个TypeOffset 数组组成的。TypeOffset
数组的每个元素占2个字节,其中,高4位是偏移类型(type),低12位表示需要重定位的地址(Offset),即,它与
VirtualAddress 相加即是指向 PE 映像中需要修改的那个代码的RVA。

再小结一波:

ImageBase(映像基址,装载基址,它是一个VA值):如果没有加载到这个地址则会重定位(就是PE文件加载进内存之后,就相当于知道了Dos头的位置,然后就可以知道其他的位置了),就是PE装入内存的基地址,默认情况下,EXE文件在内存中的基地址是0X0040
0000,DLL文件为0x0100 0000,由编译器决定!
程序入口点(OEP)
映像大小(SizeOfImage)————>把文件加载进内存,所需要的内存大小(注意是进行了块对齐之后)
代码大小(SizeOfCode)——>所有区段的总大小
代码基址(BaseOfCode)起始代码的 RVA—->.text的RVA
数据基址(BaseOfData)起始数据的RVA—–>.data的RVA
头大小(SizeOfHeaders)————>所有头部大小,就是文件主体相对文件起始的偏移
内存对齐(SectionAlignment)———–>为0x1000(4KB)
文件对齐(FileAlignment)—————->200h(0x200)
DLL标记(DllCharacteristics)——–>指示Dll特征的标志

  1. 我们知道dll文件,是动态链接库,里面有许多函数给别人调用,但是别人怎么知道里面有什么函数呢?就需要导出表清单给人家看,就如同你去餐厅点餐,却不知道餐厅有什么菜,这时服务生会拿出菜单来,这个菜单就如同导出表。
  2. 所有PE文件都可以有导出表,只是大部分情况下,exe不提供导出表而已。

NT头

下面进入正题,在HEditor 上也看到了PE,这一块就是正式的步入PE 的范畴。

永利酒店赌场 18

这是32位的PE
文件头定义,64位对应改。第一个成员就是签名,如我们所说,就是我们看到的「PE」,对应为50450000h。

这里边有两个东西,第一个就是我们之前看到的COFF
文件头,这里直接放进来了,我们不再分析。

看第二个,IMAGE_OPTIONAL_HEADER
不是说这个头可选,而是里边有些变量是可选的,而且有一些变量是必须的,否则会导致文件无法运行:

永利酒店赌场 19

有这么几个需要重点关注的成员,这些都是文件运行所必需的:

  1. Magic 魔数,对于32结构体来说是10B,对于64结构体来说是20B.
  2. AddressOfEntryPoint 持有EP 的RVA
    值,之处程序最先执行的代码起始位置,也就是程序入口。
  3. ImageBase 进程虚拟内存的范围是0-FFFFFFFF (32位)。PE
    文件被加载到这样的内存中,ImageBase 指出文件的优先装入位置。
  4. SectionAlignment, FileAlignment PE 文件的Body
    部分划分为若干段,FileAlignment
    之处段在磁盘文件中的最小单位,SectionAlignment指定了段在内存中的最小单位。
  5. SizeOfImage 指定 PE Image 在虚拟内存中所占的空间大小。
  6. SizeOfHeader PE 头的大小
  7. Subsystem 用来区分系统驱动文件与普通可执行文件。
  8. NumberOfRvaAndSizes 指定DataDirectory
    数组的个数,虽然最后一个值,指出个数是16,但实际上PE
    装载还是通过识别这个值来确定大小的。至于DataDirectory 是什么看下边
  9. DataDirectory 它是一个由IMAGE_DATA_DIRECTORY
    结构体组成的数组,数组每一项都有定义的值,里边有一些重要的值,EXPORT/IMPORT/RESOURCE,
    TLS direction 是重点关注的。

偏移类型的含义如下:

PE头部包含了Dos头,一直到节表的结束位置,.text区段开始之前
  • 数据目录表
![](https://upload-images.jianshu.io/upload_images/5676193-cfbc56c21cd7568f.png)

image.png
  • 我们知道OptionalHeader指向的DataDirectory[]数组一共有16个,每个都是一样的结构:

段头

PE 的段头直接沿用的COFF 的段头结构,上边也说过了,我们查看notepad++
的段头,可以获得各个段名,以及其信息,这里,我们可以使用一些软件查看,更加方便:

永利酒店赌场 20

Constant

Value

Description

IMAGE_REL_BASED_ABSOLUTE

  0

The base relocation is skipped. This type can be used to pad a block.

IMAGE_REL_BASED_HIGH

  1

The base relocation adds the high 16 bits of the difference to the 16bit field at offset. The 16-bit field represents the high value of a 32-bit word.

IMAGE_REL_BASED_LOW

  2

The base relocation adds the low 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the low half of a 32-bit word.

IMAGE_REL_BASED_HIGHLOW

  3

The base relocation applies all 32 bits of the difference to the 32-bit field at offset.

IMAGE_REL_BASED_HIGHADJ

  4

The base relocation adds the high 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the high value of a 32-bit word. The low 16 bits of the 32-bit value are stored in the 16-bit word that follows this base relocation. This means that this base relocation occupies two slots.

IMAGE_REL_BASED_MIPS_JMPADDR

  5

The relocation interpretation is dependent on the machine type.

When the machine type is MIPS, the base relocation applies to a MIPS jump instruction.

IMAGE_REL_BASED_ARM_MOV32

  5

This relocation is meaningfull only when the machine type is ARM or Thumb. The base relocation applies the 32-bit address of a symbol across a consecutive MOVW/MOVT instruction pair.

IMAGE_REL_BASED_RISCV_HIGH20

  5

This relocation is only meaningful when the machine type is RISC-V. The base relocation applies to the high 20 bits of a 32-bit absolute address.

 

  6

Reserved, must be zero.

IMAGE_REL_BASED_THUMB_MOV32

  7

This relocation is meaningful only when the machine type is Thumb. The base relocation applies the 32-bit address of a symbol to a consecutive MOVW/MOVT instruction pair.

IMAGE_REL_BASED_RISCV_LOW12I

  7

This relocation is only meaningful when the machine type is RISC-V. The base relocation applies to the low 12 bits of a 32-bit absolute address formed in RISC-V I-type instruction format.

IMAGE_REL_BASED_RISCV_LOW12S

  8

This relocation is only meaningful when the machine type is RISC-V. The base relocation applies to the low 12 bits of a 32-bit absolute address formed in RISC-V S-type instruction format.

IMAGE_REL_BASED_MIPS_JMPADDR16

  9

The relocation is only meaningful when the machine type is MIPS. The base relocation applies to a MIPS16 jump instruction.

IMAGE_REL_BASED_DIR64

10

The base relocation applies the difference to the 64-bit field at offset.

数据目录表也是一个结构体数组——>每一个结构体里面记录的是每个表所对应的RVA以及大小

(扩展:

  • 区段头表(它是一个结构体数组)是由多个IMAGE_SECTION_HEADER这样的结构体组成,以一个全是0的结构体结尾;
  • 导入表也是一个结构体数组(后面会重点讲),以一个全0元素结尾,导入表中的IMAGE_THUNK_DATA(文件没有加载的时候,OrignalFirstThunk与FirstThunk指向IMAGE_THUNK_DATA)也是一个结构体数组;
  • 重定位表:它也是一个结构体数组,以全0元素结尾
  • 资源表:它里面也包含结构体数组(更为详细的可以查阅相关文献)
    )
  1. VirtualAddress  虚拟偏移地址
  2. size  大小

RVA to RAW

理解PE
最重要的一个部分就是理解文件从磁盘到内存地址的映射过程,做逆向的人员,只有熟练地掌握才能跟踪到程序的调用过程和位置,才能分析和寻找漏洞。

对于文件和内存的映射关系,其实很简单,他们通过一个简单的公式计算而来:

永利酒店赌场 21

换算公式是这样的:

RAW -PointToRawData = RVA – VirtualAddress

寻找过程就是先找到RVA
所在的段,然后根据公式计算出文件偏移。因为我们通过逆向工具,可以在内存中查找到所在的RVA,进而我们就可以计算出在文件中所在的位置,这样,就可以手动进行修改。

看回我们刚才载入的nodepad++ ,其中的V Addr, 实际上就是VirtualAddress,R
offset 就是PointerToRawData。

永利酒店赌场 22

假如我们的RVA 地址是5000,那么计算方法就是,查看区段,发现在.text
中,5000-1000+400 = 4400,这就是RAW
00004400,而实际上,因为我们的ImageBase
是00400000,所以,我们在反编译时候内存中的地址是00405000.

接下来,使我们的PE头中的核心内容,IAT 和 EAT,也就是 Import address
table, export address table.

其他中文翻译:

PE中有结构体数组的结构的总结:

数录节入重!

  • 节表(区段头表)

永利酒店赌场 23

image.png

.text 段:代码段
.data段:数据段
.bss段:表示未初始化的数据,比如Static变量
.rdata 段:表示只读的数据,比如字符串
……
.relcoc段:存储重定位信息的区段
各变量存放于哪个区:
常量 ——————>.rdata区
静态变量————->.bss区
永利酒店赌场,全局变量————–>.data 区
节表里面的几个重要数据:
VirtualAddress:这个区段的相对虚拟地址
SizeofRawData:这个区段在磁盘中的大小,进行了文件对齐
PointerToRawData:区段的文件偏移,就是这个区段在磁盘文件中的起始位置
一个重要的公式:
offset(转)=RVA(需要转换的RVA)-RVA(所在区段的RVA)+offset(就是PointerToRawData)

  • NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT
    ],这个数据目录表VirtualAddress指向的是导出表的地址:

IAT

导入地址表的内容与Windows 操作系统的核心进程,内存,DLL
结构有关。他是一种表格,记录了程序使用哪些库中的哪些函数。

下面,让我们把目光转到DLL 上,Dynamic Linked Library 支撑了整个 OS。DLL
的好处在于,不需要把库包含在程序中,单独组成DLL
文件,需要时调用即可,内存映射技术使加载后的DLL
代码,资源在多个进程中实现共享,更新库时候只要替换相关DLL 文件即可。

加载DLL 的方式有两种,一种是显式链接,使用DLL
时候加载,使用完释放内存。另一种是隐式链接,程序开始就一同加载DLL,程序终止的时候才释放掉内存。而IAT
提供的机制与隐式链接相关,最典型的Kernel32.dll。

我们来看看notepad++ 调用kernel32.dll 中的CreateFileW, 使用PE
调试工具Ollydbg

永利酒店赌场 24

我们看到填入参数之后,call 了35d7ffff 地址的内容,然后我们去dump
窗口,找一下kernel.CreateFileW:

永利酒店赌场 25

我们双击汇编窗口,启动编辑,发现确实是call 的这个数值:

永利酒店赌场 26

可是问题来了,上边是E8 35D7FFFF,下边地址却是 00C62178。其实这是Win
Visita, Win 7的ASLR
技术,主要就是针对缓冲溢出攻击的一种保护技术,通过随机化布局,让逆向跟踪者,难以查找地址,就难以简单的进行溢出攻击。不过还是可以通过跳板的方式,找到溢出的办法,这就是后话了。

现在可以确定的是,35D7FFFF 可以认为保存的数值就是 CreateFileW
的地址。而为什么不直接使用CALL 7509168B 这种方式直接调用呢?
Kernel32.dll 版本各不相同,对应的CreateFileW
函数也各不相同,为了兼容各种环境,编译器准备了CreateFileW
函数的实际地址,然后记下DWORD PTR DS:[xxxxxx]
这样的指令,执行文件时候,PE 装载器将CreateFileW 函数地址写到这个位置。

同时,由于重定位的原因存在,所以也不能直接使用CALL 7509168B
的方式,比如两个DLL 文件有相同的
ImageBase,装载的时候,一个装载到该位置之后,另一个就不能装载该位置了,需要换位置。所以我们不能对实际地址进行硬编码。

IMAGE_IMPORT_DESCRIPTOR

永利酒店赌场 27

永利酒店赌场 28

对于一个普通程序来说,需要导入多少个库,就会存在多少个这样的结构体,这些结构体组成数组,然后数组最后是以NULL
结构体结束。其中有几个重要的成员:

  • OriginalFirstThunk INT Import Name Table 地址,RVA
  • Name 库名称字符串地址,RVA,就是说该地址保存库名称
  • First Thunk IAT 地址 RVA
  • INT 中个元素的值是上边那个IMAGE_IMPORT_BY_NAME 结构体指针。
  • INT 与 IAT 大小应相同。

那么PE 是如何导入函数输出到IAT 的:

  1. 读取NAME 成员,获取扩名称字符串
  2. 装载相应库: LoadLibrary(“kernel32.dll”)
  3. 读取OriginalFirstThunk成员,获取INT 地址
  4. 读取INT 数组中的值,获取相应的
    IMAGE_IMPORT_BY_NAME地址,是RVA地址
  5. 使用IMAGE_IMPORT_BY_NAME 的Hint 或者是name
    项,获取相应函数的起始位置 GetProcAddress(“GetCurrentThreadId”)
  6. 读取FistrThunk 成员,获得IAT 地址。
  7. 将上面获得的函数地址输入相应IAT 数组值。
  8. 重复4-7 到INT 结束。

这里就产生了一个疑惑,OriginalFirstThunk 和 First Thunk
都指向的是函数,为什么多此一举呢?

首先,从直观上说,两个都指向了库中引入函数的数组,鱼C 画的这张图挺直观:

永利酒店赌场 29

OriginalFirstThunk 和 FirstThunk 他们都是两个类型为IMAGE_THUNK_DATA
的数组,它是一个指针大小的联合(union)类型。
每一个IMAGE_THUNK_DATA
结构定义一个导入函数信息(即指向结构为IMAGE_IMPORT_BY_NAME
的家伙,这家伙稍后再议)。
然后数组最后以一个内容为0 的 IMAGE_THUNK_DATA 结构作为结束标志。
IMAGE_THUNK_DATA32 结构体如下:

永利酒店赌场 30

因为是Union 结构,IMAGE_THUNK_DATA 事实上是一个双字大小。
规定如下:

当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低
31位被看作一个函数序号。

当 IMAGE_THUNK_DATA 值的最高位为
0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个
RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。

我们再看IMAGE_IMPORT_BY_NAME 结构:

永利酒店赌场 31

结构中的 Hint
字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0。

Name 字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

现在重点来了:

第一个数组(由 OriginalFirstThunk
所指向)是单独的一项,而且不能被改写,我们前边称为 INT。第二个数组(由
FirstThunk 所指向)事实上是由 PE 装载器重写的。

PE 装载器装载顺序正如上边所讲的那样,我们再将它讲详细一点:

PE 装载器首先搜索 OriginalFirstThunk
,找到之后加载程序迭代搜索数组中的每个指针,找到每个
IMAGE_IMPORT_BY_NAME
结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由
FirstThunk 数组中的一个入口,因此我们称为输入地址表(IAT).

继续套用鱼C 的图,就能直观的感受到了:

永利酒店赌场 32

所以,在读取一次OriginalFirstThunk 之后,程序就是依靠IAT
提供的函数地址来运行了。

常量 描述
IMAGE_REL_BASED_ABSOLUTE       0x0 使块按照32位对齐,位置为0。
IMAGE_REL_BASED_HIGH 0x1 高16位必须应用于偏移量所指高字16位。
IMAGE_REL_BASED_LOW 0x2 低16位必须应用于偏移量所指低字16位。
IMAGE_REL_BASED_HIGHLOW 0x3 全部32位应用于所有32位。
IMAGE_REL_BASED_HIGHADJ 0x4 需要32位,高16位位于偏移量,低16位位于下一个偏移量数组元素,组合为一个带符号数,加上32位的一个数,然后加上8000然后把高16位保存在偏移量的16位域内。

EAT

搞清楚了IAT 的原理,EAT
就好理解了,目前这篇总结的有点长了,我长话短说。IAT
是导入的库和函数的表,那么EAT
就对应于导出,它使不同的应用程序可以调用库文件中提供的函数,为了方便导出函数,就需要保存这些导出信息。

回头看PE 文件中的PE头我们可以看到IMAGE_EXPORT_DIRECTORY
结构体以的位置,他在IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress
的值就是 IMAGE_EXPORT_DIREDCTORY 的起始位置。

永利酒店赌场 33

IMAGE_EXPORT_DIRECTORY结构体如下:

永利酒店赌场 34

这里边同样是这么几个重要的成员:

  • NumberOfFunctions 实际Export 函数的个数
  • NumberOfNames Export 函数中具名的函数个数
  • AddressOfFunctins Export 函数地址数组,数组个数是上边的NOF
  • AddressOfNames 函数名称地址数组,个数是上边的NON
  • AddressOfNameOrdinals Ordinal 地址数组,个数等于上边NON
  • Name 一个RVA 值,指向一个定义了模块名称的字符串。如即使Kernel32.dll
    文件被改名为”Ker.dll”。仍然可以从这个字符串中的值得知其在编译时的文件名是”Kernel32.dll”。
  • Base:导出函数序号的起始值,将AddressOfFunctions
    字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出
    序号。
    以kernel32.dll 为例,我们看一下:
![](https://upload-images.jianshu.io/upload_images/30117-6bb373c33a5b9995.jpg)

从上边这些成员,我们实际上可以看出,是有两种方式提供给那些想调用该库中函数的,一种是直接从序号查找函数入口地址导入,一种是通过函数名来查找函数入口地址导入。

先上一个鱼C 的图,方便理解:

永利酒店赌场 35

上边图,注意一点,因为AddressOfNameOrdinals
的序号应当是从0开始的,不过图中映射的是第二个函数指向的序号1。

我们分别说一下两种方式:

当已知导出序号的时候

  1. Windows 装载器定位到PE 文件头,
  2. 从PE 文件头中的 IMAGE_OPTIONAL_HEADER32
    结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA ,
  3. 从导出表的 Base 字段得到起始序号,
  4. 将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引,
  5. 检测索引值是否大于导出表的 NumberOfFunctions
    字段的值,如果大于后者的话,说明输入的序号是无效的用这个索引值在
    AddressOfFunctions
    字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA
    值,当函数被装入内存的时候,这个RVA
    值加上模块实际装入的基地址,就得到了函数真正的入口地址

当已知函数名称查找入口地址时

  1. 从导出表的 NumberOfNames
    字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环
  2. 从 AddressOfNames
    字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数,如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在
    AddressOfNamesOrdinals
    指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x
  3. 最后,以 x 值作为索引值,在 AddressOfFunctions
    字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址

一般来说,做逆向或者是写代码都是第二种方法,我们以kernel32.dll
中的GetProcAddress 函数为例,其操作原理如下:

  1. 利用 AddressOfNames 成员转到 『函数名称数组』
  2. 『函数名称数组』中存储着字符串地址,通过比较字符串,查找指定的函数名称,此时数组所以为成为name_index
  3. 利用 AddressOfNameOrdinals 成员,转到这个序号数组
  4. 在ordinal 数组中通过name_index 查找到相应的序号
  5. 利用AddressOfFunctions 成员,转到『函数地址数组』EAT
  6. 在EAT 中将刚刚得到的ordinal 作为索引,获得指定函数的入口地址

写了这么多,实际上算是对文件结构有了一个入门的认识,至少知道在程序运行过程中,系统是如何进行操作和链接的,而更加详细的内容注入运行时压缩,DLL
注入,API 钩取等技术,就需要在这个基础之上继续挖掘,所以PE ,ELF
文件结构的分析是相当重要的。

PS. 参考:
鱼C 讲解PE
文件格式之INT
《Windows PE 权威指南》
《逆向工程核心原理》
《程序员的自我修养-链接,装载与库》

例子:

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

 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY

分析常见的dll:在QQ中的 zlib.dll 文件 (在QQ安装目录下的bin文件夹中):

  1. Characteristics:现在没有用到,一般为0。
  2. TimeDateStamp:导出表生成的时间戳,由连接器生成。
  3. MajorVersion,MinorVersion:看名字是版本,实际貌似没有用,都是0。
  4. Name:模块的名字,就是dll的名称。
  5. Base:序号的基数,按序号导出函数的序号值从Base开始递增。
  6. NumberOfFunctions:所有导出函数的数量。
  7. NumberOfNames:按名字导出函数的数量。
  8. AddressOfFunctions:一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。
  9. AddressOfNames:一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。
  10. AddressOfNameOrdinals:一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。

首先找到重定位表,这里使用工具:

永利酒店赌场 36

typedef struct _IMAGE_IMPORT_BY_NAME 
{
 WORD Hint;
 CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

找到数据:

永利酒店赌场 37

永利酒店赌场 38

永利酒店赌场 39

VirtualAddress 为 0x1000,SizeOfBlock 为 0x64。第一个条目为
0x338C,高四位为 0x3,offset为 0x38C,即偏移地址为 0x138C (由 0x1000 +
0x38C得来)应用于此地址上全部32位。打开C32Asm反汇编查看:

  • AddressOfFunctions 指向所有函数的地址。
  • AddressOfNames 指向名字的地址。
  • AddressOfNameOrdinals 指向一个序号。

永利酒店赌场 40

  • 查找导出表代码(c/c++):

    int main(int argc, char *argv[])
    {
    PIMAGE_DOS_HEADER Pdos = (PIMAGE_DOS_HEADER)GetModuleHandle(L”user32.dll”);

 PIMAGE_NT_HEADERS Pnt = (PIMAGE_NT_HEADERS)((int)Pdos->e_lfanew + (int)Pdos);

 IMAGE_OPTIONAL_HEADER32 Popt = Pnt->OptionalHeader;

 IMAGE_EXPORT_DIRECTORY * Export;
 Export = (IMAGE_EXPORT_DIRECTORY*)(Popt.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (ULONG_PTR)Pdos);

 DWORD * AllAddress;
 DWORD * AllName;
 USHORT * AllOrg;

 AllAddress = (DWORD*)((int)Export->AddressOfFunctions + (int)Pdos);//函数地址数组
 AllName = (DWORD*)((int)Export->AddressOfNames + (int)Pdos);//函数名称数组
 AllOrg = (USHORT *)((int)Export->AddressOfNameOrdinals + (int)Pdos);//序号数组


 int OneAddress;
 char * OneName;
 USHORT OneOrg;
 char * Buf = new char[500];
 int ListId = NULL;

 for (int i = 0; i < (int)Export->NumberOfNames; i++)
 {

 OneName = (char*)((BYTE*)Pdos + AllName[i]);
 OneOrg = (USHORT)AllOrg[i];
 OneAddress = (int)((int)Pdos + AllAddress[OneOrg]);

 printf("Name: %s, Org :%d,Address :%xn", OneName, OneOrg, OneAddress);
 }

 return 0;
}

永利酒店赌场 41

 

网站地图xml地图