我在學習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一般分為如下步驟:
- 創建Project,預設state
- 創建位向量和符號變數,保存在記憶體/暫存器/檔案或其他地方
- 將state添加到SM中
- 運行,探索滿足條件的路徑
- 約束求解獲取執行結果
先逆向查看邏輯

程式接收一個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的搜索路徑,從而提高效率,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/538711.html
標籤:其他
