https://lwn.net/Articles/731082/

@智彻

定时器到期函数的预警

译注:原文标题是 A canary for timer-expiration functions,canary 直译为金丝雀。这种鸟很杯具,由于天生对环境敏感,旧社会被矿工抓去做瓦斯预警,新社会被 IT 民工抓到代码里做错误的检测。

一个在内核里允许攻击者去覆盖函数指针的 bug,为攻击内核打开了一个相对容易的途径。尤其是,攻击者只需要简单地等待内核去使用被事先ä¿®改过的指针。 有几种不同的技术可以保护内核函数指针在编译或者初始化时被设置,但还是有些指针是在内核运行时经常地被修改的。内核的定时器完成函数就是一个很好的例子。 在 kernel-hardening 邮件列表里最近发出的一个 RFC 补丁,主要就是增加了一种方法,可以检测那些函数指针是否已经被以非预期的方式被修改,并且去阻止内核执行那样的代码。

Kees Cook 的这个补丁 主要是解决对 timer_list 结构的函数指针域的写覆盖。这个域的函数指针将在定时器到期时被调用, 从攻击者的角度,可以方便的把与之相邻的下一个结构成员 data 传入这个函数。因此找到覆盖函数指针办法的攻击者,也可以继续覆盖 data 成员,这就导致执行一段代码, 并给这段代码传入参数变得相当简单。正如 Cook 指出:”这提供给攻击者一个和 ROP 方式类似的原语, 从而执行一断内核的函数代码而不需要做 ROP 攻击所需的所有准备工作。”

漏洞利用

在补丁说明里,Cook 指出了使用这个技术的最近两个漏洞利用程序。第一个程序是它的发现者,Philip Pettersson 在 2016 年 12 月所描述的。 这个程序使用了 AF_PACKET 套接字(主要用于 raw socket 处理,例如 tcpdump 工具),并且使用了 setsockopt() 函数来操纵 packet-socket API 发出的请求包里版本信息的修改。 通过在恰当的时间(这又利用了一个内核的竞争条件的 bug),把 TPACKET_V3 修改成 TPACKET_V1他的漏洞利用程序将导致包含 timer_list 结构的内存, 在还未删除定时器的时候,被释放掉。

因此这个定时器对象将在释放掉后继续被引用。攻击者安排这块内存被重新分配到他可以直接写的内存区域 (Pettersson 提到使用 add_key() 系统调用可以办到), timer_list 的函数和数据被覆盖。在这个例子里,这个技巧被用了两次,第一次改变了 vsyscall 表,把它从只读修改成了读写,然后注册了一个全局可写的 sysctl (/proc/sys/hack),设置了 modprobe 的可执行路径。 这个程序以 root 权限运行了 modprobe,最终运行了一个 root 权限的 shell。

第二个最近的漏洞利用,在 Andrey Konovalov 五月份的 Project Zero 上的一篇长篇博客有介绍。 作者使用 syzkaller fuzzer 工具发现了这个内核缺陷。这个程序使用了在 AF_PACKET 代码里的一个堆上缓冲区溢出的问题。 依靠安排好的恰当的堆上的内存布局,并且发送了一个网络包,包里有感兴趣的内容(即漏洞利用代码),完成了把代码放入到内存。但是这个内存是用户空间的内存,并且 Intel 的页保护技术 SWAP(supervisor mode access protection) 和 SMEP(supervisor mode execution protection) 特性将阻止内核直接访问和执行那里的代码。因此 Konovalov 使用了和 Pettersson 相同的技术, 简单的调用 native_write_cr4() 改变了 CR4 寄存器的相关位,关掉了这些保护,而这个函数调用就是利用了内核里的一个 socket 的定时器到期函数。

一旦做完这些,这个程序设置了一个新的被修改过的 socket 和环形缓冲区组合,把发送函数指针修改成一个在用户空间的 commit_creds(prepare_kernel_cred(0))。 然后简单地使用这个 socket 传送一个网络包,来执行代码,从而使当前进程获得 root 权限。

有趣的是,两个漏洞都可以在发行版上 (例如,Fedora, Ubuntu)被非特权用户利用,只要用户的命名空间 (namespace) 处于使能状态,且没有做限制。两个漏洞利用都需要设置 CAP_NET_RAW capability 以创建发送网路包的 socket,而这会被非特权用户获取,来创建一个新的用户命名空间 (namespace)。虽然问题并不是和用户命名空间的代码直接相关,但它确实进一步增加了扩展该命名空间提供的用户权限的危险。 Pettersson 和 Konovalov 都警告了允许非特权用户创建用户命名空间的风险。

	两个漏洞利用也都绕过了内核地址空间随机化 (KASLR),SMAP 和 SMEP 保护技术。Pettersson 的漏洞利用使用了硬编码的目标函数调用的偏移去绕过 KASLR,而 Konovalov 通过读 dmesg 去找到内核的代码段地址。 SMAP/SMEP 也被直接使用内核内存 (Pettersson) 或者显式地调用内核代码关闭这些特性 (Konovalov) 的方式来绕过。

	可能的修补

	Cook 的补丁在 `timer_list` 结构里增加了一个 canary(见前译注) 成员,该成员放置在了 function 函数指针成员之前。当一个定时器被初始化的时候,这个 canary 成员会被设置成该定时器函数和这个 function 成员的异或值和只有内核才知道的一个随机数。在这个方法里,如果 function 函数指针被修改,那么 canary 成员也会被修改。因此,在定时器期满函数被调用前, 这个 canary 成员的值将被重新计算并且和之前存储的取值做比较;如果他们不同,这个 function 函数指针已经被非预期地修改,因此它将不会被调用。内核日志也会有一条警告。

不走运的是,Cook 很快就意识到他的 patch 是不完整的。他只修改了使用 setup_timer_*()add_timer() 两个函数设置定时器的代码, 但是错过了使用 DEFINE_TIMER() 设置的静态的定时器的代码。他承诺会发一个新版本的补丁去处理这个问题。

但在一封邮件讨论中,Cook 说新的修改会导致非常大的定时器代码的改动。他说,改动工作之大已经超出了他的预期,但确实可以做到更干净漂亮。在随后的讨论中,他说或许会选择在静态定时器代码里弱化 canary 的使用。 因为许多跨子系统的补丁集合包含了跨越代码树的代码改动,最终使得合并到内核主线变得十分困难。从六月开始,Cook 列出他和其它人目前为止遇到的所有问题, 在 ksummit-discuss 邮件线索里讨论

正如两个漏洞利用所显示出来的,这个问题是真实存在的。关于修补这一类漏洞的解决方案会受到欢迎。然而,Cook 的 canary 方案是否是一个可行方案还待观察。