solver engine
简介
angr的威力不在于模拟执行,而在于将输入抽象为符号,通过将符号(也就是静态或者全局变量的名称,局部变量在编译过程中不具有符号)以及符号的运行过程抽象为语法树,判断输出结果。就像z3的工作一样。我们在这一章中需要解决的问题是:
我知道输出XXX所需要的操作序列是ABCD,那么我的输入应该是多少
bit vector
bit vector可以说是模拟了c语言中对于整数的解析方式。例如将int转换为32bit的储存形式。称作bit-vector
当然32位不是必须的,任意位数都可以
>>> import angr, monkeyhex
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()
# 64-bit bitvectors with concrete values 1 and 100
# 注意这里bit vector是针对state.solver对象的,也就是说针对state才有vector
>>> one = state.solver.BVV(1, 64)
>>> one
<BV64 0x1>
>>> one_hundred = state.solver.BVV(100, 64)
>>> one_hundred
<BV64 0x64>
# create a 27-bit bitvector with concrete value 9
>>> weird_nine = state.solver.BVV(9, 27)
>>> weird_nine
<BV27 0x9>
bit vector内部可以进行加减操作。但是注意需要相同长度的vector才可以运算。
可以使用zero_extend或者sign_extend进行补齐操作
# 零补齐
>>> weird_nine.zero_extend(64 - 27)
<BV64 0x9>
>>> one + weird_nine.zero_extend(64 - 27)
<BV64 0xa>
bitvector symbol
bitvector symbol(BVS
)和bitvector的区别在于:所有的运算都不会修改vector本身的值,并且这里的值只是一个未知数,就像代数中的x和y。每一次对于BVS的运算会转换为一系列运算方式(AST
): abstract syntax tree
# Create a bitvector symbol named "x" of length 64 bits
>>> x = state.solver.BVS("x", 64)
>>> x
<BV64 x_9_64>
>>> y = state.solver.BVS("y", 64)
>>> y
<BV64 y_10_64>
# compute and get AST
>>> x + one
<BV64 x_9_64 + 0x1>
>>> (x + one) / 2
<BV64 (x_9_64 + 0x1) / 0x2>
>>> x - y
<BV64 x_9_64 - y_10_64>
接下来介绍AST是什么,其实AST就是一种树状结构,每一个节点都有两个属性:op(operation)和args
下图为一个AST实例
>>> tree = (x + 1) / (y + 2)
>>> tree
<BV64 (x_9_64 + 0x1) / (y_10_64 + 0x2)>
>>> tree.op
'__floordiv__'
>>> tree.args
(<BV64 x_9_64 + 0x1>, <BV64 y_10_64 + 0x2>)
>>> tree.args[0].op
'__add__'
>>> tree.args[0].args
(<BV64 x_9_64>, <BV64 0x1>)
>>> tree.args[0].args[1].op
'BVV'
>>> tree.args[0].args[1].args
(1, 64)
下图为上述算式的AST表示。每一个最终的阶段最后都归结于BVV或者BVS
符号限制
符号限制出现在两个类型相同的AST中,当两个AST比较时,会产生一个新的数据类型:符号布尔类型
下图为比较的一些例子
# 注意这里x是BVS,但是BVS和BVV类型类似,可以构成相同类型的AST
>>> x == 1
<Bool x_9_64 == 0x1>
>>> x == one
<Bool x_9_64 == 0x1>
>>> x > 2
<Bool x_9_64 > 0x2>
>>> x + y == one_hundred + 5
<Bool (x_9_64 + y_10_64) == 0x69>
>>> one_hundred > 5
<Bool True>
# 默认作为无符号比较
>>> one_hundred > -5
<Bool False>
# 如果想要有符号比较
>>> one_hundred.SGT(-5)
<Bool True>
注意:大于小于号比较时默认作为unsigned类型(即类似c语言,降低表示级别)
如果想有符号比较,需要使用SGT类型,正如上面所展示的一样
注意,如果想要比较布尔类型,需要使用solver.is_true,这个api只对确定的值进行比较,并不执行约束求解。比如最下面maybe的true和false都是false,表示solver并没有进行约束求解
>>> yes = one == 1
>>> no = one == 2
>>> maybe = x == y
>>> state.solver.is_true(yes)
True
>>> state.solver.is_false(yes)
False
>>> state.solver.is_true(no)
False
>>> state.solver.is_false(no)
True
>>> state.solver.is_true(maybe)
False
>>> state.solver.is_false(maybe)
False
约束求解
对于每一个符号,都可以对他添加一个符号布尔类型作为比较依据,以此进行求解
下图为对于二元一次方程组的求解。和z3-solver很相似。这些约束和c语言中assert非常相似,属于必须满足的内容。
>>> state.solver.add(x > y)
>>> state.solver.add(y > 2)
>>> state.solver.add(10 > x)
>>> state.solver.eval(x)
4
一下是一个很小的综合演示,表现了怎样使用约束求解工具。注意这里必须是BVS类型,否则无法求解
注:eval常用于将bvv类型转换为int类型
# get a fresh state without constraints
>>> state = proj.factory.entry_state()
# 创建bit vector symbol:64位数字
>>> input = state.solver.BVS('input', 64)
# operation为AST类型
>>> operation = (((input + 4) * 3) >> 1) + input
>>> output = 200
# 添加约束
>>> state.solver.add(operation == output)
>>> state.solver.eval(input)
0x3333333333333381
约束求解不满足的情况,使用satinfiable
标识
>>> state.solver.add(input < 2**32)
>>> state.satisfiable()
False
注意,创建的符号和state无关。虽然目前没有涉及和state相关的计算,但是两者是分开的
# fresh state
>>> state = proj.factory.entry_state()
>>> state.solver.add(x - y >= 4)
>>> state.solver.add(y > 0)
>>> state.solver.eval(x)
5
>>> state.solver.eval(y)
1
>>> state.solver.eval(x + y)
6
上图为更加复杂一点的计算。注意在清空state之后,对于x和y并没有重新定义。symbol和state是分开的。
浮点数据
angr加入了IEEE浮点类型表示方法。和整数类型很相似
下图为创建FPV和FPS的方式,以及和BVV类似的运算结构(AST)
# fresh state
>>> state = proj.factory.entry_state()
# 注意这里的FSORT_DOUBLE是指数据宽度
>>> a = state.solver.FPV(3.2, state.solver.fp.FSORT_DOUBLE)
>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> b = state.solver.FPS('b', state.solver.fp.FSORT_DOUBLE)
>>> b
<FP64 FPS('FP_b_0_64', DOUBLE)>
>>> a + b
<FP64 fpAdd('RNE', FPV(3.2, DOUBLE), FPS('FP_b_0_64', DOUBLE))>
>>> a + 4.4
<FP64 FPV(7.6000000000000005, DOUBLE)>
>>> b + 2 < 0
<Bool fpLT(fpAdd('RNE', FPS('FP_b_0_64', DOUBLE), FPV(2.0, DOUBLE)), FPV(0.0, DOUBLE))>
但是对于AST结构体,存在着第三种op,也就是rounding type(对齐格式)有向上,向下,最近整数等等
使用solver.fp.RM_*
来添加对齐格式
尽管eval可以直接根据对齐方式输出浮点数,有的时候我们也需要精确表示浮点数。此时可以将FP转换为BVV类型。反之亦然。
# a=3.2
# 将a转换为bit vector格式
>>> a.raw_to_bv()
<BV64 0x400999999999999a>
# b是一个fps(floating point symbol),将其转换为bit vector类型,保留了符号名称
>>> b.raw_to_bv()
<BV64 fpToIEEEBV(FPS('FP_b_0_64', DOUBLE))>
# 将bvv和bvs转换为浮点类型
>>> state.solver.BVV(0, 64).raw_to_fp()
<FP64 FPV(0.0, DOUBLE)>
>>> state.solver.BVS('x', 64).raw_to_fp()
<FP64 fpToFP(x_1_64, DOUBLE)>
上面为raw表示,即完全表示,不丢失数据。如果想要尽可能保留数据的value(想要理解这一点,必须先要对float的表示类型有基本的理解:int和float的解析方式是完全不同的。直接转换为bit表示可能会和实际数据的值有很大的差距)
如果想要尽可能保持value,需要使用val_to_fp表示。
这种方法需要一个bit vector的大小作为转换参数。此方法也可以作用于有符号类型
>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> a.val_to_bv(12)
<BV12 0x3>
>>> a.val_to_bv(12).val_to_fp(state.solver.fp.FSORT_FLOAT)
<FP32 FPV(3.0, FLOAT)>
更多求解输出
之前的eval
只是输出一种可能的结果,如果想要添加更多约束,也有相应的输出方式。
表达式 | 说明 |
---|---|
solver.eval(expression) | 输出表达式的一种可能结果 |
solver.eval_one(expression) | 输出唯一结果,否则抛出异常 |
solver.eval_upto(expression, n) | 输出至多n种结果,不足不报错 |
solver.eval_atleast(expression, n) | 输出至少n种结果,不足报错 |
solver.eval_exact(expression, n) | 输出正好n个结果,多了少了都报错 |
solver.min/max(expression) | 输出某个表达式的最小、最大结果 |
更多的表达式可见附录
https://docs.angr.io/appendix/ops
总结
我们来想一下一开始提到的问题
我知道输出XXX所需要的操作序列是ABCD,那么我的输入应该是多少
这个问题现在的理解就是:操作序列和输出XXX就是加入的约束,我们需要知道的就是一开始写入的BVV,BVS,FPV,FPS数据内容。
在solver_engine中,主要学习的是求解器原理和数据结构表示方法。数据类型基于BVV,BVS,FPV,FPS代表了未知数和数据,类型是整数和浮点类型。要注意一般的大于小于比较方式是基于unsigned的。求解器基于AST数据结构(我的理解就是树状结构),每一个AST可以作为solver的一个约束条件用来求解,最后输出时我们可以指定输出格式。angr的solver原理和z3的十分相似,也可以作为z3的知识补充学习。