program state
在之前几章中,主要学习的是simulate state也就是模拟状态的程序运行状态。接下来将要学习的是state object
也就是状态这个对象的性质(包含但不限于simulate state),并且学习如何和这个对象进行交互。
基本执行
首先简单介绍一下simulator的一些性质,具体性质将在下一章介绍。这里简单介绍一些性质。
state.step()
执行符号执行的一步,并返回一个simsuccessors
和一般程序流执行不同,符号执行的一步将会返回许多结果,可以分为多种类型。我们在这里只关注.successor
类型,这代表的是对于一个给定的step,返回normal处理结果的步骤。注意successor包含了一个所有正常后继的list
为什么是一个list?(其实我觉得很好理解,就是分支)文档中是这么说明的
比如说一个条件判断if(x>4)
在angr中的某个位置,会被解析为<bool x_32_1 > 4>
是什么意思呢,就是x>4被抽想成为一个bool类型的符号。那么我们选择真的分支还是假的分支呢。我们都会选择。我们会产生两种状态:一种把x>4作为约束,另一种把!(x>4)作为约束。这样就会导致程序在正确的约束下执行。
文档给出一个说明:用来介绍succ=state.step()的用处
这个binary会先检查passwd是不是SOSNEAKY,如果是就A,不是就B,在这种情况下,就产生了分支。此程序就以此为入手点来说明state,step()的用处。
>>> 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()
# 到达了检查passwd的地址
... if len(succ.successors) == 2:
... break
... state = succ.successors[0]
>>> state1, state2 = succ.successors
>>> state1
<SimState @ 0x400629>
>>> state2
<SimState @ 0x400699
当程序从标准输入读取数据时,angr默认将其视为无限长的输入符号数据流。
我们查看一下输入数据流,一下api比较容易理解,可以看到就是两个state对应的输入。
>>> 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'
状态预设
之前讲到,我们使用的大都是entry_state,这只是许多state中的一种
state | explain |
---|---|
.blank_state | 大多数数据未初始化,进入时返回没有约束的符号值 |
.entry state | 准备调用main之前的状态 |
.full_init_state | 共享库和预定义内容都已经加载完毕,在enter状态之前,例如刚刚加载完共享库 |
.call_state | 准备调用函数的状态 |
调用方式见文档,觉得目前不需要使用这么复杂的调用。
http://angr.io/api-doc/angr.html#angr.factory.AngrObjectFactory
底层内存表示
state.mem
对象可以有load和store两种方法,它的用处就是直接将memory中放入data。(原文是In a flat way我觉得意思就是,看起来怎么样就怎么样的放入内存)
>>> 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>
由上图可以看出,BVV中的数据以大端法放入了内存。
当然是可选的,可以选择解析方式为大端还是小端。这里感觉和gdb很像。
>>> import archinfo
>>> s.memory.load(0x4000, 4, endness=archinfo.Endness.LE)
<BV32 0x67452301>
对于寄存器,state.registers
和state_memory
的使用方法是类似的。但是对于不同架构,寄存器类型不同,大小也不同。简单看一下是怎么实现的,有一个直观印象
https://github.com/angr/archinfo/blob/master/archinfo/arch_amd64.py
中的对于x86寄存器的表示方法,如图所示
可以看出主要是设置了名称,大小,下属寄存器
函数的前言和后缀
状态选项
对于每一个simstate
对象,都会有一个state.option来控制程序在后续所有步骤中的执行偏好
这样的偏好有比如将偏好分离求解;将满足偏好的分支尽量减少求解次数;避免创建有地址的符号元素等求解方式。
# 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)
详细的状态选项列表
https://docs.angr.io/appendix/options
状态插件
之前所讨论的memory,registers,mem,regs,slover
都算是一个插入在state中的一种状态。这样的设计让程序和我们的符号执行分离开来,便于模块化处理。
一些特殊的状态插件,例如abstract memory
插件可以模拟独立于地址的*浮动内存映射。
全局插件
state.globals
代表了标准python的字典,可以允许你在一个状态下任意地址储存数值
历史插件
state.history
可以用来查看历史执行的数据和选择的路径。类似于一种单链表,通过state.history.parent.parent
可以访问它。
history还有许多可以访问的内容。例如
history.descriptions
可以保留每一步执行的说明字符串
history.bbl_addrs
可以保留每一个执行的基本块的地址
history.jump_guards
可以保留每一次执行流遇到分支时的限制(constrains)的说明
等等。
函数调用插件
callstack
callstack
是一个单链表结构。每一次angr遇到call函数,都会在被追诉的callstack
最上层生成一个新的frame,当一个函数执行完,这样一个callstack
就被pop出来,这样可以增强angr分析的健壮性。
名称 | 说明 |
---|---|
callstack.func_addr | 当前执行函数(callstack单链表栈顶函数)地址 |
callstack.call_site_addr | 调用当前函数的基本块地址 |
callstack.stack_ptr | 当前函数的call_stack的栈顶指针 |
callstack.ret_addr | 当前执行函数返回到的父函数地址 |
复制和合并
复制用来对一个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
# n that can resolve to "AAAA" *or* "BBBB"
>>> aaaa_or_bbbb = s_merged.mem[0x1000].uint32_t
这里的"可以被解释为AAAA或者BBBB的状态“的含义不是很理解,于是做一个实验尝试一下
从上图中就可以看出来,其实还是一个状态,如果是状态1就是aaaa,否则就是bbbb的一个叠加体
总结
这一节主要讲了在angr分析程序执行流过程中的一些底层数据处理方式。包括对不同架构的处理细节原理,有关state的一些api和一些状态插件,求解偏好,函数调用的数据结构和对这种数据结构的交互方式。从最后一个小例子可以看出,每一个状态state其实本质就是一个逻辑序列。这一节比较抽象,没有具体的例子用来学习。