angr原理与实践(一)——原理
1本文系原创,转载请说明出处
关注微信公众号 信安科研人,获取更多的原创安全资讯
网上已经有很多介绍angr的官方文档的博客,但是怎么去用angr做一次有意义且成就感满满的分析的教程很少,目前比较常见的就是CTF中的应用,很少有从二进制程序分析技术角度介绍。
同时,也没有几篇文章介绍angr对应的论文,这篇论文很棒很经典。
因此,本博客从理论与实践两个方面介绍angr,以求让让自己更熟练掌握这个库是怎么用的,并应用到自己的工作中。
目录
一 angr简介
angr是加州大学圣巴巴拉分校Giovanni Vigna大佬领衔的安全实验室的杰作,对应的论文发表在2016年的网络安全顶会IEEE S&P上,原论文链接
二 研究背景
在这篇文章出来之前,安全界对程序进行漏洞分析的技术存在以下两种问题:
(1)研究的重复工作太多。每一个安全分析研究工作都要重新实现并调用一些以前的技术,浪费时间。
(2)其次,由于复制这些系统所需的工作量不可接受,复制其他人的结果变得不切实际。结果,单个二进制分析技术相对于其他技术的适用性变得模糊不清。再加上现代操作系统固有的复杂性,很难建立一个共同的比较基础。
批注:可以看出来,发表在CCF A上的文章所解决的问题往往是能够推动整个领域前进的问题,或者是解决一个领域内的巨大阻碍。同时,可以看到,真正的科研,往往是问题驱动,就是说解决一个问题而去做科研,往往是能够做出比为了科研而去解决问题更多的成功。
因此,angr的实现的意义来了:
大佬们创建了angr以解决这些技术共用性的问题,集成了众多文献中最先进的二进制分析技术。这样做的目的是通过以一种可访问、开放和可用的方式实施当前研究工作中的有效技术,使该领域系统化,并鼓励开发下一代二进制分析技术,以便能够轻松地相互比较。
angr 使用静态和动态分析技术为多种类型的分析提供构建分析块,以便可以轻松实现所提出的研究方法并比较这些方法之间的有效性。此外,这些构建的分析块的组合能够利用它们的不同的组合优势。
angr做出的三个贡献:
1) 在一个单一、连贯的框架中再现了攻击性二进制分析中的许多现有方法,以了解当前攻击性二进制分析技术的相对有效性。
2) 展示了将各种二进制分析技术结合起来并大规模应用的困难(以及解决这些困难的方法)。
3) 将angr开源,以供后代研究二进制代码分析之用。
三 背景知识
3.1 漏洞挖掘的静态分析技术
静态技术在不执行程序的情况下对程序进行推理。通常,程序会被抽象表示。例如,诸如存储器布局或甚至所采用的执行路径之类的程序构造也可以被抽象表示。
这篇文章将静态分析分为两种样式:
(1)将程序属性建模成图
(2)对程序数据本身进行建模的范式
静态分析的两个缺点:
(1)首先,结果不可重复:静态分析的检测必须手动验证,因为无法恢复有关如何触发检测到的漏洞的信息。
(2)其次,这些分析往往在更简单的数据域上操作,降低了对语义的洞察。简言之,分析过于近似:虽然通常可以权威性地推理某些程序属性(例如漏洞)的缺失,但在就漏洞是否存在而证明时,静态分析的误报率很高。
技术一 CFG图的恢复
控制流图 (CFG) 的恢复是几乎所有用于漏洞发现的静态技术的先决条件,其中图节点是基本的指令块,边缘是指令块之间可能的控制流传输。
主流的CFG图的生成方法为递归算法,例如,从基本块BA开始,分解并分析BA基本快,识别可能的出口点BC、BD,那么就将BC、BD接到BA上,然后从BC、BD递归地重复分析,直到识别不出新的出口点。
CFG控制流恢复有一个到目前为止都存在的挑战点:指令的间接跳转。 与直接跳转不同,在直接跳转中,目标被编码到指令本身中,因此很容易解析,而间接跳转的目标可以基于许多因素而变化,如链接库。
具体来说,间接跳转分为几类:
- 计算跳转。如跳转表,跳转表中的索引地址需要依据当时运行的寄存器或者内存的值来确定。
- 上下文敏感。间接跳转可能取决于应用程序的上下文。常见的例子是标准C库中的qsort()——该函数接受一个回调,用于比较传入的值。因此,qsort()中基本块的一些跳转目标取决于其调用方,因为调用方提供回调函数。
- 对象敏感(object-sensitive)。上下文敏感的一种特殊情况是对象敏感。在面向对象的语言中,对象多态性需要使用虚函数,通常实现为在运行时查询的函数指针的虚表,以确定跳转目标。因此,跳转目标取决于其调用方传递给函数的对象的类型。
angr对如上的CFG图恢复工作的难点一个个攻克,并定义了CFG恢复技术的两个属性:
(1)可靠性。如果在生成的图中表示所有潜在控制流传输的集合,则认定这个CFG图恢复技术是合理的。
(2)完整性。指CFG图恢复成一个其中所有边都表示实际可能的控制流传输的CFG图。
技术二 基于流模型的漏洞检测
通过分析程序属性图可以发现程序中的一些漏洞。程序属性图(例如,控制流图、数据流图和控制依赖图)可用于识别软件中的漏洞。最初应用于源代码的相关技术
技术三 基于数据建模的漏洞检测
一种代表方法:Ø值集分析(Value-Set Analysis, VSA)。VSA尝试识别程序中任何给定点的程序状态(即内存和寄存器中的值),这可以用来解决间接跳转的可能目标,或内存写入操作的可能目标识别问题。虽然这些近似值缺乏准确性,但它们是比较可靠的。也就是说,它们可能过近似,但决不可能能欠近似。
例如:使用值集分析,通过分析内存读写的近似访问模式,可以在二进制文件中识别变量和缓冲区的位置。完成后,可以分析恢复的变量和缓冲区位置,以找到重叠的缓冲区。例如,这种重叠缓冲区可能是由缓冲区溢出漏洞引起的,因此每次检测都是一个潜在漏洞。
3.2 漏洞挖掘的动态分析技术
该论文将动态分析技术分为两大类:
具体执行和(动态)符号执行。
具体执行:
具体执行的代表方案就是Fuzzing,也就是我们常说的模糊测试。这里不多介绍其概念,这里介绍几种现有的fuzzing的种类:
(1)基于覆盖率的fuzzing。基于代码覆盖率的模糊测试技术尝试产生的输入用例,使目标应用程序中执行的代码量最大化,因为执行的代码越多,执行易受攻击代码的可能性越高。经典的工具有AFL。
基于覆盖的Fuzzing缺乏对目标应用程序的语义洞察。这意味着,虽然它能够检测到某段代码尚未执行,但它无法理解输入的哪些部分发生变异以导致代码被执行。
(2)基于污点分析的Fuzzing。这样的模糊测试工具分析应用程序如何处理输入,以了解输入的哪些部分在未来运行时需要修改。
(动态)符号执行:
符号技术弥合了静态和动态分析之间的差距,并提供了一种解决方案来应对模糊测试的有限语义洞察力。动态符号执行是符号执行的一个子集,是一种动态技术,因为它在模拟环境中执行程序。
然而,这种执行发生在符号变量的抽象域中。当这些系统模拟应用程序时,它们在整个程序执行过程中跟踪寄存器和内存的状态以及对这些变量的约束。每当到达条件分支时,执行分支并遵循两条路径,将分支条件保存为对采用分支的路径的约束,并将分支条件的逆作为对未采用分支的路径的约束。
四 angr的设计
4.1 设计目标
- 跨架构
- 跨平台
- 支持不同的分析范式
- 可用性
4.2 子模块设计
- 中间语言表示。angr借鉴的是libVEX,目前支持ARM、MIPS、PPC和x86和amd64,libVEX项目的研究者目前正在对SPARC架构进行工作移植。
- 二进制文件加载。angr的这个模块主要由CLE实行,CLE 是 CLE Loads Everything 的递归首字母缩写词。 CLE 对不同的二进制格式进行抽象,以处理加载给定的二进制文件和它所依赖的任何库、解析动态符号、执行重定位以及正确初始化程序状态。CLE 通过提供众多表示二进制对象(即应用程序二进制文件、POSIX .so 或 Windows .dll)的基类、这些对象中的段和节以及表示内部位置的符号,为二进制加载程序提供了一个可扩展的接口。CLE 使用文件格式解析库(特别是用于 Linux 二进制文件的 elftools 和用于 Windows 二进制文件的 pefile)来解析对象本身,然后执行必要的重定位以公开已加载应用程序的内存映像。
- 程序状态表示与修改。angr这部分使用的是SimuVEX模块。SimuVEX 提供了通过 VEX 表示的代码块处理输入状态的能力,并生成输出状态。SimuVEX 模块负责表示程序状态(即寄存器和内存中值的快照、打开的文件等)。 在 SimuVEX 术语中名为 SimState 的状态被实现为状态插件的集合,这些插件由用户指定的状态选项控制或在状态创建时进行分析。 目前,存在以下状态插件:
- 寄存器
- 符号内存(以帮助符号执行)
- 抽象内存
- POSIX
- 日志记录
- 调试
- 求解器
- 架构(用来分析架构的特定信息)
- 数据模型。存储在SimState的寄存器和存储器中的值由另一个模块Claripy提供的抽象表示。这种模块化设计允许 Claripy 以强大的方式组合各种数据域提供的功能,并将其提供给 angr 的其余部分使用。Claripy 将所有值抽象为表达式的内部表示,该表达式跟踪使用它的所有操作。具体来说,Claripy 提供支持具体域(整数和浮点数)、符号域(由 Z3 SMT 求解器 提供的符号整数和符号浮点数)和值集抽象域的后端用于值集分析。例如,将后端提供的构造(例如,Z3 后端提供的符号表达式 x+1)解释为 Python 原语(例如作为约束求解结果的 x+1 的可能整数解)由前端提供。 前端增加了具有不同复杂性的附加功能的后端。Claripy 目前提供了几个前端:
- 全前端(FullFrontend)。 此前端向用户公开符号求解、跟踪约束、使用 Z3 后端求解并缓存结果。
- 复合前端(CompositeFrontend)。 正如 KLEE 和 Mayhem等人工作中所建议的,将约束拆分为独立的集合可以减少求解器的负载。
- 轻量级前端(LightFrontend)。此前端不支持约束跟踪,仅使用 VSA (值集分析)后端来解释 VSA 域中的表达式。
- 替换前端(ReplacementFrontend)。 ReplacementFrontend 扩展 LightFrontend 以添加对 VSA 值约束的支持。 当引入约束(即 x+1 < 10)时,ReplacementFrontend 会对其进行分析以识别所涉及变量的界限(即 0 <= x<=8)。 当随后向 ReplacementFrontend 咨询变量 x 的可能值时,它将与先前确定的范围相交,从而提供比 VSA 能够产生的更准确的结果。
- 混合前端(HybridFrontend)。 HybridFrontend 结合了 FullFrontend 和 Replacement-Frontend 为符号约束求解提供快速求解近似值的支持。
- 全程序分析。angr 提供完整的分析,例如动态符号执行和控制流图恢复。这些分析的“入口点”是Project,它代表一个二进制文件及其相关库。从这个接口,可以访问其他子模块的所有功能(即,创建状态、检查共享对象、检索基本块的中间表示、使用 Python 函数挂钩二进制代码等)。此外,还有两个用于全程序分析的主要界面:路径组(Path Group)和分析(Analyses)。
- PathGroup 是动态符号执行的接口——它在路径通过应用程序、拆分或终止时跟踪路径。
- angr 通过 Analysis 类为任何完整的程序分析提供抽象表示。 此类管理静态分析的生命周期,例如控制流图恢复和下文中介绍的复杂动态分析。
五 angr的技术实现细节
5.1 CFG图恢复算法
给定一个特定的程序,angr执行一个从程序的切入点开始的迭代的CFG图恢复技术,并进行一些必要的优化。angr利用强制执行、向后切片和符号执行的组合,在可能的情况下,恢复每个间接跳转的所有跳转目标。此外,它还生成并存储了关于目标应用程序的大量数据,这些数据可以用于以后的其他分析,如数据依赖跟踪。
该算法有三个主要缺点:它速度慢,不能自动处理“死代码”,而且可能会错过只有通过未恢复的间接跳转才能到达的代码。
为了解决这个问题,angr创建了一个辅助算法,该算法使用二进制文件的快速反汇编技术(不执行任何基本块),然后使用启发式算法来识别函数、函数内控制流和直接的函数间控制流转换。然而,次要算法的准确性要低得多——它缺乏关于函数之间可达性的信息,对上下文不敏感,并且无法恢复复杂的间接跳跃。
在本节将讨论angr称为 CFGAccurate 的高级恢复算法,以及快速算法 CFGFast。
假设:
angr首先对被测试的二进制文件做了几个假设,以优化算法的运行时间:
- 程序中的所有代码都可以分布到不同的函数中。
- 在控制流中,所有函数要么由
- 每个函数的堆栈清理行为都是可预测的,无论它是从哪里调用的。这让 CFGAccurate 在分析调用函数时安全地跳过它已经分析过的函数并保持堆栈平衡。
做这些假设的目的是假设这些被测试的程序能够正常运行, 在分析混淆或异常的二进制文件时(如恶意代码),可以不考虑这些假设,但这会导致 CFG 恢复的运行时间更长。
迭代的CFG图生成:
具体来说,使用了四种技术:强制执行、轻量级反向切片、符号执行和值集分析。 要通过这些技术迭代恢复的 CFG,在应用程序的入口点使用基本块进行初始化。
在 CFG 恢复过程中,CFGAccurate 维护一个间接跳转列表 Lj,其跳转目标尚未确定。当分析识别出这样的跳转时,将被添加到 Lj中。
在每轮迭代技术结束后,CFGAccurate 触发列表中的下一个跳转。 下一轮的技术可能会解决 Lj 中的跳转问题,可能会向 Lj 添加新的未解决跳转,并且也可能会向 CFG中添加基本块和边。
当所有技术的运行使得 Lj 或 C 没有变化时,CFGAccurate 终止,因为这意味着任何可用的分析都无法解决进一步的间接跳转。
第一阶段 强制执行技术:
定义: 强制执行确保条件分支的两个方向都将在每个分支点执行,就是说,if(a>0)会有两个分支,这两个分支都会执行。angr中参考的强制执行技术的原论文
定义的参数:基本块的工作列表 Bw 和分析块的列表 Ba,CFG图 C。
分析开始时,使用 C 中但不在 Ba 中的所有基本块初始化其工作列表。每当 CFGAccurate 分析此工作列表中的基本块时,基本块和来自该块的任何直接跳转都会添加到 C 中。
但是,不能以这种方式处理间接跳转。在强制执行下,间接跳转的目标可能与程序实际运行的目标不同,因为强制执行会以意想不到的顺序执行代码。因此,每个间接跳转都存储在列表 Lj 中以供后续分析。
由于它无法解决任何间接跳转,因此该分析用作 快速处理的 CFG 恢复分析,以快速地为其他分析提供检测到的基本块和未解决的间接跳转。
第二阶段 符号执行:
对于每个跳转J∈ Lj,CFGAccurate向后(一般来说是从下往上)遍历CFG,直到找到第一个合并点(即,在通往间接跳转的路上“万路归一”的点)或达到阈值块数(根据研究经验,发现合理的阈值为8)。在此基础上,CFGAccurate对间接跳转执行正向符号执行,并使用约束求解器检索间接跳转目标的可能值。
CFGAccurate中规定:如果计算出的可能的目标集小于阈值大小,则跳转成功解决。angr规定这个计算阈值为256,但在实践中,在跳转未成功解决的情况下,该值是不受约束的(这意味着,可能的目标集仅受地址中的位数限制)。
如果跳转目标的问题解决,则从Lj中删除J,并为跳转目标的每个可能值将边和节点添加到CFG中。
第三阶段 后向切片:
上面两种方法是针对上下文不敏感的方式进行,如果参数为函数指针,并用该指针作为简介跳转的目标,则上面两种分析是无法解决的。
这意味着CFG的生成需要一个具备上下文敏感的组件。angr通过后向切片来实现这一点。切片扩展到上一个调用上下文的开头。也就是说,如果正在分析的间接跳转位于从Fb和Fc调用的函数Fa中,则切片将从Fa中的跳转向后延伸,并包含两个开始节点:Fb开始处的基本块和Fc开始处的基本块。
然后,CfgAccurate使用Angr的符号执行引擎执行该切片,并使用约束引擎来识别符号跳转的可能目标,对于跳转目标的解集的大小具有相同的阈值256。如果跳转目标被成功解析,则从Lj和表示控制流转换的边中移除跳转,并且将目标基本块添加到恢复的CFG中。
第四阶段 CFGFast
前面三阶段的CFG图恢复技术知识能够勉勉强强的识别二进制文件中函数的位置和内容,但是缺乏控制流的表示。
CFGFast旨在快速恢复具有高覆盖率的CFG,而不必考虑了解函数之间的可达性。分为三个步骤:
- 函数识别。使用硬编码的函数序言签名来识别应用程序内部的函数,该签名可以通过ByteWeight等技术生成。
- 递归反汇编。递归反汇编用于恢复已识别函数内的直接跳转。
- 间接跳转的解决。将轻量级别名分析、数据流跟踪与预定义策略相结合,解决函数内的控制流传输识别。目前,CFGFast包括用于跳转表识别和间接调用目标解析的策略
上面四个阶段为一个完整的CFG恢复技术流程。
5.2 值集分析 VSA
值集分析是一种二进制程序的静态分析技术,该技术结合数值分析和指针分析,使用一个成为值集抽象域来近似寄存器或抽象位置在每个程序点可能保存的值,这个程序点一般称为固定点(fix-point)。例如,对于向地址A的存储器写入操作,查询并计算出的固定点中的A的值将包含所有可能的写入目标的完整列表。
VSA算法原文
第一阶段 创建一组离散的跨步间隔
VSA的基本数据类型,即跨距间隔(strided interval),本质上是一组数字的近似值。它非常适合近似一组正常的具体值。然而,如果这些值在程序中被用作跳转的目标,则跨步间隔的过度近似性质,会通过创建指向不应成为跳转目标的地址的控制流转换,在恢复的 CFG 过程中产生不合理性。
为了有效解决这个问题,angr开发了一种称为“跨步区间集”的新数据类型,它表示一组未联合在一起的跨步区间。
仅当跨步区间集合包含多于K个元素时,跨步区间集合才会被合并为单个跨步区间,其中K是可以调整的阈值。较高的K值能够保持高精度,但代价是增加了分析的复杂性。
第二阶段 将代数求解器应用于路径谓词
跟踪分支条件有助于在条件退出后或合并过程中约束状态中的变量,从而产生更精确的分析结果,跟踪分支条件对应的技术为 ,然而,它不仅实现起来很复杂(通常导致在约束表达式中支持很少的算术运算),而且在现实中计算成本很高。
angr的解决方案是实现一个轻量级的代数求解器,该求解器基于处理一些仿射关系的模算术在跨步区间域上工作。当发现新的路径谓词时(即,当遵循条件分支时),尝试简化并求解它,以获得路径谓词中涉及的变量的数字范围。
然后,执行新生成的数字范围与每个相应变量的原始值之间的交集。这能够在遇到新的分支条件时不断改进值集分析的结果,从而提高最终固定点的精度。
第三阶段 采用符号不可知域
angr基于
分三个阶段使用 VSA 进行内存损坏检测:
- 首先,在 VSA 分析期间收集程序中的所有读写访问模式。在这些访问模式之上,对堆栈和堆区域上的变量执行变量恢复。 实现类似于
- 接下来,扫描所有堆栈和堆区域以查找异常缓冲区,包括 a) 重叠缓冲区和 b) 越界缓冲区。 然后简单地将所有异常缓冲区报告为潜在的内存损坏。
angr中对应的VFG图(CFG的增强版),就包含每个程序位置出的VSA固定点的信息。VFG中包含的程序状态以SimuVEX提供的抽象布局(特别是SimAbstractMemory内存模型)呈现内存,内存中的值由Claripy提供的值集表示。angr通过分析内存访问可能采用的值范围,对这些程序状态中包含的数据执行了缓冲区重叠分析。
5.3 动态符号执行
angr的动态符号执行模块主要基于
5.4 欠约束符号执行
angr实现了在
UCSE标记在状态中缺少上下文,为欠约束。当将此类欠约束数据用作指针时,将创建一个新的欠约束区域,并将指针指向新区域。这种“按需”内存分配允许对管理复杂数据结构的代码进行分析。当识别出安全违规(即写入堆栈上保存的返回地址)时,将检查所涉及的值是否处于欠约束状态。在某些情况下(即,如果所有涉及的数据都未得到充分约束),违规将被过滤为假阳性。
angr的改进如下:
全局内存欠约束
最初的UCKLEE的实现并没有将全局内存访问视为受限。然而,这种内存是UCSE无法预测的程序上下文的一部分,因为在分析给定函数时,全局数据可能已经被覆盖。因此,angr将所有全局数据标记为欠约束,从而降低了误报率。
路径限制
最初的UC-KLEE实现有几个内置限制,以防止路径爆炸。例如,它们将限制欠约束指针解引用的深度,以避免通过从不终止的欠约束链表进行搜索。angr增加了一个额外的限制器:当发现函数导致路径爆炸时,中止对函数的分析。angr通过硬编码限制来检测这一点,当单个函数分支到这么多条路径上时,用立即返回来替换函数,并从该函数的调用位置回放分析。这通过避免路径爆炸保持了分析的可操作性,但使分析更加不准确。
六 总结
以上是angr的核心技术实现的介绍,可以说我这篇博文介绍的还是比较浅显,因为从某种程度上来说,我只是在原论文翻译的基础上,添加了一点我的理解以及别人的理解,后面的我的部分博文将会针对部分angr的核心技术,进行相应的基础知识介绍,以方便能理解angr是个多么伟大的工作。
(本篇终)