先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!
需要体系化学习资料的朋友,可以加我V获取:vip204888 (备注网络安全)
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
KernelGPT: Enhanced Kernel Fuzzing via Large Language Models
作者来自University of Illinois at Urbana-Champaign,github主页。团队工作还包括:TitanFuzz。
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
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要体系化学习资料的朋友,可以加我V获取:vip204888 (备注网络安全)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
LMs需要时,为规约生成和修复提供指导。
2.LLM
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要体系化学习资料的朋友,可以加我V获取:vip204888 (备注网络安全)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!