angr學習筆記
前言
angr 是一個基於符號執行和模擬執行的二進位制框架,可以用在很多的場景,比如逆向分析,漏洞挖掘等。本文對他的學習做一個總結。
安裝
這裡介紹 ubuntu 下的安裝,其他平臺可以看 ofollow,noindex">官方文件
首先安裝一些依賴包
sudo apt-get install python-dev libffi-dev build-essential virtualenvwrapper
然後使用
mkvirtualenv angr && pip install angr
即可安裝
建議使用 virtualenv 來安裝,因為 angr 用到的一些庫和正常下的不一樣,直接 pip 安裝可能會安裝不上去
angr常用物件及簡單使用
使用 angr 的大概步驟
-
建立 project
-
設定 state
-
新建 符號量 : BVS (bitvector symbolic ) 或 BVV (bitvector value)
-
把符號量設定到記憶體或者其他地方
-
設定 Simulation Managers , 進行路徑探索的物件
-
執行,探索滿足路徑需要的值
-
約束求解,獲取執行結果
Project物件
介紹與簡單使用
載入二進位制檔案使用 angr.Project 函式,它的第一個引數是待載入檔案的路徑,後面還有很多的可選引數,具體可以看 官方文件
p = angr.Project('./issue', load_options={"auto_load_libs": False})
auto_load_libs 設定是否自動載入依賴的庫,如果設定為 True 的話會自動載入依賴的庫,然後分析到庫函式呼叫時也會進入庫函式,這樣會增加分析的工作量,也有能會跑掛。
載入檔案後,就可以通過 project 物件獲取資訊以及進行後面的操作
In [11]: proj = angr.Project('/bin/true') In [12]: proj.loader.shared_objects Out[12]: OrderedDict([('true', <ELF Object true, maps [0x400000:0x6063bf]>), (u'libc.so.6', <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>), (u'ld-linux-x86-64.so.2', <ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>)]) In [13]: proj = angr.Project('/bin/true', load_options={"auto_load_libs": False}) In [14]: proj.loader.shared_objects Out[14]: OrderedDict([('true', <ELF Object true, maps [0x400000:0x6063bf]>)]) In [15]:
可以看到在使用 {"auto_load_libs": False} 後一些動態連結庫沒有被載入。
有兩個小點還需要了解一下
-
如果 auto_load_libs 為 true, 那麼程式如果呼叫到庫函式的話就會直接呼叫 真正的庫函式 ,如果有的庫函式邏輯比較複雜,可能分析程式就出不來了~~。同時 angr 使用 python 實現了很多的庫函式(儲存在 angr.SIM_PROCEDURES 裡面),預設情況下會使用列表內部的函式來替換實際的函式呼叫,如果不在列表內才會進入到真正的 library.
-
如果 auto_load_libs 為 false , 程式呼叫函式時,會直接返回一個 不受約束的符號值。
hook
我們可以在 angr 中使用 hook 來把指定地址的二進位制程式碼替換為 python 程式碼。angr 在模擬執行程式時,執行每一條指令前會檢測該地址處是否已經被 hook ,如果是就不執行這條語句,轉而執行hook 時指定的 python 處理程式碼。
下面看例項
目標程式地址
https://github.com/angr/angr-doc/tree/master/examples/sym-write
示例指令碼
#!/usr/bin/env python # coding=utf-8 import angr import claripy def hook_demo(state): state.regs.eax = 0 state.regs.ebx = 0xdeadbeef p = angr.Project("./examples/sym-write/issue", load_options={"auto_load_libs": False}) p.hook(addr=0x08048485, hook=hook_demo, length=2) state = p.factory.blank_state(addr=0x0804846B, add_options={"SYMBOLIC_WRITE_ADDRESSES"}) u = claripy.BVS("u", 8) state.memory.store(0x0804A021, u) sm = p.factory.simgr(state) sm.explore(find=0x080484DB) st = sm.found[0] print hex(st.se.eval(st.regs.ebx))
介紹一下指令碼的流程
-
首先 使用 angr.Project 載入檔案, 設定 auto_load_libs 為 false 則不載入依賴的 lib
-
然後 使用 p.hook 把 0x08048485 處的 2 位元組的指令 為 hook_demo,之後執行 0x08048485就會去執行 hook_demo
-
然後建立一個 state , 因為要往記憶體裡面設定 符號量 ( BVS ),設定SYMBOLIC_WRITE_ADDRESSES
-
然後新建一個 8 位長度的符號量,並把它存到 0x0804A021 (全域性變數 u 的位置)
-
然後開始探索路徑,最後求解出使得 程式執行到 you win 程式碼塊的符號量的解。
這裡主要講 p.hook 的處理, 這裡使用了 hook 函式的三個引數
p.hook(addr=0x08048485, hook=hook_demo, length=2)
-
addr 為待 hook 指令的地址
-
hook 為 hook 的處理函式,在執行到 addr 時,會執行 這個函式,同時把 當前的 state 物件作為引數傳遞過去
-
length 為 待 hook 指令的長度,在 執行完 hook 函式以後,angr 需要根據 length 來跳過這條指令,執行下一條指令
在上面的示例中, hook 了 0x08048485 處的指令
這是一條 xor eax, eax 的指令,長度為 2 .
def hook_demo(state): state.regs.eax = 0 state.regs.ebx = 0xdeadbeef
為了做示範,這裡就是把 eax 設定為 0( xor eax,eax 的作用), 然後 設定 ebx 為0xdeadbeef, 因為後續不會用到 ebx , 修改它可以在路徑探索完後檢視這個值是否符合預期。
可以看到 ebx 被修改成了 0xdeadbeef 。
SimState物件
這個物件儲存著程式執行到某一階段的狀態資訊。
通過這個物件可以操作某一執行狀態的上下文資訊,比如記憶體,暫存器等
建立state
In [8]: p = angr.Project("./hello_angr") In [9]: st = p.factory.entry_state() In [10]: st.regs.rsp Out[10]: <BV64 0x7fffffffffeff98> In [11]: st Out[11]: <SimState @ 0x4004a0> In [12]:
首先載入二進位制分析檔案,建立 project 物件,然後建立一個 entry_state , 之後就可以通過 這個 state 物件,獲取或者修改此時程式的執行狀態
entry_state : 做一些初始化工作,然後在 程式的 入口停下
還有一個用的比較多的是
st = p.factory.blank_state(addr=0x4004a0)
這會建立一個 blank_state 物件,這個物件裡面很多東西都是未初始化的,當程式訪問未初始化的資料時,會返回一個不受約束的符號量
基本操作
state 物件一般是作為 符號執行開始前 建立用來 為 後續的執行 初始化一些資料,比如棧狀態,暫存器值。
或者在 路徑探索結束後 ** 返回一個 state 物件供使用者提取需要的值或進行 **約束求解 ,解出到達目標分支所使用的符號量的值。
訪問暫存器
通過 state.regs 物件的屬性訪問以及修改暫存器的資料
In [12]: state.regs.r state.regs.r10 state.regs.r14 state.regs.rax state.regs.rdi state.regs.rip state.regs.r11 state.regs.r15 state.regs.rbp state.regs.rdx state.regs.rsi state.regs.r12 state.regs.r8 state.regs.rbx state.regs.register_default state.regs.rsp state.regs.r13 state.regs.r9 state.regs.rcx state.regs.rflags # 獲取 rip 的值 In [12]: state.regs.rip Out[12]: <BV64 0x400470> # 獲取 rsp 的值 In [13]: state.regs.rsp Out[13]: <BV64 0x7fffffffffeff78> # 獲取 rbp 的值 In [14]: state.regs.rbp Out[14]: <BV64 reg_38_36_64{UNINITIALIZED}> # 設定 rbp = rsp + 0x40 In [15]: state.regs.rbp = state.regs.rsp + 0x40 In [16]: state.regs.rbp Out[16]: <BV64 0x7fffffffffeffb8> # 對於 BVV 和 BVS 都需要通過 solver 進行求解得到具體的值 In [26]: hex(state.se.eval(state.regs.rbp)) Out[26]: '0x7fffffffffeffb8L' In [27]: hex(state.solver.eval(state.regs.rbp)) Out[27]: '0x7fffffffffeffb8L'
訪問記憶體
有兩種方式訪問記憶體,一個是通過 state.mem 使用陣列索引類似的方式進行訪問
In [64]: state.mem[state.regs.rsp].qword Out[64]: <uint64_t <BV64 0x2> at 0x7fffffffffeff78> In [65]: state.mem[state.regs.rsp].qword = 0xdeadbeefdeadbeef In [66]: state.mem[state.regs.rsp].qword Out[66]: <uint64_t <BV64 0xdeadbeefdeadbeef> at 0x7fffffffffeff78> In [67]: m = state.mem[state.regs.rsp] In [68]: m. m.STRONGREF_STATE m.double m.int32_t m.register_default m.ssize m.uint32_t m.wstring m.array m.dword m.int64_t m.resolvable m.ssize_t m.uint64_t m.byte m.example m.int8_t m.resolved m.state m.uint8_t m.char m.float m.long m.set_state m.store m.uintptr_t m.concrete m.init_state m.merge m.set_strongref_state m.string m.void m.copy m.int m.ptrdiff_t m.short m.types m.widen m.deref m.int16_t m.qword m.size_t m.uint16_t m.word
通過 得到的是一個 SimMemView 物件, 可以這個物件的屬性決定按照什麼方式進行記憶體訪問。
In [71]: m.dword # 按照 dword 進行訪問, 4 位元組 Out[71]: <uint32_t <BV32 0xdeadbeef> at 0x7fffffffffeff78> In [72]: m.qword # 按照 qword 進行訪問, 8 位元組 Out[72]: <uint64_t <BV64 0xdeadbeefdeadbeef> at 0x7fffffffffeff78> In [73]: m.int Out[73]: <int (32 bits) <BV32 0xdeadbeef> at 0x7fffffffffeff78> In [74]: m.uin m.uint16_t m.uint32_t m.uint64_t m.uint8_t m.uintptr_t In [74]: m.uint64_t Out[74]: <uint64_t <BV64 0xdeadbeefdeadbeef> at 0x7fffffffffeff78>
這些值如果需要把它轉成python中的基本資料型別
# 通過 .resolved 轉成 BVV 物件 In [75]: state.se.eval(m.qword.resolved) Out[75]: 16045690984833335023L # 通過求解器拿到具體值 In [76]: hex(state.se.eval(m.qword.resolved)) Out[76]: '0xdeadbeefdeadbeefL'
或者可以通過 state.memory 的 load 和 store 來讀取和寫入資料到記憶體
In [90]: data = claripy.BVV(0xaaaaaaaaabbbbbbbbbbbb, 0x20 * 8) In [91]: data Out[91]: <BV256 0xaaaaaaaaabbbbbbbbbbbb> In [92]: state.memory.load(state.regs.rsp, 0x40) Out[92]: <BV512 0xdeadbeefefbeaddec0fffeffffffff07ccfffeffffffff07000000000000000000000000000000001900000000000000ddfffeffffffff070000000000000000> # 存資料存的是 BVV 物件 In [93]: state.memory.store(state.regs.rsp,data) In [94]: state.memory.load(state.regs.rsp, 0x40) Out[94]: <BV512 0xaaaaaaaaabbbbbbbbbbbb00000000000000001900000000000000ddfffeffffffff070000000000000000>
此外還可以往記憶體裡面設定符號變數 (BVS)
In [96]: data = claripy.BVS("data", 0x20 * 8) In [97]: data Out[97]: <BV256 data_37_256> In [98]: state.memory.store(state.regs.rsp,data) In [99]: state.memory.load(state.regs.rsp, 0x40) Out[99]: <BV512 data_37_256 .. 0x1900000000000000ddfffeffffffff070000000000000000#256>
此時還需在建立 state 時 設定 SYMBOLIC_WRITE_ADDRESSES, 例如
state = p.factory.blank_state(addr=0x0804846B, add_options={"SYMBOLIC_WRITE_ADDRESSES"})
模擬執行
用的的程式
https://github.com/angr/angr-doc/tree/master/examples/fauxware
可以通過 state 物件來執行程式碼塊
proj = angr.Project('examples/fauxware/fauxware') state = proj.factory.entry_state() while True: succ = state.step() if len(succ.successors) == 2: break state = succ.successors[0] state1, state2 = succ.successors state1 state2
上面的程式碼就是一直執行直到出現兩個分支時停下
這兩個分支位於 authenticate 函式裡面
然後使用
In [7]: state1.posix.dumps(0) Out[7]: '\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00' In [8]: state2.posix.dumps(0) Out[8]: '\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x00\x80N\x00\x00 \x00\x00'
獲取進入特定分支, 需要往 stdin 輸入的資料。
可以看到這裡如果要進入返回 1 的分支( state1 ),只要往 stdin 輸入 SOSNEAKY ,從而認證通過,這是一個後門密碼。
angr 重寫了一些 libc 的函式,比如獲取 stdin 資料,會返回符號量,用於符號執行,在某個狀態下可以使用 state1.posix.dumps(0) 獲取進入該狀態時 stdin 需要輸入的資料 (0表示的就是 stdin, 1 則是 stdout )。
傳入命令列引數
建立 state 時還可以設定 命令列引數為 符號量 .下面用一個簡單的例子
#include <stdio.h> #include <string.h> int main(int argc, char** argv) { if (!strcmp(argv[1], "hello args test")) { printf("you win!"); } else { printf("you lose!"); } return 0; }
這裡需要傳入一個命令列引數,引數值如果為 hello args test 就會進入 you win 分支
you win 分支所在程式碼塊的地址為 0x400591. 所以我們就需要通過符號執行讓層序執行到0x400591。
指令碼如下
#!/usr/bin/env python # coding=utf-8 import angr import claripy p = angr.Project("./args_test") args = claripy.BVS("args", 8 * 16) state = p.factory.entry_state(args=['./args_test', args]) sm = p.factory.simgr(state) sm.explore(find=0x400591) st = sm.found[0] print st.se.eval(args,cast_to=str)
-
首先載入檔案,然後設定 args 符號量,長度為 16 位元組 ( 8*16 位)
-
然後建立一個 entry_state 同時把 args 作為第一個引數傳給程式。
-
之後建立 simgr 物件進行路徑探索, 指定要走到的目的碼塊 ( 0x400591 )即 win 所在的程式碼塊
-
然後使用 求解物件 st.se.eval(args,cast_to=str) 得到進入到該程式碼塊用到的 符號量的值
cast_to=str, 用於把結果轉成 字串, 否則就是 16 進位制字串
建議使用 ipython 來執行指令碼,執行完後腳本中的物件還會存在 ipython 的上下文中,可以方便做些其他的操作
SimulationManager物件
這個物件用於具體的路徑探索。
以一個簡單的例子開始
程式來自
https://github.com/angr/angr-doc/tree/master/examples/fauxware
解決的程式碼
#!/usr/bin/env python import angr p = angr.Project('fauxware') state = p.factory.entry_state() sm = p.factory.simgr(state) sm.explore(find=0x04007BD) st = sm.found[0] print st.posix.dumps(0).strip("\x00")
-
建立一個 entry_state
-
然後建立 SimulationManager 物件 sm 進行路徑探索
-
指定我們想要走到的位置是 0x04007BD (即認證通過的分支)
-
找到以後從 sm.found[0] 拿到此時的 state, 然後獲取 stdin 輸入的資料,就可以知道走到該分支需要從 stdin 輸入的資料
其他更多請看 https://docs.angr.io/docs/
例項分析
angr 還蒐集了許多使用 angr 解出來的 題目。下面就以其中的一些題來介紹 angr 的使用, 用幾遍就知道大概流程了。
csaw_wyvern
簡單分析
程式位於
https://github.com/angr/angr-doc/tree/master/examples/csaw_wyvern
通過這個題可以瞭解到怎麼往 stdin 裡面放置符號量 以及 設定約束條件
先看看大概邏輯, 首先呼叫 fgets 獲取輸入儲存到 s
.text:000000000040E1D8 lea rcx, [rbp-110h] .text:000000000040E1DF mov esi, 101h ; n .text:000000000040E1E4 mov rdi, rcx ; s .text:000000000040E1E7 mov [rbp-180h], rax .text:000000000040E1EE mov [rbp-188h], rcx .text:000000000040E1F5 call _fgets .text:000000000040E1FA lea rcx, [rbp-120h]
然後進入 start_quest ,對輸入進行處理
這裡判斷輸入字串的長度是不是 28 ( fgets 會把 輸入的字串 + \n 儲存到緩衝區,legend>>2 為 28)
所以要求輸入的字串的長度應該為 28 個位元組,且 每個位元組都不是 \x00 或者 \n , 第 29 個位元組為 \n, 表示輸入完成。
解答
最後的指令碼為
#!/usr/bin/env python import angr p = angr.Project('wyvern') st = p.factory.full_init_state() # 設定 stdin 的約束條件, 使其 前 28 個位元組 不能為 \x00 或者 \n for _ in xrange(28): k = st.posix.files[0].read_from(1) st.se.add(k != 0) st.se.add(k != 10) # 設定第 29 個位元組為終止符,即為 \n k = st.posix.files[0].read_from(1) st.se.add(k == 10) # 使得 檔案指標指向檔案的開頭 st.posix.files[0].seek(0) st.posix.files[0].length = 29 sm = p.factory.simgr(st) sm.run() # 因為是用的 sm.run() 所以要在 sm.deadended 裡面尋找結果 for st in sm.deadended: out = st.posix.dumps(1) if "flag" in out: print out
-
首先建立一個 full_init_state , 因為這裡是 c++ 程式碼, 而 angr 只是 實現了一些常用的 c函式,所以得載入所有的庫, c++ 的函式在底層才會去呼叫 c 的函式。建立 full_init_state後 angr 就會跟進 c++ 函式裡面。
-
然後對 stdin 裡面的前 29 個位元組做約束條件, 前面已經分析過,要求輸入的字串長度為28 , 最後以 \n 結束
-
最後建立 SimulationManager ,然後呼叫 .run() 跑到不能進行跑為止,然後遍歷sm.deadended 檢視 flag, 因為如果輸入正確就會打印出 flag。
執行示例
In [1]: %run my.py WARNING | 2018-05-22 21:58:34,760 | angr.analyses.disassembly_utils | Your verison of capstone does not support MIPS instruction groups. WARNING | 2018-05-22 21:58:41,288 | angr.manager | No completion state defined for SimulationManager; stepping until all states deadend WARNING | 2018-05-22 22:17:50,610 | angr.state_plugins.symbolic_memory | Concretizing symbolic length. Much sad; think about implementing. +-----------------------+ | Welcome Hero | +-----------------------+ [!] Quest: there is a dragon prowling the domain. brute strength and magic is our only hope. Test your skill. Enter the dragon's secret: success [+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}
通過列印的日誌,可以看到大概運行了 20 分鐘。下面介紹兩種加速的方法
**使用 pypy **
pypy 是一個 python 的版本,採用 jit 的方法來提升 python 指令碼的執行速度。
首先安裝
sudo apt install pypy
然後安裝 pip
wget https://bootstrap.pypa.io/get-pip.py sudo pypy get-pip.py
然後使用 pip 安裝 angr
sudo pip install angr
然後使用 pypy 來執行指令碼即可
23:52 haclh@ubuntu:csaw_wyvern $ pypy my.py WARNING | 2018-05-22 23:52:09,645 | angr.analyses.disassembly_utils | Your verison of capstone does not support MIPS instruction groups. WARNING | 2018-05-22 23:52:17,667 | angr.manager | No completion state defined for SimulationManager; stepping until all states deadend WARNING | 2018-05-22 23:58:23,344 | angr.state_plugins.symbolic_memory | Concretizing symbolic length. Much sad; think about implementing. +-----------------------+ | Welcome Hero | +-----------------------+ [!] Quest: there is a dragon prowling the domain. brute strength and magic is our only hope. Test your skill. Enter the dragon's secret: success [+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}
此時只用了大概 6 分鐘就跑完了。
使用 unicorn
還可以設定 angr 的選項,使用 unicorn 引擎來做模擬執行
#!/usr/bin/env python import angr p = angr.Project('wyvern') st = p.factory.full_init_state(add_options=angr.options.unicorn) for _ in xrange(28): k = st.posix.files[0].read_from(1) st.se.add(k != 0) st.se.add(k != 10) k = st.posix.files[0].read_from(1) st.se.add(k == 10) st.posix.files[0].seek(0) st.posix.files[0].length = 29 sm = p.factory.simgr(st) sm.run() for st in sm.deadended: out = st.posix.dumps(1) if "flag" in out: print out
然後再跑一次
23:58 haclh@ubuntu:csaw_wyvern $ pypy my.py WARNING | 2018-05-22 23:59:26,853 | angr.analyses.disassembly_utils | Your verison of capstone does not support MIPS instruction groups. WARNING | 2018-05-22 23:59:35,539 | angr.manager | No completion state defined for SimulationManager; stepping until all states deadend WARNING | 2018-05-23 00:03:58,458 | angr.state_plugins.symbolic_memory | Concretizing symbolic length. Much sad; think about implementing. +-----------------------+ | Welcome Hero | +-----------------------+ [!] Quest: there is a dragon prowling the domain. brute strength and magic is our only hope. Test your skill. Enter the dragon's secret: success [+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}
只用了 3 分鐘就跑完了。
總結
-
對於 c++ 的程式,如果呼叫了 c++ 的函式,使用 full_init_state
-
如果通過 sm.run() 來探索路徑,最後遍歷 sm.deadended 檢視結果
-
可以通過 st.posix.files[0] 對 stdin 做約束
-
可以使用 pypy 和 unicorn 來加速指令碼的執行
cmu_binary_bomb
這個是 cmu 給學生練習逆向分析能力的一個題,相信大多數計算機專業的都做過這東西。今天看看怎麼用 angr 來解決它。
程式檔案位於
https://github.com/angr/angr-doc/tree/master/examples/cmu_binary_bomb
這個例子主要用於介紹 使用 angr 設定 記憶體符號量。
以第一個關卡為例
這個就是判斷一個字串。
於是我們把 一塊記憶體設定為符號量, 然後把第一個引數設定為符號量的地址即可
import angr import claripy proj = angr.Project('bomb', load_options={'auto_load_libs': False}) start = 0x400ee0 bomb_explode = 0x40143a end = 0x400ef7 # initial state is at the beginning of phase_one() # 設定選項讓 angr 支援 記憶體符號量 state = proj.factory.blank_state(addr=start) state.options |= angr.options.unicorn state.options |= {"SYMBOLIC_WRITE_ADDRESSES"} # 初始化記憶體符號量, 128 位元組 arg = state.se.BVS("input_string", 8 * 128) bind_addr = 0x603780 # 符號量存在 記憶體中, 這塊記憶體就變成了符號量 state.memory.store(bind_addr, arg) # 設定 rdi (第一個引數為符號量的地址) state.regs.rdi = bind_addr sm = proj.factory.simgr(state) sm.explore(find=end, avoid=bomb_explode) st = sm.found[0] # 求解符號量 print st.se.eval(arg, cast_to=str)
流程就是
-
首先設定選項,讓 angr 支援 記憶體符號量
-
然後初始化一塊 128 位元組的符號量記憶體,並把符號量存到 0x603780, 此時 0x603780 處是一塊符號量記憶體
-
然後把記憶體符號量的地址設定為引數傳個第一關函式 0x400ee0
參考
https://github.com/axt/angr-utils
http://ysc21.github.io/blog/2016-01-27-angr-script.html
http://docs.angr.io/