ELF文件结构

  1. 1 背景
  2. 2 生成ELF文件
  3. 3 ELF文件结构
    1. 3.1 ELF文件整体结构
    2. 3.2 Header
    3. 3.3 Program Header Table
    4. 3.4 Section Header Table
  4. 4 总结
  5. 5 参考文献

1 背景

在计算机科学中,可执行链接格式(Executable and Linking Format, ELF)是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。ELF最初是由UNIX系统实验室开发并发布的,作为应用程序二进制接口的一部分。ELF格式文档将原生程序分成如下三类进行描述:

  • 可重定位文件(Relocatable File):包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File):包含适合于执行的一个程序,此文件规定了exec()如何创建一个程序的进程映像。
  • 共享目标文件(Shared Obiect File):包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。

图1 Android ELF文件结构

2 生成ELF文件

在解析ELF文件结构之前,先手动生成一个最简单的ELF文件,使用Android Studio的原生程序模板打包一个APK,如图2所示。

图2 原生程序模板APK

3 ELF文件结构

3.1 ELF文件整体结构

目标文件既要参与程序链接又要参与程序执行。出于方便性和效率考虑,目标文件格式提供了两种并行视图,分别反映了这些活动的不同需求,如图3所示。

图3 目标文件格式

  • 文件开始处是一个ELF头部(ELF Header),用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等等。
  • 程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
  • 节区头部表(Section Header Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

为了对ELF文件的整体结构有个直观的印象,这里给出一张宏观描述图片。

图4 ELF文件整体结构

3.2 Header

ELF文件头结构及相关常数被定义在/usr/include/elf.h中,因为ELF文件在各种平台下都通用,ELF文件有32位版本和64位版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不一样。它的文件头也有两种版本:分别叫Elf32_EhdrElf64_Ehdr。ELF header的数据结构可以参考elf.h中的定义:

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
/* 32-bit ELF base types. */
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;

#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; /* Entry point */
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;

下表是数据结构定义中用到的数据类型及用途:

名称 大小 对齐 用途
Elf32_Addr 4 4 无符号程序地址
Elf32_Half 2 2 无符号中等大小整数
Elf32_Off 4 4 无符号文件偏移
Elf32_Sword 4 4 有符号大整数
Elf32_Word 4 4 无符号大整数
unsigned char 1 1 无符号小整数

ELF Header中各个成员及其说明如下表所示:

成员 说明
e_ident[EI_NIDENT] 目标文件标识
e_type 目标文件类型
e_machine 给出文件的目标体系结构类型
e_version 目标文件版本
e_entry 程序入口的虚拟地址
e_phoff 程序头部表格的偏移量
e_shoff 节区头部表格的偏移量
e_flags 保存与文件相关的,特定于处理器的标志
e_ehsize ELF头部的字节长度
e_phentsize 程序头部表格的表项大小
e_phnum 程序头部表格的表项数目
e_shentsize 节区头部表格的表项大小
e_shnum 节区头部表格的表项数目
e_shstrndx 节区头部表格中与节区名称字符串表相关的表项的索引

将APK解压出来的libelf.dex文件拖入010 Editor中,F5运行ELF.bt脚本,即可显示出ELF文件头的信息,如图5所示。

图5 elf_header信息

如图6所示,在Linux的Terminal通过命令readelf -h libelf.so查看到的ELF文件头与图5一致。

图6 使用readelf查看elf_header

在ELF文件头中,我们需要重点关注以下几个字段:

  • e_entry:程序的入口虚拟地址,注意不是main函数的地址,而是.text段的首地址_start。当然这也要求程序本身非PIE(-no-pie)编译的且ASLR关闭的情况下,对于非ET_EXEC类型通常并不是实际的虚拟地址值。
  • e_ehsize:ELF Header结构大小。
  • e_phoffe_phentsizee_phnum:描述Program Header Table的在文件中的字节偏移、表项字节长度、表项数目。
  • e_shoffe_shentsizee_shnum:描述Section Header Table的在文件中的字节偏移、表项字节长度、表项数目。
  • e_shstrndx:这一项描述的是字符串表在Section Header Table中的索引,值1Ah表示的是Section Header Table中第26项是字符串表(String Table)。

综上,ELF header的主要作用有如下五点:

  1. 表明文件格式
  2. 记录一些该文件与环境的基本信息(文件类型、机器架构、版本)
  3. 程序的入口地址
  4. Program header table的起始地址与其结构表项的数量及大小
  5. Section header table的起始地址与其结构表项的数量及大小

3.3 Program Header Table

Program Header Table是一个结构体数组,每一个元素的类型是Elf32_Phdr,描述了一个段或者其它系统在准备程序执行时所需要的信息。其中,ELF头中的e_phentsizee_phnum指定了该数组每个元素的大小以及元素个数。一个目标文件的段包含一个或者多个节。程序的头部只有对于可执行文件和共享目标文件有意义。

程序头表及相关常数被定义在elf.h中,而且程序头表也有两种版本:分别叫Elf32_PhdrElf64_Phdr

1
2
3
4
5
6
7
8
9
10
typedef struct elf32_phdr {
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;

Elf32_Phdr数据结构定义中用到的数据类型及用途在3.2节中已介绍,Elf32_Phdr中各个成员及其说明如下表所示:

成员 说明
p_type 此数组元素描述的段的类型,或者如何解释此数组元素的信息。
p_offset 从文件头到该段第一个字节的偏移。
p_vaddr 段的第一个字节将被放到内存中的虚拟地址。
p_paddr 仅用于与物理地址相关的系统中。因为System V忽略所有应用程序的物理地址信息,此字段对与可执行文件和共享目标文件而言具体内容是未指定的。
p_filesz 段在文件映像中所占的字节数。可以为0。
p_memsz 段在内存映像中占用的字节数。可以为0。
p_flags 与段相关的标志。
p_align 可加载的进程段的p_vaddr和p_offset取值必须合适,相对于对页面大小的取模而言。此成员给出段在文件中和内存中如何对齐。数值0和1表示不需要对齐。否则p_align应该是个正整数,并且是2的幂次数,p_vaddr和p_offset对p_align取模后应该相等。

在elf_header中,e_phoff = 34h, e_phnum = 8h, e_phentsize = 20h,因此在010 Editor找到显示Elf32_Phdr的信息,如图7所示。

图7 program_header_table信息

如图8所示,在Linux的Terminal通过命令readelf –program-headers libelf.so查看到的Elf32_Phdr与图7一致。

图8 readelf查看Elf32_Phdr的程序头表

综上,ELF Program Header Table的主要作用有如下四点:

  1. 表明段的类型及段在文件中的偏移量
  2. 记录虚拟地址和物理地址
  3. 段在文件和内存中的大小
  4. 段权限和对齐方式

3.4 Section Header Table

该结构用于定位ELF文件中的每个节区的具体位置。节区头表是一个数组,每个数组的元素的类型是ELF32_Shdr每一个元素都描述了一个节区的概要内容。节区头表及相关常数也被定义在elf.h中,而且程序头表也有两种版本:分别叫Elf32_ShdrElf64_Shdr

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;

其中每个字段的定义如下表所示:

成员 说明
sh_name 给出节区名称。是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个NULL结尾的字符串。
sh_type 为节区的内容和语义进行分类。
sh_flags 节区支持1位形式的标志,这些标志描述了多种属性。
sh_addr 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为0。
sh_offset 此成员的取值给出节区的第一个字节与文件头之间的偏移。不过,SHT_NOBITS类型的节区不占用文件的空间,因此其sh_offset成员给出的是其概念性的偏移。
sh_size 此成员给出节区的长度(字节数)。除非节区的类型是SHT_NOBITS,否则节区占用文件中的sh_size字节。类型为SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间。
sh_link 此成员给出节区头部表索引链接,其具体的解释依赖于节区类型。
sh_info 此成员给出附加信息,其解释依赖于节区类型。
sh_addralign 某些节区带有地址对齐约束。例如,如果一个节区保存一个doubleword,那么系统必须保证整个节区能够按双字对齐。sh_addr对 sh_addralign取模,结果必须为0。目前仅允许取值为0和2的幂次数。数值0和1表示节区没有对齐约束。
sh_entsize 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。如果节区中并不包含固定长度表项的表格,此成员取值为0。

在elf_header中,e_shoff = 19224h, e_shnum = 1Bh, e_shentsize = 28h,因此在010 Editor找到显示Elf32_Shdr的信息,如图9所示。

图9 section_header_table信息

如图10所示,在Linux的Terminal通过命令readelf –program-headers libelf.so查看到的Elf32_Phdr与图9一致。

图10 readelf查看Elf32_Phdr的节区头表

下面的表中给出了系统使用的节区,以及它们的类型和属性。

成员 说明
.bss 包含将出现在程序的内存映像中的未初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为0。此节区不占用文件空间。
.comment 包含版本控制信息。
.data 这些节区包含已初始化数据,将出现在程序的内存映像中。
.debug 此节区包含用于符号调试的信息。
.dynamic 此节区包含动态链接信息。节区的属性将包含SHF_ALLOC位。是否SHF_WRITE位被设置取决于处理器。
.dynstr 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。
.dynsym 此节区包含了动态链接符号表。
.fini 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。
.got 此节区包含全局偏移表
.hash 此节区包含了一个符号哈希表。
.init 此节区包含可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指C语言的main函数)执行这些代码。
.interp 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含SHF_ALLOC位,否则该位为0。
.line 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。
.note 此节区中包含注释信息,有独立的格式。
.plt 此节区包含过程链接表
.rel 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含SHF_ALLOC位,否则该位置0。传统上name根据重定位所适用的节区给定。例如.text节区的重定位节区名字将是:.rel.text或者.rela.text。
.rodata 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.shstrtab 此节区包含节区名称
.strtab 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含SHF_ALLOC位,否则该位为0。
.symtab 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC位,否则该位置为0。
.text 此节区包含程序的可执行指令

在分析这些节区的时候,需要注意如下事项:

  • 以“.”开头的节区名称是系统保留的。应用程序可以使用没有前缀的节区名称,以避免与系统节区冲突。
  • 目标文件格式允许人们定义不在上述列表中的节区。
  • 目标文件中也可以包含多个名字相同的节区。
  • 保留给处理器体系结构的节区名称一般构成为:处理器体系结构名称简写+节区名称。
  • 处理器名称应该与e_machine中使用的名称相同。例如.FOO.psect节区是由FOO体系结构定义的psect节区。

综上,编译器、链接器和装载器都是依靠ELF Section Header Table来定位和访问各个节区的属性

  1. 节区的名称、类型及在文件中的偏移量
  2. 节区大小、权限
  3. 索引链接、地址对齐

4 总结

图11 ELF文件结构总结

5 参考文献

[1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019.
[2]https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#enroll-beta
[3]https://bbs.kanxue.com/thread-274573.htm
[4]https://langgithub.github.io/file/ELF%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90.pdf
[5]https://blog.csdn.net/weixin_47883636/article/details/109895223
[6]https://ctf-wiki.org/executable/elf/structure/basic-info/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏