angr-doc 4: Program State

程序状态 Programe State

到现在,我们为了展示angr操作的基本概念,只使用了angr的符号执行程序状态(SimState对象)最可能的方式。在这里,你会学到有关一个状态对象的结构和如何以各种有用的方式与它交互。


复习:读写内存和寄存器 Review: Reading and writing memory and registers

如果你已经按照顺序读了这本书(并且至少在第一次时你应该这么做),你已经看到了如何访问内存和寄存器的基本操作。state.regs通过每个寄存器名字作为后缀提供对寄存器的读写操作,state.mem通过使用下标指定地址,并且使用属性访问指定需要将内存翻译成的类型,来提供对内存的读写。

除此之外,你应该了解如何使用AST,从而理解基于位向量类型的AST在寄存器或者内存中如何存储。

这里有一些在状态中对数据进行复制和执行操作的例子。

>>> import 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

之前,我们展示了如何使用模拟管理器进行一些基本的符号执行。我们将在下一章展示模拟管理器的完整功能,但是现在我们可以使用一个简单的多的接口来展示符号执行是如何工作的:state.step()。这个方法会执行单步符号执行,并且返回一个叫做SimSuccessors的对象。不像普通的模拟过程,符号执行可以产生由多种方法分类的几个后继状态。就目前来看,我们关注的是对象的.successors属性,它是包含所有给定执行步骤的“普通”后继状态的列表。

为什么使用列表而不是只用一个单独的后继状态呢?angr符号执行的过程就只是获取编译进程序的单个指令的操作,然后通过执行它们来转换符号执行状态。当遇到像if (x > 4)这样的一行代码,如果x是一个符号变量,那会发生什么呢?在angr的底层,x > 4的比较会被执行,然后结果将会是 4>

这很好,但是下一个问题是,我们是选择“真”的分支还是“假”的分支?答案是我们全都要!我们生成两个完全无关的后继状态——一个模拟条件为真的情况,另一个模拟条件为假的情况。在第一个状态中,我们将x > 4作为约束条件添加,而在第二个条件中,我们将!(x > 4)作为约束条件添加。那样的话,无论什么时候我们使用这些后继状态执行了约束求解,状态的条件都保证我们得到的任何解都是能让执行过程按照给定的后继状态沿相同路径继续执行的合法输入

为了演示这一点,让我们使用一个假的固件镜像作为例子。如果你查看了这个二进制文件的源代码,你会发现固件的验证机制存在后门;任何用户使用“SOSNEAKY”作为密码都可以作为管理员登陆。不仅如此,对用户输入的第一次比较发生在对后面进行比较时,所以如果我们一直单步执行直到得到超过一个后继状态时,这些状态中的一个会包含以后门密码作为用户输入的约束条件。下面的代码片段实现了这一点:

>>> 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默认把它当作一个无限的符号数据流。为了执行约束求解并得到一个作为输入能够满足约束的可能的值,我们需要得到stdin真实内容的引用。我们就在这页的后面会大致介绍文件和输入子系统是如何工作的,但是现在,我们就使用state.posix.stdin.load(0,state.posix.stdin.size)来获取代表所有从stdin读取的内容的位向量。

>>> 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很有帮助地提供了几十亿能满足条件的字符串中的一种。

退回到2013年,fauxware是angr首个成功进行符号执行的程序。通过使用angr来找到它的后门,你正在按照传统方式,获取如何使用符号执行从二进制文件中提取有意义内容的最基本的理解!


预设状态 State Presets

到现在为止,无论我们什么时候使用状态,我们会使用project.factory.entry_state()来创建。这只是项目工厂中可用的几种状态构造器中的一种。

  • .blank_state()构造一个大部分数据没有被初始化的“白板”空白状态。每当访问未初始化的数据时,返回一个未约束的符号量。
  • .entry_state()构造一个准备好从主程序入口执行的状态。
  • .full_init_state()构造一个准备好能够在主程序入口前的初始化过程中执行的状态,比如,共享资源库的构造器或者预初始化过程。当它执行完这些过程时,它会跳转到程序入口。
  • .call_state()构造一个准备好执行给定函数的状态。

你也可以通过像这些构造器提供几个参数来自定义状态:

  • 所有这些构造器可以接受一个addr参数来指定开始的准确地址。
  • 如果你在可以接受命令行参数的环境中执行,你可以通过args传递参数列表并通过env传递环境变量的词典到entry_statefull_init_state中。这些结构体中的值可以是字符串或位向量,并且会被作为符号执行的参数和环境序列化进入状态中。默认的args是一个空列表,所以如果你正在分析的程序至少需要找到一个argv[0],你总应该提供它!
  • 如果你想要argc符号化,你可以将将一个符号化的位向量作为argc传递给entry_statefull_init_state构造器。只是需要小心的是:如果你这么做了,你也应该向结果状态添加一个约束,用来限制提供给argc的值不能大于传递给args的参数个数。
  • 为了使用调用状态,你应该使用.call_state(addr, arg1, arg2, ...),其中addr是你想要调用的函数的地址,argN是那个函数的第N个参数,可以是一个python整数、字符串或者是一个数组,或者是一个位向量。如果你想要分配内存,并且实际地向对象传递一个指针,你应该将它包装进一个指针包装器PointWrapper中,比如angr.PointWrapper("point to me!")。这个API的结果可能有点无法预测,但是我们正在处理它。
  • 为了指定call_state对某个函数的调用约定,你可以将一个符号执行调用约定SimCC实例作为cc参数传递。我们会尝试挑选一个合理的默认值,但是在特殊情况下,你需要帮助angr选择。

还有几个选项可以在这些构造器中的任何一个里面使用。查阅project.factory对象(一个AngrObjectFactory)的文档以获取更多细节。


内存的底层接口

state.mem接口用来从内存中装载带类型的数据很方便,但是当你想要在一定范围内装载和存储原始数据时,它显得很笨重。事实上,state.mem只是一批正确访问底层内存存储——仅仅是一片由位向量填充的平坦的地址空间state.memory——的逻辑。你可以通过.load(addr, size).store(addr, val)直接使用state.memory

>>> 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.memory的主要目的是用来装载和存储一片不带附加语义的数据。然而,如果你想要对已装载或存储的数据执行大小端变换,你可以传递一个关键字endness参数——如果你指定了小端存储,将会执行大小端变换。大小端endness应当是用于在angr中存储有关CPU架构的描述性数据的archinfo包中的Endness枚举量中的一个成员。初次之外,被分析的程序的大小端可以作为arch.memory_endness找到——为了state.arch.memory_endness实例。

>>> import archinfo
>>> s.memory.load(0x4000, 4, endness=archinfo.Endness.LE)
<BV32 0x67453201>

还有一个用来提供寄存器访问的底层接口state.registers,它使用和state.memory一样的API,但是解释它的行为涉及深入angr使用的能在多种架构上无缝工作的抽象。更简单的描述是它仅仅是一个包含了在archinfo中定义的寄存器和偏移量布局的寄存器文件。


状态选项 State Options

在angr内部可以进行许多小的调整,这可能会在某些情况下优化表现,也可能在其他情况下造成损害。这些调整通过状态选项来控制。

在每一个符号执行状态对象张,有一个所有启用的选项的集合(state.options)。每一个选项(真的就只是一个字符串)以某种方式控制angr符号执行引擎的表现。在附录中可以找到完整的选项域的列表,以及不同状态类型的默认值。你可以通过angr.options访问单个的选项从而将其添加到状态中。单个选项使用大写字母CAPITAL_LETTERS命名,但是也有一些常见的对象组你可能想要一起使用,它们使用小写字母lowercase_letter命名。

当使用任何构造器创建一个符号执行状态时,你可以传递add_optionsremove_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

在刚刚提到的选项集合之外,在符号执行状态中存储的任何东西实际上存储在附加在状态的一个插件plugin中。几乎每个我们目前已经讨论过的状态属性都是插件——memory, registers, mem, regs, solver等。这种设计既能使代码模块化,也方便以模拟状态的其他视角实现新类型的数据存储,还有能力提供插件的其他可选实现。

比如,普通的memory插件模拟了一个平坦的内存空间,但是分析过程可以选择启用“抽象内存”插件,它使用另一种地址的数据类型来模拟不依赖地址的自由浮动的内存映射,从而提供state.memory。反过来,插件可以减少代码的复杂性:state.memorystate.registers实际上是同一个插件的两个不同的实例,因为寄存器也是使用一个地址空间来模拟的。

全局插件 The globals plugin

state.globals是一个非常简单的插件:它实现了一个基本的python词典的接口,使你能够在状态中存储任意数据。

历史插件 The history plugin

state.history是一个存储有关在符号执行中状态选取的执行路径的历史数据的非常重要的插件。它实际上是由几个历史节点组成的链表,每个节点代表着单轮执行——你可以使用state.history.parent.parent等遍历这个链表。

为了更方便地使用这个结构,历史也提供了几种用于访问某几种值的历史的高效的迭代器。总的来说,这些值作为history.recent_NAME存储,并且访问它们的迭代器就是history.NAME。比如,for addr in state.history.bbl_addrs: print hex(addr)会打印出程序的一个基本块地址轨迹,而state.history.recent_bbl_addrs是在最近几次步过中执行的基本块的列表,state.history.parent.recent_bbl_addrs是上一步执行的基本块的列表,等等。如果你需要快速获得这些值的“平坦的”列表,你可以访问.hardcopy,比如state.history.bbl_addrs.hardcopy。记住,基于下标的访问是基于迭代器实现的。

这里有在历史中保存的一些数据的简单的列表:

  • history.descriptions是在状态中每轮执行的操作的字符描述的列表。
  • history.bbl_addrs是被状态执行的基本块地址的列表。也许在每轮执行中可能存在超过一个的地址,并且不是所有的地址都与二进制代码相对应——有一些可能是符号执行过程钩住的地址。
  • history.jumpkinds是在状态历史中每个控制流转换的布局的列表,作为VEX枚举字符串。
  • history.jump_guards是监视状态遇到的每一个分支的条件的列表。
  • history.events是在执行过程中发生的“有趣的事件”的语义列表,比如一个符号跳转条件的出现,程序弹出一个消息框,或者执行过程以一个退出状态码结束。
  • history.actions通常是空的,但是如果你向状态中添加了angr.options.refs选项,它会被填入程序执行的对所有的内存、寄存器和临时变量的访问的日志。

调用栈插件 The callstack plugin

angr会跟踪被模拟程序的调用栈。在每个call指令处,在被跟踪的调用栈上面会添加一个栈帧,并且无论什么时候栈指针降低到被调用的最顶部的栈帧的下面,那意味着有一个栈帧被弹出。这允许angr根据当前在模拟的函数健壮地存储数据。

与历史相类似,调用栈也是一个由节点构成的列表,但是没有提供访问节点内容的迭代器——你倒是可以直接迭代访问state.callstack,以最近的到最早的顺序,来获取每个活动栈帧的调用栈帧。如果你只想要顶部栈帧,它是state.callstack

  • callstack.func_addr是当前被执行的函数的地址。
  • callstack.call_site_addr是调用当前函数的基本块的地址。
  • callstack.stack_ptr是从当前函数开始的栈指针的值。
  • callstack.ret_addr是当前函数返回时的返回地址。

更多有关输入输出相关的内容:文件、文件系统和网络套接字 More about I/O: Files, file systems, and network sockets

请查阅使用文件系统、套接字和管道来获取更加完整和详细的有关输入输出在angr中的模型的文档。


复制与合并 Copying and Merging

状态支持非常快速的复制,以便于你探索不同的可能性:

>>> 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

状态也可以合并到一起。

# 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

TODO: 描述合并的限制

上一篇
下一篇