CTF权威指南 笔记 -第二章二进制文件- 2.4 -动态链接

目录

静态文件的缺点

动态链接

位置无关代码

延迟绑定

_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函数 并且执行

这样就实现了 函数通过动态链接寻找

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值