1. 程式人生 > 其它 >angr_ctf——從0學習angr(一):angr簡介與核心概念

angr_ctf——從0學習angr(一):angr簡介與核心概念

我在學習angr時,先是閱讀了開發者釋出在IEEE上的論文IEEE Xplore Full-Text PDF:該文章講述了自動化漏洞挖掘的背景和方法,並對angr的架構和核心模組進行了介紹,非常經典值得一讀。

而後,我閱讀了angr官方文件和API文件,對angr有了總體的、暈暈乎乎的瞭解。最後,我發現了github上的專案angr_ctf,並使用該專案在解題過程中不斷補充、修正我對angr的瞭解,以下是涉及的相關連線,你可能用得上:

angr官方文件README - angr Documentation

angr的API文件angr API documentation — angr 9.2.26 documentation

angr_ctf專案GitHub - jakespringer/angr_ctf

本系列教程是angr的入門教程,將通過做angr_ctf中的題目的形式來介紹angr,在每篇開頭會先介紹該篇所寫題目時要用到的知識,並且儘量進行詳細的補充和擴充套件,以便於你瞭解angr是一個多麼偉大的專案。觀點均來自作者的論文、angr官方文件以及本人在實踐時的思考。

angr和angr_ctf簡介

angr是一個支援多處理架構的用於二進位制檔案分析的工具包,它提供了動態符號執行的能力以及多種靜態分析的能力。專案建立的初衷,是為了整合此前多種二進位制分析方式的優點,並開發一個平臺,以供二進位制分析人員比較不同二進位制分析方式的優劣,並根據自身需要開發新的二進位制分析系統和方式。

也正是因為angr是一個二進位制檔案分析的工具包,因此它可以被使用者擴充套件,用於自動化逆向工程、漏洞挖掘等多個方面。

angr_ctf則是一個專門針對angr的專案,裡面有17個angr相關的題目。這些題目只有一個唯一的要求:你需要找出能夠使程式輸出“Good Job”的輸入,這也是符號執行常見的應用場景。

專案中序號開頭的資料夾裡面是題目的原始碼和題解。

dist中儲存了各個題目編譯後的可執行檔案,均是ELF-32bit

solutions中集合了所有題目的題解,也是所有序號開標頭檔案夾的合集。題解檔案solve包含完整題解,而scaffold則是待填充的題解,需要使用者根據程式在“???”處填入合適的內容。

angr核心概念

頂層介面

Project類是angr的主類,也是angr的開始,通過初始化該類的物件,可以將你想要分析的二進位制檔案載入進來,就像這樣:

import angr
p = angr.Project('/bin/true')

引數為待分析的檔案路徑,它是唯一必須傳入的引數,此外還有一個比較常用的引數load-options,它指明載入的方式,如下:

名稱  描述 傳入引數
auto_load_libs   是否自動載入程式的依賴 布林
skip_libs 希望避免載入的庫 庫名
except_missing_libs 無法解析庫時是否丟擲異常 布林
force_load_libs 強制載入的庫 庫名
ld_path 共享庫的優先搜尋路徑 路徑名

使用angr時最重要的就是效率問題,少載入一些無關結果的庫能夠提升angr的效率,如下:

import angr
p = angr.Project('/bin/true', auto_load_libs=False)

任何附加的引數都會被傳遞到angr的載入器,即CLE.loader中(CLE 即 CLE Loads Everything的縮寫)

Project類中有許多方法和屬性,例如載入的檔名、架構、程式入口點、大小端等等:

>>> print(p.arch, hex(p.entry), p.filename, p.arch.bits, p.arch.memory_endness )
<Arch AMD64 (LE)> 0x4023c0 /bin/true 64 Iend_LE

狀態State

Project實際上只是將二進位制檔案載入進來了,要執行它,實際上是對SimState物件進行操作,它是程式的狀態。用docker來比喻,Project相當於開發環境,State則是使用開發環境製作的映象。

要建立狀態,需要使用Project物件中的factory,它還可以用於建立模擬管理器和基本塊(後面提到),如下:

init_state = p.factory.entry_state()

預設狀態有四種方式如下:

預設狀態方式 描述
entry_state 初始化狀態為程式執行到程式入口點處的狀態
blank_state(addr=) 大多數資料都沒有初始化,狀態中下一條指令為addr處的指令
full_init_state 共享庫和預定義內容已經載入完畢,例如剛載入完共享庫
call_state 準備呼叫函式的狀態

狀態包含了程式執行時的一切資訊,暫存器、記憶體的值、檔案系統以及符號變數等,這些資訊的使用等用到時再進一步說明。

entry_state和blank_state是常用的兩種方式,後者通常用於跳過一些極大降低angr效率的指令,它們間的對比如下:

>>> state = p.factory.entry_state()
>>> print(state.regs.rax, state.regs.rip)
<BV64 0x1c> <BV64 0x4023c0>
>>> state = p.factory.blank_state(addr=0x4023c0)
>>> print(state.regs.rax, state.regs.rip)
<BV64 reg_rax_42_64{UNINITIALIZED}> <BV64 0x4023c0>

在blank_state方式中,我們仍將地址設定為程式的入口點,然而rax中的值由於沒有初始化,它現在是一個名字,也即符號變數,這是符號執行的基礎,後續在細說。

此外,可以看到暫存器中的資料型別並不是int,而是BV64,它是一個位向量(Bit Vector),有關位向量的細節之後再說。

模擬管理器(Simulation Manager)

上述方式只是預設了程式開始分析時的狀態,我們要分析程式就必須要讓它到達下一個狀態,這就需要模擬管理器的幫助(簡稱SM).

使用以下指令能建立一個SM,它需要傳入一個state或者state的列表作為引數:

simgr  = p.factory.simgr(state)

SM中有許多列表,這些列表被稱為stash,它儲存了處於某種狀態的state,stash有如下幾種:

stash 描述
active 儲存接下來可以執行並且將要執行的狀態
deadended 由於某些原因不能繼續執行的狀態,例如沒有合法指令,或者有非法指標
pruned 與solve的策略有關,當發現一個不可解的節點後,其後面所有的節點都優化掉放在pruned裡
unconstrained 如果建立SM時啟用了save_unconstrained,則沒有約束條件的state會放在這裡
unsat 如果建立SM時啟用了save_unsat,則被認為不可滿足的state會放在這裡

預設情況下,state會被存放在active中。

stash中的state可以通過move()方法來轉移,將fulter_func篩選出來的state從from_stash轉移到to_stash

simgr.move(from_stash='deadended', to_stash='more_then_50', filter_func=lambda s: '100' in s.posix.dumps(1))

stash是一個列表,可以使用python支援的方式去遍歷其中的元素,也可以使用常見的列表操作。但angr提供了一種更高階的方式,在stash名字前加上one_,可以得到stash中的第一個狀態,加上mp_,可以得到一個mulpyplexed版本的stash

此外,稍微解釋一下上面程式碼中的posix.dumps:

  • state.posix.dumps(0):表示到達當前狀態所對應的程式輸入
  • state.posix.dumps(1):表示到達當前狀態所對應的程式輸出

上述程式碼就是將deadended中輸出的字串包含'100'的state轉移到more_then_50這個stash中。

可以通過step()方法來讓處於active的state執行一個基本塊,這種操作不會改變state本身:

>>> state = p.factory.entry_state()
>>> simgr = p.factory.simgr(state)
>>> print(state.regs.rax, state.regs.rip)
<BV64 0x1c> <BV64 0x4023c0>

>>> print(simgr.one_active)
<SimState @ 0x4023c0>

>>> simgr.step()
<SimulationManager with 1 active>
>>> print(simgr.one_active)
<SimState @ 0x529240>

>>> print(state.regs.rax, state.regs.rip)
<BV64 0x1c> <BV64 0x4023c0>

最後也是SM最常用的技術:探索技術(explorer techniques)

可以使用explorer方法去執行某個狀態,直到找到目標指令或者active中沒有狀態為止,它有如下引數:

  • find:傳入目標指令的地址或地址列表,或者一個用於判斷的函式,函式以state為形參,返回布林值
  • avoid:傳入要避免的指令的地址或地址列表,或者一個用於判斷的函式,用於減少路徑

此外還有一些搜尋策略,之後會集中講解,預設使用DFS(深度優先搜尋)。

explorer找到的符合find的狀態會被儲存在simgr.found這個列表當中,可以遍歷其中元素獲取狀態。

 符號執行

angr作為一個二進位制分析的工具包,但它通常作為符號執行工具更為出名。

符號執行就是給程式傳遞一個符號而不是具體的值,讓這個符號伴隨程式執行,當碰見分支時,符號會進入哪個分支呢?

angr的回答是全都進入!angr會儲存所有分支,以及分支後的所有分支,並且在分支時,儲存進入該分支時的判斷條件,通常這些判斷條件時對符號的約束。

當angr執行到目標狀態時,就可以呼叫求解器對一路上收集到的約束進行求解,最終得到某個符號能夠到達當前狀態的值。

例如,程式接收一個int型別的輸入,當這個輸入大於0小於5時,就會執行某條儲存在該程式中,我們希望執行的指令(例如一個後門函式backdoor),具體而言如下圖所示:

angr會沿著分支按照某種策略(預設DFS)進行狀態搜尋,當達到目標狀態(也就是backdoor能夠執行的狀態),此時angr已經收集了兩個約束(x>0 以及x<=5),那麼angr就通過這兩個約束對x進行求解,解出來的x值就是能夠讓程式執行backdoor的輸入。

在複雜的程式當中,從一個符號到backdoor的路徑可能十分複雜,甚至包含一些加密解密的過程,這時就是angr大顯身手的時候了。

實戰 00_angr_find

使用angr一般分為如下步驟:

  1. 建立Project,預設state
  2. 建立位向量和符號變數,儲存在記憶體/暫存器/檔案或其他地方
  3. 將state新增到SM中
  4. 執行,探索滿足條件的路徑
  5. 約束求解獲取執行結果

先逆向檢視邏輯

程式接收一個8位元組的輸入,對它使用complex_function函式進行轉換,比較轉換後的字串是否等於JACEJGCS,現在我們要找出能夠讓程式執行puts("Good Job.")的輸入。

以下是能夠獲取結果的angr指令碼:

import angr

#載入檔案,預設狀態,執行狀態
p = angr.Project('./dist/00_angr_find', auto_load_libs=False)
init_state = p.factory.entry_state()
simgr = p.factory.simgr(init_state)
 
#puts Good的指令地址
target = 0x08048678

#搜尋能夠執行目標指令的狀態
simgr.explore(find = target)

if simgr.found:
    solution_state = simgr.found[0]
   #打印出符號條件的狀態的輸入    
    print(solution_state.posix.dumps(0))

由於該題比較簡單,因此跳過了第2和第5步。

執行結果如下:

┌──(venv)─(kali㉿kali)-[~/angrfile]
└─$ python 00.py                                                                                                          148 ⨯ 2 ⚙
WARNING | 2022-11-27 22:37:44,598 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing register with an unspecified value. This could indicate unwanted behavior.
WARNING | 2022-11-27 22:37:44,598 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2022-11-27 22:37:44,598 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2022-11-27 22:37:44,598 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2022-11-27 22:37:44,598 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2022-11-27 22:37:44,598 | angr.storage.memory_mixins.default_filler_mixin | Filling register edi with 4 unconstrained bytes referenced from 0x80486b1 (__libc_csu_init+0x1 in 00_angr_find (0x80486b1))
WARNING | 2022-11-27 22:37:44,601 | angr.storage.memory_mixins.default_filler_mixin | Filling register ebx with 4 unconstrained bytes referenced from 0x80486b3 (__libc_csu_init+0x3 in 00_angr_find (0x80486b3))
WARNING | 2022-11-27 22:37:46,904 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7ffeff50 with 4 unconstrained bytes referenced from 0x8100000 (strcmp+0x0 in extern-address space (0x0))
b'JXWVXRKX'

事實上,上述指令碼能夠解決一切有關"為了執行某條目標語句,我應該用怎樣的輸入”這樣的問題,是一個萬能指令碼。區別在於由於程式的複雜程度和邏輯不同,耗費的時間不同,因此在解決這類問題上,編寫angr指令碼的本質是在使用angr提供的各種二進位制分析方法去優化路徑,提高它的執行效率。

實戰 01_angr_avoid

這個題使用IDA開啟時就很慢,在對main進行反彙編時會提示函式過大,因此直接shift+F12查詢呼叫了Good的指令,發現是函式maybe_good

此外檢視函式avoid_me的交叉引用

可以發現該函式被main函式呼叫了多次,應該是導致main函式過大的原因,因此要對它進行避免,也就是使用explorer的avoid的引數。

整個angr指令碼如下:

import angr

p = angr.Project('./dist/01_angr_avoid')
init_state = p.factory.entry_state()
simgr = p.factory.simgr(init_state)

good = 0x080485e5
#avoid_me的函式起始的位置(並非呼叫該函式的位置,因為呼叫該函式的地方太多了) bad = 0x080485a8 simgr.explore(find= good,avoid = bad) if simgr.found: solution = simgr.found[0] print(solution.posix.dumps(0))else: raise Exception("Could not find solution")

執行結果如下:

┌──(venv)─(kali㉿kali)-[~/angr]
└─$ python 01.py                                                                                                            1 ⨯ 2 ⚙
WARNING | 2022-11-27 22:54:49,287 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing register with an unspecified value. This could indicate unwanted behavior.
WARNING | 2022-11-27 22:54:49,287 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2022-11-27 22:54:49,287 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2022-11-27 22:54:49,287 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2022-11-27 22:54:49,287 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2022-11-27 22:54:49,288 | angr.storage.memory_mixins.default_filler_mixin | Filling register edi with 4 unconstrained bytes referenced from 0x80d4591 (__libc_csu_init+0x1 in 01_angr_avoid (0x80d4591))
WARNING | 2022-11-27 22:54:49,293 | angr.storage.memory_mixins.default_filler_mixin | Filling register ebx with 4 unconstrained bytes referenced from 0x80d4593 (__libc_csu_init+0x3 in 01_angr_avoid (0x80d4593))
WARNING | 2022-11-27 22:54:55,575 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7ffeff2d with 11 unconstrained bytes referenced from 0x8197e20 (strncmp+0x0 in libc.so.6 (0x97e20))
WARNING | 2022-11-27 22:54:55,576 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7ffeff50 with 4 unconstrained bytes referenced from 0x8197e20 (strncmp+0x0 in libc.so.6 (0x97e20))
b'HUJOZMYS'

實戰:02_angr_find_condition

該題邏輯和前兩題差不多,區別在於此時使用了字串“Good Job”和字串“Try again”的指令多了很多,因此需要考慮給find和avoid引數傳入函式來篩選出所有這些使用了這兩個字串的指令。

對前兩題的指令碼進行如下修改:

def good(state):
    tag = b'Good' in state.posix.dumps(1)
    return True if tag else False

def bad(state):
    tag = b'Try' in state.posix.dumps(1)
    return True if tag else False
    
simgr.explore(find=good, avoid=bad)

上述函式使用state作為形參,在函式內部進行判斷後返回布林值,當值為true時觸發相應的操作(find或者avoid),這樣就能篩選出所有能夠打印出字串“Good Job”和字串“Try again的狀態了。

其他說明:路徑搜尋——avoid為何能提高效率

angr在模擬執行指令時,對於遇到的分支和跳轉,會全部進行保留,並且記錄用於判斷分支的條件(即約束),如下圖所示

之前說過,這些狀態都是程式執行到某些階段時的資訊,包括了記憶體、暫存器、檔案系統等多個方面,這些狀態中有滿足條件的狀態,就會被放入到found列表當中。

而在路徑搜尋時,對於滿足avoid條件的狀態,則會被丟棄,也就是說,該狀態及該狀態的後續路徑都不會被進行搜尋,因此簡化了angr的搜尋路徑,從而提高效率。