PLT表和GOT表
PLT 表和 GOT 表
由于 Linux 绑定延迟机制,程序动态编译时会采用 PLT 表和 GOT 表进行辅助
PLT 表
:程序链接表 (Procedure Linkage Table)GOT 表
:全局偏移表 (Global Offset Table)
这两个表是相对应的,PLT 表中的数据就是 GOT 表中的一个地址:
以 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 表地址
若是第一次调用
- 由函数调用跳入到 PLT 表中
- PLT 表跳到 GOT 表中
- 由 GOT 表回跳到 PLT 表中,这时候进行压栈,把代表函数的 ID 压栈
- 接着跳转到公共的 PLT 表项中
- 进入到 GOT 表
- 然后
_dl_runtime_resolve
对动态函数进行地址解析和重定位 - 把动态函数真实的地址写入到 GOT 表项中,然后执行函数并返回
若是第二次调用
- 由函数调用跳入到 PLT 表中
- PLT 表跳入到 GOT 表中,由于这个时候该表项已经是动态函数的真实地址了,所以可以直接执行然后返回,例如:
call scanf() —> scanf() 的 PLT 表 —> scanf() 的 GOT 表
- 当进入带有
@plt
标志的函数时,由函数调用跳入到 PLT 表中 - 在 PLT 表中找到对应的函数的 GOT 表项地址
- 通过 jmp 指令跳转到 GOT 表,得到函数的真实地址
- 跳转到真实函数地址执行
IDA 中的体现
程序段
段名 | 作用 |
---|---|
.got | GOT 全局偏移表。链接器为外部符号填充的实际偏移表(全局偏移表有很多种,不仅仅对应 PLT 表,只有 .got.plt 才是我们这里所探讨的,实际上还有 .got.xxx 等) |
.plt | PLT 程序链接表。它有两个功能,要么在 .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 的函数列表:
发现函数名其实有不同的标记:有高亮的和没有高亮的
- IDA 中没有高亮的函数名,代表二进制可执行文件中的符号表,例如:main、一些自定义的函数
main
所在位置:
- IDA 中有高亮的函数名,代表动态链接库中的函数,例如:write、read
由于 Linux 的绑定延迟机制,程序编译时会采用两种表进行辅助,一个为 PLT 表,一个为 GOT 表
例如:write 函数,可以发现 IDA 中有两个:_write
和 write
_write
所在位置:
write
所在位置:
可以看到调用逻辑为:
.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 地址
- 在 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
结果:
elf.plt['write']
输出的地址为 0x401030,与 IDA 中_write
地址相同,即 .plt 地址elf.got['write']
输出的地址为 0x404018,该地址存放了真正的write()
函数地址,在 IDA 中为write
的 .got.plt 地址elf.symbols['write']
输出的地址为 0x401030,与write()
函数的 PLT 地址相同elf.symbols['main']
输出的地址为 0x401153,为main()
函数的地址,与 IDA 中main()
函数地址相同
由此可见:
elf.plt[]
获取动态链接库中的函数的 .plt 地址elf.got[]
获取动态链接库中的函数的 .got.plt 地址elf.symbols[]
获取程序本身的函数的地址,用于动态链接库中的函数时,获取的是 PLT 地址