[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表延迟绑定的流程图。

参考文献:

https://yjy123123.github.io/2021/12/06/延迟绑定过程分析/

https://evilpan.com/2018/04/09/about-got-plt/ 非常完整详细的讲解博客

《程序员的自我修养 链接、装载与库》 这本书,真的是神书,全部仔细看完肯定有帮助。

[pwn基础]动态链接原理的更多相关文章

  1. java基础--动态代理实现与原理详细分析

    关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 一.代理模式                     ...

  2. linux 下动态链接实现原理

    符号重定位 讲动态链接之前,得先说说符号重定位. c/c++ 程序的编译是以文件为单位进行的,因此每个 c/cpp 文件也叫作一个编译单元(translation unit), 源文件先是被编译成一个 ...

  3. 再探Linux动态链接 -- 关于动态库的基础知识

      在近一段时间里,由于多次参与相关专业软件Linux运行环境建设,深感有必要将这些知识理一理,供往后参考. 编译时和运行时 纵观程序编译整个过程,细分可分为编译(Compiling,指的是语言到平台 ...

  4. JAVA基础复习与总结<一>(2) 父类引用指向子类对象(向上转型、动态链接)

    先来看看下列代码 public class Animal { public static void main(String[] args){ Animal animal = new Cat(); // ...

  5. 再探Linux动态链接 -- 关于动态库的基础知识(Dynamic Linking on Linux Revisited)

      在近一段时间里,由于多次参与相关专业软件Linux运行环境建设,深感有必要将这些知识理一理,供往后参考. 编译时和运行时 纵观程序编译整个过程,细分可分为编译(Compiling,指的是语言到平台 ...

  6. [pwn基础] Linux安全机制

    目录 [pwn基础] Linux安全机制 Canary(栈溢出保护) 开启关闭Cannary Canary的种类 Terminator canaries(终结者金丝雀) Random cannarie ...

  7. [pwn基础]Pwntools学习

    目录 [pwn基础]Pwntools学习 Pwntools介绍 Pwntools安装 Pwntools常用模块和函数 pwnlib.tubes模块学习 tubes.process pwnlib.con ...

  8. http 基础与通讯原理

    目录 http 基础与通讯原理 Internet 与中国 1990年10月 注册CN顶级域名 1993年3月2日 接入第一根专线 1994年4月20日 实现与互联网的全功能连接 1994年5月21日 ...

  9. 聊聊动态链接和dl_runtime_resolve

    写在前面 linux下的动态链接相关结构,重新回顾_dl_runtime_resolve的流程以及利用方法 动态链接相关结构 为了高效率的利用内存,多个进程可以共享代码段.程序模块化方便更新维护等,动 ...

随机推荐

  1. python---二分查找的实现

    from cal_time import get_running_time @get_running_time def bin_search(li, val): """ ...

  2. springboot读取配置文件赋值给静态变量

    1.实现InitializingBean接口,重写afterPropertiesSet方法,将@Value赋值给成员变量的属性赋值给静态变量,示例如下: /** * @Classname FileUt ...

  3. Codeforces Round #716 (Div. 2), problem: (B) AND 0, Sum Big位运算思维

    & -- 位运算之一,有0则0 原题链接 Problem - 1514B - Codeforces 题目 Example input 2 2 2 100000 20 output 4 2267 ...

  4. python中一些元组知识

    元组 Python 的元组与列表类似,不同之处在于元组的元素不能修改. 元组使用小括号 ( ),列表使用方括号 [ ]. 元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可. 实例(Pytho ...

  5. 重磅!业界首个云原生批量计算项目Volcano正式晋级为CNCF孵化项目

    摘要:4月7日,云原生计算基金会(CNCF)宣布,由华为云捐献的业界首个云原生批量计算项目Volcano正式晋级为CNCF孵化项目. 4月7日,云原生计算基金会(CNCF)宣布,由华为云捐献的业界首个 ...

  6. XCTF练习题---MISC---适合作为桌面

    XCTF练习题---适合作为桌面 flag:flag{38a57032085441e7} 解题步骤: 1.观察题目,下载附件 2.拿到题目以后是一张图片,切换一下通道,发现一张二维码,使用QR进行翻译 ...

  7. 【面试普通人VS高手系列】请说一下你对分布式锁的理解,以及分布式锁的实现

    一个工作了7年的Java程序员,私信我关于分布式锁的问题. 一上来就两个灵魂拷问: Redis锁超时怎么办? Redis主从切换导致锁失效怎么办? 我说,别着急,这些都是小问题. 那么,关于" ...

  8. [ubuntu18.04 python3.6] 清华源 CondaHTTPError: HTTP 000 CONNECTION

    问题 嫌官网源安装jupyter notebook太慢,所以尝试修改为清华源,但每次在Solving environment的时候就报错如下: 解决方法,修改conda配置信息: vim ~/.con ...

  9. ucore lab3 虚拟内存管理 学习笔记

    做个总结,这节说是讲虚拟内存管理,大部分的时间都在搞SWAP机制和服务于此机制的一些个算法.难度又降了一截. 不过现在我的电脑都16G内存了,能用完一半的情景都极少见了,可能到用到退休都不见得用的上S ...

  10. Typora实现云同步

    Typora很好用,但是不自带同步功能,最近找到个简便的实现Typora同步的方法. step1. 下载"坚果云": step2. 在本地创建一个"坚果云"的同 ...