原文链接:https://docs.angr.io/core-concepts/pathgroups
angr中最重要的控制接口是SimulationManager,它允许您同时控制状态组的符号执行,并应用搜索策略来探索程序的状态空间。在这里,您将学习如何使用它。
仿真管理器允许您以一种灵活的方式管理多个状态。状态被组织成“stashes”,您可以向前执行、筛选、合并和自由移动。这允许您以不同的速率步进两个不同的状态堆栈,然后将它们合并在一起。大多数操作的默认存储是active
stash,当您初始化一个新的模拟管理器时,您的状态将放在活动存储中。
stepping
仿真管理器最基本的功能是将给定存储中的所有状态向前推进一个基本块。你通过使用.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模型的真正强大之处在于,当一个状态遇到符号分支条件时,两个继承状态都会出现在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个deadended stashs!当一个状态在执行时没法再产生后继,如,它抵达了exit,它将从active
stash中转移到deadended
stash。
Stash Management
让我们看看如何使用其他stash。
要在stash之间移动状态,可以使用.move()
,它接受from_stash
、to_stash
和filter_func
(可选,默认情况下是移动所有内容)作为参数。例如,让我们移动输出中有特定字符串的所有状态:
>>> 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。这个stash中的所有状态的标准输出中都有“Welcome”,这是目前的一个很好的标准。
每个stash都只是一个列表,您可以在列表中建立索引或迭代,以访问每个状态,但是也有一些其他方法可以访问这些状态。如果您在一个stash的名称前面加上一个one_
,您将得到该stash中的第一个状态。如果您在stash的名称前加上mp_
,您将得到该stash的多路复用版本。
>>> 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
参数,指定要操作哪个stash。
仿真管理器为您管理存储提供了许多有趣的工具。我们暂时不讨论其余的部分,但是您应该查看API文档。
Stash types
您可以对任何您喜欢的内容使用stashes分类,但是有一些stashes将用于对一些特殊类型的状态进行分类。如下:
Stash | Description |
---|---|
active | 此stash包含默认情况下将逐步执行的状态。 |
deadended | 当一个状态由于某种原因不能继续执行时,它将进入deadended stash,其中包括没有更多有效的指令、所有后继都是unsat状态,或者有一个无效的指令指针。 |
pruned | 当使用LAZY_SOLVES 模式,除非绝对必要,否则不会检查状态的可满足性。当在LAZY_SOLVES 开启的情况下发现一个状态是unsat时,将遍历该状态层次结构,以确定它最初在其历史中何时成为unsat。所有继承该点的状态(该点也是unsat,因为一个状态不能成为un-unsat)都被修剪并放入这个存储中。 |
unconstrained | 如果将save_unconstrained 提供给SimulationManager构造函数,被定义成不受约束的状态(即,PC指针由用户数据或其他符号数据源控制)将被放在这里。 |
unsat | 如果将save_unsat 选项提供给SimulationManager构造函数,则确定为不可满足的状态(即,它们有相互矛盾的约束,比如输入必须同时为“AAAA”和“BBBB”)将被放在这里。 |
还有另外一个不输入stash的状态列表:errored
。如果在执行过程中引发错误,则状态将被存在ErrorRecord
对象中,该对象包含这个状态及其引发的错误,然后将记录插入errored
。你可以用record.state
来获得导致错误的执行的开始状态,你可以用record.error
来看引发了什么错误,您还可以使用record.debug()
在错误的位置启动调试shell。这是一个非常有价值的调试工具!
Simple Exploration
符号执行中一个极其常见的操作是找到到达某个地址的状态,同时丢弃经过另一个地址的所有状态。仿真管理器为这个模式提供了一个快捷方式,.explore()
方法。
例子:
>>> proj = angr.Project('examples/CSCI-4968-MBE/challenges/crackme0x00a/crackme0x00a')
>>> simgr = proj.factory.simgr()
>>> simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))
<SimulationManager with 1 active, 1 found>
>>> s = simgr.found[0]
>>> print(s.posix.dumps(1))
Enter password: Congrats!
>>> flag = s.posix.dumps(0)
>>> print(flag)
g00dJ0B!
Exploration Techniques
angr附带了一些固定的功能,可以定制模拟管理器的行为,称为探索技术。为什么需要探索技术的典型例子是修改探索程序状态空间的模式——默认的“一次完成所有事情”策略实际上是广度优先搜索,但是使用探索技术可以实现如深度优先搜索的模式。然而,这些技术的检测功能要灵活得多——您可以完全改变angr的步进过程的行为。编写您自己的探索技术将在后面的章节中介绍。
要使用探索技术,请调用simgr. use_technology (tech)
,其中tech是ExplorationTechnique子类的一个实例。angr内置的探测技术可以在anger .exploration_techniques
中找到。
下面是一些内置功能的概述:
- DFS:深度优先搜索,如前所述。一次只保持一个状态为活动状态,将其余状态放在
deferred
中,直到它结束或报错。 - Explorer:此技术实现
.explore()
功能,允许您搜索和避免特定地址。 - LengthLimiter:设置状态经过的路径的最大长度上限。
- LoopSeer:使用合理的循环计数近似值来丢弃循环次数过多的状态,将它们放入
spinning
中,如果没有其他可行的状态,则再次将它们取出。 - ManualMergepoint:将程序中的一个地址标记为合并点,到达该地址的状态将被暂时保存,在一定时间内到达同一点的其他状态将被合并在一起。
- MemoryWatcher:在step之间监视系统上空闲/可用的内存,如果内存太低,则停止探索。
- Oppologist:这是一个特别有趣的小玩意——如果启用了这种技术并且angr遇到一个不支持的指令,例如一个bizzare和外国浮点SIMD操作,它将使具体化所有该指令的输入,并且使用unicorn引擎模拟这一单独指令,使得执行可以继续。
- Spiller:当有太多active状态时,此技术可以将其中一些转储到磁盘,以保持低内存消耗。
- Threading:为步进过程添加线程级并行性。这没有多大帮助,因为python的全局解释器锁,但如果您有一个程序,它的分析花费了大量时间在angr的本地代码依赖项(unicorn、z3、libvex)中,您可以看到一些作用。
- Tracer:一种探测技术,它使执行遵循从其他源记录的动态跟踪。tracer库有一些工具来生成这些跟踪。
- Veritesting:一个CMU paper的实现,关于自动识别有用的合并点。这非常有用,您可以在SimulationManager构造函数中使用verititing =True自动启用它!注意,由于它实现静态符号执行的侵入性方式,它经常不能很好地与其他技术配合使用。