Top Level Interface
开始之前(Before You Start)
我们预期的angr主要的应用场景是Ipython(或者其他python命令行解释器)。当你不确定可以用什么接口时,tab键的 补全往往能帮助到你。
有时Ipython中的tab补全会很慢。我们发现以下解决方法很有效且不会影响补全功能的完整性:
1
2
3
4
|
# 将此文件放在Ipython配置文件的启动目录中可以避免每次都单独1运行它
import IPython
py = IPython.get_ipython()
py.Completer.use_jedi = False
|
核心概念(Core Concepts)
在开始使用angr之前,你将会对angr的基本概念和如何构造一个angr对象有一个基本的概览。我们将通过二进制程序加载后直接可用的接口来说明这些概念。
你使用angr做的第一件事往往是把二进制程序加载到工程中,我们以/bin/true
为例
1
2
|
>>> import angr
>>> proj = angr.Project('/bin/true')
|
在angr中,一个project是你所能控制的基本单元,通过project,你将能对刚刚加载的二进制程序进行分析和模拟。几乎你在angr项目中使用的每一个对象都依赖于某种形式的project而存在
基本属性(Basic properties)
首先,project有一些基本属性:它的CPU架构,它的文件名和程序入口点。
1
2
3
4
5
6
7
|
>>> import monkeyhex # 用来将数字结果转化为16进制
>>> proj.arch
<Arch AMD64(LE)>
>>> proj.entry
0x401670
>>> proj.filename
'/bin/true'
|
加载器(The loader)
从一个二进制程序到它在虚拟地址空间的映射是十分复杂的!我们有一个叫CLE
(Christophe’s Loader for Everything) 的模块去解决这个问题。CLE就是一种加载器,可以通过.loader
属性调用。我们将在后面详细了解如何使用它,但现在你只需知道你可以用它查看angr随着你的程序加载的动态链接库(shared libraries)并且执行一些对于他们地址空间的基本的查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
>>> proj.loader
<loaded true, maps [0x400000:0x5004000]>
>>> proj.loader.shared_objects # 或许和你的看起来有点不一样
{'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 # 我们已经在项目中加载了几个二进制程序。这里是主要的一个
<ELF Object true, maps [0x400000:0x60721f]>
>>> proj.loader.main_object.execstack # 查询示例:是否存在可执行栈段
False
>>> proj.loader.main_object.pic # 查询示例:这个二进制程序是地址无关代码吗?
# pic(position-independent code)指可在主存储器中任意位置正确执行,而不受其绝对地址影响的一种机器码,常用于动态链接库
|
工厂(The Factory)
在angr中有许多类,而它们中的大多数需要一个project去把他们实例化。我们并不会让你到处传递project,而是提供了project.factory
,其中包含了一些你会频繁使用到的、常见对象的构造器。
本节还将介绍angr的一些基本概念。
块(Blocks)
首先,我们有project.factory.block()
,常常用来从给定地址提取基本代码块。这是一个重要的事实——angr以块为基本单位分析代码。你会得到一个block对象,其中包含了许多关于这个代码块的有趣的东西
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
>>> block = proj.factory.block(proj.entry) # 从程序的入口点取出一个代码块
<Block for 0x401670, 42 bytes>
>>> block.pp() # pretty-print 向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 # 在这个代码块中共有多少条指令?
0xb
>>> block.instruction_addrs # 这个代码块中指令的地址分别是多少?
[0x401670, 0x401672, 0x401675, 0x401676, 0x401679, 0x40167d, 0x40167e, 0x40167f, 0x401686, 0x40168d, 0x401694]
|
更多的,你可以使用block对象获得块代码的其他表示形式
1
2
3
4
|
>>> block.capstone # capstone disassembly
<CapstoneBlock for 0x401670>
>>> block.vex # VEX IRSB (这是python内部地址,而不是程序内部地址)
<pyvex.block.IRSB at 0x7706330>
|
状态(states)
关于angr的另一个事实是——project对象仅仅相当于程序的一个“初始化镜像”。当你使用angr执行运行程序时,你正在使用一个模拟程序运行状态的对象-SimState
。比如:
1
2
|
>>> state = proj.factory.entry_state()
<SimState @ 0x401670>
|
一个SimState记录着一个程序的内存、寄存器、文件信息…任何可以通过程序执行更改的“实时数据”健康都会被存储进去。稍后我们将会介绍如何与这些状态交互,但是现在,让我们用state.regs
和state.mem
来查看当前状态下的寄存器和内存
1
2
3
4
5
6
|
>>> state.regs.rip # 取得当前指令的地址
<BV64 0x401670>
>>> state.regs.rax
<BV64 0x1c>
>>> state.mem[proj.etry].int.resolved # 将入口点的内存以C语言中int类型显示
<BV32 0x8949ed31>
|
这些并不是python的ints!这些是bitvectors
。python中的integers与CPU层面的字(words)并不是一个概念,例如python不会产生整数溢出。所以我们使用bitvectors
,可以将其视为一串bits构成的整数,angr中用它来代表CPU data。注意每个bitvector都有一个.length
属性来描述它是多少bits宽
我们在后面将会学习如何使用它们,但是现在,我们列出了如何将数字在python int和bitvector之间转换的方法
1
2
3
4
|
>>> bv = state.solver.BVV(0x1234, 32) # 创建一个32bit宽的bitvector,它的值是0x1234
<BV32 0x1234> # BVV:bitvector value
>>> state.solver.eval(bv) # 将bitvector转化为python int
0
|
你可以把bitvector存回寄存器和内存,或者直接存储python integer类型值,它会被自动转化成合适大小的bitvector
1
2
3
4
5
6
7
|
>>> 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
接口刚开始看令人困惑,因为它使用了一些强大的python魔法,下面是简要的使用指南:
-
使用array[index]
来指明确定的地址
-
用.<type>
来指定内存数据的类型(常用类型:char, short, int, long, size_t, uint8_t, uint16_t…)
-
通过mem接口你可以:
-
存储一个值到内存,既可以是bitvector,也可以是python int
-
用.resolve
来获取内存值并转化为bitvector
-
用.concrete
来获取内存值并转化为python int
还有更多高级用法将在后面提到
最终,如果你尝试读取更多寄存器的值,你可能会遇到一个长得十分奇怪的值
1
2
|
>>> state.regs.rdi
<BV64 reg_48_11_64{UNINITIALIZED}>
|
这同样是一个64位bitvector,但是它并没有携带数值,相反的,它有一个名字!它被称为符号变量,也是符号执行的基础。不要恐慌!我们会在两章之后讨论它的细节
模拟管理器(Simulation Managers)
如果程序在任意给定的时间点都有一个状态,那么必然有一种方法可以让它变成下一种状态。simulation manager
是angr执行中的主要接口。模拟(simulation)无论你如何叫它,它都是带有状态的。作为一个简短的介绍,让我们看看如何在我们之前创建的代码块中标记状态。
首先,我们要创建一个simulation manager,构造函数可以传入一个状态或状态列表
1
2
3
4
|
>>> simgr = proj.factory.simulation_manager(state)
<SimulationManager with 1 active>
>>> simgr.active
[<SimState @ 0x401670>]
|
一个simulation manager可以有多种状态的封装(stash),默认的stash是active
,用我们传入的状态初始化。如果还不够的话,我们可以查看simgr.active[0]
来进一步了解各种状态
现在, 准备好,我们要进行一些程序的执行了。
我们刚刚进行了一个基本快的符号执行!
我们可以再看看active stash,注意它已经被更新了,并且它并没有改变我们原来的状态。SimState对象在执行时被视为不可变,所以你可以安全地将单个状态作为多轮执行的开始(base)
1
2
3
4
5
6
|
>>> simgr.active
[<SimState @ 0x1020300>]
>>> simgr.active[0].regs.rip # new and exciting!
<BV64 0x1020300>
>>> state.regs.rip # still the same!
<BV64 0x401670>
|
/bin/true不是一个很好的例子来描述如何用符号执行做有趣的事,所以我们现在就到此为止
分析(Analyse)
angr预先打包了一些内置分析方法,你可以利用它们从程序中提取一些有趣的信息。
1
2
3
4
5
6
7
8
9
|
>>> proj.analyses. # 在这里按tab来列出所有内置的分析方法
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文档。作为一个简短的例子:下面说明了你应该怎样生成并使用一个快速的程序控制流图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 刚开始时,当我们加载二进制文件时,它还将其所有依赖项加载到同一块虚拟内存中
# 这对于大多数分析是不愿意看到的
>>> proj = angr.Project('/bin/true', auto_load_libs=False)
>>> cfg = proj.analyses.CFGFast()
<CFGFast Analysis Result at 0x2d85130>
# cfg.graph 是一个充满了CFGNode实例的 networkx DiGraph
# 你应该去看 networkx APIs 的文档去学习如何使用它
>>> cfg.graph
<networkx.classes.digraph.DiGraph at 0x2da43a0>
>>> len(cfg.graph.nodes())
951
# 去获取指定地址的 CFGNode, 可以用 cfg.get_any_node
>>> entry_node = cfg.get_any_node(proj.entry)
>>> len(list(cfg.graph.successors(entry_node)))
2
|
Now what?
阅读完这一页之后,你应该熟悉了angr几个重要概念:basic blocks, states, bitvectors, simulation managers 和analyses。但是除了把angr作为美化的调试器以外,你还不能做任何事情。继续阅读,你将解锁更深层次的力量…