编辑
2023-04-27
Binary Analysis
00
请注意,本文编写于 574 天前,最后修改于 574 天前,其中某些信息可能已经过时。

目录

ELF头部
e_ident数组
e_type字段
e_machine字段
e_version字段
e_entry字段
e_phoff字段
e_shoff字段
e_flags字段
e_ehsize字段
ephentsize字段&&ephnum字段
eshentsize字段&&eshnum字段
e_shstrndx字段
节头
sh_name字段
sh_type字段
sh_flags字段
sh_addr字段
sh_offset字段
sh_size字段
sh_link字段
sh_info字段
sh_addralign字段
sh_entsize字段
.init节
.fini节
.text节
.bss节
.data节
.rodata节
.rel.节&&.rela.\节
.dynamic节
.init_array节
.fini_array节
程序头
p_type字段
p_flags字段
poffset && pvaddr && ppaddr && pfilesz字段
p_align字段

芜湖✈️

ELF头部

ELF头部的格式在include/linux/elf.h中有定义 (实际上在本人测试的5.11内核中 头部定义在include/uapi/linux/elf.h)

c
typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */ Elf64_Half e_type; /* 对象文件类型 */ Elf64_Half e_machine; /* 架构 */ Elf64_Word e_version; /* 对象文件版本 */ Elf64_Addr e_entry; /* 程序入口的虚拟地址 */ Elf64_Off e_phoff; /* Program header table file offset 程序头表的偏移 */ Elf64_Off e_shoff; /* Section header table file offset 节头表的偏移 */ Elf64_Word e_flags; /* 保存与文件相关的 特定于处理器的标志 */ Elf64_Half e_ehsize; /* ELF头部大小 */ Elf64_Half e_phentsize; /* 程序头表的条目大小 */ Elf64_Half e_phnum; /* 程序头表的条目数 */ Elf64_Half e_shentsize; /* 节头表的条目大小 */ Elf64_Half e_shnum; /* 节头表的条目数 */ Elf64_Half e_shstrndx; /* 节头表中与节名称字符串表相关的条目索引 */ } Elf64_Ehdr;

e_ident数组

ELF文件以ELF头部为起始 而ELF头部以e_ident数组作为起始 通常来说e_ident数组开头的4字节是固定的 第一个字节为0xf 后面三个字节分别为ELF的ASCII码 这样处理的好处是方便file或者二进制加载器能够快速识别正在处理的是ELF二进制程序

e_ident数组其余键值的意义同样存储在include/uapi/linux/elf.h中 (5.11内核)

c
#define EI_MAG0 0 /* e_ident[] indexes */ #define EI_MAG1 1 #define EI_MAG2 2 #define EI_MAG3 3 #define EI_CLASS 4 #define EI_DATA 5 #define EI_VERSION 6 #define EI_OSABI 7 #define EI_PAD 8

EI_CLASS 主要表示的是该二进制程序架构的位宽 (是64位架构的还是32位架构的) 如果为64位则该键值为2 (常量ELFCLASS64) 如果为32位则为1 (常量ELFCLASS32)

EI_DATA 主要表示的是架构的字节序 其实就是在内存中是大端序还是小端序 如果为小端序则该键值为1 (常量ELFDATA2LSB) 如果为大端序则为2 (常量ELFDATA2MSB)

EI_VERSION 主要表示的是创建二进制文件时使用的ELF规范版本 有效值只有1 (常量EV_CURRENT)

EI_OSABI 主要表示的是关于二进制程序的二进制接口(Application Binary Interface)的相关信息 如果EI_OSABI的值为非0 则二进制文件中会使用一些ABI-或者OS-的具体扩展名 这可能会改变二进制文件中某些字段的含义 如果EI_OSABI的值为0 则表示该二进制文件以UNIX System V ABI为目标

什么是ABI?

简单来说 就是一系列约定的集合 比如FreeBSD约定函数调用的头六个参数放到RDI RSI RDX RCX R8 R9上 XMM0-XMM7用来放置浮点变元一样 可以说调用惯例就是ABI 一般来说ABI和CPU具体架构和OS相关

具体而言ABI包含以下内容:

  1. 特定的处理器指令集
  2. 函数调用惯例
  3. 系统调用方式
  4. 可执行文件的格式

统一ABI的目的是 只要OS遵守统一的ABI那么不同的应用就自动可以实现向前兼容 不用担心版本兼容性问题

EI_PAD 跟计算机网络中头部的拓展字段一样 为了以后的发展空闲出来的空间 保留以供将来使用 当前设置值为0

使用readelf命令可以查看二进制程序的头部的e_ident数组及其含义

bash
readelf -h 2

e_type字段

该字段指明了二进制文件的类型 常见的值有ET_REL(可重定位二进制程序) ET_EXEC(可执行二进制程序) ET_DYN(动态库/共享对象文件)

e_machine字段

表示了二进制文件计划在某个体系结构上运行 常见的值有EX_X86_64(64位x86架构二进制文件) EM_386(32位x86架构二进制文件) EM_ARM(ARM架构二进制文件)

e_version字段

EI_VERSION作用类似 表示的是创建二进制文件时使用的ELF版本规范 唯一有效值为1 (常量EV_CURRENT)

e_entry字段

该字段表明了二进制文件的入口点 对于上面使用readelf来测试的样例来说 程序入口点为0x400530 此处也是解释器(在第一章解释过 为ld-linux.so)将二进制程序加载到虚拟内存后转移控制权的地方

e_phoff字段

该字段表明了对于程序头表(program header table)相对于与文件起始的偏移值 单位为字节 以上面的测试样例来说 程序头表相对于文件开始为64B 此处值可以为0 表示程序不包含文件头表

e_shoff字段

该字段的作用和上面的e_phoff差不多 只不过该字段表明的是节头表(section header table)相对于文件起始的偏移值 单位为字节

e_flags字段

该字段表明了二进制程序在特定CPU处理的标志 例如在计划在ARM上运行的二进制程序可以在e_flags上设置ARM的特定标志 但是针对一般的x86程序来说 此处一般设置为0

e_ehsize字段

该字段表明了二进制程序的ELF头部大小 对于64位的二进制程序 头部大小始终为64B 对于32位的二进制程序 头部大小始终为52B

e_phentsize字段&&e_phnum字段

根据e_phoff字段可以知道程序头表的起始位置 但是链接器和加载器要遍历这些表的时候还需要知道各个程序头的大小 以及每个表中程序头的数量 而这些信息就是由e_phentsizee_phnum字段来表示的

e_shentsize字段&&e_shnum字段

跟上面的差不多 只不过此处提供的是节头表的相关信息

e_shstrndx字段

该字段包含一个名为.shstrtab的头索引(在节头表中) 这是一个专用节 专门用来保存所有节的名称 使用readelf可以通过这个专用节来正确显示所有节的名称

节头

二进制文件中的代码和数据在逻辑上被分为连续的非重叠块 称为节 每个节都是由节头描述 节头定义了节的属性 并且允许找到节中字节的位置 二进制文件中所有节的节头都包含在节头表中 但是实际上并不是所有节的内容在执行时都是需要的 比如符号信息和重定位信息在执行时其实是没有什么用的

节主要是为链接器提供服务 所以节头表其实是ELF文件的可选部分 不需要链接的文件不需要节头表

节头的定义同样存在与include/uapi/linux/elf.h

c
typedef struct elf64_shdr { Elf64_Word sh_name; /* Section name, index in string tbl */ Elf64_Word sh_type; /* Type of section */ Elf64_Xword sh_flags; /* Miscellaneous section attributes */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Size of section in bytes */ Elf64_Word sh_link; /* Index of another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;

sh_name字段

如果该字段被设置 则表示在字符串表中存在索引 也即在之前说过的.shstrtab的特殊节中存在该段的名称的字符串 sh_name字段将对本段的名称在.shstrtab中的位置进行索引 以找到本节名称的字符串

sh_type字段

表明了本节的类型 此信息主要提供给链接器在链接时使用 类型SHT_PROGBITS的节包含了程序数据 如机器指令或者常量 这些节没有特定的结构供链接器解析 类型SHT_SYMTAB(静态符号表) SHT_DYNSYM(动态符号表) SHT_STRTAB(字符串表)为特殊节类型

符号表的内容是格式明确的符号 该表会描述特定文件的偏移 地址 符号名称和类型等信息

类型为 SHT_REL 或者 SHT_RELA 的节往往包含着格式明确的重定位项 链接器可以解析该重定位项然后以便在其他节中实现重定位 (SHT_RELSHT_RELA用于静态链接)

每个重定位项都会告诉链接器二进制文件中需要重定位的特定位置 以及重定位需要解析的符号

类型SHT_DYNAMIC的节包含动态链接中所需要的信息

sh_flags字段

描述了节的一些其他信息 SHF_WRITE表示该节运行时可写 SHF_ALLOC表示在执行二进制程序时将该节加载到虚拟内存中 SHF_EXECINSTR表示该节包含可执行指令

sh_addr字段

描述该节的虚拟地址 当该值为0时 表示该节不会加载到虚拟内存中

sh_offset字段

描述该节的相对于文件起始位置的文件偏移

sh_size字段

描述该节的大小

sh_link字段

该字段表示了节与节之间的关系 比如重定位节(SHT_REL或者SHT_RELA)与描述重定位所涉及到的符号的符号表相关联 sh_link字段通过表示相关节之间在节头表中的索引来表明这些节之间的关系

sh_info字段

该字段存储与节相关的额外信息 比如对于SHT_REL或者SHT_RELA的节来说 此处存放的就是用于重定位节的节头索引

sh_addralign字段

指明了该节在内存中的对齐方式 如果为16则表示该节的基址必须为16的倍数 如果为0或者1表示没有特殊的对齐要求

sh_entsize字段

某些节(符号表或者重定位表)包含着固定大小的条目 对于这些节使用sh_entsize字段来规定每个条目的长度 单位为字节 如果该节不包含固定长度条目的表格 那么本字段为0

对于一个ELF文件我们可以使用readelf来查看该文件的节

bash
readelf --sections --wide 2

.init节

该节的``sh_flags字段SHF_EXECINSTR 即包含可执行代码 在控制权转移到二进制程序main入口点之前会先执行.init节`的代码 作用类似于构造函数

.fini节

作用类似于.init节效果类似于析构函数

.text节

包含程序的主要代码 其类型为SHF_EXECINSTR 一般来说是可执行不可写的 其中除了主要的用户代码 .text节内同样包含了许多执行初始化和终止任务的标准函数 例如_start register_tm_clones

对于一个程序的运行 通常认为是从main函数开始的 但是检查入口点会发现 最初执行的函数是_start

asm
Disassembly of section .text: 0000000000400530 <_start>: 400530: 31 ed xor ebp,ebp 400532: 49 89 d1 mov r9,rdx 400535: 5e pop rsi 400536: 48 89 e2 mov rdx,rsp 400539: 48 83 e4 f0 and rsp,0xfffffffffffffff0 40053d: 50 push rax 40053e: 54 push rsp 40053f: 49 c7 c0 60 07 40 00 mov r8,0x400760 400546: 48 c7 c1 f0 06 40 00 mov rcx,0x4006f0 40054d: 48 c7 c7 71 06 40 00 mov rdi,0x400671 400554: e8 a7 ff ff ff call 400500 <__libc_start_main@plt> 400559: f4 hlt 40055a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] ... 0000000000400671 <main>: 400671: 55 push rbp 400672: 48 89 e5 mov rbp,rsp 400675: 48 83 ec 20 sub rsp,0x20 ...

通过观察_start的汇编可以发现 在0x400554处调用函数_libc_start_main 其参数为0x400671也即为main函数的地址 那么整个调用流程即为_start->_libc_start_main->main

.bss节

该节类型为SHT_NOBITS 即这一节的内容是空的 节并不占用实际的空间 这是因为.bss节的作用是当二进制程序建立执行环境时为未初始化变量分配适当大小内存块用的 一般来说该节中的变量初始化为0 并且该节可写

.data节

该节通常保存初始化变量的默认值 因为在运行时可能变量的值需要修改 所以该节可写

.rodata节

该节通常存储只读数据 即常量 所以该节不可写

.rel.*节&&.rela.*节

这里再看一眼之前使用readelf获取到二进制程序中的节信息

可以发现在所有节中存在两个.rela.*的节 其类型为SHT_RELA 表名其包含链接器用于重定位的信息 每个SHT_RELA类型的节都是一个重定位条目 每个条目都说明了需要应用重定位的特定地址 以及在此地址需要插入的值

bash
readelf --relocs 2

可以发现针对重定位条目也存在两种不同的类型 分别为R_X86_64_GLOB_DATR_X86_64_JUMP_SLO

其中R_X86_64_GLOB_DAT主要用于将全局变量的地址存储到GOT表 当程序调用全局变量时 GOT会将函数的地址动态解析出来 以实现动态链接。

而``R_X86_64_JUMP_SLOT`主要用于将函数的地址存储到PLT中 当程序调用该函数时 PLT会将函数的地址动态解析出来 以实现动态链接

.dynamic节

bash
readelf --dynamic 2

.dynamic节中每个条目都称为标签 每个标签都有一个关联值和其类型

类型NEEDED主要是通知动态链接器解决关于可执行文件的依赖问题 类型DT_VERNEEDDT_VERNEEDNUM指定了版本依赖表的旗帜位置和条目数 而版本依赖表指定了可执行文件各种依赖的版本

.init_array节

.init_array节包含一个指向构造函数的指针数组 在程序被初始化之后 main函数调用之前会一次调用这些构造函数

bash
objdump -d --section .init_array 2

可以发现在之前测试的二进制程序中只包含一个函数__frame_dummy_init_array_entry其地址为0x400500

.fini_array节

基本同.init_array节类似

程序头

程序头表提供了二进制文件的段视角 而节头表提供了二进制程序的节视角 在二进制文件加载到进程中并执行的时候 定位相关代码和数据并确定加载到虚拟内存中时 操作系统和链接器就会使用到段视角

一般来说每个段包含多个节 程序头表使用Elf64_Phdr结构体的类型来对段视图进行描述 该结构体定义于include/uapi/linux/elf.h

c
typedef struct elf64_phdr { Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; /* Segment file offset */ Elf64_Addr p_vaddr; /* Segment virtual address */ Elf64_Addr p_paddr; /* Segment physical address */ Elf64_Xword p_filesz; /* Segment size in file */ Elf64_Xword p_memsz; /* Segment size in memory */ Elf64_Xword p_align; /* Segment alignment, file & memory */ } Elf64_Phdr;
bash
readelf --wide -segments 2

![image-20230427152036937](/Users/du4t/Library/Application Support/typora-user-images/image-20230427152036937.png)

p_type字段

表明了段的类型 该字段比较重要的类型有PT_LOAD PT_DYNAMIC PT_INTERP

PT_LOAD类型的段会在创造进程时直接加载到内存中 程序头的剩余部分表明可加载块的大小和将其加载到的地址 PT_INTERP类型的段包含了.interp节该节通常提供加载二进制程序的解释器的名称 PT_DYNAMIC类型的段相对应的包含了.dynamic节 该节告诉解释器如何解析二进制程序以执行 PT_PHDR类型的段表示其包含程序头表

p_flags字段

表明该段在执行时的权限

PF_X 该段可执行 PF_W 该段可写 PF_R 该段可读

p_offset && p_vaddr && p_paddr && p_filesz字段

效果同节头中的sh_offset sh_addr sh_size 分别指定了该段的起始文件偏移 加载的虚拟地址 以及段大小 对于可加载段p_vaddrp_offset相等

p_align字段

sh_addralign表明了内存对齐方式

本文作者:Du4t

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!