简介
ELF,即 Executable and Linking Format,直译为 “ 可执行和链接格式 ”,具有这种格式的文件称为 ELF 文件。
ELF 文件主要分为以下三种类型:
-
可重定位文件(relocatavble file),即 “.o” 文件,常用于与其他目标文件进行连接以构建可执行文件或动态链接库。
-
共享目标文件(shared object file),即动态链接库文件。它在以下两种情况被使用:第一,在连接过程中与其他动态链接库或可重定位文件一起构建新的目标文件;第二,在可执行文件被加载的过程中,被动态链接到新的进程中,成为运行代码一部分。
-
可执行文件(executable file),经过连接的,可执行的程序文件。目标文件是由汇编器(assembler)和连接编辑器(link editor)生成的,内容是二进制,而非刻度的文本形式,是可以直接在处理器上运行的代码。
动态链接库:Windows 的 .dll、Linux 的 .so
静态链接库:Windows 的 .lib、Linux 的 .a
ELF 文件都是二进制的形式,根据对应的数据结构来生成文件,有哪些字段(顺序是固定的)、各字段的长度、各字段的值代表什么含义。最后生成一个二进制文件。
如上图所示,为 ELF 文件的基本结构,其主要由四部分组成:
-
ELF Header(ELF头)
指出了 Programe Header Table 、Section Header Table 的位置、大小、数量等。 -
ELF Section Header Table(节头表)
含有文件中所有 “节” 的信息。文件里的每一个 “节” 都需要在 “节头表” 中有一个对应的注册项,这个注册项描述了节的名字、大小、节的位置等等。 -
ELF Program Header Table(程序头表)
一个可执行文件或共享目标文件的程序头表(program header table)是一个数组,数组中的每一个元素称为 “程序头(program header)”,每一个程序头描述了一个“段(segment)”或者一块用于准备执行程序的信息。一个目标文件中的 “段(segment)”包含一个或者多个“节(secition)”。程序头只对可执行文件或共享目标文件有意义,对于可重定位文件,该信息可以忽略。 -
ELF Sections
ELF文件由多个节组成,每个节包含了特定类型的数据。节可以包含代码、数据、只读数据、符号表、字符串表、重定位信息等。节是文件的静态组成部分,它们定义了文件内容的组织和结构。 -
段(Segment)
段是程序加载到内存中的动态概念。每个段由程序头表中的一个条目描述,并且通常包含一个或多个节。段定义了程序在内存中的布局,包括代码段、数据段、堆栈段等。 -
符号表(Symbol Table)
符号表是程序中所有符号(如函数、变量等)的集合。它包含了符号的名称、类型、大小、在节中的位置等信息。符号表对于链接器解析外部符号引用和程序调试非常重要。 -
重定位信息(Relocation Information)
重定位信息用于修正程序中的地址引用,使得程序能够正确地访问其他模块或库中的符号。这在动态链接和目标代码模块的链接过程中非常关键。 -
字符串表
字符串表是存储在文件中的字符串的集合,通常与符号表一起使用,用于存储符号名称和其他字符串数据。
ELF 头
64 位 ELF 头(占 64 字节)
1 | typedef struct |
-
0x00,e_ident[EI_NIDENT](16 byte):文件标识。0x04(文件类型:1表示32位;2表示64位);0x05(存储方式:1表示小端存储;2表示大端存储);0x05(版本,默认为1);其余字节预留区暂未用到。
-
0x10,e_type(2 bytes):文件类型,小端编码 “00 03”。
-
0x12,e_machine(2 bytes):CPU 平台属性。003E(AMD x86-64 architecture)
-
0x14,e_version(4 bytes):ELF 版本号,默认值 0x00000001
-
0x18, e_entry(8 bytes):ELF 程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令,真正的入口是 “_start”
-
0x20,e_phoff(8 bytes):程序头部表在文件中的字节偏移(Program Header table OFFset)。如果文件中没有程序头部表,则为 0。
-
0x28,e_shoff(8 bytes):节头表在文件中的字节偏移( Section Header table OFFset )。如果文件中没有节头表,则为 0。
-
0x30,e_flags(4 bytes):文件中与特定处理器相关的标志,这些标志命名格式为EF_machine_flag。
-
0x34,e_ehsize(2 bytes):ELF 文件头部的字节长度(ELF Header Size)
-
0x36,e_phentsize(2 bytes):程序头部表中每个表项的字节长度(Program Header ENTry SIZE)。每个表项的大小相同。
-
0x38,e_phnum(2 bytes):程序头部表的项数( Program Header entry NUMber )。因此,e_phnum 与 e_phentsize 的乘积即为程序头部表的字节长度。如果文件中没有程序头部表,则该项值为 0。
-
0x3A,e_shentsize(2 bytes):节头的字节长度(Section Header ENTry SIZE)。一个节头是节头表中的一项;节头表中所有项占据的空间大小相同。
-
0x3C,e_shnum(2 bytes):节头表中的项数(Section Header NUMber)。因此, e_shnum 与 e_shentsize 的乘积即为节头表的字节长度。如果文件中没有节头表,则该项值为 0。
-
0x3E,e_shstrndx(2 bytes):节头表中与节名字符串表相关的表项的索引值(Section Header table InDeX related with section name STRing table)。如果文件中没有节名字符串表,则该项值为SHN_UNDEF。
32 位 ELF 头(占 52 字节)
1 |
|
32 位的 elf 头,只有与地址相关的元素大小 e_entry、e_phoff、e_shoff 变成了 4 字节,其他元素的大小不变。
程序头表
64 位
1 | typedef struct { |
- p_type(4 bytes):指定了段的类型,即该段在程序加载和执行中的作用。
以下是p_type字段可能的具体数值及其含义:
1 | PT_NULL (值为0x0): |
- p_flags(4 bytes):指定了段的访问权限和特性。
以下是p_flags字段可能的具体数值及其含义:
1 | PF_X (值为0x1): |
这些标志可以组合使用,以表示段的不同访问权限。在实际的ELF文件中,p_flags字段通常是这些值的按位或(OR)组合。例如,一个段的p_flags可能被设置为PF_R | PF_X(即0x5),表示该段是可读和可执行的。
-
p_offset(8 bytes):指定了段(segment)在文件中的起始偏移量,即从文件的开始位置到段数据的第一个字节之间的字节偏移数。
应用场景:当链接器处理ELF文件时,它会使用p_offset来确定如何将节(section)内容复制到输出文件中的相应段。加载器在加载程序时也会参考p_offset来确定段在文件中的物理位置,以便正确地将段内容映射到内存中。 -
p_vaddr(8 bytes):指定了段(segment)在内存中的虚拟地址(Virtual Address),即段在虚拟内存空间中的起始位置。
使用场景:加载器(Loader)使用p_vaddr来确定如何将段映射到进程的地址空间。这个地址通常是程序运行时的预期地址 -
p_paddr(8 bytes):指定了段(segment)在内存中的物理地址(Physical Address),即段在物理内存空间中的起始位置。然而,需要注意的是,p_paddr字段在大多数现代操作系统中并不常用,因为现代操作系统通常使用虚拟内存系统,而不是直接操作物理内存。
-
p_filesz(8 bytes):指定了一个段(segment)在文件中的大小。
使用场景:p_filesz用于加载器(loader)或链接器(linker)确定如何从ELF文件中读取段数据。加载器在加载程序到内存时,会根据p_filesz的值来确定需要读取多少字节的数据。 -
p_memsz(8 bytes):指定了一个段(segment)在内存中的大小。
使用场景:p_memsz用于加载器(loader)确定如何在内存中为段分配空间。加载器在将程序加载到内存时,会根据p_memsz的值来分配足够的内存空间。 -
p_align(8 bytes):指定了段(segment)在内存中的对齐要求。
使用场景:p_align用于加载器(loader)确定如何在内存中为段分配符合对齐要求的地址。这是为了满足某些硬件架构的性能优化要求,或者遵守操作系统的内存管理规则。
32位
1 | typedef struct { |
节头表(段表)
64位的段表是以 Elf64_Shdr 结构体作为数组元素,数组元素的个数等于段的个数,每一个 “Elf64_Shdr” 代表有一个段。 “Elf64_Shdr” 又被称为段描述符。
64位
1 | typedef struct { |
-
sh_name(4 bytes):用于存储节名称的索引,它指向节名称字符串表(Section Name String Table)中对应的字符串。
使用场景:sh_name用于标识节的名称,使得链接器(linker)、加载器(loader)和调试工具能够识别和引用每个节。通过sh_name字段的索引值,可以找到对应的节名称字符串。 -
sh_type(4 bytes):指定节的类型,即节的用途和它包含的数据种类。
使用场景:sh_type用于链接器(linker)、加载器(loader)和调试工具识别节的类型,以便正确处理和解释节中的数据。 -
sh_flags(8 bytes):用于指定节的访问权限和属性,即定义了节的某些特性和如何处理这些节。
使用场景:sh_flags用于链接器(linker)、加载器(loader)和调试工具识别节的属性,以便正确处理节中的数据。例如,加载器可能会根据sh_flags中的SHF_ALLOC标志来决定是否为节分配内存。 -
sh_addr(8 bytes):指定了节在内存中的起始地址。
使用场景:sh_addr用于链接器确定节的最终位置。在创建可执行文件或共享对象时,链接器会使用这个地址来放置节;对于加载器来说,sh_addr字段指示了节在内存中的加载位置。加载器根据这个地址将节映射到进程的地址空间中。 -
sh_offset(8 bytes):指定了节在ELF文件中的起始偏移量,即从文件的开始到该节内容的起始位置之间的字节偏移数。
使用场景:sh_offset用于文件系统和加载器确定如何从文件中读取节数据。加载器在加载程序时会根据sh_offset来定位节在文件中的位置,并据此读取节内容;链接器在处理目标文件时也会使用sh_offset来识别和操作节。 -
sh_size(8 bytes):指定了节在文件中的大小,即节内容所占用的字节数。
使用场景:sh_size用于文件系统和加载器确定节在文件中的范围。加载器在加载程序时会根据sh_size来确定需要读取多少字节的数据;链接器在处理目标文件时也会使用sh_size来识别和操作节。 -
sh_link(4 bytes):指定当前节与其它节之间的关联,它提供了一个索引值,该值指向节名称字符串表中的一个条目,这个条目包含了与当前节相关联的另一个节的名称。
使用场景:sh_link用于链接器(linker)和加载器(loader)理解节之间的关系。例如,一个节可能需要与符号表(symbol table)或字符串表(string table)等其他节进行关联;在某些情况下,sh_link还可以用于指定节的合并属性,例如,多个.shstrtab节可能会被合并为一个。 -
sh_info(4 bytes):提供了与特定节相关的额外信息,其具体含义取决于节的类型。
使用场景:sh_info用于链接器(linker)和加载器(loader)理解节之间的关系,特别是在处理重定位和符号解析时。它可以帮助工具识别和操作节中的数据,确保正确的符号解析和重定位。 -
sh_addralign(8 bytes):指定了节(Section)在内存中的对齐要求。这个字段对于确保节的虚拟地址(sh_addr)满足特定的对齐约束非常重要,这通常是基于硬件架构的要求,以优化内存访问性能和满足某些指令集的对齐要求。
使用场景:sh_addralign用于加载器(loader)在加载程序到内存时,确保节的地址满足硬件架构的对齐要求。它有助于提高内存访问效率,因为许多处理器对齐的内存访问比非对齐的访问更快。 -
sh_entsize(8 bytes):指定了某些特定类型节中条目的大小。这个字段主要针对那些以固定大小条目数组形式存储数据的节,如符号表(symbol table)或重定位表(relocation table)。
使用场景:sh_entsize用于链接器(linker)、加载器(loader)和调试工具理解如何解析和处理这些特定类型的节。它有助于正确地遍历节中的数据结构,确保每个条目都能被正确地读取和解释。
段表的第一个元素是无效段描述符,它的类型为 “NULL”,除此之外每个段描述符都对应一个段。
32 位
1 | typedef struct { |
- 本文标题:PWN基础入门 - ELF文件
- 本文作者:9unk
- 创建时间:2024-03-21 21:00:00
- 本文链接:https://9unkk.github.io/2024/03/21/pwn-ji-chu-ru-men-elf-wen-jian/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!