《2021年10月9日》 【连续第9天总结】
文章目录
ELF结构
简介
ELF (Executable and Linkable Format)文件,也就是在 Linux 中的目标文件,主要有以下三种类型
- 可重定位文件(Relocatable File),包含由编译器生成的代码以及数据。链接器会将它与其它目标文件链接起来从而创建可执行文件或者共享目标文件。在 Linux 系统中,这种文件的后缀一般为
.o
。 - 可执行文件(Executable File),就是我们通常在 Linux 中执行的程序。
- 共享目标文件(Shared Object File),包含代码和数据,这种文件是我们所称的库文件,一般以
.so
结尾。一般情况下,它有以下两种使用情景:- 链接器(Link eDitor, ld)可能会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件。
- 动态链接器(Dynamic Linker)将它与可执行文件以及其它共享目标组合在一起生成进程镜像。
目标文件由汇编器和链接器创建,是文本程序的二进制形式,可以直接在处理器上运行。那些需要虚拟机才能够执行的程序 (Java) 不属于这一范围。
这里我们主要关注于 ELF 的文件格式。
文件格式
目标文件既会参与程序链接又会参与程序执行。出于方便性和效率考虑,根据过程的不同,目标文件格式提供了其内容的两种并行视图,如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjG7fq7v-1633791830799)(D:\PWN总结\高级ROP\ELF篇\截图\1.png)]
可以看成是四部分组成:ELF头+程序头部表+节区+节区头部表
首先,我们来关注一下链接视图。
-
文件开始处是 ELF 头部( ELF Header),它给出了整个文件的组织情况。
-
如果程序头部表(Program Header Table)存在的话,它会告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。
-
节区部分包含在链接视图中要使用的大部分信息:指令、数据、符号表、重定位信息等等。
-
节区头部表(Section Header Table)包含了描述文件节区的信息,每个节区在表中都有一个表项,会给出节区名称、节区大小等信息。用于链接的目标文件必须有节区头部表,其它目标文件则无所谓,可以有,也可以没有。
这里给出一个关于链接视图比较形象的展示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8YiTjtiu-1633791830807)(D:\PWN总结\高级ROP\ELF篇\截图\2.png)]
对于执行视图来说,其主要的不同点在于没有了 section,而有了多个 segment。其实这里的 segment 大都是来源于链接视图中的 section。
注意:
尽管图中是按照 ELF 头,程序头部表,节区,节区头部表的顺序排列的。但实际上除了 ELF 头部表以外,其它部分都没有严格的顺序。
ELF Header
注:在下面的介绍中,我们以 32 位为主进行介绍。
ELF Header 描述了 ELF 文件的概要信息,利用这个数据结构可以索引到 ELF 文件的全部信息,数据结构如下:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
ELF32_Half e_type;
ELF32_Half e_machine;
ELF32_Word e_version;
ELF32_Addr e_entry;
ELF32_Off e_phoff;
ELF32_Off e_shoff;
ELF32_Word e_flags;
ELF32_Half e_ehsize;
ELF32_Half e_phentsize;
ELF32_Half e_phnum;
ELF32_Half e_shentsize;
ELF32_Half e_shnum;
ELF32_Half e_shstrndx;
} Elf32_Ehdr;
其中每个成员都是 e 开头的,它们应该都是 ELF 的缩写。下面对部分重要字段说明。
e_ident
e_ident[EI_MAG0]
到 e_ident[EI_MAG3]
,即文件的头 4 个字节,被称作 “魔数”,标识该文件是一个 ELF 目标文件。
名称 | 值 | 位置 |
---|---|---|
ELFMAG0 | 0x7f | e_ident[EI_MAG0] |
ELFMAG1 | ‘E’ | e_ident[EI_MAG1] |
ELFMAG2 | ‘L’ | e_ident[EI_MAG2] |
ELFMAG3 | ‘F’ | e_ident[EI_MAG3] |
`
e_ident[EI_CLASS]
为 e_ident[EI_MAG3]
的下一个字节,标识文件的类型或容量。
名称 | 值 | 意义 |
---|---|---|
ELFCLASSNONE | 0 | 无效类型 |
ELFCLASS32 | 1 | 32 位文件 |
ELFCLASS64 | 2 | 64 位文件 |
ELF 文件的设计使得它可以在多种字节长度的机器之间移植,而不需要强制规定机器的最长字节长度和最短字节长度。ELFCLASS32
类型支持文件大小和虚拟地址空间上限为 4GB 的机器;它使用上述定义中的基本类型。
ELFCLASS64
类型用于 64 位架构。
e_ident[EI_DATA]
字节给出了目标文件中的特定处理器数据的编码方式。下面是目前已定义的编码:
名称 | 值 | 意义 |
---|---|---|
ELFDATANONE | 0 | 无效数据编码 |
ELFDATA2LSB | 1 | 小端 |
ELFDATA2MSB | 2 | 大端 |
其它值被保留,在未来必要时将被赋予新的编码。
文件数据编码方式表明了文件内容的解析方式。正如之前所述,ELFCLASS32
类型文件使用了具有 1,2 和 4 字节的变量类型。
e_ident[EI_DATA]
给出了 ELF 头的版本号。目前这个值必须是EV_CURRENT
,即之前已经给出的e_version
。
e_ident[EI_PAD]
给出了 e_ident
中未使用字节的开始地址。这些字节被保留并置为 0;处理目标文件的程序应该忽略它们。如果之后这些字节被使用,EI_PAD 的值就会改变。
e_type
e_type
标识目标文件类型。
名称 | 值 | 意义 |
---|---|---|
ET_NONE | 0 | 无文件类型 |
ET_REL | 1 | 可重定位文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件 |
ET_CORE | 4 | 核心转储文件 |
ET_LOPROC | 0xff00 | 处理器指定下限 |
ET_HIPROC | 0xffff | 处理器指定上限 |
虽然核心转储文件的内容没有被详细说明,但 ET_CORE
还是被保留用于标志此类文件。从 ET_LOPROC
到 ET_HIPROC
(包括边界) 被保留用于处理器指定的场景。其它值在未来必要时可被赋予新的目标文件类型。
e_machine
这一项指定了当前文件可以运行的机器架构。
名称 | 值 | 意义 |
---|---|---|
EM_NONE | 0 | 无机器类型 |
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel 80386 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 7 | Intel 80860 |
EM_MIPS | 8 | MIPS RS3000 |
其中 EM 应该是 ELF Machine
的简写。
其它值被在未来必要时用于新的机器。 此外,特定处理器的 ELF 名称使用机器名称来进行区分,一般标志会有个前缀EF_
(ELF Flag)。例如,在EM_XYZ
机器上名叫 WIDGET
的标志将被称为 EF_XYZ_WIDGET
。
e_entry
这一项为系统转交控制权给 ELF 中相应代码的虚拟地址。如果没有相关的入口项,则这一项为 0。
e_phoff
(ps;program header)
这一项给出程序头部表在文件中的字节偏移(Program Header table OFFset)。如果文件中没有程序头部表,则为 0。
e_shoff
(ps: section header)
这一项给出节头表在文件中的字节偏移( Section Header table OFFset )。如果文件中没有节头表,则为 0。
e_ehsize
这一项给出 ELF 文件头部的字节长度(ELF Header Size)。
e_phentsize
这一项给出程序头部表中每个表项的字节长度(Program Header ENTry SIZE)。每个表项的大小相同。
e_phnum
这一项给出程序头部表的项数( Program Header entry NUMber )。因此,e_phnum
与 e_phentsize
的乘积即为程序头部表的字节长度。如果文件中没有程序头部表,则该项值为 0。
e_shentsize
这一项给出节头的字节长度(Section Header ENTry SIZE)。一个节头是节头表中的一项;节头表中所有项占据的空间大小相同。
e_shnum
这一项给出节头表中的项数(Section Header NUMber)。因此, e_shnum
与 e_shentsize
的乘积即为节头表的字节长度。如果文件中没有节头表,则该项值为 0。
e_shstrndx
这一项给出节头表中与节名字符串表相关的表项的索引值(Section Header table InDeX related with section name STRing table)。如果文件中没有节名字符串表,则该项值为SHN_UNDEF
。关于细节的介绍,请参考后面的 “节” 和“字符串表”部分。
Program Header Table
概述
Program Header Table 是一个结构体数组,每一个元素的类型是 Elf32_Phdr
,描述了一个段或者其它系统在准备程序执行时所需要的信息。其中,ELF 头中的 e_phentsize
和 e_phnum
指定了该数组每个元素的大小以及元素个数。一个目标文件的段包含一个或者多个节。程序的头部只有对于可执行文件和共享目标文件有意义。
可以说,Program Header Table 就是专门为 ELF 文件运行时中的段所准备的。
Elf32_Phdr
的数据结构如下
typedef struct {
ELF32_Word p_type;
ELF32_Off p_offset;
ELF32_Addr p_vaddr;
ELF32_Addr p_paddr;
ELF32_Word p_filesz;
ELF32_Word p_memsz;
ELF32_Word p_flags;
ELF32_Word p_align;
} Elf32_Phdr;
基地址 - Base Address
程序头部的虚拟地址可能并不是程序内存镜像中实际的虚拟地址。通常来说,可执行程序都会包含绝对地址的代码。为了使得程序可以正常执行,段必须在相应的虚拟地址处。另一方面,共享目标文件通常来说包含与地址无关的代码。这可以使得共享目标文件可以被多个进程加载,同时保持程序执行的正确性。尽管系统会为不同的进程选择不同的虚拟地址,但是它仍然保留段的相对地址,因为地址无关代码使用段之间的相对地址来进行寻址,内存中的虚拟地址之间的差必须与文件中的虚拟地址之间的差相匹配。内存中任何段的虚拟地址与文件中对应的虚拟地址之间的差值对于任何一个可执行文件或共享对象来说是一个单一常量值。这个差值
就是基地址
,基地址的一个用途就是在动态链接期间重新定位程序。
可执行文件或者共享目标文件的基地址是在执行过程中由以下三个数值计算的
- 虚拟内存加载地址
- 最大页面大小
- 程序可加载段的最低虚拟地址
要计算基地址,首先要确定可加载段中 p_vaddr 最小的内存虚拟地址,之后把该内存虚拟地址缩小为与之最近的最大页面的整数倍即是基地址。根据要加载到内存中的文件的类型,内存地址可能与 p_vaddr 相同也可能不同。(ps:即按页对齐的最小起始虚拟地址)
Section Header Table
其实这个数据结构是在 ELF 文件的尾部( 为什么要放在文件尾部呢?? ),但是为了讲解方便,这里将这个表放在这里进行讲解。
该结构用于定位 ELF 文件中的每个节区的具体位置。
首先,ELF 头中的 e_shoff
项给出了从文件开头到节头表位置的字节偏移。e_shnum
告诉了我们节头表包含的项数;e_shentsize
给出了每一项的字节大小。
其次,节头表是一个数组,每个数组的元素的类型是 ELF32_Shdr
,每一个元素都描述了一个节区的概要内容。
Sections
节区包含目标文件中除了 ELF 头部、程序头部表、节区头部表的所有信息。节区满足以下条件
- 每个节区都有对应的节头来描述它。但是反过来,节区头部并不一定会对应着一个节区。
- 每个节区在目标文件中是连续的,但是大小可能为 0。
- 任意两个节区不能重叠,即一个字节不能同时存在于两个节区中。
- 目标文件中可能会有闲置空间(inactive space),各种头和节不一定会覆盖到目标文件中的所有字节,闲置区域的内容未指定。
许多在 ELF 文件中的节都是预定义的,它们包含程序和控制信息。这些节被操作系统使用,但是对于不同的操作系统,同一节区可能会有不同的类型以及属性。
可执行文件是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析引用(不同文件中的子例程的引用以及数据的引用,调整对象文件中的绝对引用)并且重定位指令。加载与链接过程需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节区中,比如 .dynamic
。
每一种操作系统都会支持一组链接模型,但这些模型都大致可以分为两种
类型 | 描述 |
---|---|
静态链接 | 静态链接的文件中所使用的库文件或者第三方库都被静态绑定了,其引用已经被解析了。 |
动态链接 | 动态链接的文件中所使用的库文件或者第三方库只是单纯地被链接到可执行文件中。当可执行文件执行时使用到相应函数时,相应的函数地址才会被解析。 |
有一些特殊的节可以支持调试,比如说 .debug 以及 .line 节;支持程序控制的节有 .bss,.data, .data1, .rodata, .rodata1。
名称 | 类型 | 属性 | 含义 |
---|---|---|---|
.comment | SHT_PROGBITS | 包含版本控制信息。 | |
.debug | SHT_PROGBITS | 此节区包含用于符号调试的信息。 | |
.dynamic | SHT_DYNAMIC | SHF_ALLOC SHF_WRITE | 此节区包含动态链接信息。SHF_WRITE 位设置与否是否被设置取决于具体的处理器。 |
.dynstr | SHT_STRTAB | SHF_ALLOC | 此节区包含用于动态链接的字符串,大多数 情况下这些字符串代表了与符号表项相关的名称。 |
.dynsym | SHT_DYNSYM | SHF_ALLOC | 此节区包含动态链接符号表。 |
.got | SHT_PROGBITS | 此节区包含全局偏移表。 | |
.line | SHT_PROGBITS | 此节区包含符号调试的行号信息,描述了源程序与机器指令之间的对应关系,其内容是未定义的。 | |
.plt | SHT_PROGBITS | 此节区包含过程链接表(procedure linkage table)。 | |
.relname | SHT_REL | 这些节区中包含重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。 | |
.relaname | SHT_RELA | ||
.shstrtab | SHT_STRTAB | 此节区包含节区名称。 |
注意:
- 以 “.” 开头的节区名称是系统保留的,当然应用程序也可以使用这些节区。为了避免与系统节区冲突,应用程序应该尽量使用没有前缀的节区名称。
- 目标文件格式允许定义不在上述列表中的节区,可以包含多个名字相同的节区。
- 保留给处理器体系结构的节区名称一般命名规则为:处理器体系结构名称简写 + 节区名称。其中,处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 节区是 FOO 体系结构中的 psect 节区。