Program State

原文
到目前为止,为了演示angr操作的基本概念,我们只以最简单的方式使用了angr的模拟程序状态(SimState对象)。在这里,您将了解状态对象的结构,以及如何以各种有用的方式与它交互。

复习:读写内存和寄存器

如果您已经按顺序阅读了这本书(至少在第一部分中应该是这样),那么您已经了解了如何访问内存和寄存器的基本知识。状态。注册表通过带有每个注册表名称和状态的属性提供对注册表的读写访问。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

在前面,我们展示了如何使用Simulation Manager执行一些基本的执行。我们将在下一章中展示模拟管理器的全部功能,但现在我们可以使用一个更简单的接口来演示符号执行是如何工作的:state.step()。这个方法将执行符号执行的一个步骤,并返回一个名为SimSuccessors的对象。与普通仿真不同,符号执行可以产生几个后续状态,这些状态可以以多种方式进行分类。现在,我们关心的是这个对象的.successors属性,它是一个包含给定步骤的所有“正常”继承人的列表。

为什么是一个名单,而不是一个单一的继承状态?好的,angr的符号执行过程就是将编译到程序中的单个指令的操作,并执行它们来改变SimState。当到达像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

>>> state2

不要直接查看这些状态上的约束——我们刚刚经历的分支涉及到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找到它的后门,您正在参与一个伟大的传统,即对如何使用符号执行从二进制文件中提取意义有一个基本的理解!

状态预设

到目前为止,无论何时使用状态,都是使用project.factory.entry_state()创建的。这只是项目工厂中可用的几个状态构造函数之一:

  • .blank_state()构造了一个“blank state”的空白状态,它的大部分数据都未初始化。
    当访问未初始化的数据时,将返回一个不受约束的符号值。
  • .entry_state()构造了一个状态,准备在主二进制文件的入口点执行。
  • .full_init_state()构造了一个状态,该状态可以通过任何需要在主二进制文件的入口点(例如,共享库构造函数或初始化器)之前运行的初始化器执行。
    当它完成这些时,它将跳转到入口点。
  • .call_state()构造一个准备执行给定函数的状态。

你可以通过以下构造函数的几个参数来定制状态:

  • 所有这些构造函数都可以接受一个addr参数来指定确切的开始地址。
  • 如果您正在一个可以接受命令行参数的环境或环境中执行,您可以通过args传递参数列表,通过env传递环境变量字典到entry_state和full_init_state。这些结构中的值可以是字符串或位向量,并将作为模拟执行的参数和环境序列化到状态中。默认的args是一个空列表,所以如果你正在分析的程序希望至少找到一个argv[0],你应该总是提供它!
  • 如果你想让argc是符号的,你可以将一个符号位向量作为argc传递给entry_state和full_init_state构造函数。但是要小心:如果你这样做,你还应该在结果状态中添加一个约束,即你的argc值不能大于你传递给args的参数的数量。
  • 要使用调用状态,你应该使用.call_state(addr, arg1, arg2,…)调用它,其中addr是你想要调用的函数的地址,argN是该函数的第n个参数,可以是python整数、字符串、数组或位向量。如果你想分配内存并实际传递一个指针给一个对象,你应该把它包装在一个指针包装器,也就是angr.PointerWrapper(“指向我!”)。这个API的结果可能有点不可预测,但我们正在努力。
  • 要指定使用call_state函数的调用约定,可以传递一个SimCC实例作为cc参数。

我们试着选择一个正常的默认值,但对于特殊情况,你需要帮助angr。

在这些构造函数中还可以使用更多的选项!看看这个项目的文档。project.factory object(一个AngrObjectFactory)的详细信息。

Low level interface for memory

state.mem从内存中加载类型化数据是很方便的,但是当你想进行原始的加载和从内存范围内存储数据时,它是非常麻烦的。结果是state.mem实际上只是一堆正确访问底层内存存储的逻辑,它只是一个充满位向量数据的平面地址空间:state.memory。你可以直接使用state.memory通过.load(addr, size)和.store(addr, val)方法:

>>> s = proj.factory.blank_state()
>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef0123456789abcdef, 128))
>>> s.memory.load(0x4004, 6) # load-size is in bytes

正如您所看到的,数据是以“大端”方式加载和存储的,这是state.memory的主要目的。内存是加载没有附加语义的存储数据。但是,如果你想对加载或存储的数据执行biteswap,你可以传递一个关键字参数endness—如果你指定了little-endian,biteswap将会发生。endness应该是archinfo包中endness enum的成员之一,该包用于保存用于angr的关于CPU架构的声明性数据。此外,所分析的程序的末端可以发现as.memory_endness—例如state.arch.memory_endness。

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

还有一个底层的寄存器访问接口,state.registers,它使用与state.memory完全相同的API,但是解释它的行为需要深入研究angr用来无缝地与多个架构工作的抽象。简短的版本是,它只是一个寄存器文件,寄存器和偏移量之间的映射在archinfo中定义。

State Options

在某些情况下,我们可以对angr的内部机制进行很多小小的调整,这些调整会优化行为,而在另一些情况下却会造成损害。这些调整是通过状态选项控制的。
在每个SimState对象上,都有一个其所有启用选项的集合(state.options)。每个选项(实际上只是一个字符串)以某种方式控制angr的执行引擎的行为。附录中列出了选项的完整域,以及不同状态类型的默认值。你可以通过angry.options访问添加状态的单独选项。每个选项都以CAPITAL_LETTERS(大写)命名,但也有一些常见的对象分组,您可能希望将它们组合在一起,以lowercase_letters(小写)命名。
当通过任何构造函数创建SimState时,您可以传递关键字参数add_options和remove_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

除了刚才讨论的选项集之外,存储在SimState中的所有内容实际上都存储在一个附加到该状态的插件中。到目前为止,我们讨论过的关于状态的几乎每个属性都是一个插件——内存、寄存器、mem、regs、solver等。这种设计允许代码模块化,并且能够轻松地为仿真状态的其他方面实现新的数据存储类型,或者能够提供插件的替代实现。

例如,普通内存插件模拟平面内存空间,但分析可以选择启用“抽象内存”插件,它使用替代数据类型的地址来模拟独立于地址的自由浮动内存映射,以提供state.memory。相反,插件可以降低代码复杂度:state.memory和state.registers实际上是同一个插件的两个不同实例,因为寄存器也是用地址空间模拟的。

The globals plugin

state.globals是一个非常简单的插件:它实现了标准python dict的接口,允许你在一个状态上存储任意数据。

The history plugin

state.history是一个非常重要的插件,它存储了一个状态在执行过程中所经过路径的历史数据。它实际上是一个由几个历史节点组成的链表,每个节点代表一轮执行——你可以使用state.history.parent.parent等等来遍历这个列表。

为了更方便地使用这个结构,历史记录还提供了几个针对特定值历史的有效迭代器。通常,这些值存储为历史记录。history.recent_NAME和它们上面的迭代器就是history.NAME。例如,对于state.history.bbl_addrs: print hex(addr)中的addr,将打印出二进制的基本块地址跟踪,而state.history.recent_bbl_addrs是在最近的步骤state.history.parent中执行的基本块的列表。state.history.parent.recent_bbl_addrs是在上一步中执行的基本块的列表,等等。如果您需要快速获取这些值的平面列表,您可以访问.hardcopy,例如state.history.bbl_addrs.hardcopy。但是请记住,基于索引的访问是在迭代器上实现的。

以下是一些存储在历史中的值的简要列表:

  • history.description是在状态上执行的每一轮执行的字符串描述列表。
  • history.bbl_addrs是由状态执行的基本块地址列表。
    每一轮执行可能有多个地址,并不是所有的地址都对应于二进制代码——一些地址可能是SimProcedures被钩住的地址。
  • history.jumptypes是状态历史中每个控制流转换的处理列表,作为VEX枚举字符串。
  • history.jump_guards是保护状态遇到的每个分支的条件列表。
  • history.events是在执行期间发生的“感兴趣的事件”的语义列表,例如出现符号跳转条件、程序弹出消息框或执行终止与退出代码。
  • history.actions通常是空的,但是如果您将angry.options.ref选项添加到状态中,那么它将被一个记录程序执行的所有内存、寄存器和临时值访问的日志填充。

The callstack plugin

Angr将跟踪模拟程序的调用堆栈。在每个调用指令中,一个帧将被添加到被跟踪的调用堆栈的顶部,当堆栈指针低于调用最顶层帧的点时,就会弹出一个帧。这允许angr健壮地存储当前模拟函数的本地数据。
与history类似,调用堆栈也是一个节点链表,但没有提供对节点内容的迭代器——相反,你可以直接对状态进行迭代。调用stack获取每个活动帧的调用栈帧,顺序从最近到最旧。如果你只想要最顶层的帧,这是state.callstack。

  • callstack.Func_addr是当前执行的函数的地址
  • callstack.Call_site_addr是调用当前函数的基本块的地址
  • callstack.Stack_ptr是从当前函数开始的堆栈指针的值
  • callstack.Ret_addr是当前函数返回时返回的位置

关于I/O的更多信息:文件、文件系统和网络套接字

有关如何在angr中建模I/O的更完整和详细的文档,请参阅Working with File System, Sockets, and Pipes。

复制和合并

state支持非常快的复制,因此你可以探索不同的可能性:

>>> 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: describe limitations of merging

你可能感兴趣的:(Program State)