ODDFUZZ
DOI:10.1109/SP46215.2023.10179377
ODDFUZZ: Discovering Java Deserialization Vulnerabilities via Structure-Aware Directed Greybox Fuzzing
感觉以后可以做这个,SP发表,虽然23年的文作者现在仓库仍为空,不过有点思路,值得记录
我来小巧的总结一下:
作者画了四五页讲CC链,这里不讲了。
实现方法就是:
- 先定义和hook source、sink。至于用什么定义,我建议用OpenRasp-IAST写js脚本最方便,作者文中自己写的用的手写的Instrument。
- 搜索source到sink的路径
创新点:处理运行时多态(关键优化):
- 对于Java中的虚方法调用,传统方法(如类层次分析CHA)会盲目考虑所有可能的子类重写方法,极易导致“路径爆炸”。
- ODDFUZZ的优化在于:仅当调用点(call site)本身被污染(即参数或
this对象受Source影响)时,才会启用CHA去分析子类。这个策略极大地减少了需要分析的无关路径。
与类似工具GadgetInspector使用广度优先搜索(BFS)不同,ODDFUZZ在程序调用图中,从标记的Source方法开始,基于方法摘要进行深度优先搜索(DFS)。
看懂这里先去看GadgetInspector
那么读到这里就有个疑问了,这里不是就知道Gadget了吗,知道gadget我还用这些花里胡哨的方法干嘛?接着往下看
算法中几个关键的部分:
- 关键一:前向种子突变策略
这是ODDFUZZ与传统随机变异的核心区别。传统Fuzzer随机翻转比特,极易产生语法无效的Java对象。而ODDFUZZ的变异是在“属性树”的语义层面进行的:
- 属性级变异:
- 对于基本类型(如int):调用
random.nextInt()等方法生成随机值。 - 对于类类型:从该属性所有可能的子类中,
random.choose()一个。 - 对于数组:随机设置大小,并根据元素类型生成实例。
- 对于基本类型(如int):调用
- 前向引导:
- 这是算法的精髓。Fuzzer会分析当前种子卡在链中哪个Gadget方法里(利用执行反馈)。
- 然后,它有目的地突变导致“卡住”的那个嵌套子对象所对应的属性。例如,如果执行卡在
TransformingComparator.compare(),算法就会翻转一个标识符,专门变异TransformingComparator类实例的属性(如transformer字段),而不是盲目变异整个对象。
- 混合反馈指导
变异机制主要攻克了静态分析无法解决的三大难题:
难题 静态分析的局限 变异如何智能解决 1. 满足复杂数据约束 知道方法A调用方法B,但不知道调用B需要某个字段为特定值。 随机探索:变异会不断尝试为字段赋予各种随机值(数字、字符串、对象),直到找到一个能让程序流通过的组合。 2. 构建正确的对象类型 知道需要一个 Comparator接口的实现,但不知道具体需要哪一个子类(如TransformingComparator)。结构化选择:变异时,会从一个候选类型池中,选择并实例化正确的子类对象来替换接口,从而满足运行时类型要求。 3. 引导执行流向目标 知道所有可能的岔路,但不知道哪条路最终能通向Sink。 定向引导:结合“距离”反馈,变异会优先修改那些更有可能接近目标Sink的路径上的对象属性(前向种子突变)。
算法不是盲目变异,而是用两种反馈智能指导:
- 种子距离:衡量当前种子执行轨迹与目标Sink基本块之间的“距离”。距离越小的种子,被认为越有可能到达Sink,在下一轮中被分配更多变异能量(即更多次变异机会)。
- Gadget覆盖率:衡量种子执行路径覆盖了多少个链上的Gadget方法。这有助于在初始阶段探索不同路径,避免陷入局部最优。
有个问题,这不是事先都知道当前种子执行轨迹与目标Sink块的距离了,意味着我不是都知道漏洞Sink和Gadget了,还找什么?
对,关键就是找什么
ODDFUZZ不需要事先知道一条“完整且确定可利用”的链。它启动时所需的,是“通过轻量级静态分析推测出的、可能存在缺陷的候选链骨架”。这个骨架是“可能”的路径,但其中充满了缺失的约束和不确定的分支。工具的核心任务,就是通过智能化的模糊测试,动态地、自动化地为这个骨架“填充血肉”并验证其可行性。
也就是说,事先用静态分析已经找出了Gadget(也就是前面说的BFS,我推测是修改了GadgetInspector的BFS为DFS

也就是修改GadgetInspector的BFS的出入栈为,,,等下?你告诉我GadgetInspector是什么遍历???

啥也没改吧你laodx,OK这里暂且不说
然后用ODDFUZZ来验证每个链子,也就是生成POC了。
不过能生成POC也是好事,减少了人工。
这里介绍一下文中提到的一个方法:JQF
JQF
JQF 的核心是为 Java 程序提供结构感知、覆盖引导的模糊测试。
- 不是“瞎变异”:与 AFL 等直接变异字节的模糊测试器不同,JQF 强调语义模糊测试。它通过编写的
Generator(生成器) 来产生语法有效的输入(如合法的XML、JavaScript代码)。 - 覆盖引导(Zest):JQF 的默认算法 Zest 会监控每次测试的代码覆盖率。它会优先保存和变异那些覆盖了新代码分支的输入,从而引导测试向程序更深的逻辑探索。
- 属性测试框架:只需像写单元测试一样,定义一个带有
@Fuzz注解的方法,JQF 会自动为该方法参数生成值并反复运行它。
以 README 中的 PatriciaTrieTest 为例
1 | // 1. 指定使用JQF运行器 |
assumeTrue(...):如果条件不满足,JQF 会静默丢弃当前输入,不视为失败。这用于引导工具生成更相关的数据。assertTrue(...):如果断言失败,JQF 会报告一个漏洞,并保存导致失败的输入。
运行模糊测试:
在项目根目录(包含 pom.xml)下执行:
1 | mvn jqf:fuzz -Dclass=PatriciaTrieTest -Dmethod=testMap2Trie |
- JQF Maven 插件会启动模糊测试进程。
- 控制台会实时显示执行次数、覆盖率、发现的新路径等信息。
- 当断言失败(找到 bug)时,进程停止,并会在
fuzz-results目录下生成导致失败的测试用例文件。
重现崩溃:找到 bug 后,可以使用以下命令确定性地重现它:
1 | mvn jqf:repro -Dclass=PatriciaTrieTest -Dmethod=testMap2Trie -Dinput=fuzz-results/.../id_000000 |
📝 构建您自己的测试驱动
对于更复杂的、需要自定义输入结构的场景(这正是ODDFUZZ所做的),需要:
第一步:设置 Maven 依赖
在 pom.xml 中添加 JQF 依赖和插件。
1 | <dependency> |
第二步:编写自定义生成器 (Generator)
当内置的随机生成器不满足需求时(例如需要生成特定的Java对象链),需要实现 Generator<T> 接口。
1 | public class MyComplexObjectGenerator implements Generator<MyComplexObject> { |
第三步:在测试中使用自定义生成器
使用 @From 注解指定使用哪个生成器。
1 |
|
第四步:运行与分析
- 运行:同样使用
mvn jqf:fuzz命令。 - 分析结果:
- 关注覆盖率增长:覆盖率不再增长时,可能意味着测试已充分。
- 检查
fuzz-results/目录:里面保存了独特的、触发新路径的测试用例。 - 使用
mvn jqf:repro来调试任何发现的失败。
🔧 其他运行方式与高级用法
- **命令行直接运行 (Zest CLI)**:如果您已将测试打包成 JAR,可以直接用
jqf-zest命令行工具运行,适合集成到 CI/CD。 - **使用不同的引导算法 (Guidance)**:通过
-Dengine参数可以切换不同的模糊测试引擎,例如 AFL 模式(适合二进制数据)。 - 集成到 IDE:您可以直接在 IDEA 或 Eclipse 中像运行普通 JUnit 测试一样运行
@Fuzz测试,但这通常是用于调试,真正的模糊测试需要在命令行进行长时间运行。
GPT写的一个用JQF Fuzz CC链(我还没测试):
需要实现 Generator<Object> 接口,其 generate 方法负责组装CC链
1 | public class CCChainGenerator implements Generator<Object> { |
编写JQF测试驱动 (CCDeserializationTest)
测试类负责接收生成器构造的对象,并尝试触发反序列化。
1 |
|
1 |
|
运行与调优
1 | mvn jqf:fuzz -Dclass=CCDeserializationTest -Dmethod=testDeserializeWithMonitor |
调优参数:
-Dtime=300:设置模糊测试时间(秒)。-Dtrials=100000:设置最大测试次数。- 观察覆盖率增长和独特路径数,它们是衡量探索效率的关键指标。
相当于JQF就是一个监测Gadget是否成功执行的东西,测试用例这一块
至于Fuzz的逻辑就写在switch(chainType)的函数里面,容易理解。
如果有科研后期我会写代码复现一下这个算法,如果有。。
我觉得可以创新的点:
- 变异能量不是个好办法,因为一个字符型propagator变异很多次也是原来的东西,而他离sink很近,有多次变异机会也无济于事。离sink的距离其实跟变异机会(我理解的变异机会其实就等同于Gadget中单次对象生成成功率)没有任何联系,相反,如果一个propagator有很多字段,那么才应该增加变异机会
- 从source到sink写payload从来都不是一个好的方案,我每次写payload都是从Runtime,TemplatesImpl开始写,如果一个Gadget的触发点是TemplatesImpl,那用这个工具一辈子也找不到POC,不如把搜集的Gadget看下有没有TemplatesImpl,然后从sink开始写。从sink往上找Gadget用这种变异去生成可用的对象,比从source可用性绝对大得多。
- 既然都是逐步装填写poc了,大量的FUZZ用LLM肯定不行,但是最后POC的可用性和微小语法错误(变异生成的对象大概率有语法错误)可以用LLM检查top-k遍。
如果有人做了给我说一声别让我白费功夫