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

参考文献:
https://yjy123123.github.io/2021/12/06/延迟绑定过程分析/
https://evilpan.com/2018/04/09/about-got-plt/ 非常完整详细的讲解博客
《程序员的自我修养 链接、装载与库》 这本书,真的是神书,全部仔细看完肯定有帮助。
[pwn基础]动态链接原理的更多相关文章
- java基础--动态代理实现与原理详细分析
关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 一.代理模式 ...
- linux 下动态链接实现原理
符号重定位 讲动态链接之前,得先说说符号重定位. c/c++ 程序的编译是以文件为单位进行的,因此每个 c/cpp 文件也叫作一个编译单元(translation unit), 源文件先是被编译成一个 ...
- 再探Linux动态链接 -- 关于动态库的基础知识
在近一段时间里,由于多次参与相关专业软件Linux运行环境建设,深感有必要将这些知识理一理,供往后参考. 编译时和运行时 纵观程序编译整个过程,细分可分为编译(Compiling,指的是语言到平台 ...
- JAVA基础复习与总结<一>(2) 父类引用指向子类对象(向上转型、动态链接)
先来看看下列代码 public class Animal { public static void main(String[] args){ Animal animal = new Cat(); // ...
- 再探Linux动态链接 -- 关于动态库的基础知识(Dynamic Linking on Linux Revisited)
在近一段时间里,由于多次参与相关专业软件Linux运行环境建设,深感有必要将这些知识理一理,供往后参考. 编译时和运行时 纵观程序编译整个过程,细分可分为编译(Compiling,指的是语言到平台 ...
- [pwn基础] Linux安全机制
目录 [pwn基础] Linux安全机制 Canary(栈溢出保护) 开启关闭Cannary Canary的种类 Terminator canaries(终结者金丝雀) Random cannarie ...
- [pwn基础]Pwntools学习
目录 [pwn基础]Pwntools学习 Pwntools介绍 Pwntools安装 Pwntools常用模块和函数 pwnlib.tubes模块学习 tubes.process pwnlib.con ...
- http 基础与通讯原理
目录 http 基础与通讯原理 Internet 与中国 1990年10月 注册CN顶级域名 1993年3月2日 接入第一根专线 1994年4月20日 实现与互联网的全功能连接 1994年5月21日 ...
- 聊聊动态链接和dl_runtime_resolve
写在前面 linux下的动态链接相关结构,重新回顾_dl_runtime_resolve的流程以及利用方法 动态链接相关结构 为了高效率的利用内存,多个进程可以共享代码段.程序模块化方便更新维护等,动 ...
随机推荐
- SpringBoot利用自定义注解实现通用的JWT校验方案
利用注解开发一个通用的JWT前置校验功能 设计的预期: 系统中并不是所有的应用都需要JWT前置校验,这就需要额外设计一个注解Annotation来标识这个方法需要JWT前置校验.例如: @GetMap ...
- mybatis-day1入门案例
首先应先创建maven工程 (jar包要导入,五个核心jar包) 如果测试运行时出现了不支持版本5,则要修改以下内容 类的路径如下 1.配置pom.xml依赖 <?xml version=&qu ...
- ORM中choices参数(重要)、MTV于MVC模型、多对多关系三种创建方式
choices参数(重要) **使用方式
- Blazor 生命周期
执行周期 1. SetParametersAsync 2. OnInitializedAsync(调用两次) 和 OnInitialized: 3. OnParametersSetAsync 或 On ...
- NodeJS学习day2
今天还是接着学习IO,主要方面是文件路径相关操作 练习代码如下: const fs = require('fs') // 执行C:\CS\Node.js>node .\day2\fsRoad.j ...
- 一款开源的文件搜索神器,终于不用记 find 命令了
这是 HelloGitHub 推出的<讲解开源项目>系列,用一篇文章带你快速上手有趣的开源项目. 今天给大家推荐一个好用+开源的文件搜索工具--fd 该工具支持大多数主流操作系统,快来更新 ...
- 从小白到侠客的 Windows 快捷键宝典
"天下 武功,唯快不破."你是否羡慕过那些电脑键盘侠客,他们操作起电脑行云流水,任务完成的又快又准.这到底是怎么做到的呢?我们是否也能向他们一样达到把键盘操作熟记于心呢?那就跟着笔 ...
- Pytorch 实现线性回归
Pytorch 实现线性回归 import torch from torch.utils import data from torch import nn # 合成数据 def synthetic_d ...
- PicLite 开发日志 (v0.0.3)
PicLite 开发日志 (v0.0.3) 感谢您阅读本片文章! Gitee 地址:https://gitee.com/XiaoQuQuSD/pic-lite. 新增功能 当错误出现时不再强制 rai ...
- 【面试普通人VS高手系列】Redis和Mysql如何保证数据一致性
今天分享一道一线互联网公司高频面试题. "Redis和Mysql如何保证数据一致性". 这个问题难倒了不少工作5年以上的程序员,难的不是问题本身,而是解决这个问题的思维模式. 下面 ...