ELF简介
在了解用户态热补丁原理之前,我们不得不对ELF文件进行简单的理解与剖析,所以开篇,我们先跟大家聊聊ELF。
在Linux系统中,绝大多数的二进制文件都是使用ELF格式。从生产者的角度出发,这种格式的文件由一组名为sections的节组成。sections中可以包含数据(.rodata, .data)、text通过symbols符号机制来实现代码、数据、变量的引用。例如,C程序中的main是一个特殊的符号,在完成所需的初始化以后,C runtime就会将控制权转移到main。.symtab节中就列出来需要用到的符号。可执行代码(通常被称为.text)和一些辅助数据。如下简单的c程序:
编译完成后,可以通过 readelf -S 二进制文件名称 查看ELF的Sessions
ELF格式文件主要包含三类:
- 共享使用的动态库文件,主要用来存放公共代码;
- 二进制可执行文件,主要包含应用程序;
- 可重定位目标文件,主要是由汇编文件编译而来;
在GNU C compiler的编译过程中,其实就隐藏了汇编这一步骤。这些不同的ELF格式文件种类,他们主要的区别在于是否包含了重定位类型。
这个重定位是什么呢?重定位技术是允许在二进制目标文件中更改地址的技术。该技术是实现用户态热补丁的重点,所以我们展开讲解一下。例如,当我们将一系列的.o文件链接到可执行文件时,链接器会将每个.o文件中的.text、.data节合并到一个.text、.data节中,然后链接器将调整重定位信息,例如重定位所作用的位置(称之为r_offset,对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量;对于可执行文件或共享ELF文件来说,此值是受重定位作用的存储单元的虚拟地址)、目标符号及其地址,或者是相对于符号值的加数(称之为r_addend)。某些重定位的类型,也是允许出现在最终的二进制对象中,并在动态链接器加载时解析。
以下面两段代码为例:
对这两个源码文件编译成二进制文件:gcc -c a.c b.c
对目标文件进行反汇编:
通过命令: readelf -r a.o 查看是否存在重定向文件
这些是a.o这个二进制文件重定向的条目,我们以addOne为例,大家从查询结果可以看到,addOne的r_offset为:000000000016,r_info为:000b00000004,
此时我们看b.o的反汇编结果
接着我们把这两个二进制文件编译成可执行文件:gcc a.o b.o -o ab
然后查看ab的反汇编结果,按照上文描述,ab中会合并a.o 和 b.o的.text .data内容,由于内容比较多,我们只看重点的地方
我们比较一下a.o b.o ab的反汇编结果就能够发现可执行文件ab的地址显然是变化了。这也就是重定向的逻辑。动态目标中包含了将其加载到一个随机的基地址上所有必需的数据。这种加载时使用随机的基地址将会使得库中的函数加载的地址随机化,从而使入侵者难以利用漏洞进行攻击,并且在加载多个库时也不会相互干扰。因为在编译阶段无法确定变量的地址,所以在引用动态库中的数据对象时使用GOT表。这个表中包含了变量的地址,因此访问变量需要两个步骤:首先加载GOT表的条目,然后在表中找到访问某个变量对应的条目,从而找到地址去访问。GOT表中的条目是动态链接器(例如ld-linux)通过解析.rela.dyn节中的重定位信息来完成填充的,其中只允许几种类型的重定位,例如在x86-64架构下,支持的重定位类型有R_X86_64_RELATIVE、R_X86_64_64和 R_X86_64_GLOB_DATA。动态库中所提供的符号都列在.dynsym节中,符号名称都存储在.dynstr节中。.dynamic这个特殊的节中包含了加载库时所需的所有数据,例如所需库的列表、指向重定位条目的指针等内容。
可执行目标中变量、符号等通常都是被链接到一个固定的地址上,并且不包含重定位信息。内核只需要知道如何与解释器一起来加载这种类型的对象。如果没有特别的指定,大多数二进制文件都是使用动态链接器ld-linux作为解释器。它由内核加载并且控制权被转移到这里。动态加载器的职责就是加载所有必需的库、解析符号并将控制权转移到应用程序代码中。
可重定位目标文件中允许包含任何重定位类型。静态链接器,例如ld,将它们链接进一个可执行目标或一个动态目标中。可重定位目标文件可以视为仅是一个汇编文件向二进制文件的简单转变,其中包含了符号引用的一种适当记法。即是汇编文件中的每一个符号引用,在可重定位ELF文件中都有与之对应的符号和对这个符号的重定位引用。对于每一个定义的符号,都会添加在.symtab节中。以’\0’结尾的字符串标识了符号名称,存储在.strtab节中。然后,静态链接器利用定义在其它目标文件或动态共享目标文件中的符号来解析目标文件中的符号引用。