[pwn基础]动态链接原理
[pwn基础]动态链接原理
动态链接概念
为了解决空间浪费和更新困难问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不是将它们静态链接在一起。
简单的说:不对那些组成程序的目标文件进行链接,等到程序要运行时候才进行链接。
把链接这个过程推迟到了运行时再进行
,这就是动态链接(Dynamic Linking)
的基本思想。
动态链接调用so例子
LibTest.h LibTest.c
#ifndef LIBTEST_H
#define LIBTEST_H
void foobar(int i);
#endif
#include "LibTest.h"
#include <stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d\n",i);
}
编译成.so(动态链接库)
gcc -fPIC -shared LibTest.c -o libtest.so
#-fPIC是与地址无关选项
#-shared 是编译成so 动态联机
#-o 输出,so文件名必须以lib开头
Program1.c
#include "LibTest.h"
int main(int argc,char *argv[])
{
foobar(1);
return 0;
}
Program2.c
#include "Lib.h"
int main(int argc,char *argv[])
{
foobar(2);
return 0;
}
分别编译Program1 和Program2动态调用libtest.so
gcc Program1.c -L. -ltest -o Program1
gcc Program2.c -L. -ltest -o Program2
export LD_LIBRARY_PATH=/home/pwn/testdemo:$LD_LIBRARY_PATH
#上面命令的意思分别是
#-L. 代表的是so在本地当前目录查找
#-ltest 动态调用so有一套自己的命名规则,一般必须是lib带头,然后才是so名字.所以-l后面跟的是lib之后的so名,忽略后缀。
#export LD_LIBRARY_PATH代表的是把动态链接目录加入环境变量,默认是/usr/lib下
~/testdemo » ldd Program1
linux-vdso.so.1 (0x00007ffd25b6e000)
libtest.so => /home/pwn/testdemo/libtest.so (0x00007f3ae466f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3ae446a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3ae467b000)
#ldd命令可以用来查看当前程序所调用的动态so。
运行结果:
~/testdemo » ./Program1
Printing from Lib.so 1
~/testdemo » ./Program2
Printing from Lib.so 2
GOT(全局偏移表)
GOT表的全称是Global Offset Table(全局偏移表)
可以把它理解成为了动态链接,把所有的符号偏移量或(绝对地址)都放入到了一个表里这就是GOT表
- .got表(一般放的是全局变量和static变量)
- .got.plt表(一般放的就是引用so的函数,即导入函数)
下面我们来做个实验加深下理解。
/*a.c源码*/
extern int shared; //外部符号,跨模块
int main()
{
int a = 100;
swap(&a,&shared);//外部符号,调用外部模块的swap函数
}
/*b.c源码*/
extern int shared = 1;
void swap(int *a,int *b)
{
*a ^= *b ^= *a ^= *b;
}
#编译成.so
gcc -fPIC -shared b.c -o libb.so
export LD_LIBRARY_PATH=/home/pwn/got:$LD_LIBRARY_PATH
#编译a可执行程序
gcc a.c -L. -lb -o a
从下图中可以看到shared变量的访问和之前我没静态链接篇的访问方式是一模一样的,用的是当rip+偏移这种间接寻址的方式来访问三方模块的全局变量,而函数swap
在这里则变成了swap@plt
。
利用断点跟入swap@plt函数
,然后跟到了plt表
,后面会将plt表的用途,可以看到有个jmp是间接跳转,加上偏移后刚好就是got表的位置,对应的是存放swap函数的绝对地址。
got表劫持小实验
#include <stdio.h>
void fun()
{
system("id");
}
int main()
{
//下面演示:Printf("id") 变成shell命令
printf("id");
return 0;
}
最后成功劫持,将printf劫持成了system函数,输出了当前id
PLT(延迟绑定)
PLT概念
首先, 我们要知道, GOT和PLT只是一种重定向
的实现方式. 所以为了理解他们的作用, 就要先知道什么是重定向, 以及我们为什么需要重定向.
重定向我在静态链接文章中已经介绍过,就是编译成.o文件时候,那些外部符号变量和函数无法确定时候,预留的填充值,比如用0填充,然后等待链接时候才真实的被写入。
之前介绍的是静态链接的情况,那么动态链接时候会怎么样呢?一遍实战一遍学习。
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(int argc,char *argv[])
{
print_banner();
return 0;
}
#编译分别生成.o 和可执行程序
gcc -c plt.c -o plt.o -m32
gcc -o plt plt.c -m32
编译后产生了.o和plt可执行程序,我们先用objdump来看看plt.o的汇编源码,命令是objdump -M intel -dw plt.o
可以看到call printf的这个地址是填0的,因为这时候编译器并不知道printf的函数真实地址,printf函数是需要程序被装载后才能确定地址
,那么动态链接器为什么不在程序运行起来后,装载起来后
再把真实的printf地址
填进去呢?因为这个call printf
的语句是在.text
代码段的,运行起来后代码段是无法被修改的,只能修改.data
数据段。
????????那怎么搞啊,都不能修改代码段,那搞什么。
只能羡慕大佬么的技巧,大佬么总是那么骚,还是有办法搞的,动态链接器生成了一段额外的小代码判断,通过这段代码获取printf函数地址,并完成对它的调用。
延迟绑定(PLT表)
用来存放这小片段代码的地方就是PLT表,下面是伪代码片段。
.text
....
//调用printf的call指令
call printf_plt
....
printf_plt:
mov rax,[printf函数的存储地址] //GOT表中
jmp rax //跳过去执行printf函数
.got.plt
.....
printf下标
这里存储了printf函数重定位后的真实地址
链接阶段发现printf
定义在动态库 glibc
时,链接器生成一段小代码
print@plt
,然后printf@plt
地址取代原来的printf
。因此转化为链接阶段对printf@plt
做链接重定位,而运行时才对printf
做运行时重定位,具体调用流程图,可以参考如下:
实战学习
好的,接下来我们继续用上面的例子,详细对的PLT表进行分析,首先我们用命令objdump -M intel -dw plt
查看每个段的数据,有汇编则反汇编。
000003a0 <.plt>:
3a0: ff b3 04 00 00 00 push DWORD PTR [ebx+0x4]
3a6: ff a3 08 00 00 00 jmp DWORD PTR [ebx+0x8]
3ac: 00 00 add BYTE PTR [eax],al
...
000003b0 <puts@plt>:
3b0: ff a3 0c 00 00 00 jmp DWORD PTR [ebx+0xc]
3b6: 68 00 00 00 00 push 0x0
3bb: e9 e0 ff ff ff jmp 3a0 <.plt>
...
0000051d <print_banner>:
51d: 55 push ebp
51e: 89 e5 mov ebp,esp
...
53a: e8 71 fe ff ff call 3b0 <puts@plt>
...
547: c3 ret
+0000 0x56556fd8 e0 1e 00 00 00 00 00 00 00 00 00 00 90 cd e4 f7 │....│....│....│....│
+0010 0x56556fe8 b0 de df f7 00 00 00 00 80 58 e1 f7 00 00 00 00
OK,我们对上面的代码进行分析,收我们关注printf_banner
函数调用的printf
,这里因为编译优化的缘故printf变成了puts
,在53a
这里可以看到调用了
puts@plt
,puts@plt这里有3句汇编代码,分别是jmp到ebx+0xC值的地址,然后又push0,又jmp到0x3a0,因为这里我们不知道ebx是什么值,所以需要动态调试来一步步详细的观察下,用命令pwndbg,gdb plt
,start
,b printf_banner
c
,然后单步到call puts@plt
。
从上图中可以发现,ebx的值是0x56556fd8
,这其实是got表装载到内存后的地址,我们可以用readelf -SW plt
查看文件中got表的偏移。
可以发现偏移正好是fd8
,虚拟内存的起始地址加上fd8就是0x56556fd8
。
那么如何查看程序加载的起始地址呢?可以借助强大的pwndbg中的vmmap
命令来查看内存分布。
正好是(起始地址)0x56555000
+偏移(0xfd5
)=0x56556fd8
。
OK现在回归正题,我们已经知道puts@plt
中的jmp是要跳转到GOT表中偏移0xC的位置,那么这个位置存放的是什么值呢?
聪明的你已经猜到了,他其实就是puts
函数的真实地址,但是!
为了不影响程序运行的速度,因为我们程序一运行就把所有符号地址都确定,然后都填入got表,那一但我们调用到非常的动态库时候,性能肯定会受影响的。所以,采用了延迟绑定机制。
000003b0 <puts@plt>:
3b0: ff a3 0c 00 00 00 jmp DWORD PTR [ebx+0xc]
3b6: 68 00 00 00 00 push 0x0
3bb: e9 e0 ff ff ff jmp 3a0 <.plt>
延迟绑定机制原理
我们先来看看,这个got表偏移+0xC位置,在文件位置中的值是多少,可以看到他的值是0x3B6
,你可以仔细看看puts@plt
函数,jmp后下一句汇编地址是多少?
00001ee0
00000000
00000000
000003B6 [ebx+0xC]
刚好是0x3b6
,对应的汇编语句是push 0,接着又跳到了jmp 0x3a0 <.plt>,跳到了plt表。
3b6: 68 00 00 00 00 push 0x0
plt表中的汇编如下:
这几句汇编代码会调用内核的_dl_runtime_resolve()
函数,把puts函数在动态库中的真实地址
放入到got表
中。
000003a0 <.plt>:
3a0: ff b3 04 00 00 00 push DWORD PTR [ebx+0x4]
3a6: ff a3 08 00 00 00 jmp DWORD PTR [ebx+0x8]
3ac: 00 00 add BYTE PTR [eax],al
所以延迟绑定机制的原理:就是第一次在调用函数时候,才把真实的地址放入got表(进行绑定),之后再调用这函数则直接jmp到真实地址。
最后,在其他大佬博客上偷了张详细的函数调用plt表延迟绑定的流程图。
参考文献:
//yjy123123.github.io/2021/12/06/延迟绑定过程分析/
//evilpan.com/2018/04/09/about-got-plt/ 非常完整详细的讲解博客
《程序员的自我修养 链接、装载与库》 这本书,真的是神书,全部仔细看完肯定有帮助。