動若脫兔:深入淺出angr--初步理解符號執行以及angr架構


一:概論 

 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM