PLT 表和 GOT 表

由于 Linux 绑定延迟机制,程序动态编译时会采用 PLT 表和 GOT 表进行辅助

PLT 表程序链接表 (Procedure Linkage Table)
GOT 表全局偏移表 (Global Offset Table)

这两个表是相对应的,PLT 表中的数据就是 GOT 表中的一个地址:

CTF - Pwn_PLT表 & GOT表1.png

read() 函数为例:

  • plt['read'] -> got['read'].address
  • got['read'] -> read.address

由此可知:

  • 当使用指令 call [rbp] 时,rbp 存储的应该是 got['read']
  • 而当使用指令 call rbp 时,rbp 储存的应该是 plt['read']

PLT 表其实是一个过渡的作用,PLT 表中只存放 GOT 表项的地址,而不是函数真实的地址,函数真实的地址存放在 GOT 表中


地址的调用流程

scanf() 函数为例,当 main() 函数开始,会请求 PLT 表中这个函数对应的 GOT 表地址

  • 若是第一次调用

    1. 由函数调用跳入到 PLT 表中
    2. PLT 表跳到 GOT 表中
    3. 由 GOT 表回跳到 PLT 表中,这时候进行压栈,把代表函数的 ID 压栈
    4. 接着跳转到公共的 PLT 表项中
    5. 进入到 GOT 表
    6. 然后 _dl_runtime_resolve 对动态函数进行地址解析和重定位
    7. 把动态函数真实的地址写入到 GOT 表项中,然后执行函数并返回
  • 若是第二次调用

    1. 由函数调用跳入到 PLT 表中
    2. PLT 表跳入到 GOT 表中,由于这个时候该表项已经是动态函数的真实地址了,所以可以直接执行然后返回,例如:call scanf() —> scanf() 的 PLT 表 —> scanf() 的 GOT 表
    3. 当进入带有 @plt 标志的函数时,由函数调用跳入到 PLT 表中
    4. 在 PLT 表中找到对应的函数的 GOT 表项地址
    5. 通过 jmp 指令跳转到 GOT 表,得到函数的真实地址
    6. 跳转到真实函数地址执行

CTF - Pwn_PLT表 & GOT表2.png


IDA 中的体现

程序段

段名作用
.gotGOT 全局偏移表。链接器为外部符号填充的实际偏移表(全局偏移表有很多种,不仅仅对应 PLT 表,只有 .got.plt 才是我们这里所探讨的,实际上还有 .got.xxx 等)
.pltPLT 程序链接表。它有两个功能,要么在 .got.plt 节中拿到地址,并跳转。要么当 .got.plt 没有所需地址的时,触发链接器去找到所需地址
.got.plt这个是 GOT 专门为 PLT 准备的。也就是说 .got.plt 中的值是 GOT 的一部分。它包含上述 PLT 表所需地址(已经找到的和需要去触发的)
.plt.got

汇编代码

mov edi, offset unk_4006E4
mov eax, 0
call __isoc99_scanf
mov rax, [rbp + var_18]
mov rsi, rax
mov edi, offset format     ; "%p\n"
mov eax, 0
call _printf
mov eax, 0
mov rdx, [rbp + var_8]
xor rdx, fs : 28h
jz short locret_40065A

这里 call _printf 并不是跳转到了实际的 _printf 函数的位置,因为在编译时程序并不能确定 printf 函数的地址

这个 call 指令实际上是相对跳转,跳转到了 PLT 表中的 _printf 项,然后再根据 PLT 表中的地址跳转到 GOT 表,才能获取到实际的 _printf 函数地址,进而执行 printf 函数


IDA 函数名和 pwntools

以蒸米 Level 5 为例:hitcon-level5

IDA 的函数列表:
CTF - Pwn_PLT表 & GOT表3.png

发现函数名其实有不同的标记:有高亮的和没有高亮的

  1. IDA 中没有高亮的函数名,代表二进制可执行文件中的符号表,例如:main、一些自定义的函数

main 所在位置:

CTF - Pwn_PLT表 & GOT表8.png

  1. IDA 中有高亮的函数名,代表动态链接库中的函数,例如:write、read

由于 Linux 的绑定延迟机制,程序编译时会采用两种表进行辅助,一个为 PLT 表,一个为 GOT 表
例如:write 函数,可以发现 IDA 中有两个:_writewrite

_write 所在位置:

CTF - Pwn_PLT表 & GOT表5.png

CTF - Pwn_PLT表 & GOT表7.png

write 所在位置:

CTF - Pwn_PLT表 & GOT表4.png

CTF - Pwn_PLT表 & GOT表6.png

可以看到调用逻辑为:

.plt:0000000000401030 FF 25 E2 2F 00 00             jmp     cs:off_404018

.got.plt:0000000000404018 48 40 40 00 00 00 00 00       off_404018 dq offset write

extern:0000000000404048 00 00 00 00 00 00 00 00       extrn write:near

即:首先调用 _write 跳转到 cs:off_404018 处,然后在 cs:off_404018 处存放的是 offset write;而 offset write 处为 extrn write:near,调用 lib 动态链接库中的 write() 函数

因此,offset write 存放了真正的 write 函数地址

extrn write:near 声明了一个外部符号 write,表示它是在其他模块或文件中定义的,它是一个近地址的符号,可能是一个函数或变量

通常,这种声明用于告诉汇编器和链接器在连接时需要在其他地方找到 write 的定义。这种外部符号声明允许在当前模块中使用 write,而不必提供它的具体定义

也就是说:在 IDA 的函数列表中,如果是动态链接库中的函数 (函数名带高亮),_write 指的是 PLT 地址,write 指的是 GOT 地址

  1. 在 Pwntools 中验证:
elf = ELF("./level5")

print("plt write: ", hex(elf.plt['write']))
print("got write: ", hex(elf.got['write']))
print("symbols write: ", hex(elf.symbols['write']))
print()

print("symbols main: ", hex(elf.symbols['main']))


# plt write:  0x401030
# got write:  0x404018
# symbols write:  0x401030

# symbols main:  0x401153
  • 结果:

    1. elf.plt['write'] 输出的地址为 0x401030,与 IDA 中 _write 地址相同,即 .plt 地址
    2. elf.got['write'] 输出的地址为 0x404018,该地址存放了真正的 write() 函数地址,在 IDA 中为 write 的 .got.plt 地址
    3. elf.symbols['write'] 输出的地址为 0x401030,与 write() 函数的 PLT 地址相同
    4. elf.symbols['main'] 输出的地址为 0x401153,为 main() 函数的地址,与 IDA 中 main() 函数地址相同
  • 由此可见:

    1. elf.plt[] 获取动态链接库中的函数的 .plt 地址
    2. elf.got[] 获取动态链接库中的函数的 .got.plt 地址
    3. elf.symbols[] 获取程序本身的函数的地址,用于动态链接库中的函数时,获取的是 PLT 地址