Linux内核是一个迭代代码库,截至2021年有超过3000万行代码。但是,从用户的角度来看,它也是一个非常分散的代码库,这具有重要的安全隐患,因为用户通常不会运行最新的顶级Linux内核。一致的安全属性(跨内核版本、处理器架构、编译器版本)是grsecurity的一个重要目标,但顶层通常不会向后移植添加的预防性安全检查或功能到较旧的受支持内核,并且该状态未被快速采用最终用户,事实上,它必须是经验丰富的以安全为中心的内核开发人员才能确定他们自己系统的状态。作为系统管理员,我们首先关心的是系统的安全性和稳定性,其次是系统的有效性和效率。一个系统是否安全,一般与系统的复杂程度直接相关。对于必须开放接入的系统,成熟可靠的系统保护措施非常重要。至于操作系统作为各种程序的运行平台,系统越复杂,可能隐藏的攻击面就越多。受copy_*_user检查影响的漏洞类型在过去已多次显示,例如在MathiasKrause的2013年修复报告中。在讨论这些检查时,谈论当前的顶层状态是最有意义的,之后我们可以谈论代码的历史以及它是如何随着时间的推移而演变的。当前顶层copy_*_user()检查在当前顶层内核中,copy_from_user(用于将数据从用户级复制到内核)和copy_to_user(用于将数据从内核复制到用户级)实现如下:因此,如果通过了check_copy_size(),则调用较低级别的copy_*_user例程。这些较低级别的例程可能内联也可能不内联,具体取决于处理器架构,但它们的实现保持不变:access_ok()是一个古老的例程,其目的是验证范围是否(作为copy_from_user的源,copy_to_user的目标)实际上驻留在用户空间。该API最近发生了变化,从thread_info结构中删除了addr_limit并将内核的内核副本拆分为自己的API。以前,在恢复addr_limit之前在set_fs(KERNEL_DS)之后运行的代码,或者恶意修改的addr_limit(过去几年的常见漏洞利用技术)可以(滥用)使用copy*user进行任意内核读/写。should_fail_usercopy()与错误注入(模糊增强)有关,可以忽略。instrument_*也可以忽略,因为它与KASAN(调试/模糊增强)有关。出于生产安全目的,我们唯一感兴趣的代码是access_ok()和check_copy_size()。让我们稍微解释一下这里显示的内容:__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)”检查将生成运行时警告。由于这是在unsignedsize_t类型上计算的,因此当在signedint或long类型上解释时,这具有拒绝负长度的附加效果(即在oss-sec上报告的错误的情况)。稍后将详细描述此检查。check_object_size()顶层USERCOPY函数的有限版本。它的返回值没有被检查,因为它不像其他检查那样简单地使复制失败,而是usercopy_abort()中的BUG()在简单的情况下会简单地终止所涉及的进程,但在更复杂的情况下,例如持有互斥锁围绕用户空间副本可能会导致某些代码路径锁定,或者在panic_on_oops场景中导致系统崩溃。关于oss-sec报告中讨论的漏洞的实际影响,由于2018年6月在“fsi:添加cfam字符设备”中为Linux4.19引入了不正确的CFAM更改,cfam_write()案例将使用负数进入copy_from_user()length,达到check_copy_size(),其中__compiletime_object_size()将为复制到的__be32数据变量返回4,然后由于bytes不是编译时常量,将调用copy_overflow(),触发包含在其中的WARN(),中止复制操作的错误。为了简洁起见,我们将把分析限制在这些方面,而不是深入研究raw_copy_*_user()本身的实现,它已经看到了自己的架构特定的发展,包括SMAP/PAN/etc的引入和幽灵的发现。在继续之前要注意的最后一件事是memset()仅存在于_copy_from_user()中,内核对此注释如下:注意:1.只有copy_from_user()在短复制的情况下将目标清零。2.__copy_from_user()和__copy_from_user_inatomic()都不为零。3.他们的调用者绝对必须检查返回值。请注意,__copy_*_user(两个下划线)和_copy_*_user(一个下划线)是不同的。之所以有这个memset(),大概是为了解决copy_*_user在Linux历史上被标记为__must_check属性之前的情况,要求调用者检查返回值是否有误。考虑一个常见的情况,从用户空间复制一个结构,内核更改了一些字段,然后将该结构复制回用户空间。如果从用户级或内核集字段未写入的区域复制回用户级并且未初始化,它们可能会将以前的内存内容泄漏到用户级(信息泄漏)。Userland还可以通过使部分复制范围包含未映射的地址或无效的操作权限来强制低级复制例程的部分失败。check_object_size()的历史该检查于2016年6月首次出现在Linux4.8中,通过提交“mm:Hardenedusercopy”。它的提交消息是:“这是将PAX_USERCOPY移植到主线内核的开始。这是由CONFIG_HARDENED_USERCOPY控制的第一组功能。这项工作基于PaX团队和BradSpengler的代码,以及CaseySchaufler的早期端口。其他非平面页面测试来自RikvanRiel。“check_object_size()通过引用堆和其他元数据来工作,以便在运行时尽可能多地验证复制操作发生在单个对象的范围内。虽然上面提到的提交消息是移植完整功能的开始,但在5多年来,除了禁用上面添加的页面测试(grsecurity中不存在)之外,该功能没有发生重大变化,这破坏了内核的几个方面。PAX_USERCOPY最初于2009年发布,大约比limited早7年上游发布年份。加固的用户复制代码未向后移植到早期版本,包括当时活跃的LTS版本。因此,顶级4.4XLTS。__compiletime_object_size()/__builtin_object_size()__builtin_object_size()的历史由ArjanvandeVen编写在2005年首次用于FORTIFY_SOURCE补丁。它最初专注于典型的str*和mem*api,FORTIFY_SOURCE也涵盖在用户空间。在2009年,我扩展了Arjan的工作n调查FORTIFY_SOURCE在内核中的实际覆盖率以对更多函数执行检查,并增加其对某些[k|v]malloc对象大小的编译时知识,发现它仅检查了大约30%的覆盖API实例,虽然从安全的角度来看,它不太可能看起来被黑客入侵了。这些str*和mem*API在2017年7月的提交“include/linux/string.h:addtheoptionoffortifiedstring.hfunctions”中针对4.13内核进行了介绍,但未提及早期工作或任何改进使用一些动态分配的对象大小的知识,已将其有效覆盖率降低到我最初调查的30%以下。ArjanvandeVen在2009年通过提交“x86:使用__builtin_object_size()来验证copy_from_user()的缓冲区大小”将用于copy_*_user的__builtin_object_size()添加到x86顶层。自2013年10月以来,Linux2.6.34的初始版本仅涵盖copy_from_user和copy_to_user,其中JanBeulich为Linux3.13提交了“x86:统一copy_to_user()并向其添加大小检查”。它进行了许多重构,最终通过AlViro在2017年3月为Linux4.12版提交的“generic...copy_..._userprimitives”最终形成了一个独立于体系结构的变体。正如我们之前提到的,__builtin_object_size()由编译器提供.2013年4月,为了应对在某些GCC版本上使用内置函数的内核中存在的编译时错误,GuenterRoeck合并了“gcc4:为GCC4.6+禁用__compiletime_object_size”,使得整个练习对于受影响的编译器来说不是必需的。无用的版本(当时,最新的GCC版本是4.8.0)。“我想指出,虽然__compiletime_object_size()仅限于4.6之前的gcc,但整个构造将变得越来越没有意义。但是,我会质疑提交2fb0815c9ee6b9ac50e15dd8360ec76d9fa46a2(“gcc4:disable__compiletime_object_sizeforGCC4.6+”)确实是必要的,相反,这应该像从一开始就在这里完成的那样处理。然而,直到JoshPoimboeuf的Linux4.8于2016年8月提交:“mm/usercopy:摆脱CONFIG_DEBUG_STRICT_USER_COPY_CHECKS”,对GCC>=4.1和<4.6使用__builtin_object_size()的限制更改为GCC>=4.1。在此提交时,GCC6.2是最新的编译器版本。Josh的提交还消除了启用调试选项以获得功能的需要。总而言之,在大约三年的时间里,除了少数内核开发人员之外,所有的用户都在为内核工作,除了检查之外,没有一个用户对此做任何事情。还应该注意的是,放宽__builtin_object_size()版本限制的4.8提交从未向后移植到早期内核,这意味着即使对于今天最新的4.4XLTS版本,涉及此的任何检查对于几乎所有现代用户来说都是完全不切实际的。你可能想知道Clang在构建Linux时是如何发挥作用的,尽管谷歌在2018年底开始使用Clang构建内核的4.4内核。Clang在历史上(甚至在最近的版本中)伪造了4.2.1的GCC版本,因此不受这些更改的影响。WARN_ON_ONCE(bytes>INT_MAX)的历史该检查由LinusTorvalds本人于2005年2月首次完成2.6.11内核通过BUG_ON()检查i386。感谢AndiKleen的提交“[PATCH]i386:Removecopy_*_userBUG_ONsfor(size<0)”,这些检查在2.6.22的内核中被删除两年多后,现在可以在2.6.22中使用,并被误解为“access_ok检查此条件,无需检查两次”。这种解释是错误的,因为虽然access_ok()确实有效地检查了相同的条件,但BUG_ON()避免了后续memset()的执行,之后memset()可以被执行。在grsecurity中,2009年8月的Linux2.6.29版本在几乎所有Linux支持的架构上安全地实现了此检查,没有BUG_ON()。重要的是,此检查在access_ok()之前执行,并且在copy_from_user()的情况下避免了稍后讨论的易受攻击的memset()。在顶层,KeesCook于2019年12月针对Linux5.5引入了此检查,“uaccess:disallow>INT_MAXcopysizes”。由于只有2行更改,一个月后它被反向移植到Linux5.4。然而,由于copy_*_userAPI在过去几年发生了相当大的变化,这个简单的变化没有被进一步向后移植,因此没有出现在上游4.4、4.9、4.14或4.19XLTS内核中,而这些内核仍然支持今天的XLTS。copy_from_usermemset在x86上的历史至少可以追溯到2002年之前,只有在access_ok()成功时才将失败的copy_from_user归零。例如,当copy_from_user的大小不是编译时常量时,将调用__generic_copy_from_user的实现。后来,事情发生了变化,从Linux2.4.3.4中的这个提交开始,到随后的2.4.35版本中从这个更改开始,它在access_ok()失败时添加了归零设置。AndrewMorton在2003年6月发布了一个x86补丁,其中提到重新定位memset()以防access_ok()失败。最有趣的是,早在2002年的Linux2.4.4.4中,ARM就有了如下变化:尚不清楚评论中指的是它减轻了之前描述的安全漏洞,还是引入了一个可以被攻击者利用的漏洞Buffersthatcontrol长度归零,但是,此更改确实引入了潜在的漏洞。考虑由于计算中的某些溢出导致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年进行的顶层更改也会通过panic导致DoS。
