Blind Return Oriented Programming (BROP) Attack - 攻擊原理

wyzsk發表於2020-08-19
作者: mctrain · 2014/09/27 13:02

0x00 寫在前面


第一次在WooYun發文章,不知道是否符合眾客官口味,望輕拍。

這篇文章翻譯至我的這篇部落格,主要介紹了一種叫做BROP的攻擊,該文章主要介紹原理部分,對該攻擊的重現可以參看我的另外一篇部落格

BROP攻擊基於一篇發表在Oakland 2014的論文Hacking Blind,作者是來自Standford的Andrea Bittau,以下是相關paper和slide的連結:

paper

slide

以及BROP的原網站地址:

Blind Return Oriented Programming (BROP) Website

可以說這篇論文是今年看過的最讓我感到興奮的論文(沒有之一),如果要用一個詞來形容它的話,那就只有“不能更帥”才能表達我對它的喜愛程度了!

這篇文章假設讀者已經瞭解Return-Oriented Programming (ROP) 的基本概念,所以只是介紹BROP的實現原理,如果還不清楚什麼是ROP,請先出門左轉,看看Wiki的相關介紹。

BROP的實現真的是讓人感到非常“cool”和“smart”,我希望能夠透過這篇文章把它講清楚。

0x01 BROP攻擊的目標和前提條件


目標:透過ROP的方法遠端攻擊某個應用程式,劫持該應用程式的控制流。我們可以不需要知道該應用程式的原始碼或者任何二進位制程式碼,該應用程式可以被現有的一些保護機制如NX, ASLR, PIE, 以及stack canaries等保護,應用程式所在的伺服器可以是32位系統或者64位系統。

初看這個目標感覺實現起來特別困難。其實這個攻擊有兩個前提條件的:

  • 必須先存在一個已知的stack overflow的漏洞,而且攻擊者知道如何觸發這個漏洞;
  • 伺服器程式在crash之後會重新復活,並且復活的程式不會被re-rand(意味著雖然有ASLR的保護,但是復活的程式和之前的程式的地址隨機化是一樣的)。這個需求其實是合理的,因為當前像nginx, MySQL, Apache, OpenSSH, Samba等伺服器應用都是符合這種特性的。

0x10 BROP的攻擊流程 1 - 遠端dump記憶體


由於我們不知道被攻擊程式的記憶體佈局,所以首先要做的事情就是透過某種方法從遠端伺服器dump出該程式的記憶體到本地,為了做到這點我們需要呼叫一個系統呼叫write,傳入一個socket檔案描述符,如下所示:

write(int sock, void *buf, int len)

將這條系統呼叫轉換成4條彙編指令,如圖所示:

write gadgets

所以從ROP攻擊的角度來看,我們只需要找到四個相應的gadget,然後在棧上構造好這4個gadget的記憶體地址,依次進行順序呼叫就可以了。

但是問題是我們現在連記憶體分佈都不知道,該如何在記憶體中找到這4個gadgets呢?特別是當系統部署了ASLR和stack canaries等保護機制,似乎這件事就更難了。

所以我們先將這個問題放一放,在腦袋裡記著這個目標,先來做一些準備工作。

攻破Stack Canaries防護

如果不知道什麼是stack canaries可以先看這裡,簡單來說就是在棧上的return address下面放一個隨機生成的數(成為canary),在函式返回時進行檢查,如果發現這個canary被修改了(可能是攻擊者透過buffer overflow等攻擊方法覆蓋了),那麼就報錯。

那麼如何攻破這層防護呢?一種方法是brute-force暴力破解,但這個很低效,這裡作者提出了一種叫做“stack reading”的方法:

假設這是我們想要overflow的棧的佈局:

stack layout

我們可以嘗試任意多次來判斷出overflow的長度(直到程式由於canary被破壞crash了,在這裡即為4096+8=4104個位元組),之後我們將這4096個位元組填上任意值,然後一個一個位元組順序地進行嘗試來還原出真實的canary,比如說,我們將第4097個位元組填為x,如果x和原來的canary中的第一個位元組是一樣的話,那麼程式不會crash,否則我們嘗試下一個x的可能性,在這裡,由於一個位元組只有256種可能,所以我們只要最多嘗試256次就可以找到canary的某個正確的位元組,直到我們得到8個完整的canary位元組,該流程如下圖所示:

stack reading

我們同樣可以用這種方法來得到儲存好的frame pointerreturn 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取到了至關重要的作用。

尋找可利用的(potentially useful)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,從而放棄它,這個步驟如下圖所示:

useful gadget but crash

但是,如果我們有了stop gadget,那麼整個過程將會很不一樣. 如果我們在需要嘗試的return address之後填上了足夠多的stop gadgets,如下圖所示:

stop gadgets usage

那麼任何會造成程式crash的gadget最後還是會造成程式crash,而那些useful gadget則會進入block狀態。儘管如此,還是有一種特殊情況,即那個我們需要嘗試的gadget也是一個stop gadget,那麼如上所述,它也會被我們標識為useful gadget。不過這並沒有關係,因為之後我們還是需要檢查該useful gadget是否是我們想要的gadget.

最後一步:遠端dump記憶體

到目前為止,似乎準備工作都做好了,我們已經可以繞過canary防護,並且得到很多不會造成程式crash的“potential useful gadget”了,那麼接下來就是該如何找到我們之前所提到的那四個gadgets呢?

find write gadgets

如上圖所示,為了找到前兩個gadgets:pop %rsi; retpop %rdi; ret,我們只需要找到一種所謂的BROP gadget就可以了,這種gadget很常見,它做的事情就是恢復那些callee saved registers. 而對它進行一個偏移就能夠生成pop %rdipop %rsi這兩個gadgets.

不幸的是pop %rdx; ret這個gadget並不容易找到,它很少出現在程式碼裡, 所以作者提出一種方法,相比於尋找pop %rdx指令,他認為可以利用strcmp這個函式呼叫,該函式呼叫會把字串的長度賦值給%rdx,從而達到相同的效果。另外strcmpwrite呼叫都可以在程式的Procedure Linking Table (PLT)裡面找到.

所以接下來的任務就是:

  • 找到所謂的BROP Gadget
  • 找到對應的PLT項。

尋找BROP Gadget

事實上BROP gadgets特別特殊,因為它需要順序地從棧上pop 6個值然後執行ret。所以如果我們利用之前提到的stop gadget的方法就可以很容易找到這種特殊的gadget了,我們只需要在stop gadget之前填上6個會造成crash的地址:

find brop gadget

如果任何useful gadget滿足這個條件且不會crash的話,那麼它基本上就是BROP gadgets了。

尋找PLT項

PLT是一個跳轉表,它的位置一般在可執行程式開始的地方,該機制主要被用來給應用程式呼叫外部函式(比如libc等),具體的細節可以看相關的Wiki。它有一個非常獨特的signature:每一個項都是16個位元組對齊,其中第0個位元組開始的地址指向改項對應函式的fast path,而第6個位元組開始的地址指向了該項對應函式的slow path:

plt structure

另外,大部分的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中其它對應的函式項如dup2execve等。

0x11 BROP的攻擊流程 2 - 實施攻擊


到目前為止,最具挑戰性的部分已經被解決了,我們已經可以得到被攻擊程式的整個記憶體空間了,接下來就是按部就班了(從論文中翻譯):

  • 將socket重定向到標準輸入/輸出(standard input/output)。攻擊者可以使用dup2close,跟上dup或者fcntl(F_DUPFD)。這些一般都能在PLT裡面找到。
  • 在記憶體中找到/bin/sh。其中一個有效的方法是從symbol table裡面找到一個可寫區域(writable memory region),比如environ,然後透過socket將/bin/sh從攻擊者這裡讀過去。
  • execve shell. 如果execve不在PLT上, 那麼攻擊者就需要透過更多次的嘗試來找到一個pop rax; retsyscall的gadget.

歸納起來,BROP攻擊的整個步驟是這樣的:

  • 透過一個已知的stack overflow的漏洞,並透過stack reading的方式繞過stack canary的防護,試出某個可用的return address;
  • 尋找stop gadget:一般情況下這會是一個在PLT中的blocking系統呼叫的地址(sleep等),在這一步中,攻擊者也可以找到PLT的合法項;
  • 尋找BROP gadget:這一步之後攻擊者就能夠控制write系統呼叫的前兩個引數了;
  • 透過signature的方式尋找到PLT上的strcmp項,然後透過控制字串的長度來給%rdx賦值,這一步之後攻擊者就能夠控制write系統呼叫的第三個引數了;
  • 尋找PLT中的write項:這一步之後攻擊者就能夠將整個記憶體從遠端dump到本地,用於尋找更多的gadgets;
  • 有了以上的資訊之後,就可以建立一個shellcode來實施攻擊了。

0x100 後記


以上就是BROP攻擊的原理,在這篇博文中重現了這個攻擊,有興趣的可以去看看。

其實在整個攻擊過程中最酷的要數第一個步驟:如何dump記憶體,之後的步驟其實就是傳統的ROP攻擊了。明白了原理之後,其實最好的瞭解該攻擊的方法就是看原始碼了,這個對了解整個ROP會有非常大的幫助。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章