controlling angr
怒りを制御する
アドカレの丁度いいネタも無く、やる気も無く、普通にCTFやりたいなと思っていたところ、angr Documentationの日本語訳のようなものを下書きから見つけたので供養します。
angr-docについての日本語の情報があまり無い&&あっても古いものが多いので,自分の為に記録していたものです。が、これももう古い。
正直これ読むよりdeepl片手に公式読んだほうがいいと思います。
2021/06時点での情報.
agenda
- Introduction
- Get_Started
- Top_Level_Interfaces
- Loading_a_Binary
- Solver_Engine
- Program_State
- imulation_Managers
- Execution_Engines
- Analyses
- CFG
- Backward_Slicing
- Function_Identifier
- Advanced_Topics
- Gotchas
- The_Whole_Pipeline
- The_Mixin_Pattern
- Optimizing_Symbolic_Execution
Introduction
angrは,マルチアーキテクチャに対応したバイナリ解析用のツールキットで,バイナリに対して動的なシンボリック実行や,様々な静的解析を行う事ができる.
バイナリ解析にはいくつかの課題があり,それらは大まかに
- バイナリを解析プログラムに読み込む
- バイナリを中間表現に変換する
- 実際に以下のような解析を行う
- プログラムの一部,またはすべてを対象とした静的解析を行う(依存関係の解析,プログラムのスライスなど)
- プログラムの状態空間のシンボリックな探索(例: オーバーフローが見つかるまで実行できるか)
- 上記の組み合わせ(例: オーバーフローを見つける為に,メモリ書き込みにつながるプログラムスライスのみを実行する など)
に分類できる。
angrには,これらの課題をすべて満たすコンポーネントがある.
Get_Started
インストール方法などは公式からアナウンスされている最新の情報を参照したほうが良い.
Dependencies
on Ubuntu
sudo apt-get install python3-dev libffi-dev build-essential virtualenvwrapper
Top_Level_Interfaces
Before_You_Start
IPython
(または他のPythonコマンドラインインタプリタ)でangrを使うのが便利.
しかし,IPythonのタブ補完は便利な一方,遅い事があるので,以下の回避策を実行すると良い.
# Drop this file in IPython profile's startup directory to avoid running it every time.
import IPython
py = IPython.get_ipython()
py.Completer.use_jedi = False
Core_Concepts
angrを使い始める前に,angrの基本的な概念と,基本的なangrオブジェクトの構築方法についての基本的な概要を知っておく必要がある.
ここでは,バイナリをロードした後に直接利用できる機能について説明する.
angrを使って最初にすることは,プロジェクトにバイナリをロードすることである.ここでは/bin/true
を例とする
>>> import angr
>>> proj = angr.Project('/bin/true')
Project
は,angrの管理拠点となる.Project
を使って,ロードしたバイナリに対して,解析やシミュレーションを行う事ができる.
angrで扱うほとんどのオブジェクトは,何らかの形でProject
に依存している.
Basic_properties
Project
の基本的なプロパティとして,CPUアーキテクチャ,ファイル名,エントリーポイントのアドレスがある.
>>> import monkeyhex # this will format numerical results in hexadecimal
>>> proj.arch
<Arch AMD64 (LE)>
>>> proj.entry
0x401670
>>> proj.filename
'/bin/true'
arch
はarchinfo.Arch
オブジェクトのインスタンスで,これにはarch.bits
やarch.bytes
,arch.name
,arch.memory_endness
が含まれるentry
はバイナリのエントリーポイントfilename
はバイナリの絶対パス
The_loader
バイナリから仮想アドレス空間での表現での表現に至るまでは非常に複雑である.
これを処理する為に,CLE
というモジュールがある.
CLE
の処理結果はloaderと呼ばれ,.loader
プロパティで確認できる.
このプロパティについては,今の所,プログラムと一緒に読み込まれた共有ライブラリを確認したり,読み込まれたアドレス空間に関する基本的なクエリを実行したりできる事を確認する.
topleveo
ader
<Loaded true, maps [0x400000:0x5004000]>
>>> proj.loader.shared_objects # may look a little different for you!
{'ld-linux-x86-64.so.2': <ELF Object ld-2.24.so, maps [0x2000000:0x2227167]>,
'libc.so.6': <ELF Object libc-2.24.so, maps [0x1000000:0x13c699f]>}
>>> proj.loader.min_addr
0x400000
>>> proj.loader.max_addr
0x5004000
>>> proj.loader.main_object # we've loaded several binaries into this project. Here's the main one!
<ELF Object true, maps [0x400000:0x60721f]>
>>> proj.loader.main_object.execstack # sample query: does this binary have an executable stack?
False
>>> proj.loader.main_object.pic # sample query: is this binary position-independent?
True
The_factory
angrには多くのクラスがあるが,そのほとんどがインスタンス化の為にproject
を必要とする.
projectをいくつも配置するのではなく,project.factory
を提供している.
これには,頻繁に使用する一般的なオブジェクトの為の便利なコンストラクタがいくつか用意されている.
Blocks
まず,project.factory.block()
は,与えられたアドレスからコードのbasic blockを抽出する為の物.
重要な点として,angrはコードをbasic block単位で分析する.
得られるのはBlock
オブジェクトで,コードのブロックについての様々な情報を持っている.
>>> block = proj.factory.block(proj.entry) # lift a block of code from the program's entry point
<Block for 0x401670, 42 bytes>
>>> block.pp() # pretty-print a disassembly to stdout
0x401670: xor ebp, ebp
0x401672: mov r9, rdx
0x401675: pop rsi
0x401676: mov rdx, rsp
0x401679: and rsp, 0xfffffffffffffff0
0x40167d: push rax
0x40167e: push rsp
0x40167f: lea r8, [rip + 0x2e2a]
0x401686: lea rcx, [rip + 0x2db3]
0x40168d: lea rdi, [rip - 0xd4]
0x401694: call qword ptr [rip + 0x205866]
>>> block.instructions # how many instructions are there?
0xb
>>> block.instruction_addrs # what are the addresses of the instructions?
[0x401670, 0x401672, 0x401675, 0x401676, 0x401679, 0x40167d, 0x40167e, 0x40167f, 0x401686, 0x40168d, 0x401694]
さらに,Block
オブジェクトを使って,コードのブロックの他の表現を得る事もできる.
>>> block.capstone # capstone disassembly
<CapstoneBlock for 0x401670>
>>> block.vex # VEX IRSB (that's a python internal address, not a program address)
<pyvex.block.IRSB at 0x7706330>
States
Project
オブジェクトは,プログラムの初期化されたイメージを表しているに過ぎない.
angrを実行する際には,シミュレーションされたプログラムの状態を表す特定のオブジェクト(SimState
)を扱う事になる.
>>> state = proj.factory.entry_state()
<SimState @ 0x401670>
SimState
には,プログラムのメモリ,レジスタ,ファイルシステムのデータ等,実行によって変更可能なあらゆるlive dataがstateに含まれている.
state
の操作方法については後ほど扱うが,今はstate.regs
とstate.mem
を使ってレジスタとメモリにアクセスしてみる.
>>> state.regs.rip # get the current instruction pointer
<BV64 0x401670>
>>> state.regs.rax
<BV64 0x1c>
>>> state.mem[proj.entry].int.resolved # interpret the memory at the entry point as a C int
<BV32 0x8949ed31>
得られるのはただの整数でなく,bitvectorsである.
Pythonの整数は,オーバーフロー時のwrap等,CPU上と同じ挙動ではない.
そのため,bitvectors
を使用してCPU上のデータを表現している.
各bitvectorsは,bit単位での幅を表す.length
を持っている.
bitvectors
の扱いについては後ほど扱うが,ここではPythonの整数からbitvectors
への変換方法を説明する.
>>> bv = state.solver.BVV(0x1234, 32) # create a 32-bit-wide bitvector with value 0x1234
<BV32 0x1234> # BVV stands for bitvector value
>>> state.solver.eval(bv) # convert to python int
0x1234
このbitvectors
をレジスタやメモリに格納する事もできるし,Pythonの整数を直接格納することもできる.
その場合,適切なサイズのbitvectorsに変換される.
>>> state.regs.rsi = state.solver.BVV(3, 64)
>>> state.regs.rsi
<BV64 0x3>
>>> state.mem[0x1000].long = 4
>>> state.mem[0x1000].long.resolved
<BV64 0x4>
mem
はかなり複雑な為,簡単に説明すると,
- アドレスの指定には
array[index]
を用いる .<type>
を使って,メモリを<type>
として解釈する事ができる(char, short, int, long, size_t, uint8_t, uint16_t ...
)- 以下のように用いる事ができる
bitvectors
または通常の整数値を格納するbitvectors
としての値を取得するには.resolved
を使用する.concrete
を使用すると,通常の整数値が得られる
さらに高度な使い方については後ほど扱う. 最後に,もう少しレジスタを読んで見ると,奇妙な値が得られる事がある.
>>> state.regs.rdi
<BV64 reg_48_11_64{UNINITIALIZED}>
これも,64bitのbitvectorsだが,値が含まれておらず,代わりに名前がついている. これはシンボリック変数と呼ばれ,シンボリック実行の基礎となる. これらの詳細については後ほど扱う.
Simulation Managers
ある時点でのプログラムを表す事ができるstateであれば,それを次の時点に移す方法もある.
Simulation Managerは,stateを使って実行やシミュレーションを行うための主要なインターフェイスである.
簡単な導入として,いくつかの基本的なブロックを前にして,先程作成したstateをどのように動かすかを示す.
まず,これから使用するSimulation Managerを作成する.コンストラクタには,state
またはそのリストを渡すことができる.
>>> simgr = proj.factory.simulation_manager(state)
<SimulationManager with 1 active>
>>> simgr.active
[<SimState @ 0x401670>]
Simulation Managerには,複数の状態のstashを含める事ができる.
デフォルトのstashであるactive
は,渡された状態で初期化されている.
simgr.active[0]
を見ることで,状態確認する事ができる.
さて,実行してみる.
>>> simgr.step()
これで,basic blockに相当するシンボリックな実行が行われる.
アクティブなstashを再び見ると,更新されている事,さらに,元の状態は変更されていない事がわかる.
SimState
オブジェクトは,実行によってimmutableに扱われる.
単一の状態を,複数の実行ラウンドのベースとして安全に使用する事ができる.
>>> simgr.active
[<SimState @ 0x1020300>]
>>> simgr.active[0].regs.rip # new and exciting!
<BV64 0x1020300>
>>> state.regs.rip # still the same!
<BV64 0x401670>
/bin/true
はシンボリック実行で面白いことをする方法を説明するのにあまり良い例ではないので,今はここで止めておく.
analyses
angrには,プログラムから情報を引き出すのに使える,いくつかの分析機能が予め組み込まれている.
>>> proj.analyses. # Press TAB here in ipython to get an autocomplete-listing of everything:
proj.analyses.BackwardSlice proj.analyses.CongruencyCheck proj.analyses.reload_analyses
proj.analyses.BinaryOptimizer proj.analyses.DDG proj.analyses.StaticHooker
proj.analyses.BinDiff proj.analyses.DFG proj.analyses.VariableRecovery
proj.analyses.BoyScout proj.analyses.Disassembly proj.analyses.VariableRecoveryFast
proj.analyses.CDG proj.analyses.GirlScout proj.analyses.Veritesting
proj.analyses.CFG proj.analyses.Identifier proj.analyses.VFG
proj.analyses.CFGEmulated proj.analyses.LoopFinder proj.analyses.VSA_DDG
proj.analyses.CFGFast proj.analyses.Reassembler
これらのうちいくつかは本書の後半で説明されているが,一般には,特定の分析の使用方法を知りたい場合はapi documentationを参照すると良い. 非常に簡単な例として,バイナリからCFGを生成し,使用する方法を示す.
# Originally, when we loaded this binary it also loaded all its dependencies into the same virtual address space
# This is undesirable for most analysis.
>>> proj = angr.Project('/bin/true', auto_load_libs=False)
>>> cfg = proj.analyses.CFGFast()
<CFGFast Analysis Result at 0x2d85130>
# cfg.graph is a networkx DiGraph full of CFGNode instances
# You should go look up the networkx APIs to learn how to use this!
>>> cfg.graph
<networkx.classes.digraph.DiGraph at 0x2da43a0>
>>> len(cfg.graph.nodes())
951
# To get the CFGNode for a given address, use cfg.get_any_node
>>> entry_node = cfg.get_any_node(proj.entry)
>>> len(list(cfg.graph.successors(entry_node)))
2
Now what?
ここまでの説明を読めば,angrのbasic block,state,bitvectors,simulation manager,analysesなどの重要な概念を理解できる. しかし,angrをただのデバッガとして使う以外にも面白い事をするためには,この先まで読み進める必要がある.
Loading_a_Binary
前章では,angrのローディング機能のほんの一部を解説した.
/bin/true
をロードし,共有ライブラリを除いて再度ロードを行った.
また,proj.loader
とその機能をいくつか紹介した.
ここからは,これらのインターフェイスの微妙な違いと,それらから得られる情報について説明する.
angrでバイナリを読み込むコンポーネントであるCLEについても簡単に触れたが,CLEとは,CLE Loads Everythingの略で,バイナリ(及び依存しているライブラリ)を受け取り,作業しやすい形でangrの他の部分に提示する役割を担っている.
The_Loader
examples/fauxware/fauxwareをロードして,インタラクティブなローダの活用方法を見てみる.
>>> import angr, monkeyhex
>>> proj = angr.Project('examples/fauxware/fauxware')
>>> proj.loader
<Loaded fauxware, maps [0x400000:0x5008000]>
Loaded Objects
CLEのローダー(cle.Loader
)は,ロードされたバイナリオブジェクトの集合体全体を表し,1つのメモリ空間にロードされ,マッピングされる.
各バイナリオブジェクトは,そのファイルタイプを扱えるローダーバックエンド(cle.Backend
のサブクラス)によってロードされる.
例えば,cle.ELF
は,ELFバイナリのロードに使用される.
また,メモリ内には,ロードされたバイナリに対応しないオブジェクトも存在する. 例えば,thread-localなストレージのサポートに使用されるオブジェクトや,未解決のシンボルを提供するために使用されるexternsオブジェクトなどがある.
CLEがロードしたオブジェクトの全リストは,loader.all_objects
で得る事ができ,さらにいくつかの対象分類もある.
# All loaded objects
>>> proj.loader.all_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
<ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>,
<ELFTLSObject Object cle##tls, maps [0x3000000:0x3015010]>,
<ExternObject Object cle##externs, maps [0x4000000:0x4008000]>,
<KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>]
# This is the "main" object, the one that you directly specified when loading the project
>>> proj.loader.main_object
<ELF Object fauxware, maps [0x400000:0x60105f]>
# This is a dictionary mapping from shared object name to object
>>> proj.loader.shared_objects
{ 'fauxware': <ELF Object fauxware, maps [0x400000:0x60105f]>,
'libc.so.6': <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
'ld-linux-x86-64.so.2': <ELF Object ld-2.23.so, maps [0x2000000:0x2227167]> }
# Here's all the objects that were loaded from ELF files
# If this were a windows program we'd use all_pe_objects!
>>> proj.loader.all_elf_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
<ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>]
# Here's the "externs object", which we use to provide addresses for unresolved imports and angr internals
>>> proj.loader.extern_object
<ExternObject Object cle##externs, maps [0x4000000:0x4008000]>
# This object is used to provide addresses for emulated syscalls
>>> proj.loader.kernel_object
<KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>
# Finally, you can to get a reference to an object given an address in it
>>> proj.loader.find_object_containing(0x400000)
<ELF Object fauxware, maps [0x400000:0x60105f]>
これらのオブジェクトを直接操作して,オブジェクトからメタデータを抽出する事ができる.
>>> obj = proj.loader.main_object
# The entry point of the object
>>> obj.entry
0x400580
>>> obj.min_addr, obj.max_addr
(0x400000, 0x60105f)
# Retrieve this ELF's segments and sections
>>> obj.segments
<Regions: [<ELFSegment memsize=0xa74, filesize=0xa74, vaddr=0x400000, flags=0x5, offset=0x0>,
<ELFSegment memsize=0x238, filesize=0x228, vaddr=0x600e28, flags=0x6, offset=0xe28>]>
>>> obj.sections
<Regions: [<Unnamed | offset 0x0, vaddr 0x0, size 0x0>,
<.interp | offset 0x238, vaddr 0x400238, size 0x1c>,
<.note.ABI-tag | offset 0x254, vaddr 0x400254, size 0x20>,
...etc
# You can get an individual segment or section by an address it contains:
>>> obj.find_segment_containing(obj.entry)
<ELFSegment memsize=0xa74, filesize=0xa74, vaddr=0x400000, flags=0x5, offset=0x0>
>>> obj.find_section_containing(obj.entry)
<.text | offset 0x580, vaddr 0x400580, size 0x338>
# Get the address of the PLT stub for a symbol
>>> addr = obj.plt['strcmp']
>>> addr
0x400550
>>> obj.reverse_plt[addr]
'strcmp'
# Show the prelinked base of the object and the location it was actually mapped into memory by CLE
>>> obj.linked_base
0x400000
>>> obj.mapped_base
0x400000
Symbols and Relocations
CLEを使用しながらシンボルを扱う事もできる. シンボルはexectable formatの世界では基本的な概念であり,名前とアドレスを効果的にマッピングする.
CLEからシンボルを取得する最も簡単な方法は,loader.find_symbol
で,これは名前とアドレスのどちらかを受け取り,Symbol
オブジェクトを返す.
>>> strcmp = proj.loader.find_symbol('strcmp')
>>> strcmp
<Symbol "strcmp" in libc.so.6 at 0x1089cd0>
シンボルの最も便利な属性は,名前所有者,アドレスだが,シンボルのアドレスは曖昧な場合がある.
Symbol
オブジェクトは3つの方法でそのアドレスを示す.
.rebased_addr
は,グローバルアドレス空間におけるそのアドレスである..linked_addr
は,バイナリのリンク前のベースアドレスである.readelf(1)
で得られるアドレスと同じ..relative_addr
は,オブジェクトベースからの相対的なアドレスである.これは,主にWindowsのコンテキストではRVA(relative virtual address)
として知られている.
>>> strcmp.name
'strcmp'
>>> strcmp.owner
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>
>>> strcmp.rebased_addr
0x1089cd0
>>> strcmp.linked_addr
0x89cd0
>>> strcmp.relative_addr
0x89cd0
シンボルは,デバッグ情報の提供だけでなく,ダイナミックリンクの概念もサポートしている.
libc
はstrcmp
シンボルをexportとして提供しており,メインのバイナリはこれに依存している.
CLEにメインオブジェクトから直接strcmp
シンボルを与えるような設定をすると,これはimport symbolであるという情報が得られる.
import symbolには,意味のあるアドレスは関連付けられていないが,.resolvedby
のように,シンボルの解決に使用されたシンボルへの参照が提供される.
>>> strcmp.is_export
True
>>> strcmp.is_import
False
# On Loader, the method is find_symbol because it performs a search operation to find the symbol.
# On an individual object, the method is get_symbol because there can only be one symbol with a given name.
>>> main_strcmp = proj.loader.main_object.get_symbol('strcmp')
>>> main_strcmp
<Symbol "strcmp" in fauxware (import)>
>>> main_strcmp.is_export
False
>>> main_strcmp.is_import
True
>>> main_strcmp.resolvedby
<Symbol "strcmp" in libc.so.6 at 0x1089cd0>
インポートとエクスポートのリンクを具体的にどのようにメモリに登録すべきかは,relocationsという別の概念で処理される.
relocationとは,「[import]とexportのシンボルが一致したら,exportのアドレスを[location]に[format]の形式で書く」というような物.
オブジェクトに対するリロケーションの完全なリスト(Relocation
のインスタンス)は,obj.relocs
として,シンボル名とリロケーションのマッピングだけをobj.imports
として見る事ができる.対応するexport symbolのリストはない.
リロケーションに対応するimport symbolは,.symbol
としてアクセスできる.
リロケーションが書き込むアドレスは,Symbol
に使用できるアドレス識別子のどれでもアクセス可能で,リロケーションを要求するオブジェクトへの参照も.owner
で取得できる.
# Relocations don't have a good pretty-printing, so those addresses are python-internal, unrelated to our program
>>> proj.loader.shared_objects['libc.so.6'].imports
{'__libc_enable_secure': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fce780>,
'__tls_get_addr': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT at 0x7ff5c6018358>,
'_dl_argv': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fd2e48>,
'_dl_find_dso_for_object': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT at 0x7ff5c6018588>,
'_dl_starting_up': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fd2550>,
'_rtld_global': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fce4e0>,
'_rtld_global_ro': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fcea20>}
共有ライブラリが見つからない等の理由で,importがどのexportにも解決できない場合,CLEは自動的にexternsオブジェクト(loader.extern_obj
)を更新して,シンボルをexportとして提供する.
Loading_Options
angr.Project
を使って何かをロードしている時,Project
が暗黙のうちに生成するcle.Loader
インスタンスにオプションを渡したい場合,Project
のコンストラクタにキーワード引数を直接渡す事で,CLEにオプションを指定できる.
オプションとして渡せるものをすべて知りたい場合は,CLE APIのドキュメントを参照する必要があるが,ここでは重要かつ頻繁に使用される物について説明する.
Basic Options
auto_load_libs
については既に説明している.
これは,CLEが共有ライブラリの依存関係を自動的に解決しようとする機能を有効または無効化するもので,デフォルトでは有効化されている.
さらに,逆の機能として,expect_missing_libs
がある.これをtrue
に設定すると,バイナリに解決できない依存関係がある場合に例外がthrow
される.
force_load_libs
に文字列のリストを渡すと,リストに記載されているものはすべて未解決の共有ライブラリの依存関係として最初から扱われる.
また,skip_libs
に文字列のリストを渡すと,その名前のライブラリが依存関係として解決されるのを防ぐ事が出来る.
さらに,文字列のリスト(または単一の文字列)をld_path
に渡す事ができる.これは,ロードされたプログラムと同じディレクトリ,cwd,システムのライブラリといったデフォルトのパスの前に,共有ライブラリの追加の検索パスとして使用される.
Per-Binary Options
CLEでは,特定のバイナリオブジェクトのみに適用されるオプションを指定することも可能.
main_opts
とlib_opts
に,オプションのdictを渡す事でそれが出来る.
main_opts
はオプション名からオプション値へのマッピングで,lib_opts
はライブラリ名からオプション名とオプション値をマッピングするdictへのマッピング.
使用できるオプションはバックエンド毎に異なるが,一般的なものは以下の通り.
backend
どのバックエンドを用いるかを,クラス名または名前で指定するbase_addr
ベースアドレスの指定entry_point
エントリーポイントの指定arch
使用するアーキテクチャの指定
>>> angr.Project('examples/fauxware/fauxware', main_opts={'backend': 'blob', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
<Project examples/fauxware/fauxware>
Backends
CLEには現在,ELF,PE,CGC,Mach-O,ELF core dumpを静的にロードするバックエンドと,フラットなアドレス空間にロードするバックエンドがある. ほとんどの場合,CLEは指定すべきバックエンドを自動的に検出するため,よほど特殊な場合で無い限り,どのバックエンドを使用するか指定する必要はない.
CLEでは,上述のようにオプションにdictのkeyを含める事で,あるオブジェクトに特定のバックエンドを使用させることができる.
バックエンドの中には,どのアーキテクチャを使用するかを自動検出できないものもあり,その場合はarch
を指定する必要がある.
angrは,サポートされているアーキテクチャの共通識別子があれば,どのアーキテクチャを指しているのかを識別できる.
バックエンドを参照するには以下の表の名前を使用する
backend name | description | requires arch ? |
---|---|---|
elf | PyELFToolsベースのELF用スタティックローダ | no |
pe | PEFileベースのPEファイル用スタティックローダ | no |
mach-o | Mach-Oファイル用のスタティックローダ.ダイナミックリンクやリベースには非対応 | no |
cgc | Cyber Grand Challengeバイナリ用のスタティックローダ | no |
backendcgc | メモリとレジスタのバッカーを指定できるCGCバイナリ用のスタティックローダ | no |
elfcore | ELF core dump用のスタティックローダ | no |
blob | ファイルをフラットイメージとしてメモリにロードする | yes |
Symbolic_Function_Summaries
デフォルトでは,Projectはライブラリ関数の外部呼び出しをSimProceduresと呼ばれるシンボリックなサマリ(事実上,ライブラリ関数のstateへの影響を模倣するPython関数)を使って置き換えようとする.
angrでは,一連の関数をSimProcedures
として実装している.
これらのビルトインプロシージャは,angr.SIM_PROCEDURES
dictで利用できる.
このdictは,2つのレベルで構成されており,最初にパッケージ名(libc, posix, win32, stubs),次にライブラリ関数の名前をキーにしている.
システムからロードされる実際のライブラリ関数ではなく,SimProceduresを実行することで,潜在的な不正確さを犠牲にしても,分析が非常に用意になる.
ある機能について,サマリが無い場合
auto_load_libs
がTrue
の場合(デフォルト),代わりに実際のライブラリ関数が実行される.これは,実際の関数に応じて望ましいものであるかどうかを判断する.例えば,libcの関数の中には解析が非常に複雑なものがあり,それを実行しようとするとパスの状態数が爆発的に増加する可能性がある.auto_load_libs
がFalse
の場合,外部の関数は未解決であり,ProjectはReturnUnconstrained
と呼ばれる一般的なstub SimProceduresに解決する.これは,その名の通り,呼び出される度にユニークな誓約の無いシンボリックな値を返す.use_sim_procedures
(これは,cle.Loader
ではなく,angr.Project
のパラメタ)がFalse
の場合(デフォルトではTrue
),externオブジェクトいよって提供されるシンボルのみがSimProcedures
に置き換えられ,シンボル値を返すだけで何もしないstubReturnUnconstrained
に置き換えられる.angr.Project
のパラメタであるexclude_sim_procedures_list
とexclude_sim_procedures_func
を使用して,SimProcedures
に置き換えられないように,特定のシンボルを指定することができる.- 正確なアルゴリズムは,
angr.Project._register_object
のコードを確認するとわかる.
Hooking
angrがライブラリのコードをPythonのサマリで置き換えるメカニズムはhookingと呼ばれており,自分で行う事も出来る.
シミュレーションを行う際,angrは各ステップで,現在のアドレスがhookされているかをチェックし,フックされている場合のみ,そのアドレスのバイナリの代わりにフックを実行する.
これを行うためのAPIはproj.hook(addr, hook)
で,hook
はSimProceduresのインスタンス.
プロジェクトのhookは,.is_hooked
,.unhook
,.hooked_by
で管理できる.
アドレスをフックする為の別のAPIとして,関数デコレータとしてのproj.hook(addr)
を使用することで,フックとして使用する独自のoff-the-cuff関数を指定できるものがある.
この場合,オプションでlength
を指定して,フックが終了した後にRIPを何バイトか先に進める事もできる.
>>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # this is a CLASS
>>> proj.hook(0x10000, stub_func()) # hook with an instance of the class
>>> proj.is_hooked(0x10000) # these functions should be pretty self-explanitory
True
>>> proj.hooked_by(0x10000)
<ReturnUnconstrained>
>>> proj.unhook(0x10000)
>>> @proj.hook(0x20000, length=5)
... def my_hook(state):
... state.regs.rax = 1
>>> proj.is_hooked(0x20000)
True
さらに,proj.hook_symbol(name, hook)
を使って,シンボルの名前を第一引数として与え,シンボルが存在するアドレスをフックする事ができる.
非常に重要な使い方の一つは,angrの組み込みライブラリであるSimProceduresの動作を拡張することである.
これらのライブラリ関数は単なるクラスなので,それらをサブクラス化して動作の一部をオーバーライドし,サブクラスをフックで使用する事ができる.
So far so good!
ここまでで,CLEローダとangrプロジェクトのレベルで解析を行う環境をコントロールする方法について,ある程度理解できた. また,angrは複雑なライブラリ関数を,その関数の効果を要約すSimProceduresにフックする事で,解析を簡素化しようとしている事も理解できた.
CLEローダとそのバックエンドで出来る事をすべて確認するには,CLE APIドキュメントを参照すると良い.
Solver_Engine
angrの威力は,エミュレータである事でなく,シンボリック変数とよばれるものを使って実行できる事にある. 変数が具体的な数値を持っているのではなく,シンボル,つまり名前を持っていると言える. そして,その変数で算術演算を行うと,演算の木(コンパイラの文脈で言うAST)が出来る. ASTは,z3の様なSMTソルバの制約条件に変換する事ができ,「この一連の操作の出力を考えると,入力を何でなければならないか」といった問題にできる. ここでは,この問題に答えるためのangrの使い方を学ぶ.
Working_with_Bitvectors
ダミーのprojectとstateを用意し,数字で遊んでみる.
>>> import angr, monkeyhex
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()
bitvectorとは,単なるビット列であり,算術の為の有界整数のセマンティクスで解釈される. いくつか例を作ってみる.
# 64-bit bitvectors with concrete values 1 and 100
>>> one = state.solver.BVV(1, 64)
>>> one
<BV64 0x1>
>>> one_hundred = state.solver.BVV(100, 64)
>>> one_hundred
<BV64 0x64>
# create a 27-bit bitvector with concrete value 9
>>> weird_nine = state.solver.BVV(9, 27)
>>> weird_nine
<BV27 0x9>
このように,任意のビット列をbitvectorと呼ぶ事ができる. また,これらを使って計算を行う事もできる.
>>> one + one_hundred
<BV64 0x65>
# You can provide normal python integers and they will be coerced to the appropriate type:
>>> one_hundred + 0x100
<BV64 0x164>
# The semantics of normal wrapping arithmetic apply
>>> one_hundred - one*200
<BV64 0xffffffffffffff9c>
しかし,one + weird_nine
はできない.
長さの異なるbitvectorに対しての演算は型エラーとなる.
しかし,weird_nine
を適切なビット長になるように拡張する事はできる.
>>> weird_nine.zero_extend(64 - 27)
<BV64 0x9>
>>> one + weird_nine.zero_extend(64 - 27)
<BV64 0xa>
zero_extend
は,左のbitvectorを指定された数のゼロビットで埋める.
また,sign_extended
を使用すると,最上位ビットの複製でパディングを行い,2の補数符号付き整数セマンティクスの下でbitvectorの値を保持する事ができる.
ここでいくつかのシンボルを導入してみる.
# Create a bitvector symbol named "x" of length 64 bits
>>> x = state.solver.BVS("x", 64)
>>> x
<BV64 x_9_64>
>>> y = state.solver.BVS("y", 64)
>>> y
<BV64 y_10_64>
これで,x
とy
はシンボリック変数となった.
これは,代数における変数のようなもの.
提供した名前は,インクリメントするカウンタを追加することによって破壊されている点に注意が必要.
それらで好きなだけ算術を行う事ができるが,数値は返ってこず,得られるのはASTである.
>>> x + one
<BV64 x_9_64 + 0x1>
>>> (x + one) / 2
<BV64 (x_9_64 + 0x1) / 0x2>
>>> x - y
<BV64 x_9_64 - y_10_64>
技術的には,x
やy
,さらにはone
もASTである.
bitvectorは,たとえそれが一層しか無いとしても,演算の木である.
これを理解する為に,ASTの処理方法を学ぶ.
各ASTには,.op
と.args
がある.
op
は,実行される操作の名前を示す文字列で,args
は操作が入力として受け取る値である.
op
がBVV
やBVS
(または他のいくつかの何か)で無い限り,args
は他のすべてのASTであり,木は最終的にBVVまたはBVSで終わる.
>>> tree = (x + 1) / (y + 2)
>>> tree
<BV64 (x_9_64 + 0x1) / (y_10_64 + 0x2)>
>>> tree.op
'__floordiv__'
>>> tree.args
(<BV64 x_9_64 + 0x1>, <BV64 y_10_64 + 0x2>)
>>> tree.args[0].op
'__add__'
>>> tree.args[0].args
(<BV64 x_9_64>, <BV64 0x1>)
>>> tree.args[0].args[1].op
'BVV'
>>> tree.args[0].args[1].args
(1, 64)
ここから先は,最上位の演算がbitvectorを生成するASTを指して,bitvectorという言葉を使う. ASTで表現されるデータは他にもあり,浮動小数点数や,これから説明するようなbool値がある.
Symbolic_Constraints
似たような型のAST同士で比較演算を行うと,別のASTが生成される. bitvectorではなく,シンボリックなブーリアンである.
>>> x == 1
<Bool x_9_64 == 0x1>
>>> x == one
<Bool x_9_64 == 0x1>
>>> x > 2
<Bool x_9_64 > 0x2>
>>> x + y == one_hundred + 5
<Bool (x_9_64 + y_10_64) == 0x69>
>>> one_hundred > 5
<Bool True>
>>> one_hundred > -5
<Bool False>
ここからわかる事は,比較はデフォルトでは符号無しで行われるということである.
最後の例の-5
は,<BV64 0xfffffffffffffffb>
に強制的に変換されているが,これは間違いなく100未満の値ではない.
比較を符号付きにしたい場合は,one_hundred.SGT(-5)
(sined greater-than)を使う.
演算の全リストは,この章の最後にある.
このスニペットは,angrを扱う上での重要なポイントを示している.
つまり,if文やwhile文の条件の中で,たとえ具体的な真理値があったとしても,変数間の比較を直接使用してはならない.
if one > one_hundred
とすると,例外が発生する.
代わりに,solver.is_true
とsolver.is_false
を使うべきである.
これらは,制約解決を行わずに具体的な真偽をテストする.
>>> yes = one == 1
>>> no = one == 2
>>> maybe = x == y
>>> state.solver.is_true(yes)
True
>>> state.solver.is_false(yes)
False
>>> state.solver.is_true(no)
False
>>> state.solver.is_false(no)
True
>>> state.solver.is_true(maybe)
False
>>> state.solver.is_false(maybe)
False
Constraint_Solving
symbolic boolianは,stateに制約として追加することで,シンボリック変数の有効な値に関するアサーションとして扱う事ができる. そして,シンボリック式の評価を求めることで,シンボリック変数の有効な値を問い合わせる事ができる.
ここで説明するよりも,例を挙げた方がわかりやすいだろう.
>>> state.solver.add(x > y)
>>> state.solver.add(y > 2)
>>> state.solver.add(10 > x)
>>> state.solver.eval(x)
4
これらの制約をstateに追加することで,制約ソルバは,それが返す値について満たさなければいけないアサーションとしてそれらを考慮する事を余儀なくされる.
このコードを実行すると,x
に対して異なる値が得られる可能性があるが,その値は間違いなく3より大きく(yは2より大きく,xはyより大きくなければならないので),10より小さくなる.
さらに,state.solver.eval(y)
とすれば,得られたx
の値と一致するy
の値が得られる.
2つのクエリの間に何の制約も加えなければ,結果は一致する.
ここからは,冒頭で提案した「ある出力を生み出す入力を見つける」という課題をどの様に行うかが簡単にわかる.
# get a fresh state without constraints
>>> state = proj.factory.entry_state()
>>> input = state.solver.BVS('input', 64)
>>> operation = (((input + 4) * 3) >> 1) + input
>>> output = 200
>>> state.solver.add(operation == output)
>>> state.solver.eval(input)
0x3333333333333381
この解決策は,bitvectorのセマンティクスでのみ機能することに注意しなければいけない. もし,整数の領域を扱うのであれば,解決策はない.
矛盾する制約を追加して,制約が満たされるように変数に代入できる値が無い場合,そのstateは未充足(unsat)となり,それに対するクエリは例外を発生させる.
状態の充足可能性を確認するためには,state.satisable()
を使用する.
>>> state.solver.add(input < 2**32)
>>> state.satisfiable()
False
また,単一の変数だけでなく,より複雑な式を評価する事も出来る.
# fresh state
>>> state = proj.factory.entry_state()
>>> state.solver.add(x - y >= 4)
>>> state.solver.add(y > 0)
>>> state.solver.eval(x)
5
>>> state.solver.eval(y)
1
>>> state.solver.eval(x + y)
6
この事から,eval
は任意のbitvectorをPythonのプリミティブに,stateの整合性を尊重しながら変換する汎用的な方法である事がわかる.
具象bitvectorからPython intへの変換にeval
を用いるのもこのためである.
また,x
とy
の変数は,古い状態で作成されたにも関わらず,この新しいstateで使用する事ができる事にも注意が必要.
変数は,どのstateにも縛られず,自由に存在する事ができる.
Floating_point_numbers
z3は,IEEE754の浮動小数点数をサポートしているので,angrでもそれを使う事ができる.
主な違いは,幅の代わりに,浮動小数点数にはsortがあることである.
FPV
やFPS
で浮動小数点のシンボルや数を作ることができる.
# fresh state
>>> state = proj.factory.entry_state()
>>> a = state.solver.FPV(3.2, state.solver.fp.FSORT_DOUBLE)
>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> b = state.solver.FPS('b', state.solver.fp.FSORT_DOUBLE)
>>> b
<FP64 FPS('FP_b_0_64', DOUBLE)>
>>> a + b
<FP64 fpAdd('RNE', FPV(3.2, DOUBLE), FPS('FP_b_0_64', DOUBLE))>
>>> a + 4.4
<FP64 FPV(7.6000000000000005, DOUBLE)>
>>> b + 2 < 0
<Bool fpLT(fpAdd('RNE', FPS('FP_b_0_64', DOUBLE), FPV(2.0, DOUBLE)), FPV(0.0, DOUBLE))>
まず第一に,浮動小数点数の場合,pretty-printingはそれほど賢くない.
さらに,ほとんどの演算には3つ目のパラメタがあり,これはバイナリ演算子を使う時に暗黙的に追加される.
IEEE754の仕様では,複数の丸め(round-to-nearest,round-to-zero,round-to-positive等)がサポートされており,z3でもそれらをサポートしている.
演算に丸め込みのモードを指定したい場合は,第一引数に丸め込みモード(solver.fp.RM_*
のいづれか)を指定して,明示的にFP演算を仕様する(例: solver.fpAdd
).
制約と解法は同じように動作するが,eval
が浮動小数点数を返すようになっている.
>>> state.solver.add(b + 2 < 0)
>>> state.solver.add(b + 2 > -1)
>>> state.solver.eval(b)
-2.4999999999999996
これは良いことだが,時にはbitvectorとしてのfloatの表現を直接扱う事ができる必要がある.
raw_to_bv
及びraw_to_fp
メソッドを使用すると,bitvectorをfloatとして解釈したり,その逆を行う事ができる.
>>> a.raw_to_bv()
<BV64 0x400999999999999a>
>>> b.raw_to_bv()
<BV64 fpToIEEEBV(FPS('FP_b_0_64', DOUBLE))>
>>> state.solver.BVV(0, 64).raw_to_fp()
<FP64 FPV(0.0, DOUBLE)>
>>> state.solver.BVS('x', 64).raw_to_fp()
<FP64 fpToFP(x_1_64, DOUBLE)>
これらの変換は,floatポインタをintポインタに,あるいはその逆にキャストした時のように,ビットパターンを保持する.
しかし,floatをint型にキャストしたように(あるいはその逆),可能な限り値を保持したい場合には,val_to_fp
およびval_to_bv
という別のメソッド群を使用できる.
これらのメソッドは,浮動小数点数の性質を持つため,対象となる値のサイズまたはsortをパラメタとして受け取る必要がある.
>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> a.val_to_bv(12)
<BV12 0x3>
>>> a.val_to_bv(12).val_to_fp(state.solver.fp.FSORT_FLOAT)
<FP32 FPV(3.0, FLOAT)>
これらのメソッドは,ソースまたはターゲットのbitvectorの符号性を指定する符号付きのパラメタを取る事もできる.
More_Solving_Methods
eval
は,式に対する1つの可能な解を与えてくれるが,複数の解が欲しい時はどうすれば良いか.
また,解が一意である事を確認したい場合はどうすれば良いか.
ソルバは,一般的な解法パターンに対応するいくつかの方法を提供している.
solver.eval(expression)
は,与えられた式に対する一つの可能な解を与えるsolver.eval_one(expression)
は,与えられた式の解を与えるが,複数の解がある場合はエラーをthrowするsolver.eval_upto(expression, n)
は,与えられた式に対する最大n個の解を与え,n個以下の解が可能な場合はそれを返すsolver.eval_atleast(expression, n)
は,与えられた式のn個の解を与え,n個未満の解しか得られなかった場合はエラーとなるsolver.eval_exact(expression, n)
は,与えられた式のn個の解を与え,それより少ないか多い場合はエラーとなるsolver.min(expression)
は,与えられた式に対する最小の解を与えるsolver.max(expression)
は,与えられた式に対して最大限の解を与える
さらに,これらのメソッドはすべて,以下のキーワード引数を取る事ができる.
extra_constraints
は,制約のタプルとして渡すことができる.これらの制約は,この評価のために考慮されるが,stateには追加されない.cast_to
には,結果をキャストするデータ型を渡すことができる.現在のところ,これはint
とbytes
のみで,メソッドは基礎となるデータの対応する表現を返すことになる.例えば,state.solver.eval(state.solver.BVV(0x41424344, 32), cast_to=bytes)
はb'ABCD'
を返す.
Summary
この章を読んだ後は,bitvector,ブーリアン,浮動小数点数を作成して操作し,演算木を作成し,stateに接続された制約ソルバに一連の制約下で可能な解を問い合わせることができるようになるだろう. この時点で,ASTを使って計算を表現する事の威力と,制約ソルバの威力を理解できれば良い.
付録には,ASTに適用できるすべての追加操作のリファレンスが掲載されているため,表を見たい時に便利.
Program_State
これまでの説明では,angrの操作に関する基本的な概念を示すために,angrのシミュレートされたプログラムの状態(SimStateオブジェクト)を可能な限り最小限の方法で使用してきた. ここでは,stateオブジェクトの構造と,様々な方法でstateオブジェクトを操作する方法について学ぶ.
Reading_and_writing_memory_and_registers
このドキュメントを順に読んできた場合,既にメモリやレジスタへのアクセス方法の基本を知っていると思う.
state.regs
では,各レジスタの名前を指定した属性によってレジスタへの読み書きのアクセスを提供し,state.mem
では,アドレスを指定するインデックスアクセス記法に続いて,メモリをどの様な型として解釈したいかを指定する属性アクセス記法によって,メモリへの型付きの読み書きのアクセスを提供している.
さらに,ASTの扱いを知っているので,bitvector型のASTであれば,レジスタやメモリに格納できる事が理解できるようになったはずだ.
ここでは,stateのデータをコピーしたり,操作を行ったりする簡単な例を紹介する.
mport angr, claripy
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()
# copy rsp to rbp
>>> state.regs.rbp = state.regs.rsp
# store rdx to memory at 0x1000
>>> state.mem[0x1000].uint64_t = state.regs.rdx
# dereference rbp
>>> state.regs.rbp = state.mem[state.regs.rbp].uint64_t.resolved
# add rax, qword ptr [rsp + 8]
>>> state.regs.rax += state.mem[state.regs.rsp + 8].uint64_t.resolved
Basic_Execution
先程,Simulation Managerを使って基本的な実行を行う方法を紹介した.
次の章では,Simulation Managerの全機能を紹介するが,今はよりシンプルなインターフェイスを使って,シンボリック実行がどのように機能するのかを説明する.
state.step()
は,シンボリック実行の1ステップを実行し,SimSuccessors
というオブジェクトを返す.
通常のエミュレーションとは異なり,シンボリック実行では,様々な方法で分類できる複数のsuccessor stateを生成することができる.
ここでは,このオブジェクトの.successor
プロパティに注目する.
これは,与えられたステップのすべての通常のsuccessorを含むリストである.
なぜ単一のsuccessor stateでなく,リストなのか.
angrのシンボリック実行のプロセスは,プログラムにコンパイルされた個々の命令の操作をSimStateを変異させるために実行しているだけである.
if (x > 4)
のようなコードに達した時,x
がシンボリックなbitvectorであればどうなるか.
angrの奥深くのどこかで,x > 4
という比較が実行され,その結果は<Bool x_32_1 > 4>
となる.
それは良い事だが,次の疑問は,trueの枝をとるのか,falseの枝をとるのかという点である.
その答えは,両方の枝をとるである.
条件が真であった場合のシミュレーションと,条件が偽であった場合のシミュレーションの2つの全く異なるsuccessor stateを生成する.
最初のstateでは,x > 4
を制約として追加し,2番目のstateでは,!(x > 4)
を制約として追加する.
このようにして,これらのsuccessor stateのいずれかを使用して制約解決を実行する時には,stateの条件によって,得られる解決策が有効な入力であり,与えられたstateがたどったのと同じ経路を実行する事が保証される.
これを実証するために,偽のファームウェアイメージを例に挙げる. このバイナリのソースコードを見ると,ファームウェアの認証メカニズムがバックドア化されているのがわかる. どのようなユーザ名でも,パスワードSOSNEAKYを使えば管理者として認証される. さらに,最初に行われるユーザ入力との比較は,バックドアとの比較であるため,複数のsuccessor stateが得られるまでステップすると,それらのstateの1つには,ユーザ入力がバックドアのパスワードである事を制約する条件が含まれる. 次のスニペットは,これを実装している.
>>> proj = angr.Project('examples/fauxware/fauxware')
>>> state = proj.factory.entry_state(stdin=angr.SimFile) # ignore that argument for now - we're disabling a more complicated default setup for the sake of education
>>> while True:
... succ = state.step()
... if len(succ.successors) == 2:
... break
... state = succ.successors[0]
>>> state1, state2 = succ.successors
>>> state1
<SimState @ 0x400629>
>>> state2
<SimState @ 0x400699>
注目すべきは,これらの状態に対する制約ではない.
先程の分岐では,strcmp
の結果が含まれているが,これはシンボリックにエミュレートするのが難しい関数であり,結果として制約は非常に複雑なものになる.
今回エミュレートしたプログラムは,標準入力からデータを取得したが,angrはデフォルトでシンボリックなデータの無限なstreamとして扱う.
制約の解消を行い,制約を満たすために入力が取り得る値を得るためには,stdinの実際の内容を参照する必要がある.
ファイルと入力のサブシステムがどのように動作するかについては,後ほど説明するが,今の所,stdinからこれまでに読み込まれたすべてのコンテンツを表すbitvectorを取得するために,state.posix.stdin.load(0, state.posix.stdin.size)
を使用する事ができる.
>>> input_data = state1.posix.stdin.load(0, state.posix.stdin.size)
>>> state1.solver.eval(input_data, cast_to=bytes)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00\x00\x00'
>>> state2.solver.eval(input_data, cast_to=bytes)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x00\x80N\x00\x00 \x00\x00\x00\x00'
見てわかるように,state1
のパスを通るためには,パスワードとして裏技のような文字列SOSNEAKYを指定しなければならない.
state2
移行する為には,SOSNEAKY以外の何かを与える必要がある.
z3は,この条件を満たす数十億の文字列のうちの1つを提供した.
fauxware
は,2013年にangrのシンボリック実行が初めて成功したプログラムであった.
angrをつかってfauxwareのバックドアを見つける事で,バイナリから意味を引き出すためのシンボリック実行の使い方を素直に理解するという壮大な伝統に参加することになる.
State_Presets
これまで,stateを扱うときにはproject.factory.entry_state()
を作成してきた.
これは,project factoryで利用できるいくつかのstate constractorsの内の一つである.
.blank_state()
は,ほとんどのデータが初期化されていない,まっさらな状態のblank stateを構築する.初期化されていないデータにアクセスすると,制約の無いシンボリック値が返される..entry_state()
は,メインバイナリのエントリーポイントで実行可能なstateを構築する..full_init_state()
は,メインバイナリのエントリーポイントの前に実行する必要のある初期化処理(共有ライブラリのコンストラクタや,preinitializerなど)を実行する準備が整ったstateを構築する.これらの処理が終了すると,エントリーポイントにジャンプする..call_state()
は,与えられた関数を実行する準備が整ったstateを構築する.
これらのコンストラクタにいくつかの引数を与えることで,stateをカスタマイズできる.
- これらのコンストラクタはすべて,開始する正確なアドレスを指定する
addr
引数を取ることができる. - コマンドライン引数や環境変数を受け取れる環境で実行する場合は,
args
で引数のリストを,env
で環境変数のdictをentry_state
やfull_init_state
に渡すことができる.これらの構造体の値は文字列またはbitvector
で,シミュレートされた実行時の引数や環境変数としてstateにシリアライズされる.デフォルトのargs
は空のリストで,解析しているプログラムが少なくともargs[0]
を参照することを期待しているのであれば,常にそれを提供する必要がある. argc
をシンボリックにしたい場合,entry_state
やfull_init_state
のコンストラクタにシンボリックなbitvectorをargc
として渡すことができる.その場合は,生成されるstateに,argc
の値がargs
に渡した引数の数よりも大きくなってはならないという制約を加える必要がある.- call stateを使用する為には,
.call_state(addr, arg1, arg2, ...)
のように呼び出さなければいけない.addr
は呼び出したい関数のアドレスで,argN
は,その関数のN番目の引数で,Pythonの整数,文字列,配列またはbitvectorである.メモリを確保して実際にオブジェクトへのポインタを渡したい場合は,PointerWrapper
でラップする必要がある.例えば,angr.PointerWrapper("point to me!")
のようになる.このAPIの結果は予測不可能なものになる可能性があるが,現在調整中である. call_state
で関数に使われる呼び出し規則を指定するには,cc
引数として,SimCC インスタンスを渡す.
基本的には正常なデフォルトを選ぶようにしているが,特殊なケースではユーザが指定する必要がある.
これらのコンストラクタで使用できるオプションは他にもいくつかある. 詳しくは,project.factoryオブジェクトのドキュメントを参照.
Low_level_interface_for_memory
state.mem
インターフェイスは,型付けされたデータをメモリから読み込むのには便利だが,メモリの範囲に対して生のロードやストアを行いたい場合には非常に面倒である.
実際には,state.mem
は基礎となるメモリストレージに正しくアクセスする為のロジックの集合であり,bitvectorデータで満たされたフラットなアドレス空間に過ぎない事がわかった.
state.mem
は,.load(addr, size)
メソッドや,.store(addr, val)
メソッドで直接使用できる.
>>> s = proj.factory.blank_state()
>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef0123456789abcdef, 128))
>>> s.memory.load(0x4004, 6) # load-size is in bytes
<BV48 0x89abcdef0123>
state.memoy
の主な目的は,セマンティクスが付与されていない大量のデータを読み込んだり保存したりすることなので,データはbig-endianで読み込まれ,保存されいる.
しかし,読み込まれたデータや保存されたデータに対してバイトスワップを行いたい場合は,キーワード引数にendianess
を渡す事ができる.
endianess
は,angrのCPUアーキテクチャに関する宣言的なデータを保持するために使用されるarchinfo
パッケージのEndianess
enumのメンバーの一つでなければいけない.
さらに,解析するプログラムのendianess
は,arch.memory_endness
として得る事ができる.例えば,state.arch.memory_endness
のように.
>>> import archinfo
>>> s.memory.load(0x4000, 4, endness=archinfo.Endness.LE)
<BV32 0x67452301>
また,レジスタへのアクセスの為の低レベルインターフェイスであるstate.registers
は,state.memory
と全く同じAPIを提供しているが,その動作を説明するには,angrが複数のアーキテクチャをシームレスに動作させるために使用している抽象化に踏み込む必要がある.
簡単に説明すると,これは単なるレジスタファイルで,レジスタとオフセットのマッピングはarchinfoで定義されている.
State_Options
angrの内部では,ある状況下では動作を最適化し,ある状況下では不利になるような,多くの細かな調整が可能である. これらの調整は,state optionで制御する.
各SimStateオブジェクトには,すべての有効なオプションのセット(state.options
)がある.
各オプション(実際には単なる文字列)は,angrの実行エンジンの動作を微調整するものである.
オプションの全領域と,異なるstate typesのデフォルト値のリストは付録にある.
angr.options
では,stateに追加する個々のオプションにアクセスできる.
個々のオプションには,大文字の名前がついているが,よく使われるオプションをまとめて小文字で表したものもある.
コンストラクタでSimStateを作成する際,キーワード引数のadd_options
とremove_options
を渡す事ができる.
# Example: enable lazy solves, an option that causes state satisfiability to be checked as infrequently as possible.
# This change to the settings will be propagated to all successor states created from this state after this line.
>>> s.options.add(angr.options.LAZY_SOLVES)
# Create a new state with lazy solves enabled
>>> s = proj.factory.entry_state(add_options={angr.options.LAZY_SOLVES})
# Create a new state without simplification options enabled
>>> s = proj.factory.entry_state(remove_options=angr.options.simplification)
State_Plugins
先程説明した一連のオプションを除いて,SimStateに保存されているすべてのものは,実際にはstateに取り付けられたプラグインに保存されている. メモリ,レジスタ,mem,regs,ソルバなど,これまで説明してきたstate上のほとんどすべてのプロパティがプラグインである. この設計により,コードのモジュール化だけでなく,エミュレートされたstateの他の側面の為に新しい種類のデータストレージを簡単に実装したり,プラグインの代替実装を提供したりする事が可能になる.
例えば,通常のmemory
プラグインはフラットなメモリ空間をシミュレートするが,解析者は,アドレスに代替データ型を使用してアドレスに依存しないフリーフローティングメモリマッピングをシミュレートするabstract memoryプラグインを有効にして,state.memory
を提供する事ができる.
逆に言えば,プラグインの複雑さを軽減する事ができる.
つまり,state.memory
とstate.registers
は,実際には同じプラグインの2つの異なるインスタンスであり,レジスタもアドレス空間でエミュレートされるからである.
The globals plugin
state.globals
は非常にシンプルなプラグインで,標準的なdictのインターフェイスを実装しており,stateに任意のデータを格納する事ができる.
The history plugin
state.history
はstateが実行中にとったパスに関する履歴データを保存する,非常に重要なプラグインで,実際には,複数のhistoryノードのlinked listであり,それぞれのノードは1回の実行ラウンドを表している.
このリストは,state.history.parent.parent
などでたどる事が出来る.
この構造をより便利に扱うために,historyは特定の値の履歴に対するいくつかの効率的なイテレータも提供している.
一般的に,これらの値はhistory.recent_NAME
として格納され,それらに対するイテレータは単にhistory.NAME
となる.
例えば,for addr in state.history.bbl_addrs: print(hex(addr))
はバイナリのbasic blockのアドレスのトレースを出力する.
一方,state.history.parent.recent_bbl_addrs
は前のステップで実行されたbasic blockのリストなどで,これらの値のフラットなリストをすばやく取得する必要がある場合は,.hardcopy
にアクセスする事ができる(例: state.history.bbl_addrs.hardcopy
).
ただし,インデックスベースのアクセスはイテレータに実装されていることに注意が必要.
ここでは,historyの中で蓄積されてきた値の一部を紹介する
history.descriptions
はそのstateで実行された各ラウンドのstring descriptionsのリストhistory.bbl_addrs
は,stateによって実行されたbasic blockアドレスのリスト.また,すべてのアドレスがバイナリのコードに対応しているわけでなく,SimProceduresがフックされているアドレスもある.history.jumpkinds
は,VEX enum文字列として,stateの履歴における制御フロー遷移のそれぞれの処分のリストhistory.jump_guards
は,stateが遷移した各分岐を保護する条件のリストhistory.events
は,実行中に発生したinteresting eventsの意味的なリスト.例えば,シンボリックなジャンプ条件の存在,プログラムがメッセージボックスをポップアップすることで,実行が終了コードで終了することなど.history.actions
は通常は空だが,angr.options.refs
オプションをstateに追加すると,プログラムが実行したすべてのメモリ,レジスタ,一時的な値へのアクセスのログが入力される.
The callstack plugin
angrは,エミュレートされたプログラムのコールスタックを追跡する. 呼び出しを行う命令毎に,追跡されたコールスタックの最上位のフレームが呼び出されたポイントより下がる度に,フレームがポップされる. これにより,angrは現在のエミュレートされた関数のローカルなデータを堅牢に保存できる.
historyと同様に,コールスタックもノードのリンクリストだが,ノードの内容に対するイテレータは用意されていない.
代わりに,state.callstack
を直接イテレートすることで,アクティブなフレームごとのコールスタックフレームを,新しいものから古いものの順に取得できる.
一番上のフレームだけが必要な場合は,state.callstack
で得られる.
callstack.func_addr
は,現在実行されている関数のアドレスcallstack.call_site_addr
は,現在の関数を呼び出したbasic blockのアドレスcallstack.stack_ptr
は,現在の関数の先頭からのスタックポインタの値callstack.ret_addr
は,現在の関数がリターンしたときに戻るアドレス
More about I/O: Files, file systems, and networks sockets
angrでのI/Oのモデル化については,file systemを参照
Copying_and_Merging
stateは非常に高速なコピーをサポートしているので,様々な可能性を追求する事ができる.
>>> proj = angr.Project('/bin/true')
>>> s = proj.factory.blank_state()
>>> s1 = s.copy()
>>> s2 = s.copy()
>>> s1.mem[0x1000].uint32_t = 0x41414141
>>> s2.mem[0x1000].uint32_t = 0x42424242
また,stateを統合する事も可能
# merge will return a tuple. the first element is the merged state
# the second element is a symbolic variable describing a state flag
# the third element is a boolean describing whether any merging was done
>>> (s_merged, m, anything_merged) = s1.merge(s2)
# this is now an expression that can resolve to "AAAA" *or* "BBBB"
>>> aaaa_or_bbbb = s_merged.mem[0x1000].uint32_t
Simulation_Managers
angrで最も重要な制御インターフェイスは,SimulationManagerで,プログラムの状態空間を探索するための検索ストラテジを適用し,stateのグループに対するシンボリックな実行を同時に制御することができる. ここでは,その使い方について説明する.
SimulationManagerは,複数のstateを巧みに操ることができる.stateはstashesに整理されており,必要に応じて転送,フィルタリング,結合,移動などを行う事が出来る. これにより,例えば2つの異なるstashのstateを異なる速度でステップし,それらをマージする事ができる. ほとんどの場合,デフォルトのstashはアクティブなstashで,新しいSimulationManagerを初期化したときにstateが置かれる場所である.
Stepping
SimulationManagerの最も基本的な機能は,与えられたstash内のすべてのstateを1つのbasic blockごとに進めることで,これは.step()
で行える.
>>> import angr
>>> proj = angr.Project('examples/fauxware/fauxware', auto_load_libs=False)
>>> state = proj.factory.entry_state()
>>> simgr = proj.factory.simgr(state)
>>> simgr.active
[<SimState @ 0x400580>]
>>> simgr.step()
>>> simgr.active
[<SimState @ 0x400540>]
もちろん,stash modelの真の力は,あるstateがシンボリックな分岐条件に遭遇したときに,そのsuccessor stateの両方がstashに現れ,両方を同期してステップすることができることである.慎重に解析をコントロールすることに関心がなく,ただステップするものがなくなるまでステップしたい場合は,.run()
メソッドを使えば良い.
# Step until the first symbolic branch
>>> while len(simgr.active) == 1:
... simgr.step()
>>> simgr
<SimulationManager with 2 active>
>>> simgr.active
[<SimState @ 0x400692>, <SimState @ 0x400699>]
# Step until everything terminates
>>> simgr.run()
>>> simgr
<SimulationManager with 3 deadended>
これで3つのdeadened stateができた.あるstateが実行中にsuccessorを生み出すことができなかった場合,例えばexit
syscallに到達したなどの理由で,そのstateはactive stashから削除され,deadended stashに置かれる.
Stash_Management
他のstashとの連携を見てみる.
stash間でstateを移動させるには,from_stash
,to_stash
,filter_func
(オプション,デフォルトはすべてを移動させる)を引数に取る.move()
を使う.
例として,ある文字列が出力されているものをすべて移動させてみる.
>>> simgr.move(from_stash='deadended', to_stash='authenticated', filter_func=lambda s: b'Welcome' in s.posix.dumps(1))
>>> simgr
<SimulationManager with 2 authenticated, 1 deadended>
authenticatedという名前の新しいstashを作成し,そこにstateを移動するように要求した.このstashにあるすべてのstateの標準出力にはWelcomeと表示されており,これは今のところ良い指標となっている.
各stashは単なるリストであり,個々のstateにアクセスするためにリストにインデックスを作成したり,イテレートすることができるが,stateにアクセスするための別の方法もいくつかある.
stashの名前の前にone_
を付けると,そのstashの最初のstateが表示される.
stashの名前の前にmp_
を付けると,そのstashのmulpyplexedバージョンが表示される.
>>> for s in simgr.deadended + simgr.authenticated:
... print(hex(s.addr))
0x1000030
0x1000078
0x1000078
>>> simgr.one_deadended
<SimState @ 0x1000030>
>>> simgr.mp_authenticated
MP([<SimState @ 0x1000078>, <SimState @ 0x1000078>])
>>> simgr.mp_authenticated.posix.dumps(0)
MP(['\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00',
'\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x80\x80\x80\x80@\x80@\x00'])
もちろん,step
,run
,その他の単一のパスのstashを操作するメソッドは,どのスタッシュを操作するかを指定するstash
引数を取ることができる.
SimulationManagerには,stashを管理するための楽しいツールがたくさんある.
今のところ,それらの残りの部分については説明しないが,APIドキュメントを見るとわかる.
Stash_types
stashは何にでも使用出来るが,いくつかのstashは,特殊なstateを分類するために使用される.
stash | description |
---|---|
active | 代替stashが指定されていない限り,デフォルトでステップされるstateが含まれている. |
deadended | stateがdeadended stashに入るのは,有効な命令がなくなった,すべてのsuccessor stateが解除された,無効な命令ポインタがあるなど,何らかの理由で実行を継続できなくなった時. |
pruned | LAZY_SOLVES を使用する際には,絶対に必要な場合を除き,stateの充足可能性をチェックしない.LAZY_SOLVES の存在下でstateが不完全であることが判明した場合,stateの階層を走査し,そのstateがhistoryの中で最初に不完全になった時点を特定する.その時点の子孫であるすべてのstate(stateがun-unsatになることはないので,un-satになる)は刈り取られ,このstashに入れられる. |
unconstrained | SimulationManagerのコンストラクタにsave_unconstrained オプションが指定されている場合,制約がないと判断されたstate(ユーザーデータやその他のシンボリックデータによって命令ポインタが制御されているstate)がここに置かれる. |
unsat | SimulationManagerのコンストラクタにsave_unsat オプションが指定されている場合,un-satと判断されたstate(入力がAAAAとBBBBの両方でなければならないなど,矛盾した制約があるstate)はここに置かれる. |
stashではないstateのリストとして,errored
がある.
実行中にエラーが発生した場合,stateはErrorRecord
オブジェクトにラップされ,stateと発生したエラーが格納され,そのレコードはerrored
に挿入される.
エラーの原因となった実行tickの最初のstateをrecord.state
で知ることができ,発生したエラーをrecord.error
で見ることができ,エラーが発生した場所でrecord.debug()
でデバッグシェルを起動することができる.
これは非常に貴重なデバッグツールである.
Simple_Exploration
シンボリック実行で極めて一般的な操作は,あるアドレスに到達するstateを見つけ,一方で別のアドレスを経由する状態はすべて破棄するというものである.
SimulationManagerには,このパターンのショートカットとして,.explore()
メソッドがある.
.explore()
にfind
を引数として渡すと,find
の条件にマッチするstateが見つかるまで実行される.
find
の条件には,停止する命令のアドレス,停止するアドレスのリスト,stateを受け取ってそれがある基準を満たしているかどうかを返す関数などがある.
active stash内のstateが検索条件に一致すると,それらはfound stashに置かれ,実行が終了する.
その後,見つかったstateを探索したり,それを破棄して他のstateを継続することができる.
また,find
と同じ形式でavoid
を指定することもできる.
あるstateがavoid
の条件にマッチすると,そのstateはavoided stashに入れられ,実行が続行される.
最後に,num_find
引数は,戻る前に見つかるべきstateの数を制御する.
デフォルトでは1.
もちろん,これだけの数の解決策を見つける前にactive stashのstateがなくなった場合は,実行は停止される.
簡単な例を見てみる.
まずはバイナリをロードする
>>> proj = angr.Project('examples/CSCI-4968-MBE/challenges/crackme0x00a/crackme0x00a')
次に,SimulationManagerを作成する
>>> simgr = proj.factory.simgr()
条件に合致するstate(つまり,flagの条件)が見つかるまで,シンボリック実行を行う
>>> simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))
<SimulationManager with 1 active, 1 found>
見つかったstateからflagを得る事ができる
>>> s = simgr.found[0]
>>> print(s.posix.dumps(1))
Enter password: Congrats!
>>> flag = s.posix.dumps(0)
>>> print(flag)
g00dJ0B!
簡単
examplesには他にもいろんな例題がある.
Exploration_Techniques
angrには,SimulationManagerの動作をカスタマイズするために,exploration techniquesと呼ばれるいくつかの機能が搭載されている. exploration techniquesの典型的な例は,プログラムのstate空間を探索するパターンを変更することである. デフォルトのstep everything at once戦略は,事実上の幅優先探索だが,exploration techniquesを使えば,例えば,深さ優先探索を実行することができる. しかし,これらのtechniqueのinstrumentation能力はそれよりもはるかに柔軟で,angrのステッピング・プロセスの動作を完全に変更することができる. 独自の探索手法の実装については,後の章で説明する.
exploration techniqueを使うには,simgr.use_technique(tech)
を使う.
tech
はExplorationTechnique
のサブクラスである必要がある.
ここでは,build-inのものを中心に紹介する.
- DFS: 深さ優先探索.一度に1つのstateだけをactiveにしておき,残りのstateはdeadendやエラーが発生するまで
deferred stash
に置いておく. - Explorer: このtechniqueは,
.explore()
を実装しており,アドレスを検索してavoidできる. - LengthLimiter: stateが通過するパスの最大長に上限を設ける.
- LoopSeer: ループカウントの合理的な近似値を使用して,ループを何度も通過しているように見えるstateを破棄し,それらを
spinning stash
に置き,他の実行可能なstashが無くなった場合に再び引き出す. - ManualMergepoint: プログラム内のあるアドレスを
merge point
としてマークし,そのアドレスに到達したstateは一時的に保持され,タイムアウト以内に同じポイントに到達した他のstateがマージされる. - MemoryWatcher: simgrのステップの間にシステム上でどれだけのメモリが空いているか,利用可能かを監視し,少なくなりすぎると探索を停止する.
- Oppologist: operation apologistは,angrがサポートされていない命令に遭遇した場合,その命令へのすべての入力を具体化し,unicorn engineを使用して単一の命令をエミュレートし,実行を継続できるようにする.
- Spiller: アクティブなstateが多すぎる場合,このtechniqueは,メモリの消費を抑えるために,その一部をディスクにダンプすることができる.
- Threading: ステッピングプロセスにthreadレベルの並列処理を追加する.Pythonのグローバルインタプリタのロックのせいで,あまり役に立たないが,解析がangrのネイティブコードの依存関係(unicorn,z3,libvex)に多くの時間を費やしているプログラムであれば,いくらか役に立つ.
- Tracer: 他のソースから記録された動的なトレースに沿って実行させるtechnique.dynamic tracerのリポジトリには,これらのトレースを生成するためのツールがいくつか用意されている.
- Veritesting: 有用なmerge pointを自動的に特定するというCMUの論文の実装.非常に便利で,SimulationManagerのコンストラクタで
veritesting=True
を指定すると自動で有効になる.静的なシンボリック実行を実装しているため,他のtechniqueとの相性が悪い点に注意が必要.
より詳しい情報はsimulation managerとexploration techniquesのドキュメントを参照.
Execution_Engines
angrでは,一連のエンジン(SimEngine
クラスのサブクラス)を使用して,コードのある部分がinput stateに与える影響をエミュレートする.
angrの実行コアは,利用可能なすべてのエンジンを順に試し,ステップを処理できる最初のエンジンを選ぶ.
以下は,デフォルトのエンジンのリスト.
- 前のステップで継続不可能な状態になったとき,failure engine が作動する.
- 前のステップがシステムコールで終了したとき,syscall engine が作動する.
- 現在のアドレスがフックされたとき,hook engine が作動する.
UNICORN
stateオプションが有効で,そのstateにシンボリックデータが無いとき,UNICORN engine が作動する.- 最終的なフォールバックとして,VEX engine が作動する.
SimSuccessors
実際にすべてのエンジンを順番に試すコードはproject.factory.successors(state, **kwargs)
で,引数を各エンジンに渡している.
この関数は,state.step()
とsimulation_manager.step()
の中心となる.
この関数は,前に簡単に説明したSimSuccessorsオブジェクトを返す.
SimSuccessorsの目的は,様々なリスト属性に格納されているsuccessor stateの簡単な分類を行うことである.
それらは
Attribute | Guard Condition | Instruction Pointer | Description |
---|---|---|---|
successors |
True(シンボリックでも良いが,Trueに限定される) | シンボリックでも良いが,解の上限は256(unconstrained_successors 参照) |
エンジンによって処理されたstateの,正常で満足できるsuccessor state.このstateの命令ポインタはシンボリックなものである可能性があり(つまり,ユーザーの入力に基づいて計算されたジャンプ),そのため,そのstateは実際には今後のいくつかの実行継続の可能性を表しているかもしれない. |
unsat_successors |
False(シンボリックでも良いが,Falseに限定される) | シンボリックでも良い | 不充足なsuccessor.これは,ガード条件が偽にしかならないsuccessor.(つまり,取ることができないジャンプや,取らなければならないジャンプのデフォルトブランチ) |
flat_successors |
True(シンボリックでも良いが,Trueに限定される) | 具体的な値 | 前述のように,successors リストのstateはシンボリックな命令ポインタを持つことができる.これは,コードのどこかで(つまり,SimEngineVEX.process で,そのstateをステップさせるとき),1つのプログラムstateがコードの1つのスポットの実行を表しているだけだと仮定しているので,かなり混乱する.この問題を解決するために,シンボリックな命令ポインタを持つsuccessorプログラムのstateに遭遇した場合,そのstateに対して可能なすべての具体的な解(256以下の任意の閾値まで)を計算し,そのような解ごとにstateのコピーを作成する.この処理をflatteningと呼んでいる.Flat_successors は,それぞれが異なる具体的な命令ポインタを持つstate.例えば,あるsuccessors のstateの命令ポインタがX+5 で,X がX > 0x800000 ,X <= 0x800010 の制約を持っていた場合,0x800006 の命令ポインタを持つstate,0x800007 の命令ポインタを持つstate,そして0x800015 までの16種類のflat_successors stateにflatteningする. |
unconstrained_successors |
True(シンボリックでも良いが,Trueに限定される) | シンボリック(256以上の解を持てる) | 上述のflatteningの間に,命令ポインタに256以上の可能な解があることが判明した場合,命令ポインタが制約のないデータで上書きされた(即ち,ユーザーデータによるスタックオーバーフロー)と仮定する.この仮定は一般的には健全ではない.このような状態はunconstrained_successors に置かれ,successor には置かれない. |
all_successors |
Anything | シンボリックでも良い | successors +unsat_successors +unconstrained_successors |
BreakPoints
ほかの実行エンジンのように,angrはブレークポイントをサポートしている. ブレークポイントは以下のように設定する.
>>> import angr
>>> b = angr.Project('examples/fauxware/fauxware')
# get our state
>>> s = b.factory.entry_state()
# add a breakpoint. This breakpoint will drop into ipdb right before a memory write happens.
>>> s.inspect.b('mem_write')
# on the other hand, we can have a breakpoint trigger right *after* a memory write happens.
# we can also have a callback function run instead of opening ipdb.
>>> def debug_func(state):
... print("State %s is about to do a memory write!")
>>> s.inspect.b('mem_write', when=angr.BP_AFTER, action=debug_func)
# or, you can have it drop you in an embedded IPython!
>>> s.inspect.b('mem_write', when=angr.BP_AFTER, action=angr.BP_IPYTHON)
メモリへの書き込み以外にもbreakできる場所は複数ある.
以下はそのリストで,これらのイベントのそれぞれについて,BP_BEFORE
またはBP_AFTER
でbreakすることができる.
Event type | Event meaning |
---|---|
mem_read |
メモリの読み込み |
mem_write |
メモリへの書き込み |
address_concretiazation |
シンボリックなメモリアクセスの解決 |
reg_read |
レジスタの読み込み |
reg_write |
レジスタへの書き込み |
tmp_read |
tempの読み込み |
tmp_write |
tempへの書き込み |
expr |
式の作成(つまり,算術演算の結果やIRの定数) |
statement |
IR statementの翻訳 |
instruction |
新しい(ネイティブな)命令の翻訳 |
irsb |
新しいbasic blockの翻訳 |
constraints |
新しい制約が加えられたstate |
exit |
実行により生成されたsuccessor |
fork |
シンボリックな実行状態が複数のstateに分岐している |
symbolic_variable |
新しいシンボリック変数が作成された |
call |
call命令が発行された |
return |
ret命令が発生された |
simprocedure |
simprocedure (またはsyscall )が実行された |
dirty |
dirtyなIR callbackが実行された |
syscall |
syscall が実行された(simprocedure イベントに加えて呼び出される) |
engine_process |
SimEngineがコードを処理する |
これらのイベントでは,異なるattributeが公開される
Event type | Attribute name | Attribute availability | Attribute mear |
---|---|---|---|
mem_read | mem_read_address | BP_BEFORE orBP_AFTER |
メモリを読み出す際のアドレス |
mem_read | mem_read_expr | BP_AFTER |
書き込むアドレスでの式 |
mem_read | mem_read_length | BP_BEFORE orBP_AFTER |
読み出すメモリの長さ |
mem_read | mem_read_condition | BP_BEFORE orBP_AFTER |
メモリを読み込んだ時の状態 |
mem_write | mem_write_address | BP_BEFORE orBP_AFTER |
メモリへの書き込みを始めるアドレス |
mem_write | mem_write_length | BP_BEFORE orBP_AFTER |
メモリを書き込む長さ |
mem_write | mem_write_expr | BP_BEFORE orBP_AFTER |
メモリを書き込む式 |
mem_write | mem_write_condition | BP_BEFORE orBP_AFTER |
メモリを書き込む時の状態 |
reg_read | reg_read_offset | BP_BEFORE orBP_AFTER |
読み込むレジスタのオフセット |
reg_read | reg_read_length | BP_BEFORE orBP_AFTER |
読み込むレジスタの長さ |
reg_read | reg_read_expr | BP_AFTER |
読み込むレジスタの式 |
reg_read | reg_read_condition | BP_BEFORE orBP_AFTER |
読み込むレジスタの状態 |
reg_write | reg_write_offset | BP_BEFORE orBP_AFTER |
書き込むレジスタのオフセット |
reg_write | reg_write_length | BP_BEFORE orBP_AFTER |
書き込むレジスタの長さ |
reg_write | reg_write_expr | BP_BEFORE orBP_AFTER |
書き込むレジスタの式 |
reg_write | reg_write_condition | BP_BEFORE orBP_AFTER |
書き込むレジスタの状態 |
tmp_read | temp_read_num | BP_BEFORE orBP_AFTER |
読まれるtempの番号 |
tmp_read | temp_read_expr | BP_AFTER |
読まれるtempの式 |
tmp_write | temp_write_num | BP_BEFORE orBP_AFTER |
書き込まれるtempの番号 |
tmp_write | temp_write_expr | BP_AFTER |
書き込まれるtempの式 |
expr | expr | BP_BEFORE orBP_AFTER |
IRの式 |
expr | expr_result | BP_AFTER |
式が評価された値(ASTなど) |
statement | statement | BP_BEFORE orBP_AFTER |
IR statementのインデックス |
instruction | instruction | BP_BEFORE orBP_AFTER |
ネイティブ命令のアドレス |
irsb | address | BP_BEFORE orBP_AFTER |
basic blockのアドレス |
constraints | added_constraints | BP_BEFORE orBP_AFTER |
追加される制約式のアドレス |
call | function_address | BP_BEFORE orBP_AFTER |
呼び出される関数の名前 |
exit | exit_target | BP_BEFORE orBP_AFTER |
SimExit のターゲットを表す式 |
exit | exit_guard | BP_BEFORE orBP_AFTER |
SimExit のガードを表す式 |
exit | exit_jumpkind | BP_BEFORE orBP_AFTER |
SimExit の種類を表す式 |
symbolic_variable | symbolic_name | BP_AFTER |
作成されるシンボリック変数の名前.ソルバエンジンがこの名前を(一意なIDとlengthを追加することで)変更する可能性がある.最終的なシンボリック式はsymbolic_expr を確認 |
symbolic_variable | symbolic_size | BP_AFTER |
作成されるシンボリック変数のサイズ |
symbolic_variable | symbolic_expr | BP_AFTER |
新しいシンボリック変数を表す式 |
address_concretiazation | address_concretiazation_strategy | BP_BEFORE orBP_AFTER |
アドレスの解決に使用されるSimConcretizationStrategy .これは,適用されるストラテジw変更するために,breakpoint handlerによって変更する事が出来る.breakpoint handlerがこれをNoneに設定した場合,このストラテジはスキップされる. |
address_concretiazation | address_concretiazation_action | BP_BEFORE orBP_AFTER |
メモリアクションの記録に使用するSimAction オブジェクト |
address_concretiazation | address_concretiazation_memory | BP_BEFORE orBP_AFTER |
アクションが行われたSimMemory オブジェクト |
address_concretiazation | address_concretiazation_expr | BP_BEFORE orBP_AFTER |
解決されるメモリインデックスを表すAST.breakpoint handlerは,解決されるアドレスに影響を与えるためにこれを変更する事が出来る. |
address_concretiazation | address_concretiazation_add_constraints | BP_BEFORE orBP_AFTER |
このreadに制約を加えるべきかどうか |
address_concretiazation | address_concretiazation_result | BP_AFTER |
解決されたメモリアドレスのリスト(int).breakpoint handlerは,これらを上書きして異なる解決結果を得ることができる. |
syscall | syscall_name | BP_BEFORE orBP_AFTER |
システムコールの名前 |
simprocedure | simprocedure_name | BP_BEFORE orBP_AFTER |
simprocedureの名前 |
simprocedure | simprocedure_addr | BP_BEFORE orBP_AFTER |
simprocedureのアドレス |
simprocedure | simprocedure_result | BP_AFTER |
simprocedureの戻り値.これをBP_BEFORE でオーバーライドすると,実際のsimprocedureはスキップされ,指定した戻り値が使用される. |
simprocedure | simprocedure | BP_BEFORE orBP_AFTER |
SimProcedure オブジェクト |
dirty | dirty_name | BP_BEFORE orBP_AFTER |
dirty callの名前 |
dirty | dirty_handler | BP_BEFORE |
dirty callを処理するために実行される関数.これをオーバーライドすることもできる |
dirty | dirty_args | BP_BEFORE orBP_AFTER |
dirtyのアドレス |
dirty | dirty_result | BP_AFTER |
dirty callの戻り値.BP_BEFOREでオーバーライドすることもできる.この場合,実際のdirty callはスキップされ,代わりに指定した戻り値が使用される. |
engine_process | sim_engine | BP_BEFORE orBP_AFTER |
処理を行っているSimEngine |
engine_process | successors | BP_BEFORE orBP_AFTER |
エンジンの結果を定義するSimSuccessors オブジェクト |
これらのattributeは,適切なbreakpoint callback中にstate.inspect
のメンバとしてアクセスし,適切な値にアクセスすることができる.
これらの値を変更して,さらに値の使用方法を変更することもできる.
>>> def track_reads(state):
... print('Read', state.inspect.mem_read_expr, 'from', state.inspect.mem_read_address)
...
>>> s.inspect.b('mem_read', when=angr.BP_AFTER, action=track_reads)
さらに,これらのプロパティはそれぞれinspect.b
のキーワード引数として使用する事ができ,breakpointを条件付きで設定する事ができる.
# This will break before a memory write if 0x1000 is a possible value of its target expression
>>> s.inspect.b('mem_write', mem_write_address=0x1000)
# This will break before a memory write if 0x1000 is the *only* value of its target expression
>>> s.inspect.b('mem_write', mem_write_address=0x1000, mem_write_address_unique=True)
# This will break after instruction 0x8000, but only 0x1000 is a possible value of the last expression that was read from memory
>>> s.inspect.b('instruction', when=angr.BP_AFTER, instruction=0x8000, mem_read_expr=0x1000)
実際に条件として関数を指定する事もできる.
# this is a complex condition that could do anything! In this case, it makes sure that RAX is 0x41414141 and
# that the basic block starting at 0x8004 was executed sometime in this path's history
>>> def cond(state):
... return state.eval(state.regs.rax, cast_to=str) == 'AAAA' and 0x8004 in state.inspect.backtrace
>>> s.inspect.b('mem_write', condition=cond)
Caution_about_mem_read_breakpoint
mem_read
breakpointは,実行中のプログラムまたはバイナリ解析によってメモリが読み込まれたときにトリガーされる.
mem_read
にbreakpointを使用し,state.mem
を使用してメモリアドレスからデータをロードしている場合,技術的にメモリを読み込んでいるのでbreakpointが発動することに注意が必要.
メモリからデータをロードし,設定したmem_read
breakpointを作動させないようにするには,キーワード引数にdisable_actions=True
とinspect=False
を指定して,state.memory.load
を使用する.
これはstate.find
にも当てはまり,同じキーワード引数を使ってmem_read
breakpointの発火を防ぐことができる.
Analyses
angrの目的は,バイナリプログラムの有用な解析を簡単に行うことである.
この目的のために,angrでは解析コードを共通のフォーマットでパッケージ化し,どのプロジェクトにも簡単に適用できるようにしている.
独自の解析を書くことについては後で説明するが,考え方としては,すべての解析はproject.analysis
の下に表示され(例えば,project.analysis.CFGFast()
),関数として呼び出され,解析結果のインスタンスを返すことができる.
Built-in_Analyses
Name | Description |
---|---|
CFGFast | プログラムの高速なControl Flow Graphを構築する |
CFGEmulated | プログラムの正確なControl Flow Graphを構築する |
VFG | プログラムの総すべての関数でVFAを実行し,Value Flow Graphを作成し,スタック変数を検出する |
DDG | Data Dependency Graphを算出し,ある値がどのstatementに依存しているかを判断できる |
BackwardSlice | あるターゲットに対するプログラムのBackward Sliceを計算する |
Identifier | CGCバイナリの共通ライブラリ関数の特定 |
More! | angrには多くの解析機能があり,そのほとんどが動作する.使い方を知りたい場合は,ドキュメントを要求するissueを書く必要がある |
Resilience
分析は回復力のあるように書くことができ,基本的にどんなエラーでもキャッチして記録する.
これらのエラーは,キャッチされた方法に応じ,analysisのerrors
またはnamed_errors
属性に記録される.
しかし,エラーが処理されないように,分析をfail fastモードで実行したい場合がある.
これを行うには,引数fail_fast=True
をanalysisのコンストラクタに渡すことができる.
Remarks
このドキュメントをここまで読めば,バイナリ解析を始めるために必要なangrの基本的なコンポーネントをすべて理解できたはずである.
究極的に,angrは単なるエミュレータである.
確かに環境を考慮した高度な計測が可能な非常にユニークなエミュレータだが,angrで行う作業の核心は,バイトコードの束がCPU上でどのように動作するかという知識を引き出すことである.
angrの設計では,特定の一般的な作業をより便利にするために,エミュレータの上にツールと抽象化を提供しようとしたが,SimStateを操作して.step()
の影響を観察するだけで解決できない問題はない.
本ドキュメントを読み進めていくと,より技術的なテーマや,複雑なシナリオのためにangrの動作を調整する方法が説明されている. これらの知識は,与えられた問題に対する解決策への最短経路を取ることができるように,angrの使用に役立つはずだが,最終的には,自由に使えるツールで創造性を発揮して問題を解決したいと思うだろう. 問題を解決し,定義された扱いやすい入力と出力を持つ形にすることができれば,angrを使用して目標を達成することができるが,その目標はバイナリを分析することである. angrが提供する抽象化やinstrumentationは,与えられたタスクに対するangrの使い方のすべてではない. angrは,あなたが望むように,統合的にもアドホックにも使えるように設計されている. 問題から解決への道筋が見えれば,それに従えばいい.
もちろん,angrのような巨大なテクノロジーに精通することは非常に困難である. そのためには,コミュニティ(angr slackが最適)でangrについて議論したり,問題を解決したりすることが必要になる.
CFG
angrには,バイナリプログラムのControl Flow Graphを復元する解析機能がある. これには,関数の境界の回復や,間接ジャンプやその他の有用なメタデータの推論も含まれる.
General_ideas
バイナリで行う基本的な解析は,Control Flow Graphである. CFGは,(概念的に)basic blockをノードとし,jump/call/retなどをエッジとするグラフである.
angrでは,静的なCFG(CFGFast)と動的なCFG(CFGEmulated)の2種類のCFGを生成することができる.
CFGFastは静的解析を用いてCFGを生成する. これにより,大幅に高速化されるが,理論的には,実行時にしか解決できない制御フローの遷移があるため,制限される. これは,他の一般的なリバースエンジニアリングツールが行うCFG解析と同じ種類のものであり,その結果はそれらの出力と同等である.
CFGEmulatedは,シンボリック実行を用いてCFGをキャプチャする. 理論的にはより正確だが,劇的に遅くなる. また,エミュレーションの精度に問題があるため,一般的に完全ではない(システムコール,ハードウェア機能の欠落など)
どのCFGを使うべきかわからない場合や,CFGEmulatedで問題がある場合は,まずCFGFastを試してみるのがよい.
CFGは次のようにして構築できる.
>>> import angr
# load your project
>>> p = angr.Project('/bin/true', load_options={'auto_load_libs': False})
# Generate a static CFG
>>> cfg = p.analyses.CFGFast()
# generate a dynamic CFG
>>> cfg = p.analyses.CFGEmulated(keep_state=True)
Using_the_CFG
CFGのその中核を成すのは,NetworkXのdi-graphである. つまり,通常のNetworkXのAPIのすべてが利用可能.
>>> print("This is the graph:", cfg.graph)
>>> print("It has %d nodes and %d edges" % (len(cfg.graph.nodes()), len(cfg.graph.edges())))
CFGのノードは,CFGNodeクラスのインスタンス. コンテキストの違いにより,あるbasic blockはグラフ内に複数のノードを持つことができる(複数のコンテキストに対して).
# this grabs *any* node at a given location:
>>> entry_node = cfg.get_any_node(p.entry)
# on the other hand, this grabs all of the nodes
>>> print("There were %d contexts for the entry block" % len(cfg.get_all_nodes(p.entry)))
# we can also look up predecessors and successors
>>> print("Predecessors of the entry point:", entry_node.predecessors)
>>> print("Successors of the entry point:", entry_node.successors)
>>> print("Successors (and type of jump) of the entry point:", [ jumpkind + " to " + str(node.addr) for node,jumpkind in cfg.get_successors_and_jumpkind(entry_node) ])
Viewing_the_CFG
CFGのレンダリングは難しい問題である. angrはCFG解析の出力をレンダリングするためのビルトインのメカニズムを提供しておらず,matplotlibのような伝統的なグラフレンダリングライブラリを使おうとすると,使い物にならない画像になってしまう. angrのCFGを見るための一つの解決策が,axtのangr-utilsリポジトリにある.
Shared_Libraries
CFG解析は,異なるバイナリオブジェクトのコードを区別しなり.
つまり,デフォルトではロードされた共有ライブラリを介した制御フローを解析しようとする.
これはほとんど意図された動作ではない.
なぜなら,これは解析時間をおそらく数日にまで延ばすことになるからである.
共有ライブラリを含まないバイナリをロードするには,Project
のコンストラクタに次のキーワード引数を追加する
load_options={'auto_load_libs': False}
Function_Manager
CFGの結果は,cfg.kb.functions
からアクセスできるFunction Managerと呼ばれるオブジェクトを生成する.
このオブジェクトの最も一般的な使用方法は,dictのようにアクセスすることである.
このオブジェクトは,アドレスをFunction
オブジェクトにマッピングし,Function
に関するプロパティを知ることができる.
>>> entry_func = cfg.kb.functions[p.entry]
Functionにはいくつかの重要なプロパティがある.
entry_func.block_addrs
は,その関数に属するbasic blockが始まるアドレスのsetentry_func.blocks
は,関数に属するbasic blockのsetで,capstoneを使って探索したり分解したりできるentry_func.string_references()
は,関数内の任意の時点で参照されたすべての定数文字列のlistを返す.文字列は(addr, string)
タプルとしてフォーマットされている.addr
はバイナリのデータセクションにある文字列のアドレス.string
は文字列の値を含むPythonの文字列entry_func.returning
は,その関数がreturnできるかどうかを示すbool値.Falseはすべてのパスでreturnしないことを示すentry_func.callable
は,この関数を参照するangrのCallable
オブジェクト.Pythonの引数を使ってPythonの関数のように呼び出すことができ,あたかも引数を使って関数を実行したかのような実際の結果(シンボリックな場合もある)を得る事ができる.entry_func.transition_graph
は,関数自体のcontrol flowを記述するNetworkX DiGraphである.これは,IDAが関数ごとに表示するControl Flow Graphに似ている.entry_func.name
は関数の名前entry_func.has_unresolved_calls
と,entry.has_unresolved_jumps
は,CFG内の不正確さを検出する事と関係がある.関節呼び出しやジャンプの対象が何であるか,解析では検出できないことがある.関数内でこのような事態が発生した場合,その関数の適切なhas_unresolved_*
がTrue
に設定される.entry_func.get_call_sites()
は,他の関数への呼び出しで終わるbasic blockのすべてのアドレスのlistを返す.entry_func.get_call_target(callsite_addr)
は,call site addressesのリストから,callsite_addr
が与えられると,そのcall siteがどこにcall outするかを返す.entry_func.get_call_return(callsite_addr)
は,callsite addressesのリストから,callsite_addr
が与えられると,そのcall siteがどこに戻るべきかを返す.
他にもいろいろある
CFGFast_details
CFGFastは,静的なControl Flowと関数の復元を行う. エントリポイント(またはユーザ定義のポイント)を起点として,おおよそ以下のような手順が実行される.
1) basic blockをVEX IRに変換し,すべての出口(jump,call,ret,次のブロックへの接続)を収集する. 2) 各出口について,この出口が定数アドレスの場合は,正しいタイプのCFGにエッジを追加し,目的のブロックを解析対象ブロックのsetに追加する. 3) 関数呼び出しの場合,宛先ブロックは新しい関数の開始点ともみなされる.対象となる関数がリターンすることがわかっている場合は,呼び出し後のブロックも解析される. 4) リターンの場合,現在の関数はリターンとマークされ,call graphとCFGの適切なエッジが更新される. 4) すべての間接ジャンプ(非定数の目的地を持つブロックの出口)に対して,間接ジャンプの解決が行われる.
Finding function starts
CFGFastは,関数の開始と終了を決定する複数の方法をサポートしている.
まず,バイナリのメインエントリーポイントが分析される. シンボルがあるバイナリ(stripされていないELFやPEのバイナリなど)では,すべての関数のシンボルが開始点として使用される. stripされたバイナリや,blob loader backendを使用してロードされたバイナリなど,シンボルのないバイナリの場合,CFGはバイナリのアーキテクチャに定義された関数プロローグのセットをスキャンする. 最後に,デフォルトでは,プロローグやシンボルに関係なく,バイナリのコードセクション全体が実行可能なコンテンツとしてスキャンされる.
これらに加えて,CFGEmulatedと同様に,関数の開始は,与えられたアーキテクチャ上のcall命令のターゲットである場合にも考慮される.
これらのオプションはすべて無効にすることができる.
FakeRets and function returns
関数呼び出しが観測された場合,まず呼び出し側の関数が最終的にリターンすると仮定し,それ以降のブロックを呼び出し側の関数の一部として扱る. この推測される制御フローのエッジはFakeRetとして知られている. calleeを分析した結果,これが真実ではないことがわかった場合,CFGを更新し,このFakeRetを削除し,それに応じてコールグラフとファンクションブロックを更新する. このように,CFGは2回復元される. このようにして,各関数のブロックのセットと,その関数がリターンするかどうかを復元し,直接伝播することができる.
Indirect Jump Resolution
TODO
Options
CFGFastを使用する際の便利なオプション
Option | Description |
---|---|
force_complete_scan | 関数検出のために,バイナリ全体をコードとして扱う. blob(コードとデータが混在している場合など)がある場合はオフにすべき(デフォルトではTrue) |
function_starts | 分析のエントリーポイントとして使用するアドレスのリスト |
normalize | 結果として得られた関数を正規化する(例えば,各basic blockは最大で1つの関数に属する,バックエッジはbasic blockの開始点を指すなど)(デフォルトではFalse) |
resolve_indirect_jumps | CFG作成時に発見されたすべての間接的なジャンプのターゲットを見つけるため,追加の分析を行う(デフォルトではTrue) |
more! | 最新のオプションについてはp.analyze.CFGFast のdocstringを参照 |
CFGEmulated_details
Options
CFGEmulatedの一般的なオプション
Option | Description |
---|---|
context_sensitivity_level | 分析のコンテキスト感度レベルを設定する.詳細については,後述のcontext sensitivity levelのセクションを参照.デフォルトでは1に設定されている. |
starts | 解析のエントリーポイントとして使用するアドレスのlist |
avoid_runs | 解析で無視するアドレスのlist |
call_depth | 解析の深さをいくつかの数の呼び出しに制限する.これは,特定の関数がどの関数に直接ジャンプできるかをチェックするのに便利(call_depth を1に設定することでできる) |
initial_state | initial stateをCFGに提供することができ,CFGはそのstateを解析中に使用する. |
keep_state | メモリを節約するために,各basic blockでのstateはデフォルトで破棄される.keep_state がTrue の場合,stateはCFGNodeに保存される. |
enable_symbolic_back_traversal | 間接的なジャンプを解消するための集中的な手法を可能にするかどうか |
enable_advanced_backward_slicing | 直接的なジャンプを解決するための別の集中的な手法を可能にするかどうか |
more! | 最新のオプションについては,p.analyze.CFGEmulated のdocstringを参照 |
Context Sensitivity Level
angrは,すべてのbasic blockを実行し,その結果を見ることでCFGを構築する. しかし,これにはいくつかの課題がある. 例えば,あるブロックが関数のreturnで終わる場合,そのbasic blockを含む関数のcalleeが異なれば,returnの対象も異なる.
context sensitivity levelとは,概念的には,コールスタックに残すべきそのような呼び出し元の数のことである. この概念を説明するために,次のようなコードを見てみる.
void error(char *error)
{
puts(error);
}
void alpha()
{
puts("alpha");
error("alpha!");
}
void beta()
{
puts("beta");
error("beta!");
}
void main()
{
alpha();
beta();
}
上記のサンプルには,main>alpha>puts
,main>alpha>error>puts
およびmain>beta>puts
,main>beta>error>puts
という4つのコールチェーンが含まれている.
この場合,angrはおそらく両方のコールチェーンを実行することができるが,大規模なバイナリでは実行不可能になる.
そこで,angrは,context sensitivity levelによって制限された状態でブロックを実行する.
つまり,各関数は,呼び出されるユニークなコンテキストごとに再分析される.
例えば,上記のputs()
関数は,異なるコンテキスト感度レベルを与えられた以下のコンテキストで分析される.
Level | Meaning | Contexts |
---|---|---|
0 | Callee-only | puts |
1 | One caller, plus callee | alpha>puts beta>puts error>puts |
2 | Two callers, plus callee | alpha>error>puts main>alpha>puts beta>error>puts main>beta>puts |
3 | Three callers, plus callee | main>alpha>error>puts main>alpha>puts main>beta>error>puts main>beta>puts |
context sensitivityのレベルを上げると,CFGからより多くの情報が得られるようになる.
例えば,context sensitivityが1の場合,CFGは,alpha
から呼び出されたputs
はalpha
に戻り,error
から呼び出されたputs
はerror
に戻る,というように表示する.
context sensitivityが0の場合,CFGは単にputs
がalpha
,beta
,error
に戻ることを示す.
これは,特にIDAで使用されているcontext sensitivityレベルと等しい.
context sensitivity levelを上げると,解析時間が指数関数的に増加するというデメリットがある.
Backward_Slicing
program sliceとは,元のプログラムから,通常0個以上のステートメントを削除して得られるステートメントのサブセットのことである. スライスは,デバッグやプログラムの理解に役立つことが多い. 例えば,プログラムスライスでは,変数のソースを特定するのが容易である.
backward sliceは,プログラム内のターゲットから構築され,このスライス内のすべてのデータフローはtargetで終了する.
angrにはBackwardSlice
と呼ばれる,後ろ向きのプログラムスライスを構築するための解析が組み込まれている.
このセクションでは,angrのBackwardSlice
解析のハウツーを説明し,その後,実装上の選択と制限について,いくつかの詳細な議論を行う.
First_Step_First
BackwardSlice
を構築するには,以下の情報を入力する必要がある.
- Required CFG: プログラムのCFG.このCFGは,
CFGEmulated
で生成されたものでなければならない. - Required Target: backward sliceが終了するtarget.
- Optional CDG: CFGから派生したControl Dependence Graph.
- Optional DDG: CFG上に構築されたData Dependence Graph.
BackwardSlice
は,以下のコードで構築できる
import angr
# Load the project
>>> b = angr.Project("examples/fauxware/fauxware", load_options={"auto_load_libs": False})
# Generate a CFG first. In order to generate data dependence graph afterwards, you’ll have to:
# - keep all input states by specifying keep_state=True.
# - store memory, register and temporary values accesses by adding the angr.options.refs option set.
# Feel free to provide more parameters (for example, context_sensitivity_level) for CFG
# recovery based on your needs.
>>> cfg = b.analyses.CFGEmulated(keep_state=True,
... state_add_options=angr.sim_options.refs,
... context_sensitivity_level=2)
# Generate the control dependence graph
>>> cdg = b.analyses.CDG(cfg)
# Build the data dependence graph. It might take a while. Be patient!
>>> ddg = b.analyses.DDG(cfg)
# See where we wanna go... let’s go to the exit() call, which is modeled as a
# SimProcedure.
>>> target_func = cfg.kb.functions.function(name="exit")
# We need the CFGNode instance
>>> target_node = cfg.get_any_node(target_func.addr)
# Let’s get a BackwardSlice out of them!
# `targets` is a list of objects, where each one is either a CodeLocation
# object, or a tuple of CFGNode instance and a statement ID. Setting statement
# ID to -1 means the very beginning of that CFGNode. A SimProcedure does not
# have any statement, so you should always specify -1 for it.
>>> bs = b.analyses.BackwardSlice(cfg, cdg=cdg, ddg=ddg, targets=[ (target_node, -1) ])
# Here is our awesome program slice!
>>> print(bs)
DDGを得ることが困難な場合や,単にCFGの上にプログラムスライスを構築したい場合もある. これが,DDGがオプションのパラメータである理由である. こうすることで,CFGのみに基づいてBackwardSliceを構築することができる.
>>> bs = b.analyses.BackwardSlice(cfg, control_flow_slice=True)
BackwardSlice (to [(<CFGNode exit (0x10000a0) [0]>, -1)])
Using_the_BackwardSlice_Object
BackwardSlice
オブジェクトを使用する前に,このクラスの設計はかなり恣意的であり,近い将来変更される可能性があることを認識しておく必要がある.
Members
構築後のBackwardSlice
は,プログラムスライスを記述する以下のメンバを持っている.
Member | Mode | Meaning |
---|---|---|
runs_in_slice | CFG-only | プログラムスライス内のブロックとSimProceduresのアドレス,およびそれらの間のトランジションを示すnetworkx.DiGraph のインスタンス |
cfg_nodes_in_slice | CFG-only | プログラムスライス内のCFGNodeとその間のトランジションを示すnetworkx.DiGraph のインスタンス |
chosen_statements | With DDG | basic blockのアドレスを,プログラムスライスの一部であるstatement IDのlistにマッピングするdict |
chosen_exits | With DDG | basic blockのアドレスをexitsのlistにマッピングするdict.このリストの各exitは,プログラムスライスの有効な遷移である. |
chosen_exit
の各exitは,statement IDとターゲットアドレスのリストを含むタプルである.
例えば,exitは以下のようになる
(35, [ 0x400020 ])
exitが,basic blockのデフォルトの出口の場合は,以下のようになる
(“default”, [ 0x400085 ])
Export an Annotated Control Flow Graph
User-friendly Representation
Implementation Choices
Limitations
Completeness
Soundness
TODO
Function_Identifier
この識別子は,テストケースを使用して,CGC バイナリの一般的なライブラリ関数を識別する. この識別子は,スタック変数や引数に関するいくつかの基本的な情報を見つけることで前処理を行う. スタック変数についての情報は,他のプロジェクトでも一般的に有用である.
>>> import angr
# get all the matches
>>> p = angr.Project("../binaries/tests/i386/identifiable")
# note analysis is executed via the Identifier call
>>> idfer = p.analyses.Identifier()
>>> for funcInfo in idfer.func_info:
... print(hex(funcInfo.addr), funcInfo.name)
0x8048e60 memcmp
0x8048ef0 memcpy
0x8048f60 memmove
0x8049030 memset
0x8049320 fdprintf
0x8049a70 sprintf
0x8049f40 strcasecmp
0x804a0f0 strcmp
0x804a190 strcpy
0x804a260 strlen
0x804a3d0 strncmp
0x804a620 strtol
0x804aa00 strtol
0x80485b0 free
0x804aab0 free
0x804aad0 free
0x8048660 malloc
0x80485b0 free
Advanced_Topics
- Gotchas
- The_Whole_Pipeline
- The_Mixin_Pattern
- Optimizing_Symbolic_Execution
- The_Emulated_Filesystem
- Intermediate_Representation
- Working_with_Data_and_Conventions
- Claripy
- Symbolic_Memory_Addressing
- Java_Symbolic_Execution
- Symbion
Gotchas
このセクションでは,angrのユーザーや被害者がよく遭遇する問題をまとめている.
SimProcedure_inaccuracy
シンボリックな実行をより扱いやすくするために,angrは一般的なライブラリ関数をPythonで書かれたサマリで置き換える.
これらのサマリをSimProceduresと呼んでいる.
SimProceduresは,例えばstrlen
がシンボリックな文字列上で実行された場合に生じるパスの爆発的な増加を緩和することができる.
残念ながら,angrのSimProcedureは完璧ではない. angrが予期せぬ動作をした場合,バグのある/不完全なSimProcedureが原因である可能性がある. その場合,いくつかできることがある
- SimProcedureを無効にする(
angr.Project
クラスにオプションを渡すことで,特定のSimProcedureを除外することがでる).この方法は,問題の関数への入力を注意深く制限しない限り,パスの爆発につながる可能性が高いという欠点がある.パスの爆発は,他のangr機能(Veritestingなど)で部分的に緩和することができる. - SimProcedureを,問題となっている状況に直接書かれたものに置き換える.例えば,angrの
scanf
の実装は完全ではないが,単一の既知のフォーマット文字列をサポートするだけであれば,それを行うフックを書くことができる. - SimProcedureの修正
Unsupported_syscalls
システムコールは,SimProceduresとしても実装されている. 残念ながら,angrではまだ実装されていないシステムコールがある. サポートされていないシステムコールには,いくつかの回避策がある.
- システムコールを実装する.
- システムコールのcallsiteを(
project.hook
をつかって)フックし,必要な修正をその場で行う. state.posix.queued_syscall_returns
listを使って,システムコールの戻り値をキューイングする.戻り値がキューイングされた場合,システムコールは実行されず,代わりにその値が使用される.さらに,関数をreturn valueとして代わりにキューに入れる事ができ,システムコールがトリガーされたときに,その関数がstateに適用されることになる.
Symbolic_memory_model
angrが使用するデフォルトのメモリモデルは,Mayhemを参考にしている.
このメモリモデルは,限られたシンボリックな読み書きをサポートする.
読み込みのメモリインデックスがシンボリックで,その値の範囲が広すぎる場合,そのインデックスは単一の値に具体化される.
書き込みのメモリインデックスが全くシンボリックでない場合は,インデックスは単一の値に具体化される.
これは,state.memory.MEMORY
のmemory concretization strategiesを変更することで設定できる.
Symbolic_lengths
SimProcedures,特にread()
やwrite()
のようなシステムコールでは,バッファの長さがシンボリックであるという状況に遭遇することがある.
多くの場合,この長さは完全に具体化されるか,後の実行ステップで遡及的に具体化される.
そうでない場合でも,ソースファイルやデスティネーションファイルの見た目が少し変になってしまうことがある.
The_Whole_Pipeline
Understanding_the_Execution_Pipeline
ここまで来れば,angrが非常に柔軟で強力なinstrumentが可能なエミュレータであることがわかるだろう.
angrを最大限に活用するためには,simgr.run()
と言ったときに各段階で何が起こっているのかを知っておく必要がある.
このドキュメントはより高度な内容を意図している.
私たちが話していることを理解するためには,SimulationManager
,ExplorationTechnique
,SimState
,およびSimEngine
の機能と意図を理解する必要がある.
これを理解するために,angrのソースを開いておくとよいだろう.
各ステップでは,各関数が**kwargs
を受け取り,階層内の次の関数に渡すので,階層内のどのポイントにもパラメータを渡すことができ,それが下のすべてに伝わる.
Simulation Managers
run()
SimulationManager.run()
はいくつかのオプションのパラメータを取るが,これらはすべてステッピングループから抜け出すタイミングを制御する.
n
は即座に使用され,run
関数はループしてstep()
関数を呼び出し,n
ステップ実行されるか,他の終了条件が発生するまですべてのパラメータを渡す.
n
が指定されていない場合は,until
関数が指定されていない限り,デフォルトで1
が設定される.
さらに,使用しているstashが空になった場合,実行を終了しなければならないことも考慮される.
要約すると,run()
を呼び出すと,以下のいずれかになるまで,step()
がループで呼び出される.
n
回のステップが経過するuntil
関数がtrueを返す- exploration techniquesの
completion()
フック(SimulationManager.completion_mode
パラメータ/アトリビュートを介して結合されたもので,デフォルトではany
builtin functionとなっているが,例えばall
に変更することも可能)が解析の完了を示す. - 実行中のstashが空になる
An aside: explore()
SimulationManager.explore()
はrun()
の非常に薄いラッパーで,Explorer
のexploration techniquesを追加しているが,これは1回限りの探索を行うことが非常に一般的なアクションだからである.
そのコードの全体像は以下の通り.
num_find += len(self._stashes[find_stash]) if find_stash in self._stashes else 0
tech = self.use_technique(Explorer(find, avoid, find_stash, avoid_stash, cfg, num_find))
try:
self.run(stash=stash, n=n, **kwargs)
finally:
self.remove_technique(tech)
return self
Exploration technique hooking
ここから先は,SimulationManager内のすべての関数がexploration techniquesによってinstrumentationされる.
具体的な仕組みとしては,SimulationManager.use_technique()
を呼び出すと,angrがシミュレーションマネージャをモンキーパッチし,exploration techniqueの本体に実装されている関数を,最初にexploration techniqueの関数を呼び出し,2回目の呼び出しで元の関数を呼び出す関数に置き換える.
これは実装がやや面倒で,スレッドセーフではないが,exploration techniqueがステッピングの動作を計測するためのクリーンで強力なインターフェースを提供する.
さらに,モンキーパッチされた関数は,次に適用されるフックの元の関数になるだけなので,複数のexploration techniqueが同じ関数をフックすることができる.
step()
degenerateなケースを処理するために,step()
には多くの複雑なロジックがある.
主に,deadended
stashの生成,save_unsat
オプションの実装,filter()
exploration techniqueフックの呼び出しなどがある.
しかし,それ以外のほとんどのロジックは,stash
引数で指定されたstashをループさせ,各stateでstep_state()
を呼び出し,step_state()
のdict結果をstashリストに適用するというものである.
最後に,step_func
パラメータが指定されている場合は,ステップが終了する前にSimulationManagerをパラメータとして呼び出す.
step_state()
SimSuccessors
オブジェクトを返すsuccessor()
を呼び出し,それをstash名とそのstashに追加されるべき新しいstateをマッピングするdictに変換する.
また,エラー処理も実装されている.
successors()
がエラーをthrowした場合,それはキャッチされ,ErrorRecord
がSimulationManager.errored
に挿入される.
successors()
successors()
は,exploration techniqueを用いて,あるstateを取得し,それをstepさせ,そのsuccessorを分類したSimSuccessors
オブジェクトを
あらゆるstash logicとは無関係に返すことになっている.
successor_func
パラメタが提供された場合,それが使用され,その戻り値が直接返される.
このパラメタが提供されていない場合は,project.factory.successor
メソッドを使用して,stateを前に進め,SimSuccessors
を取得する.
The Engine
実際にsuccessorを生成する際には,実際に実行する方法を把握する必要がある.
angrドキュメントでは,この章にたどり着くまでに,SimEngine
とは,stateを取得してそのsuccessorを生成する方法を知っているデバイスであることがわかるように構成されている.
1つのプロジェクトには1つのdefault engineしか無いが,ステップの実行にどのエンジンを使用するかを指定するために,engine
パラメタを提供することができる.
このパラメタは,.step()
,.explore()
,.run()
など,実行を開始するすべてのものの先頭で指定することができ,それらはこのレベルまでフィルタリングされることに留意するべきである.
追加のパラメタは,意図したエンジンの部分に到達するまで,下に渡され続ける.
エンジンは,理解できないパラメタを破棄する.
一般的に,エンジンのメインのエントリーポイントはSimEngine.process()
であり,好きな結果を返すことができる.
しかし,SimulationManagerのために,エンジンはSuccessorsMixin
を使用する必要がある.
このメソッドは,SimSuccessors
オブジェクトを作成し,他のMixinがそれを埋める事が出来るようにprocess_successors()
を呼び出す.
angrのデフォルトエンジンであるUberEngine
には,process_successors()
メソッドを提供するいくつかのmixinが含まれている.
SimEngineFailure
: 縮退したジャンクションのあるステッピング・ステートを処理する.SimEngineSyscall
: システムコールが実行され,その実行が必要なステッピング状態を処理する.HooksMixin
: フックされたアドレスに到達し,フックを実行する必要のあるステッピング状態を処理する.SimEngineUnicorn
: unicornエンジンでマシンコードを実行する.SootMixin
: SOOT IRを介してjavaバイトコードを実行する.HeavyVEXMixin
: VEX IR経由でマシンコードを実行する.
これらの各mixinは,現在のstateを処理できる場合はSimSuccessors
オブジェクトを埋めるように実装され,そうでない場合はsuper()
を呼び出してスタックの次のクラスに仕事を渡す.
Engine mixins
SimEngineFailure
はエラーケースを処理する.
SimEngineFailure
は,前のJumpkind
がIjk_EmFail
, Ijk_MapFail
, Ijk_Sig*
, Ijk_NoDecode
(ただし,アドレスがフックされていない場合のみ), Ijk_Exit
のいずれかである場合にのみ使用される.
最初の4つのケースでは,そのアクションは例外を発生させることで,最後のケースでは,そのアクションは単に後継者を作らないこと.
SimEngineSyscall
はsyscallをサービスする.
前のジャンプキンドがIjk_Sys*
のような形式の場合に使用される.
このsyscallに対応するために実行されるべきSimProcedure
を取得するためにSimOS
を呼び出し,それを実行することで動作する.
HooksMixin
は,angrのフッキング機能を提供する.
これは,あるstateがフックされたアドレスにあり,前のJumpkindがIjk_NoHook
でない場合に使用される.
これは単純に,関連するSimProcedureを検索して,state上でそれを実行する.
また,パラメタprocedure
を取り,アドレスがフックされていなくても,与えられたプロシージャを現在のステップで実行する.
SimEngineUnicorn
は,Unicornエンジンで具体的な実行を行う.
stateオプションのo.UNICORN
が有効で,最大の効率を得るために設計された無数の他の条件(後述)が満たされているときに使用される.
SootMixin
はSOOT IR上で実行する.
javaのバイトコードを解析するのでなければ,あまり重要ではないが,その場合は非常に重要である.
SimEngineVEX
は大物.
前者のいずれかが使用できない場合に使用される.
SimEngineVEXは,現在のアドレスからIRSBにバイトを取り込み,そのIRSBをシンボリックに実行する.
この処理を制御できるパラメータは膨大な数に上るので,それらを説明したAPIリファレンスにリンクするだけにしておく.
SimEngineVEXがIRSBを読み込む正確なプロセスは少し複雑だが,基本的にはブロックのすべてのステートメントを順に実行する. このコードは,angrのシンボリック実行の真の内部コアを見たい場合に読む価値がある.
When using Unicorn Engine
o.UNICORN
stateオプションを追加すると,各ステップでSimEngineUnicornが呼び出され,具体的な実行にUnicornを使用することが許可されているかどうかを確認する.
本当にやりたいことは,事前に定義されたo.unicorn
(小文字)オプションのsetをstateに追加すること.
unicorn = { UNICORN, UNICORN_SYM_REGS_SUPPORT, INITIALIZE_ZERO_REGISTERS, UNICORN_HANDLE_TRANSMIT_SYSCALL }
これにより,いくつかの追加機能やデフォルト設定が有効になり,使用感が大きく向上する.
さらに,state.unicorn
プラグインには,調整できるオプションがたくさんある.
unicornがどのように動作するかを理解するには,unicornのサンプル実行時のログ出力
logging.getLogger('angr.engine.unicorn_engine').setLevel('DEBUG');
logging.getLogger('angr.state_plugins.unicorn_engine').setLevel('DEBUG')
を見るのが良い.
INFO | 2017-02-25 08:19:48,012 | angr.state_plugins.unicorn | started emulation at 0x4012f9 (1000000 steps)
ここで,angrはユニコーンエンジンに切り替わり,0x4012f9
のbasic blockからスタートする.
最大ステップ数は1000000
に設定されているので,実行が1000000
ブロックの間ユニコーンに留まると,自動的にポップアウトする.
これは無限ループに陥らないようにするため.
ブロック数は,state.unicorn.max_steps
変数で設定できる.
INFO | 2017-02-25 08:19:48,014 | angr.state_plugins.unicorn | mmap [0x401000, 0x401fff], 5 (symbolic)
INFO | 2017-02-25 08:19:48,016 | angr.state_plugins.unicorn | mmap [0x7fffffffffe0000, 0x7fffffffffeffff], 3 (symbolic)
INFO | 2017-02-25 08:19:48,019 | angr.state_plugins.unicorn | mmap [0x6010000, 0x601ffff], 3
INFO | 2017-02-25 08:19:48,022 | angr.state_plugins.unicorn | mmap [0x602000, 0x602fff], 3 (symbolic)
INFO | 2017-02-25 08:19:48,023 | angr.state_plugins.unicorn | mmap [0x400000, 0x400fff], 5
INFO | 2017-02-25 08:19:48,025 | angr.state_plugins.unicorn | mmap [0x7000000, 0x7000fff], 5
angrは,ユニコーンエンジンがアクセスするデータに対し,アクセスされるたびに遅延マッピングを行う.
0x401000
は実行中の命令のページ,0x7fffffffe0000
はスタック,といった具合に.
これらのページの中にはシンボリックなものもあり,アクセスされるとUnicornから実行が中断されるようなデータが少なくとも含まれていることを意味する.
INFO | 2017-02-25 08:19:48,037 | angr.state_plugins.unicorn | finished emulation at 0x7000080 after 3 steps: STOP_STOPPOINT
実行は3つのbasic blockの間Unicornに留まり(必要な設定を考慮すると計算上の無駄),その後,simprocedureの場所に到達し,angrでsimprocを実行するために飛び出す.
INFO | 2017-02-25 08:19:48,076 | angr.state_plugins.unicorn | started emulation at 0x40175d (1000000 steps)
INFO | 2017-02-25 08:19:48,077 | angr.state_plugins.unicorn | mmap [0x401000, 0x401fff], 5 (symbolic)
INFO | 2017-02-25 08:19:48,079 | angr.state_plugins.unicorn | mmap [0x7fffffffffe0000, 0x7fffffffffeffff], 3 (symbolic)
INFO | 2017-02-25 08:19:48,081 | angr.state_plugins.unicorn | mmap [0x6010000, 0x601ffff], 3
simprocedureの後,実行は再びUnicornにジャンプする.
WARNING | 2017-02-25 08:19:48,082 | angr.state_plugins.unicorn | fetching empty page [0x0, 0xfff]
INFO | 2017-02-25 08:19:48,103 | angr.state_plugins.unicorn | finished emulation at 0x401777 after 1 steps: STOP_EXECNONE
バイナリがゼロページにアクセスしたため,実行はほとんどすぐにUnicornから返ってくる.
INFO | 2017-02-25 08:19:48,120 | angr.engines.unicorn_engine | not enough runs since last unicorn (100)
INFO | 2017-02-25 08:19:48,125 | angr.engines.unicorn_engine | not enough runs since last unicorn (99)
unicornの実行がsimprocedureやsyscall以外の理由で中止されたときに,unicornに戻る前に特定の条件が成立するのを待つクールダウン(state.unicorn
プラグインのattribute)がある.
ここでは,100ブロックが実行されてからジャンプバックするという条件を待っている.
The_Mixin_Pattern
angrのより深い部分に集中して取り組もうとしているのであれば,よく使われるデザインパターンの一つであるmixinパターンを理解する必要がある.
簡単に説明すると,mixinパターンとは,pythonのサブクラス化機能をIS-A関係(ChildはPersonの一種)を実装するために使用するのではなく,よりモジュール化された保守性の高いコードを作るために,1つの型の機能の一部を異なるクラスに実装することである. ここでは,mixinパターンの例を紹介する.
ass Base:
def add_one(self, v):
return v + 1
class StringsMixin(Base):
def add_one(self, v):
coerce = type(v) is str
if coerce:
v = int(v)
result = super().add_one(v)
if coerce:
result = str(result)
return result
class ArraysMixin(Base):
def add_one(self, v):
if type(v) is list:
return [super().add_one(v_x) for v_x in v]
else:
return super().add_one(v)
class FinalClass(ArraysMixin, StringsMixin, Base):
pass
この構造により,Base
クラスに非常にシンプルなインターフェイスを定義し,2つのmixinを mixing inすることで,同じインターフェイスを持ちながら機能を追加したFinalClass
を作成することができる.
これはpythonの強力な多重継承モデルによって実現されており,Method Resolution Order(MRO)を作成することでメソッドのディスパッチを処理する.
MROは意外にもリストで,super()
の呼び出しによって実行されるメソッドの呼び出し順序を決定する.
クラスのMROは次のように見ることができる.
FinalClass.__mro__
(FinalClass, ArraysMixin, StringsMixin, Base, object)
つまり,FinalClass
のインスタンスを取ってadd_one()
を呼び出すと,pythonはまずFinalClass
がadd_one
を定義しているかどうかをチェックし,次にArraysMixin
をチェックする,というように,順番にチェックしていく.
さらに,ArraysMixin
がsuper().add_one()
を呼び出すと,pythonはMROの中でArraysMixin
をスキップして,まずStringsMixin
がadd_one
を定義しているかどうかをチェックする.
多重継承はサブクラスの関係に奇妙な依存関係を作り出す可能性があるため,MROを生成するためのルールや,ミックスインの組み合わせが許されるかどうかを判断するためのルールがある. これは,互いに依存関係にある多くのmixinを含む複雑なクラスを構築する際に理解する必要がある. つまり,左から右へ,深さ優先で,複数のサブクラスで共有されるベースクラス(継承グラフのダイヤモンドパターンのマージポイント)は,深さ優先の検索で遭遇する最後のポイントまで延期する. 例えば,A, B(A), C(B), D(A), E(C, D)というクラスがあった場合,メソッドの解決順序はE, C, B, D, Aとなる. MROが曖昧になるようなケースがあった場合,クラスの構築は不正であり,import時に例外が発生する.
Mixins in Claripy Solvers
yan please write something here
Mixins_in_angr_Engines
SimEngineのmainのエントリーポイントはprocess()
だが,それが何をするかはどうやって決めるのか?
SimEngineとその仲間たちでは,機能の一部を静的解析とシンボリック解析の間で再利用できるようにするために,mixinモデルが使われている.
デフォルトのエンジンであるUberEngine
は,以下のように定義されている.
class UberEngine(SimEngineFailure, SimEngineSyscall, HooksMixin, SimEngineUnicorn, SuperFastpathMixin, TrackActionsMixin, SimInspectMixin, HeavyResilienceMixin, SootMixin, HeavyVEXMixin):
pass
これらのmixinは,それぞれ異なる媒体での実行や,instrumentation機能の追加を提供する.
ここには明示的に記載されていないが,この階層には暗黙的にベースクラスが存在し,このクラスの処理方法が設定されている.
これらのmixinのほとんどは,SuccessorsMixin
を継承しており,基本的なprocess()
の実装を提供している.
この関数は,他のmixinが埋めるべきSimSuccessorsを設定し,実行モードを提供する各mixinが実装するprocess_successors()
を呼び出す.
mixinがそのステップを処理できる場合は処理して戻り,そうでない場合はsuper().process_successors()
を呼び出す.
このようにして,エンジンクラスのMROは,エンジンの部品の優先順位を決定している.
HeavyVEXMixin and friends
最後のmixinであるHeavyVEXMixin
について,もう少し詳しく見てみる.
angrのengines
サブモジュールのモジュール階層を見ると,vex
サブモジュールには,特定のstateタイプやデータタイプにどれだけ強く結びついているかによって整理された,たくさんのピースがあることがわかる.
heavy VEX mixinは,これらの要素の集大成ともいえるものである.
その定義を見てみよう.
class HeavyVEXMixin(SuccessorsMixin, ClaripyDataMixin, SimStateStorageMixin, VEXMixin, VEXLifter):
...
# a WHOLE lot of implementation
つまり,heavy VEX mixinは,SimState上で完全にinstrumentedなシンボリック実行を提供することを目的としている. これは何を意味するのか,mixinが物語っている.
まず,通常のVEXMixin
.このmixinは,VEXブロックを処理するための最も基本的なフレームワークを提供するように設計されている.
そのソースコードを見てみる.
このmixinの主な目的は,VEX IRSBの予備的な処理を行い,その処理をミックスインが提供するメソッドにディスパッチすることである.
pass
またはreturn NotImpremented
なメソッドでは,stateのタイプやstate内のデータワードのタイプを一切想定していないことに注意する必要がある.
この仕事は他のmixinに委ねられており,VEXMixin
はVEXブロックに関する文字通りあらゆる分析のための適切なベースクラスとなっている.
次に興味深いのはClaripyDataMixin
で,そのソースコードはここ.
このMixinは,Claripy ASTのドメイン上で実行しているという事実を実際に統合する.
VEXMixin
では実装されていないいくつかのメソッドを実装することで,これを実現している.
最も重要なのは,ITE式,すべての操作,およびclean helper.
SimStateに実際に触れるとどうなるかという点では,SimStateStorageMixin
は,メモリ書き込み等のためのVEXMixin
のインターフェースと,メモリ書き込み等のためのSimStateのインターフェースとの間の接着剤を提供する.
これは,ClaripyDataMixin
との間の小さな相互作用を除いて,特に目立たないものである.
また,Claripy mixinは,bitvector型と浮動小数点型の間で変換する目的で,メモリ/レジスタのread/write関数をオーバーライドしている.
このため,MROではclaripy mixinがstorage mixinの前になければならない.
これは,このページの最初にあるadd_one
の例のような相互作用で,1つのmixinが別のmixinのデータフィルタリング層として機能する.
Instrumenting the data layer
ここで,HeavyVEXMixin
には含まれておらず,UberEngine
の式に明示的に組み込まれているMixin,TrackActionsMixin
に注目してみる.
このmixinはSimActionsを実装したもので,angrの用語で言うところのデータフローのトラッキングである.
もう一度,ソースコードを見る.
その方法は,データフローに関する追加情報を渡すために,データレイヤーをラップしたりアンラップしたりすることである.
例えば,RdTmp
の実装方法を見てみる.
これは,データフローモデルで一時的な変数をアトミックにしたいかどうかによるが,読み込まれたtmp
だけか,tmp
に書き込まれた値の依存関係になる.
このパターンは,このmixinが触れるすべてのメソッドで続いている. 受け取った式は,式とその依存関係に展開され,結果はその依存関係と一緒にパッケージ化されてから返されなければならない. これが機能するのは,上のmixinがどのようなデータを受け渡しているかを想定しておらず,下のmixinが依存関係を一切見ないからである. 実際,このようなwrap-unwrapトリックを実行するmixinが複数存在しても,それらは平和に共存することができる.
このようにデータ層をinstrumentするmixinは,式の値を受け取ったり返したりするすべてのメソッドをオーバーライドする義務がある.
たとえ式に対してwrap/unwrap以外の操作を行わないとしても.
その理由を理解するために,mixinが_handle_vex_cons
t式をオーバーライドしていないので,即時の値のロードには依存性が注釈されていないことを想像する.
handle_vex_const
を提供するmixinから返される式の値は,(expression, deps)
のタプルではなく,単なる式になる.
この実行がWrTmp(t0, Const(0))
のコンテキストで行われていると想像する.
const式は書き込み先のtmpの識別子と一緒にWrTmp
ハンドラに渡される.
しかし,_handle_vex_stmt_WrTmp
は,データ層に触れるmixinにオーバーライドされるので,depsを含むタプルが渡されることを期待しており,タプルではない値を展開しようとするとクラッシュする.
このようにして,データ層を触るmixinは,pythonの存在しない型システムの中で実際に契約を作っているようなものだと想像できる.
Mixins in the memory model
audrey please write something here. or fish, I’m not picky
Optimizing_Symbolic_Execution
解析ツールやエミュレータとしてのangrの性能は,その多くがpythonで書かれているという事実によって,大きなハンディキャップを負っている. それでも,angrをより速く,より軽くするための最適化や調整はたくさんある.
General_speed_tips
- pypyを使う. 10倍早くなる場合もある
- 必要な
SimEngine mixin
のみを使用する. SimEngineでは,新しいクラスを構築することで機能を追加したり削除したりできるmixinモデルを採用している.デフォルトのエンジンは,可能な限りの機能をミックスしており,その結果,必要以上に遅くなっている.UberEngine
(デフォルトのSimEngine)の定義を見て,その宣言をコピーし,必要のない機能を提供するすべてのベースクラスを削除する. - 必要ない共有ライブラリをロードしない angrのデフォルト設定では,OSのライブラリから直接読み込むことも含めて,読み込んだバイナリと互換性のある共有ライブラリを何としても見つけようとする. これは,多くの場合,物事を複雑にする. 基本的なシンボリック実行よりも抽象度の高い解析を行う場合,特に制御フローグラフの構築を行う場合は,精度を犠牲にして扱いやすさを追求した方が良い. angrは,存在しない関数をライブラリから呼び出したときに,正常な動作をするように適切な仕事をしてくれる.
- hookとSimProceduresを使う 共有ライブラリを有効にしている場合,複雑なライブラリ関数に飛び込む際には,必ずSimProceduresを書いておきたい. このプロジェクトに自律性が要求されていない場合は,分析がうまくいかない個々の問題点を切り分けて,フックでまとめることができる.
- SimInspectを使う SimInspectは,angrの最も使われていない機能であり,最も強力な機能の一つ. メモリインデックスの解決(angr解析で最も遅い部分であることが多い)を含め,angrのほぼすべての動作をフックして変更することができる.
- concretization strategyを書く メモリインデックス解決の問題をより強力に解決する方法として,concretization strategyがある.
- Replacement Solverを使う
これは
angr.options.REPLACEMENT_SOLVER
stateオプションで有効にできる. replacement solverでは,解答時に適用されるASTの置換を指定することができる. 置換を追加して,解答時にすべてのシンボリックデータが具象データに置き換えられるようにすれば,実行時間が大幅に短縮される. 置換を追加するためのAPIは,state.se._solver.add_replacement(old, new)
. replacement solverは少し扱いが難しく,いくつかの問題があるが,間違いなく役に立つ.
If you’re performing lots of concrete or partially-concrete execution
unicorn engine
を使う unicorn engineがインストール済みならば,angrをビルドする事でconcreteのエミュレーションにunicornを利用することができる.
Comments