什么是 PLT/GOT?

逆向工程 x86 二元分析 小精灵 amd64
2021-06-23 00:07:56

有时,在反汇编 x86 二进制文件时,我会偶然发现对PLT和 的引用GOT,尤其是在从动态库调用过程时。

例如,在 中运行程序时gdb

(gdb) info file
Symbols from "/home/user/hello".
Local exec file: `/home/user/hello', file type elf64-x86-64.
Entry point: 0x400400
    0x0000000000400200 - 0x000000000040021c is .interp
    0x000000000040021c - 0x000000000040023c is .note.ABI-tag
    0x000000000040023c - 0x0000000000400260 is .note.gnu.build-id
    0x0000000000400260 - 0x0000000000400284 is .hash
    0x0000000000400288 - 0x00000000004002a4 is .gnu.hash
    0x00000000004002a8 - 0x0000000000400308 is .dynsym
    0x0000000000400308 - 0x0000000000400345 is .dynstr
    0x0000000000400346 - 0x000000000040034e is .gnu.version
    0x0000000000400350 - 0x0000000000400370 is .gnu.version_r
    0x0000000000400370 - 0x0000000000400388 is .rela.dyn
    0x0000000000400388 - 0x00000000004003b8 is .rela.plt
    0x00000000004003b8 - 0x00000000004003c6 is .init
 => 0x00000000004003d0 - 0x0000000000400400 is .plt
    0x0000000000400400 - 0x00000000004005dc is .text
    0x00000000004005dc - 0x00000000004005e5 is .fini
    0x00000000004005e8 - 0x00000000004005fa is .rodata
    0x00000000004005fc - 0x0000000000400630 is .eh_frame_hdr
    0x0000000000400630 - 0x00000000004006f4 is .eh_frame
    0x00000000006006f8 - 0x0000000000600700 is .init_array
    0x0000000000600700 - 0x0000000000600708 is .fini_array
    0x0000000000600708 - 0x0000000000600710 is .jcr
    0x0000000000600710 - 0x00000000006008f0 is .dynamic
 => 0x00000000006008f0 - 0x00000000006008f8 is .got
 => 0x00000000006008f8 - 0x0000000000600920 is .got.plt
    0x0000000000600920 - 0x0000000000600930 is .data
    0x0000000000600930 - 0x0000000000600938 is .bss

然后,当反汇编 ( puts@plt) 时:

(gdb) disas foo
Dump of assembler code for function foo:
   0x000000000040050c <+0>: push   %rbp
   0x000000000040050d <+1>: mov    %rsp,%rbp
   0x0000000000400510 <+4>: sub    $0x10,%rsp
   0x0000000000400514 <+8>: mov    %edi,-0x4(%rbp)
   0x0000000000400517 <+11>:    mov    $0x4005ec,%edi
=> 0x000000000040051c <+16>:    callq  0x4003e0 <puts@plt>
   0x0000000000400521 <+21>:    leaveq
   0x0000000000400522 <+22>:    retq
End of assembler dump.

那么,这些 GOT/PLT 是什么?

2个回答

PLT 代表过程链接表,简单地说,用于调用在链接时地址未知的外部过程/函数,并在运行时由动态链接器解析。

GOT 代表全局偏移表,类似地用于解析地址。既PLT和GOT和其他重定位信息被在更大的长度中说明本文

此外,GOLD的作者 Ian Lance Taylor在他的博客上发表了一篇非常值得一读的文章系列(二十部分!):这里的入口点“链接器第 1 部分”

让我总结一下https://reverseengineering.stackexchange.com/a/1993/12321 上给出的链接,暂时不进行认真的反汇编分析。

当 Linux 内核 + 动态链接器要运行带有 的二进制文件时exec,它通常只是在链接期间将 ELF 部分转储到链接器指定的已知内存位置。

因此,每当您编码时:

  • 在您的代码中引用了一个全局变量
  • 从代码内部调用函数

编译器 + 链接器可以将地址硬编码到程序集中,一切都会正常工作。

但是,在处理共享库时,我们如何做到这一点,因为每次都必须在可能不同的地址加载这些共享库,以避免两个共享库之间发生冲突?

天真的解决方案是将重定位元数据保留在最终的可执行文件上,就像实际的链接器所做的那样,每当程序加载时,让动态链接器检查每一次访问并用正确的地址修补它。

但是,这太费时了,因为在一个程序上可能有很多要修补的引用,然后该程序将需要很长时间才能开始运行。

与往常一样,解决方案是添加另一层间接:GOT 和 PLT,它们是编译系统 + 动态链接器设置的两个额外的内存块。

程序启动后,动态链接器会检查共享库的地址,并修改 GOT 和 PLT,使其正确指向所需的共享库符号:

  • 每当您的程序访问共享库的全局变量时,编译器 + 链接器都会发出两次内存访问:

    mov    0x200271(%rip),%rax        # 200828 <_DYNAMIC+0x1a0>
    mov    (%rax),%eax
    

    第一个将动态链接器先前设置的 GOT 中变量的真实地址加载到rax.

    第二次间接访问实际上是通过 from 的地址间接访问变量rax

  • 对于代码,事情有点复杂。

    每当调用共享库中的函数时,链接器都会让我们跳转到 PLT 中的地址。

    第一次调用函数时,PLT 代码使用存储在 GOT 中的偏移量来决定函数的实际最终位置,然后:

    • 存储这个预先计算的值
    • 跳到那里

    下次调用该函数时,已经计算了该值,因此它直接跳转到那里。

    由于这种懒惰的解析机制:

    • 即使共享库有很多符号,程序也可以快速开始运行
    • 我们可以通过使用LD_PRELOAD变量来动态替换函数

如今,位置无关可执行文件 (PIE)是 Ubuntu 18.04 等发行版的默认设置。

与共享库非常相似,这些可执行文件经过编译,以便在执行时可以将它们放置在内存中的随机位置,以便使某些漏洞更难被利用。

因此,在这种情况下不可能再对绝对函数和变量地址进行硬编码。可执行文件必须:

  • 用户指令指针相对寻址,如果它们在汇编语言上可用,例如:
    • ARMv8:
      • B做 26 位跳转,B.cond19 位
      • “LDR(文字)”执行 19 位加载
      • ADR 计算其他指令可以使用的 21 位相对地址
  • 否则使用 GOT / PLT