Angr:Solver Engine

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


angr的强大之处不在于它是一个模拟器,而在于它能够使用我们所称的符号变量来执行。与其说变量有一个具体的数值,不如说它包含一个符号,实际上只是一个名称。然后,使用该变量执行算术操作将生成操作树(根据编译器理论,称为抽象语法树或AST)。AST可以转换为SMT求解器(如z3)的约束,以便提出诸如“给定这个操作序列的输出,输入必须是什么?”在这里,您将学习如何使用angr来回答这个问题。


Working with Bitvectors

让我们得到一个虚拟的项目和状态,这样我们就可以开始玩耍了。

>>> import angr, monkeyhex
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()

位向量就是一组位的序列,用有界整数的语义来解释。我们来做几个。

# 64-bit bitvectors with concrete values 1 and 100
>>> 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>

正如您所看到的,您可以有任何位序列,并将它们称为位向量。你也可以用它们做运算:

>>> one + one_hundred
<BV64 0x65>

# You can provide normal python integers and they will be coerced to the appropriate type:
>>> one_hundred + 0x100
<BV64 0x164>

# The semantics of normal wrapping arithmetic apply
>>> one_hundred - one*200
<BV64 0xffffffffffffff9c>

不过,你不能执行one + weird_nine。对不同长度的位向量执行操作是一种类型错误。但是,您可以扩展weird_nine,这样它就有合适的比特数:

>>> weird_nine.zero_extend(64 - 27)
<BV64 0x9>
>>> one + weird_nine.zero_extend(64 - 27)
<BV64 0xa>

zero_extend将用零填充左边的位向量。您还可以使用sign_extend填充与最高位一样的位,即符号扩展。
现在,让我们引入一些符号变量。

# 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>

x和y现在是符号变量,有点像你们在初中数学里学过的变量。注意,您提供的名称已经被附加递增计数器修饰过,您可以使用它们执行任意数量的算术操作,但是您不会得到一个数字,而是得到一个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>

从技术上讲,xy甚至one也是AST——任何位向量都是操作树,即使树只有一层深。为了理解这一点,让我们学习如何处理AST。
每个AST都有.op.args,op是正在执行的操作的名称,args是操作的输入。树将终止于BVV或BVS,在op不是BVV或BVS时,输入是其他所有的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)

从这里开始,我们将使用“bitvector位向量”这个词来指代其最顶层操作生成位向量的任何AST。还可以通过AST表示其他数据类型,包括浮点数和我们即将看到的布尔值。


Symbolic Constraints符号约束

在任意两个类型相似的AST之间执行比较操作将生成另一个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>

从这里可以看出比较默认是无符号的。最后一个示例中,-5被强制转换成<BV64 0xfffffffffffffffb>,结果比100大。如果你想进行有符号的比较,你可以使用one_hundred.SGT(-5)(“signed greater-than”)。完整的操作列表可以在本章末尾找到。
这段代码还说明了使用angr的一个重要问题——永远不要在if或while语句的条件中直接进行不同变量之间的比较,因为答案可能没有一个具体的真值。

>>> 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

Constraint Solving约束求解

您可以将任何符号布尔值视为关于符号变量有效值的断言,方法是将其添加为状态的约束。然后,您可以通过请求对符号表达式求值来查询符号变量的有效值。
通过一个例子可能看得更清楚:

>>> state.solver.add(x > y)
>>> state.solver.add(y > 2)
>>> state.solver.add(10 > x)
>>> state.solver.eval(x)
4

通过向状态添加这些约束,我们迫使约束求解器将它们视为必须满足其返回的任何值的断言。如果运行这段代码,可能会得到x的不同值,但是这个值肯定大于3(因为y必须大于2,x必须大于y),并且小于10。
从这里,很容易看到如何完成我们在本章开始时提出的任务——查找产生给定输出的输入。

# get a fresh state without constraints
>>> state = proj.factory.entry_state()
>>> input = state.solver.BVS('input', 64)
>>> operation = (((input + 4) * 3) >> 1) + input
>>> output = 200
>>> state.solver.add(operation == output)
>>> state.solver.eval(input)
0x3333333333333381

注意,同样,这个解决方案只适用于位向量语义。如果我们在整数域上操作,就没有解!
如果我们添加冲突或矛盾的约束,这样就没有可以分配给变量的值,这样约束就得到了满足,状态就变得不可满足,或者unsat,对它的查询将引发异常。您可以使用state. satisizable()检查状态的可满足性。

>>> state.solver.add(input < 2**32)
>>> state.satisfiable()
False

您还可以计算更复杂的表达式,而不仅仅是单个变量。

# 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

从这里我们可以看出,eval是一种通用的方法,它可以将任何位向量转换成python基本元,同时又保证了状态的完整性。这也是为什么我们使用eval将具体的位向量转换为python int的原因!
还请注意,尽管使用旧状态创建了x和y变量,但仍然可以在新状态中使用它们。变量不受任何一种状态的约束,可以自由存在。


Floating point numbers浮点型

z3支持IEEE754浮点数的理论,因此angr也可以使用它们。主要的区别是,浮点数有一个排序,而不是宽度。您可以使用FPV和FPS创建浮点值和符号。

# fresh state
>>> state = proj.factory.entry_state()
>>> 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))>

因此,这里有一些需要解释的地方——对于初学者来说,打印浮点数的结果显得没有那么聪明。但除此之外,大多数操作实际上都有使用二进制运算符时隐式添加的第三个参数-舍入模式。IEEE754规范支持多种舍入模式(四舍五入、去尾、进位等),因此z3必须支持它们。如果希望为操作指定舍入模式,请在fp操作时(以solver.fpAdd为例),指定一个舍入模式(solver.fp.RM_*其中的一个)作为第一个参数。
约束和求解方法是相同的,但是eval返回一个浮点数:

>>> state.solver.add(b + 2 < 0)
>>> state.solver.add(b + 2 > -1)
>>> state.solver.eval(b)
-2.4999999999999996

这很好,但有时我们需要能够直接将浮点数表示为位向量。可以用raw_to_bvraw_to_fp方法将位向量解释为浮点数,反之亦然:

>>> a.raw_to_bv()
<BV64 0x400999999999999a>
>>> b.raw_to_bv()
<BV64 fpToIEEEBV(FPS('FP_b_0_64', DOUBLE))>

>>> 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)>

这些转换保留位模式,就像将浮点指针转换为int指针一样,反之亦然。但是,如果希望尽可能地保留值,就像将浮点数转换为int(反之亦然)一样,可以使用另一组方法val_to_fpval_to_bv。由于浮点数的浮点性,这些方法必须将目标值的大小或排序作为参数。

>>> 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个解,如果可能小于n,返回的值将小于n。
  • solver.eval_atleast(expression, n)将给出给定表达式的n个解,如果可能小于n,则抛出一个错误。
  • solver.eval_exact(expression, n)将给出给定表达式的n个解,如果可能的值小于或大于,则抛出一个错误。
  • solver.min(expression)将给出给定表达式的最小可能解。
  • solver.max(expression)将给出给定表达式的最大可能解。

此外,所有这些方法都可以采用以下关键字参数:

  • extra_constraints可以作为约束的元组传递。这些限制将被考虑,但是不会被添加到该状态。
  • cast_to可以传递要将结果转换为什么数据类型。目前,这只能是int和bytes,这将导致方法返回底层数据的对应表示。例如,state.solver.eval(state.solver.BVV(0x41424344, 32), cast_to=bytes) 将返回b'ABCD'
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值