顶层接口 Top Level Interfaces
开始使用angr的第一步是将一个二进制文件加载到一个项目中。我们使用/bin/true
为例。
>>> import angr
>>> proj = angr.Project('/bin/true')
在angr中,我们通过项目控制一切。你可以使用项目在你刚刚加载的可执行文件上进行分析和模拟。你在angr中使用的几乎每一个对象都会依赖一个以某种形式存在的项目。
基本属性 Basic properties
首先,我们有关于项目的一些基本属性,包括它的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
对象的一个实例,无论程序是被编译成什么架构的,在这个例子中是小端存储的amd64架构。它包含许多有关程序运行架构的信息,可以根据需要仔细阅读。通常需要关注的是arch.bits
,arch.bytes
(这个用来声明Arch
类的@property
),arch.name
和arch.memory_endness
。 - entry是程序的入口。
- filename是程序的绝对文件名称。
装载器 The loader
把一个二进制文件映射到它所表示的虚拟地址空间是很复杂的。我们使用一个叫CLE的模块去处理。CLE的结果,被称为装载器,可以通过.loader
属性访问。我们很快会仔细介绍如何使用它,但是现在我们只需要知道可以使用它查看angr和程序一起加载的共享库,并且执行一些有关加载地址的简单查询。
>>> proj.loader
<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.factory
,它有几个方便的建造器来提供常用的一些对象。
这一节也会对angr的几个基本概念进行介绍。
块 Blocks
首先,我们有project.factory.block()
,它用于从给定地址提取出一个基本的代码块。这说明来一个基本的事实——angr以基本块为单位分析代码。我们会得到一个块对象,它含有代码块的信息。
>>> 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.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
在angr中,Project
对象只表示程序的“初始化镜像”。当你使用angr进行符号执行时,你会遇到专门用来表示一个符号执行的程序的状态的对象——一个符号执行状态Simsate
。
>>> state = proj.factory.entry_state()
<SimState @ 0x401670>
一个符号执行状态包含程序的内存、寄存器、文件系统数据等信息。任何能在符号执行过程中改变的“存活数据”都保存在状态中。我们之后会介绍如何与状态深度交互,但是现在我们就只使用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>
返回的数据不是python的int类型。它们是位向量bitvector。python整数类型和CPU上的单字语意并不一致。比如,包装溢出时。所以我们使用位向量,你可以认为在angr中它将一个整数表示为一系列的位来表示CPU的数据。我们可以注意到每个位向量都有一个.length
属性来描述它的位宽。
我们会很快学到如何使用它们,但是现在,我们先看看如何在python的整数类型和位向量之间相互转换。
>>> 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>
我们可以把这些位向量存储回寄存器和内存,或者你可以直接存储一个python的整数类型,它会转换成合适大小的位向量。
>>> 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的一些技巧。简单说,我们可以这样使用它:
- 使用
数组[下标]
的标记来指定一个地址。 - 使用
.
来指定内存应该被转译为(通常使用的类型包括:char, short, int, long, size_t, uint8_t, uint16_t……) -
之后你可以:
- 往里面保存一个值,这个值可以是一个位向量或者是一个python的整数值。
- 使用
.resolved
来获取位向量形式的值 - 使用
.concrete
来获取python整型形式的值
之后会介绍更多高级的使用方法。
最后,如果你试图读取更多的一些寄存器,你可能遇到一个看上去很奇怪的值:
>>> state.regs.rdi
<BV64 reg_48_11_64{UNINITIALIZED}>
这个仍然是一个64位的位向量,但是它不包含一个数值量,而是有一个名字。它叫做符号变量并且它是符号执行的基础。不用紧张。我们会在两章后讨论这些细节。
符号执行管理器 Simulation Managers
如果我们使用一个状态来表示在某个给定的时间的程序,那必然有能够在一定时间内使它进入下一个状态的方法。一个符号执行管理器是angr对状态进行执行,模拟,或者无论你把它叫做什么,的基本接口。
首先,我们创建将要使用的符号执行管理器。建造器可以获取一个状态或者一个状态的列表。
>>> simgr = proj.factory.simulation_manager(state)
<SimulationManager with 1 active>
>>> simgr.active
[<SimState @ 0x401670>]
一个符号执行管理器可以包含多个状态的存储stash。默认的存储,active
,和我们传递进去的状态一起加载。我们可以通过simgr.active[0]
来看更多有关我们的状态。
现在,准备好,我们将开始进行一些符号执行。
>>> simgr.step()
我们刚刚执行了一个符号执行中的一个基本块!我们可以再次查看active存储,注意到它刚刚被更新了,不仅如此,它没有改变我们原来的状态。符号执行状态对象在执行中是无法被改变的——你可以安全的使用单个的状态作为多轮符号执行的“基础”。
>>> 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文档。举一个很简单的例子:这是如何创建并使用一个快速控制流图:
# 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的几个重要的概念:基本块、位向量、符号执行管理器、以及代码分析。但是,除了把angr当作一个漂亮的调试器以外,你还不能用它做一些真正有趣的事情。如果你继续向下阅读,你就可以解锁更深层次的能力。