angr 文档翻译(4):机器状态——内存,寄存器等
到目前为止,我们只在最基础的层次上使用angr模拟的程序状态(SimState
对象)来展示angr操作的核心概念。在这里,你将会学习更多的state对象的结构,并且学习多样、有用的方式和它交互。
回顾:读、写内存和寄存器
如果你按照顺序阅读本文档(至少在文档的第一部分你应该这么做),那么你应该已经看到了访问内存和寄存器的基本方法。state.regs
对象通过以各个寄存器名命名的属性,提供对各寄存器的读写权限;state.mem
提供了对内存的读写权限,你可以通过下标索引符号访问各个内存,可以使用内存对象的属性来指定内存应该被作为什么类型的数据处理。
另外,现在你应该知道什么是AST,并且理解了任何位向量形式的AST都可以被存储在内存或寄存器中。
下面是一些关于从state是拷贝和执行数据操作的快捷例子:
基础执行
在前面的文档中,我们展示了如何使用Simulatin Manager(SM)来做一些基础执行。在下一章我们将会展示SM的全部能力,但是现在我们使用一个简单得多的接口state.step()
来展示符号执行是如何工作的。这个方法会执行一步符号执行操作,并返回一个Simsuccessors
。和正常的模拟不同,符号执行可以产生几种可以按照多种方式分类的后继状态。现在我们只关心这个对象的.successors
属性,它是包含程序执行一步之后的所有“正常”后继状态的列表。
为什么使用列表而不是单个继承状态?angr的符号执行过程是仅仅取出编译在程序中的要执行的独立的指令,并且执行它来改变Simstate
。当一行诸如if(x > 4)
的语句被执行时,如果x是个符号(而没有具体数值),那么会发生什么呢?在angr底层的某个地方,比较x > 4
将会被执行,且结果将会是
这很好,那么接下来的问题是,我们是要“true”分支,还是要“false”分支呢?答案是,我们两个都要!我们产生两个完全分开的状态——一个用于模拟条件为真,另一个用于模拟条件为假。在第一个状态中,我们添加x > 4
为约束条件,而在第二个状态中,我们添加!(x > 4)
为约束条件。用这样的方式,就能够确保不论何时我们使用这两个后继状态之一,状态中的约束条件都能够保证约束求解器解出的任何结果都是一个使得程序按照该状态指向的程序路径执行的合法输入。
为了演示这一点,让我们使用一个伪造的固件镜像 作为例子。如果你看过这个程序的源码,你将会看到它的授权机制有一个后门;任意用户名,都能通过验证,只要密码是“SOSNEAKY”。此外,用户输入的第一层比较判断是后门代码的判断,所以如果我们一直执行,直到我们得到不止一个继承状态,它们中的一个将会包含使得用户输入通过后门判断的限制条件。下面是代码实现:
不要直接看这些状态的约束——我们刚刚执行过的代码中包括库函数strcmp
的执行,用符号化模拟这个函数很复杂,并且导致了结果中的约束条件也很复杂。
译者注:让我看看有多复杂 :P
emmmm
我们模拟的程序从标准输入中读入数据,angr默认把标准输入看做一个无限符号数据流。为了得到满足约束的可能输入值,我们需要获取标准输入的实际内容的引用。我们将会在本章的后半部分介绍我们的文件和输入系统是如何工作的,但是现在先让我们使用state.posix.files[0].all_bytes()
来获取一个表示标准输入内容的符号。
正如你看到的,为了进入state1指向的分支,你必须提供一个密码“SOSNEAKY”给后门代码。为了进入state2指向的分支,你必须给出不是“SOSNEAKY”的输入。z3已经从数以万计的符合条件的字符串中提取了一个符合条件的字符串给我们。
Fauxware是使用angr第一次成功符号执行的程序(2013年)。通过使用angr找到这个后门的同时,你也在参与一项伟大的传统:理解如何使用符号执行从二进制文件中提取有意义的信息。
状态预置
到目前为止,每当我们想要使用state,我们都用这样一个语句创建:project.factory.entry_state()
。这只是factory提供的多个构造函数中的一个:
-
.blank_state()
构造一个“空状态”,它的大多数数据都是未初始化的。当使用未初始化的的数据时,一个不受约束的符号值将会被返回。 -
.entry_state
构造一个已经准备好从函数入口点执行的状态。 -
.full_init_state()
构造一个已经执行过所有与需要执行的初始化函数,并准备从函数入口点执行的状态。比如,共享库构造函数(constructor)或预初始化器。当这些执行完之后,程序将会跳到入口点。 -
.call_state()
构造一个已经准备好执行某个函数的状态。
你可以通过对这些构造函数传参来构建自定义状态:
- 所有这些构造函数都可以获取一个
addr
作为参数来从一个指定地点开始。 - 如果你在一个可以获取命令行参数或环境参数的环境下运行,你可以传入一个
args
列表和一个存储环境参数的env
字典给entry_steate()
或full_init_state
。列表和字典里的值可以是字符串或位向量,并且被序列化存入state中,作为模拟执行的命令行参数和环境参数。默认的args
是一个空列表,所以如果你要分析一个至少需要args[0]
的程序,那么你应该提供它。
译者注:一般.exe和.elf文件的args[0]都是程序名。
如果你希望
argc
被符号化,你可以传入一个位向量argc
给entry_state
和full_init_state
构造函数。但是要小心:如果你这么做了,那么你还得在结果状态中添加一条约束,即argc的值不能大于你传入args
中的参数的个数。为了使用
call_state
,你应该用call_state(addr,arg1,arg2,arg3...)
这样的方式,其中addr
是要调用的函数的地址,argN
是要调用的函数的第N个参数:可以是python整型、字符串、或者数组、或者位向量。如果你想申请一块内存并把指向内存的指针传入一个对象,那么你应该将指针用PointerWrapper
包装一下,例如,angr.PointerWrapper("point to me")
。这个API的调用结果可能有点不可预测,我们还在努力改进它。-
为了指定使用
call_state
来调用函数时的调用约定,你可以传入SimCCInstance 作为cc
参数。我们正努力选择一个比较好的默认参数,但是在特定情况下需要你来告诉angr使用哪种调用约定。
还有更多能用于上述所有构造函数的选项,在本章的末尾会列出来。
低级内存接口
state.mem
接口对于从内存中以指定类型取出数据是很方便的,但是如果你想手动在某块内存中做存取操作,这个接口就显得很笨拙了。实际上,state.mem
只是一堆用于正确访问下层内存存储的逻辑,而内存存储是一块填充着位向量数据的平坦地址空间:state.memory
。你可以直接对state.memory
使用.load(addr,size)
和store(addr,val)
方法:
可以看到,数据按照“大端”字节序存取,因为state.memory
的主要目的是存取没有附加语义的大块数据。然而,如果你想对存取的数据执行字节交换,那么你可以传入关键字参数endness
——如果你指定小端顺序,字节交换就会发生。字节序类型必须是在archinfo
包中angr用于存储CPU架构陈述性信息的枚举变量Endness
的成员之一:
另外,被分析的程序的字节序类型可以在arch.memory_endness
中找到——比如state.arch.memory_endness
。
寄存器也有低级接口,state.registers
:它和state.memory
使用完全相同的API,但是要解释它的行为需要深入angr用于无缝操作各个CPU架构的抽象结构。比较简短的解释是它仅仅是一个寄存器文件,其中包含着archinfo中定义的寄存器和偏移的映射。
状态选项
你可以对angr内部做许多调整,这些调整在某些情况下会优化程序执行,而有些情况下则正相反。这些调整可以通过状态选项控制。
对于每个SimState对象,都有一个state,option
集合,里面存储着所有所有已经开启的状态选项。每个选项(只是一个字符串)都以某种分钟的方式控制这angr执行引擎的行为。所有可选的选项,以及它们在不同类型的状态下的默认值都可以在这个附录中找到。你可以通过angr.options
获取一个独立的(状态)选项来添加到某个state中。这些独立的(状态)选项都被大写字母命名,当然你有可能想要使用多个选项对象的组合,这些组合用小写字母命名。
当你通过任何一个构造函数构造一个SimState时,你可以传入关键字参数add_options
和remove_options
,它们需要以选项集合的形式传入来修改默认的初始选项集合。
上图第一行启动了“lazy solves”选项,这个选项将会导致对约束条件是否满足的检测尽可能慢地执行;这个改变将会对从这一行代码之后由这个状态产生的所有衍生状态有效。
第二行代码在初始化状态的时候加入了“lazy solves”选项。
第三行代码创建了一个没有simplification选项的状态。
译者注:稍微看一下angr.options里面有哪些可选状态:
状态插件
除了刚刚讨论过的选项集,所有存储在SimState中的东西实际上都存储在附加在state上的“插件”中。到目前为止我们讨论的几乎所有state的属性都是一个插件——memory
、registers
、mem
、regs
、solver
等等。这种设计带来了代码的模块化和能够便捷地为模拟状态的其他方面实现新的数据存储,或者提供插件的替代实现能力。
比如说,通常memory
插件模拟一个平坦地址空间,但是在分析中可以选择开启“抽象内存”插件来支持state.memory
,“抽象内存”使用新的数据类型表示地址,以模拟浮动的独立内存空间映射。反过来,插件可以减少代码的复杂性:state.memory
和state.registers
实际上是同一个插件的不同实例,因为寄存器也是用一块地址空间模拟的。
全局插件
state.global
是一个很简单的插件:它实现了标准python dict接口,允许你在state中存储任意数据。
history插件
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
是上一个state最近执行的基础块地址的列表,等等。如果你需要快速获取这些值的平面列表,你可以使用.hardcopy
,比如,state.history.bbl_addrs.hardcopy
。但是请记住,基于索引的访问是在迭代器上实现的。
译者注:下面是对上面那些属性的测试,以便更加清楚地了解history迭代器的使用方法:
下面是一些存储在history插件中的值的简要列表:
-
history.descriptions
是描述state上每轮执行的状态的字符串列表。
history.bbl_addrs
是state已经执行过的基本块的地址列表。这里可能每轮执行一个以上,并且不是所有的地址都有对应的二进制代码——一些地址可能已经被SimProcedure hook了。-
history.jumpkinds
是状态历史中每个控制流转换的处置列表,就像VEX枚举字符。译者注:原文是 “VEX enum strings”,查不到相关资料,但是姑且看下执行效果:
这些字符串的含义可以在这里看到详细介绍,这里放一张表:
-
history.guards
是一个state当前所走路径需要满足的条件列表。译者注:这里应该是history.jump_guards:
-
history.events
是一些执行过程中发生的“有趣的事情”的列表,比如说符号跳转条件、程序弹出一个消息框或者程序执行到退出代码退出。
-
history.action
通常是空的,但如果你加入angr.options.refs
选项到state中,它将填充程序对所有内存、寄存器、临时值访问的日志。
调用栈插件
angr会跟踪所模拟的程序的调用栈。在每条call指令处,一个栈帧会被加载到被跟踪的调用栈的栈顶,并且当栈指针低于最上层被调用的栈帧时,一个栈帧就被弹出。这使得angr能够稳健地存储当前模拟的函数的局部数据。
和history一样,调用栈仍然是链表结构的,但是没有提供调用栈的专门的迭代器——取而代之,你可以直接使用state.callstack
来获取每一个没有被弹出的栈帧,顺序是最近调用优先。如果你想要最高的栈帧,那就是stata.callstack
。
译者注:迭代器方法使用state.callstack:
使用state.callstack 访问最高栈帧:
-
callstack.func_addr
是当前正在执行的函数地址。 -
callstack.call_site_addr
是调用当前函数的基本块地址。 -
callstack.stack_ptr
是从当前函数开始时的栈顶指针译者注:stack_ptr相当于是栈底指针(x86中的ebp),从下面的测试中可以看到:
-
callstack.ret_addr
是当前函数将要返回的地址。
posix插件
还没实现
使用文件系统
未完成:描述什么是文件系统
许多选项都可以被传给state的初始化过程来影响对文件系统的使用。包括fs
,concrete fs
和chroot
选项。
fs
选项使你可以文件名的字典来预配置SimFile对象。这使得你可以做例如设置文件内容的具体大小等事情。
设置concrete fs
选项为True
将会导致angr尊重磁盘上的文件。例如,如果在concrete fs
选项为False
的情况下(默认情况),模拟过程中程序试图打开“banner.txt”,一个新的SimFile对象会被创建并且模拟会继续进行,就好像这个文件确实存在一样。当concrete_fs
被置为true
时,如果“banner.txt”存在,一个新的SimFile对象会在有具体文件的支持的情况下被创建,这可以减少由完全符号化的文件引起的状态数爆炸。另外,在concrete_fs
模式下,如果“banner.txt”不存在,那么在调用打开文件的函数时时,SimFile对象将不会被创建并且返回一个错误码。另外需要强调的是如果要打开的文件的路径以/dev/
开头,那么这个文件将不会实际被打开,即使concrete_fs
被设置为True
。
chroot
选项是你能够在使用concrete_fs
选项时指定一个根目录。这在你分析的程序使用绝对路径打开一个文件时是很方便的。例如,如果你分析的程序试图打开/etc/passwd
,那么它将会实际打开$CWD/etc/passwd
这个文件。
这个例子将会创建一个最多能从标准输入读入30个字符的状态,并且对文件引用的具体解析都会在新的根目录/angr-chroot
下进行。
拷贝和合并
state支持快速拷贝,使你可以尝试不同的可能性:
state还可以被合并到一起:
merge操作将会返回一个三元组。
第一个元素是合并后的状态;
第二个元素是描述状态标志的符号变量;
第三个元素是描述合并是否完成的布尔值。
上图中的aaaa_or_bbbb
变量可以被解析成“AAAA”或者*“BBBB”
试着解析了一下s_merged的0x1000地址处的值,结果是一个“保留”值,其值根据一个符号的值决定,暂时不知道这个merge函数的用途和用法。
未完成:描述合并操作的局限性。