[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 pltstart,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. Vue UI 可视化项目管理界面

    除了直接使用npm的命令进行安装脚手架的安装以外,我们还可以使用Vue提供的GUI方法vue ui来进行项目的构建以及安装 win+R powershell 打开终端 在一个干净的目录下输入命令 vu ...

  2. Ubuntu安装docker(摘自官网,自用)

    在 Ubuntu 上安装 Docker 引擎(按照标红顺序执行命令) 预计阅读时间:11分钟 适用于 Linux 的 Docker 桌面 Docker Desktop 可帮助您在 Mac 和 Wind ...

  3. Windows安装使用wget

    Windows安装使用wget 0x01 什么是wget 你肯定知道,否则就不会安装了 0x02 下载wget 下载地址:https://eternallybored.org/misc/wget/ 在 ...

  4. Java设计模式——抽象工厂模式

    抽象工厂模式也是创建模式,可以把它理解成创建工厂的工厂,这种模式也是我们经常使用的.在抽象工厂中的接口是用来创建工厂的,每个生成的工厂又都可以按照工厂模式创建其他对象. 举例说明: 创建Shape接口 ...

  5. 探索Django验证码功能的实现 - DjangoStarter项目模板里的封装

    前言 依然是最近在做的这个项目,用Django做后端,App上提交信息的时候需要一个验证码来防止用户乱提交,正好我的「DjangoStarter」项目脚手架也有封装了验证码功能,不过我发现好像里面只是 ...

  6. JetBrains Rider C# 学习①

    Rider 发现 Alt+F7 键无效: 把GeForce Experience里的游戏覆盖关闭 前言 C#从入门到精通 链接:https://pan.baidu.com/s/1UveJI_f-c5D ...

  7. springcloud集群测试

    使用ribbon实现负载均衡,访问同一个url,轮询不同的服务提供端,从不同的数据库中取数据.

  8. 【Azure API 管理】解决API Management添加AAD Group时遇见的 Failed to query Azure Active Directory graph due to error 错误

    问题描述 为APIM添加AAD Group时候,等待很长很长的时间,结果添加失败.错误消息为: Write Groups ValidationError :Failed to query Azure ...

  9. nodejs的tream(流)解析与模拟文件读写流源码实现

    什么是流? 可读流于可写流 双工流于转换流 背压机制与文件流模拟实现 一.什么是流? 关于流的概念早在1964年就有记录被提出了,简单的说"流"就是控制数据传输过程的程序,比如在那 ...

  10. mybaitis查询 (数据库与实体类字段名不相同)

    1.这是我的数据库字段名和实体类字段名 2.方法 方法一: 查询的结果标题 会跟实体类的属性一一匹配,一定要一致就算数据库字段和属性不一致,我们可以把查询结果设置一个别名,让别名=属性名 方法二:使用 ...