delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

04 Oct 2009

NULL指针引用和内核bug的利用

总算是发公告了,可以说具体的事情了。

FreeBSD昨天发布了2项安全公告和1项Errata Notice:SA-09:13.pipeSA-09:14.devfsEN-09:05.null。两个安全公告修正的是同一类问题,也就是我们常说的多线程程序中的竞态条件(Race Condition);EN-09:05.null则是增加了一个使这类问题不再那么容易被利用达到特权提升目的的功能,但默认并不启用。

在 C 程序中,NULL 指针是一项很有用的特性。NULL有很多功能,例如:

  • 表示链表结束,或指针没有引用数据对象。因为NULL是0,因此判断一个指针是否是NULL要比判断一个指针是否指向某一个特定的对象要高效。
  • 作为调试工具:引用NULL会立即触发缺页(Page Fault),通常系统会将用户进程虚拟地址空间中地址为0开始的至少一页不予映射,因此这次缺页将让OS直接向进程发出SIGSEGV,这可以让程序尽早崩溃,以帮助开发人员找到问题。(映射一整页能够让访问 NULL 和 NULL 附近的内存时都产生缺页,例如指向结构 struct mystruct 的指针 struct mystruct *p,在访问 p->m_member 的时候,实际访问的内存地址是 offsetof(struct mystruct, m_member) 而未必是 0。

等等。与用户态程序类似,内核也使用 NULL 完成相同的目的。当内核自己引用空指针内存(NULL deferencing)时,x86/amd64硬件会产生异常12。

不过,传统上 Unix 系统并不明确地禁止用户在虚拟地址0上映射自己的内存页。因为对于程序来说,它也完全可以把地址 0 看作一个再普通不过的内存地址。另一方面,有些应用程序,特别是模拟器一类的程序,很可能会需要利用这一特性来完成一些功能,因为如果靠截取 SIGSEGV 并进行特殊处理的话,效率会很差。

事实上,用户程序能够在地址 0 上映射内存页这件事本身并不是一个问题。真正的问题在于,为了改善性能,许多操作系统(如果不是全部支持 x86 的通用操作系统的话)在内存分配方面会采用一个小技巧,将内核内存和用户内存映射在同一个虚拟地址空间,从而避免在执行系统调用进入和退出内核时进行完整的地址空间切换,因为这个操作在x86硬件上的代价是比较大的,而另一方面,由于同样的原因,这个技巧也会改善内核访问用户地址空间中内存的性能。

然而,这一切能够相安无事的前提是,内核没有bug,或者用户态运行的程序是"友善"的。由于欠锁等原因,内核可能会引用 NULL 指针(例如,在没有获取对象引用的情况下调用对象中的函数指针,而此时另一线程已经将对象的内存释放并进行了抹零操作)。这种竞态条件通常会导致系统崩溃,但如果用户能够控制虚拟地址为0的那一小块内存,则这类竞态条件就有可能被利用来执行用户所指定的代码,进而导致特权提升问题。

从 FreeBSD 8.0-RELEASE 开始,FreeBSD将默认禁止用户程序映射虚拟地址为0的内存页(返回EINVAL,个人认为应返回EPERM)。修正日期之后的 FreeBSD 其他版本如 6.x 和 7.x,可以通过将 security.bsd.map_at_zero=“0” 增加到 /boot/loader.conf 来启用这一特性。