动态连接的PLT与GOT

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

能够先说一下结论 : plt和got是动态连接中用来重定位的。github

GOT

咱们知道,通常咱们的代码都须要引用外部文件的函数或者变量,好比#include<stdio.h>里的printf,可是因为咱们代码中用到的共享对象是运行时加载进来的,在虚拟地址空间的位置并不肯定,因此代码里call <addr of printf>addr of printf不肯定,只有等运行时共享对象被加载到进程的虚拟地址空间里时,才能最终肯定printf的地址,再进行重定位地址缓存

看一个最简单的例子:ide

#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(图有点挫)。code

image

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

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

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

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的虚拟地址。

下面这张图能够总结上面的五步过程:

image

(完)

朋友们能够关注下个人公众号,得到最及时的更新:

相关文章
相关标签/搜索