sir

堆栈基础2-GOT和PLT表

动态链接

在二进制文件(比如object file)中会有一段叫relocations的部分,这部分的内容在链接时候(link time)再进行敲定确切的值,注意链接可以发生在运行前(称为静态链接,toolchain linker),也可发生在运行时(称为动态链接,dynamic linker)。具体relocations部分中的内容就是在讲:“确定X这个符号(symbol)的值,然后把这个值写到二进制文件的Y偏移处”。每一条relocation都有确定的类型(定义与ABI文档中),从而确切地说明每个类型的值到底该如何敲定。

在ELF文件的动态连接机制中,每一个外部定义的符号在全局偏移表 (Global Offset Table,GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table,PLT)中也有相应的条目,且一个PLT 条目对应一个GOT条目。

延迟绑定技术

EFL采用了一种叫做叫做延迟绑定的做法,基本思想就是当函数第一次被用到时才进行绑定(符号查找,重定位等),如果没有用到则不进行绑定。 所有程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接库来负责绑定。

优点: 这样的做法可以大大加快程序的启动速度。 特别有利于一些有大量函数引用和大量模块的程序

PLT

ELF使用PLT(precedure Linkage Table)过程链接表的方法来实现延迟绑定

当我们调用某个外部函数模块的函数时,如果按照通常的做法应该是通过GOT中相应的项进行间接跳转。 PIT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转。 调用函数并不直接通过GOT表跳转, 而是通过一个叫做PLT项的结构来进行跳转。 每个外部函数在PLT中都有一个对应的项, 每条PLT又对应着一条GOT表中的一项。

比如外部函数bar()在PLT中的项地址为bar@plt. 让我们来看看bar@plt的实现

大致流程如下

  1. 为了实现延迟绑定,链接器在初始化阶段没有将bar()的地址填入该项, 而是将上面代码中第二条指令"push n" 的地址填入到bar@GOT中,这样,第一条指令就会跳转到第二条指令。
  2. 第二条指令 push n 将一个数字n压入栈中,这个数字就是bar这个符号引用在重定位表".rel.plt" 中的下标
  3. 接着有是一条push指令将模块的ID压入到堆栈, 然后跳转到_dl_runtime_resolve. 这个实际就是lookup(module,function) 这个函数的调用,先将所需要的符号的下表压入堆栈,再将模块ID压入堆栈,然后调用动态链接器的_dl_runtime_resolve 函数来完成符号解析和重定位工作。
  4. 一旦bar()函数被解析完毕, 当我们再次调用bar@plt时, 第一条jmp指令就能够跳转到真正的bar()函数中。bar函数返回时会根据堆栈里的EIP直接返回到调用者,而不会继续执行第二条push n的指令了

上面就是PLT的基本原理, PLT真正实现要比这个结构稍微复杂一些,ELF 将GOT拆分成了两个表叫做" .got" 和".got.plt" . 其中 ".got" 用来保存全局变量引用的地址, ".got.plt" 用来保存函数引用的地址, 也就是说, 所有对于外部函数的引用全部被分离出来放到了" .got.plt" 中。 另外 ".got.plt" 还有一个特殊的地方就是它的前三项是由特殊意义的,分别是
- 第一项保存的是" .dynamic" 段的地址, 这个段描述了本模块动态链接相关的信息
- 第二项保存的是被模块的ID
- 第三项保存的是_dl_runtime_resolve()的地址

我们来看一个ret2libc3的PLT表
plt段:

这些外部函数都有.got.plt对应的项

我们只需要再.got.plt项寻找xrefs引用,就可以得到gets,puts这样的。

demo: test.c

另外多说一句,我们可以在运行某使用so库的可执行文件时,使用LD_PRELOAD 来修改动态链接时,符号解析的顺序。也就是说,LD_PRELOAD会告诉动态链接器,想找什么符号的话,先从这里找,如果LD_PRELOAD中所提供so库里有foo这个符号,那么动态链接器会首先到这里找foo的地址,而不会去其他so库中去找。

PLT表和GOT表的执行流程

以一道ret2lib的题目为例:

这里setbuf()函数执行了两次,我们来看一下第一次函数调用和第二次函数调用有什么不一样

第一次调用setbuf我们单步进入

程序调用plt段的setbuf@plt,通过IDA我们可以看到setbuf@plt的地址正是这个地址

程序继续运行: push 0x38,jmp 0x8048420 在上面介绍了,这其实就是plt的延迟绑定技术,0x38是setvbuf这个函数的符号引用在重定位表.ret.plt 中的位置

然后程序jmp到了0x8048420执行,这是plt段的起始位置,plt里的每个函数都会跳到这里来执行, 下图更加明了一些

plt[0]处的代码为:

plt[0]再次跳转到got.plot处的未初始化代码。

这段代码的作用就是根据edx的下标,通过调用call 0xf7fe87e0 去获取setvbuf的符号引用的真正的物理地址,并且放到eax中.

最后用ret 0xc 跳转到真正的setvbuf函数中去执行. 最后返回到main函数。

我们继续追踪第二个setvbuf函数,程序执行的完jmp DWORD PTR ds:0x804a028 直接就跳转到.setvbuf函数中执行,说明此事ds:0x804a028已经被patch成setvbuf的物理地址了。通过gdb调试我们很清楚看到了plt的运行机制。

我们接下来回到题目本身,如何解题呢?
因为system的参数是假的,eip跳到system代码处也无法执行成功。

我们通过看汇编知道,system函数是执行了call system@plt指令,即每个调用的函数在plt表中都有符号引用,并且接受两个参数,一个是/bin/sh字符串,一个是返回地址,我们可以通过布置堆栈上的数据从而让eip跳到plt段直接执行system@plt函数,而不是跳到text代码段去执行。

我们可以利用ROPgdget得到真的/bin/sh字符串,返回地址随便填写即可。

exp如下

调用完setvbuf@plt后重新回到main,我们继续看下去,get@plt函数需要读取payload,我们根据exp填入我们的payload来进行调试

填写我们的payload

通过gets覆盖掉main函数的栈帧,ebp=0xffffcf38

程序执行完ret后跳转到如下:

接下来和上面几乎一样

最后程序通过ret 0xc进入到system函数中,执行system('/bin/sh')

但我们为什么要构造payload是这样的:

因为我们正常的调用system函数参数的入栈为:

move参数入栈后,call还会将返回地址eip入栈,栈中的数据如下

而我们可以直接构造栈中的数据,将return_addr 指向system@plt,结果如下:

我们只需要伪造system@plt地址即可,然后填入对应的参数即可,第一个参数是system@plt的返回地址,然后是函数参数。

参考: https://paper.seebug.org/272/

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic_rop/#ret2libc

喜欢 0