内核漏洞概述
内核漏洞的分类
运行在 Ring0 上的操作系统内核、设备驱动、第三方驱动能共享同一个虚拟地址空间,可以完全访问系统空间的所有内存,而不像用户态进程那样拥有独立私有的内存空间。由于内核程序的特殊性,内核程序漏洞类型也更加丰富。(书中收集了近年内公布的内核漏洞,并将相关的分析资料整理打包)
可以从漏洞的严重程度和漏洞的利用原理两个角度来对内核漏洞进行分类。漏洞的严重程度是指漏洞利用后所造成的危害;漏洞的利用原理是指漏洞利用过程中使用的原理和技术。
按照漏洞严重程度可分为以下 4 类:远程拒绝服务、本地拒绝服务、远程任意代码执行和本地权限提升。拒绝服务指能够利用来使得远程系统崩溃或资源耗尽的内核程序 bug 或缺陷。
按照漏洞利用原理可分为以下 4 类:拒绝服务、缓冲区溢出、内存篡改和设计缺陷。
内存篡改又可以分成以下 3 个子类:任意地址写任意数据、固定地址写任意数据、任意地址写固定数据。
内核漏洞的研究过程
漏洞重现环节,需要搭建测试环境,通常为虚拟机环境;另外需要注意有漏洞的内核文件或驱动文件的版本,如果版本不对,是不可能重现的;还要确认该漏洞暂时还未打补丁;最后,如果该漏洞公布有 POC 源码,还需要对 POC 源码进行编译。在漏洞重现环节中,如果最终重现失败,不能说明漏洞不存在,如果环境搭建的没有问题,那可以考虑是否 POC 源码有误,或者该漏洞还依赖于其他条件。因此,建议先进行漏洞分析环节,通过漏洞分析可以加深对漏洞的理解,这样边分析边重现,往往问题就迎刃而解了。从漏洞重现到漏洞分析,是一个"由表及里"的过程。
漏洞分析环节,是整个漏洞学习的核心环节,如果分析不清漏洞的前因后果,那么漏洞利用也无从入手。漏洞分析过程其实是一个"刨根究底"的过程,也可以说是"打破沙锅问到底"的过程,只不过是"问"自己而已。漏洞分析有很多方法,如果有源码的话,可以先对源码进行白盒分析;如果没有源码可以对内核或驱动 PE 文件反汇编分析;如果漏洞公布中有 POC 源码的话,还可以对 POC 源码进行分析(通过阅读 POC 源码和注释,可以很快地对该漏洞有一个准确的认识);如果该漏洞的补丁已经发布了,还可以在打补丁后,提取新版本的内核或驱动文件,通过对比进行分析;另外还可以通过给有漏洞的内核或驱动文件下断点进行调试分析;如果能触发有漏洞的内核或驱动蓝屏,还可以针对蓝屏后的 Memory Dump(完整转储、内核转储、小型内存转储)文件进行蓝屏分析。
漏洞利用环节,是在漏洞分析的基础上,编写出能够利用该漏洞实现特定目标的代码,并进行测试的过程。对于内核漏洞利用而言,主要有 5 种目标:特权提升、远程溢出、本地溢出、远程 DOS 和本地 DOS。在实际漏洞利用过程中,最终达到的目标不外乎这 5 种,但是漏洞利用的细节各有不同,"各显神通"。
漏洞总结环节,是在完成了漏洞重现、漏洞分析和漏洞利用过程后,回过头来审视造成该漏洞的根本原因,并提出修补方法的过程。如果把以上环节比喻为攻击,那么漏洞总结必须站在攻击与防御的对立面,才能有所体会和感悟,才能寻求到突破。通过漏洞总结,能够将学习过程中获取到的知识升华为一种经验和能力。
内核漏洞挖掘的过程实际上就是,通过工具挖掘或手工挖掘,达到触发漏洞的目标,可能是一个蓝屏,也可能是一个内核异常,然后对此进行详细分析和测试,最终编写漏洞 POC 的过程。
编写安全的驱动程序
站在开发者的角度,内核漏洞的原因大体可以归结为:未验证输入和输出,未验证调用者,代码逻辑错误,系统设计存在安全缺陷等。在驱动开发的过程中,注意这些方面就可以大大提高驱动程序的安全性。
输入输出检查
输入输出检查是指对不可信的输入输出地址及数据长度进行合法性检查的过程。这种方法在 Windows 内核 API 中应用的十分广泛。
例如,在 NtReadFile 函数中,如果 PreviousMode 不是 KernelMode,即 NtReadFile 函数是从用户态被调用的,可以使用 ProbeForWrite 函数检测输入输出缓冲区是否可写,参见 ReactOS 中的代码如下:
1 NTSTATUS NTAPI NtReadFile(IN HANDLE FileHandle, 2 IN HANDLE Event OPTIONAL, 3 IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, 4 IN PVOID ApcContext OPTIONAL, 5 OUT PIO_STATUS_BLOCK IoStatusBlock, 6 OUT PVOID Buffer, 7 IN ULONG Length, 8 IN PLARGE_INTEGER ByteOffset OPTIONAL, 9 IN PULONG Key OPTIONAL) 10 { 11 KPROCESSOR_MODE PreviousMode = KeGetPreviousMode(); 12 //省略部分代码...... 13 /* Validate User-Mode Buffers */ 14 if (PreviousMode != KernelMode) 15 { 16 _SEH2_TRY 17 { 18 /* Probe the status block */ 19 ProbeForWriteIoStatusBlock(IoStatusBlock); 20 /* Probe the read buffer */ 21 ProbeForWrite(Buffer, Length, 1); 22 //省略部分代码......
类似地,在 NtWriteFile 函数中当发现 PreviousMode 不是 KernelMode 时,即从用户态调用过来的,可以使用 ProbeForRead 函数进行检测。
此外,在 IoControl 中如果 IoControlCode 指定的 Method 为 METHOD_NEITHER 时,也应当对输入和输出地址使用 ProbeForRead 和 ProbeForWrite 函数进行检验。
验证驱动的调用者
有很多驱动程序加载后,会在驱动程序入口函数 DriverEntry 中创建驱动设备,并创建符号链接,同时还会指定派遣例程。这样一来,所有用户态程序都可以通过 DeviceIoControl 函数,调用该驱动的派遣例程。即存在 Ring3 恶意调用 Ring0 驱动派遣例程的问题,Ring0 程序应该对这种调用进行验证和过滤。
作为不够健壮的第三方驱动程序,更容易因为这种恶意调用被干扰,发生逻辑错误,甚至触发可能存在的内核漏洞。因此需要考虑驱动程序的通信对象和调用来源,在派遣例程中对此进行必要的安全验证和过滤。
验证和过滤的方法有很多,例如检查调用者进程的 PEPROCESS,进程文件的 MD5,等等。除此之外,还可以考虑用户态程序和驱动程序的通信加密,对于解密失败或非法通信数据的情况可以不予处理。
白名单机制的挑战
目前有很多安全软件,为了防止病毒木马进入 Ring0 而提高权限,这样已经防止加载驱动了。然而为了避免影响第三方驱动的正常运行,安全软件大多开设了白名单机制,在白名单中的驱动加载时是不会被拦截的。但是如果白名单中的驱动存在内核漏洞呢?
虽然病毒木马很难加载他们自己的驱动,但是只要白名单中的驱动存在漏洞,利用漏洞进行提权等操作,同样可以实现需要的功能,甚至完全瓦解软件的防御体系,这便是 "白名单机制的挑战"。
要解决这个问题,需要对白名单设立"准入制度"。只有通过了安全评估、检测、分析的驱动才能被加入白名单列表中。另一方面,我们还需要对白名单中的驱动进行定期审计,一旦发现该驱动的漏洞或外部公布了该驱动的漏洞,需要在第一时间通知用户,提供补丁升级服务。