设计SockFuzzer,XNU网络Syscall Fuzzer

2021-04-23 04:34:36

Warning: Can only detect less than 5000 characters

Warning: Can only detect less than 5000 characters

“单位”通过Syscall层测试内核,听起来像是一个大任务,但如果你放弃了一些复杂性,它比你期望的更容易。我们首先使用原始构建标志从源构建所有单个内核对象文件。但是,而不是将所有内容联系在一起以生成最终内核二进制文件,而是仅在我们的目标攻击表面中包含代码的对象子集中链接。然后,我们存根或伪造其余功能。感谢上一节中的RECH,我们已经知道我们要从模糊的函数调用哪些功能。我使用该信息来准备最小的源对象列表以包含在我们的Userland端口中。

在我们潜入之前,让我们定义项目的整体结构,如下图所示。在C ++中实现了一个模糊目标,它将模糊输入转换为与userlland XNU库的交互。如攻击曲面审查部分中提到的,目标代码LibxNu(libxnu)对Syscalls和IP_INPUT公开了一些包装器符号。模糊目标还将其随机序列的字节序列暴露于内核API,例如CopyIn或Copyout,其实现已被使用模糊输入数据的假货替换。

要使开发更易于管理,我决定使用CMake创建一个新的构建系统,因为它支持Ninja以快速重建。这里的一个缺点是每次更新上游时都必须运行原始构建系统,以处理生成的源,但这是值得的,可以获得更快的开发循环。我在普通内核构建期间捕获了所有编译器调用,并使用那些重建传递的标志来构建各种内核子系统。这就是第一次通行证的样子:

要获得初始的目标模糊工作,我们可以通过与包含所有这些的存根实现的文件进行链接来完成一个简单的技巧。我们在这里利用C的弱型系统。对于我们需要实现的每个功能,我们可以链接实现void func(){sensert(false); }。传递给函数的参数只是忽略,每当目标代码尝试调用它时会发生崩溃。通过链接器标志可以实现此目标,但这是一个简单的解决方案,使我能够在击中未实现的功能时获得良好的回溯。

然后,我们只需将此文件链接到我们构建的XNU库中,通过将其添加到源列表:

如您所见,XNU库中包含一些其他文件,它表示伪造的实现和辅助代码以公开一些内部内核API。要确保我们的模糊目标将在链接库中调用代码,而不是带有冲突名称的其他主机函数(syscalls),我们默认隐藏libxnu中的所有符号,然后公开一组调用这些功能的包装器代表我们。我默认使用cmake设置set_target_Properties隐藏所有名称(XNU属性C_Visibility_Preset)。然后我们可以链接在文件(Fuzz / Syscall_Wrappers.c)中包含以下内容的包装器:

注意从库中显式导出符号的可见性属性。由于这些包装器的简单性,我创建了一个脚本,以使用syscalls.master自动化这个名为generate_fuzzer.py。

通过存根到位,我们现在可以开始编写模糊目标并回来处理以后实施它们。每次目标代码都尝试使用我们最初遗漏的功能之一时,我们会看到崩溃。然后我们决定包括真实的实现(并且可能递归地需要更多的存根函数实现)或伪造功能。

获得构建与CMake合作的奖金是创建具有不同仪器的多个目标。这样做允许我使用clang-coverage生成覆盖报告:

通过,我们只需添加模糊目标文件和Protobuf文件即可与Protobuf-Mutator一起使用,我们已准备好开始:

此时,我们已经将一大块XNU集成到了一个方便的库中,但我们仍然需要通过编写模糊目标来与之交互。起初,我以为我可能会为不同的功能编写许多目标,但我决定为这个项目编写一个单片目标。我相信细粒度的目标可以做出更好的功能,以更好地努力模糊,例如TCP状态机,但我们将坚持为简单性。

我们首先使用Protobuf指定输入语法,其中部分如下所示。该语法是完全任意的,将由我们将写入的相应的C ++线束使用。 libfuzzer有一个名为libprotobuf-mutator的插件,知道如何突变protobuf消息。这将使我们能够有效地进行基于语法的突变模糊,同时仍然利用覆盖范围的反馈。这是一个非常强大的组合。

我留下了一些待命的评论,所以你可以看到语法如何始终得到改善。正如我在类似的模糊项目中所做的那样,我有一个名为Seass会话的顶级消息,封装单个模糊迭代或测试用例。此会话包含一系列“命令”和可以在随机时使用的一个字节序列(例如,在执行CopyIn时)。命令是Syscalls或随机数据包,其又是他们自己的消息,具有相关数据。例如,我们可能有一个会话,该会话具有包含“套接字”消息的单个命令消息。套接字消息具有与syscall的每个参数相关联的数据。在我们的C ++基础目标中,我们的工作将此自定义规范的消息转换为真实的Syscalls和相关API调用。我们通知Libprotobuf-Mutator,即我们的模糊目标期望一次通过宏定义_binary_proto_fuzzer获取一个“会话”消息。

虽然Syscalls通常是Protobuf消息的直接翻译,但其他命令更复杂。为了改善随机生成的数据包的结构,我添加了自定义消息类型,然后我在将相关的导线结构转换为IP_Input之前转换为相关的导线结构。这是它如何寻找TCP:

不幸的是,Protobuf不支持UINT8类型,因此我必须为某些字段使用UINT32。这是一些失去的模糊性能。您还可以查看一些我添加的合成TCP标头标志,以使某些标志组合更有可能:is_pure_syn和is_pure_ack。现在我必须编写一些代码来将有效数据包缝合在这些嵌套字段中。如下所示,代码只是处理TCP标题。

如您所见,我制作自由主义使用自定义语法以实现更好的质量模糊。这些努力是值得的,因为随机化高级结构更有效。我们更容易稍后解释崩溃的测试用例,因为它们将具有相同的高级表示。

现在我们有代码构建和初始模糊目标运行,我们开始第一次通过实现我们的模糊目标可以访问的所有存根代码。因为我们有一个构建和运行的模糊目标,所以我们现在获得瞬间反馈关于我们的目标命中的功能。必须在找到任何错误之前支持某些核心功能,因此首次运行模糊的尝试值得自己的开发阶段。例如,直到支持动态内存分配,几乎没有内核代码,我们尝试覆盖会考虑如何使用这些代码是多么的代码。

我们将使用尝试具有相同语义的虚假变体实现我们的存根函数。例如,当测试使用外部数据库库的代码时,您可以用简单的内存实现替换数据库。如果您不关心查找数据库错误,这通常会使模糊更简单,更强大。对于与网络无关的某些内核子系统,我们可以完全使用不同或无效的实现。此过程让人想起高级仿真,是游戏机仿真中使用的想法。而不是旨在模拟硬件,您可以尝试保留语义,但使用API​​的自定义实现。因为我们只关心测试网络,这就是我们如何在该项目中接近假设子系统。

我总是首先查看原始功能实现。如果是可能的话,我也只是在该代码中链接。但有些功能与我们的模糊不兼容,必须伪造。例如,由于虚拟内存已由我们的主机内核管理已经管理,因此Zalloc应呼叫userland malloc,并且我们提供分配器设施。同样,CopyIn和Copyout需要伪造,因为它们不再用于复制用户和内核页面之间的数据。有时我们也只是“nop”我们不关心的功能。我们将覆盖

......