转载,原文作者: 载酒

附址:https://github.com/ctfer-zaijiu?tab=stars

仅供个人学习参考

延迟绑定机制+got表plt表

也不必说,鸣蝉在树叶里长吟,肥胖的黄蜂伏在菜花上 🌾

.plt表和.got.plt表

【定义】

got表思想就是设计一个got表用来存放所有全局符号地址,早在静态链接的设计中就有,其中实现上被实现为了以下两个表:

1、.got存放全局变量引用地址

2、.got.plt存放函数引用地址

为了实现延迟绑定机制,才在got表的基础上接入了plt表,其加入有2 个设计目标:

1、实现第一次调用函数时会做重定位,而后第二次之后的所有函数调用就都是直接调用

2、实现传参并调用_dl_runtime_resolve()

这两个表被ld.so也就是动态链接器所维护,主要是:在ld.so里定义了一个_dl_runtime_resolve函数用来进行重定位+符号地址解析,并将结果更新在plt、got表中。

【结构】

plt表存储指令,got表存储指令要用到的 数据。他们都是:可复用的东西放表头,某函数独有的东西做表项。

  • plt表的结构如下:

    • 表头是调用runtime_resorve函数的指令,每个函数在第一次用runtime_resorve函数进行“符号地址解析”时就对该段指令进行复用。

    • 每个表项是以下三条指令:

      1、jmp,用来跳到函数处,使用的地址存在got表表项。如果成功就直接调到函数,如果失败(符号地址未被解析)就跳到接下来的指令“2”“3”

      2、push,用来给runtime_resorve函数传参,数据用立即数

      3、jmp,跳回plt表头

  • got表的结构如下:

    • 表头是三个特殊地址,是每个函数进行重定位时可以复用的调用runtime_resolve函数的数据,其中:
      • got[0]是.dymanic段的基址
      • got[1]是一个指向linkmap结构体实现的指针,该结构体实现位于栈上
      • got[2]是_dl_runtime_resolve函数的地址
    • 每个表项都是一个地址指针,被重定位前指向plt表的对应字段的第二句指令开始处,被重定位后指向对应函数地址处

总体结构如下图↓:

Untitled

【位置】

这两个表是每个程序私有的,而不是可以多个程序之间共享的。

也就是说,在不开ASLR的情况下,每一次运行elf程序时got表、plt表的基址的后12位是一样的!

“got”表基址通常存放在特定的寄存器中,以便在程序执行时快速访问。具体来说,常见的做法是将”got”表基址存放在全局偏移表寄存器(Global Offset Table Register,通常简称为GOT寄存器)中。

GOT寄存器的命名和具体寄存器编号会根据不同的体系结构和操作系统而有所不同。以下是一些常见架构和操作系统中使用的GOT寄存器示例:

x86架构(32位):

  • 在x86架构的Linux系统中,GOT寄存器通常是EBX(32位寄存器)。

x86架构(64位):

  • 在x86架构的Linux系统中,GOT寄存器通常是R12(64位寄存器)。

ARM架构:

  • 在ARM架构中,GOT寄存器通常是R9(ARMv7架构)或X19(ARMv8架构)。

MIPS架构:

  • 在MIPS架构中,GOT寄存器通常是GP(全局指针寄存器)。
    需要注意的是,具体的寄存器使用可能会受到编译器、操作系统和编译选项的影响。因此,对于特定的平台和环境,可能会有一些变化。

【相关操作】

可以通过pwntools的以下指令来获得这两个表中某个函数的地址:

1
2
3
4
elf = ELF("./pwntest")

read_plt = elf.plt['read']
read_got = elf.got['read']

可以通过objdump来查看一个elf的got表plt表中共有哪些函数:

1
2
3
4
#查看文件的got表
$ objdump -R pwn3
#查看文件的plt表
$ objdump -dj .plt migration

延迟绑定机制

【定义】

又叫lazybinding,其通过got表和plt表的联动,实现:

第一次访问plt表时会执行run_time_resolve,第二次及以后访问plt表时就会自动跳转到函数代码,如图↓:

Untitled

当函数第一次用到时才进行重定位获取位置,这样会大大提高程序启动速度

【实现过程】

第一次执行该对该func的call时的指令执行流程:

执行完dl_runtime_resolve函数完成重定位后会返回call语句的前一句语句位置,然后再执行第2次对该func的call

执行完dl_runtime_resolve函数完成重定位后会返回call语句的前一句语句位置,然后再执行第2次对该func的call

重定位完成后,第2、3、4…次执行对该func的call时的指令执行流程:

Untitled

不使用延迟绑定

1、在一些情况下,一个 GCC 编译的程序可能不会有 PLT 表。这通常是因为编译器或链接器在某些情况下会进行优化,例如,如果程序中没有对动态链接库函数的显式调用或者只有很少的动态链接库函数,那么编译器或链接器可能会选择不生成 PLT 表。

2、在linux系统中使用使用FullRelro安全策略时,默认直接装载时全部重定位,此时也不会有plt表而只会有got表

3、弱符号引用:当程序使用弱符号引用时,通常也不会使用延迟绑定。弱符号是一种特殊类型的符号引用,(举个栗子:在c/c++语言中,编译器默认函数和已经初始化的全局变量为强符号,未初始化的全局变量为弱符号)其绑定是可选的(即弱引用),如果找不到符号的定义,程序可以继续执行(默认值为0或别的特殊值)。在这种情况下,通常会在程序启动时立即解析和绑定弱符号引用,以便在需要时进行正确的调用或访问。