PLT表和GOT表

前言

    我们在做pwn题目的时候,经常会用到这两个表,如果对这两个表了解的不是特别深刻的话,会对你的泄露造成一定影响,故我们在这里着重介绍一下这两个表。

    GOT是一个存储外部库函数的表

    PLT则是由代码片段组成的,每个代码片段都跳转到GOT表中的一个具体的函数调用

例子

    我们通过例子来了解一下这两个表会出现在什么地方。

PLT表和GOT表

    我们可以观察到[email protected]这个函数,为什么后面加了个@plt?这个值为PLT表中的数据的地址。那为什么反编译中的代码地址为PLT表中的地址呢?我们带着疑问接下去看。

出处

    能让计算机执行都是二进制文件,是LF可执行文件,就如我们做pwn题的pwn文件都是可执行文件。
ELF可以生成一种特殊的代码——与位置无关的代码(PIC)。用户对gcc使用-fPIC指示GNU编译系统生成PIC代码。它是实现共享库或共享可执行代码的基础。这种代码的特殊性在于它可以加载到内存地址空间的任何地址执行。这也是加载器可以很方便的在进程中动态链接共享库。
PIC的实现运用了一个事实,就是代码段中任何指令和数据段中的任何变量之间的距离都是一个与代码段和数据段的绝对存储器位置无关的常量。因此,编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(global offset table,GOT)。GOT包含每个被这个目标模块引用的全局数据目标的表目。编译器还为GOT中每个表目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个表目,使得它包含正确的绝对地址。PIC代码在代码中实现通过GOT间接的引用每个全局变量,这样,代码中本来简单的数据引用就变得复杂,必须加入得到GOT适当表目内容的指令。对只读数据的引用也根据同样的道理,所以,加上PIC编译成的代码比一般的代码开销大。
如果一个ELF可执行文件需要调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT(procedure linkage table,过程链接表)。这两个节之间的交互可以实现延迟绑定(lazy binging),这种方法将过程地址的绑定推迟到第一次调用该函数。为了实现延迟绑定,GOT的头三条表目是特殊的:GOT[0]包含.dynamic段的地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;GOT[1]包含动态链接器的标识;GOT[2]包含动态链接器的延迟绑定代码的入口点。GOT的其他表目为本模块要引用的一个全局变量或函数的地址。PLT是一个以16字节(32位平台中)表目的数组形式出现的代码序列。其中PLT[0]是一个特殊的表目,它跳转到动态链接器中执行;每个定义在共享库中并被本模块调用的函数在PLT中都有一个表目,从PLT[1]开始.模块对函数的调用会转到相应PLT表目中执行,这些表目由三条指令构成。第一条指令是跳转到相应的GOT存储的地址值中.第二条指令把函数相应的ID压入栈中,第三条指令跳转到PLT[O]中调用动态链接器解析函数地址,并把函数真正地址存入相应的GOT表目中。被调用函数GOT相应表目中存储的最初地址为相应PLT表目中第二条指令的地址值,函数第一次被调用后,GOT表目中的值就为函数的真正地址。因此,第一次调用函数时开销比较大.但是其后的每次调用都只会花费一条指令和一个间接的存储器引用。

结论

    当程序编译时会采用两种表进行辅助,一个为PLT表,一个为GOT表,PLT表可以称为内部函数表,GOT表为全局函数表,这两个表是相对应的。

PLT表和GOT表

    PLT表中的每一项的数据内容都是对应的GOT表中一项的地址这个是固定不变的,PLT表中的数据根本不是函数的真实地址,而是GOT表项的地址。
在进入带有@plt标志的函数时,这个函数其实就是个过渡作用,因为GOT表项中的数据才是函数最终的地址,而PLT表中的数据又是GOT表项的地址,我们就可以通过PLT表跳转到GOT表来得到函数真正的地址。
@plt函数是编译系统自己加的。

PLT表和GOT表

    这个函数只有三行代码:第一行跳转,它的作用是通过PLT表跳转到GOT表,而在第一次运行某一个函数之前,这个函数PLT表对应的GOT表中的数据为@plt函数中第二行指令的地址,针对图中来说步骤如下:
1、jmp指令跳转到GOT表
2、GOT表中的数据为0x400486
3、跳转到指令地址为0x400486
4、执行push 0x3#这个为在GOT中的下标序号
5、在执行jmp 0x400440
6、而0x4004400x400440为PLT[0]的地址
7、PLT[0]的指令会进入动态链接器的入口
8、执行一个函数将真正的函数地址覆盖到GOT表中

函数第一次被调用过程图:

PLT表和GOT表


    第一步由函数调用跳入到PLT表中,然后第二步PLT表跳到GOT表中,可以看到第三步由GOT表回跳到PLT表中,这时候进行压栈,把代表函数的ID压栈,接着第四步跳转到公共的PLT表项中,第5步进入到GOT表中,然后_dl_runtime_resolve对动态函数进行地址解析和重定位,第七步把动态函数真实的地址写入到GOT表项中,然后执行函数并返回。

函数之后被调用过程图:

PLT表和GOT表


    函数之后被调用过程:第一步还是由函数调用跳入到PLT表,但是第二步跳入到GOT表中时,由于这个时候该表项已经是动态函数的真实地址了,所以可以直接执行然后返回。
对于动态函数的调用,第一次要经过地址解析和回写到GOT表项中,第二次直接调用即可。