内核漏洞分类
未初始化/未验证/已损坏的指针解引用
未初始化
当指针变量没有初始化的时候,分配的内存中用来存储指针变量的值是任意的,而这个指针变量的值也就可能是任意的。攻击者如果可以预测指针的内容并加以利用就可以完成一次攻击。
未验证
“未经验证的”指针常常在内核区域的多用户寻址空间显示巨大威力。内核区域地址在用户区域上,它的页表在所有进程的页表上都有备份。有些虚拟地址被选作“限定地址”,所以这个地址以上或以下的归内核使用,而其他的归用户进程使用。内核函数用这个“限定地址”来判断一个指针指向的是内核还是用户进程。如果指针没有经过验证,那么指向用户区域的指针可能就会被赋予过多的权限。
损坏
当内核试图从销毁的指针处读取数据时,将会发生任意读的情况,而当内核试图向指针所关联的内存地址中存储一个值时,将会发生任意写的情况。而且,攻击者能够全部或者部分控制指针指向的地址时,就会发生一个受控的或是部分受控的读/写操作,而当攻击者也无法控制被销毁的指针的值时,就会发生一个不可控的读/写操作。但攻击者也能在一定程度上预测不可控的读/写操作的源地址和目的地址,因此也能利用这种情况。
内存破坏漏洞
内核栈漏洞
所有进程的内核栈都是一个相同的虚拟地址空间(内核地址空间)的一部分,他们开始于不用的虚拟地址也占据不同的虚拟地址。内核态栈漏洞基本和用户态栈漏洞差不多。
内核堆漏洞
和堆溢出类似。
整数误用
算术/整数溢出
这种溢出发生在将一个超出整数数值存储范围的数赋值给一个整数变量。这类情况在C语言标准中定义为“不可预知的行为”。在实际中,这可能会使得无符号数的数值发生变化,有符号数改变符号。
符号转换错误
符号转换错误出现在将一个无符号数当做有符号数处理的情况下。如0xFFFFFFFF在无符号下为2^32-1,有符号数则为-1。
竞态条件
有两个或者两个以上执行者将要执行某一动作并且执行结果会由于它们的执行顺序的不同而完全不同。竞态条件要想发生,必须有多个竞争者同时执行任务,或者至少一个执行和另一个执行是交错进行的。第一种情况是在对称多进程操作系统中特有的,因为有多CPU或多核,就有多条不同的内核路径同时进行。第二种情况是单处理器系统中竞态条件发生的唯一可能的情况。开始的任务以某种方式被中断而让第二个任务去执行。
逻辑bug
引用计数器溢出
内核子系统的一个明显的目的就是为进程服务。每个进程一定需要一些资源,这些资源需要先分配,然后释放。有时候一个资源会带着或多或少的约束而被分配给不同的进程,这就变成了共享资源。对于共享资源,则是在最后一次引用关闭后一定要释放资源。这就需要引用计数器了,用来跟踪记录当下有多少进程在使用该特殊资源。
通过使计数器溢出并返回至0,以及成功调用fget()/fdrop()函数对,可以释放文件描述符结构,但是我们仍然控制着很多指向空结构的指针。这将导致空指针或者废弃指针的解引用(例如,我们试图关闭其他描述符之一时)。或者,这个漏洞将从逻辑上被利用,这是因为:内核结构一旦被释放,将会在随后的调用中重新分配,这取决于其所在的子系统,而这种重新分配在何处发生是可控的。这是触发这类漏洞的另一种普遍的(而且可能是从设计上来说更有逻辑性的)途径。
物理设备输入验证
操作系统另一个功能是管理物理设备,这往往交给设备驱动来处理。能支持大量物理设备同时运行是一个成功操作系统的目标。如果该操作系统是面向桌面用户的,那么就应该尽量支持更多便携的、可插拔的外部设备。这是一个大大方便了桌面用户的技术,叫做即插即用技术或者叫热插拔技术(意思是可以在机器生命周期的任意时刻添加一个设备并且立刻使用),这是靠自动检测技术来完成的(将自动识别设备,自动装载配套的驱动,并且可以“自动地”立刻进入使用状态)。当然,硬件设备也可以被劫持和恶意篡改。一个简单而且很普遍的例子是在Windows中当USB设备插入机器后,我们可以执行一些用户控制的指令。
内核生成的用户态漏洞
由内核态程序和用户态帮助程序的交互而产生。现代操作系统的内核中,把传统内核态任务放到用户态应用程序中已经比较常见。
这类做法有以下好处:
1. 运行于用户态的代码所面对的限制比运行于内核态的代码面对的限制少(拥有独立地址空间,可以任意进入睡眠状态,依赖于用户态内存分配器,使用任意大小的栈,等等)。
2. 从架构上看,运行于用户空间的代码可以以更低的权限运行并且可以降低其权限(从操作系统的角度来看)。
3. 如果发生错误不会危及整个系统。
4. 拥有特定许可证的运行于用户态的程序可以移植到另一个操作系统上而不会影响这些特定的许可证,而内核就是在这样的许可证下发布的。
然而多路传播(一对多)socket只会被保留给root,单一传播(一对一)socket却不是。唯一需要的是正确的目的地址,对于这类socket来说,也就是进程的pid。ps命令能找到这一信息,并且/proc/net/netlink里保存着所有进程的pid,这就使得此类漏洞的利用更加方便了。这一漏洞可以以许多种方法利用,并且能够获取几乎所有主流版本Linux系统的root权限,而且能够绕过所有内核安全补丁。
这一漏洞是设计缺陷的一个典型例子,无论守护进程是用简单的C语言,还是C++、Python或者Java编写,这种漏洞依旧存在。也就是说,这种缺陷存在于非常高的层次,在架构层次上容易发生。