ELF文件分析(一) 文件头和程序头表

0x00 参考

  1. 资料参考
  • 《Linux二进制分析》作者:Ryan O’Neill (好书拜读ing)
  • elf的一些宏定义,主要是参考一些解析elf格式的软件的头文件,glibc的elf.h可以跳转到 github 上查看,文章中不会特别把宏的数值展示出来,只会指出某个值对应的宏是什么,因此你可以在这个头文件中查找和学习。
  • elf man手册,网页查看,简单的翻译版文件放在蓝奏云上,密码是dkq3
  1. 工具参考
  • readelf 和 objdump

    1
    2
    3
    # readelf和objdump在binutils中
    sudo apt update && sudo apt upgrade -y
    sudo apt install binutils
  • imhex/010editor

    推荐imhex,主要是现在换成双系统(linux Mint为主力)了,而 imhex 开源免费,此外功能更丰富

    安装参考官网:https://github.com/WerWolv/ImHex/blob/master/INSTALL.md

    1
    2
    3
    4
    5
    # 先到官网上获取deb文件
    # 依赖和deb包下载
    sudo apt update && sudo apt upgrade -y
    sudo apt install libglfw3 libmbedtls14
    sudo apt install ./imhex*.deb

0x01 内容

ELF文件规范有非常丰富的内容,我们主要关注下面这些:

  • ELF文件头
  • ELF程序头(段信息)
  • ELF节头(节信息)
  • ELF节
  • ELF符号
  • ELF重定位
  • ELF动态链接

下面是关于64位ELF的一个简单的文件布局,可以对大体位置先留个印象。

elf-1-0

0x02 ELF文件头解析

1
2
3
4
5
6
7
# file文件类型
file ELF-file
# 十六进制查看
cat ELF-file | xxd -b | head -n 4
hexdump -n 64 -C ELF-file
# readelf解析 -h ELF文件头
readelf -h ELF-file

根据 man elf 查询ELF的离线手册,文件头的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define EI_NIDENT 16

typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF文件的标识,包含魔数
uint16_t e_type; // ELF文件类型
uint16_t e_machine; // 运行的机器架构
uint32_t e_version; // ELF版本,默认01
ElfN_Addr e_entry; // 入口点
ElfN_Off e_phoff; // 程序头偏移量
ElfN_Off 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; // 节头字符串表在节头的索引
} ElfN_Ehdr;

其中要注意的是:

  • e_type 标记文件类别, 有下面这几种
    1. ET_NONE 类型未确定

    2. ET_REL 可重定位文件,有时也称目标文件

    3. ET_EXEC 可执行文件,又称为程序

    4. ET_DYN 共享目标文件,又称共享库

    5. ET_CORE 核心文件,程序崩溃或进程传递 SIGSEGV 信号产生的转储文件,用于调试找错

note

上面的描述不完全。特别的,对于位置无关可执行文件(PIE) , 其也被标记为了ET_DYN, 由于有入口点 _start, 因此可以执行,如下面的实例所示。

  • e_ident 这个部分可以继续划分,前4个字节为魔数 0x7F 0x45 0x4C 0x46,而第5个字节 EI_CLASS 标识了文件的位数,如果是 01 代表是32位,如果是 02 则是64位,而对于 00 这种情况一般代表无效,后续无效值不讨论,只考虑能让加载器正常工作的情形。第六个字节 EI_DATA 标识文件的字节序(大小端),如果是 01 代表是小端序,如果是 02 则是大端序。

info

32位的ELF文件和64位的文件的 e_ehsize, e_phentsize, e_shentsize是不同的, 正常情况下:

  • 32位:e_ehsize=52 字节 ; e_phentsize=32 字节 ; e_shentsize=40 字节
  • 64位:e_ehsize=64 字节 ; e_phentsize=56 字节 ; e_shentsize=64 字节
  • e_machine 机器架构,这个与运行加载相关,机器架构不支持就无法执行
  • e_entry 程序入口点,加载器最后将 CPU 的控制权转移到这个地址,从而开始执行程序的代码。

info

对于可执行文件,e_entry一般是直接指向程序真正的第一条指令,通常是C语言运行库(CRT)的一部分,一般是 _start 函数,这一点对静态链接的或者是动态链接的可执行文件都适用。在动态链接的情况下,内核会先启动 PT_INTERP 段指定的动态链接器,由它完成准备工作后,再跳转到 e_entry 指向的 _start 函数,开始执行程序。

  • e_phoff 和 e_shoff 分别是程序头偏移量节头偏移量, 一般 offset 指的是文件偏移,而 address指的是加载到内存后的虚拟地址。

实例

elf-1-1

0x03 ELF程序头

1
2
3
4
# readelf解析 -l 程序头表
readelf -l ELF-file
# 宽行显示, 比较推荐
readelf -Wl ELF-file

程序头表项的结构如下,程序头表是一个有程序头表项组成的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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; // 程序段的权限标识,有RWX
uint32_t p_align; // 程序段的内存对齐
} Elf32_Phdr;

typedef struct {
uint32_t p_type; // 类似与上面的
uint32_t p_flags; // 这个提前是为了8字节对齐,方便访问
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr;

p_type 程序段的类型,具体有下面几种

  1. PT_NULL 未定义

    未使用,成员值未定义,被忽略

  2. PT_LOAD 可加载

    数组元素指定一个可加载段, 由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段的开头。 如果段的内存大小 p_memsz 大于或等于文件大小 p_filesz,如果大于则额外字节填充为0, 紧随段的初始化区域。程序头表中可加载段的条目,按 p_vaddr 大小升序排列。

note

一个可执行文件至少存在一个可加载的段 一般程序有 text 和 data 这两个可加载段(注意,不是.text和.data节)

  1. PT_DYNAMIC 动态链接

    动态链接可执行文件特有的,包含但不限于:

    • 运行时所需的共享库列表
    • 全局偏移表地址(GOT)
    • 重定向条目信息
  2. PT_INTERP 解释器

    只将位置和大小信息存放在一个以 null 结尾的字符串中,描述解释器位置

    /lib64/ld-linux-x86-64.so.2

  3. PT_NOTE 注释

    包含特定供应商或者系统相关的附加信息

  4. PT_PHDR 程序头

    保存了程序头表本身的位置和大小,该段只能出现一次,且程序头表是程序内存映像的一部分才能出现。如果出现,在所有可加载段之前。

note

这个地方有点意思,在程序头记录的数据中,把程序头看作数据也当作了一个段。 可以看下面的实例,其中PHDR段的文件偏移是0x40, 这不就是我们的程序头表嘛!

  1. PT_LOPROC, PT_LOPROC 这是一个供处理器特定语义使用的范围

  2. PT_GNU_STACK GNU拓展

  • p_flags 段的权限标识

    类似与linux的权限管理机制,通过掩码进行组合

    • PF_R 读权限
    • PF_W 写权限
    • PF_X 执行权限

实例

elf-1-2
elf-1-3

下面是 imhex 中的,我选择第一段方便查看。

elf-1-4

对于具体字段的情况,可以通过 imhex 的模式数据的地方展开看到,就不进行数值上的介绍了。

0x04 关于程序头表、程序头表项和段

  • 程序头表(或者简称程序头)是一个由多个程序头表项 组成的数组。
  • “每个程序头表项描述了数据在内存中的安排,它定义了一个——也就是程序执行所必需的一块内存区域。
  • 程序头表的本质是运行时内存布局的“装载地图”。它告诉操作系统加载器需要将文件中哪些部分的数据(段)加载到内存的什么位置,并赋予它们何种权限(如只读、可执行等),为程序的最终执行做好准备。

0x05 小问题

关于文件头

  • 在十六进制的输出中找到 入口点地址程序头表地址节头表地址
  • 如何确定位数大小段

关于程序段

  • 分析内存中段和文件中段的情况,它们是连续的还是不连续的?
  • 如何确定段的权限,我可以随意修改他们吗?
  • p_filesz不等于p_memsz的情况有哪些?需要用到后面节的相关知识。
  • 文件修补(patch)中,我们需要修改某个地址(虚拟地址)的值,根据程序段的知识,我们应该如何知道其在文件中的位置(文件偏移)?