本文同时发表在https://github.com/zhangyachen/zhangyachen.github.io/issues/147

最近在研究缓冲区溢出攻击的试验,发现其中有一种方法叫做ret2plt。plt?这个词好熟悉,在汇编代码里经常见到,和plt经常一起出现的还有一个叫got的东西,但是对这两个概念一直很模糊,趁着这个机会研究一下。

可以先说一下结论 : plt和got是动态链接中用来重定位的。

GOT

我们知道,一般我们的代码都需要引用外部文件的函数或者变量,比如#include<stdio.h>里的printf,但是由于我们代码中用到的共享对象是运行时加载进来的,在虚拟地址空间的位置并不确定,所以代码里call <addr of printf>addr of printf不确定,只有等运行时共享对象被加载到进程的虚拟地址空间里时,才能最终确定printf的地址,再进行重定位地址

看一个最简单的例子:

#include <stdio.h>

int main(){

    printf("Hello World");

    return 0;
}

用GDB调试一下(关于GDB调试汇编可以参考之前写的GDB 单步调试汇编):

(gdb) ni
0x000000000040054e in main ()
=> 0x000000000040054e <main+14>: e8 71 fe ff ff callq 0x4003c4 <printf@plt>

可以看出,call <addr of printf>callq 0x4003c4代替,而这个0x4003c4并不是真正的printf函数的地址。

可能有人已经想到了,为什么不能直接在printf函数地址确定后,直接将call <addr of printf>修改为call <real addr of printf>,像静态链接那样呢(静态链接是在链接阶段进行重定位,直接修改的代码段)?有两个原因:

  • 现代操作系统不允许修改代码段,只能修改数据段。
  • 如果上面的代码片段是在一个共享对象内,修改了代码段,那么它就无法做到系统内所有进程共享同一个共享对象,因为代码段被修改了。而动态库的主要一个优点就是多个进程共享同一个共享对象的代码段,节省内存空间,但是进程拥有数据段的独立副本。

所以,我们很容易的想到,既然不能修改代码段,能修改数据段,我们可以在共享对象加载完成后,将真实的符号地址放到数据段中,代码中直接读取数据段内的地址就行,这里开辟的空间就叫做GOT(图有点挫)。

  • 为每一个需要重定位的符号建立一个GOT表项。
  • 当动态链接器装载共享对象时查找每一个需要重定位符号的变量地址,填充GOT。
  • 当指令需要访问变量或者函数的地址时,从对应的GOT表项中读出地址,再访问即可。对应的指令可能是callq *(addr in GOT)或者movq offset(%rip) %rax(%rax就是全局变量的地址,可以用(%rax)解引用)。

但是这样有一个问题,一个动态库可能有成百上千个符号,但是我们引入该动态库可能只会使用其中某几个符号,像上面那种方式就会造成不使用的符号也会进行重定位,造成不必要的效率损失。我们知道,动态链接比静态链接慢1% ~ 5%,其中一个原因就是动态链接需要在运行时查找地址进行重定位。

所以ELF采用了延迟绑定的技术,当函数第一次被用到时才进行绑定。实现方式就是使用plt。

PLT

我们可以先自己独立思考如何实现延迟绑定。

  • 上文描述的是动态链接器主动将确定好的符号地址放到GOT中,延迟绑定需要我们自己主动告诉一个模块:我现在需要该符号的确定地址。假设该模块叫做_dl_runtime_resolve()
  • 我们需要告诉_dl_runtime_resolve()需要寻找的符号,也就是函数参数。可以放到栈中或者寄存器传递。
  • _dl_runtime_resolve()寻找完符号的特定地址后,放到寄存器上,比如%rax,供调用者使用。

所以初步的实现步骤是:

callq plt_printf    <printf@plt>
......
...... plt_printf:
pushq %rbp ## allocate stack frame
movq %rsp,%rbp
pushq iden_of_printf ## 告诉_dl_runtime_resolve()找printf函数地址,即_dl_runtime_resolve()的参数>
callq _dl_runtime_resolve()
callq %rax ## %rax存放printf真实地址
leaveq ## deallocate stack frame
retq

上面的步骤可以实现通过一段小代码(plt)实现延迟绑定,但是存在一个问题:每一次调用printf的时候都需要走一遍这个步骤,然而printf的地址一旦确定就不会变了,所以我们需要一个缓存机制,将查找好的printf地址缓存起来。

PLT与GOT

上面说过_dl_runtime_resolve会将确定好的符合地址放到GOT中,那么在需要延迟加载的情况下,GOT里存放什么地址?上面说过需要我们需要将确定好的符号地址缓存起来,那么ELF是如何通过PLT与GOT的配合做到延迟加载的?我们直接看一个真实的例子就行:

#include <stdio.h>

int main(){

    printf("Hello World");

    printf("Hello World Again");

    return 0;
}

gdb调试一下:

One 调用printf的plt

第一次调用printf,会调用printf对应的plt代码片段,与上面我们自己分析实现延迟加载的步骤一样:

(gdb) ni
0x000000000040054e in main ()
=> 0x000000000040054e <main+14>: e8 71 fe ff ff callq 0x4003c4 <printf@plt>

Two 调到printf对应的GOT里存储的地址

进到<printf@plt>看看:

(gdb) si
0x00000000004003c4 in printf@plt ()
=> 0x00000000004003c4 <printf@plt+0>: ff 25 56 05 20 00 jmpq *0x200556(%rip) # 0x600920 <printf@got.plt>

这里跳到了printf对应的GOT里存储的地址。(elf对got做了细分:got存放全局变量引用的地址,got.plt存放函数引用的地址

看看动态链接在将确定的符号地址放到GOT前,GOT里存放的是什么地址:

(gdb) x 0x600920
0x600920 <printf@got.plt>: 0x004003ca
(gdb) disas 0x4003c4
Dump of assembler code for function printf@plt:
0x00000000004003c4 <+0>: jmpq *0x200556(%rip) # 0x600920 <printf@got.plt>
=> 0x00000000004003ca <+6>: pushq $0x0
0x00000000004003cf <+11>: jmpq 0x4003b4
End of assembler dump.

有意思的是jmp到了下一条指令的地址。其实这个时候我们已经可以猜出来了:延迟加载之前,got.plt里存放的是下一条指令地址,延迟加载之后,got.plt里存放的就是真实的符号地址,就可以直接jmp到printf函数里了。

Three 将printf对应的标识压到栈中,并跳到plt[0]

(gdb) ni
0x00000000004003ca in printf@plt ()
=> 0x00000000004003ca <printf@plt+6>: 68 00 00 00 00 pushq $0x0
(gdb) ni
0x00000000004003cf in printf@plt ()
=> 0x00000000004003cf <printf@plt+11>: e9 e0 ff ff ff jmpq 0x4003b4
(gdb) si
0x00000000004003b4 in ?? () ## 这里应该是plt[0],但是gdb不知道为什么没有显示出来
=> 0x00000000004003b4: ff 35 56 05 20 00 pushq 0x200556(%rip) # 0x600910 <_GLOBAL_OFFSET_TABLE_+8>

Four 在plt[0]中调用_dl_runtime_resolve查找符合真实地址

说明这个是什么地址??0x600910

(gdb)
0x00000000004003b4 in ?? ()
=> 0x00000000004003b4: ff 35 56 05 20 00 pushq 0x200556(%rip) # 0x600910 <_GLOBAL_OFFSET_TABLE_+8>
(gdb)
0x00000000004003ba in ?? ()
=> 0x00000000004003ba: ff 25 58 05 20 00 jmpq *0x200558(%rip) # 0x600918 <_GLOBAL_OFFSET_TABLE_+16>
(gdb)
_dl_runtime_resolve () at ../sysdeps/x86_64/dl-trampoline.S:34
34 subq $56,%rsp
=> 0x00007ffff7deef30 <_dl_runtime_resolve+0>: 48 83 ec 38 sub $0x38,%rsp

我们不用管_dl_runtime_resolve是怎么处理的,直接看_dl_runtime_resolve处理完成后printf对应的GOT的值:

(gdb)
56 jmp *%r11 # Jump to function address.
=> 0x00007ffff7deef8e <_dl_runtime_resolve+94>: 41 ff e3 jmpq *%r11
0x00007ffff7deef91: 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 data32 data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
(gdb)
0x00007ffff7a7b5d0 in printf () from /lib64/libc.so.6
=> 0x00007ffff7a7b5d0 <printf+0>: 48 81 ec d8 00 00 00 sub $0xd8,%rsp
(gdb)
......
......
(gdb) x 0x600920
0x600920 <printf@got.plt>: 0xf7a7b5d0

与之前猜测的一样,printf对应的GOT表项目前已经存放了printf真实的虚拟地址。那么在下次调用时就避免再重定位,直接跳到printf地址了。

Five 第二次调用printf

(gdb) si
0x00000000004003c4 in printf@plt ()
=> 0x00000000004003c4 <printf@plt+0>: ff 25 56 05 20 00 jmpq *0x200556(%rip) # 0x600920 <printf@got.plt>
(gdb) x 0x600920
0x600920 <printf@got.plt>: 0xf7a7b5d0
(gdb) si
0x00007ffff7a7b5d0 in printf () from /lib64/libc.so.6
=> 0x00007ffff7a7b5d0 <printf+0>: 48 81 ec d8 00 00 00 sub $0xd8,%rsp

直接跳到printf的虚拟地址。

下面这张图可以总结上面的五步过程:

动态链接的PLT与GOT的更多相关文章

  1. ELF文件加载与动态链接(二)

    GOT应该保存的是puts函数的绝对虚地址,这里为什么保存的却是puts@plt的第二条指令呢? 原来“解释器”将动态库载入内存后,并没有直接将函数地址更新到GOT表中,而是在函数第一次被调用时,才会 ...

  2. 深入了解GOT,PLT和动态链接

    之前几篇介绍exploit的文章, 有提到return-to-plt的技术. 当时只简单介绍了 GOT和PLT表的基本作用和他们之间的关系, 所以今天就来详细分析下其具体的工作过程. 本文所用的依然是 ...

  3. Mach-O 的动态链接(Lazy Bind 机制)

    ➠更多技术干货请戳:听云博客 动态链接 要解决空间浪费和更新困难这两个问题最简单的方法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态的链接在一起.简单地讲,就是不对那些组成程序的目标文 ...

  4. ELF动态链接

    为什么要使用动态链接? 在现代的linux系统中,假设一个普通的程序会使用到c语言静态库至少1MB以上,那么,如果我们的机器运行100个这样的程序,就用浪费近100MB的内存:如果磁盘有2000个这样 ...

  5. 程序的链接和装入及Linux下动态链接的实现

    http://www.ibm.com/developerworks/cn/linux/l-dynlink/ 程序的链接和装入及Linux下动态链接的实现 程序的链接和装入存在着多种方法,而如今最为流行 ...

  6. 实例分析ELF文件动态链接

    参考文献: <ELF V1.2> <程序员的自我修养---链接.装载与库>第6章 可执行文件的装载与进程 第7章 动态链接 <Linux GOT与PLT> 开发平台 ...

  7. ELF 动态链接 - so 的 重定位表

    动态链接下,无论时可执行文件还是共享对象,一旦对其他共享对象有依赖,也就是所有导入的符号时,那么代码或数据中就会有对于导入符号的引用.而在编译时期这些导入符号的确切地址时未知的.只有在运行期才能确定真 ...

  8. ELF文件加载与动态链接(一)

    关于ELF文件的详细介绍,推荐阅读: ELF文件格式分析 —— 滕启明.ELF文件由ELF头部.程序头部表.节区头部表以及节区4部分组成. 通过objdump工具和readelf工具,可以观察ELF文 ...

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

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

随机推荐

  1. 091 01 Android 零基础入门 02 Java面向对象 02 Java封装 01 封装的实现 03 # 088 01 Android 零基础入门 02 Java面向对象 02 Java封装 02 static关键字 01 static关键字(上)

    091 01 Android 零基础入门 02 Java面向对象 02 Java封装 01 封装的实现 03 # 088 01 Android 零基础入门 02 Java面向对象 02 Java封装 ...

  2. 【转载】绕过CDN找到源站的思路

    [原文:https://mp.weixin.qq.com/s/8NUvPqEzVjO3XbmCBukUvQ] 绕过CDN的思路 网上有很多绕过CDN的思路,但是存在很多问题,以下是收集并总结的思路.站 ...

  3. 远程触发Jenkins的Pipeline任务的并发问题处理

    前文概述 本文是<远程触发Jenkins的pipeline任务>的续篇,上一篇文章实战了如何通过Http请求远程触发指定的Jenkins任务,并且将参数传递给Jenkins任务去使用,文末 ...

  4. java安全编码指南之:锁的双重检测

    目录 简介 单例模式的延迟加载 double check模式 静态域的实现 ThreadLocal版本 简介 双重检测锁定模式是一种设计模式,我们通过首次检测锁定条件而不是实际获得锁从而减少获取锁的开 ...

  5. Redis GEO 功能使用场景

    本文来源:https://www.dazhuanlan.com/2020/02/05/5e3a0a3110649/ 背景 前段时间自己在做附近直播相关业务,其中有一个核心的点就是检索用户附近的主播,也 ...

  6. 第3天 | 12天搞定Python,用VSCode编写代码

    Visual Studio Code (简称 VS Code), 是一款免费并且开源的现代化轻量级代码编辑器,支持语法高亮.智能代码补全.自定义热键.括号匹配.代码片段等特性,并针对网页开发做了优化. ...

  7. Selenium之自动化常遇问题

    1.等待方式的选择 大家都知道Selenium中等待方式有三种,当在页面没有找到定位的元素抛出异常,那么加个等待,还有问题就换个等待方式 强制等待 time.sleep(10) 显式等待 driver ...

  8. swoft运行流程

    启动命令 php bin/swoft http:start 或者  swoftctl run -c http:start 1 入口文件 bin/swoft.php #!/usr/bin/env php ...

  9. spring boot: 设计接口站api的版本号,支持次版本号(spring boot 2.3.2)

    一,为什么接口站的api要使用版本号? 1,当服务端接口的功能发生改进后, 客户端如果不更新版本,    则服务端返回的功能可能不能使用,    所以在服务端功能升级后,     客户端也要相应的使用 ...

  10. centos8安装lvs

    一,配置ip转发 [root@localhost sysctl.d]# sysctl -a | grep ip_forward net.ipv4.ip_forward = 1 说明:如果net.ipv ...