angr-doc 5: Simulation Managers

模拟管理器 Simulation Managers

在angr中最重要的控制界面是模拟管理器,它允许你同时控制几组状态的符号执行,并使用搜索策略来探索程序的状态空间。在这里,你会学到如何使用它。

模拟管理器让你灵活地处理多种状态。状态按照“存储stash”来管理,你可以自由地步进,过滤,合并和到处查看。这能让你,比如,按照不同的速率步进两个不同的存储,然后将它们合并起来。对于大部分操作的默认存储是active存储,当你初始化一个新的模拟管理器时你会将状态保存在这里。

步进 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>]

当然,存储模型的真正厉害之处在于当一个状态遇到了一个符号分支条件时,后继的所有状态将会出现在存储中,然后你可以同步地步进它们。当你不用太关注仔细地控制分析过程,并且你只想要步进到终点的时候,你可以只用.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个到尽头的状态了!当一个状态在执行过程中不能产生任何后继状态时,比如,因为它执行到一个exit系统调用时,它会被从active存储中移除,放置到deadended存储中。

存储管理 Stash Management

让我们看看如何使用其他存储。

为了在存储中移动状态,使用.move(),它会接受from_stash, to_stashfilter_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”的新的存储。在这个存储中的所有状态都有“Welcome”在它们的标准输出中,目前这是一个很好的公用标准。

每个存储就只是一个列表,你可以使用下标或者迭代器操作列表的方式访问每个单独的状态,但是也有一些可选的方法来访问这些状态。如果你在一个存储的名称前预置了one,你会被提供存储中的第一个状态。如果你在一个存储的名字前面预置了mp,你会被提供存储中一个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参数,来指定操作的存储。

模拟管理器提供来许多用来管理你的存储的有趣的工具。我们现在不会深入介绍剩下的部分,但是你可以查看API文档。TODO: 添加链接


存储类型

你可以使用随意使用存储,但是有一些存储被用来对特定种类的状态进行分类。它们分别是:

存储 描述
active 这个存储包含来默认情况下要步进的状态,除非指定来一个替换的存储
deadended 当一个状态由于某种原因,包括没有更多有效指令,后继状态都不能满足,或者一个无效的指令指针,不能继续执行,它会进入deadended存储中。
pruned 当我们使用LAZY_SOLVES时,除非绝对必要,状态不会被检查是否满足条件。当使用LAZY_SOLVES时一个状态被发现不能被满足时,会遍历各层状态来检查在历史中,刚开始变得不能被满足的状态。那个节点的所有后继状态(它们也会是不能满足的,因为一个节点不能变得不能不被满足)会被修建掉,并且放入这个存储中。
unconstrained 如果save_unconstrained选项被提供给了模拟管理器的构造器,被认为是不受约束的状态(比如受到用户数据或者其他一些来源的符号数据控制的指令指针)会被存放在这里。
unsat 如果save_unsat选项被提供给了模拟管理器的构造器,被认为是不能被满足的状态(比如,有像输入同时都必须是“AAAA”和“BBBB”这样相互冲突的约束)会被存放到这里。

还有一个不是存储的状态列表:errored。如果在符号执行过程中,出现了一个错误,那么状态会被包装在一个ErrorRecord对象中,它包含这个状态和状态引起的错误,然后这个记录会被插入errored里面。你可以通过record.state来获取引起错误的状态,因为它在执行记号的开头。你可以使用record.error查看引发的错误。你还可以使用record.debug()在错误现场打开一个调试shell。这是一个无价的调试工具!

简单的探索 Simple Exploration

在符号执行中,一个极端常见的操作是找到一个到达特定地址的状态的同时丢弃到达另外一个地址的所有状态。模拟管理器应对这种情况的捷径,.explore()方法。

当使用find参数调用.explore()时,符号执行会执行到找到一个满足寻找条件的状态为止,这个条件可以是要停止的指令的一个地址,要停止的指令地址的列表,或者是一个接受状态输入,返回是否满足某种表准的函数。当在active存储中的任何状态满足find条件时,它们会被存放到found存储中,然后结束符号执行。你可以探索找到的状态,或者决定抛弃它并使用其他状态继续。你也可以按照和find同样的格式,指定一个avoid条件。当一个状态满足回避条件时,它会被存放到avoided存储中,然后继续符号执行。最终,num_find参数控制在返回前应该被找到的状态的个数,它默认为1。当然,如果你在找到这么多解之前用完了active存储中的所有状态,符号执行无论如何都会停下来。

让我们看看一个简单的crackme例子:

首先,我们加载二进制文件:

>>> proj = angr.Project('examples/CSCI-4968-MBE/challenges/crackme0x00a/crackme0x00a')

接着,我们创建一个模拟管理器。

>>> simgr = proj.factory.simgr()

现在我们进行符号执行,直到我们找到一个能满足我们的条件的状态(比如,“win”条件)。

>>> simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))
<SimulationManager with 1 active, 1 found>

现在,我们可以从那个状态中获取到flag了!

>>> 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_technique(tech),其中tech是一个ExplorationTechnique子类的实例。angr内置的探索方式可以在angr.exploration_techniques找到。

这里有一些内置的探索方式的快速预览:

  • DFS:深度优先搜索,正如刚刚提到的那样。一次只保持一个状态处于活动,将其余的状态放在deferred存储中直到当前状态执行到尽头或者出错。
  • Explorer:这个方式实现了.explore()的功能,它允许你寻找和回避地址。
  • LengthLimiter:在一个状态能经过的最大路径长度上加以限制。
  • LoopSeer:使用一个合理的回环计数估计来放弃看上去将要太多次经历一个循环的状态,并将它们放入spinning存储中。如果我们用完了其他的可访问的状态就再把这些状态拿出来。
  • ManualMergepoint:将程序中的一个地址标记为合并点,到达那个地址的状态将会被短暂保存,并且任何在超时时间内到达同一个合并点的其他状态都会被合并在一起。
  • MemoryWatcher:监视在simgr步进过程中系统空闲/可用内存数量,如果内存过少则停止探索。
  • Oppologist:“运算辩护者”是一个尤其有趣的小组件——如果这个方式被启用了,并且angr遇到了一个不支持的指令,比如一个奇怪且外来的浮点SIMD指令,它会将那个指令的所有输入具体化并且使用unicorn引擎模拟那单个指令,让符号执行得以继续。
  • Spiller:当有太多活动状态时,这个方式会将它们中的一部分保存到磁盘中来保持低内存占用。
  • Threading:向步进中的进程添加线程级并行。这个方式因为python的全局解释器锁而作用不大。但是如果你有一个在angr的本地代码依赖(unicorn, z3, libvex)中的分析花费许多时间的程序,你可以尝试以取得一些效果。
  • Tracer:一个会使符号执行跟随一个从其他来源记录的动态树的探索方式。动态跟踪器源中有一些生成这些跟踪器的工具。
  • Veritesting:一个有关自动识别有用的合并点的CMU论文实现。它很有用,你可以在模拟管理器的构造器中使用veritesting=True自动启用它。需要注意由于它使用了侵入式的静态符号执行,它和其他方式的相性不佳。

查看模拟管理器探索方式的API文档来获取更多的信息。

上一篇
下一篇