[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. MySQL安装配置教程(超级详细)

    一. 下载MySQL Mysql官网下载地址:https://downloads.mysql.com/archives/installer/ 1. 选择要安装的版本,本篇文章选择的是5.7.31版本, ...

  2. Python入门-多进程

    1.获取本机CPU # 早期的CPU是单核:实现多个程序并行,在某一时间点,其实只有一个进程 # 后来硬件多核CPU:多个进程是并行执行. from multiprocessing import cp ...

  3. ELK日志保留7天-索引生命周期策略

    一.简介 ELK日志我们一般都是按天存储,例如索引名为"kafkalog-2022-04-05",因为日志量所占的存储是非常大的,我们不能一直保存,而是要定期清理旧的,这里就以保留 ...

  4. GIL全局解释器锁、协程运用、IO模型

    GIL全局解释器锁 一.什么是GIL 首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念.就好比C是一套语言(语法)标准,但是可以用不 ...

  5. 如何使用 python 爬取酷我在线音乐

    前言 写这篇博客的初衷是加深自己对网络请求发送和响应的理解,仅供学习使用,请勿用于非法用途!文明爬虫,从我做起.下面进入正题. 获取歌曲信息列表 在酷我的搜索框中输入关键词 aiko,回车之后可以看到 ...

  6. C# 利用.NET 升级助手将.NET Framework项目升级为.NET 6

    ​概述 .NET6 正式版本已经发布有一阵子了,今天我就体验一下如何将.NET Framework的项目升级为.NET 6. 升级条件: Windows 操作系统 .NET 6 SDK Visual ...

  7. 在网站copy时自带的版权小尾巴以及“复制代码“,可以怎么实现

    前言 每天网上的博客各个领域都会涌现新文章,有时候看到感兴趣的知识就想把某段文字 copy下来 摘录下来,等有时间后慢慢品味 在部分网站上,如果只是复制少量文字,并没有什么不同.但是当我们复制的文字多 ...

  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. 2021.08.05 P1738 洛谷的文件夹(树形结构)

    2021.08.05 P1738 洛谷的文件夹(树形结构) P1738 洛谷的文件夹 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 重点: 1.树!! 题意: 给出n个网页路径,求 ...

  10. sqlmap源码分析(一)

    Sqlmap源码分析(一) 此次分析的sqlmap目标版本为1.6.1.2#dev 只对sqlmap的核心检测代码进行详细分析其他的一带而过 sqlmap.py文件结构 为了不让篇幅过长下面只写出了s ...