angr-Core Concepts(翻译)

官方文档原文:https://docs.angr.io/core-concepts

注:该翻译纯为自嗨


loading a binary

在之前,加载/bin/true时,你只看到了angr最基本的加载工具,在接下来,我们会在没有动态库的情况下再次加载它。你同样会看到project.loader和一些它能做到的事。现在,我们将深入讨论这些接口的细微差别以及它们能提供的信息。

我们简要地提到了angr的二进制加载文件,CLE。CLE代表CLE Loads Everything,它负责获取二进制文件并以易于使用的方式提供给angr的其余部分。

 

the loader

让我们重新加载/bin/true,然后深入了解如何与loader进行交互

>>> import angr, monkeyhex
>>> proj = angr.Project('/bin/true')
>>> proj.loader
<Loaded true, maps [0x400000:0x5008000]>

 

loaded objects

CLE loader(cle.Loader)表示要加载的二进制对象的整个集合,加载并映射到某个内存空间。每一个二进制对象都由一个可处理其文件类型的加载器后端加载。例如,cle.ELF用于加载ELF文件。

内存中也存在一些对象与任何已加载二进制文件都无法对应。比如,用于提供本地线程存储支持的对象,以及用于提供未解析符号的外部对象。

你可以使用loader.all_objects获取CLE已加载对象的完整列表,以及一些更有针对性的分类

 

# All loaded objects
>>> proj.loader.all_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
<ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>,
<ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>,
<ELFTLSObject Object cle##tls, maps [0x3000000:0x300d010]>,
<KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>,
<ExternObject Object cle##externs, maps [0x5000000:0x5008000]>
# This is the "main" object, the one that you directly specified when loading the project
>>> proj.loader.main_object
<ELF Object true, maps [0x400000:0x60105f]>
# This is a dictionary mapping from shared object name to object
>>> proj.loader.shared_objects
{ 'libc.so.6': <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>
'ld-linux-x86-64.so.2': <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>}
# Here's all the objects that were loaded from ELF files
# If this were a windows program we'd use all_pe_objects!
>>> proj.loader.all_elf_objects
[<ELF Object true, maps [0x400000:0x60105f]>,
<ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>,
<ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>]
# Here's the "externs object", which we use to provide addresses for unresolved imports and angr internals
>>> proj.loader.extern_object
<ExternObject Object cle##externs, maps [0x5000000:0x5008000]>
# This object is used to provide addresses for emulated syscalls
>>> proj.loader.kernel_object
<KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>
# Finally, you can to get a reference to an object given an address in it
>>> proj.loader.find_object_containing(0x400000)
<ELF Object true, maps [0x400000:0x60105f]>

你可以与这些对象直接交互来提取元数据

>>> obj = proj.loader.main_object
# The entry point of the object
>>> obj.entry
0x400580
>>> obj.min_addr, obj.max_addr
(0x400000, 0x60105f)
# Retrieve this ELF's segments and sections
>>> obj.segments
<Regions: [<ELFSegment offset=0x0, flags=0x5, filesize=0xa74, vaddr=0x400000, memsize=0xa74>,
<ELFSegment offset=0xe28, flags=0x6, filesize=0x228, vaddr=0x600e28, memsize=0x238>]>
>>> obj.sections
<Regions: [<Unnamed | offset 0x0, vaddr 0x0, size 0x0>,
<.interp | offset 0x238, vaddr 0x400238, size 0x1c>,
<.note.ABI-tag | offset 0x254, vaddr 0x400254, size 0x20>,
...etc
# You can get an individual segment or section by an address it contains:
>>> obj.find_segment_containing(obj.entry)
<ELFSegment offset=0x0, flags=0x5, filesize=0xa74, vaddr=0x400000, memsize=0xa74>
>>> obj.find_section_containing(obj.entry)
<.text | offset 0x580, vaddr 0x400580, size 0x338>
# Get the address of the PLT stub for a symbol
>>> addr = obj.plt['abort']
>>> addr
0x400540
>>> obj.reverse_plt[addr]
'abort'
# Show the prelinked base of the object and the location it was actually mapped into memory by CLE
>>> obj.linked_base
0x400000
>>> obj.mapped_base
0x400000

 

Symbol and Relocations

当你使用CLE时,你也可以使用符号。符号是可执行格式世界中的基本概念,它有效地将名称映射为一个地址。

从CLE获取符号最简单的方法是使用loader.find_symbol,它以一个名字或地址作为参数,返回一个符号对象。

>>> malloc = proj.loader.find_symbol('malloc')
>>> malloc
<Symbol "malloc" in libc.so.6 at 0x1054400>

一个符号对象最有用的属性是它的名字,它的所有者和它的地址,但一个符号的地址其实是含糊的。符号对象有三种方式来报告它的地址:

  • .rebased_addr是它全局地址空间中的地址,这就是打印输出的中显示的内容
  • .linked_addr 是它相对于二进制的预链接基址的地址
  • .relative_addr是相对对象基址的偏移地址,在Windows中也被称为RVA
>>> malloc.name
'malloc'
>>> malloc.owner_obj
<ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>
>>> malloc.rebased_addr
0x1054400
>>> malloc.linked_addr
0x54400
>>> malloc.relative_addr
0x54400

除了提供额外的调试信息以外,符号也支持动态链接的概念。libc以malloc作为导出符号,而主要的二进制文件依赖于它。如果我们让CLE直接从main_object获取一个malloc符号,它会告诉我我们这是一个导入符号。导入符号没有与它们相关联的有意义的地址,但它确实提供了用于解析它们符号的引用,像.resolvedby

>>> malloc.is_export
True
>>> malloc.is_import
False
# On Loader, the method is find_symbol because it performs a search operation to find the symbol.
# On an individual object, the method is get_symbol because there can only be one symbol with a given name.
>>> main_malloc = proj.loader.main_object.get_symbol("malloc")
>>> main_malloc
<Symbol "malloc" in true (import)>
>>> main_malloc.is_export
False
>>> main_malloc.is_import
True
>>> main_malloc.resolvedby
<Symbol "malloc" in libc.so.6 at 0x1054400>

导入符号和导出符号之间的链接需要在内存中进行注册,具体的方式是由名为重定位的过程进行处理的。重定位的意思是,当一个导出符号与[import]符号相匹配,就将这个导出符号的地址写入[location]中,然后格式化为[format]。我们可以将对象的重定位完整列表(重定位实例)看作obj.relocs,或者只将从符号名到重定向的映射看作obj.imports。但没有相应的导出符号列表。

一个重定位项对应的导入符号可以通过.symbol来访问。重定位要写入的地址可通过用于symbol的地址标识符来访问,且你也可以使用.owner_obj来获取对请求重定位对象的引用。

# Relocations don't have a good pretty-printing, so those addresses are python-internal, unrelated to our program
>>> proj.loader.shared_objects['libc.so.6'].imports
{u'__libc_enable_secure': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4221fb0>,
u'__tls_get_addr': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x425d150>,
u'_dl_argv': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4254d90>,
u'_dl_find_dso_for_object': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x425d130>,
u'_dl_starting_up': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x42548d0>,
u'_rtld_global': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4221e70>,
u'_rtld_global_ro': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4254210>}

如果导入符号不能被任何导出符号处理,比如,共享库无法找到,CLE会自动更新外部对象(loader.extern_obj)来声明它将作为符号导出。

 

Loading Options

如果你正使用angr.Project加载某些东西,并且你想传递某种操option到隐式创建的cle.Loader实例中,你可以直接传入关键字参数到Project构造器中,接着它就会传递到CLE去。如果你想知道所有可以传入的option,你应该查看CLE API文档,而我们只介绍一些重要和常用的option。

 

Basic Options

我们已经讨论过auto_load_libs,它可以启用或禁用CLE自动解析动态依赖库,它在默认情况下是开启的。此外,也有与之相反的except_missing_libs,若它设置为true,每当二进制文件出现无法解析动态库依赖的情况,就会抛出异常。

你可以向force_load_libs传递一个字符串列表,列出的任何内容分都将被视为未解析的共享库依赖关系。或者你也可以床底一个字符串列表到skip_libs,以防止该名称的任何库被解析为依赖项。此外,你可以传递字符串列表到ld_path,它用于添加共享库的搜索路径,且在任何默认方式之前:被加载程序的相同目录,当前工作目录,系统共享库等。

 

Per-Binary Options

如果你想指定一些options,且只应用到某个特定的二进制对象,CLE也能做到。参数main_ops和lib_opts通过使用options字典来完成这个操作。main_opts是一个从option name到option value的映射,当lib_opts是一个从library name到字典的映射,这个字典将option name映射到option value。

你可以使用的option因后端而异,不过这有一些常见的option:

  • backend,使用哪一个后端作为class或name
  • base_addr,基址的使用
  • entry_point,入口点的使用
  • arch,架构名称的使用

例子:

angr.Project(main_opts={'backend': 'ida', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})

 

Backends

CLE目前拥有静态加载ELF,PE,CGC,Mach-O和ELF core dump文件的后端,就和IDA加载文件到平坦地址空间一样。在大多数情况下,CLE会自动检测并使用正确的后端,所以你不需要具体得去指定某个后端,除非你做了一些十分奇怪的事。

就像上面说的那样,你可以通过在其options字典中包含一个键来强制CLE来使用特定的后端作为对象。一些后端不能自动检测使用哪一种体系结构,这时就必须具体的指定arch。key不需要匹配任何体系结构列表。对于任何支持的架构,angr会根据你指定的结构体系,来给出公共标识符。

要引用一个后端,可以使用下表中的名字:

 

 

Symbolic Function Summaries

默认的,Project尝试使用称为SimProcedures的符号摘要替换对库函数的外部调用,它的本质是一个python函数,用于模拟库函数对状态的影响。

我们已经实现了一大堆SimProcedures功能。这些内建过程可通过angr.SIM_PROCEDURES字典来使用,这个字典有两个级别,第一个键是package name(libc, posix, win32, stubs),然后是这些库函数名。执行SimProcedures而非真正的库函数能让分析变得更容易处理,但代价是存在一些不准确性。

当没有一个指定函数的相关summary时:

  • 如果auto_load_libs为True(缺省值),则实际的库函数会被执行。它会不会像你想的那样,取决于实际的函数,比如,一些libc的函数分析起来极为复杂,十分可能造成路径状态数量的激增。
  • 如果auto_load_libs为False,则外部函数是未解析的,Project将解析它们为名为ReturnUnconstrained的通用"stub"SimProcedure。就像它的名字说的那样,它每次调用时都会返回一个唯一的无约束符号值。
  • 如果use_sim_procedures(它是angr.Project的参数,而非cle.Loader的)为False(缺省值为True),则只有外部对象提供的符号会被替换为SimProcedure。且它们会被一个stub ReturnUnconstrained替换,它们除了返回一个符号值以外什么也不做
  • 你可以排除指定的特殊符号,并通过向angr.Project传参来用SimProcedures替换这些符号:exclude_sim_procedures_list和exclude_sim_procedures_func
  • 查看angr.Project_register_object的代码可以获取确切的算法

 

Hooking

angr用python summary来替换函数库代码的机制称为hook,你也可以这样做。在模拟执行时,每一步angr都会检查当前地址是否已hook,如果被hook,则执行这个hook而不是这个地址原本的二进制代码。project.hook(addr, hook)这个API能让你完成这项工作,其中hook是一个SimProcedure实例。你可以使用.is_hooked,.unhook和.hooked_by等方法来管理你的project's hook。

还有一个用于hook的备用API,它使用project.hook(addr)作为函数装饰器,它能让你指定自己的off-the-cuff函数用于hook。如果你这样做了,你也可以可选择的指定一个length关键字,当你的hook执行完成后,向前跳转length字节。


solver engine 

angr的强大之处并非是作为模拟器,而是它可以用符号变量的方式进行执行。符号实际上只是一个名字,并没有具体的数值。然后,使用这种变量执行算数运算来产生一颗操作树(即编译器理论中的抽象语法树),抽象语法树可以为z3这样的SMT求解器提供约束,用于解决给定这一系列输出,输入是什么的问题。在这里,你将学习到如何用angr来回答这个问题。

 

working with Bitvectors

首先我们构造一个project和state

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

一个bitvector就是一个bit序列,它可以用算数的有界整数的语义来解释

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

就像你看到的那样,你可以将任意bit序列作为bitvector,你可以将它们用于数学计算

>>> 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,由于它们的bit长度不同,会导致类型错误,你可以扩展weird_nine,使得它有适当的位数

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

zero_extend将使用给定的零位数填充左侧的位向量,同样的,你也可以使用sign_extend来进行符号扩展。

接下来让我们介绍混合一些符号到其中

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

确切的说,x和y,甚至是one都是AST,任意bitvector都是一颗操作树。即使这棵树只有一层的深度。为了理解这一点,我们来看看如何处理AST。

每一个AST都有.op和.args属性。op是要完成的操作的字符串名字,args则是操作的输入值。除非op就是BVV或BVS,否则agrs就是所有的AST,树总是以BVV或BVS来结尾

>>> tree = (x + 1) / (y + 2)
>>> tree
<BV64 (x_9_64 + 0x1) / (y_10_64 + 0x2)>
>>> tree.op
'__div__'
>>> 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来代指最顶层操作产生bitvector的AST,也可以通过AST来表示其他数据类型,包括浮点数以及即将看到的布尔数。

 

Symbol Constraint

将两个相同类型的AST进行比较会产生另一种AST,它并非bitvector,而是符号化的boolean

>>> 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),SGT即(signed greater-than),完整的操作列表可以在本文末尾找到。

这一消息也解释了使用angr的一个重要的点,你不应该在if或while语句中直接使用变量的比较,因为结果可能并非具体的真实值。即使有具体的值,if one > one_hundred也会产生一个异常。你应该使用solver.is_true和solver.is_false,它们可以在不执行约束求解的情况下测试具体的truthyness/falsiness。

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

你可以将任何符号化boolean作为一个关于符号值的assertion,可以将它作为一个约束条件添加到state中。之后,你可通过评估符号表达式来查询符号变量的有效值。

一个更清晰的例子如下

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

通过添加这些约束到state中,我们强制要求约束求解器将它们作为assertions,任何返回值都必须满足这些assertions。如果你执行这段代码,你可能会得到不同的x值,但这个x值一定是大于3,小于10的。如果你用state.solver.eval(y),你就会得到一个与x对应的y值。若你不在这两个查询中添加任何约束,则结果会彼此一致。

从这里开始,很容易就看出如何完成开头的任务,即找到给定结果的输入值。

# 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

请注意,同样的,这个解决方法只使用与bitvector的语义。若我们在整数域上进行这个操作,那就没有解决方法。

如果我们添加矛盾的约束,比如没有满足约束条件的值,state就会变为不可满足状态,或者unsat,这时查询操作就会出现异常,你可以用state.satisfiable()来检查state的可满足性。

>>> 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是一种通用的方法,它可以将任意bitvector转换为python原语,并维持state的完整性,这就说明我们可以用eval完成bitvector与python int的转换。

需要注意的是,变量不依赖于任何state,所以即使x和y是在旧的state时创建的,但在新的state中也可以使用它们。

 

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

 所以这里有一点需要解释,想要漂亮的打印浮点数并非一个好的做法。而除此之外,大多数操作都有3个参数,在使用二元运算符时会隐式添加舍入模式。IEEE754支持多种舍入模式(舍入到最近,舍入到零,舍入到正等),因此z3必须支持它们。如果你想某种操作指定舍入模式,请显示使用fp操作,并将舍入模式作为第一个参数。

约束和求解都是相同的方式,只是最后eval返回浮点数。

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

这很好,但有时候我们需要直接使用浮点数作为bitvector,你可以使用raw_to_bv和raw_to_fp将浮点数解释为bitvector,反之亦然

>>> 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类型一样。然而,如果要尽可能保持值,就像float转换为int,可以使用另一组方法val_to_fp和val_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)>

这些方法也可以以signed为参数,指定源的符号类型和目标bitvector


program state

到目前为止,我们只用了angr模拟程序状态(SimState对象)中最基本的部分来演示一些关于angr操作的基本概念。在这里,你可以看到state对象的结构以及如何有效的与之交互。

 

Review:Reading and writing memory and register

如果你看了本书的前面部分,你应该知道基本的访问寄存器和内存的方法。state.regs提供了寄存器的读写访问,通过作为属性的寄存器名指定要修改的寄存器。state.mem提供了内存的读写访问,通过索引访问符号来指定地址,后跟访问属性,来指定内存解释的类型。

此外,你应该知道如何使用ASTs,因此你现在可以理解bitvector类型的AST都可以存放在寄存器或内存中。

>>> import angr, claripy
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()
# copy rsp to rbp
>>> state.regs.rbp = state.regs.rsp
# store rdx to memory at 0x1000
>>> state.mem[0x1000].uint64_t = state.regs.rdx
# dereference rbp
>>> state.regs.rbp = state.mem[state.regs.rbp].uint64_t.resolved
# add rax, qword ptr [rsp + 8]
>>> state.regs.rax += state.mem[state.regs.rsp + 8].uint64_t.resolved

 

Basic Execution

之前我们演示了如何使用Simulation Manager来完成一些基本的执行。在之后的一章中,我们可以看到simulation manager的所有能力,但现在,我们只使用一些非常简单的接口来演示符号执行是如何工作的。state.step()这个方法会执行一步符号执行的操作然后返回一个名为SimSuccessors的对象。不像普通的模拟器,符号执行可以产生一些可以用多种方式分类的后继状态(successor state)。现在我们关心的是该对象的.successors属性,该属性是包含了给定step后的所有"normal" successors列表。

为什么是一个列表而非单个的successor呢?angr的符号执行就是过程就是将编译的单个指令操作取出,并且执行它们来改变SimState。当发现如同if (x > 4)的一行代码时,如果x是一个符号化bitvector会发生什么呢?在angr的某个深处,x > 4这个比较会被执行,而它的结果会是<Bool x_32_1 > 4>。

这很好,但接下来的一个问题是,我们是执行true的分支还是false的分支呢?答案是所有。我们产生两个完全独立的successor,一个模拟条件为true的情况,一个模拟条件为false的情况。在第一种情况,我们添加一个x > 4作为一个约束,而在第二种情况,我们添加一个!(x > 4)作为约束。这样,每当我们使用这些后继状态进行约束求解时,条件状态会确保我们得到的任何方案都睡有效的输入,这也会导致相同状态下一定会执行相同路径。

为了演示我们所说的,让我们使用一个fake firmware image作为例子。如果你去查看这个二进制文件的源码,你可以看到这个firmware的认证机制是一个后门。任何用户使用"SOSNFAKY"的密码来认证都将以管理员的身份登录。此外,对用户的输入进行的第一次比较是与后门的比较,所以,如果我们通过单步执行来获得多个后继状态,而其中一个状态将会包含约束用户输入为后门密码的条件,以下代码实现了这一点

>>> 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()
... if len(succ.successors) == 2:
... break
... state = succ.successors[0]
>>> state1, state2 = succ.successors
>>> state1
<SimState @ 0x400629>
>>> state2
<SimState @ 0x400699>

不要直接去看这些state的约束,这里经历的分支与strcmp的结果相关,strcmp是一个很难用符号来模拟的函数,而且最终的约束结果十分复杂。

我们模拟的程序从标准输入流获得数据,默认情况下,angr将其作为无限的符号数据流。为了进行约束求解以及获取输入可能的取值来满足约束。我们需要获得stdin实际内容的引用。我们将在后面讨论我们的文件和输入系统如何工作,但现在,我们直接使用state.posix.stdin.load(0, state.posix.stdin.size)来从stdin中读取到目前为止用bitvector表示的所有内容。

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

就像你看到的那样,为了沿着state1路径走下去,你必须指定一个密码,即这个后门字符串"SOSNEAKY"。为了沿着state2路径走下去,你必须指定"SOSNEAKY"意外的字符串。z3有助于提供符合这个标准的数十亿字符串。

 

State Presets

到目前为止,每当我们要使用一个state时,我们都会用project.factory.entry_state()来创建它。这只是project.factory构造器的其中一种

  • .blank_state()构造一个称为"blank state"的空白状态,该state中中大部分数据未初始化,当访问这些未初始化的值时,会返回一个符号值。
  • .entry_state()构造一个在二进制文件入口点准备执行的state
  • .full_init_state()构造一个state,这个state通过在二进制文件入口点之前的任意一个初始化器准备好执行。举例来说就是共享库构造器和预初始化构造器,在它们工作完成后,跳转到入口点。
  • .call_state()构造一个状态来执行指定的函数

你可通过一些参数来自定义这些构造器:

  • 所有构造函数都可以用addr参数来指定启动的地址
  • 如果你在有命令行参数或环境变量的环境中执行,你可以传递一个列表作为args参数,以及一个字典作为env参数到entry_state()和full_init_state()函数。这些结构的值可以是字符串或者bitvector,然后经过序列化作为参数和环境到模拟执行中。默认的args是一个空的列表,所以如果你分析的程序期望至少找到一个argv[0],你就必须提供它。
  • 如果你想将argc作为符号化的,你可以传递一个符号化的bitvector作为argc传递到entry_state和full_init_state构造器中。需要当心的是,如果你这样做了,你必须添加一个约束到结果的state中,即你的argc不能大于argv的列表长度。
  • 为了使用call state,你可以用.call_state(addr, arg1, arg2, ···),addr是你想调用的函数地址,argN表示这个函数的第N个参数,它可以是python的整数,字符串,数组或bitvector。如果你想分配内存并传入指向实际对象的指针,你应该将它包含在PointerWrapper中,就像这样angr.PointerWrapper("point to me!")。这个API返回的结果有点不可预测,但我们正在努力。
  • 要在call_state中指定函数的调用约定,可以传递Simcc实例作为cc参数

 

Low Level interface for memory

state.mem接口可以很方便的用来从内存加载数据,但当你要在内存范围内进行原始加载和存储时,会变得非常麻烦。事实证明,state.mem实际上只是一堆用于正确访问底层内存存储的的逻辑,这个内存存储空间是由bitvector数据填充的平坦地址空间,即state.memory。你可以使用.load(addr, size)和.store(addr, val)方法来直接访问state.memory:

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

就像你看到的那样,数据以"big-endian"的方式加载和存储,因为state.memory的基本目的是加载和存储没有附加语义的大量数据。然而,如果你想对加载或存储的数据进行字节交换,你可以传递endness关键字作为参数,如果指定little-endian,就会发生字节交换。endness是archinfo package中Endness枚举的成员之一,它保存有关angr的CPU结构体系的声明性数据。此外,正在分析的程序的endness可以用arch.memory_endness找到,它是一个state.arch.memory_endness实例。

>>> import archinfo
>>> s.memory.load(0x4000, 4, endness=archinfo.Endness.LE)
<BV32 0x67453201>

也有关于寄存器访问的low level interface,即state.registers,它使用API的方式与state.memory完全相同,但要解释其行为得深入了解angr用于多体系结构无缝协作的抽象过程。一种简单的方法是将它作为一个寄存器文件,也就是archinfo中定义的寄存器与偏移量之间的映射。

 

state option

可以对angr内部进行一些细微的调整,这些调整在某些情况下会优化一些地方的行为而会危害一些其他情况。这些调整是通过state option来控制的。

对每一个SimState对象,所有已启用选项都有一个state.options集合(set)。每一个option(实际上是一个字符串)以某种细小的方式控制这angr执行引擎的行为。可以在附录中找到option的完整域列表以及不同状态类型的默认值。你可以通过angr.opions访问单个option来添加到一个state中。单个option被称为CAPITAL_LETTERS,但是也有你可能想绑定使用的常见对象分组,名为lowercase_letters。

当通过任意构造器创建一个SimState时,你可以传递关键字add_options和remove_options作为参数,它们是可以用于修改默认的初始options集合。

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

 

State Plugins

除了刚才讨论的一组options,所有存放在SimState的东西本质上都是存放在一个附加到state的plugin中。几乎每一个我们目前讨论的state的属性都是一个plugin,如memory,registers,mem,regs,solver等。这种设计允许代码模块化,以及为一个模拟state其他方面实现新类型数据存储的能力,或提供plugin的替代实现的能力。

举个例子,普通的memory plugin模拟一个平坦内存空间,但分析时可以选择启动"抽象内存"plugin,这个plugin使用备用数据类型的地址来模拟自由浮动的内存映射,来提供state.memory。相反的,plugins可以减少代码的复杂程度,state.memory和state.registers实际上是相同plugin的两个不同实例,因为寄存器也是用地址空间来模拟的。

 

The globals plugin

state.globals是一个非常简单的plugin:它是用标准python dict实现的一个接口,允许你存储任意类型的数据到state中。

 

The history plugin

state.history是是一个非常重要的plugin,它用于存储相关state在执行过程中采用的路径的历史数据。它实际上是一些历史结点组成的链表,每一个结点表示一轮执行,你可以用state.history.parent.parent来遍历这个列表。

为了更方便的使用这个结构,history也提供了一些有效值的迭代器。通常,这些值存储为history.recent_NAME而迭代器则是history.NAME。举个例子

for addr in state.history.recent_bbl_addrs:
    print hex(addr)

会打印出一个基本块地址的trace,而state.sitory.recent_bbl_addrs是最近一步执行的基本块列表。state.history.parent.recent_bbl_addrs是上一步执行的基本块列表。如果你需要快速获取这些值的平坦列表,你可以访问.hardcopy,例如state.history.bbl_addrs.hardcopy。但要记住,基于索引的访问是在交互器的基础上实现的。

这有一些存放在history中的值的简要列表:

  • history.descriptions是对每轮state执行的字符串描述列表
  • history.bbl_addrs是state执行的基本块地址列表。每轮执行可能不止一个,且并非所有地址都有对应的二进制代码,也有可能是SimProcedures被hook的地址
  • history.jumpkinds是state在history中每一个控制流转换的处理列表,作为VEX枚举字符串
  • history.guards是state碰到的每一个分支条件列表
  • history.events是在执行过程中有趣事件的语义列表,比如存在符号化的jump条件,程序弹出一个消息框,或者伴随这exit代码的执行终止
  • history.actions通常为空,但如果你添加angr.options.refs选项到state中,它将弹出一个程序执行中的内存,寄存器,临时数据访问的日志。

 

The callstack plugin

angr会跟模拟程序的调用栈。对每一个call指令,都会添加一个帧到被跟踪的callstack栈顶,每当栈指针下降到调用栈最顶层帧之下时,这个帧被pop了。这允许angr稳健地存储当前模拟函数的局部数据。

和history类似,callstack也是由结点组成的链表,但它不提供迭代器来遍历所有结点内容,而是可以直接使用state.callstack来遍历每一个活动的callstack帧,按照最新到最旧的顺序,若你只想要最顶层的帧,那就是state.callstack

  • callstack.func_addr是当前正执行函数的地址
  • callstack.call_site_addr是调用当前函数的基本块地址
  • callstack.stack_ptr是当前函数开头的栈指针值
  • callstack.ret_addr是当前函数返回的地址

Simulation Managers 

在angr中最重要的控制接口是SimulationManager,他允许你同时控制state组的符号执行,并应用搜索策略来探索程序state空间。在这里,你将了解如何使用它。

Simulation managers能让你轻松处理多个states。states被组织成"stashes",你可以按自己希望的方式进行单步向前,过滤,合并,移动。例如,这允许你以不同的速率执行两个不同的stashes of states,然后将它们合并到一起。绝大多数操作的默认stash是active stash,这是初始化一个新的simulation manager时的state。

 

Stepping

一个simulation manager最基本的能力是,通过一个基本块将给定stash中的所有state向前执行。你可以用.step()做到这一点。

>>> import angr
>>> proj = angr.Project('examples/fauxware/fauxware', auto_load_libs=False)
>>> state = proj.factory.entry_state()
>>> simgr = proj.factory.simgr(state)
>>> simgr.active
[<SimState @ 0x400580>]
>>> simgr.step()
>>> simgr.active
[<SimState @ 0x400540>]

当然,stash模式的真正强大是当一个state碰上一个符号分支条件时,两边的后继state都会出现在stash中,且你可以同步它们。如果你不是非常关心控制分析并且只是想执行到没有步骤可以执行,你可以就使用.run()方法

# Step until the first symbolic branch
>>> while len(simgr.active) == 1:
... simgr.step()
>>> simgr
<SimulationManager with 2 active>
>>> simgr.active
[<SimState @ 0x400692>, <SimState @ 0x400699>]
# Step until everything terminates
>>> simgr.run()
>>> simgr
<SimulationManager with 3 deadended>

 我们现在有3个deadended states!当一个state在执行期间未能产生任何successor,比如,由于抵达了一个exit系统调用,他就会从active stash中被去除,并放置到deadended stash中。

 

Stash Management

让我们来看如何使用其他stashes。

为了移动stashes之间的states,可以使用.move(),它有from_stash, to_stash和filter_func(可选参数,默认是移动所有) 3个参数。举个例子,让我们移动输出中具有特定字符串的所有内容:

>>> simgr.move(from_stash='deadended', to_stash='authenticated', filter_func=lambda s: b'Welcome' in s.posix.dumps(1))
>>> simgr
<SimulationManager with 2 authenticated, 1 deadended>

我们可以创建一个新的stash名为"authenticated"用来将states移动到这里。所有在这个stash中的states的标准输出流中都有"Welcome"字符串,这是目前一个较好的指标。

每一个stash都是一个列表,你可以用索引来遍历这个列表来访问单个state,但也有也写内部方法来访问state。如果一个stash的名字添上one_作为前缀,你将获得stash中的第一个state。如果一个stash的名字添上mp_作为前缀,你将获得一个mulpyplexed版本的stash。

>>> for s in simgr.deadended + simgr.authenticated:
... print(hex(s.addr))
0x1000030
0x1000078
0x1000078
>>> simgr.one_deadended
<SimState @ 0x1000030>
>>> simgr.mp_authenticated
MP([<SimState @ 0x1000078>, <SimState @ 0x1000078>])
>>> simgr.mp_authenticated.posix.dumps(0)
MP(['\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00',
'\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x80\x80\x80\x80@\x80@\x00'])

当然,step,run以及在单个路径stash上运行的任何其他方法都可以使用stash参数,来指定要对哪个stash进行操作。

simulation manager提供了许多有趣的工具用于管理你的stashes,我们暂时不讨论其他内容,你可自行查看API文档。

 

Stash types

你可以以你喜欢的方式使用stash,但有一些stash将用于为一些特殊state进行分类:

  • active,除非指定了备用的stash,否则该stash包含默认被步进的state
  • deadended,当一个state由于某些原因无法继续执行时,它就会到deadended stash中,这些原因包括没有继续执行的有效指令,所有successor都为unsat state,或者出现一个无效的指令指针
  • pruned,当正使用LAZY_SOLVES时,除非十分必要,否则不会检测state的可满足性。When a state is found to be unsat in the presence of LAZY_SOLVES, the state hierarchy is traversed to identify when, in its history, it initially became unsat. All states that are descendants of that point (which will also be unsat, since a state cannot become un-unsat) are pruned and put in this stash.
  • unconstrained,如果save_unconstrained选项被提供给SimulationManager构造器,则被确定为不受约束的state将放在这里
  • unsat,如果save_unsat选项被提供给SimulationManager构造器,则被确定为不可满足的state将放在这里

 

Simple Exploration

在符号执行中一个十分常见的操作是找到到达某地址时的state,同时丢弃到达另一地址的所有state。Simulation manager具有这种匹配的快捷方式,也就是.explore()方法

当启动.explore()并使用find参数时,执行会一直进行下去,直至找到与查找条件匹配的state,它可以是停止的指令的地址,要停止的地址列表,或者使用这个state的函数,并返回该state是否满足某些东西。当active stash中的任何state匹配了find参数中的条件,它们就会被放到found stash中,然后终止执行。你接下来就可以浏览found state,或者决定丢弃它并继续使用其他state。你也可以指定一个avoid条件,它的格式与find相同。当一个state匹配avoid的条件时,就会将其放入avoided stash中,然后继续执行。最后,num_find参数控制返回前应找到的state数量,默认为1。当然,如果你在找到这么多解决反感之前用光了active stash,执行也会停止。

我们来看一个简单的cm例子:

首先加载二进制文件

>>> proj = angr.Project('examples/CSCI-4968-MBE/challenges/crackme0x00a/crackme0x00a')

然后我们创建一个SImulationManager

>>> simgr = proj.factory.simgr()

现在我们符号执行直到匹配我们指定的条件

>>> simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))
<SimulationManager with 1 active, 1 found>

然后我们可以得到最后的flag了

>>> s = simgr.found[0]
>>> print(s.posix.dumps(1))
Enter password: Congrats!
>>> flag = s.posix.dumps(0)
>>> print(flag)
g00dJ0B!

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值