ODDFUZZ: Discovering Java Deserialization Vulnerabilities via Structure-Aware Directed Greybox Fuzzing

摘要

 可以用静态分析来定位java反序列化漏洞。ODDFUZZ使用轻量级污点分析定位候选的gadget链(避免漏报),之后使用定向灰盒测试生成poc用例来减少误报。ODDFUZZ使用结构感知种子生成方法保证用例有效性,并使用混合反馈与步进策略来指导定向模糊测试。

 找到ysoserial库34个漏洞中的16个(baseline 为3个),并在其他库中找到6个CVE。

Introduction

 开放动态反序列化 OOD 具有破坏性(?),通过静态分析查找反序列化漏洞的工具有:(1)GadgetInspector,但是会有精度问题,从而产生漏报与误报;(2)SerHybrid,分析堆访问路径来寻找影响 sink 的 source。

 挑战:(1)精确率与召回率之间的权衡。由于java的运行时多态,任何重写的方法都可能变为gadget链,遍历所有可能的gadget链会使得路径爆炸,现有方法是使用污点分析剪枝,但是这种方法要么精度不行,要么计算代价太高。(2)现有fuzz生成的testcase都无法到达sink,生成有效的testcase需要多层级结构、且属性满足特定控制流约束。(3)现有方案以代码覆盖率为导向,而不是以目标为导向,就会浪费很多资源去找无法到达sink的path。

 ODDFUZZ贡献:(1)处理运行时多态时,对准确率与召回率之间进行权衡;(2)建模gadget链的数据约束,进行结构感知的fuzz;(3)使用step-forword策略与hybrid feedback,快速找到漏洞。

 使用java fuzz框架JQF来写ODDFUZZ。

Background

ODD

 开源动态反序列化ODD,又叫做对象注入漏洞OIV,指的是攻击者将序列化对象注入到应用程序的行为。ODD漏洞在js、php、.NET中也会有。open表示任意对象都可以被反序列化,dynamic表示能够调用invoke多态方法或基于反射的行为去找寻路径。由于java是动态的,所以很难预测会invoke哪个方法。攻击者可将应用程序的代码片段链接在一起(并按顺序执行它们)并将数据传递到安全敏感的调用点,攻击就成功。gadget链实例如下所示:

image-20231108211644048

 面向属性的编程POP。精细的定义要反序列化的属性,以链接不同级别的对象,从而进行成功的攻击。下图是对上图的POP编程,通过构造的PriorityQueue,最后可进行代码的远程执行。

image-20231108213655198

威胁模型

image-20231108213822316

定向灰盒测试 DGF

 以代码覆盖率为导向的灰盒测试(CGF)与定向灰盒测试(DGF)。对于某些场景,静态报告验证,漏洞就在那儿了,需要自己探索。DGF引导fuzzer到特定位置,并生成POC testcase。以下是DGF的流程:

image-20231108214859002

 (1)静态分析阶段:提取调用图与控制流图,计算二进制文件与目标之间的距离。(2)fuzzing循环阶段,目标距离、覆盖率、相似性作为反馈信息,以快速引导fuzzer到达目标点。利用反馈信息,fuzzer在seed pool中选择seed,并分配适合的能量进行变异。seed的能量确定生成多少新种子。fuzzer使用多种变异策略来引导种子向目标站点进化。具有更小距离的新种子将被保留,以进行下一次fuzzing循环。

Motivation

挑战1:运行时多态

 现有的工作可以识别代码中gadget的组合,攻击者使用gadget来组合gadget链。由于运行时多态,无法根据声明类型确定虚拟方法调用,因此,无法精确找到运行时的程序路径,从而导致高漏报。解决此问题的方法是进行类层次分析(CHA),综合考虑显式与隐式方法调用,但是盲目的考虑所有可行的gadget会导致路径爆炸问题。

 gadgetInspector分别追踪了函数参数返回值、其他方法调用的数据流,并列举了基于类继承层次结构的所有可用方法gadget链的相关函数重写。然而,攻击者可控制的属性能够从污点参数传播到其子类的参数,这类情况gadgetInspector没有考虑,即没有考虑过程内污点分析

 FUGIO在其构建的深度有界调用树上追踪了过程内数据流,以对不可行的gadget进行剪枝。然而,进行java ODD(开源动态反序列化)调用链发现时,这种方法不管用,这是因为传统java程序会集成很多个具有各自依赖关系的库,这会导致很多类和方法,使得调用链很深很宽,难以处理。

image-20231109214851761

挑战2:结构化的输入构建

 注入的对象一般都是由多个子对象构成的,通过POP编程,要求构造的对象在语法上可以反序列化,在语义上要满足一定的控制与数据流约束,从而使得gadget链可以实现有效的fuzz输入。如果没有复杂嵌套形式的对象结构的先验知识,那么很难构造出合适的fuzz case。

image-20231111105100799

 一种方法是generation-based fuzz技术。SerHybrid执行points-to分析,以产生堆访问路径,该路径可以到达安全敏感点。对堆访问路径中未出现的字段属性分配随机值,以生成有效的注入对象,然后执行。然而,随着可用gadget链的增加,fuzzer不知道注入对象的多级类层次结构,因此很难对属性分配正确的值。例如,很难从Comparator的多种实现中选取适合的TransformingComparator。FUGIO基于候选的链生成了属性树,并通过启发式规则突变属性。但是,任意子类的随机组合语义上可能是无效的。

挑战3:目标定向模糊测试

 由于gadget链是由攻击者可控的方法组成的,这些方法在对象反序列化期间自动执行。常规代码覆盖率无法指导fuzzer,因为trigger很多code片段可能也无法到达sink点。

 本文没有追求最大的代码覆盖率,而是使用定向灰盒测试DGF来优先考虑接近sink的seed。之前的定向灰盒测试器计算种子执行轨迹上所有基本块距离的算数平均值,来调度种子,以快速达到sink。其问题是,种子距离是可能有偏差的,并且种子执行路径可能没有像想象中那样朝sink运行,对注入对象属性的修改可能会使执行路径发生巨大变化。因此,目标定向fuzz的feedback是很重要的。

ODDFUZZ 设计

image-20231114195459925

A. 总览

 上图的program代表jar、war、class file,将其作为输入,并使用轻量级污点分析自动识别所有潜在的gadget链。之后,基于潜在的gadget链,生成结构感知的seed,以生成语义上有效的fuzz注入对象。在fuzzing loop阶段,oddfuzz结合前向突变策略与混合反馈(seed距离与gadget覆盖率)去引导fuzzer将注入对象突变,并到达sink。

B. 污点分析

 如果有可利用的gadget链,那么进行污点分析,一定能从source到达sink。可以构建call graph(CG),来寻找可达的路径。然而,由于java运行时多态,虚函数调用无法被确定。因此,我们使用轻量的基于摘要的污点分析,来标记可疑的gadget链

函数摘要的计算。oddfuzz首先计算所有函数的静态摘要,具体来说,oddfuzz提取函数参数与this作为其摘要。之后,追踪方法中变量的传播,主要针对四种类型,Assigin/Load/Store/Call数据依赖于方法参数的变量也包含在方法摘要中。这些摘要用于识别可利用的gadgets(gadget链上的点)。

gadget链的识别。oddfuzz指定可利用的方法与敏感调用站点(sink)的列表,并基于上一步的函数摘要来识别可疑的链,在本文中指定了16种可利用的方法与30个sink(附录A)。(1)之后,由于gadgetInspector使用BFS(广度优先搜索),在不可行路径上遍历过的gadget就不会再考虑,从而导致漏报。而本文中,一旦在程序路径中找到前面标记过的可利用方法,那么就会从source gadget开始,基于方法摘要调用DFS,来找可利用的gadget链。(2)为了避免无休止循环,制定门限以规定链的最大深度。(3)为了处理java的运行时多态,使用层次结构分析CHA,仅当调用点被污染时,才会使用CHA,避免由于盲目考虑所有的gadget带来的路径爆炸问题。(4)oddfuzz的工作方式像普通的CG污点分析器。(5)所有来自于source的路径分析完成之后,就开始运行validator。借助轻量污点分析,可以平衡有效性(识别尽可能多的gadget链)与扩展性(可接受的时间开销)。

C. 结构感知的定向灰盒模糊测试

 给一个java程序与候选gadget链,oddfuzz使用结构感知的定向灰盒测试生成用于validation的可注入对象。fuzzing loop算法在附录B的算法1中。

结构化种子生成。生成语法上有效的注入对象需要:(1)设计反映gadget执行流的嵌套对象结构。(2)分配合适的属性值,方便到达sink。为此,设计了结构感知的种子生成方法,通过使用属性树的分层数据结构处理复杂的嵌套,其中根节点代表一个保存多个gadget的类对象。如下图所示,首先实例化gadget链涉及的类,并利用反射来收集类的可用属性,并构造属性树。

image-20231114215527381

 具体来说,如果属性树中节点的类型是对象,且此对象由另一个属性树表示,且此对象对应的类保存目标链的下一个gadget,则我们通过连接此节点来合并两个属性树。并且,当属性树中某个字段节点的类型是另一个属性树的根节点(类对象)实现的接口时,两个属性树也会合并,如上图的 Comparator comparator 和 TransformingComparator 连接一样。

 当oddfuzz识别出可疑的 gadget 链(需要validate)时,它将被送入输入生成器以构造相应的属性树。目标gadget链的多级类层次结构可以使用属性树很好地建模。然后,fuzzer开始遍历属性树的主干,将其转换为用于fuzz的初始注入对象(上图右侧)。其他没有后继的属性节点(例如上图的Object[])将被设置为null,以进行突变。

使用混合反馈来对种子的优先级进行排序。对注入对象进行随机生成和变异不一定会到达sink,这种没有明确反馈指导的不确定性fuzz会使fuzzer退化为语义盲目的愚蠢模糊器。为了有效地选择和调度种子以到达sink,使用混合反馈驱动的种子优先级排序方式。即,为更接近sink的种子分配更多能量。oddfuzz考虑了两种类型的反馈指标:种子距离和gadget coverage。

(1)种子距离。计算种子距离以优先排序并安排种子以尽快到达sink是目标灰盒测试 DGF 的核心。种子s与sink所属的目标基本块$T_b$之间的距离计算为:

 其中$d{\boldsymbol{b}}(m,T{\boldsymbol{b}})$是种子s执行轨迹中的基本块m与目标基本块$T_b$之间的距离。我们不是枚举种子 s 执行路径上的所有基本块,而是收集目标链的 gadget 内执行的基本块$\xi(s)$来计算种子距离,避免fuzzer探索不相关但更接近的路径。

(2)gadget coverage。使用此指标来优先考虑覆盖更多程序路径的种子。在最初的模糊测试阶段,gadget coverage旨在引导fuzzer选择不同的种子并对其进行优先级排序,避免因偏爱具有特定执行路径的某些种子而陷入局部最优。在power 分配阶段,gadget coverage尝试为种子提供相同的距离,但覆盖更多的分支,从而提高突变的机会

 oddfuzz将所有生成的种子根据距离升序排序,并维护一个两级队列。第一个种子(或距离相同但覆盖范围不同的种子)将被放入优先队列中,其余种子将被放入较不优先队列中。因此,oddfuzz 有更大的机会从优先队列中选择下一个种子进行变异。至于power分配,oddfuzz使用如下方程,以便为所选种子输入分配适当的能量:

 其中$\psi(s)$表示gadget coverage,并且$\widetilde{d}(s,T_b)=\frac{d(s,T_b)-minD}{maxD-minD}$是归一化种子距离。通过此方程,fuzzer可以确定应用于当前种子的变异机会数量,并评估在种子优先级排序过程中是否应优先考虑变异种子,从而在探索不同的执行路径和对种子进行优先级排序之间取得平衡。

前向种子突变。以前的fuzz是通过位翻转等操作随机改变二进制文件来产生新的输入。然而,当应用于结构化输入时,这种位突变可能会导致无效语法。为了解决这个问题,我们利用 JQF(参数化模糊测试框架),它将结构化输入映射到一系列参数,以在位级别改变生成的种子。参数上的这些位突变对应于结构化注入对象上的属性级突变。然后,oddfuzz 应用前向种子突变策略来有效引导种子前往sink。

 具体来说,fuzzer首先遍历要变异的注入对象的属性树并检查每个属性的类型。对于原始数据类型(例如 boolean、int),fuzzer使用 JQF 中的多种伪随机方法将无类型位参数转换为随机类型值;对于reference数据类型,fuzzer为特定类型定制目标模板;当类型为class时,fuzzer 将通过random.choose()方法从该属性的子类中随机选择一个类。对于数组属性,模糊器使用 random.nextInt() 方法随机设置数组大小,并根据元素类型(即继承数组类类型的实例)为数组分配随机值。例如,从Fig.7.中的属性树生成的注入对象的参数序列为:

image-20231115194857259

 上图的解释如下:为了改变属性 size 的值(类 PriorityQueue 中的 int 类型变量),fuzzer调用 random.nextInt() 方法来生成随机整数 1。为了生成对象数组队列,fuzzer调用方法random.choose() 从预定义的字典中为其分配一个实例对象,该对象由候选 gadget 链中所有类或方法涉及的一些特定属性值(例如类对象、字符串对象)组成。

 此外,为了引导种子走向sink,oddfuzz在位级别上改变感兴趣的注入对象的嵌套子对象。为此,我们使用 random.nextBool() 方法将额外的标识符字节插入到注入对象的参数序列中。当fuzzer在遍历属性树时遇到类对象节点时,fuzzer会添加一个字节作为标识符,以标记是否更改此嵌套子对象的属性值。我们利用fuzzer收集的gadget coverage来识别注入对象覆盖的最后一个分支所在的类,一旦注入对象被卡在某些gadget中(不按照链向下运行),fuzzer就会将相应的标识符字节设置为 true,并将随机值分配给参数,这些参数对应于被卡gadget所属类的属性的结构突变,以产生新的输入。

 为了说明前向突变,考虑以下参数序列$\sigma_{2}$:

image-20231115195815150

 如上所示,假设有一个注入对象卡在gadget TransformingComparator.compare() 中,fuzzer将其 Identifier 翻转为 true 并改变 TransformingComparator 类对应的参数序列(例如,将实例T分配给属性transformer)。基于这种前向突变策略,fuzzer可以有效地生成更有可能到达sink的输入。

 最后,当变异的种子到达sink时,fuzzer将报告给定的gadget链是可以被利用的。

应用

 我们基于流行的 Java 模糊测试平台 JQF 实现了 oddfuzz。我们定制了它的组件,使其适合gadget链模糊测试,同时搭载 JQF 的底层功能,例如运行时检测(tai-e能做到这一点吗?)。

污点分析。 oddfuzz 使用 Soot 来解析 Java 字节码并将其转换为中间语言 Jimple 。基于 Jimple 的基本类信息(例如类修饰符、字段、方法),实现了基于方法摘要的污点分析。

结构化模糊测试oddfuzz 修改了 JQF 中内置的 junit-quickcheck 生成器,以根据候选 gadget 链随机生成和变异结构化注入对象,而不是手动编写输入格式的声明性规范(例如上下文无关语法或协议缓冲区)。为了启用和促进结构化感知种子生成,采用 JRE 提供的类 sun.msic.Unsafe,允许用户创建类的实例,而无需调用其构造函数代码、初始化代码、各种 JVM 安全检查和所有其他低层次的东西。

运行时Instrumentation。当 JVM 加载类时,我们使用 ASM 工具包通过 javaagent 动态检测 Java 字节码。当测试程序 PUT 启动时,oddfuzz 工具会注入一个静态方法调用,该调用在每次调用或跳转指令后执行,以跟踪注入对象的执行。出于效率考虑,检测仅限于与gadget链相关的字节码,而不是整个程序。

反馈收集。对于覆盖率信息,我们对 JQF 进行了最小的修改,通过跳转指令检测每个基本块,来收集branch覆盖率。对于距离信息,oddfuzz 基于 ASM 在字节码级别生成相应的 gadget 链的过程内控制流图(CFG)。 CFG的根节点(即gadget)由方法签名来标识,而其他CFG节点则由相应基本块的跳转指令来标识。当将 gadget 链送到fuzzer进行验证时,oddfuzz 距离计算器会根据 gadget 链的调用顺序和生成的 CFG 计算每个基本块到sink的过程间距离。距离计算器是用 JGraphT 库实现的。

To be continued…

评估

讨论

相关工作

结论

留言

© 2024 wd-z711

⬆︎TOP