Angr:Program State

原文链接:https://docs.angr.io/core-concepts/states


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


回顾:读写内存和寄存器

在前面的文章中,您已经了解了访问内存和寄存器的基本知识。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

基本执行

前面,我们展示了如何使用模拟管理器执行一些基本的执行。我们将在下一章中展示模拟管理器的全部功能,但现在我们可以使用一个简单得多的接口来演示符号执行的工作原理:state.step()。这个方法将执行符号执行的一个步骤,并返回一个名为SimSuccessors的对象。与普通模拟不同,符号执行可以生成多个继承状态,这些状态可以用多种方式进行分类。现在,我们关心的是这个对象的.successors属性,这是一个给定步骤的所有“正常”后继的列表。
为什么是一个列表,而不是一个单独的后继呢?angr的符号执行过程就是将编译到程序中的各个指令的操作执行到程序中,并对SimState进行修改。当到达像if (x > 4)这样的一行代码时,如果x是一个符号位向量会发生什么?在angr的某个深度,比较x > 4,结果是<Bool x_32_1 > 4>
这很好,但是下一个问题是,我们应该选择 “true” 分支还是“false”分支?答案是,都选!我们生成两个完全独立的继承状态——一个模拟条件为真,另一个模拟条件为假。在第一种状态中,我们添加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提供了符合这个标准的数十亿字符串中的一个。
Fauxware是angr在2013年成功开发的第一个符号执行程序。通过使用angr找到它的后门,是彻底理解符号执行过程必经的一个过程。


State Presets状态预约束

到目前为止,我们都是用project.factory.entry_state()来创建我们需要的状态。这只是project工厂中可用的几个状态构造函数之一:

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

您可以通过以下构造函数的几个参数自定义状态:

  • 所有这些构造函数都可以使用addr参数来指定要开始的确切地址。
  • 如果您在可以接受命令行参数的环境或环境中执行,则可以通过args将参数列表传递给entry_statefull_init_state,并通过env将环境变量字典传递给entry_statefull_init_state。这些结构中的值可以是字符串或位向量,并将作为模拟执行的状态中的参数和环境。
    默认的args是一个空列表,所以如果您正在分析的程序希望至少找到一个argv[0],您应该始终提供它!
  • 如果希望argc是符号变量,可以将符号位向量argc传递给entry_statefull_init_state构造函数。但是要小心:如果这样做,还应该为结果状态添加一个约束,即你提供给argc的值不能大于放在args中的参数的数量。
  • 要使用call state,应该使用.call_state(addr, arg1, arg2, ...),其中addr是你想要调用的函数,argN是函数的第N个参数,可以是一个python整型,字符串,数组,位向量。如果您想要分配内存并实际传递一个指向对象的指针,您应该将它封装在一个PointerWrapper中,即angr.PointerWrapper("point to me!")。这个API的结果可能有点不可预测,但是我们正在努力。
  • 要指定call_state函数使用的调用约定,可以将SimCC实例作为cc参数传递。我们试图选择一个合理的默认值,但对于特殊情况,您需要帮助angr解决这个问题。

在这些构造函数中可以使用更多的选项,请参阅项目的文档。


Low level interface for memory 内存的低层接口

state.mem接口便于从内存中加载类型化数据,但是当您想要在内存范围内执行原始加载和存储时,这是非常麻烦的。事实证明,state.mem只是一堆为正确访问底层内存存储变量类型的复杂逻辑,而真正的状态内存state.memory是一个充满位向量的平滑地址空间。你可以直接用state.memory.load(addr, size)state.memory.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
<BV48 0x89abcdef0123>

正如您所看到的,数据是以“big-endian”方式加载和存储的,因为state.memory最初的目的是加载没有附加语义的存储段数据。但是,如果希望对加载或存储的数据执行字节转换,可以传递关键字参数endness——如果指定little-endian,则会发生字节转换。endness应该是archinfo包中Endness 的成员之一,该包用于保存关于angr CPU架构的声明数据。此外,被分析程序的端序可以通过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的内部进行许多小小的调整,从而优化某些情况下的行为,并对其他情况造成损害。这些调整是通过状态选项控制的。
在每个SimState对象上,都有一组选项(state.options)。每个选项(实际上只是一个字符串)都以某种细微的方式控制angr执行引擎的行为。附录中列出了完整的选项域,以及不同状态类型的默认值。你可以通过angr.options给一个状态增加一个单独的选项。单独的选项使用大写字母命名,但是也有一些常见的对象组合,您可能希望将它们捆绑在一起使用,使用小写字母命名。
当通过任何构造函数创建SimState时,可以传递关键字参数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状态插件

除了刚才讨论的一组选项之外,SimState中存储的所有内容实际上都存储在附加到该状态的插件中。到目前为止,我们讨论的几乎所有的状态属性都是插件—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还提供了几个针对特定值的历史记录的有效迭代器。通常,这些值存储为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是由状态执行的基本块地址的列表。
    每轮执行可能有多个地址,并且不是所有的地址都对应于二进制代码——有些地址可能是SimProcedure所在的地址。
  • history.jumpkinds是状态历史中每个控制流转换的配置的列表,如VEX枚举字符串。
  • history.jump_guards列出了状态遇到的每个分支的条件。
  • history.events是在执行期间发生的“有趣事件”的语义列表,例如出现符号跳转条件、程序弹出消息框或执行退出代码终止执行。
  • history.actions通常是空的,但是如果你向该状态添加了angr.options.refs选项,它将填充一个日志,其中包含程序执行的所有内存、寄存器和临时值访问。

The callstack plugin函数调用栈插件

angr将跟踪模拟程序的调用堆栈。在每条调用指令中,都会将一个帧添加到跟踪的调用堆栈的顶部,每当堆栈指针低于调用最顶层帧的位置时,就会弹出一个帧。这允许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
Copying and Merging

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值