第一次在WooYun發文章,不知道是否符合眾客官口味,望輕拍。
這篇文章翻譯至我的這篇博客,主要介紹了一種叫做BROP的攻擊,該文章主要介紹原理部分,對該攻擊的重現可以參看我的另外一篇博客。
BROP攻擊基于一篇發表在Oakland 2014的論文Hacking Blind,作者是來自Standford的Andrea Bittau,以下是相關paper和slide的鏈接:
以及BROP的原網站地址:
Blind Return Oriented Programming (BROP) Website
可以說這篇論文是今年看過的最讓我感到興奮的論文(沒有之一),如果要用一個詞來形容它的話,那就只有“不能更帥”才能表達我對它的喜愛程度了!
這篇文章假設讀者已經了解Return-Oriented Programming (ROP) 的基本概念,所以只是介紹BROP的實現原理,如果還不清楚什么是ROP,請先出門左轉,看看Wiki的相關介紹。
BROP的實現真的是讓人感到非常“cool”和“smart”,我希望能夠通過這篇文章把它講清楚。
目標:通過ROP的方法遠程攻擊某個應用程序,劫持該應用程序的控制流。我們可以不需要知道該應用程序的源代碼或者任何二進制代碼,該應用程序可以被現有的一些保護機制如NX, ASLR, PIE, 以及stack canaries等保護,應用程序所在的服務器可以是32位系統或者64位系統。
初看這個目標感覺實現起來特別困難。其實這個攻擊有兩個前提條件的:
由于我們不知道被攻擊程序的內存布局,所以首先要做的事情就是通過某種方法從遠程服務器dump出該程序的內存到本地,為了做到這點我們需要調用一個系統調用write
,傳入一個socket文件描述符,如下所示:
write(int sock, void *buf, int len)
將這條系統調用轉換成4條匯編指令,如圖所示:
所以從ROP攻擊的角度來看,我們只需要找到四個相應的gadget,然后在棧上構造好這4個gadget的內存地址,依次進行順序調用就可以了。
但是問題是我們現在連內存分布都不知道,該如何在內存中找到這4個gadgets呢?特別是當系統部署了ASLR和stack canaries等保護機制,似乎這件事就更難了。
所以我們先將這個問題放一放,在腦袋里記著這個目標,先來做一些準備工作。
如果不知道什么是stack canaries
可以先看這里,簡單來說就是在棧上的return address
下面放一個隨機生成的數(成為canary),在函數返回時進行檢查,如果發現這個canary被修改了(可能是攻擊者通過buffer overflow等攻擊方法覆蓋了),那么就報錯。
那么如何攻破這層防護呢?一種方法是brute-force暴力破解,但這個很低效,這里作者提出了一種叫做“stack reading”的方法:
假設這是我們想要overflow的棧的布局:
我們可以嘗試任意多次來判斷出overflow的長度(直到進程由于canary被破壞crash了,在這里即為4096+8=4104
個字節),之后我們將這4096個字節填上任意值,然后一個一個字節順序地進行嘗試來還原出真實的canary,比如說,我們將第4097個字節填為x
,如果x
和原來的canary中的第一個字節是一樣的話,那么進程不會crash,否則我們嘗試下一個x
的可能性,在這里,由于一個字節只有256種可能,所以我們只要最多嘗試256次就可以找到canary的某個正確的字節,直到我們得到8個完整的canary字節,該流程如下圖所示:
我們同樣可以用這種方法來得到保存好的frame pointer
和return address
。
stop gadget
到目前為止,我們已經得到了合適的canary來繞開stack canary的保護, 接下來的目標就是找到之前提到的4個gadgets。
在尋找這些特定的gadgets之前,我們需要先來介紹一種特殊的gadget類型:stop gadget
.
一般情況下,如果我們把棧上的return address
覆蓋成某些我們隨意選取的內存地址的話,程序有很大可能性會掛掉(比如,該return address
指向了一段代碼區域,里面會有一些對空指針的訪問造成程序crash,從而使得攻擊者的連接(connection)被關閉)。但是,存在另外一種情況,即該return address
指向了一塊代碼區域,當程序的執行流跳到那段區域之后,程序并不會crash,而是進入了無限循環,這時程序僅僅是hang在了那里,攻擊者能夠一直保持連接狀態。于是,我們把這種類型的gadget,成為stop gadget
,這種gadget對于尋找其他gadgets取到了至關重要的作用。
假設現在我們找到了某個可以造成程序block住的stop gadget
,比如一個無限循環,或者某個blocking的系統調用(sleep
),那么我們該如何找到其他 useful gadgets
呢?(這里的“useful”是指有某些功能的gadget,而不是會造成crash的gadget)。
到目前為止我們還是只能對棧進行操作,而且只能通過覆蓋return address
來進行后續的操作。假設現在我們猜到某個useful gadget
,比如pop rdi; ret
, 但是由于在執行完這個gadget之后進程還會跳到棧上的下一個地址,如果該地址是一個非法地址,那么進程最后還是會crash,在這個過程中攻擊者其實并不知道這個useful gadget
被執行過了(因為在攻擊者看來最后的效果都是進程crash了),因此攻擊者就會認為在這個過程中并沒有執行到任何的useful gadget
,從而放棄它,這個步驟如下圖所示:
但是,如果我們有了stop gadget
,那么整個過程將會很不一樣. 如果我們在需要嘗試的return address
之后填上了足夠多的stop gadgets
,如下圖所示:
那么任何會造成進程crash的gadget最后還是會造成進程crash,而那些useful gadget
則會進入block狀態。盡管如此,還是有一種特殊情況,即那個我們需要嘗試的gadget也是一個stop gadget
,那么如上所述,它也會被我們標識為useful gadget
。不過這并沒有關系,因為之后我們還是需要檢查該useful gadget
是否是我們想要的gadget.
到目前為止,似乎準備工作都做好了,我們已經可以繞過canary防護,并且得到很多不會造成進程crash的“potential useful gadget”了,那么接下來就是該如何找到我們之前所提到的那四個gadgets呢?
如上圖所示,為了找到前兩個gadgets:pop %rsi; ret
和pop %rdi; ret
,我們只需要找到一種所謂的BROP gadget
就可以了,這種gadget很常見,它做的事情就是恢復那些callee saved registers
. 而對它進行一個偏移就能夠生成pop %rdi
和pop %rsi
這兩個gadgets.
不幸的是pop %rdx; ret
這個gadget并不容易找到,它很少出現在代碼里, 所以作者提出一種方法,相比于尋找pop %rdx
指令,他認為可以利用strcmp
這個函數調用,該函數調用會把字符串的長度賦值給%rdx
,從而達到相同的效果。另外strcmp
和write
調用都可以在程序的Procedure Linking Table (PLT)里面找到.
所以接下來的任務就是:
BROP Gadget
;BROP Gadget
事實上BROP gadgets
特別特殊,因為它需要順序地從棧上pop
6個值然后執行ret
。所以如果我們利用之前提到的stop gadget
的方法就可以很容易找到這種特殊的gadget了,我們只需要在stop gadget
之前填上6個會造成crash的地址:
如果任何useful gadget
滿足這個條件且不會crash的話,那么它基本上就是BROP gadgets
了。
PLT是一個跳轉表,它的位置一般在可執行程序開始的地方,該機制主要被用來給應用程序調用外部函數(比如libc等),具體的細節可以看相關的Wiki。它有一個非常獨特的signature:每一個項都是16個字節對齊,其中第0個字節開始的地址指向改項對應函數的fast path,而第6個字節開始的地址指向了該項對應函數的slow path:
另外,大部分的PLT項都不會因為傳進來的參數的原因crash,因為它們很多都是系統調用,都會對參數進行檢查,如果有錯誤會返回EFAULT而已,并不會造成進程crash。所以攻擊者可以通過下面這個方法找到PLT:如果攻擊者發現好多條連續的16個字節對齊的地址都不會造成進程crash,而且這些地址加6得到的地址也不會造成進程crash,那么很有可能這就是某個PLT對應的項了。
那么當我們得到某個PLT項,我們該如何判斷它是否是strcmp
或者write
呢?
對于strcmp
來說, 作者提出的方法是對其傳入不同的參數組合,通過該方法調用返回的結果來進行判斷。由于BROP gadget
的存在,我們可以很方便地控制前兩個參數,strcmp
會發生如下的可能性:
arg1 | arg2 | result
:--: | :--: | :--:
readable | 0x0 | crash
0x0 | readable | crash
0x0 | 0x0 | crash
readable | readable | nocrash
根據這個signature, 我們能夠在很大可能性上找到strcmp
對應的PLT項。
而對于write
調用,雖然它沒有這種類似的signature,但是我們可以通過檢查所有的PLT項,然后觸發其向某個socket寫數據來檢查write
是否被調用了,如果write
被調用了,那么我們就可以在本地看到傳過來的內容了。
最后一步就是如何確定傳給write
的socket文件描述符是多少了。這里有兩種辦法:1. 同時調用好幾次write,把它們串起來,然后傳入不同的文件描述符數;2. 同時打開多個連接,然后使用一個相對較大的文件描述符數字,增加匹配的可能性。
到這一步為止,攻擊者就能夠將整個.text
段從內存中通過socket寫到本地來了,然后就可以對其進行反編譯,找到其他更多的gadgets,同時,攻擊者還可以dump那些symbol table之類的信息,找到PLT中其它對應的函數項如dup2
和execve
等。
到目前為止,最具挑戰性的部分已經被解決了,我們已經可以得到被攻擊進程的整個內存空間了,接下來就是按部就班了(從論文中翻譯):
dup2
或close
,跟上dup
或者fcntl(F_DUPFD)
。這些一般都能在PLT里面找到。/bin/sh
。其中一個有效的方法是從symbol table里面找到一個可寫區域(writable memory region),比如environ
,然后通過socket將/bin/sh
從攻擊者這里讀過去。execve
shell. 如果execve
不在PLT上, 那么攻擊者就需要通過更多次的嘗試來找到一個pop rax; ret
和syscall
的gadget.歸納起來,BROP攻擊的整個步驟是這樣的:
stop gadget
:一般情況下這會是一個在PLT中的blocking系統調用的地址(sleep等),在這一步中,攻擊者也可以找到PLT的合法項;BROP gadget
:這一步之后攻擊者就能夠控制write
系統調用的前兩個參數了;strcmp
項,然后通過控制字符串的長度來給%rdx
賦值,這一步之后攻擊者就能夠控制write
系統調用的第三個參數了;write
項:這一步之后攻擊者就能夠將整個內存從遠端dump到本地,用于尋找更多的gadgets;以上就是BROP攻擊的原理,在這篇博文中重現了這個攻擊,有興趣的可以去看看。
其實在整個攻擊過程中最酷的要數第一個步驟:如何dump內存,之后的步驟其實就是傳統的ROP攻擊了。明白了原理之后,其實最好的了解該攻擊的方法就是看源代碼了,這個對了解整個ROP會有非常大的幫助。