20199137 《网络攻防实践》综合实践

DIFUZE: Interface aware fuzzing for kernel drivers

1.摘要

在类似于Unix的现代系统中,设备驱动程序是必不可少的部分,用于处理物理设备上的操作,从硬盘和打印机到数码相机和蓝牙扬声器。新硬件的涌现,特别是在移动设备上,激增了系统内核中设备驱动程序的爆炸性增长。第三方开发人员提供了许多此类驱动程序,这些驱动程序容易受到安全漏洞的影响,并且缺乏适当的审查。不幸的是,用于设备驱动程序的复杂输入数据结构使传统的分析工具(如模糊测试)效率降低,到目前为止,有关内核驱动程序安全性的研究还相对较少。
论文引入了DIFUZE,这是一种界面感知的模糊工具,可自动生成有效的输入并触发内核驱动程序的执行。论文利用静态分析来构成用户空间中结构正确的输入,以探索内核驱动程序。DIFUZE是全自动的,范围从识别驱动程序处理程序到映射到设备文件名,再到构造复杂的参数实例。在七种现代Android智能手机上评估了论文的方法。结果表明,DIFUZE可以有效地识别内核驱动程序错误,并报告32个以前未知的漏洞,包括导致任意代码执行的漏洞。

2.整体工作

在本文中,我们介绍了DIFUZE,这是一种技术的新颖组合,可实现界面感知的模糊测试,并促进对设备驱动程序提供的ioctl接口的动态探索。DIFUZE对内核驱动程序代码执行自动静态分析,以恢复其特定的ioctl接口,包括有效命令和关联的数据结构。它使用此恢复的接口来生成ioctl调用的输入,这些输入可以从用户空间程序分派到内核。这些输入与驱动程序使用的命令和结构相匹配,从而可以对ioctl进行高效,更深入的探讨。恢复的接口允许模糊器在更改数据时做出有意义的选择:即,不应将像指针,枚举和整数之类的类型化字段简单地视为字节序列来处理。DIFUZE强调了有关驾驶员所做的假设,并暴露出严重的安全漏洞。
在我们的实验中,我们分析了7种现代移动设备,发现了36个漏洞,其中32个以前未知(在实验过程中修补了4个由DIFUZE发现的漏洞),其严重程度从造成问题的设备崩溃导致拒绝的缺陷到严重服务(DoS)中存在的错误,这些错误可以使攻击者完全控制电话。

3.模糊测试

模糊测试的主要前景是生成“最有效”的输入以执行目标程序,行使广泛的功能并触发导致漏洞的极端情况。动态污染追踪是一种广泛使用的产生潜在输入的策略。Dowser和BuzzFuzz使用污点跟踪生成更可能触发某些类型漏洞的输入。 但是,对于需要高度约束输入的ioctl函数,这些技术效果较差。存在基于污点分析的Ap方法来恢复底层程序使用的输入格式,但是它们无法恢复值之间的交叉依赖关系,例如,给定特定命令标识符,ioctl处理程序将期望特定类型的进一步论证。
进化技术代表了模糊系统中的另一种常见的输入生成策略。VUzzer和SymFuzz将静态分析与基于突变的进化技术相结合,以有效地产生输入。但是,这些技术在生成高度受限的输入方面无效。DIFUZE通过首先收集可能的ioctl命令值,然后仅对不受约束的值和预期的输入格式进行模糊处理来解决此问题。
如果已知程序的输入格式,则可以通过指定有效输入来增强模糊处理。 Peach 是行业标准工具之一。但是,它无法生成实时数据(即,包含指向其他数据的活动指针的数据),许多设备驱动程序都需要包含指针的输入结构。基于语法的技术已用于模糊文件格式,解释器和编译器,但是这些技术要求输入具有固定格式。

3.1内核和驱动程序模糊不清

模糊操作系统接口或系统调用是测试操作系统内核的一种实用方法。大多数驱动程序使用ioctl函数(一种POSIX标准)与用户空间进行交互。ioctl很复杂,它们需要特定的命令值和用户生成的数据格式。标识有效的命令值及其关联的数据结构是ioctl模糊测试的两个关键问题。已经开发了一些工具来测试Windows内核的ioctl接口,例如iofuzz,ioattack ,ioctlbf 和ioctlfuzzer 。但是,这些工具取决于Windows内核提供的大量日志记录和信息跟踪,以及Windows特有的ioctl命令的格式。而且,许多这些工具本质上都是简单化的。它们只涉及附加到进程并挂接Windows ioctl调用。钩住后,该工具会在调用时更改值,这在多个方面都缺乏,例如该过程可能无法发挥驱动程序的全部功能,并且您不知道传入数据的类型信息。为解决此问题,DIFUZE分析设备驱动程序的源代码以识别有效命令和相应的数据结构。
Trinity 和syzkaller是专为Linux syscall模糊测试开发的。在模糊设备驱动程序的ioctl处理程序时,它们的性能很差。尽管syzkaller使用其他的检测技术(例如Kernel Address Sanitizer )来检测更多的错误,但是这些技术不能直接在供应商的设备上使用,因为它们需要分析人员使用自定义固件重新刷新设备。几种方法集中在模糊特定选择的系统调用和驱动程序上。但是,它们仅专注于特定功能,而不能推广到其他系统调用和驱动程序。DIFUZE是第一个完全自动化的系统,可以概括为对运行未修改内核的设备上的所有Linux内核驱动程序进行模糊处理。

3.2符号执行

符号执行是一种使用符号变量来生成约束输入并满足复杂检查的技术。DART ,SAGE,Fuzzgrind 和Driller结合了符号执行和随机测试来增加代码覆盖率。BORG 使用符号执行来生成更有可能触发缓冲区溢出的输入。在原始设备上执行符号执行的工程问题和基本路径爆炸问题(由于复杂的系统内核而更加糟糕)使这些技术对内核驱动程序不切实际。

3.3静态分析

静态分析是一种流行的技术,它可以查找程序漏洞而无需执行所讨论的程序。
为了使精度最大化,这些技术通常需要源代码来执行分析。由于许多系统内核(包括Linux内核)和设备驱动程序都是开源的,因此内核安全可以从静态分析中大大受益。例如,Ashcraft等。开发了编译器扩展,以捕获从Linux和OpenBSD内核中不受信任的源读取的整数。Post等使用有界模型检查器来查找Linux内核中的死锁和内存泄漏。鲍尔等建立了带有一组规则的静态分析工具,以证明Windows驱动程序的正确性。

4.工作流程

DIFUZE需要输入目标主机的内核源代码(其中将包括设备驱动程序的源代码)作为输入。由于Linux是根据GNU通用公共许可证获得许可的,因此还必须发布与Linux链接的任何软件,例如内核驱动程序接口代码。因此,Android设备的内核资源随时可用,并且可以用于我们的分析。
有了此输入,DIFUZE会经历多个阶段,以恢复设备驱动程序的交互接口,生成正确的结构以使用此接口,并触发目标主机内核对这些结构的处理。由于触发内核错误通常会导致系统不稳定(导致挂起或重新启动),因此只有DIFUZE的最后阶段在目标主机上在体内完成。其他阶段在外部分析主机上执行,其结果记录在本地(用于输入重播,以防触发错误),然后通过网络连接或调试接口传输到目标主机。
更详细地,这些阶段包括:接口恢复。 在第一阶段,DIFUZE将分析提供的源,以检测目标主机上启用了哪些驱动程序,用于与它们进行交互的设备文件,它们可以接收哪些ioctl命令以及它们希望传递给这些对象的结构。这一系列的分析是使用LLVM实现的。该阶段的最终结果是一组用于目标驱动程序,目标ioctl命令和结构类型定义的设备文件名元组。
结构生成。对于每个结构,DIFUZE连续生成结构实例:代表从上一步中恢复的类型信息实例化的内存内容。 这些实例以及关联的目标设备文件名和目标ioctl命令标识符将被记录并传输到目标主机。
设备上执行。实际的ioctl触发组件驻留在目标主机本身上。收到目标设备文件名,目标ioctl命令和生成的结构实例后,执行程序将继续触发ioctl的执行。

为了理解DIFUZE,我们提供了一个简单驱动程序的示例。图1(结构定义),图2(围绕copy_from_user函数的包装,为分析带来了一些小麻烦),图4(主驱动程序初始化代码)和图3(ioctl处理程序本身)中给出了此示例。
图3-5中的函数driver_init是驱动程序初始化函数,它将被称为内核初始化的一部分。此函数使用名称“ example_device”注册设备(第8行),并指定当用户空间应用程序对设备文件(在本例中为/ dev)执行ioctl系统调用(第10和11行)时,应调用ioctl_handler函数。/ example_device)。
尽管文件名是example_device,但是文件的绝对路径取决于设备的类型。正在运行的示例中的设备是字符设备,它将在/ dev目录下创建,但是,还有其他类型的设备文件,它们将在不同的目录中创建。 例如,将在/ proc目录下创建proc设备。
图1 正在运行的示例的结构定义

图2 示例驱动程序附带了包装好的copy_from_user函数

图3 ioctl处理分为多个功能


图4 正在运行的示例的主要驱动程序初始化功能

4.1接口恢复

为了有效地模糊设备驱动程序的图标,DIFUZE需要恢复该驱动程序的接口。 设备驱动程序的接口包括用于与设备通信的设备文件的名称/路径,该设备的ioctl命令的有效值以及不同ioctl命令的ioctl数据参数的结构定义。
为了恢复该数据,DIFUZE使用了在LLVM中实现的分析组合。 由于Linux内核不适合使用LLVM进行分析(甚至编译),因此我们首先开发了另一种构建过程。 完成此操作后,我们确定由设备驱动程序创建的设备文件的文件名,找到ioctl处理程序,恢复有效的ioctl命令标识符集,并检索这些ioctl命令的数据参数的结构定义。

构建系统仪表

我们采取了几个步骤来使DIFUZE能够在Linux设备驱动程序上执行LLVM分析。
GCC编译:首先,我们执行手动步骤,使用GCC设置要编译的目标主机的内核和驱动程序源。 尽管这通常是一个文档完善的过程,但是移动设备的供应商不会竭尽全力使其GPL要求的源代码版本易于编译,因此需要一些手动配置工作。一旦可以使用GCC编译源树,我们将运行完整的编译并记录所有已执行的命令。
从GCC到LLVM的转换:在编译步骤中,我们使用为DIFUZE创建的GCC-to-LLVM命令转换实用程序来处理已执行命令的日志。该实用程序将命令行标志从GCC期望的格式转换为LLVM实用程序期望的格式,并允许通过LLVM编译内核源代码。 在其编译中,LLVM为每个源文件生成一个位码文件。我们使调试信息能够嵌入到位码文件中,这有助于我们提取结构定义。
位码合并:DIFUZE对每个驱动程序分别进行分析。因此,我们合并了各种位代码文件,以每个驱动程序创建一个位代码文件。这使我们可以在单个位码文件上执行接口恢复分析,从而简化了分析。在以下阶段中,将使用此合并的位代码文件执行分析。

ioctl处理程序标识

设备驱动程序的大部分交互都是通过ioctl接口进行的。应用程序从用户空间调用ioctl()系统调用,将文件描述符传递给驱动程序的设备文件,命令标识符和所需的结构化数据参数。在内核空间中收到此系统调用后,将调用相应的驱动程序的ioctl处理程序。然后,此处理程序根据命令标识符将请求分派到驱动程序内部的其他功能。在我们正在运行的示例中,ioctl处理程序函数为ioctl_handler。
为了恢复有效的命令标识符和其他ioctl参数的结构定义,DIFUZE必须首先标识顶级ioctl处理程序。 每个驱动程序都可以为其每个设备文件注册一个顶级ioctl处理程序,并且在Linux内核中有几种方法可以执行此操作。 但是,所有这些方法都涉及为此目的而创建的一组结构2的创建,其中这些结构的字段之一是指向ioctl处理程序的功能指针。
我们识别ioctl处理程序的分析非常简单:使用LLVM的分析功能,我们可以在驱动程序中找到所有这些结构的所有用法,并恢复ioctl处理程序函数指针的赋值。 在我们的运行示例中,我们确定对file_operations结构(图3-5,第9行)的unlocked_ioctl字段的写操作。然后,我们可以将函数ioctl_handler视为ioctl处理程序。

4.2设备文件检测

为了确定与ioctl处理程序相对应的设备文件,我们需要标识ioctl处理程序的注册中提供的名称(例如,在我们正在运行的示例中,该设备文件为/ dev / example_device,图4第7行))。
根据设备的类型,有几种方法可以在Linux内核中注册文件名。例如,字符设备的注册将使用alloc_chrdev_region方法将名称与设备相关联。对于proc设备,使用proc_create方法提供文件名。 此外,根据设备类型,在其中找到设备文件的目录可能会有所不同。
给定一个ioctl处理程序,我们使用以下过程来标识相应的设备名称。
(1)首先,我们搜索任何将地址存储到附录A中列出的任何操作结构的字段之一的LLVM存储指令。
(2)然后,我们检查任何注册功能中是否有对操作结构的引用。
(3)我们分析设备文件名的参数值,如果它是常量,则将其返回。
在图4的示例运行时,我们先前确定ioctl处理函数是ioctl_handler。 我们在第9行(步骤1)中确定ioctl_handler存储在file_operations结构中(即driver_ops),然后在第10行中检查driver_ops的用法作为函数cdev_init的参数(步骤2)。 函数cdev_add表示该设备是字符设备。在第7行,我们回溯到设备元数据(alloc_chrdrv_region)的分配函数,该函数的第三个参数是设备名称,将其检测为常量字符串,然后将/ dev / example_device返回为设备名称。驱动程序可以使用动态创建的文件名,如图5所示。
图5 在手机上动态生成的设备名称

4.3参数类型识别

ioctl命令标识符和相应的数据结构定义具有多对多关系:每个ioctl命令可以采用几种不同的结构(例如,基于全局配置),并且每个命令结构都可以传递给多个 ioctl命令。为了找到这些结构,我们首先确定所有copy_from_user函数的路径,Linux内核使用该函数将数据从用户空间复制到内核空间,如图3-4中的第16行→清单2中的第3行。网站的源操作数(即copy_from_user的第二个参数)不是传递给ioctl函数的参数,因为这种情况无法帮助我们确定ioctl参数的类型。 在其余的每个呼叫位置,我们找到源操作数的类型。
这是ioctl处理程序的用户数据参数必须遵循的类型定义。
请注意,指针转换可能会隐藏实际的结构类型。
考虑正在运行的示例,其中可以从ioctl处理程序访问图2-3中第3行的copy_from_user,从图3-4中的ioctl_handler可以从多个路径(如第16行,第21行和第32行→第41行)访问。 但是,调用站点中源操作数的实际类型为void *。另外,copy_from_user函数可能驻留在包装函数中,并由ioctl函数间接调用(例如,图3-4中的第16行→清单2中的第3行),该函数分布在不同的函数或文件中。
为了解决这个问题,我们执行过程间,路径敏感的类型传播,以确定在每个路径中可能分配给copy_to_user函数的源操作数的所有可能的类型。对于给定的路径,这为我们提供了ioctl处理程序的用户数据参数的可能类型集。
为了将命令标识符与这些结构类型中的每一个相关联,我们还在执行类型传播时沿路径收集了相等约束(如第4.4节中所述)。 在到达copy_from_user函数的路径上对命令值的约束表示与结构类型关联的可能的命令标识。
对于图3-4中正在运行的示例,我们首先确定到达copy_from_user调用站点的所有路径(请注意,实际的调用通过包装函数copy_from_user_wrapper进行)。
为简便起见,我们忽略了对cmd具有相同约束并到达相同呼叫站点的路径。我们还忽略了路径6,因为源操作数不是用户参数(即图3-4中的第49行,copy_from_user_wrapper的第二个参数不是argp)。最后,对于其余路径,我们确定目标copy_from_user调用站点的目标操作数的类型,以确定命令值的类型。例如,对于表1中的路径1,argp的类型与3中第16行的目标操作数curr_idx相同,在第6行将其定义为uint32_t。对于每个命令值,我们可以得到多种类型。例如,如表1所示,路径1和路径2具有相同的cmd约束值,但参数类型不同。
对于每个命令值,我们关联所有可能的参数类型。例如,从表1中可以将命令值0x1003与参数类型uint32_t和uint8_t关联。接下来,我们需要提取参数的结构定义。
我们使用从GCC到LLVM的管道生成相应的预处理文件。 由于预处理文件应包含所有必需类型的定义,因此我们找到了已标识类型的定义。然后,我们运行c2xml工具将C结构定义解析为XML格式,然后从中提取所需的类型定义。
表1 ioctl处理程序到copy_from_user调用站点的相关路径

4.4接口提取评估

接口提取的所有步骤都在同一实验平台上运行,该平台是一台运行Ubuntu 16.04.2 LTS的Intel Xeon CPU E5-2690(3.00 GHz)。平均而言,完成一个内核的整个接口提取阶段需要55.74分钟。
DIFUZE在七个内核中识别出总共789个ioctl处理程序设备。处理程序的数量也与相应电话上的驾驶员数量非常接近。
设备名称标识:我们用于设备名称标识的方法能够在不同的供应商特定设备上工作。DIFUZE可以自动识别469个设备名称,占ioctl处理程序的59.44%。 大多数识别失败都来自内核主线驱动程序。例如,仅在Xperia XA的供应商驱动程序上进行的名称恢复就可以恢复90%以上的名称。这种差异的原因是,主线驱动程序倾向于使用动态生成的名称,而供应商驱动程序倾向于使用静态名称。我们手动提取了那些动态创建的设备名称。
有效命令标识符:DIFUZE总共在所有内核的所有驱动程序中找到3,565个有效命令标识符。有效命令标识符的数量在不同内核之间适当地变化。模糊器发现的崩溃次数与有效命令标识符的数量呈正相关。
图6显示了每个ioctl处理程序的有效命令标识符数量的分布。11%的ioctl处理程序不期望任何命令 这些ioctl的代码是有条件的由内核配置编译和保护。 在我们的编译过程中,ioctl处理程序代码被禁用,因此相应的ioctl处理程序在生成的位代码文件中显示为空,这在我们的命令识别过程中导致零命令值。有50%的ioctl处理程序期望单个命令标识符。其中大多数归因于v4l2_ioctl_ops。它们是嵌套的处理程序,用于管理(单个)特定命令。大多数ioctl处理程序(98.3%)的有效命令标识符少于20个。我们手动研究了剩余的ioctl(1.7%)以及超过20个命令标识符,发现我们的方法近似了某些ioctl函数的函数指针。尽管这种高估会在我们随后的模糊处理步骤中导致额外的无效模糊处理单位,但对整体性能产生边际影响(尤其是考虑到此类情况所占的百分比很小)。
图6 ioctl处理程序相对于有效命令标识符的数量的CDF

5.结果

我们收集了所有崩溃日志和系统调用的崩溃序列,对其进行了手动分类,并过滤掉了少量重复项。总体而言,DIFUZE能够在用于测试的七台Android设备中找到36个独特的错误。表5列出了已发现的错误。
我们无法让syzkaller在Galaxy S6上运行,并且DIFUZEm无法触发其上的任何错误,这使其成为唯一发现零个错误的Android设备。在所有其他设备上,我们发现从Xperia XA中的两个漏洞(在Honor 8中)到十四个漏洞。
syzkaller的基本配置(没有接口信息)无法在我们的测试中找到任何错误。 给它正确的驱动程序路径(syzkaller + path),只会在所有设备上造成3次崩溃。 这表明盲目模糊的内核驱动程序不是很有效,这很可能是因为此类测试是由供应商在这些设备出厂之前进行的。
当我们以提取的ioctl数字的形式添加部分接口信息时,DIFUZEi能够找到22个错误。尽管这本身令人印象深刻,但将剩余的接口信息(ioctl参数结构定义)添加到接口后,发现的错误数量显着增加了54.5%,总计34个错误。此结果显示了感知接口的模糊测试的有效性,并且还显示了恢复的ioctl命令标识符和结构信息对于ioctl处理程序的分析的重要性。
我们的实验中一个特别有趣的结果是,DIFUZEm仅比DIFUZEs少发现了四个错误。Syzkaller是最先进的工具,内置了许多模糊测试策略和优化功能,而MangoFuzz是一种简单的模糊测试原型类型。我们相信这表明使用准确的界面信息进行模糊测试非常有力。我们对每个崩溃进行了简要的分类,并快速对设备崩溃的原因进行了分类。这些结果示于下图。

即使当崩溃本身看起来是良性的时,这些通常也是严重的错误。例如,一个断言错误可能是由一个更严重的潜在错误触发的,恶意用户可以精心制作以获取更强大的原语。除此之外,发现的更有趣的错误之一是我们可以绕过遇到的大多数断言。绕过这些检查的能力使许多可能会被挫败的情况变为现实。
为了证明结果的严重性,我们利用任意写入漏洞之一在内核中获取了代码执行权,并将其从应用程序级特权升级为根用户。我们目前正在以负责任的方式向供应商披露漏洞。这样做时,我们发现在实验过程中修补了四个错误。据我们所知,其余36个bug中有32个是0天。

6.结论

我们提出了接口感知模糊测试,以提高对接口敏感代码(例如Linux内核驱动程序)的自动分析的有效性。我们提供了一组技术来恢复ioctl接口规范以对此类代码进行模糊处理。
我们将所有技术实现在一个自动化管道中,该管道可以通过一个命令直接在内核源归档中工作。我们表明,对于大多数驱动程序,我们的技术可以有效地恢复组件,设备文件名,有效的命令标识符以及相应的接口参数类型。我们在7种型号的Android手机上使用DIFUZE的几种不同配置进行了全面的评估,以证明我们实现的界面感知模糊器是有效的,发现了36个错误,其中32个是以前未知的漏洞。

7.全课总结与建议

网络攻防这门课对我来说是一个全新的领域,因为本科从未接触过这方面的知识并且甚至自己匮乏的代码知识是一个致命的弱点。从假期作业开始,第一次接触虚拟机,第一次配置网络环境,只记得那个时候我坐在电脑面前崩溃的进行安装。第一周上课的时候特别开心,因为听老师讲解感觉这门课可有意思了,做的社会工程实践也有趣。到了第二周,要开始配置环境,当时我以为把虚拟机全部下载下来之后添加就行,根本没有去管IP的事情,后来,看了大佬们提交的博客,发现和自己理解的完全不是一回事,当时自己又不是很懂原理,只能安装大佬们的步骤一步步来,后来老师让画了原理图之后才渐渐的理解弄懂,第一次两个虚拟机之间ping通的时候我还激动的发了一条微博,感觉这对我这个小白来说是巨大的进步。现在想想哈哈哈哈,幼稚,更难的还在后面等你!再到后面,其实可以发现每次的作业都是用几个口令,下载几个工具就可以弄出来,但是老师每节课上课的提问让我意识到,光做出来还不够,还要弄懂其中的原理,他为什么能攻击这个漏洞,不能一味的追求按照步骤做出来了。我们接触了wireshark这个神奇的工具,让我也对网络嗅探有了初步的了解。了解了一个个windows或者linux的漏洞,对他们实施相应的攻击。掌握一些防范技术,交接最基本的攻防知识。最后一节实践课的时候,看着孙启龙那一顿行云流水的操作,深深的佩服!原来这才是攻防该有的思维和意识与操作。争取在今后的日子努力提升自己。我觉得网络攻防这门课让我可以在电脑前做一天一动也不动,但是当你运行出相应的结果时是真的感觉很开心!这一个学期下来,也确实学到了很多知识,可以说收获颇丰!
整体课堂安排非常合理,就是感觉越到后面综合性越强,实践起来很有难度,希望老师可以在做实践之前可以给点点拨,介绍一下实验思路,感觉能少走不少的弯路。

8.代码复现

运行在ubuntu环境中
首先我们要先配置docker环境

然后运行

然后我们构建架构

原文地址:https://www.cnblogs.com/xiaoming319/p/13195241.html