当前位置: 首页 > article >正文

ELF文件格式解读及其生成过程(上)

引言: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 16

typedef 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;


http://www.kler.cn/a/369691.html

相关文章:

  • 神经架构搜索:自动化设计神经网络的方法
  • VantUI
  • 【云原生】云原生与DevOps的结合:提升软件开发与交付的效率
  • 使用Git进行团队协作开发
  • xlnt加载excel报错:‘localSheetId‘ expected
  • C++学习路线(二十七)
  • Python 中的 object
  • React 前端框架开发入门案例
  • WebRTC VAD 详解与代码示例
  • 群体智能(Swarm Intelligence)算法:三种Python实现
  • Qt/C++地图雷达扫描/动态扇形区域/标记线实时移动/轮船货轮动态轨迹/雷达模拟/跟随地图缩放
  • OpenCV基本操作(python开发)——(6)视频基本处理
  • 无人机之姿态测量技术篇
  • 玩转HF/魔搭/魔乐社区
  • Canvas简历编辑器-选中绘制与拖拽多选交互设计
  • YOLOv11改进策略【模型轻量化】| 替换华为的极简主义骨干网络:VanillaNet
  • 三周精通FastAPI:19 Body - Updates 请求体 - 更新数据
  • 【GeekBand】C++设计模式笔记9_Abstract Factory_抽象工厂
  • 如何让Nginx更安全?
  • css绘制s型(grid)
  • 【Linux学习】(8)第一个Linux编程进度条程序|git三板斧
  • 静态局部变量
  • 深入RAG:知识密集型NLP任务的解决方案
  • 路由守卫重定向页面
  • vxe-table 表格中使用输入框、整数限制、小数限制,单元格渲染数值输入框
  • 雷军救WPS“三次”,WPS注入新生力量,不再“抄袭”微软