AEGG - Automatic Exploit GG

AEG(Automatic Exploit Generation)是 CMU 2011 年發表的一篇 paper,然後我還參考了 angr 的一個範例 insomnihack_aeg,這個範例他寫了簡單的自動產生 shellcode 的腳本,不過沒有像 AEG 那篇論文寫的有完整架構。然後在 UCSB 最近也有了一篇講了很多 angr 架構的 paper,裡面也有提到 AEG,所以參考這幾篇之後打算也來自己寫了一個,叫做 AEGG

使用方法可以點上面連結直接到 github 上看,而這邊來紀錄一下 AEGG 的架構,主要有四個主要類別:

  1. BugFinder:第一階段是找漏洞,如果找到漏洞就可以把觸發漏洞的 payload(或路徑) 丟到下一個階段。
  2. Analyzer:從上一個階段拿到的 payload(或路徑),會對其進行分析,紀錄他在動態執行的結果中的一些資訊。
  3. Exploiter:將路徑和分析結果丟入這個階段,他會查詢分析結果並且嘗試產生一組攻擊 payload。
  4. Verifier:最後一個階段是要檢查產生出來的攻擊 payload,如果無法成功開 shell 則代表該 payload 無效。

而這四個類別會由 AEGG 這個類別把他們包裝起來,如下是使用四個類別的虛擬碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def hack():
paths = bug_finder.find()
for path in paths:
exploit_gen(path)

def exploit_gen(path):
analysis = analyzer.analyze(path)
for payload in exploiter.generate(path, analysis):
if verifier.verify(payload):
# Generated!
payloads.append(payload)
return True
# Can not generate any payload.
return False

BugFinder

目前在 BugFinder 裡面找漏洞的方法,是用 angr 直接跑,直到找到不受限制的路徑,並且回傳所找到的 angr 路徑物件。這邊有個小 future work,之後回傳可以改成一個 payload 而不是一個路徑物件,好處是這樣第一階段在找漏洞的方法就不限制只能使用 angr 了,還可以用 afl fuzzing 等等其他工具,不過這樣在 Analyzer 那邊也需要先把 payload 丟入 angr 動態執行一次,要得到經過模擬後的路徑物件才會有更多資訊可以拿。

在 BugFinder 裡執行的腳本其實不多,不過有一些要注意的地方:

1
2
3
4
5
6
7
def _init_pg(self):
p = angr.Project(self.binary)
extras = {o.REVERSE_MEMORY_NAME_MAP, o.TRACK_ACTION_HISTORY}
state = p.factory.full_init_state(add_options=extras)
state.libc.buf_symbolic_bytes = 200
pg = p.factory.path_group(state, save_unconstrained=True)
return pg

一開始設定狀態時,要加上 REVERSE_MEMORY_NAME_MAP 這個額外選項,目的是為了保留對記憶體位址的資訊,下一個階段 Analyzer 在找所有可能存在的 buffers 時會用到。而對 path_group 的設定,要設定 save_unconstrained=True,才會保留那些不受限制的路徑。而 TRACK_ACTION_HISTORY 則是可以自己除錯用,方便查看之前所模擬執行過的狀態的 ACTION 紀錄,而非只有這一次狀態的紀錄而已。

如果這邊單用符號執行來找漏洞的話,會有很多優化的問題,像是路徑爆炸、環境配置、路徑優先性的選擇等等,這些都有人提出解決方法。

關於優化:
對於路徑爆炸,就有 veritesting、或是結合 fuzzing 來避免讓符號執行佔太多時間來探索路徑;
環境配置的話會有像是 libc 的函式、開檔讀檔等等,angr 的解法會有 sim_proceduressim_file 這類已經先建好的物件來模擬;
再來是路徑的選擇,因為遇到迴圈和判斷式就容易產生許多路徑,有的策略會挑選能把迴圈走完的路徑優先下去執行,或是先挑有 bug 但還夠不成可以利用的路徑優先選擇。

Analyzer

從 BugFinder 拿到路徑後,需要分析和紀錄一些執行檔和路徑的資訊,方便在下個階段比較好做事。以執行檔來說,我們需要蒐集 checksec 上的資訊,像是 stack 可不可以執行、有沒有 ASLR 等等。而路徑上的資訊主要是蒐集所有 buffer,因為沒有原始碼可以看出哪裡會有 buffer,所以在這裡的作法是參考 angr 的範例 insomnihack_aeg。

關於找 buffer 的話分成三個步驟:

  1. 暴力搜尋所有 stdin 可能所在的記憶體位置
    state.posix.get_file(0).variables() 可以得到變數的名字,然後再拿這個變數名字去搜尋,用 state.memory.addrs_for_name(var) 來搜尋這變數存在記憶體的哪些位址上。需要開 REVERSE_MEMORY_NAME_MAP 才可以用。所以第一步可以得到很多實際的記憶體位址(而不是用符號變數表示),而這些位址都是 stdin 可控的。
  2. 檢查 buffer 的連續性
    接著對所有所蒐集到的位址一一當作 buffer 的起始位址,找出他的長度。找長度的方法只要確認他的下一個記憶體位址是否也是落在 stdin 可控的範圍內即可。
  3. 紀錄並回傳
    最後紀錄 buffer 的起始位址以及長度即可。

這樣的作法的效果在於,如果是要找 global buffer,就可以明確的找到該位址,因為 global buffer 記憶體位址並不會隨著每次執行而不一樣。但有一個問題是,可能會找到 stack 上的連續區段當作 buffer,但該區段記憶體位址是會變動的,會造成在真正跑執行檔時那個位址並不是 stdin 可控。

Exploiter

這個階段會將路徑和分析路徑的結果拿來試著產生 payload,目前做了 Ret2Stack 以及 ROP,這兩個類別被實作在 exploits/ 裡,同時都繼承 Exploit 這個類別。而 Exploiter 負責一一用 exploits/ 裡的類別們來嘗試產生 payload。

Ret2Stack

這個利用的作法分成三個步驟:

  1. 找到一個足夠長的 buffer 來放 shellcode
  2. 將 ip 指向 buffer 地址開頭
  3. 檢查是否路徑可滿足

第一步的 shellcode 可以是各式各樣的,包含一般典型的 shellcode (22 bytes,pwntool 內建的),還可以放專為 scanf 的,或是 alphanumeric 等等。這個利用方法會一一嘗試放每個 shellcode 並且檢查是否真的可放入。
第二步驟則是我們要改返回地址,改到我們放 shellcode 的地方。而第三步驟就是檢查,放 ip 和放 shellcode 的作法,我們需要把想設定的值當作一個限制式讓 angr 去解看看是否可滿足。

做檢查的原因在於:假設有一個 buffer overflow,paylaod 是 payload = "AAAAAAAAAAAAAAAAABBBB",payload 長度是 21 bytes。假設程式執行會把 paylaod 放在記憶體 0xffff1000 的地方,而 "BBBB" 正是壓到返回地址的記憶體位址。如果今天 Ret2Stack 發現 0xffff1000 這個地方有 buffer,並且將 shellcode 放在這個位址,但同時又要把 "BBBB" 改成 0xffff1000 讓程式返回到 buffer 開頭來執行 shellcode,這其實是不可能滿足的,因為 shellcode 的長度大於 payload 的長度,無法在 "BBBB" 同時要是 shellcode 的一部分又同時要是返回地址。
但對於手寫利用腳本來說,這其實有辦法作到的,只要 shellcode 在 "BBBB" 之前執行 jmp 跳過 "BBBB" 就可以解決,不過目前 AEGG 還沒有寫這個部份。

這邊對於 angr 所產生的 payload 有點小疑問,在 angr 執行後發現一個可以觸發 buffer overflow 的 payload 是:'\x90' * 21 + ret_addr,但實際上跑執行檔要觸發的 payload 卻是:'\x90' * 17 + ret_addr,目前暫時的解法是把 payload 前面也放入想要返回的地址,這樣就有機會壓到對的返回地址並且執行 shellcode,實際上為什麼 angr 產生的 offset 不同還待研究。

ROP

再來是 ROP 的部份,因為 ROPgadget 已經有辦法自動產生 ROPchain,我想說來做點不一樣的,於是來作 leak info + rop 這個方法。
作法是先寫好一個腳本的模板,然後紀錄該路徑可以觸發 buffer overflow 的一段 payload,第一次疊主要用來洩漏 libc_base 的位址,利用的方法是用執行檔內部有使用到可以印到 stdout 的函式呼叫(像是 puts, printf 等等),接著目標要洩漏的對象直接設成 "__libc_start_main",他一定被呼叫過所以會被解析。
當我們印出 "__libc_start_main" 的記憶體位址時,還要知道到底是在 stdout 的哪個 byte 被印出,目前的解決方法是先嘗試在路徑的 ip 設成一個我們已知的隨意數值,如果在 stdout 有找到這個隨意數值的話,就紀錄這個數值出現在 stdout 的位移量是多少,這個位移量是返回位址出現在 stdout 的位移量,所以往後數幾個 bytes 大約就是被洩漏的 libc 位址,不過只是大約,所以還需要一個範圍來暴力搜尋。而如果對於有些執行檔並不會印出 buffer 導致他也並不會印出返回地址,就無法使用這個方法,只能把目前的 stdout 長度設為位移量,從這個位移量開始的範圍去尋找。
再來是和 Ret2Stack 有一樣的問題,為了避免 angr 產生的 payload 算錯了 padding 的長度,這裡除了嘗試不同的位移量之外,還會嘗試調整不同的 payload 長度。
最後一個步驟是,第一次洩漏 libc_base 的位址後,會跳回 main 裡執行,而第二次用同樣的 payload 來觸發 buffer overflow,並且返回到 system 即可開 shell。

Verifier

用來確認產生出來的 payload 是否真的可以開 shell,檢查的方法有很多,我目前的作法是只要確認送完 payload 後再送某個指令(像是 uname -a 之類),並且自己也額外實際執行指令得到結果,確認指令執行結果有在 stdout 裡面就可以了。
由於在 Exploiter 產生的 payload 有分成兩種:一種是單純是字串當作輸入,一種是產生一個腳本要執行。Verifier 對第一種只要直接跑起目標的執行檔並且將 payload 全都丟輸入,最後再輸入某個指令即可。而第二種則是把腳本存成檔案並且實際跑腳本,並且要在最後加上要執行的某個指令,然後實際跑腳本執行即可。

結語

其實自動利用生成還有很多地方要補強,而且目前可以產生利用的執行檔也都是一般很普遍的漏洞,並且要關閉一些保護。如果要對 CTF 比賽裡的一些題目來自動產生的話,還要修正和增加不少地方。寫起來有點像是一個例子對一個例子寫解決方法,感覺有點難寫出通用大部分執行檔的自動攻擊腳本生成。

是說有些地方想用圖來表示,不知道有沒有什麼好用的工具來畫記憶體之類的?