引言:ELF是一种对象文件的格式,用于定义不同类型的对象文件中存放什么内容以及以何种格式存放这些内容.
Exccutable和Linkable标识ELF文件的俩大特性:可执行和可链接。ELF文件由各种各样的结构体连接在一起。
1.Executable(执行状态)
ELF文件将参与文件的执行工作,包括二进制程序的运行以及动态so的加载,它保持一个Execution View执行状态。它的单位是段。
2.Linkable(链接状态)
链接态的单位是节,链接视图在一个个节中,它的组成就是一个个节,我们介绍的就是这些节,了解这些节就可以清楚的认识链接视图。
一:ELF文件头
在日常的二进制文件中,主要靠偏移来获取相应的值。所以文件头在运行状态之前,我们可以手动指定一些偏移来去掉文件头。这篇文章将进行分析ELF文件头。
解压一个app或者是编译apk后用unzip解压,找到libxxx.s文件,通过010Editor打开,选择ELF模块来解析文件,头文件格式如下:
struct elf_header
struct program_header_table
struct section_header_table
struct dynamic_symbol_table
在讨论 ELF (Executable and Linkable Format) 文件格式时,涉及到几个关键的数据结构,它们定义了文件的不同部分和功能。ELF 文件是一种广泛使用的文件格式,用于定义程序如何在类 Unix 操作系统(如 Linux)上被加载和执行。以下几个结构的详细解释:
1. struct elf_header
这个结构是 ELF 文件的头部,包含了描述整个文件的基本信息。它位于文件的最开始位置,提供了关于文件本身的元数据,如文件类型(可执行文件、共享对象文件等)、机器类型(指定了哪种硬件架构适用,例如 x86 或 ARM)、入口点地址(程序开始执行的内存地址)等。
typedef struct {unsigned char e_ident[EI_NIDENT]; // 魔数和其他信息uint16_t e_type; // 对象文件类型uint16_t e_machine; // 架构uint32_t e_version; // 对象文件版本uint64_t e_entry; // 入口点虚拟地址uint64_t e_phoff; // 程序头表的文件偏移(一般偏移量是64->0x40)uint64_t e_shoff; // 节头表的文件偏移uint32_t e_flags; // 处理器特定标志uint16_t e_ehsize; // ELF 头的大小uint16_t e_phentsize; // 程序头表项的大小uint16_t e_phnum; // 程序头表项的数量uint16_t e_shentsize; // 节头表项的大小uint16_t e_shnum; // 节头表项的数量uint16_t e_shstrndx; // 包含节名称的字符串表的节头表索引
} Elf64_Ehdr;
进一步认识:e_ident[EI_NIDENT]; // 魔数和其他信息
在 ELF(可执行和可链接格式)文件的头部中,e_ident
数组是用来识别文件并提供解释文件内容所需的机器无关数据的关键部分。
1.1.1char file_identification[4]
这是 e_ident
数组的前四个字节,用于标识文件为 ELF 文件。这四个字节通常被称为“魔数”。预期的值是:
0x7F
7F'E'
45'L'
4C'F'
46
这四个字节连在一起形成 0x7F 'E' 'L' 'F'
,这是 ELF 文件的标识符。
1.1.2. enum ei_class_2_e ei_class_2
这通常是 e_ident
数组的第五个字节,指定了该二进制文件的架构类型。它表明文件是 32 位还是 64 位二进制文件。可能的值包括:
ELFCLASSNONE
(0):无效类别。ELFCLASS32
(1):32 位对象。ELFCLASS64
(2):64 位对象。
1.1.3. enum ei_data_e ei_data
这个字节指示文件中特定于处理器的数据的数据编码方式。它指定了解释 ELF 头中多字节字段时的字节顺序。可能的值包括:
ELFDATANONE
(0):无效的数据编码。ELFDATA2LSB
(1):小端格式(最低有效字节优先)。ELFDATA2MSB
(2):大端格式(最高有效字节优先)。
1.1.4. enum ei_version_e ei_version
这个字节表示文件符合的 ELF 规范的版本。在大多数实际应用中,这通常设置为 1。可能的值包括:
EV_NONE
(0):无效版本。EV_CURRENT
(1):当前版本。
1.1.5. enum ei_osabi_e ei_osabi
这个字节标识为哪个操作系统和 ABI(应用程序二进制接口)准备的二进制文件。这可以影响 ELF 文件某些方面的行为。一些常见的值包括:
ELFOSABI_NONE
(0):UNIX System V ABI。ELFOSABI_LINUX
(3):Linux ABI。ELFOSABI_SOLARIS
(6):Solaris ABI。ELFOSABI_FREEBSD
(9):FreeBSD ABI。
1.1.5.示例:e_ident
数组的定义
以下是如何在 C 结构体中为 ELF 头部定义这些元素的示例:
#define EI_NIDENT 16typedef struct {unsigned char e_ident[EI_NIDENT]; // 魔数和其他信息// 其他字段...
} Elf64_Ehdr;// 访问 e_ident 中的元素
Elf64_Ehdr header;
header.e_ident[EI_MAG0] = 0x7F; // 魔数字节 0
header.e_ident[EI_MAG1] = 'E'; // 魔数字节 1
header.e_ident[EI_MAG2] = 'L'; // 魔数字节 2
header.e_ident[EI_MAG3] = 'F'; // 魔数字节 3
header.e_ident[EI_CLASS] = ELFCLASS64; // 64 位架构
header.e_ident[EI_DATA] = ELFDATA2LSB; // 小端
header.e_ident[EI_VERSION] = EV_CURRENT; // 当前版本
header.e_ident[EI_OSABI] = ELFOSABI_LINUX; // Linux ABI
1.2.e_type
在 ELF (Executable and Linkable Format) 文件头中,e_type
字段是一个非常重要的部分,它定义了 ELF 文件的类型。这个字段是一个 16 位的无符号整数(uint16_t
),用来指明文件是可执行文件、可重定位文件还是共享对象文件等。
1.2.常见的 e_type
值及其描述:
ET_NONE
(0): 未知类型,表示文件类型未定义或无效。ET_REL
(1): 可重定位文件。这种类型的文件包含代码和数据,可以在编译时被修改以创建可执行文件或其他可重定位文件。ET_EXEC
(2): 可执行文件。这种类型的文件包含可以直接执行的程序,通常已经进行了所有必要的重定位,可以被加载到内存中直接运行。ET_DYN
(3): 共享对象文件,通常称为动态链接库(DLLs 在 Windows 或 .so 文件在 Unix/Linux)。这种文件包含可以被多个程序共享的代码和数据。ET_CORE
(4): 核心文件。这种类型的文件通常是操作系统在程序异常终止时生成的,包含程序终止时的内存映像和其他调试信息。ET_LOOS
(0xFE00) 到ET_HIOS
(0xFEFF): 操作系统特定的值范围,用于定义操作系统特定的文件类型。ET_LOPROC
(0xFF00) 到ET_HIPROC
(0xFFFF): 处理器特定的值范围,用于定义处理器特定的文件类型。
1.2.1.示例代码
在 C 语言中,你可能会看到如下的定义和使用:
#include <elf.h>
#include <stdio.h>void print_elf_type(uint16_t e_type) {switch (e_type) {case ET_NONE:printf("未知类型\\n");break;case ET_REL:printf("可重定位文件\\n");break;case ET_EXEC:printf("可执行文件\\n");break;case ET_DYN:printf("共享对象文件\\n");break;case ET_CORE:printf("核心文件\\n");break;default:printf("其他类型\\n");break;}
}// 使用示例
Elf64_Ehdr header;
header.e_type = ET_EXEC; // 假设我们有一个可执行文件的 ELF 头
print_elf_type(header.e_type);
2.2. e_machine
CPU架构,183标识ARM64,40标识ARM32, 62标识AMD64 或 Intel 64
• EM_AARCH64
(183): ARM 64-bit(AArch64)。
• EM_X86_64
(62): x86-64(也称为 AMD64 或 Intel 64)。
2.3. e_version
目标文件的版本,取值同e_ident[version]
2. struct program_header_table
这个结构定义了程序头表(Program Header Table),它描述了如何从文件创建一个进程映像。程序头表是一系列条目,每个条目指定了一个段(或程序的一部分)如何被加载到内存中。这些段包括代码、数据、堆栈等。
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;
IDA打开ELF文件就是使用struct program_header_table
来解析的。执行视图是木有节头的,也就是说,我们直接从内存dump SO文件是木有节区表的。所以IDA不能正常解析。我们从内存中dump一个so文件。
从so文件的内存分布来看,我们会发现缺少0x1000偏移。使用frida从内存在dump一个执行视图下的so文件,将文件用010打开,我们会发现节区表全部都是空的,产生这种结果的原因什么?答案是:因为010的ELF解析模板不正确,它是按偏移来解析的,使所以是空的。
3. struct section_header_table
节头表(Section Header Table)描述了文件中的所有节(section),每个节包含了程序或库文件的一部分数据,如程序代码、数据、符号表、重定位信息等。每个节头描述了节的名称、大小、位置等信息。
节区表中包含目标文件中的所有信息,目标文件中的每个节区表都有对应的节区头部来描述它,反过来,有节区头不意味着有节区。
typedef struct {uint32_t sh_name; // 节名称(字符串 tbl 索引)名字是一个null结尾的字符串uint32_t sh_type; // 节类型uint64_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;
3.1下面认识一下各个节区的含义
在 ELF(可执行和可链接格式)文件中,节区(section)是数据和代码的组织单位,每个节区都有特定的功能。
3.1.1. .shstrtab
- 用途: 节区头字符串表,指定节的名称。
- 内容: 存储所有节区的名称,以 null 字符结束(
'\\0'
)。 - 重要性: 允许 ELF 文件中的其他节区引用其名称。节区头表中的每个节区都有一个索引,指向
shstrtab
中的相应字符串。
3.1.2. .text
- 用途: 代码节区。
- 内容: 包含程序的机器代码,即可执行指令。
- 重要性: 这是程序的核心部分,操作系统在加载可执行文件时会将这个节区映射到内存中执行。
.text起始偏移,下图:
在IDA中跳到指定的地址(上面的C14h),这正是.text的起始位置
3.1.3. .bss
- 用途: 未初始化的数据节区,包含一块内存区域。
- 内容: 存储未初始化的全局变量和静态变量,或者初始化为零的变量。
- 重要性: 这个节区在文件中不占用实际空间,操作系统在加载程序时会为这些变量分配内存。节省了文件大小,因为未初始化的数据不需要在文件中存储。
3.1.4. .data
- 用途: 初始化数据节区。
- 内容: 包含全局变量和静态变量的初始化值。
- 重要性: 这个节区在加载时会被映射到内存,并且可以被程序读写。它的内容在程序运行时是可变的。
3.1.5. .rodata
- 用途: 只读数据节区。
- 内容: 包含常量数据,例如字符串字面量和其他不可修改的数据。
- 重要性: 这个节区的内容在程序运行时不应被修改,可以提高安全性和性能。
3.1.6. .systab
- 用途: 系统调用表。
- 内容: 存储系统调用的相关信息,具体内容和格式依赖于平台。
- 重要性: 这个节区通常与操作系统的实现相关,提供了对系统调用的支持。
3.1.7. .dynstr
- 用途: 动态字符串表。
- 内容: 存储动态链接所需的符号名称和其他字符串,例如动态库的名称。
- 重要性: 在动态链接过程中,加载器需要查找符号名称,这个节区提供了必要的信息。
3.1.8. .got
(Global Offset Table)
- 用途: 全局偏移表。
- 内容: 存放全局变量的地址和函数指针。
- 重要性: 支持动态链接和重定位,确保在运行时能够找到正确的地址。通过 GOT,程序能够在运行时访问动态库中的变量和函数。
3.1.9. .hash
- 用途: 符号哈希表。
- 内容: 提供符号名称的哈希值,用于快速查找符号在动态链接中的位置。
- 重要性: 加速动态链接过程,特别是在符号查找时,避免线性搜索的性能损失。
3.1.10. .interp
- 用途: 解释器节区。
- 内容: 指向用于加载和链接可执行文件的动态链接器的路径(例如
/lib64/ld-linux-x86-64.so.2
)。 - 重要性: 告诉操作系统在加载可执行文件时使用哪个动态链接器。
3.1.11. .line
- 用途: 行号信息节区。
- 内容: 提供源代码行号和调试信息,通常与调试信息相关联。
- 重要性: 在调试过程中,允许调试器将机器代码行映射回源代码行,帮助开发者调试程序。
3.1.12. .plt
(Procedure Linkage Table)
- 用途: 过程链接表。
- 内容: 存储动态链接函数的调用信息。
- 重要性: 使得程序能够通过 PLT 调用动态库中的函数。PLT 中的每个条目都对应一个函数,通过间接跳转实现动态链接。
3.1.13. .dynsym
- 用途: 动态符号表。
- 内容: 包含动态链接所需的符号,包括函数和全局变量的名称和信息。
- 重要性: 在动态链接过程中,符号表提供了符号的地址和属性,帮助链接器解析符号。
4. struct dynamic_symbol_table
动态符号表(Dynamic Symbol Table)是 ELF 文件中的一个节,包含了动态链接符号的信息。每个条目提供了符号的名称、位置、大小等信息,这些符号在运行时被动态链接器用来解析外部函数和变量的地址。
typedef struct {uint32_t st_name; // 符号名称(字符串 tbl 索引)uint8_t st_info; // 符号类型和绑定属性uint8_t st_other; // 保留位(未使用)uint16_t st_shndx; // 符号定义位置的节索引uint64_t st_value; // 符号的值uint64_t st_size; // 符号的大小
} Elf64_Sym;