ELF与动态链接
ELF文件
- 一种Linux下常用的可执行文件
- ELF中的数据按照Segment和Section两个概念来划分
- Segment
- 用于告诉内核,在执行ELF文件时应该如何映射内存
- 每个Segment主要包含加载地址,文件中的范围,内存的权限,对齐方式
- 是运行时必须提供的信息
- Section
- 用于告诉链接器,ELF中每个部分是什么,哪里是代码,哪里是只读数据,哪里是重定位信息
- 每个Section主要包含Section类型,文件中的位置,大小等信息
- 链接器依赖Section信息将不同的对象文件的代码,数据信息合并,并修复互相引用
- Segment与Section的关系
- 相同权限的Section会放入同一个Segment
- 一个Segment包含许多Section,一个Section可以属于多个Segment
ELF文件格式
- ELF Header
- 架构,ABI版本等基础信息
- program header table的位置和数量
- section header table的位置和数量
- Program header table
- 每个表项定义了一个segment
- 每个segment可包含多个section
- Section header table
- 每个表项定义了一个section
动态链接
-
例如在调用libc库中的时,由于动态链接在运行时才会加载,那么一开始我们并不知道要调用的函数在哪,这是需要用到重定位
-
重定位:
- 指二进制文件中的待填充项
- 链接器在链接时填充,例如链接多个目标文件时,修正互相应用的函数,变量地址
- 动态链接器在运行时填充,例如动态解析库函数printf
- 指二进制文件中的待填充项
-
动态链接中的延迟绑定
- 外部函数的地址在运行时才会确定
- 外部函数符号通常在首次调用时才会被解析
- 外部变量不使用延迟绑定机制
-
原因:当整个程序调用了多个外部函数时,在一开始就绑定了所有外部函数这样会影响运行速度。
-
目的:按需解析,提高效率
GOT表
-
GOT表常常用于存放外部库函数地址或局部变量(就是一些待填充项)
-
GOT表项初始状态指向一段PLT代码
-
当库函数被首次调用,真正的函数地址会被解析并填入相应的GOT表项
-
每个外部函数均有一段独立的PLT代码,用于跳转到相应GOT表项中存储的地址
以调用printf函数为例:
最初代码编译时并不会去找printf函数在哪,只是知道要调用libc库(待填充项),运行到含有printf函数的代码时,就会去GOT表里查找。可以从GOT查找到一段PLT代码,通过这段代码去找到printf函数。然后将printf函数的地址存到GOT表中,下次再调用printf只需在GOT表上查找即可
-
.got Section中存放外部全局变量的GOT表,采用非延迟绑定
-
.got.plt Section中存放外部函数的GOT表,采用延迟绑定
-
.got.plt前三项有特殊含义,第四项开始保存引用的外部函数的
-
.plt Section存放了所有外部函数对于的PLT代码
延迟绑定
1.调用了一个外部函数foo call foo@plt
2.跳转至foo@GOT处的地址,实际指向下一条指令,即foo@plt+6 jmp *(foo@GOT)
3.将符号’foo’的符号入栈 push index
4.调用dl_runtime_resolve(link_map, index),这时会把foo()函数的真实地址填入GOT表
5.再次调用foo()函数时只需
GOT表劫持
-
延迟绑定机制要求GOT表必须可写
-
内存漏洞可导致GOT表项被改写,从而劫持PC
-
例:GOT表内本来应该是foo()函数地址的地方,但是由于恶意修改现在指向了system函数。
-
如何防御GOT表劫持
-
重定位只读缓解措施
-
编译选项:gcc -z,relro
-
在进入main()之前,所有的外部函数都会被解析
-
所有GOT表设置为只读
-
绕过方法:
1.劫持为开启该保护的动态库中的GOT表(例如libc中的GOT表)
2.改写函数返回地址或是函数指针
-
-