angr學習筆記
前言
angr 是一個基於符號執行和模擬執行的二進位框架,可以用在很多的場景,比如逆向分析,漏洞挖掘等。本文對他的學習做一個總結。
安裝
這裡介紹 ubuntu 下的安裝,其他平台可以看 官方文檔
首先安裝一些依賴包
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]>), (ulibc.so.6, <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>), (uld-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]: x00x00x00x00x00x00x00x00x00SOSNEAKYx00
In [8]: state2.posix.dumps(0)
Out[8]: x00x00x00x00x00x00x00x00x00Sx00x80Nx00x00 x00x00
獲取進入特定分支, 需要往 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 會把 輸入的字元串 +
保存到緩衝區,legend>>2 為 28)
所以要求輸入的字元串的長度應該為 28 個位元組,且 每個位元組都不是 x00 或者
, 第 29 個位元組為
, 表示輸入完成。
解答
最後的腳本為
#!/usr/bin/env python
import angr
p = angr.Project(wyvern)
st = p.factory.full_init_state()
# 設置 stdin 的約束條件, 使其 前 28 個位元組 不能為 x00 或者
for _ in xrange(28):
k = st.posix.files[0].read_from(1)
st.se.add(k != 0)
st.se.add(k != 10)
# 設置第 29 個位元組為終止符,即為
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 , 最後以
結束 - 最後創建 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 dragons 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 dragons 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 dragons 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/
作者:hackedbylh
歡迎來安全脈搏查看更多的乾貨文章和我們一起交流互動哦!
脈搏地址:安全脈搏 | 分享技術,悅享品質
微博地址:Sina Visitor System
推薦閱讀: