堆棧溢出通常是所有的緩沖區溢出中最容易進行利用的。了解堆棧溢出之前,先了解以下幾個概念:
緩沖區
簡單說來是一塊連續的計算機記憶體區域,可以儲存相同資料類型的多個執行個體。
堆棧
堆 棧是一個在計算機科學中經常使用的抽象資料類型。堆棧中的物體具有一個特性:最後一個放入堆棧中的物體總是被最先拿出來,這個特性通常稱為後進先出 (LIFO)隊列。堆棧中定義了一些操作。兩個最重要的是PUSH和POP。PUSH操作在堆棧的頂部加入一個元素。POP操作相反,在堆棧頂部移去一個 元素,并将堆棧的大小減一。
寄存器ESP、EBP、EIP
CPU的ESP寄存器存放目前線程的棧頂指針,
EBP寄存器中儲存目前線程的棧底指針。
CPU的EIP寄存器存放下一個CPU指令存放的記憶體位址,當CPU執行完目前的指令後,從EIP寄存器中讀取下一條指令的記憶體位址,然後繼續執行。
現 代計算機被設計成能夠了解人們頭腦中的進階語言。在使用進階語言構造程式時最重要的技術是過程(procedure)和函數(function)。從這一 點來看,一個過程調用可以象跳轉(jump)指令那樣改變程式的控制流程,但是與跳轉不同的是,當工作完成時,函數把控制權傳回給調用之後的語句或指令。 這種進階抽象實作起來要靠堆棧的幫助。堆棧也用于給函數中使用的局部變量動态配置設定空間,同樣給函數傳遞參數和函數傳回值也要用到堆棧。
堆棧由邏輯堆棧幀組成。當調用函數時邏輯堆棧幀被壓入棧中,當函數傳回時邏輯堆棧幀被從棧中彈出。堆棧幀包括函數的參數,函數地局部變量,以及恢複前一個堆棧幀所需要的資料,其中包括在函數調用時指令指針(IP)的值。
當一個例程被調用時所必須做的第一件事是儲存前一個 FP(這樣當例程退出時就可以恢複)。然後它把SP複制到FP,建立新的FP,把SP向前移動為局部變量保留白間。這稱為例程的序幕(prolog)工 作。當例程退出時,堆棧必須被清除幹淨,這稱為例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作。
下面我們用一個簡單的例子來展示堆棧的模樣: example1.c:
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
為了了解程式在調用function()時都做了哪些事情, 我們使用gcc的-S選項編譯, 以産生彙編代碼輸出:
$ gcc -S -o example1.s example1.c
通過檢視彙編語言輸出, 我們看到對function()的調用被翻譯成:
pushl $3
pushl $2
pushl $1
call function
以從後往前的順序将function的三個參數壓入棧中, 然後調用function(). 指令call會把指令指針(IP)也壓入棧中. 我們把這被儲存的IP稱為傳回位址(RET). 在函數中所做的第一件事情是例程的序幕工作:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
将幀指針EBP壓入棧中. 然後把目前的SP複制到EBP, 使其成為新的幀指針. 我們把這個被儲存的FP叫做SFP. 接下來将SP的值減小, 為局部變量保留白間. 我 們必須牢記:記憶體隻能以字為機關尋址. 在這裡一個字是4個位元組, 32位. 是以5位元組的緩沖區會占用8個位元組(2個字)的記憶體空間, 而10個位元組的緩沖區會占用12個位元組(3個字)的記憶體空間. 這就是為什麼SP要減掉20的原因. 這樣我們就可以想象function()被調用時堆棧的模樣:
是以,從上圖來看,假如我們輸入的buffer1超長了,直接覆寫掉後面的sfp和ret,就可以修改該函數的傳回位址了。下面來看一個示例吧。
關于如何編寫Shell Code,如何在記憶體中預先準備好一段危險的執行代碼以及如何精确計算通過緩沖區溢出執行那段危險代碼同時又讓傳回位址調回原來傳回位址……這中間涉及太 多的底層彙編知識,小弟不才也隻是走馬觀花,成不了真正的黑客高手。但從黑客朋友的水準之高看來,提高我們的代碼安全性是非常必要的!
是以,在這個例子中,我們假設所謂的危險代碼已經在 源代碼中,即函數bar。函數foo是正常的函數,在main函數中被調用,執行了一段非常不安全的strcpy工作。利用不安全的strcpy,我們可 以傳入一個超過緩沖區buf長度的字元串,執行拷貝後,緩沖區溢出,把ret傳回位址修改成函數bar的位址,達到調用函數bar的目的。
#include <stdio.h>
#include <string.h>
void foo(const char* input)
{
char buf[10];
printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
strcpy(buf, input);
printf("buf = %s\n", buf);
printf("Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
void bar(void)
printf("Augh! I've been hacked!\n");
int main(int argc, char* argv[])
printf("Address of foo = %p\n", foo);
printf("Address of bar = %p\n", bar);
if (argc != 2)
{
printf("Please supply a string as an argument!\n");
return -1;
}
foo(argv[1]);
printf("Exit!\n");
return 0;
用GCC編譯上面的程式,同時注意關閉Buffer Overflow Protect開關:
gcc -g -fno-stack-protector test.c -o test
為了找出傳回位址,我用gdb調試上面編譯出來的程式。
//(前面啟動gdb,設定參數和斷點的步驟省略……)
(gdb) r
Starting program: /media/Personal/MyProject/C/StackOver/test abc
Address of foo = 0x80483d4 //函數foo的位址
Address of bar = 0x8048419 //函數bar的位址
Breakpoint 1, main (argc=2, argv=0xbfe5ab24) at test.c:24
24 foo(argv[1]);
//在調用foo函數前,我們檢視ebp值
(gdb) info registers ebp
ebp 0xbfe5aa88 0xbfe5aa88 //ebp值為0xbfe5aa88
(gdb) n
Breakpoint 2, foo (input=0xbfe5c652 "abc") at test.c:4
4 {
6 printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
//執行到foo後,我們再檢視ebp值
ebp 0xbfe5aa68 0xbfe5aa68 //ebp值變成了0xbfe5aa68
//我們來檢視一下位址0xbfe5aa68究竟是啥東東:
(gdb) x/ 0xbfe5aa68
0xbfe5aa68: 0xbfe5aa88 //原來位址0xbfe5aa68存放的居然是我們之前的ebp值,其實豁然開朗了,因為這是執行了push %ebp後将之前的ebp儲存起來了,和前面說的居然是一樣的!
My stack looks like:
0xb7ee04e0
0x8048616
0xbfe5aa74
0xb7edfff4
0xbfe5aa88 //看,在代碼中輸入堆棧資訊中也出現了熟悉的0xbfe5aa88,是以可以斷定該處為儲存的上一級的ebp值。對應上上面那個圖中的sfp。
0x8048499 //假如0xbfe5aa88就是sfp的話,那0x8048499應該就是ret(傳回位址)了,下面來驗證一下
7 strcpy(buf, input);
//檢視0x8048499裡面是什麼東東
(gdb) x/i 0x8048499
0x8048499 <main+108>: movl $0x8048653,(%esp) //這句代碼是main函數中的代碼,正是我們執行完foo函數後的下一個位址。不信,看看main的assemble:
(gdb) disassemble main
Dump of assembler code for function main:
0x0804842d <main+0>: lea 0x4(%esp),%ecx
0x08048431 <main+4>: and $0xfffffff0,%esp
0x08048434 <main+7>: pushl -0x4(%ecx)
0x08048437 <main+10>: push %ebp
//(中間省略……)
0x08048494 <main+103>: call 0x80483d4 <foo>
0x08048499 <main+108>: movl $0x8048653,(%esp) //就是這裡了!哈
0x080484a0 <main+115>: call 0x8048340 <puts@plt>
是以,我們隻要輸入一個超長的字元串,覆寫掉0x08048499,變成bar的函數位址0x8048419,就達到了調用bar函數的目的。為了将0x8048419這樣的東西輸入到應用程式,我們需要借助于Perl或Python腳本,如下面的Python腳本:
import os
arg = 'ABCDEFGHIJKLMN' + '"x19"x84"x04"x08'
cmd = './test ' + arg
os.system(cmd)
注意上面的08 04 84 19要兩個兩個反着寫。執行一下:
$python hack.py
Address of foo = 0x80483d4
Address of bar = 0x8048419 //bar的函數位址
0xb7fc24e0
0xbf832484
0xb7fc1ff4
0xbf832498
0x8048499 //strcpy前函數傳回位址0x8048499
buf = ABCDEFGHIJKLMN�
Now the stack looks like:
0xbf83246e
0x42412484
0x46454443
0x4a494847
0x4e4d4c4b
0x8048419 //瞧,傳回位址被修改為了我們想要的bar的函數位址0x8048419
Augh! I've been hacked! //哈哈!bar函數果然被執行了!
堆是記憶體的一個區域,它 被應用程式利用并在運作時被動态配置設定。堆記憶體與堆棧記憶體的不同在于它在函數之間更持久穩固。這意味着配置設定給一個函數的記憶體會持續保持配置設定直到完全被釋放為 止。這說明一個堆溢出可能發生了但卻沒被注意到,直到該記憶體段在後面被使用。這裡隻是簡單了解一下,下面看一個最簡單的堆溢出例子:
/*heap1.c – 最簡單的堆溢出*/
#include <stdlib.h>
int main(int argc, char *argv[])
char *input = malloc(20);
char *output = malloc(20);
strcpy(output, "normal output");
strcpy(input, argv[1]);
printf("input at %p: %s\n", input, input);
printf("output at %p: %s\n", output, output);
printf("\n\n%s\n", output);
我們來看執行結果:
[root@localhost]# ./heap1 hackshacksuselessdata
input at 0x8049728: hackshacksuselessdata
output at 0x8049740: normal output
normal output
[root@localhost]# ./heap1 hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks
input at 0x8049728: hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks
output at 0x8049740: hackshackshackshacks5hacks6hacks7
hackshacks5hackshacks6hackshacks7
[root@localhost]# ./heap1 "hackshacks1hackshacks2hackshacks3hackshacks4what have I done?"
input at 0x8049728: hackshacks1hackshacks2hackshacks3hackshacks4what have I done?
output at 0x8049740: what have I done? //我們看到,output變成了what have I done?
what have I done?
[root@localhost]#
我們來看看是如何溢出的:
這類錯誤是指使用printf,sprintf,fprint等函數時,沒有使用格式化字元串,比如:正确用法是:
printf("%s", input)
如果直接寫成:
printf(input)
将會出現漏洞,當input輸入一些非法制造的字元時,記憶體将有可能被改寫,執行一些非法指令。
我們經常碰到需要在Unicode和ANSI之間互相轉換,絕大多數Unicode函數按照寬字元格式(雙位元組)大小,而不是按照位元組大小來計算緩沖區大小,是以,轉換的時候不注意的話就可能會造成溢出。比如最常受到攻擊的函數是MultiByteToWideChar,看下面的代碼:
BOOL GetName(char *szName)
WCHAR wszUserName[256];
// Convert ANSI name to Unicode.
MultiByteToWideChar(CP_ACP, 0,
szName,
-1,
wszUserName,
sizeof(wszUserName)); //問題出在這個參數上,sizeof(wszUserName)将會等于2*256=512個位元組
wszUserName是寬字元的,是以,sizeof(wszUserName)将會是256*2個位元組,是以存在潛在的緩沖區溢出問題。正确的寫法應該是這樣的:
MultiByteToWideChar(CP_ACP, 0,
szName,
-1,
wszUserName,
sizeof(wszUserName) / sizeof(wszUserName[0]));
曾真實出現的Internet列印協定緩沖區溢出就是由于此類問題導緻的。
避免使用不安全的字元串處理函數,比如使用安全的函數代替:
不安全的函數
安全函數
strcpy
strncpy
strcat
strncat
sprintf
_snprintf
gets
fgets
/GS選項能夠阻止堆棧的破壞,保證堆棧的完整性,但是不能完全防止緩沖區溢出問題,比如,對于堆溢出,/GS是無能為力的。
最簡單的源代碼掃描:
grep strcpy *.c
然後就是一些開源的或是商業的源代碼掃描工具了。
源代碼工具包含ApplicationDefense、SPLINT、ITS4和Flawfinder。
二進制工具包含各種fuzzing工具包和靜态分析程式,例如Bugscan。
Michael Howard, David LeBlanc. "Writing Secure Code"
Mike Andrews, James A. Whittaker "How to Break Web Software"
<a href="http://book.csdn.net/bookfiles/228/index.html">http://book.csdn.net/bookfiles/228/index.html</a>
緩沖區溢出的原理和實踐(Phrack)
本文轉自CoderZh部落格園部落格,原文連結:http://www.cnblogs.com/coderzh/archive/2008/09/06/1285693.html,如需轉載請自行聯系原作者