You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
我们知道,一般我们的代码都需要引用外部文件的函数或者变量,比如#include<stdio.h>里的printf,但是由于我们代码中用到的共享对象是运行时加载进来的,在虚拟地址空间的位置并不确定,所以代码里call <addr of printf>的addr of printf不确定,只有等运行时共享对象被加载到进程的虚拟地址空间里时,才能最终确定printf的地址,再进行重定位地址。
最近在研究缓冲区溢出攻击的试验,发现其中有一种方法叫做ret2plt。plt?这个词好熟悉,在汇编代码里经常见到,和plt经常一起出现的还有一个叫got的东西,但是对这两个概念一直很模糊,趁着这个机会研究一下。
可以先说一下结论 : plt和got是动态链接中用来重定位的。
GOT
我们知道,一般我们的代码都需要引用外部文件的函数或者变量,比如
#include<stdio.h>
里的printf
,但是由于我们代码中用到的共享对象是运行时加载进来的,在虚拟地址空间的位置并不确定,所以代码里call <addr of printf>
的addr of printf
不确定,只有等运行时共享对象被加载到进程的虚拟地址空间里时,才能最终确定printf的地址,再进行重定位地址。看一个最简单的例子:
用GDB调试一下(关于GDB调试汇编可以参考之前写的GDB 单步调试汇编 ):
可以看出,
call <addr of printf>
被callq 0x4003c4
代替,而这个0x4003c4并不是真正的printf函数的地址。可能有人已经想到了,为什么不能直接在printf函数地址确定后,直接将
call <addr of printf>
修改为call <real addr of printf>
,像静态链接那样呢(静态链接是在链接阶段进行重定位,直接修改的代码段)?有两个原因:所以,我们很容易的想到,既然不能修改代码段,能修改数据段,我们可以在共享对象加载完成后,将真实的符号地址放到数据段中,代码中直接读取数据段内的地址就行,这里开辟的空间就叫做GOT(图有点挫)。
callq *(addr in GOT)
或者movq offset(%rip) %rax
(%rax
就是全局变量的地址,可以用(%rax)
解引用)。但是这样有一个问题,一个动态库可能有成百上千个符号,但是我们引入该动态库可能只会使用其中某几个符号,像上面那种方式就会造成不使用的符号也会进行重定位,造成不必要的效率损失。我们知道,动态链接比静态链接慢1% ~ 5%,其中一个原因就是动态链接需要在运行时查找地址进行重定位。
所以ELF采用了延迟绑定的技术,当函数第一次被用到时才进行绑定。实现方式就是使用plt。
PLT
我们可以先自己独立思考如何实现延迟绑定。
_dl_runtime_resolve()
。_dl_runtime_resolve()
需要寻找的符号,也就是函数参数。可以放到栈中或者寄存器传递。_dl_runtime_resolve()
寻找完符号的特定地址后,放到寄存器上,比如%rax
,供调用者使用。所以初步的实现步骤是:
上面的步骤可以实现通过一段小代码(plt)实现延迟绑定,但是存在一个问题:每一次调用printf的时候都需要走一遍这个步骤,然而printf的地址一旦确定就不会变了,所以我们需要一个缓存机制,将查找好的printf地址缓存起来。
PLT与GOT
上面说过
_dl_runtime_resolve
会将确定好的符合地址放到GOT中,那么在需要延迟加载的情况下,GOT里存放什么地址?上面说过需要我们需要将确定好的符号地址缓存起来,那么ELF是如何通过PLT与GOT的配合做到延迟加载的?我们直接看一个真实的例子就行:gdb调试一下:
One 调用printf的plt
第一次调用printf,会调用printf对应的plt代码片段,与上面我们自己分析实现延迟加载的步骤一样:
Two 调到printf对应的GOT里存储的地址
进到
<printf@plt>
看看:这里跳到了printf对应的GOT里存储的地址。(elf对got做了细分:got存放全局变量引用的地址,got.plt存放函数引用的地址)
看看动态链接在将确定的符号地址放到GOT前,GOT里存放的是什么地址:
有意思的是jmp到了下一条指令的地址。其实这个时候我们已经可以猜出来了:延迟加载之前,got.plt里存放的是下一条指令地址,延迟加载之后,got.plt里存放的就是真实的符号地址,就可以直接jmp到printf函数里了。
Three 将printf对应的标识压到栈中,并跳到plt[0]
Four 在plt[0]中调用_dl_runtime_resolve查找符合真实地址
说明这个是什么地址??0x600910
我们不用管
_dl_runtime_resolve
是怎么处理的,直接看_dl_runtime_resolve
处理完成后printf对应的GOT的值:与之前猜测的一样,printf对应的GOT表项目前已经存放了printf真实的虚拟地址。那么在下次调用时就避免再重定位,直接跳到printf地址了。
Five 第二次调用printf
直接跳到printf的虚拟地址。
下面这张图可以总结上面的五步过程:
朋友们可以关注下我的公众号,获得最及时的更新:
The text was updated successfully, but these errors were encountered: