PWN基础入门 - ELF文件
9unk Lv5

简介

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

1

ELF 文件都是二进制的形式,根据对应的数据结构来生成文件,有哪些字段(顺序是固定的)、各字段的长度、各字段的值代表什么含义。最后生成一个二进制文件。

2

如上图所示,为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* 幻数及其他信息 */
Elf64_Half e_type; /* 对象文件类型 */
Elf64_Half e_machine; /* 架构 */
Elf64_Word e_version; /* 对象文件版本 */
Elf64_Addr e_entry; /* 程序入口虚拟地址 */
Elf64_Off e_phoff; /* 程序头表的偏移量 */
Elf64_Off e_shoff; /* 节头表的偏移量 */
Elf64_Word e_flags; /* 保存与文件相关的、特定于处理器的标志 */
Elf64_Half e_ehsize; /* ELF头部的大小 */
Elf64_Half e_phentsize; /* 程序头表的条目大小 */
Elf64_Half e_phnum; /* 程序头表的条目数 */
Elf64_Half e_shentsize; /* 节头表的条目大小 */
Elf64_Half e_shnum; /* 节头表的条目数 */
Elf64_Half e_shstrndx; /* 节头表中与节名称字符串相关联的条目的索引 */
} Elf64_Ehdr;

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT       16  
typedef struct elf32_hdr{
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; /* ELF 文件头部的字节长度 */
Elf32_Half e_phentsize; /* 程序头表的大小 */
Elf32_Half e_phnum; /* 程序头表的条目数 */
Elf32_Half e_shentsize; /* 节头表的条目大小 */
Elf32_Half e_shnum; /* 节头表的条目数 */
Elf32_Half e_shstrndx; /* 节头表中与节名称字符串相关联的条目的索引 */
} Elf32_Ehdr;

32 位的 elf 头,只有与地址相关的元素大小 e_entry、e_phoff、e_shoff 变成了 4 字节,其他元素的大小不变。

程序头表

64 位

1
2
3
4
5
6
7
8
9
10
typedef struct {
uint32_t p_type; // 程序头类型,描述段的类型和目的
uint32_t p_flags; // 程序头标志,描述段的访问权限和属性
uint64_t p_offset; // 段在文件中的偏移量
uint64_t p_vaddr; // 段在内存中的虚拟地址
uint64_t p_paddr; // 段在内存中的物理地址(通常与虚拟地址相同)
uint64_t p_filesz; // 段在文件中的大小
uint64_t p_memsz; // 段在内存中的大小
uint64_t p_align; // 段的对齐要求
} Elf64_Phdr; // 对于32位系统,使用 Elf32_Phdr 类型
  • p_type(4 bytes):指定了段的类型,即该段在程序加载和执行中的作用。

以下是p_type字段可能的具体数值及其含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    PT_NULL (值为0x0):
占位符,没有实际的段与之对应。

PT_LOAD (值为0x1):
表示段是可以被加载到内存中执行的。这是最常见的段类型,通常包括代码段、数据段等。

PT_DYNAMIC (值为0x2):
包含动态链接信息,用于动态链接的共享对象。

PT_INTERP (值为0x3):
指定动态链接器的路径,告诉系统在加载共享对象时应该使用哪个动态链接器。

PT_NOTE (值为0x4):
包含特定于系统的信息,通常用于记录版权信息或其他注释。

PT_SHLIB (值为0x5):
保留,不应用于一般的程序。

PT_PHDR (值为0x6):
程序头表自身的描述信息,用于定位程序头表在文件中的位置。

PT_TLS (值为0x7):
线程局部存储段,用于存储线程特定的数据。

PT_NUM (值为0x8):
用于程序链接编辑器的段,不用于程序的执行。

PT_LOOS (值为0x60000000):
操作系统特定的低范围开始。

PT_GNU_EH_FRAME (值为0x6474e550):
包含异常处理帧信息,用于C++异常处理。

PT_GNU_STACK (值为0x6474e551):
描述栈的属性,通常用于指示栈是否可执行。

PT_GNU_RELRO (值为0x6474e552):
读只段,用于标记那些在运行时只读但在加载时需要写权限的段。

PT_LOSUNW (值为0x6ffffffa):
操作系统特定的低范围结束。

PT_SUNWBSS (值为0x6ffffffa):
用于标记BSS段的开始。

PT_SUNWSTACK (值为0x6ffffffb):
用于标记栈的开始。

PT_HISUNW (值为0x6fffffff):
操作系统特定的高范围结束。

PT_HIOS (值为0x6fffffff):
应用程序特定的高范围结束。

PT_LOPROC (值为0x70000000):
应用程序特定的低范围开始。

PT_HIPROC (值为0x7fffffff):
应用程序特定的高范围结束。

PT_SHT_ARM_EXIDX (值为0x70000001):
ARM特有的段,用于异常索引表。

PT_SHT_ARM_PREEMPTMAP (值为0x70000002):
ARM特有的段,用于预取映射表。

PT_SHT_ARM_ATTRIBUTES (值为0x70000003):
ARM特有的段,用于属性表。

PT_SHT_ARM_DEBUGOVERLAY (值为0x70000004):
ARM特有的段,用于调试覆盖。

PT_SHT_ARM_OVERLAYSECTION (值为0x70000005):
ARM特有的段,用于覆盖区。

这些p_type的值定义了段的不同类型和用途,操作系统和加载器根据这些类型来正确地处理ELF文件中的段。需要注意的是,并非所有的操作系统和加载器都支持所有的段类型,某些类型可能是特定于某个平台或操作系统的。
  • p_flags(4 bytes):指定了段的访问权限和特性。

以下是p_flags字段可能的具体数值及其含义:

1
2
3
4
5
6
7
8
PF_X (值为0x1):
表示段是可执行的。如果设置了这个标志,意味着这段内存可以被CPU执行指令。

PF_W (值为0x2):
表示段是可写的。这个标志指示段允许被写入数据。

PF_R (值为0x4):
表示段是可读的。这个标志指示段允许被读取数据。

这些标志可以组合使用,以表示段的不同访问权限。在实际的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
2
3
4
5
6
7
8
9
10
typedef struct {
uint32_t p_type; // 程序头类型
Elf32_Off p_offset; // 文件中的偏移量
Elf32_Addr p_vaddr; // 虚拟内存地址
Elf32_Addr p_paddr; // 物理内存地址(通常不使用)
uint32_t p_filesz; // 文件中的大小
uint32_t p_memsz; // 内存中的大小
uint32_t p_flags; // 访问权限标志
uint32_t p_align; // 对齐要求
} Elf32_Phdr;

节头表(段表)

64位的段表是以 Elf64_Shdr 结构体作为数组元素,数组元素的个数等于段的个数,每一个 “Elf64_Shdr” 代表有一个段。 “Elf64_Shdr” 又被称为段描述符。

64位

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
uint32_t sh_name; // 节名称的索引,指向节名称字符串表
uint32_t sh_type; // 节类型,描述节的内容和用途
uint32_t sh_flags; // 节标志,描述节的访问权限和特性
uint64_t sh_addr; // 节的虚拟地址,只有在可执行文件或共享对象中才有效
uint64_t sh_offset; // 节在文件中的偏移量
uint64_t sh_size; // 节的大小
uint32_t sh_link; // 链接到其他节的索引,通常用于符号表和重定位信息
uint32_t sh_info; // 用于辅助信息,如重定位表中的符号表条目数量
uint64_t sh_addralign; // 节内地址对齐要求
uint64_t sh_entsize; // 节中条目的大小,仅对某些类型的节(如符号表)有效
} Elf64_Shdr; // 对于32位系统,使用 Elf32_Shdr 类型
  • 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
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
uint32_t sh_name; // 节名称的索引,指向节名称字符串表
uint32_t sh_type; // 节类型,描述节的内容和用途
uint32_t sh_flags; // 节标志,描述节的访问权限和特性
uint32_t sh_addr; // 节的虚拟地址,只有在可执行文件或共享对象中才有效
uint32_t sh_offset; // 节在文件中的偏移量
uint32_t sh_size; // 节的大小
uint32_t sh_link; // 链接到其他节的索引,通常用于符号表和重定位信息
uint32_t sh_info; // 用于辅助信息,如重定位表中的符号表条目数量
uint32_t sh_addralign; // 节内地址对齐要求
uint32_t sh_entsize; // 节中条目的大小,仅对某些类型的节(如符号表)有效
} Elf32_Shdr;
  • 本文标题: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 许可协议。转载请注明出处!