模糊测试开发4:快照、代码覆盖率与模糊测试
2024年6月23日
背景
这是系列博客文章的下一篇,详细介绍了开发基于Bochs作为目标执行引擎的快照模糊测试器的过程。您可以在Lucid代码库中找到该模糊测试器和代码。
引言
之前,我们实现了足够的Linux仿真逻辑,使Lucid能够运行静态PIE版本的Bochs直到其启动菜单。在过去的几个月里,我们取得了很大进展。现在我们已经实现了快照、代码覆盖率反馈和更多Linux仿真逻辑,以至于我们实际上可以开始模糊测试了!因此,在本文中,我们将回顾代码库中添加的一些主要功能,以及如何设置模糊测试器进行模糊测试的示例。
快照
该模糊测试器设计的一个关键优势(感谢Brandon Falk)是模拟/目标系统的整个状态完全由Bochs封装。这里的吸引力在于,如果我们能够可靠地记录和重置Bochs的状态,我们就默认获得了目标快照。将来,当我们的目标影响设备状态时,这将对我们有益,比如模糊测试网络服务。所以现在我们的问题变成了,在Linux上,如何完美地记录和重置进程的状态?
我想出的解决方案我认为非常美观。我们需要重置Bochs中的以下状态:
- Bochs镜像本身中的任何可写PT_LOAD内存段
- Bochs的文件表
- Bochs的动态内存,如堆分配
- Bochs的扩展CPU状态:AVX寄存器、浮点单元等
- Bochs的寄存器
首先,动态内存应该很容易记录,因为我们在系统调用仿真代码中自己处理所有对mmap的调用。这样我们可以很容易地快照MMU状态。这也适用于文件表,因为我们以相同的方式控制所有文件I/O。不过,目前我还没有实现文件表快照,因为在我用于开发的模糊测试工具中,Bochs不接触任何文件。我暂时采用的方法是,如果在模糊测试过程中文件被接触,就将它们标记为脏,并在此时panic。以后,我们应该能够以处理MMU相同的方式处理文件快照。
扩展CPU状态可以用机器指令保存
但对我来说,一个悬而未决的问题是弄清楚如何记录和重置PT_LOAD段。在Linux用户空间,我们无法很好地跟踪这些页面的脏状态,因为它们会在本地发生。如果您想差异性地恢复这些页面,模糊测试领域有一些常见方法:
- 将这些页面标记为不可写,并为每个页面处理写访问故障。这种方法将让您知道Bochs是否使用了可写页面。一旦处理了故障,您可以将页面永久标记为可写,然后在每次模糊测试迭代中惰性重置它。
- 使用为检查点恢复工作暴露的一些实用程序,如d0c s4vage讨论的/proc中的内容。
但最终,为了简单起见,我决定每次重置所有可写段。
真正的问题在于,Bochs的动态内存分配可能非常巨大,因为它会分配堆内存来保存模拟的客户机内存(您的目标系统)。因此,如果您配置一个具有2GB RAM的客户VM,Bochs将尝试进行2GB的堆分配。这使得捕获和恢复快照非常昂贵,因为每次模糊测试迭代进行2GB的memcpy将非常耗费资源。所以我需要一种避免这种情况的方法。Bochs确实有内存访问钩子,因此我可以通过这种方式跟踪客户机中的脏内存。如果我们发现当前实现成为性能瓶颈,这可能是未来的实现。
与我目前对Lucid的项目理念一致,即我们愿意为了内省或架构/实现简单性而牺牲性能。我决定,鉴于我们是映射Bochs到内存的人而不是内核,我们可以利用一个很好的解决方案。只要ELF镜像可加载段的顺序使得可写段最后加载,这意味着我们开始需要重置的内存块。此时,您可以这样思考内存中的映射:
|
|
这对我们来说很好,因为我们现在实际上有一个需要每次模糊测试迭代恢复的连续可写内存块的起始点。Bochs将影响的其他可变内存,我们关心快照的,可以任意映射,让我们思考一下:
- Bochs的扩展状态保存区域:是的,我们控制其映射位置,我们可以使用mmap和MAP_FIXED将其映射到最后一个可写ELF段旁边。现在我们的连续块也包含了扩展状态。
- MMU动态内存(Brk,Mmap):是的,我们控制这个,因为我们预分配动态内存,然后使用这些系统调用作为基本上