前言
我们rop里有一种高级的攻击方法ret2dl,它和动态链接有关,在我去学习ret2dl时发现自己对ELF文件中的一些section不是那么熟悉,而且不了解静态链接是怎么完成的,对动态链接的过程有些不知所措,这里总结一下最近学习的静态链接以及其相关section对ret2dl做一些补充说明。我这里只表述我认为的静态链接最为关键的部分,如果对其他方面有兴趣可以在参考部分寻找需要的资料。
ELF结构
ELF文件最前方的是ELF文件头 (ELF Header),包含着文件的基本信息,接下来就是ELF文件的各种段。在这些不同的段中最为重要的就是段表(section Header Table),也就是SHT,这个段表述了ELF文件中包含的所有段信息。
我们先看看链接器是如何分析一个ELF文件的
静态链接的重要section,以及链接器的分析
ELF Header
我们可以通过
readelf -h <file name> |
这样的命令来查看一个ELF文件的文件头,这里不妨查看libc的ELF Header
通过上图可以看到ELF文件里定义了很多东西,包括魔数、文件机器字节长度、储存方式等信息,而这里对我们静态链接过程中最为重要的就是Start of section headers,这里是我们段表在文件中的偏移,通过上图我们知道段表在文件的第1864377字节开始。而链接器也会通过该项找到我们SHT的位置。
Section Header Table(SHT)
我们的ELF文件中有各种各样的不同段,而段表就是记录这些段的基本,通过上面查看的ELF Header可以得到段表的起始位置为1864377 Bytes,每个表项大小是64 Bytes,一共是72项
我们可以通过
readelf -S <file name> |
这样的命令来查看每个段的具体信息
SHT每项包含
成员 | 含义 |
---|---|
sh_name | Section name:这里存放的是其name字符在字符串表的偏移 |
sh_type | Section type:表示该段的类型,例如.text是程序段 |
sh_flag | Section flag:表示该段的标志位 |
sh_addr | Section Address:段虚拟地址 |
sh_offset | Section Offset:段偏移 表示该段在文件中的偏移 |
sh_size | Section Size:该段长度 |
sh_link | Section Link:链接有关段专属,段链接信息 |
sh_info | Section information:同上 |
sh_addralign | Section Address Alignment:段地址对齐方式 |
sh_entsize | Section Entry Size:如果该段有一些固定大小项,这里展示每项的大小 |
有了这个SHT我们就可以找到任何一个想要找到的section在哪里了。这里链接器就可以知道所有段的位置和长度。而仅仅找到每个段的位置是不足以让来链接器完成链接的工作的,我们不能单单是把相似的section合并了事。
在runtime阶段,我们要调用一个变量或者函数是需要知道该函数或者是变量的位置的,这里就需要
对符号进行分析。这里也需要一个表来寻找不同的符号,去对符号进行解析。
symbol Table
如果我们要定位到每个符号在ELF中的位置,这就需要一个section来表述这些符号,也就是.symtab(符号表)。我们知道C语言不难在函数的内部再次定义函数,所以一个ELF文件平分为两层:函数的内部、函数外部。外部的函数和变量是对链接可见的,所以它们可以被其他的ELF文件所引用,也就是符号。
成员 | 含义 |
---|---|
st_name | st_name 保存了指向符号表中字符串表(位于.dynstr 或者.strtab) 的偏移地址,偏移地址存放着符号的名称,如 printf。 |
st_value | st_value 存放符号的值(可能是地址或者位置偏移量)。 |
st_size | st_size 存放了一个符号的大小,如全局函数指针的大小,在一个 32 位 系统中通常是 4 字节。 |
st_other | st_other 变量定义了符号的可见性。 |
st_shndx | 每个符号表条目的定义都与某些节对应。st_shndx 变量保存了相关节头表的索引。 |
st_info | st_info 指定符号类型及绑定属性。 |
st_info
st_info分为两个部分Symbol Type和Symbol Binding。
Bind:
宏定义 | 值 | 说明 |
---|---|---|
STB_LOCAL | 0 | 本地符号在目标文件之外是不可见的,目标文件包含 了符号的定义,如一个声明为 static 的函数。 |
STB_GLOBAL | 1 | 全局符号对于所有要合并的目标文件来说都是可见 的。一个全局符号在一个文件中进行定义后,另外一个文件可以对这 个符号进行引用。 |
STB_WEAK | 2 | 与全局绑定类似,不过比 STB_GLOBAL 的优先级低。 被标记为 STB_WEAK 的符号有可能会被同名的未被标记为 STB_WEAK 的符号覆盖。 |
Type:
宏定义 | 值 | 说明 |
---|---|---|
STT_NOTYPE | 0 | 符号类型未定义。 |
STT_OBJECT | 1 | 该符号是一个数据对象,比如变量、数组等 |
STT_FUNC | 2 | 表示该符号与函数或者其他可执行代码关联。 |
STT_SECTION | 3 | 该符号表示一个段,这种符号必须是STB_LOCAL的 |
STT_FILE | 4 | 该符号表示文件名,一般是该目标文件对应的源文件名,它一定是STB_LOCAL类型的,并且它的st_shndx一定是SHN_ABS |
st_shndx
宏定义 | 值 | 说明 |
---|---|---|
SHN_ABS | 0xfff1 | 表示该符号包含一个绝对的值,比如文件名的符号 |
SHN_COMMON | 0xfff2 | 表示该符号是一个“common块”类型的符号,一般未初始化的全局符号就是这种类型的。 |
SHN_UNDEF | 0 | 表示该符号未定义。这个符号表示该服符号在目标文件被引用,但定义在其他目标文件中 |
以上就是除了重定位段以外关于静态链接的的核心section。重定位我们在后面讨论静态链接的过程中再讨论。
静态链接过程
符号解析
静态链接的第一步需要符号解析,在ELF文件被读入内存后,不同的文件可能有相同的符号名。符号解析需要解决这种冲突,需要在所有的ELF中选择一个作为该符号的定义,当然对于局部符号而言是不需要考虑不同文件件的冲突问题的。
对于全局符号而言其有四种状态
状态 | 说明 |
---|---|
Defined(有定义) | 该符号在ELF文件中有确切的位置来储存其数值 |
Tentative(临时定义) | 该符号没有确定的储存空间,也就是在SHN_COMMON中的全局变量,在编译阶段无法确定其最最终归属 |
Undefined(未定义) | 该符号没有储存空间,也就是其属于SHN_UNDEF段 |
Weak Bind(弱定义) | 该符号属于弱定义,也就是其bind为STB_WEAK |
这些符号的优先级为Undefined < Weak Bind < Tentative < Defined
全局符号只能存在一个,当出现重复的全局符号时只能选择一个作为目标文件的符号,这里的四种符号,强符号只有Defined这一种,其余三项都是弱符号
而选择符号有以下条规则:
- 当存在强符号,且只有这一个强符号时,选择该强符号
- 当同时存在多个强符号时,链接器无法处理,报错
- 当没有强符号存在时,按照优先级选择一个弱符号
大多数情况下可以通过这个规则选择出需要的符号,但若在最后一种情况下出现type和size的区分有时候链接器也会无法做出决定。
经过这一步链接器就将不同的ELF文件中的符号全部处理完成了
Section merge
当我们的符号解析完成后,接下来就是对相同类型的section进行合并。通过被解析的符号,将相同section的符号放在同一section,这样我们就确定了每个符号在EOF文件中的大小和起始位置,建立起一个映射关系。合并起来的section被称作Segment。
引用重定位
完成符号解析和section合并后基本EOF文件成型,但是函数或者变量之间的调用位置还没有写入,这时就需要对所有的符号进行重定位。
我们先来看看重定位表的结构
Relocation Table
对于可以重定位的ELF文件来说,它必须有重定位表。对于每个要被重定位的ELF段都有一个对应的重定位表。对应.text有.rel.text,对于.data有.rel.data。每个要被重定位的地方叫做重定位入口。
重定位入口结构为:
成员 | 含义 |
---|---|
r_offset | 重定位入口的偏移,这个值表示该重定位入口所要修正的位置的第一个字节相对于段的偏移 |
r_info | 重定位入口的类型和符号,一部分表示重定位入口的类型,一部分是重定位入口在符号表的下标 |
链接器知道了以上两个成员就知道了在哪里进行重定位,又重定位到哪里去。
而重定位的类型则对应着多种多样的寻址模式,而重定位的基本类型分为相对寻址和绝对寻址。
两种指令修正方式
我们先设:
A = 保存在被修正位置的值
P = 被修正的位置,可以通过r_offset得到
S = 符号的实际地址,这个可以通过r_info得到
相对寻址
对与相对寻址而言,我们先通过重定位表的r_offset找到了P,也就是我们要修改的位置。
然后通过r_info找到对应要引用符号的的符号表,也就找到了S,符号的相对位置。
仅仅这样是不够的我们还要考虑rip指针在引用时的位置,这由指令本身决定,在被要被修改的地方保存着这值A。
那么相对寻址要修改为S-P+A。这样我们就实现了相对寻址。
绝对寻址
绝对寻址就不用考虑二者的偏移,只需要考虑S和A即可,既写入的是S+A。
结语
这些就是静态链接的一些基本的框架了,了解了这些也就基本了解了链接的大致过程。学习这一块翻了好多书,也看了些视频,我会列在下面的参考资料里,有不明白的地方供大家参考。
参考资料
哔哩哔里up主yaaangmin的视频:深入理解计算机系统合集(https://www.bilibili.com/video/BV17K4y1N7Q2)
以及其编写的文档:https://github.com/yangminz/bcst_csapp/releases/tag/chapter_1_2_3
《深入了解计算机系统(csapp)》
《程序员的自我修养——链接、装载与库》
《linux二进制分析》