ELF 文件是Linux以及Unix系统下的标准二进制文件格式。ELF 文件包含可执行文件、动态链接文件、目标文件、内核引导镜像文件等<sup>1</sup> 。ELF文件一般采用的后缀有:none, .axf, .bin, .elf, .o, .prx, .puff, .ko, .mod and .so。magic number 为:0x7F 'E' 'L' 'F'。了解二进制文件结构是进行逆向工程、二进制文件分析等操作的基础和前提。
本文主要内容基于《Linux二进制分析》一书,摘自个人博客,博客持续更新:https://greagen.github.io/posts/2020-05-21-ELF-Format.html
ELF文件可以被标记为以下几种类型:
文件头是ELF文件的最开头部分,从文件的 0 偏移量开始。文件头信息是除了文件头之外的剩余部分文件的一个映射。该部分主要记录了ELF文件类型,程序头个数,节个数等等。通过readelf -h
命令查看文件头信息,样例如下所示:
ELF头的源码定义以及各个字段的意义<sup>2</sup> 。
简要说明如下:
每个ELF文件包含一个文件头,后面紧接文件数据。后面的数据包括:程序头和节头等。
段(segment)和节(section)是二进制文件内容的两个重要概念。许多人经常把二者搞混。我们首先看一下wiki上对二者的解释。
The segments contain information that is needed for run time execution of the file, while sections contain important data for linking and relocation. Any byte in the entire file can be owned by one section at most, and orphan bytes can occur which are unowned by any section.
一个段包含零个至多个节,一个elf文件包含零个或多个段。
二者的区别也体现在接下来的程序头和节头的概念中。
An ELF file has two views: the program header shows the segments used at run time, whereas the section header lists the set of sections of the binary.
程序头描述的是段的信息。程序头表是程序头列表,跟在 ELF 头的后面。
段提供执行层面的操作,与OS交互,其中可加载的段(loadable segments)加载到进程镜像。段告诉操作系统该段是否应该被加载到内存中,该段的可读可写属性等。节提供链接层面的信息,包含指令、符号表,重定向信息等,与链接器打交道。节告诉链接器的是跳转符号、指令原始内容等。没有节头表,程序仍然可以正常运行,因为节头表没有对程序的内存布局进行描述,对程序内存布局的描述是在程序头表中。节头对于程序的运行来说不是必须的。
也就是说,可重定位文件不存在程序头,因为.o 文件会被链接到可执行文件中,不会直接加载到内存中执行。此外,编译后的.o 文件链接后可能会增加新的节,比如.dynsym等,此类的节与动态链接和运行时重定位有关。关于节的详细介绍见下文第五章节。
程序头是对段(segment)的描述,程序头表是文件中所有段组成的程序头列表,跟在最开始的 ELF 头后面。ELF 头中的e_phoff
定义了文件中的程序头表的偏移量。程序头的源码定义见官方文档说明<sup>3</sup> 。
程序头类型有十余种,详细说明见官方文档。常见的程序头类型有五种:PT_LOAD, PT_DYNAMIC, PT_NOTE, PT_INTERP, PT_PHDR。
样例如下:
一个可执行文件至少包含一个 PT_LOAD 类型的段。这类段是可装载的段,将会被装载或映射到内存中。例如,存放程序代码的text段和存放全局变量的动态链接信息的data段会被映射到内存中,根据p_align
中的值进行内存对齐。text 段(代码段)权限一般设置为PF_X| PF_R
(读和可执行),通常将data段的权限设置为`PF_W | PF_R)(读和写)。千面人病毒会修改被感染的程序的text段的权限。在程序头的段标记(p_flags)处增加PF_W标记来修改text段的权限。
动态段是动态链接文件独有的段,包含了动态链接器所必需的一些信息。在动态段中包含了一些标记值和指针。包括但不限于以下内容:
动态段包含.dynamic
节,该段中有个独特的标志,_DYNAMIC
,它包含的是以下结构的一个array<sup>4</sup> 。
PT_NOTE
类型的段可能保存了特定供应商或系统相关的信息。官方解释如下<sup>3</sup> :
Sometimes a vendor or system builder needs to mark an object file with special information that other programs will check for conformance, compatibility, etc. Sections of type SHT_NOTE
and program header elements of type PT_NOTE
can be used for this purpose. The note information in sections and program header elements holds a variable amount of entries.
事实上可执行程序运行时是不需要这个段的,这个段就成了很容易被病毒感染的一个地方。感兴趣的话可以自行搜索NOTE段病毒感染的相关信息。
PT_INTERP
段是对程序解释器位置的描述。
该段保存了程序头表的位置和大小。没少好说的,官方解释如下:
The array element, if present, specifies the location and size of the program header table itself, both in the file and in the memory image of the program. This segment type may not occur more than once in a file. Moreover, it may occur only if the program header table is part of the memory image of the program. If it is present, it must precede any loadable segment entry. See ``Program Interpreter'' below for more information.
代码程序段和数据段实际上不是段的类型,只是按照段内容划分的重用概念。
代码段包含的是可读可执行的指令和只读的数据,一般包含以下一些节:
数据段包含的是可读可写的数据和指令,通常包含一些一些节:
如上所示,PT_DYNAMIC段也指向.dynamic
节,而且实际上,.plt
节不仅可以出现在text段,也可能属于数据段,这都依赖于处理器。更多信息可以查阅Global Offset Table'
and Procedure Linkage Table
在每个段中,代码或者数据会被划分为不同的节。节头表是对所有节的位置和大小的描述,用于链接和调试。如果二进制文件中缺少节头,并不意味着节不存在,只是没有办法通过节头来引用节,对于调试器和反编译成程序来说只是可以参考的信息变少了。节头也可以被故意从节头表中删去来增加调试和逆向工程的难度。GNU的binutils
工具,像objdump
,objcopy
还有gdb
等都需要依赖节头定位到存储符号数据的节来获取符号信息。可以使用readelf -S
查看节头信息:
该节保存的是代码指令。因此节的类型为SHT_PROGBITS
。该类型官方定义为<sup>5</sup> :
SHT_PROGBITS
The section holds information defined by the program, whose format and meaning are determined solely by the program.
.rodata 保存了只读的数据。例如代码中的字符串。
因为.rodata节是只读的,它存在于只读段中。因此,.rodata是在text段而不是data段。该节类型同样也是SHT_PROGBITS
。
该节保存的是动态链接器从共享库导入的函数所必须的相关代码。因为保存的是代码,同样也存在与text段中,且节类型为SHT_PEOGBITS
。相关详细信息可查看过程链接表(Procedure Linkage Table,PLT)。
.data 节存在于data段,保存的是初始化的全局变量等数据,节类型也为SHT_PROGBITS
。
.bss节保存的是未经初始化的全局变量数据,也是data段的一部分。占用空间不超过4字节,仅表示这个节本身的空间。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于该节没有保存实际的数据,因此节类型为SHT_NOBITS
。
.got 节保存了全局偏移表。.got 节和.plt 节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。该节与程序执行有关,因此节类型为SHT_PROGBITS
。
该节保存的是动态链接相关的导入导出符号,该节保存在text段中,节类型被标记为SHT_DYNSYM
。
该节保存的是动态符号字符串表,是三种字符串表之一。表中的字符串以空字符为终止符,代表了符号的名称。
三种字符串表
重定位节保存了重定位相关的信息,这些信息描述了如何在链接或者运行时对ELF目标文件的某部分内容或者进程镜像进行补充或者修改,节类型为SHT_REL
。
.hash 节有时也成为.gnu.hash,保存了一个用于查找符号的哈希表。Hash Table 相关内容可查阅链接。
该节包含的是一个Symbol Table ,保存了符号信息,节类型为SHT_SYMTAB
。
该节包含了的是字符串,大多数是符号表的实体名,即符号字符串表,.symtab 节中的st_name
条目引用的信息。该节类型为SHT_STRTAB
。
该节保存的是节头字符串表,表中以空字符截止的字符串为各个节的节名。ELF文件头中的节头字符串表偏移索引变量e_shstrndx即为该节的索引。
符号是对某种类型的变量和代码(例如全局变量和函数)的符号引用。例如,printf() 函数会在动态符号表.dynsym 中有一个指向该函数的符号条目。在大多数共享库和动态链接可执行文件中,存在两个符号表:.dynsym 和 .symtab。
.dynsym 保存了来自外部文件符号的全局符号。例如printf() 此类的库函数。.dynsym保存的符号是.symtab所保存的符号的子集。后者还保存了可执行文件本地的全局符号,例如全局变量或者是本地函数。因此.symtab保存了所有的符号,而.dynsym只保存了动态/全局符号。
.dynsym被标记为ALLOC(A),而.symtab没有标记。有A标记意味着运行时分配并装入内存。而.symtab 不是运行是必需的,因此不会被装载到内存中。.dynsym 保存的符号是运行时动态链接器所需要的唯一符号,是必需的。而.symtab符号表只是用来进行调试和链接,因此有时为了节省空间,会将.symtab 符号表从生产二进制文件中删除。
符号的源码定义中,st_name 变量指向符号表中字符串表(.dynstr 或者 .strtab)的字符串偏移。偏移位置保存的是符号的名称字符串,例如 printf。
st_info 定义了符号的类型和绑定属性。
符号类型和符号绑定通过函数进行打包转换成 st_info 的值。
符号的存在可以大大方便调试、反编译等工作。但是符号也可以被去掉。
一个二进制文件的符号表 .symtab 可以很容易地被去掉,但是动态链接可执行文件会保留.dynsym,因此文件中会显示导入库的符号。如果一个文件通过静态编译,或者没有使用libc进行链接,然后使用strip命令清理。这个文件就不会有符号表,此时动态符号表也不存在,因为不是必需的。符号删除后,函数名会被替换成sub_xxxx的形式,增加辨识难度。
重定位的作用是将符号定义和符号引用进行链接。可重定位文件(.o文件)包含了如何修改节内容的相关信息,进而使链接过程中能够获取所需信息。例如obj1.o中调用了函数foo(),obj2.o中定义了foo()函数,链接器将二者进行分析,获取重定位信息,然后将两个文件链接到一个可执行文件。符号的引用(obj1.o)或被解析成符号的定义(obj2.o)。
重定位前符号引用位置的地址为隐式加数(32位系统为: -sizeof(uint32_t)),根据重定位类型(例如,R_386_PC32),采用相应的新地址计算方式(S + A - P)获得新的偏移量,新的偏移量指向函数定义,然后使用新的偏移量替换原来符号引用处的值。
可执行文件如果调用了其他动态链接文件中的函数,动态链接器会修改可执行文件中的GOT (Global Offset Table,全局偏移表)。GOT 位于数据段(.got.plt 节)中,因为GOT 需要是可写的。链接前,GOT 中保存的跳转地址为PLT (.plt,过程链接表,保存相关代码,在text段)中相应条目的地址,动态链接器解析动态库中的函数地址后,根据PLT 中的条目信息,首先跳转到相应的GOT 条目,将该条目中原来指向PLT 中相应调目的跳转地址改为解析的共享库函数地址。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)