目录
静态文件的缺点
动态链接
位置无关代码
延迟绑定
_dl_runtime_reslove 函数定义
深入审视
静态文件的缺点
随着可执行文件的增加 静态链接带来的浪费空间问题就会愈发严重
如果大部分可执行文件都需要glibc 那么在链接的时候就需要把 libc.a链接进去
如果一个libc.a为5M 那么100就是 5G 例如下面的左边
静态链接的一个很明显的缺点 对标准函数进行一点点 的修改 都需要重新编译整个源文件
动态链接
如果不把系统库和自己编写的代码链接到一个可执行文件 而是分割到两个独立的模块 等到程序进行运行了 再进行链接 这样就可以节省硬盘空间 并且在内存中一个系统库可以被多个程序使用 这样还会节省物理空间
在这种 运行和加载的时候 在内存中完成链接的过程叫做动态链接
这些用于动态链接的系统库 我们叫做共享库 或者 共享对象
这个过程我们通过 动态链接器完成
例如上图的右边
func1.elf func2.elf 不再包含单独的testLib.o
当程序运行func1.ELF的时候 系统将func1.o和依赖的testLib.o载入内存中
进行动态链接
完成后 系统将控制器交给程序入口点
程序开始执行
后面func2.ELF开始执行 因为内存中已经有testLib.o 不需要重复加载 所以直接链接即可
继续使用之前静态链接的例子
这里我们把func.c编译为共享库
gcc -shared -fpic -o func.so func.c
-shared 生成共享库
-fpic 生成和位置无关的代码
gcc -fno-stack-protector -o func.ELF2 main.c ./func.so
main.c 和func.so进行动态链接
生成 func.ELF2
我们查看 func.ELF2的链接格式
查看 汇编格式
objdump -d -M intel --section=.text func.ELF2 |grep -A 11 "<main>"
这里我们就能发现 call 后面跟上了偏移量 地址等
位置无关代码
可以加载而无需重定位的代码就是位置无关代码PIC
通过gcc -fpic就可以生成位置无关代码
一个程序的代码段和数据段的相对位置都是保持不变的 所以指令和变量之间的距离是一个运行时常量 与绝对内存地址无关
于是就有了全局偏移量表 GOT
GOT表位于 数据段的开头
用于保存全局变量和库函数的引用
每个条目占8字节 在加载时会重定位并且填入符号的绝对地址
为了RELRO保护
GOT 被拆分为 .got 和.got.plt
.got 用于保存全局变量引用 写入内存为只读 不需要延迟绑定
.got.plt 用于保存函数引用 具有读写权限 需要延迟绑定
我们可以看看func.o的情况
objdump -h func.so
readelf -r func.so| grep tmp
R_X86_64_GLOB_DAT 表示 动态链接器找到 tmp的值 存入0x3fd8
我们使用汇编可以看看代码
objdump -d -M intel --section=.text func.so |grep -A 20 "<func>"
这里我们能发现 调用函数 rip+0x2ead的地方 rip为下一条指令的地址 这里是 112b
112b + 2ead =3fd8
所以汇编我们能发现调用函数就是指向 实际地址 3fd8
延迟绑定
如果动态链接是要通过动态链接器加载进行 如果要重定位的符号(库函数)多了之后 肯定会影响性能
延迟绑定就是为了解决这个问题
延迟绑定的思想就是
如果函数是第一次被调用 动态链接器才进行符号查找 重定位的操作
如果没有调用就不进行绑定
ELF文件通过 PLT和GOT的配合来实现延迟绑定
每一个被调用的库函数都有一组 GOT 和PLT
位于代码段.got.plt节 是一个PLT数组 每一个条目占16字节
PLT[0] 跳转到动态链接器
PLT[1] 调用系统启动函数 _libc_start_main()函数
main函数就是从里面进行调用
PLT[2] 开始就被调用的各个函数条目
位于数据段.got.plt节的GOT也是一个数组 每条占8字节
GOT[0]和GOT[1]
包含着动态链接库解析函数地址所需要的两个地址 (dynamic和relor条目)
GOT[2] 是动态链接器 ld-linux.so的入口点
GOT[3] 开始就是各个条目这些条目默认指向对应PLT条目的第二条指令
绑定后才会修改函数的实际地址
我们使用例子说明
#include<stdio.h>
void print_bananer(){
printf("Welcome got and plt");
}
int main(void){
print_bananer();
return 0;
}
进行编译
gcc -Wall -g -o test.o -c test.c -m32
gcc -o test test.o -m32 -no-pie
我们直接通过反汇编进行查看
objdump -d test
我们先看到main函数
看到call
11d8: e8 c0 ff ff ff call 119d <print_bananer>
这里地址送11d8
e8为call的机器码
c0ffffff为printf_bananer的地址 这里第一次调用 不知道地址 所以用这个代替
119d是去的地址 指向 print_bananer函数
我们去看119d的地址
发现119d是print_bananer的地址
然后我们看到里面的关键调用
11ba: e8 91 fe ff ff call 1050 <printf@plt>
11ba地址
e8 ->call
91 fe ff ff 为函数地址
我们去看看1050地址
我们发现是
printf.plt地址 就是
然后我们进行代码审计
这里我们可以通过另一个编译 让pie停止
gcc -o test test.o -m32 -no-pie
然后重新进行查看
objdump -d test
发现一个地址 我们使用gdb进行查看这个地址
gdb test
b mian
r
x/x 0x804c010
这里是printf的got表
这里得到另一个地址 我们回去反汇编看看
发现就是下一条 push 0x8这个地址 所以第一个代码是返回 printf@plt的push
然后执行完push
又进行跳转
这里有一个地址 我们去看看
发现是main的共享库文件-0x10偏移量 的地址
然后进行push 0x804c004
又跳到0x804c008
gdb test
x/x 0x804c008
我们发现 这里是0
因为第一次执行 还没有找到printf函数
我们让他执行起来
b main
r
x/x 0x804c008
发现得到了地址 这个地址就是 printf的地址
0x804c008这个的地址 其实就是指向 _dl_runtime_resolve函数
这个函数在动态链接里有着很重要的作用
计算出地址 更新got表等
我们现在来梳理一下
printf.plt ->printf.got.plt -> printf.plt->公共plt->_dl_runtime_reslove函数
这就是printf函数调用过程
这里我们还需要知道
_dl_runtime_reslove 怎么找到printf函数地址
这个是因为在之前我们执行printf@plt的时候 push了0x8
这个就类似于 printf的id 让 _dl_runtime_reslove去找这个id的函数
_dl_runtime_reslove 函数定义
_dl_runtime_resolve(link_map_obj, reloc_index)
reloc_index 这个参数是来寻找 .rel.plt表的 这个表是ELF rel结构体
这个结构体为
typedef struct
{
ELF64_Addr r_offset
ELF64_Xword r_info
}ELF64_Rel
r_offset是用于保存 需要进行重定位和重定位写入内存的位置
r_info的高三位是保存 符号在.dynsym里的下标
通过上面的图像 我们可以发现 r_info又指向 .dynsym中的ELF_sym结构体
这个结构体的内容为
typedef struct
{
ELF64_Word st_name; 符号名称在字符串表中的偏移量
unsigned char st_info; 包含符号类型和捆绑信息
unsigned char st_other; 保留位
ELF64_Half st_shndx; 指向和该符号关联的段/节索引值
ELF64_Addr st_value; 该符号的值 就是地址
ELF64_Xword st_size; 符号所占的字节
}ELF64_Sym
st_value和st_name为重要成员
st_value是符号导出时存放的虚拟地址 不能导出为null
st_name 是相对.dynstr的偏移
然后就是去.dynstr找函数
这个表中存放着 函数地址
所以我们从函数开始导入开始看
程序导入函数 .dynstr中增加一个函数名字符串
在.dynsym中增加一个ELF Sym的结构体 其中 S_name指向 .dynstr
然后又在.rel.plt中增加一个 ELF Rel结构体 其中 r_info指向 .dynsym的 ELF Sym结构体
最后 r_offset 构成 GOT表 存储在 .plt.got段的地址上
这样 我们函数就可以在程序的.plt.got中了
在我们明白这个了 我们重新对test进行审视
深入审视
这里第一个跳转 是无条件跳转到 GOT[0]处
但是GOT[0]在初始化的时候 默认指向 plt的第二条 就是PLT[1]->push 0x8
然后进行跳转到公共的PLT 然后去找_dl_runtime_reslove函数
这里的执行就是
无条件跳转到 xxx.got 把link_map_obj压入栈内
然后开始调回 plt 然后压入 函数id push 0x8
然后进行跳转去找_dl_runtime_reslove函数 并且执行
这样就实现了 函数通过动态链接寻找