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 輸入的數據

其他更多請看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

參考

github.com/axt/angr-uti

ysc21.github.io/blog/20

docs.angr.io/

作者:hackedbylh

歡迎來安全脈搏查看更多的乾貨文章和我們一起交流互動哦!

脈搏地址:安全脈搏 | 分享技術,悅享品質

微博地址:Sina Visitor System


推薦閱讀:

TAG:筆記 | 漏洞 | Ubuntu |