KernelGPT: Enhanced Kernel Fuzzing via Large Language Models
- 1.Introduction
- 2.Background
- 2.1.Kernel and Device Drivers
- 2.2.Kernel Fuzzing
- 2.2.1.Syzkaller规约
- 2.2.2.规约生成
- 3.Approach
- 3.1.Driver Detection
- 3.2.Specification Generation
- 3.2.1.Command Value
- 3.2.2.Argument Type
- 3.2.3.Type Definition
- 3.3.Specification Validation and Repair
- 4.Implementation
- 5.Evaluation
- 5.1.Overall Results
- 5.2.Comparison with Baselines
- 5.3.Kernel Bug Detection
- 6.Conclusion
- 7.参考文献
1.Introduction
kernel fuzzing是检测内核漏洞(crash、缓冲区溢出写等)的常用手段,kernel fuzzing通常生成大量的system call作为test input。
Syzkaller是目前最受欢迎的一款kernel fuzzing工具。许多工作从种子生成、种子选择、引导变异和系统调用规约生成4个方面改进Syzkaller。其中使用syzlang编写的系统调用规约成为其中最关键的组件之一,对Syzkaller的有效性做出了重大贡献,使其能够涵盖更多的内核模块。这些规约指定了系统调用的类型及其相互依赖关系,从而使得能够生成更多有效的系统调用序列,深入探索内核代码逻辑。
然而,手写系统调用规约需要对内核有深入了解。为解决这个问题,近年来的一些研究专著于自动化生成系统调用规约,特别是针对设备驱动程序的系统调用规约生成。
DIFUSE和SyzDescribe利用静态代码分析来识别设备驱动程序系统调用处理程序并推断其相应的规约,下图展示了基于静态分析技术的规约生成工具的工作流程。
-
最初,专家们借助于他们对内核代码库和现有Syzkaller规约示例的理解手动定义规则,将设备驱动程序源代码转换为规约。
-
然后,这些规则硬编码到静态分析工具中,这是一个挑战性较大且耗时的过程。生成的系统调用规约的准确性和有效性严重依赖于这些映射规则的全面性。而且,随着内核代码库的演变,这些映射规则经常发生变化。
以图2a和图2b为例,这两图展示了与设备映射驱动程序相关的两个结构体变量的源代码,该驱动程序负责将物理块设备映射到更高级别的虚拟块设备。
具体而言,变量 _ctl_fops
和 _dm_misc
是设备操作处理程序及其引用,对于推断设备名称至关重要。当前的系统调用规约生成器,如SyzDescribe,通常依赖于结构体 miscdevice
中的 .name
字段来确定用于驱动程序交互的设备名称,这是一种常规用例。
然而,在这个例子中,正确的设备名称实际上是在 .nodename
字段中指定的,这是一种合法但罕见的用例,导致SyzDescribe错误地推断。此外,SyzDescribe还无法准确分析 ioctl
的命令值,这是与设备交互的系统调用接口。这是因为命令值在代码中被修改过 —— cmd = _IOC_NR(command)
——其中 command
是用户提供的命令值。这样的情况在SyzDescribe中没有考虑到,导致其在生成的规约中错误地将 cmd
而不是用户提供的 command
用作命令值,如图2c所示。
为此,作者提出了基于LLM的规约推断方法KernelGPT,Kernel GPT包含下面几个步骤:
-
1.使用LLM根据设备操作处理程序代码及其引用来推断设备名称及生成其初始化规约。
-
2.为了恢复驱动程序的命令值、参数类型和类型定义,KernelGPT迭代地应用LLM来分析相关的源代码。
-
3.最后,KernelGPT通过询问LLM来处理遇到的错误消息来验证和修复生成的规约。
2.Background
2.1.Kernel and Device Drivers
1.内核
内核是操作系统的核心,为用户空间应用程序提供关键功能,包括虚拟内存、文件系统、网络和设备访问。为了保障所有应用程序和用户的安全,用户空间与内核之间的交互受限于明确定义的系统调用(syscall)接口,如POSIX标准。通过syscalls由用户空间应用程序触发的内核漏洞和崩溃具有重大风险,因为它们影响所有使用内核的应用程序,攻击可能绕过内核执行的所有安全策略。因此,通过syscall接口检测内核漏洞至关重要。
2.设备驱动程序
在Linux中,设备被抽象为文件,通常位于 /dev
目录中,并且可以通过相同的syscall机制访问。设备驱动程序在初始化时向内核注册其syscall处理程序。然后,内核将从用户空间分发syscalls到相应的驱动程序处理程序。
在图2中的示例中,驱动程序首先构建了 struct file_operations
类型的变量 _ctl_fops
(图2a),将syscalls映射到特定的驱动程序处理程序,如 .open
和 .unlocked_ioctl
指向的函数。然后,驱动程序创建了 struct miscdevice
类型的变量 dm_misc
,将 struct file_operations
整合,并在 .nodename
字段中指定设备文件。最后,驱动程序调用 misc_register(&_dm_misc)
向内核注册。当应用程序试图打开由 .nodename
表示的文件(如 /dev/mapper/control
),内核会调用注册的 dm_open
函数,并将其返回的文件描述符(fd
)与设备映射驱动程序关联。以后,内核将使用 fd
分派到相应的注册处理程序,例如 ioctl(fd,...)
将触发 dm_ctl_ioctl
。
驱动开发者通常在处理程序中实现标准控制逻辑,如 .open
、.close
和 .release
。然而,许多驱动程序还需要独特的控制逻辑,这在syscall接口中没有相似的对应,例如,设备映射驱动程序需要一个操作来获取所有 dm
设备名称的列表(图2d)。对于这样的驱动程序特定操作,开发者通常使用通用的 ioctl(int fd, unsigned long request, void *argp)
syscall作为调度程序来调用相应的驱动程序函数。
-
第一个参数
fd
是设备文件的打开文件描述符。 -
第二个参数
request
用作命令标识符以选择要执行的操作。 -
第三个参数
argp
被转换为驱动程序特定的数据类型,以在内外传递信息。例如,要获取dm
设备名称列表,应用程序首先构造驱动程序特定的结构struct dm_ioctl data = ...;
,然后通过int fd = open("/dev/mapper/control");
打开设备文件。然后,通过ioctl(fd, DM_LIST_DEVICES, &data);
可以检索设备名称。由于每个驱动程序可能需要与特定命令标识符相关联的唯一数据类型,这种专门的使用将ioctl
转化为成千上万个syscalls,其接口在很大程度上没有得到很好的文档化或标准化。
2.2.Kernel Fuzzing
模糊测试(fuzzing)是检测内核漏洞最有效的技术之一。kernel fuzzers生成syscalls并在目标内核上执行,通常启用sanitizers,直到发生崩溃。Syzkaller是基于覆盖引导的内核fuzzer,已发现并修复了数千个内核漏洞。Syzkaller使用领域特定语言syzlang定义syscall规约,引导测试用例生成,使其能够深入内核的代码路径。
2.2.1.Syzkaller规约
允许在定义syscall时考虑参数的类型和依赖关系,允许为同一个syscall定义多个实例,每个实例可以具有具体的参数值。再以 ioctl
为例,无类型信息的指针的具体类型取决于命令标识符的值,而这又取决于前一次 open
调用使用的具体文件名。这使得syzkaller能够灵活处理依赖于先前调用结果的情况,如 ioctl
中底层类型依赖于命令标识符的值。
图3展示了MSM驱动程序的三个syscall的规约(为简化起见,某些参数和名称已被省略或缩短)。syscall openat$msm
是syscall openat的一个实例,其中msm是一个自定义但唯一的名称,用于区分syscall实例。该规约为 openat$msm
定义了具体的参数值 "/dev/msm"
— MSM设备的名称。对于Syzkaller可以动态创建的参数,规约概述了它们的类型,例如 ioctl$NEW
和 ioctl$CLOSE
的arg参数分别是指向结构体 drm_msm_submitqueue
和整数 msm_submitqueue_id
的指针。
在syzlang中,有一种特殊类型 resource
,用于表示syscalls之间的依赖关系。一个资源必须在被其他调用用作输入之前由调用生成。图3中的规约引入了两个资源,fd_msm
代表一个打开的文件描述符,msm_submitqueue_id
代表MSM驱动程序内部使用的队列ID。资源 fd_msm
由 openat$msm
返回,然后作为 ioctl$NEW
和 ioctl$CLOSE
的输入参数使用。因此,Syzkaller只会在openat$msm
之后放置 ioctl$NEW
和 ioctl$CLOSE
。
更细粒度的依赖关系也得到支持,比如在结构体中指定对一个字段的依赖关系。例如,对于 ioctl$NEW
的 arg
参数的 inout
注解表示结构体 drm_msm_submitqueue
既用作输入又用作输出。在 drm_msm_submitqueue
中,字段 msm_submitqueue_id
具有out
注解,表示它是输出。由于 ioctl$CLOSE
以 msm_submitqueue_id
为输入,因此只有在 ioctl$NEW
填充了msm_submitqueue_id
之后才能生成 ioctl$CLOSE
。有了这个规约,Syzkaller可以大大缩小搜索空间,并专注于 fuzz drm_msm_submitqueue
的内部字段。
2.2.2.规约生成
尽管它们很有效,规约通常是由Syzkaller和内核开发人员手动编写的,需要开发人员对内核和特定内核模块有深入的专业知识。因此,现有的Syzkaller规约只涵盖一部分syscalls,特定设备驱动程序的规约则相对缺乏。现有的规约也可能在内核演进时变得过时。
自动化规约生成显然是研究热点,但面临一些关键挑战。其中之一是提取预期的参数值和类型定义。另一个是推断syscalls之间的依赖关系并在参数中编码这些依赖关系。未能解决这些挑战将导致不准确的规约,降低模糊测试活动的有效性。
在规约自动生成方面:
-
KSG通过打开现有设备文件并探测内核以找到被访问的数据结构,动态地找到syscall处理程序结构。然后,通过符号执行收集类型和范围信息。
-
DIFUSE和SyzDescribe都对内核源代码进行静态分析,识别常见的实现模式以生成规约。
-
DIFUZE从常见设备注册函数使用的数据结构列表中找到syscall处理程序。
-
SyzDescribe首先找到内核模块的初始化函数,然后追踪到找到syscall处理程序的函数指针。
-
DIFUZE和SyzDescribe都遵循特定的编程模式,以提取设备名称和命令标识符,例如,在处理程序内部的
switch case
可能根据命令值调用相应的子处理程序。现有的规约生成方法主要依赖于在分析工具中硬编码人工总结的模式,以将源代码实现转换为syscall规约。相反,KernelGPT利用LLMs的潜力自动化和改进规约推断规则的学习,从而提高性能。
-
3.Approach
图4为KernelGPT的overview,它利用代码提取器和LLM自动生成驱动程序规约以增强kernel fuzzing。KernelGPT以内核代码库和定位的设备操作处理程序为输入,通过三个自阶段操作:1.Driver Detection(驱动程序检测),2.Specification Generation(规约生成),以及3.Specification Validation and Repair(规约验证和修复)。
-
首先,KernelGPT使用LLMs通过推断设备名称和它们的初始化规约来识别驱动程序。
-
随后,KernelGPT确定描述设备的
ioctl
处理程序的命令值、参数类型和类型定义。(这个应该是test input对应的syscall所需要的具体参数) -
最后,KernelGPT验证生成的规约。如果发现错误,它将尝试用错误信息来引导LLM修复错误。
在解析代码时,作者使用的LLVM的API进行代码解析搜索内核代码,以定位初始化 ioctl
处理程序函数的设备操作处理程序结构的实例。
具体来说,作者搜索操作处理程序结构中的 ioctl
或 unlocked_ioctl
字段的初始化实例。例如,图2a中的设备映射驱动程序通过使用dm_ctl_ioctl
函数初始化 _ctl_fops
结构中的 unlocked_ioctl
字段。作者标记 dm_ctl_ioctl
为 ioctl
处理程序,_ctl_fops
为设备操作处理程序。
KernelGPT的重点是从源代码推断出规约,而不是定位设备操作或 ioctl
处理程序。
3.1.Driver Detection
Driver Detection的主要目的是推测驱动设备名称以及生成初始化操作对应的规约。
图5为Driver Detection用到的prompt示例,包括: 指令(Instruction部分)、操作处理程序的源代码(结构体变量 _ctl_fops
的初始化部分)及其引用(结构体 _dm_misc
的初始化部分,其中引用了 _ctl_fops
),最后是由LLMs生成的规约。LLM不仅将确定设备名称(图5中的 DEVICE NAME
),还将分析与设备关联的初始化规约(INITIALIZATION
)。
通常,对于大多数驱动程序,初始化操作是使用 syscall openat
或syz_open_dev
进行描述的。设备映射驱动程序中,操作处理程序_ctl_fops
在 struct miscdevice _dm_misc
的初始化代码中被引用。通过对这个部分代码的分析,LLM能够确定正确的设备名称,本例中为mapper/control
,基于 miscdevice
结构体中的 nodename
字段。
3.2.Specification Generation
在这个阶段,作者通过利用LLM分析驱动程序的源代码生成 ioctl
的规约。为了提高LLM的性能,作者将该过程分为三个阶段:推断命令值,识别参数类型和类型定义。
这种结构化的方法使LLM能够在每个阶段集中精力在一个特定的方面,从而提高效率和专注力。与设备名称推断过程类似,作者在规约生成的每个阶段都使用few-shot prompt。
这一步在实现时需要考虑两方面因素:
-
1.尽管像GPT-4这样的最新LLM支持128K的上下文大小,但向LLM提供所有与驱动程序相关的代码仍然是不切实际的。
-
2.这一步目标是推断
ioctl
处理程序的命令值和参数类型,不是所有代码或辅助函数都与这一目标相关。
3.2.1.Command Value
推断命令值示例如图6所示。prompt包括与推断任务相关的函数的源代码,涵盖 ioctl
处理程序函数和与命令值分析相关的任何辅助函数。比如之前 _ctl_fops
结构体的 unlocked_ioctl
字段被赋值为 dm_ctl_ioctl
函数,那么推断命令值就需要分析 dm_ctl_ioctl
函数的源代码。
LLM的输出是成功推断的命令值集合。如果推测命令值还需要分析其它函数,作者要求LLM列出“缺失”分派函数的名称和调用详细信息(分析结果 UNKNOWN
字段)。此外,还包括使用命令值变量的代码片段。如果LLMs识别出任何未知的命令值,KernelGPT将继续分析新识别出的分派函数,并整合其在上一步中的使用信息。前一步 的UNKNOWN
字段的输出在引导后续步骤时作为参考。
比如在示例中分析 dm_ctl_ioctl
时LLM反馈需要继续分析 ctl_ioctl
函数,继续分析 ctl_ioctl
时LLM反馈需要分析 lookup_ioctl
函数,不过在分析 ctl_ioctl
时,LLM已经确定参数值 DM_VERSION
。(这可能意味着推断命令值设计多轮分析多个结果),到目前为止,LLM已经确定命令值 DM_VERSION
的处理函数为 ctl_ioctl
。
3.2.2.Argument Type
在确定了命令值之后,接下来便要分析该命令对应的参数类型,如图7 prompt所示。这一步骤的输入为命令推断的输出,包括相关函数和展示参数用法的代码片段。KernelGPT随后提取这些相关函数的源代码,并将其输入LLM,由它们来识别参数类型。如果涉及其它函数,那么这个类型仍然会被标记为 UNKNOWN
。在这种情况下,KernelGPT会利用LLMs提供的新信息继续其分析工作。
图7展示了设备映射器驱动程序中针对 DM_REMOVE_ALL
命令值的参数推断的实际例子。在之前的步骤中,LLM识别与该命令值相关的函数为 remove_all
,以及引用该函数的代码。因此,KernelGPT向LLM提供了 remove_all
的源代码,以指导参数类型推断。通过分析其签名,LLM推断出该参数应该是指向 struct dm_ioctl
结构的指针。此外,LLM将 struct dm_ioctl
放置在 TYPES
字段中,为下一阶段的类型定义分析做好准备。
注:这一步SyzDescribe通过规则进行推断,SyzDescribe只分析 ioctl
调用分析对应的命令值及对应的参数类型。比如下面代码片段中,SyzDescribe通过对 if
和 switch
的条件分析判断命令值为 cmd_1
、cmd_2
、cmd_3
之一,当命令值为 cmd_1
时,参数 arg
会被转换为 struct xx_type
类型。因此这里需要复杂的启发式规则识别参数类型。因此推测用LLM是为了更好地识别这类转换操作识别真正的参数类型。
long xx_ioctl(struct file *file, int cmd, long arg) {switch (cmd) {case cmd_1:struct xx_type xx_arg;copy_from_user(&xx_arg, arg, sizeof(xx_arg));...break;case cmd_2:fd = get_unused_fd_flags(...);file = anon_inode_getfile(..., &no_fops, ...);fd_install(fd, file);return fd;default:xx = file->private_date;if (xx.ops->ioctl)xx.ops->ioctl(file, cmd, arg);}if (cmd == cmd_3) {...}...
3.2.3.Type Definition
在识别了参数类型之后,KernelGPT继续为这些类型生成规约。图8展示了在这一阶段使用的提示。本质上,KernelGPT从Linux内核代码库中检索类型定义源代码。然后将这个源代码呈现给LLM,由它们创建相应的Syzkaller规约。在类型定义中引用了嵌套类型的情况下,LLM被指示在它们的输出中将它标记为 UNKNOWN
。这些被标记的类型将在后续步骤中进一步分析。
图8中展示的类型定义推断例子涉及一个示例:PhysDevAddr_struct
结构体中的一个字段是一个由 SCSI3Addr_struct
结构体类型组成的数组。由于这个新引用的结构体 SCSI3Addr_struct
的源代码没有包含在提供的信息中,因此预期LLM会将其标识为 UNKNOWN
。这种标识表明SCSI3Addr_struct
需要在后续步骤中进一步分析。(疑问:这一步好像可以直接用解析工具层次化生成,无需LLM)
3.3.Specification Validation and Repair
在这一阶段,受到最新基于LLM的程序修复研究启发,作者旨在验证由KernelGPT生成的规约,并自动更正任何无效的部分。这一步骤极为关键,因为LLM在规约生成过程中可能出错。为了解决这一问题,作者采用了Syzkaller的工具syz-extract来验证规约的有效性,该工具能够识别语法错误,如未定义的变量和类型。首先,KernelGPT通过syz-extract提供的错误信息定位规约中的错误之处,并将错误信息与相应规约匹配。接着,针对检测到错误的规约,KernelGPT通过提供few-shot prompt向LLM反馈。
如图9所示,这个过程包括向LLM提供错误的规约、相应的错误信息以及内核代码库中的相关源代码以供修复。之后,LLM预期将输出修正后的正确规约。
这里 vfio_pci_hot_reset_info
的类型描述最初是错误的。这是因为syzlang要求数组 [type, length]
规约 中的长度必须是一个常数,而g中使用了可变长度的数组。在syzlang中,可变长度数组的正确格式仅为 array[type]
。通过分析syz-extract生成的错误信息 “count is unsupported on all arches”
,LLMs修复了这个问题,通过将 devices
重新定义为可变长度数组,并将count指定为类型 len[devices]
,从而使得规约与syzlang的要求保持一致。
4.Implementation
1.Source Code Extractor
作者采用LLVM工具链实现了Linux内核源代码提取器。该提取器通过模式匹配解析内核代码库,识别设备操作处理程序,并准备输入数据供KernelGPT使用。此外,它还提取操作处理程序内的 ioctl
处理程序及其引用位置,并编译内核中的所有函数、结构体、联合和枚举定义。这些定义在LLMs需要时,为规约生成和修复提供指导。
2.LLM
KernelGPT基于最先进的LLM GPT4构建。在每一步分析中,作者通过OpenAI API访问GPT4,并设置低温度参数为0.1。分析的最大迭代次数 MAX_ITER
默认设置为5。
3.Few-shot prompting
-
为了推断设备名称和命令值,作者采用了3-shot prompt来适应LLM的上下文大小限制。这是因为在这两个步骤中涉及到的函数源代码通常较长,这需要限制少量示例的数量。
-
相比之下,对于其他阶段,如参数类型推断和类型定义分析,作者选择了6次提示。
-
在修复阶段,考虑到潜在的错误多样性以及必要的修复信息和错误消息通常的简洁性,作者选择了9次提示的策略。这种方法旨在为LLM提供对修复过程的更广泛理解。
4.Driver Selection
在初步评估中,作者专注于为Syzkaller中未曾描述的驱动程序或处理程序创建规约。这一重点极为重要,因为缺少规约的驱动程序通常未经过彻底测试。因此,这些驱动程序成为作者的关注焦点。在设备名称分析之后,作者首先确认Syzkaller是否已存在该设备的规约。如无现有规约,作者便着手生成新的规约。目前,作者的主要工作集中在常规驱动程序,如字符和块设备,而网络和USB设备的规约生成则计划在未来进行。
5.Evaluation
-
RQ1:KernelGPT为未描述的内核驱动生成的规约数量及其质量如何?质量主要通过覆盖率衡量。
-
RQ2:与baseline相比,KernelGPT生成的系统调用规约的质量如何?主要也是通过覆盖率比较。
-
RQ3:KernelGPT生成的规约能否检测到内核中的real-world bug?
作者选择了Linux内核版本6.7作为target,该版本于2023年11月5日发布,hash标识为d2f51b。此外,为了规约生成和评估,作者使用了Linux内核的syzbot配置。这个选择为谷歌在通过QEMU对Linux内核进行模糊测试时的决策。
作者的Fuzzing配置遵循默认的Syzkaller设置,使用4个QEMU实例,每个实例利用2个CPU核心。为了确保公平比较并减轻崩溃重现的影响,作者在收集和比较覆盖范围结果时禁用了reproduce
特性。为了展示KernelGPT生成的规约的质量,作者选择了SyzDescribe (当前SOTA系统调用规约生成方法)和由人类专家制定的现有Syzkaller规约作为基准。
5.1.Overall Results
在syzbot内核配置下检测到132个 ioctl
处理程序,不包括网络和USB驱动程序处理程序。在这些设备中,有50个(占37.9%)根据KernelGPT推断的设备名称在Syzkaller中被识别为缺少规约。然后,作者为这些未描述的驱动程序生成规约,成功推断出其中的39个,如表1所示。
对于其余设备未能生成规约的失败主要归因于几个原因。
-
首先,它们代码的复杂逻辑使得LLMs难以理解,特别是当
ioctl
函数的源代码过长时,阻碍了GPT4的理解能力。 -
此外,当要分析的源代码超过GPT4的上下文限制时,它就无法正确推断。
在KernelGPT成功生成的39个规约中,有24个直接通过验证,另外8个通过KernelGPT成功修复,使它们能够通过验证检查。然而,KernelGPT无法修复其余的7个规约。对于32个有效的规约,作者使用这些经过验证的规约运行Syzkaller,并其中有17个可以执行。设备无法执行的一个潜在原因是它们需要复杂的初始化步骤,这些步骤无法在规约中定义(例如,像 syz_open_dev
这样的辅助函数),或者无法被GPT4准确推断。另一个可能的因素是这些设备在syzbot内核配置中没有启用。
作者分别对每个新检测到的驱动程序进行了8小时的测试,使用了默认设置。结果显示在表2中,# Descriptions
列显示每个设备的系统调用规约数量,Cov
列是作者为每个设备独立生成的规约进行8小时模糊测试时覆盖的行数。 Unique Cov
列反映了与标准Syzkaller设置相比的唯一覆盖情况,在相同条件下激活了所有3,912个系统调用。在这个标准设置中,覆盖了143,838行。由Kernel GPT生成的先前未解决的驱动程序的规约增加了129个(3.3%)规约,并有助于覆盖额外的6,668个(5%)唯一行。这些数字突显了为先前未解决的驱动程序提供规约的重要性,并展示了作者生成的规约的有效性。
除了单独运行每个驱动程序的规约外,作者将所有新驱动程序的规约与原始的Syzkaller规约集成在一起。这意味着在一次运行中,同时执行由KernelGPT生成的新规约和Syzkaller中的现有规约。鉴于系统调用数量庞大,约为四千个左右,作者将这个组合运行的模糊测试时长延长到了24小时。随后,将此组合运行的覆盖结果与在相同设置下仅运行现有的Syzkaller规约的结果进行比较。集成KernelGPT为未描述的驱动程序合成的规约已被证明是有效的,触发的崩溃数量比仅使用现有的Syzkaller规约多28.6%(28 vs. 36)。就代码覆盖率而言,由于实验的初步性质和规模较小,结果相对较为适度。由于KernelGPT引入的新系统调用规约数量相对较小(129),与总数(3912)相比,集成后的代码覆盖率增加适度,额外覆盖了1.5K行。尽管如此,即使是这个有限规模的实验也突显了使用KernelGPT推断更多系统调用的规约的巨大潜力,这可能在未来的应用中实现更为有效的内核模糊测试。
5.2.Comparison with Baselines
为评估KernelGPT生成的规约质量,作者选择了10个由baseline方法(Syzkaller和SyzDescribe )规约的“现有”设备。作者使用KernelGPT为这些设备生成规约,并在独立的运行中比较了每个规约的覆盖结果,仅启用了规约中包含的系统调用。
表3显示了KernelGPT、SyzDescribe和Syzkaller生成的规约的结果,在8小时的模糊测试后详细列出了系统调用数量、定义的类型和行覆盖情况。KernelGPT实现了最高的行覆盖,超过基线方法21.3%以上,突显了其生成高质量规约以提升模糊测试性能的效果。此外,KernelGPT为驱动程序定义了最多的类型,展示了其在类型分析方面的强大能力。对于SyzDescribe,两个规约未能产生覆盖,其中一个问题源于SyzDescribe对设备名称的错误推断。总体而言,比较结果表明KernelGPT在描述的驱动程序上甚至超过了Syzkaller,预示着将KernelGPT应用于更多驱动程序可能取得更显著的效果。
5.3.Kernel Bug Detection
表4展示了KernelGPT在未描述的驱动程序中检测到的漏洞。到目前为止,KernelGPT已经检测到8个独特的漏洞,其中7个以前是未知的。值得注意的是,所有这些漏洞都无法通过默认配置的Syzkaller检测到。
KernelGPT检测到的7个新漏洞均位于两个新描述的驱动程序中,分别是device mapper和CEC。
第一个漏洞是在设备映射器驱动程序中的 ctl_ioctl
函数中发现的 kmalloc
漏洞,而第二个漏洞是在 dm_table_create
函数中发现的 kmalloc
漏洞。
它们的成因不同但相似:驱动程序忽略了对 kvmalloc
分配大小的检查,导致可能分配过大的内存大小。具体而言:
-
bug1崩溃与
dm_ioctl
结构中的data_size
字段有关。该字段在copy_param
过程中的数据结构准备阶段中分配内存起着关键作用。 -
bug2问题集中在
DM_TABLE_LOAD_CMD
命令值和target_count
字段。在执行dm_table_create
时,这些元素对于分配目标至关重要。值得注意的是,尽管SyzDescribe为此驱动程序生成了一个规约,但其中包含有误的设备文件名、错误的命令值和不精确的类型,因此未能检测到这两个漏洞。第一个漏洞已经得到了内核开发人员的确认。
其余5个的新发现的漏洞都是在CEC驱动程序中发现的,这是一个用于HDMI CEC硬件的标准化内核接口。
-
bug3: 图10包含与
KASAN: slab-use-after-free Read in cec_queue_msg_fh
漏洞相关的代码片段,其中CEC驱动程序从已被kfree(fh)
释放的变量中读取数据。 -
bug4: 在
cec_transmit_msg_fh
中发生的 ODEBUG漏洞是由于驱动程序试图使用kfree(data)
释放活动对象。 -
bug5:
cec_data_cancel
中的WARNING是由CEC驱动程序内部检查触发的,该检查期望变量处于当前或挂起状态。 -
bug6:
INFO: task hung in cec_claim_log_addrs
中的问题是由于内核挂起,因为CEC设备等待配置任务的完成。 -
bug7: 在
cec_transmit_done_ts
中发生的一般保护错误是由于CEC设备尝试引用一个非规范地址,导致内核崩溃。
6.Conclusion
在本文中,作者提出了KernelGPT,这是第一个利用LLM自动生成系统调用规约以增强kernel fuzzing的方法。它采用迭代式方法自主生成规约的所有必要组件,并使用验证反馈进一步修复这些规约。初步结果表明,KernelGPT有助于提高Syzkaller的覆盖率,并检测到了7个先前未知的新描述驱动程序中的漏洞。此外,Syzkaller团队表示有兴趣将由KernelGPT生成的规约整合进规约库。
7.参考文献
Yang C, Zhao Z, Zhang L. KernelGPT: Enhanced Kernel Fuzzing via Large Language Models[J]. arXiv preprint arXiv:2401.00563, 2023.