Linux ELF格式解析
郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,如果您不同意请关闭该页面!任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!
前言
在写PE结构解析的时候,以为ELF结构没必要去看文件结构,直到后面做了PWN题发现不是这样的,所有还是学习下ELF结构,然后再写个解析器
常见结构区分
目前,PC平台流行的可执行文件格式(Executable)主要包含如下两种,它们都是COFF(Common File Format)格式的变种
- Windows下的PE(Portable Executable)
- Linux下的ELF(Executable Linkable Format)
源代码经过编译后但未进行连接的那些中间文件(Windows的.obj
和Linux的.o
),它与可执行文件的格式非常相似,所以一般跟可执行文件格式一起采用同一种格式存储。
其中动态链接库(DDL,Dynamic Linking Library)和静态链接库(Static Linking Library)的格式都和当前系统对应的可执行文件结构一样
- 动态链接库:Windows的
.dll
、Linux的.so
- 静态链接库:Windows的
.lib
、Linux的.a
ELF
文件格式
ELF文件有三种类型,可以通过ELF Header中的e_type
成员进行区分:
- 可重定位文件(Relocatable File):
ETL_REL
。一般为.o
文件,可以与其他目标文件链接来创建可执行文件或共享目标文件的代码和数据。静态链接库属于可重定位文件 - 可执行文件(Executable File):
ET_EXEC
。可以执行的一个程序,此文件规定了exec()如何创建一个程序的进程映像。 - 共享目标文件(Shared Object File):
ET_DYN
。一般为.so
文件。- 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
- 动态链接器(Dynamic Linker)将其与某个可执行文件或其他共享目标文件结合一个可执行文件,创建进程映像。
如图所示,为ELF文件的基本结构,主要由四部分组成:
- ELF Header
- ELF Program Header Table(或称Program Headers、程序头)
- ELF Section Header Table(或称Section Headers、节头表)
- ELF Section
从图中,就能看出它们各自的数据结构以及相互之间的索引关系。下面依次介绍。
段(Segment
)与节(Section
)的区别在于,段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中。相比而言,节的粒度更小。
ELF Header
文件的最开始几个字节给出如何解释文件的提示信息。 这些信息独立于处理器, 也独立于文件中的其余内容。
|
详细解释
e_ident
数组给出了ELF的一些标识信息,这个数组中不同下标的含义如表所示:
这些索引访问包含以下数值的字节:
-
EI_MAG0 到 EI_MAG3:魔数(Magic Number),标志此文件是一个ELF目标文件。
名称 取值 位置
EI_MAG0 0x7f e_ident[EI_MAG0]
EI_MAG1 'E' e_ident[EI_MAG1]
EI_MAG2 'L' e_ident[EI_MAG2]
EI_MAG3 'F' e_ident[EI_MAG3] -
EI_CLASS:标识文件的类别,或者说,容量
名称 取值 位置
ELFCLASSNONE 0 非法类别
ELFCLASS32 1 32位目标
ELFCLASS64 2 64位目标
ps:ELFCLASS32支持虚存范围4GB,ELFCLASS64是为64位预留的,不过文件中的其他内容都没有针对64位定义 -
EI_DATA:字节e_ident[EI_DATA] 给出处理器特定数据的数据编码方式。
名称 取值 位置
ELFDATANONE 0 非法数据编码
ELFDATA2LSB 1 高位在前
ELFDATA2MSB 2 低位在前 -
EI_VERSION:ELF头部的版本号码,目前此值必须是EV_CURRENT。
-
EI_PAD:标记e_ident中未使用字节的开始,初始化为0。
e_type
目标文件类型
名称 取值 含义 |
e_machine
给出文件的目标体系结构类型
名称 取值 含义 |
e_version
目标文件版本
名称 取值 含义 |
e_entry
程序入口的虚拟地址,如果目标文件没有程序入口,可以为0
e_phoff
程序头部表格(Program Header Table)的偏移量(按字节计算)。如果文件没有程序头部表格,可以为0
e_shoff
节区头部表格(Section Header Table)的偏移量(按字节计算)。如果文件没有节区头部表格,可以为0
e_flags
保存与文件相关的,特定于处理器的标志。标志名称采用EF_machine_flag的格式
e_ehsize
ELF头部的大小(以字节计算)
e_phentsize
程序头部表格的表项大小(按字节计算)
e_phnum
程序头部表格的表项数目。可以为0
e_shentsize
节区头部表格的表项大小(按字节计算)
e_shnum
节区头部表格的表项数目。可以为0
e_shstrndx
节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为SHN_UNDEF
实例演示
#Linux ubuntu 5.4.0-55-generic #61-Ubuntu SMP Mon Nov 9 20:49:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux |
ELF文件结构示意图中定义的ELF_Endr
的各个成员的含义与readelf具有对应关系:
成员 | 含义 |
---|---|
e_ident | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |
class: ELF64 | |
Data: 2’s complement, little endian | |
Version: 1 (current) | |
OS/ABI: UNIX - System V | |
ABI Version: 0 | |
e_type | Type: DYN (Shared object file) |
共享目标文件 | |
e_machine | Advanced Micro Devices X86-64 |
ELF文件的CPI平台属性 | |
e_version | Version: 0x1 |
ELF版本号。一般为常数1 | |
e_entry | Entry point address: 0x5cd0 |
入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。 | |
e_phoff | Start of program headers: 64 (bytes into file) |
e_shoff | Start of section headers: 127896 (bytes into file) |
Section Header Table 在文件中的偏移 | |
e_word | Flags: 0x0 |
ELF标志位,用来标识一些ELF文件平台相关的属性 | |
e_ehsize | Size of this header: 64(bytes) |
ELF Header本身的大小 | |
e_phentsize | Size of program headers:56 (bytes) |
e_phnum | Number of program headers: 13 |
e_shentsize | Size of section headers: 64 (bytes) |
单个Section Header大小 | |
e_shnum | Number of section headers: 30 |
Section Header的数量 | |
e_shstrndx | Section header string table index: 29 |
Section Header字符串在Section Header Table中的索引 |
ELF Section Header Table
ELF节头表是一个节头数组。每一个节头都描述了其所对应的节的信息,如节名、节大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的。
e_shoff
成员给出从文件头到节区头部表格的偏移字节数;e_shnum
给出表格中条目数目;e_shentsize
给出每个项目的字节数。从这些信息中可以确切地定位节区的具体位置、长度。
节区头部表格中比较特殊的几个小标如下:
名称 | 取值 | 说明 |
---|---|---|
SHN_UNDEF | 0 | 标记未定义的、缺失的、不相关的,或者没有含义的节区引用 |
SHN_LORESERVE | 0xFF00 | 保留索引的下界 |
SHN_LOPROC | 0xFF00 | 保留给处理器特殊的语义 |
SHN_HIPROC | 0xFF1F | 保留给处理器特殊的语义 |
SHN_ABS | 0xFFF1 | 包含对应引用量的绝对取值。这些值不会被重定位所影响 |
SHN_COMMON | 0xFFF2 | 相对于此节区定义的符号是公共符号。如FORTRAN中COMMON或者未分配的C外部变量 |
SHN_HIRESERVE | 0xFFFF | 保留索引的上界 |
介于SHN_LORESERVE
和SHN_HIRESERVE
之间的表项不会出现在节区头部表中。
每个节区头部可用如下数据结构描述:
typedef struct { |
注意:索引为零(SHN_UNDFF)的节区头部也是存在的,尽管此索引标记的是未定义的节区引用,并且节区的内容固定
部分字段解析
sh_type
节名是一个字符串,只是在链接和编译过程中有意义,但它不能真正地表示节的类型。对于编译器和链接器来说,主要决定节的属性是节的类型(sh_type
)和节的标志位(sh_flags
)
节的类型相关常量以SHT_
开头,上述readelf -S
命令执行的结果省略了该前缀。节区类型定义如表:
名称 | 取值 | 说明 |
---|---|---|
SHT_NULL | 0 | 此值标志节区头部是非活动的,没有对应的节区。(无效节) |
SHT_PROGBITS | 1 | 程序节。此节区包含程序定义的信息,其格式和含义都有程序来解释,代码节、数据节都是这种类型 |
SHT_SYMTAB | 2 | 此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能会发生变化。一般,SHT_SYMTAB节区提供用于链接编辑(指ld而言)的符号,尽管也可用来实现动态链接 |
SHT_STRTAB | 3 | 此节区包含字符串表。目标文件可能包含多个字符串表节区 |
SHT_RELA | 4 | 此节区包含重定位表项,其中可能会有补齐内容(addend),例如32位目标文件中的Elf32_Rela类型。目标文件可能拥有多个重定位节区 |
SHT_HASH | 5 | 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除 |
SHT_DYNAMIC | 6 | 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制 |
SHT_NOTE | 7 | 此节区包含以某种方式来标记文件的信息,即提示信息 |
SHT_NOBITS | 8 | 表示该节在文件中没有内容,如.bss 节。这种类型的节区不占用文件中的空间,其他方面和SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移 |
SHT_REL | 9 | 此节区包含重定位表项,其中没有补齐(addends),例如32位目标文件中的Elf32_rel类型。目标文件中可以拥有多个重定位节区 |
SHT_SHLIB | 10 | 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与ABI不兼容 |
SHT_DYNSYM | 11 | 动态链接的符号表。作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个SHT_DYNSYM节区,其中保存动态链接符号的一个最小集合,以节省空间 |
SHT_LOPROC | 0x70000000 | 这一段(包含两个边界),是保留给处理器专用语义的 |
SHT_HIPROC | 0x7FFFFFFF | 这一段(包含两个边界),是保留给处理器专用语义的 |
SHT_LOUSER | 0x80000000 | 此值给出保留给应用程序的索引下界 |
SHT_HIUSER | 0x8FFFFFFF | 此值给出保留给应用程序的索引上界 |
sh_flags
sh_flags字段定义了一个节区中包含的内容是否可以修改、是否可以执行等消息。如果一个标志位被设置,则该位取值为1。未定义的各位都设置为0。
名称 | 取值 | 含义 |
---|---|---|
SHF_WRITE | 0x1 | 表示该节在进程空间中可写 |
SHF_ALLOC | 0x2 | 表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。 |
SHF_EXECINSTR | 0x4 | 表示该节在进程空间中可以被执行 |
SHF_MASKPROC | 0xF0000000 |
其中已经定义了的各位含义如下:
- SHF_WRITE:节区包含进程执行过程中将可写的数据
- SHF_ALLOC:此节区在进程执行中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为0
- SHF_EXECINSTR:节区包含可执行的机器指令
- SHF_MASKPROC:所有包含于此掩码中的四位都用于处理器专用的语义
sh_link和sh_info
如果节的类型是与链接有关的(无论是动态链接还是静态链接),如重定位表、符号表等,则sh_link
、sh_info
两个成员所包含的意义如下所示。其它类型的节中这两个成员没有意义。
根据节区类型的不同,sh_link和sh_info的具体含义也有所不同:
ELF Sections
节的分类
.text
.text
节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr,则.text
节就会存在于text
段中。由于.text
节保存了程序代码,所以节类型为SHT_PROGBITS
。
.rodata
.rodata
节保存了只读的数据,如一行c语言代码中的字符串。由于.rodata
节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text
段(而不是data
段)中找到.rodata
节。由于.rodata
节是只读的,所以节类型为SHT_PROGBITS
。
.plt
.plt
节也称为过程链接表(Procedure Linkage Table),其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt
节保存了代码,所以节类型为SHT_PROGBITS
。
.data
.data
节存在于data
段中,其保存了初始化的全局变量等数据。由于.data
节保存了程序的变量数据,所以节类型为SHT_PROGBITS
。
.bss
.bss
节存在于data
段中,占用空间不超过4字节,仅表示这个节本来的空间。.bss
节保存了未进行初始化的全局数据。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss
节未保存实际的数据,所以节类型为SHT_NOBITS
。
.got.plt
.got
节保存了全局偏移量。.got
节和.plt
节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got .plt
节与程序执行有关,所以节类型为SHT_PROGBITS
。
.dynsym
.dynsym
节保存在text
段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM
。
.dynstr
.dynstr
保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。
.rel.name .relaname
name根据重定位所适用的节区给定。例如.text
节区的重定位节区名字将是:.rel.text
或者.rela.text
。重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对ELF目标文件的某些部分或者进程镜像进行补充或者修改。由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL
。
.hash
.hash
节也称为.gnu.hash
,其保存了一个用于查找符号的散列表。
.symtab
.symtab
节是一个ElfN_Sym
的数组,保存了符号信息。节类型为SHT_SYMTAB
。
.strtab
.strtab
节保存的是符号字符串表,表中的内容会被.symtab
的ElfN_Sym
结构中的st_name
引用。节类型为SHT_STRTAB
。
.ctors&.dtors
ctors
(构造器)节和.dtors
(析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在main函数执行之前需要执行的代码;析构函数是在main函数之后需要执行的代码。
我们可以使用readelf工具来查看节头表。
ascotbe@ubuntu:~$ readelf -S /bin/sh |
符号表
节的分类中我们介绍了.dynsym
节和.symtab
节,两者都是符号表。那么它们到底有什么区别呢?存在什么关系呢?
符号是对某些类型的数据或代码(如全局变量或函数)的符号引用,函数名或变量名就是符号名。例如,printf()
函数会在动态链接符号表.dynsym
中存有一个指向该函数的符号项(以Elf_Sym
数据结构表示)。在大多数共享库和动态链接可执行文件中,存在两个符号表。即.dynsym
和.symtab
。
.dynsym
保存了引用来自外部文件符号的全局符号。如printf
库函数。.dynsym
保存的符号是.symtab
所保存符合的子集,.symtab
中还保存了可执行文件的本地符号。如全局变量,代码中定义的本地函数等。
既然.dynsym
是.symtab
的子集,那为何要同时存在两个符号表呢?
通过readelf -S
命令可以查看可执行文件的输出,一部分节标志位(sh_flags
)被标记为了A(ALLOC)、WA(WRITE/ALLOC)、AX(ALLOC/EXEC)。其中,.dynsym
被标记为ALLOC,而.symtab
则没有标记。
ALLOC表示有该标记的节会在运行时分配并装载进入内存,而.symtab
不是在运行时必需的,因此不会被装载到内存中。.dynsym
保存的符号只能在运行时被解析,因此是运行时动态链接器所需的唯一符号。.dynsym
对于动态链接可执行文件的执行是必需的,而.symtab
只是用来进行调试和链接的。
上图所示为通过符号表索引字符串表的示意图。符号表中的每一项都是一个Elf_Sym
结构,对应可以在字符串表中索引得到一个字符串。该数据结构符号表项的格式如下:
typedef struct { |
字符串表
类似于符号表,在大多数共享库和动态链接可执行文件中,也存在两个字符串表。即.dynstr
和.strtab
,分别对应于.dynsym
和symtab
。此外,还有一个.shstrtab
的节头字符串表,用于保存节头表中用到的字符串,可通过sh_name
进行索引。
ELF文件中所有字符表的结构基本一致,如上图所示。
重定位表
重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使可执行文件和共享目标文件能够保存进程的程序镜像所需要的正确信息。
重定位表是进行重定位的重要依据。我们可以使用objdump工具查看目标文件的重定位表:
typedef struct { |
参考文章
http://www.chuquan.me/2018/05/21/elf-introduce/ |