3 ELF节

ELF节

image-20220923162501124 image-20220923162517638

对于各个节,readelf都会显示相关的基本信息,包括节头表里的索引、节的名称和类型。此外,你还可以查看节的虚拟地址、文件偏移及大小。对包含诸如符号表和重定向表的节,还有一列显示每个条目的大小。最后,readelf还显示每个节的相关标志、链接节的索引(如果存在)、其他信息(特定于节类型)及对齐要求。

.init和.fini节

.init节(清单2-5中的索引11)包含可执行代码,==用于执行初始化工作,并且在二进制文件执行其他代码之前运行。==可执行代码会有SHF_EXECINSTR标志,使用readelf查看Flg列,可发现该值为X❷。将控制权转移到二进制文件的main入口点之前,系统会先执行.init节的代码,如果熟悉面向对象编程,你可以将.init节看作构造函数。

.fini节(索引15)类似于.init节,不同之处在于其在主程序运行完后执行,本质上起到一种析构函数的作用。

.text节

.text节包含程序的主要代码,所以它是二进制文件分析或者逆向工作的重点。

.text节的类型为SHT_PROGBITS❸,因为其包含用户定义的代码。同时要注意节的标志,这些标志位指定了该节为可执行的节,但不可写入❹==。一般来说,可执行的节是不可写的,反之,可写的节一般是不可执行的,因为既可写又可执行的节会让攻击者利用漏洞直接覆盖代码来修改程序,使得攻击变得容易。==

除了从源代码编译的某些特定应用程序以外,GCC编译的典型二进制文件中的.text节包含了许多执行初始化和终止任务的标准函数,如__start、register_tm_clones及frame_dummy。现在_start是最重要的标准函数,

image-20220923164407609

在编写C程序时,需要以一个main函数开始,但是如果检查二进制文件的入口点,你会发现它没有指向main的地址0x400526❹,而是指向了0x400430,即_start❶的开头。

那么反汇编到底是怎么到达main函数的呢?如果看得仔细一点,你会发现__start在地址0x40044d处包含了一条指令,该指令将main地址移动到rdi寄存器❷。

该寄存器是在x64平台进行函数调用时传递参数的寄存器之一。随后__start调用了一个名为_libc_start_main❸的函数, 该函数位于.plt节,意味着该函数是共享库的一部分。_libc_start_main最终调用main的地址,并开始执行用户定义的代码。

.bss、.data及.rodata节

通常因为代码节不可写,所以变量会被保存在一个或多个可写的专用节中。

常量一般保存在自身的节中,使二进制文件变得井井有条。

编译器有时会在代码节中输出常量数据,目前版本的GCC和Clang通常不会混淆代码和数据,但Visual Studio有时会混淆代码和数据。

  • .rodata节代表“只读数据”,用于存储常量,因此==.rodata节是不可写的==。

  • 初始化变量的默认值存储在.data节中,因为变量的值可能会在运行时修改,所以==.data节被标记为可写==。

  • .bss节为未初始化的变量保留空间,最初.bss节的名称代表着“以符号开头的块”,是为(符号)变量保留内存块的意思。.bss节中的变量被初始化为零,并且该节被标记为可写。

延迟绑定和 .plt、.got及.got.plt节

在将二进制文件加载到进程中执行的时候动态链接器执行了最后的重定位。例如在编译时由于不知道加载地址,因此它会解析共享库中函数的引用。这里需要简单介绍一下,实际上在加载二进制文件的时候许多重定位一般都不会立即完成,而是延迟到未解析位置进行首次引用之前,这就是延迟绑定。

1.延迟绑定和 .plt

延迟绑定保证了动态链接器不会在重定位上浪费时间,而只在运行中有需要的时候执行。在Linux操作系统上,延迟绑定是动态链接器的默认行为。导出环境变量LD_BIND_NOW可以强制链接器执行所有重定位,[2]除非应用程序有实时性能要求,否则通常不会这样做。

Linux ELF二进制文件中的延迟绑定是通过两个特殊的节实现的,这两个节分别称为过程链接表(Procedure Linkage Table,PLT,也称.plt节)和全局偏移表(Global Offset Table,GOT,也称.got节)。

ELF二进制文件通常包含一个单独的、名为.got.plt的 GOT,用于在延迟绑定过程中与.plt节结合使用。.got.plt节类似于 常规的.got节,.plt节是包含可执行代码的代码节,就像.text节,而.got.plt则是数据节。

PLT由定义好格式的存根组成,用于从.text节到合适库文件的调用。

image-20220923215733834 image-20220923220333523

PLT的格式如下:首先有一个默认存根❶(稍后讨论),然后是一系列函数存根❷❹:每个库函数一个存根,它们都遵循相同的模式,并且其压入栈的值依次递增❸❺,而该值是一个标识符(会在稍后介绍)。现在我们研究清单2-7中所示的PLT存根如何调用共享库函数(见图2-2),以及如何辅助延迟绑定过程。

.got和.got.plt节的区别:.got用于引用数据项,而.got.plt用于存储通过PLT访问的(已解析的)库函数地址。

.rel.*和.rela.*节

名为rela.*的节,节的类型为SHT_RELA,这意味着它们包含链接器用于执行重定位的信息。

.dynamic节

在加载和创建要执行的ELF二进制文件时,.dynamic节将充当操作系统和动态链接器的“路线图”。

.dynamic节包含了一个Elf64_Dyn的结构体数组(在/usr/include/elf.h中指定),也称为标签(tag)。标签有各种类型,而每个标签都有一个关联值。

image-20220923221922574

如清单2-9所示,.dynamic节中每个标签的类型都显示在输出的第二列中。类型为DT_NEEDED的标签会通知动态链接器关于可执行文件的依赖问题,如二进制文件使用libc.so.6共享库❶中的puts函数,因此在执行二进制文件时需要将其加载。DT_VERNEED❷和DT_VERNEEDNUM❸ 类型的标签指定了版本依赖表的起始地址和条目数,而版本依赖表则指定了可执行文件的各种依赖的预期版本。

除了列出的依赖关系之外,.dynamic节还包含指向动态链接器所需的其他重要信息的指针(如由类型分别 为DT_STRTAB、DT_SYMTAB、DT_PLTGOT及DT_RELA的标签指定的动态字符串表、动态符号表、.got.plt节及动态重定位节)。

.init_array和.fini_array节

.init_array节包含一个指向构造函数的指针数组,在二进制文件被初始化之后、main函数被调用之前,这些构造函数会被依次调用。虽然前面提到.init节包含可执行代码,在启动可执行文件前执行一些关键的初始化工作,但.init_array节却是一个数据节,其中包含所需数量的函数指针,包括指向自定义构造的函数指针。

在GCC中,可以通过 attribute((constructor))装饰C源文件中的函数,并将其标记为构造函数。

.fini_array的确与.init_array相似,.fini_array包含指向析构函数的指针,但是.init_array和.fini_array中包含的指针很容易被修改,使其成为方便插入钩子的位置。钩子将初始化或者结束代码添加至二进制文件中以修改其行为。要注意较早版本的GCC生成的二进制文件可能包含名为.ctors和.dtors的节,而不是.init_array和.fini_array。

.shstrtab、.symtab、.strtab、.dynsym 及.dynstr节

  • .shstrtab节只是一个以NULL结尾的字符串数组,其中包含二进制文件中所有节的名称。其通过节头进行索引,允许诸如readelf之类的工具找出节的名称。
  • .symtab节包含一个符号表,该表是一个Elf64_Sym结构体数组,每个条目都将符号名与二进制文件中的代码和数据(如函数或者变量)相关联。
  • 包含符号名的实际字符串保存在.strtab节中,而这些字符串被Elf64_Sym结构体所指。在二进制分析的实际情况中,文件一般都会被剥离,这意味着.symtab和.strtab节中的表已经被删除。
  • .dynsym和.dynstr节类似于.symtab和.strtab,不同之处在于它们包含了动态链接而非静态链接所需的符号和字符串。因为在动态链接期间需要这些节的信息,所以不能剥离。

另外要注意,静态符号表的节类型为SHT_SYMTAB,而动态符号表的节类型为SHT_SYNSYM。这样诸如strip之类的工具在剥离二进制文件的时候就可以轻松地识别,哪些符号表可以安全地删除,哪些不能删除。