GOT表和PLT表知识详解

无尽防御-CTF 专栏收录该内容
10 篇文章 1 订阅

GOT 表和 PLT 表在程序中的作用非常巨大,接下来的讲解希望大家可以仔细看看

我们用一个非常简单的例子来讲解,代码如下:
图1

这里写图片描述


然后我们编译

我们直接 gdb./a.out 来进行反编译处理,然后通过 disasmain 查看 main 函数中的反编译代码如下:

图3

这里写图片描述


我们可以观察到 gets@plt puts@plt 这两个函数,为什么后面加了个 @plt ,因为这个为 PLT 表中的数据的地址。那为什么反编译中的代码地址为 PLT 表中的地址呢。

原因

为了更好的用户体验和内存 CPU 的利用率,程序编译时会采用两种表进行辅助,一个为 PLT 表,一个为 GOT 表, PLT 表可以称为内部函数表, GOT 表为全局函数表(也可以说是动态函数表这是个人自称),这两个表是相对应的,什么叫做相对应呢, PLT 表中的数据就是 GOT 表中的一个地址,可以理解为一定是一一对应的,如下图:

4

这里写图片描述


PLT表中的每一项的数据内容都是对应的 GOT 表中一项的地址这个是固定不变的,到这里大家也知道了 PLT 表中的数据根本不是函数的真实地址,而是 GOT 表项的地址,好坑啊。

其实在大家进入带有 @plt 标志的函数时,这个函数其实就是个过渡作用,因为 GOT 表项中的数据才是函数最终的地址,而 PLT 表中的数据又是 GOT 表项的地址,我们就可以通过 PLT 表跳转到 GOT 表来得到函数真正的地址。

那问题来了,这个 @plt 函数时怎么来的,这个函数是编译系统自己加的,大家可以通过disas gets看看里面的代码,如下图:

5

这里写图片描述


大家可以发现,这个函数只有三行代码,第一行跳转,第二行压栈,第三行又是跳转,解释:
第一行跳转,它的作用是通过PLT表跳转到 GOT 表,而在第一次运行某一个函数之前,这个函数 PLT 表对应的 GOT 表中的数据为 @plt 函数中第二行指令的地址,针对图中来说步骤如下:

  1. jmp 指令跳转到 GOT
  2. GOT 表中的数据为 0x400486
  3. 跳转到指令地址为 0x400486
  4. 执行push 0x3#这个为在 GOT 中的下标序号
  5. 在执行jmp 0x400440
  6. 0x400440 PLT[0] 的地址
  7. PLT[0] 的指令会进入动态链接器的入口
  8. 执行一个函数将真正的函数地址覆盖到 GOT 表中

这里我们要提几个问题:
1. PLT[0] 处到底做了什么,按照我们之前的思路它不是应该跳转到 GOT[0] 吗?
2. 为什么中间要进行 push 压栈操作?
3. 压入的序号为什么为 0x3 ,不是最开始应该为 0x0 吗?

解决问题

问题1

看下图:
6

这里写图片描述


我们尝试着查看0x400440地址的数据内容发现一个问题,从 0x4004400x400450 之间的数据完全不知道是什么,而真正的 PLT[x] 中的数据是从 0x400450 开始的,从这里才有了 @plt 为后缀的地址,但是我们disas gets看代码的时候是从 0x400440 开始的,我们可以通过x /5i 0x400440查看 0x400440 处的代码,如下:
7

这里写图片描述


我们看到了后面的#之后又一个16进制数,一看便可以知道是 GOT 表的地址,为什么这么肯定呢,因为我们可以通过objdump -R ./a.out查看一个程序的 GOT 函数的地址,如下图:
8

这里写图片描述


这里都是些GOT地址,我们发现都是 0x601... 这些,所以可以断定图 7 中的也是GOT地址,那么我们可以猜想出,在正式存储一个函数的 GOT 地址前,我们的 PLT 表前面有一项进行一些处理,我们暂且不具体深入剖析这些代码有什么用,但是我们可以肯定 puts@plt 前面那 16 个字节也算是 PLT 表中的内容,这其实就是我们的 PLT[0] ,正如我们之前问题提到的那样,我们的 PLT[0] 根本没有跳转到 GOT[0] ,它不像我们的 PLT[1] 这些存储的是 GOT 表项的地址,它是一些代码指令,换句话说, PLT[0] 是一个函数,这个函数的作用是通过 GOT[1] GOT[2] 来正确绑定一个函数的正式地址到 GOT 表中来。

咦,这里问题好像又产生了,本来按照最开始的思路 PLT[1] 也是跳转到 GOT[1] 的, GOT[2] 同理,但是这两个数据好像被 PLT[0] 利用了,同时 GOT[0] 好像消失了,这里 GOT[0] 暂且不说它的作用是什么,针对 GOT[1] GOT[2] PLT[0] 利用,所以我们程序中真实情况其实是从 PLT[1] GOT[3] , PLT[2] GOT[4] ,所以我们推翻了我们的图 4 ,建立一张新的处理表

9

这里写图片描述


plt[0] 代码做的事情则是:由于 GOT[2] 中存储的是动态链接器的入口地址,所以通过 GOT[1] 中的数据作为参数,跳转到 GOT[2] 所对应的函数入口地址,这个动态链接器会将一个函数的真正地址绑定到相应的 GOT[x] 中。

这就是 PLT 表和 GOT 表,总而言之,我们调用一个函数的时候有两种方法,一个是通过 PLT 表调用,一个则是通过 GOT 表调用,因为 PLT 表最终也是跳转 GOT 表, GOT 表中则是一个函数真正的地址,这里需要注意的是,在一个函数没有运行一次之前, GOT 表中的数据为 @plt 函数中下一条指令的地址,图 5 有说。

问题2

中间进行的压栈是为了确定PLT对应的 GOT 表项,即是 PLT[1]>GOT[3] 0x3 就是 GOT 的下标 3 ,也就是说压栈后我们跳转到PLT[0],接着 PLT[0] 中的指令会通过这次压栈的序号来确定操作的 GOT 表项为多少

问题3

好像都在第一个问题都已经解决了,这里压入 0x3 的原因是因为,我们的 GOT[0] , GOT[1] , GOT[2] 都有额外用处。要从 GOT[3] 开始

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值