目录
ELF二进制文件实际上包含4种类型的组件:ELF头部、一系列(可选)程序头、多个节以及节对应的各个(可选)节头。
图1
1.ELF头部
每个ELF二进制文件都是从头部开始的,该头部是一系列化结构化的字节,告诉你这是一个什么样的二进制文件,以及在文件的什么地方找其他内容。下图展示了ELF头部的类型定义
显示一个可执行文件的头部,如下图所示,可以看到与上图结构体中的属性一一对应
(1)e_ident: 上图除了ELF头部多种属性,还多了Class、Data、Version、OS/ABI、 Version,这些字节都包括在e_ident数组中:
- Class字节表示该二进制文件用于32位还是64位体系结构(32位置为1,64位置为2);
- Data字节表示二进制文件的字节序(小端字节序置为1,大端字节序置为2);
- Version字节指示创建二进制文件时使用的ELF规范版本;
- OS/ABI字节和Version字节表示的是关于应用程序二进制接口(Application Binary Interface)和操作系统(Operation System)的信息;
- 此外,在图1中标1的红框中看到,还有PAD字节(注意,这里统一省去了前缀EI_),这些字节当前都被指定填充,设置为0,被保留供将来使用。
(2)e_type:指定二进制文件的类型,常遇到的值是ET_REL(表示可重定位的对象文件)、ET_EXEC(可执行二进制文件)及ET_DYN(动态库,也称为共享对象文件);
(3)e_machine:表示二进制文件计划运行的体系结构;
(4)e_version:与e_ident数组中的EI_VERSION字节相同;
(5)e_entry:表示二进制文件的入口点,这是应该开始执行的虚拟地址;
(6)e_phoff和e_shoff:分别表示程序头表和节头表距离开始的偏移量。位于ELF二进制文件中固定位置的唯一数据结构是ELF头部,该头部始终位于开始位置。
(7)e_*entsize和e_*num:e_phentsize和e_phnum分别表示表中每个程序头的大小和程序头的数目,e_shentsize和e_shnum分别表示表中每个节头的大小和节头的数目;
(8)e_flags:保存二进制文件在特定处理器的标志;
(9)e_ehsize:以字节单位指定ELF头部大小,对于64位x86二进制文件,ELF头部大小始终为64字节;
(10)e_shstrndx:这是一个专用节,其中包含一个以空值结尾的ASCII字符串表,该表将所有节的名称存储在二进制文件中
2.节头
ELF二进制文件中的代码和数据在逻辑上被分为连续的非重叠块,称为节。节是代码或者数据的非结构化blob,每个节由节头描述,节头指定了节的属性并允许找到节中字节的位置,主要供链接器使用,只在链接时存在,在运行时不存在。二进制文件中所有节的节头都包含在节头表中。
(1)sh_name:保存节在特殊节.shstrtab(这是一个表)中的索引,通过索引在.shstrtab表中查找该节的名称,如果索引为0,表示该节没有名称
(2)sh_type:表示该节内容结构的信息:
- SHT_PROGBITS表示该节包含了程序数据,如机器指令或常量
- 符号表:SHT_SYMTAB表示静态符号表,SHT_DYNSYM表示动态链接器使用的符号表, SHT_STRTAB表示字符串表
- SHT_REL/SHT_RELA:包含格式明确的重定位项,因此对链接器特别重要,只用于静态链接。
- SHT_DYNAMIC:包含动态链接所需要的信息。
(3)sh_flags:描述了节的其他信息,重要的标志是:
- SHF_WRITE:表示该节在运行时可写,这样可以轻松区分包含静态数据的节和包含变量的节
- SHF_ALLOC:指示在执行二进制文件时将节的内容加载到虚拟内存
- SHF_EXECINSTR:指示该节包含可执行指令,这对反汇编二进制文件来说很有用
(4)sh_addr/sh_offset/sh_size:分别描述该节的虚拟地址,文件偏移及大小;
(5)sh_link:供链接器了解节与节之间的关系
(6)sh_info:存放关于节的额外信息
(7)sh_addrlign:以特定方式在内存中对其,来提高内存访问的效率
(8)sh_entsize:包含固定大小条目的节中每个条目的长度字节数
3.节
下图中对每个节的描述正是在‘2.节头’中描述的字段
下面介绍在二进制分析中可能看到的、最有趣的节内容:
- init和.fini:包含可执行代码,用于执行初始化工作,并且在二进制文件执行其他代码之前运行
- .init_array/.fini_array:.init_array包含一个指向构造函数的指针数组,在二进制文件被初始化之后、main函数被调用之前,这些构造函数会被依次调用
- .text:包含程序的主要代码,除了从源代码编译的某些特定应用程序外,gcc编译的典型二进制文件中的.text节包含了许多执行初始化和终止任务的标准函数。
- .bss\.data\.rodata:.bss节为未初始化的变量保留空间,可写;.rodata包含“只读数据”,用于存储常量,不可写;.data存储初始化变量的默认值,可写
- .shstrtab/.symtab/.strtab/.dynsym/.dynstr:.shstrtab前文已介绍;.symtab节包含一个符号表,该表是一个Elf64_Sym结构体数组,,每个条目都将符号名与二进制文件中的数据和代码关联起来,,包含符号名的实际字符串保存在.strtab节中,这些字符串被Elf64_Sym所指。在二进制分析的实际情况中,文件一般都会被剥离,这意味着.symtab和.strtab节中的表已被删除;.dynsym和.dynstr节类似于.shstrtab和.symtab,不同之处在于他们包含了动态链接而非静态链接所需的符号和字符串,因为在动态链接期间需要这些信息,因此不能被剥离;
- .plt/.got:ELF二进制文件中的延迟绑定是通过过程链接表.plt(Procedure Linkage Table,PLT)和全局偏移表.got(Global Offset Table, GOT)实现的,先介绍.plt,如下图所示,.plt涉及的主要概念存根的具体的解释也如图所示,每个库函数一个存根
延迟绑定过程如下图所示,以调用puts函数为例,该函数是已知libc库的一部分,直接调用相应的plt存根puts@plt(下图步骤1),plt存根以间接跳转指令jmp开头,该指令跳转到存储在.got.plt节中的地址(步骤2)。最初,在延迟绑定之前,该地址只是函数存根的下一条指令地址(push)(步骤3),push将一个整数压入栈中,该整数是PLT存根的标识符,然后下一条指令又跳转到所有PLT函数存根之间的通用默认存根(步骤4),默认存根会push另一个标识符(从GOT中获得),以表示可执行文件自身,然后间接的,通过GOT跳转到动态链接器(步骤5)。动态链接器通过之前push的PLT存根的标识符,确定puts的地址,将puts地址插入与put@plt相关联的GOT条目中,所以GOT条目不再像最初那样指向PLT存根,而是指向现在的puts实际地址,至此,延迟绑定完成。对于之后puts@plt的调用,PLT存根开头的跳转直接进入puts,而没有涉及动态链接器(步骤6)。
.got用于引用数据项,.got.plt用于存储通过PLT访问的已解析的库函数地址。
- .rel.*和.rela.*:提供重定位使用的信息
- .dynamic:在加载和创建要执行的ELF二进制文件时,.dynamic节将充当操作系统和动态链接器的“路线图”,如下图所示:
4.程序头
程序头表提供了二进制文件的段视图,将二进制文件加载到进程并执行的时候,定位相关代码和数据并确定加载到虚拟内存中的内容时,操作系统和动态链接器会用到段视图。段的结构定义如下图所示:
- p_type:标识段的类型,包括PT_LOA(创建进程时,加载到内存中)、PT_INTERP(提供了加载二进制文件的解释器的名称)、PT_DYNAMIC(告诉解释器如何解析二进制文件用于执行)
- p_flags:指定段在运行时的访问权限:PF_X(该段可执行)、PF_W(该段可写)、PF_R(该段可读)
- p_offset/p_vaddr/p_paddr/p_files/p_memsz:分别表示该段的起始文件偏移量、加载的虚拟地址以及段大小。p_memsz表示段在段在内存中的大小,p_files表示段在文件中的大小
- p_align:段所需的内存对齐方式(以字节方式)
段实际上是由若干个节组成的,如下图所示: