最近在研究缓冲区溢出攻击的试验,发现其中有一种方法叫作ret2plt。plt?这个词好熟悉,在汇编代码里常常见到,和plt常常一块儿出现的还有一个叫got的东西,可是对这两个概念一直很模糊,趁着这个机会研究一下。git
能够先说一下结论 : plt和got是动态连接中用来重定位的。github
咱们知道,通常咱们的代码都须要引用外部文件的函数或者变量,好比#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
callq *(addr in GOT)
或者movq offset(%rip) %rax
(%rax
就是全局变量的地址,能够用(%rax)
解引用)。可是这样有一个问题,一个动态库可能有成百上千个符号,可是咱们引入该动态库可能只会使用其中某几个符号,像上面那种方式就会形成不使用的符号也会进行重定位,形成没必要要的效率损失。咱们知道,动态连接比静态连接慢1% ~ 5%,其中一个缘由就是动态连接须要在运行时查找地址进行重定位。对象
因此ELF采用了延迟绑定的技术,当函数第一次被用到时才进行绑定。实现方式就是使用plt。blog
咱们能够先本身独立思考如何实现延迟绑定。
_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地址缓存起来。
上面说过_dl_runtime_resolve
会将肯定好的符合地址放到GOT中,那么在须要延迟加载的状况下,GOT里存放什么地址?上面说过须要咱们须要将肯定好的符号地址缓存起来,那么ELF是如何经过PLT与GOT的配合作到延迟加载的?咱们直接看一个真实的例子就行:
#include <stdio.h> int main(){ printf("Hello World"); printf("Hello World Again"); return 0; }
gdb调试一下:
第一次调用printf,会调用printf对应的plt代码片断,与上面咱们本身分析实现延迟加载的步骤同样:
(gdb) ni 0x000000000040054e in main () => 0x000000000040054e <main+14>: e8 71 fe ff ff callq 0x4003c4 <printf@plt>
进到<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函数里了。
(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>
说明这个是什么地址??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地址了。
(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的虚拟地址。
下面这张图能够总结上面的五步过程:
(完)
朋友们能够关注下个人公众号,得到最及时的更新: