一:概論
angr作為符號執行的工具,集成了過去的許多分析方式,它不僅能進行動態符號執行,而且還能進行很多靜態分析,他在分析二進制程序中能發揮很大的作用,下面為一些應用:
1:利用符號執行探究執行路徑,自動解ctf逆向題
2:利用angr獲取程序控制流(CFG)
3:利用angr生成rop鏈
4:利用angr發現漏洞
5:利用angr加密程序
6:進行污點跟蹤
由上可以發現,angr的應用是非常多的,里面關於符號執行的應用和思路(特別是自動化相關的思路)是非常值得學習的,本篇不涉及angr的具體應用,主要講一下angr整個設計的架構。
二:符號執行
先初略了解一下符號執行,對angr有個大致的了解,后續利用angr再對符號執行的理解進行加深。最傳統的符號執行是靜態符號執行,首先將輸入的變量符號化,如果通俗點的話就是設置輸入變量為x,然后通過靜態分析程序的cfg流,轉化為中間語言,獲得符號化的變量在程序流程中的改變,從而輸出一個帶符號化變量的值。舉個例子:
a = raw_input()
b = 2*a if( b == 10): print "win" else: print "lose"
在這個簡單的代碼段里,傳統的運行是 先輸入a的值,再運行下來的代碼。在靜態符號執行的過程中,首先將a進行符號化,就是轉化為x,所以b就是 2*x,當b == 2*x時,則走入”win"路徑,如果 b!=2*x時,則走入lose路徑。路徑合起來稱之為執行樹,(b==2*x)和(b!=2*x)即為路徑約束式,當符號執行結束時(程序正常或者異常退出),約束求解器就會對路徑約束式進行求解(可以簡單理解為解方程),解出的答案就是走到這個路徑需要的值。
當然,這種方式看起來很美麗,但是在實際執行過程中會出現很多問題,其中一個就是約束式無法通過約束式求解的問題,這里的解決方案是將傳統的靜態符號執行和實際執行結合起來,稱之為動態符號執行(concolic execution),concolic維持了兩個狀態。一種是實際變量的狀態,另一種是符號化的狀態。實際狀態將隨機生成值映射到變量中,而符號化狀態將變量進行符號化。concolic首先將實際狀態運行,並收集實際運行時該路徑的變量符號化的約束式,i求解。並將約束式取反,獲取另一條路徑的約束式並求解。過程不斷重復,知道路徑被探索完,或者達到用戶設置的限制。
以上面的代碼為示例,Concolic隨機生成變量(a = 7),然后實際運行走了lose路徑。在判斷語句中,根據收集的約束式取反(b== 2*x),可以得到另一條路徑。通過實際運行這種方式,可以很好的避免了約束式無法識別和求解的問題。
三:Angr架構
angr架構非常清晰,主要分為下圖這些模塊,每個模塊的功能以及彼此間的聯系。
3.1 CLE模塊
二進制的裝載組建是CLE(CLE Load Everything),它負責裝載二進制對象以及它所依賴的庫,將自身無法執行的操作轉移給angr的其它組件,最后生成地址空間,表示該程序已加載並可以准備運行。
>>> import angr, monkeyhex >>> proj = angr.Project('/bin/true') >>> proj.loader <Loaded true, maps [0x400000:0x5008000]>
cle.loader代表着將整個程序映射到某個地址空間,而地址空間的每個對象都可以由一個加載器后端加載,例如cle.elf用於加載linux的32位程序。下面是地址空間的分類
>>> proj.loader.all_objects [<ELF Object fauxware, maps [0x400000:0x60105f]>, <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>, <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>, <ELFTLSObject Object cle##tls, maps [0x3000000:0x300d010]>, <KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>, <ExternObject Object cle##externs, maps [0x5000000:0x5008000]>
其中,類型可以分為 proj.loader.main_object,proj.loader.share_object,proj.loader.kernel_object等等...獲取特定object之后,可以與object進行交互獲取更詳細的信息
3.2 ArchInfo模塊
archinfo是包含特定於體系結構的信息的類的集合。太過於底層,在日后的分析中逐步解釋
3.3 PyVex模塊
angr需要處理不同的架構,所以它選擇一種中間語言來進行它的分析,angr使用Valgrind的中間語言——VEX來完成這方面的內容。VEX中間語言抽象了幾種不同架構間的區別,允許在他們之上進行統一的分析。各種中間語言在設計理念上有很多的共通點,這里又會是一個很大的話題,所以暫且拋開,具體關於IR語言的語法規則請查閱 https://docs.angr.io/docs/ir.html。
3.4 SimuVEX模塊
這里是中間語言VEX執行的模擬器,它允許你控制符號執行。
3.5 Clarity
這個模塊主要專注於將變量符號化,生成約束式並求解約束式,這也是符號執行的核心所在,在angr中主要是利用微軟提供的z3庫去解約束式
3.6 angr以及以上
這些則為上層封裝好的接口,后續使用時在描述。
四:angr的輸入輸出
一般來說,命令行程序主要有兩種數據輸入的方式,第一種是利用api(get,read),第二種是放在argc上,其它的方法有很多,最后也會提供一種通用的解法。
當數據輸入在argc上時,一般使用claripy庫,將輸入的數據符號化,具體代碼如下:
import angr import claripy p = angr.Project("test") args = claripy.BVS('args', 8*16) initial_state = prog.factory.entry_state(args=["./vul", args])
for i in range(0,8):
initial_state.add_constraints(argc.get_byte(0) >= argvc.get_byte(1)) pg = p.factory.path_group(initial_state) pg.explore(find=(0x4005d1,)) print pg # <PathGroup with 18 deadended, 4 active, 1 found> print pg.found[0] # <Path with 64 runs (at 0x4005d1)> print pg.found[0].state.posix.dumps(0)
claripy庫是求解器引擎,絕大部分只是用來做z3的前端,而在這里起到的作用主要是將參數符號化,核心代碼為第四行。
當利用api時,主要通過對st.posix.files[0]進行符號化,具體代碼如下:
p = angr.Project('wyvern') st = p.factory.full_init_state(args=['./wyvern'], add_options=angr.options.unicorn) for _ in xrange(28): k = st.posix.files[0].read_from(1) st.solver.add(k != 0) st.solver.add(k != 10) k = st.posix.files[0].read_from(1) st.solver.add(k == 10) st.posix.files[0].seek(0) st.posix.files[0].length = 29
state.pix在angr中是 angr.state_plugins.posix.SimSystemPosix類,該類的主要作用是用於模擬符合posix環境的數據存儲和輸入輸出。其中files[0]代表着數據的輸入,read_from表示讀取輸入的數據。第3到第8行對輸入的數據進行限制,
最后兩行將指針重新指向開頭並設置長度。
第三種是最通用的方式,直接訪問並修改內存,無論程序是通過何種方式進行輸入,輸入的數據總是在內存中,可以通過對內存進行符號化。具體代碼示例如下:
import angr p = angr.Project('./vul') s = p.factory.blank_state(addr=0x80485c8) bvs = s.se.BVS('to_memory', 8*4) s.se.add(bvs > 1000) s.memory.store(0x08049b80, bvs, endness='Iend_LE') pg = p.factory.path_group(s, immutable=False)
其中 endness有三個值,分別為
Variables: LE – little endian, least significant byte is stored at lowest address BE – big endian, most significant byte is stored at lowest address ME – Middle-endian. Yep.
關於內存操作還可以多說一下,s.memory.store可以用於存儲數據,s.memory.load用於讀取數據.。
五:angr解題步驟:
這里本篇主要利用simulation_manager(老版本為factory_group)求解
#!/usr/bin/env python # -*- coding: utf-8 -*- import angr import claripy angr.l.setLevel('DEBUG') p = angr.Project('./vul', load_options={"auto_load_libs": False}) args = claripy.BVS('args', 8*100) initial_state = p.factory.entry_state(args=[p.filename, args], add_options={'BYPASS_UNSUPPORTED_SYSCALL'}) #pg = p.factory.path_group(initial_state, immutable=False),在新版本被代替,和simlation_manager等效 pg = p.factory.simulation_manager(initial_state) find_addrs = (0x400546, ) avoid_addrs = () pg.explore(find=find_addrs, avoid=avoid_addrs) print pg print ans = pg.found[0].state.se._solver.result.model
print pg.found[0].state.posix.dumps(0) //代表該狀態程序的所有輸入
print pg.found[0].state.posix.dumps(1) //代表該狀態程序的所有輸出
simualtion_manager初始化運行之后,一般有以下幾種狀態
step()表示向下執行一個block(42bytes),step()函數產生active狀態,表示該分支在執行中;
run()表示運行到結束,run()函數產生deadended狀態,表示分支結束;
explore()可以對地址進行限制以減少符號執行遍歷的路徑。例如 sm.explore(find=0x400676,avoid=[0x40073d]) explore()產生found狀態,表示探索的結果等等。也可以使用條件來進行find匹配
1
|
pg.explore(find=lambda s:
"Congrats"
in
s.posix.dumps(1))
|
simulation_manager式angr最重要的控制接口,模擬管理器的最基本功能是利用step()通過一個基本塊將給定存儲中的所有狀態向前推進。當然,如果不對路徑探究過程進行細致研究,只需要使用run()和explore()就好了,在簡述run和explore之前,首先對路徑的stashes進行描述。
當explore()以find參數運行時,程序會一直運行,直到找到與查找條件相匹配的狀態,該條件可以是要停止的指令的地址(地址列表),或者是一些運行時是否符合的狀態。當條件存儲的任何條件與find條件匹配時,他們將置於found存儲中,同時也可以設置avoid,符合avoid條件時也會將其置於avoid中。和run()函數不同的是,run()函數會存儲所有的路徑狀態,而expore()只會存儲find的狀態。
在路徑探索中,一般使用廣度優先算法進行探究,當然我們也可以自由設置使用其它方法,例如深度優先...具體可以使用angr.exploration_techniques