论文题目 | 论文出处 |
---|---|
Automatic Techniques to Systematically Discover New Heap Exploitation Primitives | USENIX 2020 |
Breaking Through Binaries: Compiler-quality Instrumentation for Better Binary-only Fuzzing | USENIX 2021 |
ICSFuzz: Manipulating I/Os and Repurposing Binary Code to Enable Instrumented Fuzzing in ICS Control Applications | USENIX 2021 |
APICRAFT: Fuzz Driver Generation for Closed-source SDK Libraries | USENIX 2021 |
文章目录
- Automatic Techniques to Systematically Discover New Heap Exploitation Primitives
- Breaking Through Binaries: Compiler-quality Instrumentation for Better Binary-only Fuzzing
- ICSFuzz: Manipulating I/Os and Repurposing Binary Code to Enable Instrumented Fuzzing in ICS Control Applications
- APICRAFT: Fuzz Driver Generation for Closed-source SDK Libraries
Automatic Techniques to Systematically Discover New Heap Exploitation Primitives
作者:Insu Yun† Dhaval Kapil‡ Taesoo Kim† (†Georgia Institute of Technology ‡ Facebook)
出处:USENIX 2020
背景
堆相关的漏洞是最常见并且很严重的安全问题。现有的堆利用工具一方面是应用程序无关的,不需要深入理解程序逻辑,另一方面堆相关的漏洞的利用空间很大,很小的一个漏洞都可能造成巨大的危害。普适性的堆漏洞利用技术很难被发现,大部分的方法都是手工构造的专门针对某个漏洞的。并且构造出来的东西也很难理解。现有的研究也存在很明显的倾向性。因此这篇文章想提出一种自动化的堆漏洞利用生成方法。
Insight
这篇文章借鉴fuzzing的思想把它用在自动化堆利用生成方面。也就是通过随机生成一些和堆相关的操作指令去执行,看是否能够利用成功。如果利用成功就生成一个PoC。
它定义了一些基于堆操作序列以及变异值的变异策略来生成一些堆操作序列作为测试用例,然后通过定义一些堆利用能造成的影响来判断是否利用成功。然后对利用成功的操作序列进行修剪,生成一个PoC。这个方法的好处是能够发现一些新的堆利用技术,论文中也确实发现了5种新的堆利用技术。
挑战
- 要想通过fuzzing来触发一个堆漏洞,需要构造特定的序列以及特定的数据。
- 需要快速的估计堆利用的可能性。一般的fuzzing需要用明确的信号确定程序是否崩溃。
- 模糊测试工具生产给你的测试用例通常冗余并且难以理解,需要大量的时间去分析。
背景知识
堆分配
动态内存分配在管理程序的堆空间时有重要作用。C标准库定义了一些API来实现动态内存分配,如malloc函数接收一个数值n,就会申请n个字节大小的空间,然后返回指向这个空间的指针。由于n的大小不确定,堆空间分配就面临性能和内存占用之间的矛盾。就是说一方面要尽可能占用的段数少提高性能,一方面又要减少内存的浪费(因为一个块中可能会有多出来的零碎的剩余空间)。目前有三种堆分配方式。
- binning:把一个内存区域划分成多个大小不同的组,小的块就用来兼顾性能,大的块关注分配器的内存使用。每次根据申请的大小选择最合适的块。
- In-place metadata:为了尽可能减少内存占用的段,内存分配算法会维护一个关于已分配和已释放内存区域的信息的元数据。把元数据和有效载荷放在临近的地方可以增加局部性。
- Cardinal data。相比于In-place metadata,内存分配器它只会保留一些和查找等相关的关键信息。
因此,作者认为这篇文章的方法应该适用于不同的堆内存分配方式。对于binning应该探索不同大小的组。对于另外两个就可以限定搜索区域的范围。
ptmalloc2
ptmalloc2是glibc中使用的堆分配器。下图是它的chunk的结构。在ptmalloc2中用户请求分配以及请求释放的空间都用一个chunk来表示。chunk的大小是8bit对齐的。
-
(a)是一个使用中的chunk。
- p=0表示前一个chunk为空闲,等于1表示前一个正在使用。主要用于内存块的合并。ptmalloc分配的第一个块总是将p设为1,以防止程序引用到不存在的域。
- M=1为mmap映射区域分配;M=0为heap区域分配。
- A=1为非主分区分配;A=0 为主分区分配。
-
(b)是一个空闲的chunk。
- 空闲的chunk会放在空闲的链表bins上,当用户申请空间的时候会先查空闲链表bins上是否有合适的。
- fd指向前一个空闲chunk,bk指向后一个空闲chunk。
- prev_size表示前一个空闲chunk的大小。
-
ptmalloc将大小相似的chunk用双向循环链表连接起来,这样的一个链表称为bin。ptmalloc中一共维护了128个bin,并使用一个数组来存储这些bin(数组实际存储的是指针)
- fast bins。fast bins是small bins的高速缓冲区。fast bin使用单向链表实现。当程序运行需要申请和释放一些较小的内存空间。一般对于不大于max_fast(在SIZE_SZ为4B的平台默认是64B)的chunk释放后就会先被放到fast bins中,fast bins中有七个chunk空闲链表(bin),每个bin的chunk大小依次为16B、24B….64B。fast bins中的chunk并不改变它的使用标志P,这样就无法合并他们。当需要给用户分配小于或者是等于max_fast大小的内存时,ptmalloc会首先在fast bins中查找,然后才会去Bins中查找空闲的chunk。有时ptmalloc也会遍历fast bins中的chunk,将相邻的空闲的chunk进行合并,并将合并后的chunk加入到unsorted bin中,然后将unsorted bin中的chunk加入到bin中。
- unsorted bin。相当于small bins和large bins的一个缓冲区,unsorted bin是bins数组中的第一个,用双向链表管理chunk,空闲的chunk不排序,所有的chunk在回收时都要先放到unsorted bin中。进行malloc操作时,如果fast_bins中没有找到合适的chunk,则ptmalloc就会在unsorted bin中查找空闲的chunk,如果unsorted bin中没有合适的chunk,就会把unsorted bin中所有chunk分别加入到所属的bin中,然后再在bin中查找合适的chunk。Bins数组中元素bin[1]用来存储unsorted bin的链表头。
- small bins。数组中从2号下标开始的到64号下标的62个bin称为small bins。同一个small bin中的chunk具有相同的大小,两个相邻的small bin中的chunk大小在SIZE_SZ为4B的平台上相差 8bytes,在SIZE_SZ为8B的平台上相差 16bytes。small bin 中chunk按照最近使用顺序进行排列,最后释放的chunk被连接到链表的头部,而申请的chunk是从链表尾部开始的。
- large bins。在SIZE_SZ为4B的平台上,大于等于512B的空闲的chunk由large bin管理。large bins一共包含63个bin,每个bin中的chunk大小不是一个固定的等差数列,而是分成6组bin,每组bin是一个固定的等差数列。每组bin数量依次为32、16、8、4、2、1,公差依次为64B、512B、4096B、32768B、262144B等。
- tcache。在glibc2.27中,tcache的基本结构与fastbin类似,都是根据chunk的大小将chunk放到不同的bins里,也都是通过单链表来存储释放的chunk,但是单链表的链接方式还是有所不同的。
堆利用
相关工作
Unsafe unlink attack就是对于双向链表的chunk去修改它的fd和bk使他指向人工构造的代码位置。针对这个攻击,已有的防御技术是增加一个安全检查,也就是规定fd和bk必须是指向一个chunk的。其实解决不了问题,因为攻击者也可以构造一个假的chunk来越过这个安全检查。
堆抽象模型
- bug类型
由于溢出支持多种攻击路径,进一步描述为:
2. 漏洞利用的影响
设计目标
自动探索新类型的堆利用技术,给定任何堆分配器,都可以支持自动化漏洞利用生成。
- 系统地发现未知类型的堆利用。
- 全面评估流行的堆分配器的安全性。
- 分析如何改进它们。
技术挑战
- 堆空间自动推理。实现堆利用需要在一个大的空间范围内寻找可用的API,数据以及buffer。对于已知漏洞利用技术可以缩小搜索的范围,但是对于未知漏洞利用就不适用了。本文使用一种随机搜索算法。
- 构思漏洞利用技术。对于可能的漏洞利用需要进行验证,很难。
- 归一化。尽管随机搜索在探索大的搜索空间时是有效的,但由该算法发现的利用技术往往是冗余和不必要的,需要大量的时间来分析结果。
方法设计
ARCHEAP总体的思想和fuzzing差不多,但是是用于堆漏洞利用。首先用户可以提供初始的堆动作序列(相当于fuzzing中的初始测试用例),不提供的话就自己生成可能的堆操作,比如堆内存申请、释放、写buffer等。然后执行,ARCHEAP判断执行结果有没有构成堆利用的影响。如果有,就最小化堆动作,然后生成一个PoC。
堆相关的操作
- ARCHEAP随机生成5种类型的堆相关的操作:allocation, deallocation, buffer writes, heap writes, and bug invocation.
-
allocation. 基于malloc()函数申请内存空间,然后把返回对象的地址存储起来,还使用了malloc_usable_size()用于存储对象块的大小以及状态。申请空间的大小按照下表的策略随机生成。
-
deallocation.用free()函数随机选择一个堆指针释放。为了避免double_free,ARCHEAP会检查对象的状态,就是说如果选中的这个堆指针已经释放了就会忽略这个deallocation操作。
-
heap & Buffer write.ARCHEAP还会向堆对象或者全局 buffer写随机的数据。它会利用in-place metadata和cardinal data来时它写的这个值尽可能地准确。ARCHEAP还引入一定的噪声来帮助这个工具找到不同的利用技术。根据值得类型,它会用线性(加、乘法)或者移位等操作修改这个值。
-
Bug invocation. 目前会处理6中bugs:overflow、write-after-free、off-by-one overflow,、off-by-one NULL overflow、double free、arbitrary free 。它会故意构造一个错误动作使潜在得上述bug发生。
- 模型设定。用户可以选择性地提供模型规范,以指示ARCHEAP专注于某种类型的利用技术,或者限制目标环境的条件。它接受五种类型的模型规范:chunk sizes, bugs, impacts, actions, knowledge。
- 检测堆漏洞利用技术。
fuzzing可以通过crash来判断程序是否异常,在这里,ARCHEAP就通过定义四种漏洞利用的影响来作为漏洞利用是否可以的标志。它定义了四种影响,arbitrary-chunk (AC), overlapping-chunk (OC), arbitrary-write (AW),and restricted-write (RW)。
- 对于AC和OC,ARCHEAP确定每个分配中的任何重叠块(图3-2中的第18行)。为了保证检查的安全性,它会在malloc之后复制块的地址和大小,因为在执行错误操作时它可能会被破坏。使用存储的地址和大小,它可以快速检查块是否与其数据结构(AC)或其他块(OC)重叠。
- 为了检测AW和RW,ARCHEAP使用称为影子内存的技术安全地复制其数据结构、容器和全局缓冲区。在执行期间,ARCHEAP在执行可以修改其内部结构的操作时同步影子内存的状态:容器的分配和全局缓冲区的缓冲区写入。然后,ARCHEAP在执行任何操作时检查阴影内存的分歧或矛盾。由于ARCHEAP保持了显式的一致性,因此只有在以前执行的操作通过堆分配器的内部操作修改ARCHEAP的数据结构时,才会发生分歧。稍后,可以重新制定这些操作,以修改应用程序的敏感数据,而不是用于攻击的数据结构。
- 生成PoC。如果一个testcase成功利用了,接下来就是把没用的操作给去掉。许多操作都是独立的,所以可以用delta调试来把对利用没有影响的操作去掉。去掉之后,把剩下的操作转化成C语言。
新的堆利用技术
- Unsorted bin into stack (UBS):此技术重写未排序的bin以链接假块,以便它可以返回假块(即任意块)的地址。这类似于house of lore。然而,Unsorted bin into stack技术只需要一种分配,而不像house of lore,它需要两种不同的分配,将一个块移动到一个小的bin列表中。
- House of unsorted einherjar (HUE):这是house of einherjar的一个变体,它使用off-by-one NULL字节溢出并返回任意块。在house of einherjar中,攻击者应该事先知道一个堆地址来破坏ASLR。然而,在house of unsorted einherjar中,攻击者可以在没有这个前提条件的情况下达到相同的效果。我们将这项技术命名为“House of unsorted einherjar”,因为它有趣地将两种技术“house of einherjar”和“unsorted bin”组合到堆栈中,以放宽对众所周知的开发技术的要求。
- Unaligned double free (UDF):这是一种非常规的技术,它滥用在一个small bin里的double free,由于全面的安全检查,这通常被认为是一个薄弱的攻击面。为了避免安全检查,双重空闲的受害者块应该有适当的元数据,并被欺骗为正在使用(即下一个块的P位是1)。由于double-free不允许任意修改元数据,现有技术只能滥用fast bin或tcache,后者的安全检查比small bin弱。
- Overlapping chunks using a small bin (OCS):这是重叠块(OC)的一种变体,它滥用unsorted bin来生成重叠块,但是这种技术在一个small bin中处理块的大小。与OC不同,它需要更多的操作:三个malloc()和一个free(),但不需要攻击者控制分配大小。当攻击者无法调用任意大小的malloc()时,此技术可以有效地构建重叠块以供攻击。
- Fast bin into other bin (FDO):这是另一种有趣的技术,它允许攻击者返回任意地址:它滥用合并功能将受害者块的类型从fast bin转换为另一种类型。首先,它会破坏一个快速的bin-free列表来插入一个假块。然后,在释放过程中,它调用malloc_consolidate() 将假块移动到未排序的bin中。与其他fast bin相关的技术不同,这个假块不必在fast bin中。由于空间限制,排除了这个PoC,但它在存储库中是可用的。
Breaking Through Binaries: Compiler-quality Instrumentation for Better Binary-only Fuzzing
作者:Stefan Nagy, Anh Nguyen-Tuong, Jason D. Hiser, Jack W. Davidson, Matthew Hicks
出处: USENIX 2021
概要
有源码的情况下,编译时插桩带来的低性能损耗能够保证fuzzing的高吞吐量。但是对于无源码的二进制程序的fuzzing,现有的插桩在速度上很慢。
作者实现的ZAFL利用二进制重写技术(binary rewriting)将编译器级别的插桩能力扩展到无源码的二进制程序上。实验结果表明,ZAFL在触发crash的能力和吞吐量两个指标上均取得了显著的提升,并且在应用到真实世界软件的模糊测试时,从软件体积、复杂度、以及运行平台等维度上具有良好的可扩展性。
这篇论文我没有总结出一个核心的insight,作者更多地是分析现有方法的局限性分析,和现有技术的比较,然后总结出设计目标并利用已有的技术实现了一个二进制程序插桩工具。
方法设计
- 作者根据已有的fuzzers总结了四种fuzzing-enhancing transformation:
- 插桩剪枝:给定N个基本块,M条覆盖这些基本块的路径。通过修剪,得到N的一个子集使得M条路径覆盖这个子集和覆盖这N个基本块效果一样。
- 插桩降级:现有的技术主要是计算edge的哈希值作为插桩标记,计算哈希需要插桩插入很多指令,但是给定一个基本块,应该尽可能地减少插桩的指令。比如,对于只有一个前驱基本块的基本块就可以减少插入的指令数。
- 子指令解析。对magic bytes. Nested checksums, switch cases这些复杂指令进行指令解析,把它们解构成逐字节比较,让fuzzing更容易被覆盖。
- 其它的代码覆盖率行为跟踪。如上下文敏感的代码覆盖它会记录一条边的前驱地址。
- 作者对已有的二进制插桩方法的局限性进行了讨论。
- 硬件支持的tracing。Intel PT 能够提供硬件的代码覆盖率收集支持,但是它速度很慢,并且它不能修改二进制文件,也就不能提供加强版的代码覆盖信息收集。
- 动态二进制翻译。PIN、QEMU等能够提供动态二进制翻译,但是想能损耗高。
- 静态二进制重写。静态二进制重写限制很多。比如AFL-Dyninst性能损耗500%,并且仅限于Linux程序。RetroWrite依赖于AFL的汇编插桩,不支持transformation,比起编译时插桩慢10%-100%。并且仅限于position-independent linux C programs。
- 方法设计时考虑的四个方面
- 重写还是翻译。动态二进制翻译需要支持目标主机架构对应的指令集,并且现有的工具性能都很差。相比之下,二进制重写性能损耗低,并且可以支持自定义的操作。
- 内联还是蹦床。插桩的指令如何被执行,如果使用一个蹦床的话就需要两个跳转:跳到要执行的程序然后跳回来。但是容纳这种重定向所需的所有指令和基本块的大小的关联性很强;而且它们的开销大。因此选择内联。
- 寄存器分配。它会跟踪寄存器的使用情况,尽可能避免保存/恢复死(未动)状态的代码寄存器。
- 真实程序的拓展能力。相比动态二进制翻译,静态重写的局限性很高。他应该支持多种平台、二进制程序格式。
方法实现
-
总体流程。
给定一个二进制程序,生成一个中间表达,然后ZAX通过4个步骤对插桩进行优化,然后对二进制程序进行重构。
-
静态重写引擎是在GCC IR-inspired static rewriter Zipr工具的基础上进行了拓展,也就是有LLVM IR-basedrewriter McSema的功能但是overhead要低。
-
四个步骤。
- 看示意图应该是要把一些指令如magic bytes进行解构。
- 在优化好的控制流图的基础上分析一些额外的信息,比如寄存器的使用情况。
- 选择插桩的位置。把对未来检测没有必要的基本块去掉。
- 插桩。
ICSFuzz: Manipulating I/Os and Repurposing Binary Code to Enable Instrumented Fuzzing in ICS Control Applications
作者:Dimitrios Tychalas1, Hadjer Benkraouda2 and Michail Maniatakos2(1NYU Tandon School of Engineering, Brooklyn, NY, USA, 2New York University Abu Dhabi, Abu Dhabi, UAE)
出处:USENIX 2021
工业控制系统(ICS)利用可编程逻辑控制(PLC)应用程序来实现对算数或者逻辑值进行接收、处理和传递。对PLC程序的安全性进行评估主要有两方面的挑战,一方面PLC程序构建工具的提供商各自有全栈的开发框架,并且是闭源的只会提供有限的文档。另一方面,不同的厂商构建出来的PLC程序格式不一样。运行时的环境处理还有实时的输入传送都是特定的,很难有通用的方法。
这篇文章主要是针对工业控制系统中的PLC应用程序提出了一种fuzzing方法。
- 首先PLC程序可以用图形或者文字等多种方式,多种语言进行开发,作者对各种IEC61131-3标准的编程语言进行分析,主要有LD,FBD 和ST。作者发现不同的语言产生的机器码类似的,这篇论文重点针对ST语言进行分析。
- 对PLC程序中的内存操作相关的函数进行重点分析。
- 然后提出一种fuzzing方法,这个方法针对PLC程序的运行、输入控制、插桩等存在的问题提出了解决方案使得fuzzing适应PLC测试。
- PLC程序运行时可能会把一个二进制文件加载为一个自己的线程进行运行,那么两个二进制程序之间的依赖存在的安全问题也要考虑到。作者通过对动态链接库、共享对象(so)及其包含的函数进行fuzzing,通过ghidra进行逆向工程得到反编译的结果,然后模拟它们的行为。
实验的话主要是从fuzzing的角度对执行速度、crash时间、crash数量、代码覆盖率等维度进行评估。
APICRAFT: Fuzz Driver Generation for Closed-source SDK Libraries
作者:Cen Zhang§ Xingwei Lin‡ Yuekang Li § Yinxing Xue† Jundong Xie‡ Hongxu Chen § Xinlei Ying ‡ Jiashui Wang ‡ Yang Liu § (§Nanyang Technological University ‡Ant Group †University of Science and Technology of China)
出处:USENIX2021
背景
我们用fuzzing对库文件进行测试的时候需要一个fuzz driver来测。这个fuzz driver需要知道被测的库文件中含有哪些API,以及这些API之间的调用关系。现有的方法如FUDGE和FUZZGEN通过已有的调用这个库文件的开源程序来提取其中的API调用,从而构建fuzz driver。但是很多库文件是闭源的,利用这个库文件的程序也是闭源的,在这种情况下,上述方法便无法支撑。
挑战
对于库文件以及调用它的应用程序都是无源代码的情况下,fuzz driver的构建面临两个挑战:
1. 关于库文件中存在的API,以及API的使用都很难提取。
2. API之间的语义关系复杂,并且API是否被正确调用也是不知道的。
Insight
作者认为API的组合依赖应该满足多样性、正确性以及简洁性三个维度的优化,通过多目标优化模型来建模求解。
方法
API函数依赖收集
- 数据依赖
- 如果把一个API函数F的输入和输出分别定义为
I
F
I_F
IF和$ O_F$ , 函数A和B之间有数据依赖要满足
(
I
F
A
∩
O
F
B
)
∪
(
I
F
B
∩
O
F
A
)
≠
∅
(I_{F_A} \cap O_{F_B}) \cup(I_{F_B} \cap O_{F_A}) \neq \emptyset
(IFA∩OFB)∪(IFB∩OFA)=∅,这里的交集的意思是有数据依赖。
- A的返回值被用作B的参数。
- A的输出参数被用作B的输入参数。
- 上述依赖用过数据的类型和值可以被提取出来。
- 分析header中的声明来获取参数的类型和返回值的类型。
- 获取调用库函数的程序的运行trace获取和数值相关的信息。
- hook函数的入口和出口,记录输入和输出相关的线程id,嵌套等级以及递归内存等信息。
- 嵌套等级:如果这个API是直接被外层程序调用的,等级就是1.如果被等级为x的其它API 调用,它的等级就是x+1。那些被内部API调用的API相比之下就不那么重要,因为构造fuzz drive要考虑的往往是程序直接调用库文件中的API。
- 递归内存:如果不是指针或者结构体那就直接dump,如果是结构体就dump每个成员的值,如果是指针就dump它自己的值以及指向的值。如果输入参数是指针,那么也要考虑指针在执行过程中值得变化,那么输出集中也要包含他。
- 此外,根据上述信息还可以推测出一些API之间的依赖。
- 如果API A和B的输出都与API C的输入有依赖关系,其中A的输出与D的输入有依赖关系,那么推测B与D也有依赖关系。
- 根据输入输出的类型进行推测。
- 记录线程id就可以考虑线程间的依赖关系。
- 控制依赖
- 利用静态和动态分析来判断一个API函数是否需要error handling以及对应的error condition。
- 对于输出集中有指针的都检查指针是否为空。
- 利用污点分析确定调用error handling的API。
函数依赖组合
- 定义了三个维度的适应性指标:多样性、正确性以及简洁性。
- 多目标遗传算法:每个函数是一个节点,两个节点之间的一条边是基因,它们之间输入集和输出集之间的关系使得两个节点可能有多条边,这多条边就是染色体。
- 通过遗传算法NSGA-II构建多目标优化模型,获取API之间的依赖组合,同时实现三个维度目标的最优化。
实验
- 测试对象
- 实验部分首先展示了APICRAFT的运行信息来说明它功能的有效性。
- 然后用手动构造的fuzz driver和APICRAFT进行比较。
- 最后探究每个部分的效率。