5.3.1 angr
简介
angr 是一个多架构的二进制分析平台,具备对二进制文件的动态符号执行能力和多种静态分析能力。在近几年的 CTF 中也大有用途。
安装
在 Ubuntu 上,首先我们应该安装所有的编译所需要的依赖环境:
强烈建议在虚拟环境中安装 angr,因为有几个 angr 的依赖(比如z3)是从他们的原始库中 fork 而来,如果你已经安装了 z3,那么肯定不希望 angr 的依赖覆盖掉官方的共享库,开一个隔离的环境就好了:
如果这样安装失败的话,那么你可以按照下面的顺序从 angr 的官方仓库安装:
例如下面这样:
安装过程中可能会有一些奇怪的错误,可以到官方文档中查看。
另外 angr 还有一个 GUI 可以用,查看 angr Management。
使用方法
快速入门
使用 angr 的第一步是新建一个工程,几乎所有的操作都是围绕这个工程展开的:
这样就得到了二进制文件的各种信息,如:
程序加载时会将二进制文件和共享库映射到虚拟地址中,CLE 模块就是用来处理这些东西的。
所有对象文件如下,其中二进制文件本身是 main_object,然后还可以查看对象文件的相关信息:
通常我们在创建工程时选择关闭 auto_load_libs 以避免 angr 加载共享库:
project.factory 提供了很多类对二进制文件进行分析,它提供了几个方便的构造函数。
project.factory.block() 用于从给定地址解析一个 basic block,对象类型为 Block:
另外,还可以将 Block 对象转换成其他形式:
程序的执行需要初始化一个模拟程序状态的 SimState 对象:
该对象包含了程序的内存、寄存器、文件系统数据等等模拟运行时动态变化的数据,例如:
这里的 BV,即 bitvectors,可以理解为一个比特串,用于在 angr 里表示 CPU 数据。看到在这里 rdi 有点特殊,它没有具体的数值,而是在符号执行中所使用的符号变量,我们会在稍后再做讲解。
下面是 Python int 和 bitvectors 之间的转换:
于是 bitvectors 可以进行数学运算:
使用 bitvectors 可以直接来设置寄存器和内存的值,当传入的是 Python int 时,angr 会自动将其转换成 bitvectors:
初始化的 state 可以经过模拟执行得到一系列的 states,模拟管理器(Simulation Managers)的作用就是对这些 states 进行管理:
angr 提供了大量函数用于程序分析,在这些函数在 Project.analyses.,例如:
如果要想画出图来,还需要安装 matplotlib。
二进制文件加载器
我们知道 angr 是高度模块化的,接下来我们就分别来看看这些组成模块,其中用于二进制加载模块称为 CLE。主类为 cle.loader.Loader,它导入所有的对象文件并导出一个进程内存的抽象。类 cle.backends 是加载器的后端,根据二进制文件类型区分为 cle.backends.elf、cle.backends.pe、cle.backends.macho 等。
首先我们来看加载器的一些常用参数:
auto_load_libs:是否自动加载主对象文件所依赖的共享库except_missing_libs:当有共享库没有找到时抛出异常force_load_libs:强制加载列表指定的共享库,不论其是否被依赖skip_libs:不加载列表指定的共享库,即使其被依赖custom_ld_path:可以到列表指定的路径查找共享库
如果希望对某个对象文件单独指定加载参数,可以使用 main_ops 和 lib_opts 以字典的形式指定参数。一些通用的参数如下:
backend:使用的加载器后端,如:"elf", "pe", "mach-o", "ida", "blob" 等custom_arch:使用的 archinfo.Arch 对象custom_base_addr:指定对象文件的基址custom_entry_point:指定对象文件的入口点
举个例子:
加载对象文件和细分类型如下:
proj.loader.main_object:主对象文件proj.loader.shared_objects:共享对象文件proj.loader.extern_object:外部对象文件proj.loader.all_elf_object:所有 elf 对象文件proj.loader.kernel_object:内核对象文件
通过对这些对象文件进行操作,可以解析出相关信息:
根据地址查找我们需要的东西:
通过 obj.relocs 可以查看所有的重定位符号信息,或者通过 obj.imports 可以得到一个符号信息的字典:
这一部分还有个 hooking 机制,用于将共享库中的代码替换为其他的操作。使用函数 proj.hook(addr, hook) 和 proj.hook_symbol(name, hook) 来做到这一点,其中 hook 是一个 SimProcedure 的实例。通过 .is_hooked、.unhook 和 .hooked_by 来进行管理:
当然也可以利用装饰器编写自己的 hook 函数:
求解器引擎
angr 是一个符号执行工具,它通过符号表达式来模拟程序的执行,将程序的输出表示成包含这些符号的逻辑或数学表达式,然后利用约束求解器进行求解。
从前面的内容中我们已经知道 bitvectors 是一个比特串,并且看到了 bitvectors 做的一些具体的数学运算。其实 bitvectors 不仅可以表示具体的数值,还可以表示虚拟的数值,即符号变量。
而符号变量之间的运算同样不会时具体的数值,而是一个 AST,所以我们接下来同样使用 bitvector 来指代 AST:
每个 AST 都有一个 .op 和一个 .args 属性:
知道了符号变量的表示,接下来看符号约束:
正因为布尔值是符号化的,所以在需要做 if 或者 while 判断的时候,不要直接使用比较作为条件,而应该使用 .is_true 和 .is_false 来进行判断:
为了进行符号求解,首先要将符号化布尔值作为符号变量有效值的断言加入到 state 中,作为限制条件,当然如果添加了无法满足的限制条件,将无法求解:
angr 使用 z3 作为约束求解器,而 z3 支持 IEEE754 浮点数的理论,所以我们也可以使用浮点数。使用 FPV 和 FPS 即可创建浮点数值和浮点符号:
bitvectors 和浮点数的转换使用 raw_to_bv 和 raw_to_fp:
或者如果我们需要指定宽度的 bitvectors,可以使用 val_to_bv 和 val_to_fp:
程序状态
state.step() 用于模拟执行的一个 basic block 并返回一个 SimSuccessors 类型的对象,由于符号执行可能产生多个 state,所以该对象的 .successors 属性是一个列表,包含了所有可能的 state。
程序状态 state 是一个 SimState 类型的对象,angr.factory.AngrObjectFactory 类提供了创建 state 对象的方法:
.blank_state():返回一个几乎没有初始化的 state 对象,当访问未初始化的数据时,将返回一个没有约束条件的符号值。.entry_state():从主对象文件的入口点创建一个 state。.full_init_state():与 entry_state() 类似,但执行不是从入口点开始,而是从一个特殊的 SimProcedure 开始,在执行到入口点之前调用必要的初始化函数。.call_state():创建一个准备执行给定函数的 state。
下面对这些方法的参数做一些说明:
所有方法都可以传入参数
addr来指定开始地址可以通过
args传入参数列表,env传入环境变量。类型可以是字符串,也可以是 bitvectors通过传入一个符号 bitvector 作为
argc,可以将argc符号化对于
.call_state(addr, arg1, arg2, ...),addr是希望调用的函数地址,argN是传递给函数的 N 个参数,如果希望分配一个内存空间并传递指针,则需要使用angr.PointerWrapper();如果需要指定调用约定,可以传递一个 SimCC 对象作为cc参数
创建的 state 可以很方便地复制和合并:
我们已经知道使用 state.mem 可以很方便的操作内存,但如果你想要对内存进行原始的操作时,可以使用 state.memory 的 .load(addr, size) 和 .store(addr, val):
可以看到默认情况下 store 和 load 都使用大端序的方式,但可以通过指定参数 endness 来使用小端序。
通过 state.options 可以对 angr 的行为做特定的优化。我们既可以在创建 state 时将 option 作为参数传递进去,也可以对已经存在的 state 进行修改。例如:
SimState 对象的所有内容(包括memory、registers、mem等)都是以插件的形式存储的,这样做的好处是将代码模块化,如果我们想要在 state 中存储其他的数据,那么直接实现一个插件就可以了。
state.globals:实现了一个标准的 Python dict 的接口,通过它可以在一个 state 上存储任意的数据。state.history:存储了一个 state 在执行过程中的路径历史数据,它是一个链表,每个节点表示一个执行,通过像history.parent.parent这样的方式进行遍历。为了得到 history 中某个具体的值,可以使用迭代器history.NAME,这样的值保存在history.recent_NAME。如果想要快速得到这些值的一个列表,可以查看.hardcopy。history.descriptions:对 state 每次执行的描述的列表。history.bbl_addrs:state 每次执行的 basic block 的地址的列表,每次执行可能多于一个地址,也可能是被 hook 的 SimProcedures 的地址。history.jumpkinds:state 每次执行时改变控制流的操作的列表。history.guards:state 执行中遇到的每个分支的条件的列表。history.events:state 执行中遇到的可能有用的事件的列表。history.actions:通常是空的,但如果启用了options.refs,则会记录程序执行时访问的所有内存、寄存器和临时变量。
state.callstack:用于记录函数调用堆栈,它是一个链表,可以直接遍历state.callstack获得每个调用的 frame。callstack.func_addr:当前正在执行的函数的地址。callstack.call_site_addr:调用当前函数的 basic block 的地址。callstack.stack_ptr:从当前函数开头开始计算的堆栈指针的值。callstack.ret_addr:当前函数的返回地址。
模拟管理器
模拟管理器(Simulation Managers)是 angr 最重要的控制接口,它允许同时对各组状态的符号执行进行控制,同时应用搜索策略来探索程序的状态空间。states 会被整理到 stashes 里,从而进行各种操作。
我们用一个小程序来作例子,它有 3 种可能性,也就是 3 条路径:
模拟管理器最基本的功能是将一个 stash 里所有的 states 向前推进一个 basic block,利用 .step() 来实现,而 .run() 方法可以直接执行到程序结束:
于是我们得到了 3 个 deadended 状态的 state。这一状态表示一个 state 一直执行到没有后继者了,那么就将它从 active stash 中移除,放到 deadended stash 中。
stash 默认的类型有下面几种,当然你也可以定义自己的 stash:
active:默认情况下存储可以执行的 state。deadended:当 state 无法继续执行时会被放到这里,包括没有更多的有效指令,没有可满足的后继状态,或者指令指针无效等。pruned:当启用LAZY_SOLVES时,除非绝对必要,否则是不会在执行中检查 state 的可满足性的。当某个 state 被发现是不可满足的,则 state 会被回溯上去,以确定最早是哪个 state 不可满足。然后这之后所有的 state 都会被放到prunedstash 中。unconstrained:如果在 SimulationManager 创建时启用了save_unconstrained,则那些没有约束条件的 state 会被放到unconstrainedstash 中。unsat:如果在 SimulationManager 创建时启用了save_unsat,则那些被认为不可满足的 state 会被放到unsatstash 中。
另外还有一个叫做 errored 的列表,它不是一个 stash。如果 state 在执行过程中发生错误,则该 state 会被包装在一个 ErrorRecord 对象中,该对象包含 state 和引发的错误,然后这个对象被插入到 errored 中。
可以使用 .move(),将 filter_func 筛选出来的 state 从 from_stash 移动到 to_stash:
每个 stash 都是一个列表,可以用列表的操作来遍历它,同时 angr 也提供了一些高级的方法,例如在 stash 名称前面加上 one_,表示该 stash 的第一个 state;在名称前加上 mp_,将得到一个 mulpyplexed 版本的 stash:
最后再介绍一下模拟管理器所使用的探索技术(exploration techniques)。默认策略是广度优先搜索,但根据目标程序或者需要达到的目的不同,我们可能需要使用不同的探索技术,通过调用 simgr.use_technique(tech) 来实现,其中 tech 是一个 ExplorationTechnique 子类的实例。angr 内置的探索技术在 angr.exploration_techniques 下:
Explorer:该技术实现了.explore()功能,允许在探索时查找或避免某些地址。DFS:深度优先搜索,每次只探索一条路径,其它路径会放到deferredstash 中。直到当前路径探索结束,再从deferred中取出最长的一条继续探索。LoopLimiter:限制路径的循环次数,超出限制的路径将被放到discardstash 中。LengthLimiter:限制路径的最大长度ManualMergepoint:将程序中的某个地址标记为合并点,将在一定时间范围内到达的所有 state 合并在一起。Veritesting:是这篇论文的实现,试图识别出有用的合并点来解决路径爆炸问题。在创建 SimulationManager 时通过veritesting=True来开启。Tracer:记录在某个具体输入下的执行路径,结果是执行完最后一个 basic block 的 state,存放在tracedstash 中。Oppologist:当遇到某个不支持的指令时,它将具体化该指令的所有输入并使用 unicorn engine 继续执行。Threading:将线程级并行添加到探索过程中。Spiller:当处于 active 的 state 过多时,将其中一些转存到磁盘上以保持较低的内存消耗。
VEX IR 翻译器
angr 使用了 VEX 作为二进制分析的中间表示。VEX IR 是由 Valgrind 项目开发和使用的中间表示,后来这一部分被分离出去作为 libVEX,libVEX 用于将机器码转换成 VEX IR(更多内容参考章节5.2.3)。在 angr 项目中,开发了模块 PyVEX 作为 libVEX 的 Python 包装。当然也对 libVEX 做了一些修改,使其更加适用于程序分析。
一些用法如下:
到这里 angr 的核心概念就介绍得差不多了,更多更详细的内容还是推荐查看官方教程和 API 文档。另外在我的博客里有 angr 源码分析的笔记。
扩展工具
由于 angr 强大的静态分析和符号执行能力,我们可以在 angr 之上开发其他的一些工:
CTF 实例
查看章节 6.2.3、6.2.8。
参考资料
Last updated