原文
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
>>> one_hundred = state.solver.BVV(100, 64)
>>> one_hundred
# create a 27-bit bitvector with concrete value 9
>>> weird_nine = state.solver.BVV(9, 27)
>>> weird_nine
正如你所看到的,你可以有任何的位序列并称之为位向量。你也可以用它们来做数学:
>>> one + one_hundred
# You can provide normal python integers and they will be coerced to the appropriate type:
>>> one_hundred + 0x100
# The semantics of normal wrapping arithmetic apply
>>> one_hundred - one*200
但是你不能说one + weird_nine。对不同长度的位向量执行操作是一个类型错误。但是,你可以扩展weird_nine,让它有一个合适的位数:
>>> weird_nine.zero_extend(64 - 27)
>>> one + weird_nine.zero_extend(64 - 27)
zero_extend将用给定的零位数填充左边的位向量。您还可以使用sign_extend来填充最高位的副本,在2的补符号整数语义下保留位向量的值。
现在,让我们引入一些符号。
# Create a bitvector symbol named "x" of length 64 bits
>>> x = state.solver.BVS("x", 64)
>>> x
>>> y = state.solver.BVS("y", 64)
>>> y
X和y现在是符号变量,有点像你在七年级代数中学到的变量。请注意,您提供的名称由于添加递增计数器而被打乱了,您可以对它们进行任意的算术运算,但您不会得到一个数字,而是得到一个AST。
>>> x + one
>>> (x + one) / 2
>>> x - y
从技术上讲,x和y甚至其中一个也是ast——任何位向量都是一棵操作树,即使这棵树只有一层深度。为了理解这一点,让我们学习如何处理ast。
每个AST都有一个.op和一个.args。op是命名正在执行的操作的字符串,args是操作作为输入的值。除非op是BVV或BVS(或其他一些…),否则参数都是其他所有的ast,树最终以BVV或BVS结束。
>>> tree = (x + 1) / (y + 2)
>>> tree
>>> tree.op
'__floordiv__'
>>> tree.args
(, )
>>> tree.args[0].op
'__add__'
>>> tree.args[0].args
(, )
>>> tree.args[0].args[1].op
'BVV'
>>> tree.args[0].args[1].args
(1, 64)
从这里开始,我们将使用“位向量”这个词来指代任何其最顶层操作产生位向量的AST。还可以通过ast表示其他数据类型,包括浮点数,以及我们即将看到的布尔值。
Symbolic Constraints
在任意两个相似类型的AST之间执行比较操作将产生另一个AST—不是位向量,而是符号布尔值。
>>> x == 1
>>> x == one
>>> x > 2
0x2>
>>> x + y == one_hundred + 5
>>> one_hundred > 5
>>> one_hundred > -5
从这里您可以看到一个小窍门,在默认情况下,比较是无符号的。上一个示例中的-5被强制转换为
这段代码还说明了处理angr的一个重要要点——永远不要在if或while语句的条件中直接使用变量之间的比较,因为答案可能没有具体的真值。即使有一个具体的真值,if one > one_hundred将引发异常。相反,你应该使用solver.is_true和solver.is_false,在不执行约束求解的情况下测试具体的真/假。
>>> 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。此外,如果你运行state.solver.eval(y)你会得到y的值,它和你得到的x的值是一致的。如果您没有在两个查询之间添加任何约束,那么结果将彼此一致。
从这里,很容易看到如何完成本章开始时提出的任务——找到产生给定输出的输入。
# 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.satisfiable()检查状态的可满足性。
>>> 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
>>> b = state.solver.FPS('b', state.solver.fp.FSORT_DOUBLE)
>>> b
>>> a + b
>>> a + 4.4
>>> b + 2 < 0
因此这里有一点需要解释——对于初学者来说,漂亮的打印对于浮点数来说并不那么聪明。但除此之外,大多数操作实际上都有第三个参数,在使用二进制运算符时隐式添加——舍入模式。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_bv和raw_to_fp方法将位向量解释为浮点数,反之亦然:
>>> a.raw_to_bv()
>>> b.raw_to_bv()
>>> state.solver.BVV(0, 64).raw_to_fp()
>>> state.solver.BVS('x', 64).raw_to_fp()
这些转换保留位模式,就像将float指针强制转换为int指针,反之亦然。但是,如果您希望尽可能地保持该值,就像您将float类型转换为int类型一样(反之亦然),您可以使用一组不同的方法,val_to_fp和val_to_bv。由于浮点数的浮点性质,这些方法必须将目标值的大小或排序作为参数。
>>> a
>>> a.val_to_bv(12)
>>> a.val_to_bv(12).val_to_fp(state.solver.fp.FSORT_FLOAT)
这些方法还可以接受一个有符号的参数,用于指定源或目标位向量的有符号性。
More Solving Methods
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(表达式)将给出给定表达式的最小可能解。
- solver.max(表达式)将给出给定表达式的最大可能解。
此外,所有这些方法都可以接受以下关键字参数: - extra_constraints可以作为一个约束元组传递。这些约束将被考虑到这个评估中,但不会添加到状态中。
- cast_to可以传递一个数据类型来将结果转换。
目前,这只能是int和bytes,这将导致该方法返回底层数据的相应表示。
例如,state.solver.eval (state.solver.BVV(0x41424344, 32), cast_to=bytes)将返回b'ABCD '。
总结
太多了!!阅读本文后,您应该能够创建和操作位向量、布尔值和浮点值,以形成操作树,然后查询附加到状态的约束求解器,以获得一组约束下的可能解决方案。希望至此您了解了使用ast表示计算的能力,以及约束求解器的能力。
在附录中,您可以找到可以应用于ast的所有附加操作的参考,以备您需要快速查看表时使用。