Linux 内核 API 的复杂历史

系统教程10个月前发布 jaypp
3 0 0

Linux 内核是一个不断迭代的代码库,截至 2021 年,其代码行数已超过 3000 万行。然而,由于用户通常不会运行最新的顶层 Linux 内核,因此从用户的角度来看,它也是一个非常分散的代码库,这对安全性有重要影响。一致的安全属性(跨内核版本、处理器架构、编译器版本)是grsecurity一个重要目标,但顶层通常不会将添加的预防性安全检查或功能向后移植到旧支持的内核,并且这些措施的状态没有很快被最终用户采用,实际上,必须是有经验的专注于安全的内核开发人员才能确定他们自己系统的状态。

作为系统管理员,我们首先要担心的问题是一个系统的安全性和稳定性,其次才是它的效能和效率。一个系统是否安全,一般来说与系统的复杂性有直接关系。对于必须开放访问的系统来说,成熟而可靠的系统保护措施是十分重要的。而对作为各种程序运行平台的操作系统而言,系统越复杂可能隐藏的攻击面也就越多。

受 copy_*_user 中的检查影响的漏洞类型在过去已经出现过几次,例如, Mathias Krause 在 2013 年的修复报告。在讨论这些检查时,最有意义的是谈论当前的顶层状态,之后我们可以讨论代码的历史以及它是如何随着时间的推移而演变的。

当前顶层 copy_*_user() 检查

在当前的顶层内核中,copy_from_user(用于将数据从用户级复制到内核)和copy_to_user(用于将数据从内核复制到用户级)实现如下:

Linux 内核 API 的复杂历史

因此,如果 check_copy_size() 通过,则将调用较低级别的 copy_*_user 例程。这些较低级别的例程可以内联或不内联,具体取决于处理器架构,但它们的实现方式保持不变:

Linux 内核 API 的复杂历史

access_ok() 是一个古老的例程,其目的是验证用户空间中提供的范围(作为 copy_from_user 的源,copy_to_user 的目标)是否实际驻留在用户空间中。这个 API 最近发生了变化,从 thread_info 结构中删除了 addr_limit 并将内核拆分为内核副本到它自己的 API。以前,在 addr_limit 恢复之前的 set_fs(KERNEL_DS) 或恶意修改的 addr_limit(过去几年的常见漏洞利用技术)之后运行的代码可以(ab)使用 copy*user 进行任意内核读/写。

should_fail_usercopy() 与错误注入(模糊增强)有关,可以忽略。 instrument_* 同样可以忽略,因为它与 KASAN(调试/模糊测试增强)有关。出于生产安全目的,我们感兴趣的唯一代码是 access_ok() 和 check_copy_size()。

Linux 内核 API 的复杂历史

让我们稍微解释一下这里显示的内容:__compiletime_object_size()是一个宏,它利用了编译器的__builtin_object_size()。如果内核中用于复制的源或目标(适当)的对象的大小能够在编译时确定,这将返回该对象的大小。否则,编译器版本不支持__builtin_object_size(),它返回-1。

当内核对象的大小是静态已知的并且要复制的长度也是恒定的时,__bad_copy_from() 和 __bad_copy_to() 都是编译时出现的错误,这种情况在实践中不太可能会变成安全问题,除非代码从未测试/使用过。当对象大小静态已知但复制长度不是编译时常量时,Copy_overflow()是一个运行时警告。

如果复制长度大于 INT_MAX,“WARN_ON_ONCE(bytes > INT_MAX)”检查将产生运行时警告。由于这是在无符号 size_t 类型上计算的,因此当在有符号 int 或 long 类型上解释时(即 oss-sec 上报告的错误的情况),这具有拒绝负长度的附加效果。稍后会详细介绍此检查。

check_object_size() 来自顶层的USERCOPY 功能的有限版本。不会对其返回值进行检查,因为它不会像其他检查那样简单地使复制失败,而是在 usercopy_abort() 中执行 BUG() ,这在简单的情况下将简单地终止所涉及的进程,但在更复杂的场景中,例如在用户空间副本周围保留互斥锁可能会导致某些代码路径锁定,或者在 panic_on_oops 场景中,会导致系统崩溃。

关于 oss-sec 报告中讨论的漏洞的实际影响,由于 2018 年 6 月的”fsi: Add cfam char devices ” 中针对 Linux 4.19 引入了错误的 CFAM 更改,因此 cfam_write() 案例将进入 copy_from_user()使用负长度,到达 check_copy_size() ,其中 __compiletime_object_size() 将返回 4 用于复制到的 __be32 数据变量,然后由于字节不是编译时常量,将调用 copy_overflow(),触发其中包含的WARN(),并错误地中止复制操作。

为了简洁起见,我们将把分析限制在这些方面,而不会深入raw_copy_*_user()本身的实现,它已经看到了自己的体系结构特定的发展,包括SMAP/PAN/等的引入和Spectre的发现。

在继续之前要注意的最后一点是memset()只存在于_copy_from_user()中,内核注释如下:

注意:1.只有 copy_from_user() 在短复制的情况下将目标置零。2.__copy_from_user() 和 __copy_from_user_inatomic() 都不为零。3.他们的调用者绝对必须检查返回值。

注意__copy_*_user(两个下划线)和_copy_*_user(一个下划线)是不同的。这个memset()的原因大概是为了解决Linux历史上copy_*_user被标记为__must_check属性之前的情况,要求调用者检查返回值是否错误。考虑一种常见的情况,即从用户级复制结构,内核更改了某些字段,然后将结构复制回用户级。如果来自用户级的副本或内核设置字段没有写入的区域被复制回用户级,且未初始化,它们可能会将之前的内存内容泄漏到用户级(信息泄漏)。用户级还可以通过使部分复制范围包括未映射的地址或操作的无效权限来强制低级复制例程中的部分失败。

check_object_size() 的历史

这个检查第一次出现在2016年6月的Linux 4.8版本中,通过commit“mm: Hardened usercopy”。它的提交信息如下:

“这是将PAX_USERCOPY移植到主线内核的开始。这是由CONFIG_HARDENED_USERCOPY控制的第一组特性。这项工作是基于PaX Team和Brad Spengler的代码,以及Casey Schaufler的早期移植。其他非平板页面测试来自Rik van Riel。”

check_object_size() 通过引用堆和其他元数据来工作,以便在运行时尽可能验证复制操作是否发生在单个对象的范围内。

尽管上面提到的提交消息是移植完整功能的开始,但在5年里,除了禁用上述添加的页面测试(grsecurity中不存在)以外,该功能没有出现其他重大变化,这破坏了内核的几个方面。PAX_USERCOPY最初发布于2009年,比有限的上游版本早了大约7年。强化后的用户复制代码没有向后移植到早期版本,包括当时的活动 LTS 版本。因此,顶层 4.4 XLTS。

__compiletime_object_size()/__builtin_object_size() 的历史

__builtin_object_size()在2005年Arjan van de Ven编写的FORTIFY_SOURCE补丁中首次使用。它最初关注的是FORTIFY_SOURCE在用户级中也涵盖的典型的str*和mem* api。2009 年,在研究 FORTIFY_SOURCE 在内核中的实际覆盖范围时,我将 Arjan 的工作扩展到对更多函数执行检查,并增加了它对一些 [k|v]malloc 对象大小的编译时知识,发现它只检测了约 30% 的涵盖 API 实例,不过从安全角度来看,是不太可能出现被攻击的。

这些 str* 和 mem* API 的覆盖范围是在 2017 年 7 月为 4.13 内核提交“include/linux/string.h: add the option of fortified string.h functions ”,但没有提及早期的工作,或者任何使用一些动态分配对象大小的改进知识,将其有效覆盖率降低到我最初调查的 30% 以下。

2009 年 Arjan van de Ven 通过提交“x86: Use __builtin_object_size() to validate the buffer size for copy_from_user()”为 x86 顶层添加了用于 copy__*_user 的 __builtin_object_size()。这个Linux 2.6.34的初始版本只覆盖了copy_from_user,仅从 2013 年 10 月开始涵盖 copy_to_user,其中 Jan Beulich 为 Linux 3.13 提交了”x86: Unify copy_to_user() and add size checking to it “。它看到了许多重构,最终通过 Al Viro 在 2017 年 3 月为 4.12 版本的 Linux 提交”generic …copy_…_user primitives”产生了一个独立于架构的变体。

正如我们之前提到的,__builtin_object_size() 是由编译器提供的。 2013 年 4 月,为了响应内核在某些 GCC 版本上现有使用内置函数产生的编译时错误,Guenter Roeck 合并了“gcc4: disable __compiletime_object_size for GCC 4.6+”,使得整个练习对受影响的编译器毫无用处版本(当时,最新发布的 GCC 版本是 4.8.0)。

“我想指出,尽管 __compiletime_object_size() 在 4.6 之前被限制为 gcc,但整个构造将变得越来越没有意义。 然而,我会质疑 commit 2fb0815c9ee6b9ac50e15dd8360ec76d9fa46a2 (“gcc4: disable__compiletime_object_size for GCC 4.6+”) 确实是必要的,相反,这应该像从一开始就在这里做的那样处理。”

然而,直到2016年8月Josh Poimboeuf的Linux 4.8 commit:“mm/usercopy: get rid of CONFIG_DEBUG_STRICT_USER_COPY_CHECKS”,对GCC >= 4.1和< 4.6使用__builtin_object_size()的限制被更改为GCC >= 4.1。在这次提交时,GCC 6.2是最新的编译器版本。Josh的提交也消除了启用调试选项以获得功能的需要。

综上所述,在大约三年的时间里,除了少数内核开发人员对内核进行检查之外,用户都没有进行过有关操作。还应该注意的是,放宽对 __builtin_object_size() 版本限制的 4.8 提交从未向后移植到早期的内核,这意味着即使对于今天最新的 4.4 XLTS 版本,任何涉及此的检查对于几乎任何现代用户都是完全无实操可能性的。

你可能想知道,Clang在构建Linux时是如何发挥作用的,尽管谷歌在 2018 年底开始使用 Clang 构建内核的 4.4 内核。 Clang 历史上(甚至在最新版本中)伪造了 4.2.1 的 GCC 版本,因此不受这些更改的影响。

WARN_ON_ONCE 的历史(bytes > INT_MAX)

这种检查最早是 Linus Torvalds 本人在 2005 年 2 月对2.6.11 内核通过BUG_ON() 对 i386 进行的检查。由于Andi Kleen 的提交“[PATCH] i386: Remove copy_*_user BUG_ONs for (size < 0)”,两年多后,这些检查在2.6.22版本的内核中被删除,并错误地解释为“access_ok检查这种情况,不需要检查两次”。这种解释是错误的,因为虽然access_ok()确实有效地检查了相同的情况,但BUG_ON()避免了后续memset()的执行,此后memset()就可以执行了。

在 grsecurity 中,2.6.29 版本的 Linux 在 2009 年 8 月时在 Linux 支持的几乎所有架构中安全地实施了此检查,没有 BUG_ON()。重要的是,此检查是在 access_ok() 之前执行的,并且在 copy_from_user() 的情况下避免了稍后将讨论的易受攻击的 memset()。

在顶层,这项检查是由 Kees Cook 在 2019 年 12 月针对 Linux 5.5通过“uaccess: disallow > INT_MAX copy sizes”引入的。由于只有 2 行更改,因此一个月后它被反向移植到 Linux 5.4。然而,由于 copy_*_user API 在前几年经历了相当大的变动,这个简单的改变并没有被进一步的反向移植,因此在上游的4.4、4.9、4.14或4.19 XLTS内核中也没有出现,而这些内核现在仍然支持XLTS。

copy_from_user memset 的历史

在 x86 上,至少可以追溯到 2002 年之前,只有在 access_ok() 成功时才对失败的 copy_from_user 进行归零。例如当copy_from_user的大小不是编译时常量时调用 __generic_copy_from_user 的实现。后来,情况发生了变化,从 Linux 2.4.3.4 的这个提交开始,从随后的2.4.35版本的这个改变开始,该版本在access_ok()失败时添加了归零设置。

Andrew Morton在2003年6月发布了一个x86补丁,提到了在access_ok()失败的情况下重新放置memset()。

最有趣的是,早在2002年的Linux 2.4.4.4中,ARM就有以下变化:

Linux 内核 API 的复杂历史

目前尚不清楚该评论是指它缓解了前面描述的安全漏洞还是引入了一个漏洞,该漏洞可能由攻击者控制长度的缓冲区归零,然而,这一改变确实带来了一个潜在漏洞。考虑到由于计算中的一些溢出而导致 n = -1 的情况:access_ok() 在 32 位 ARM 上将失败,因为 n 表示为添加到任何用户空间的无符号值地址将涵盖包括内核空间在内的范围。 else 情况将被触发,导致长度为 0xffffffff 的 memzero(),肯定会导致系统无法恢复的 DoS。

目前该问题仍然存在于顶层中。

由于2019年添加的禁止INT_MAX副本大小的顶层检查是在check_copy_size()中实现的,因此它失败了,避免了对_copy_from_user()的调用,这将执行错误的memset(),与十年前的grsecurity更改相同。然而,由于未知的原因,在提交消息或它所引用的邮件列表讨论中,根本没有提到修复这个漏洞。

如上所述,由于顶层的更改未向后移植到 4.4、4.9、4.14 或 4.19版本种,因此可以将负长度传递给 copy_from_user() 的内核版本中存在的错误可能会导致大量内存损坏并保证系统 DoS。如果有人接受某些人提出的缓解建议并启用 panic_on_warn,2019年进行的顶层更改也会通过恐慌导致 DoS。

© 版权声明

相关文章