【外评】谷歌:从源头消除内存安全漏洞
内存安全漏洞仍然是软件安全的一个普遍威胁。在谷歌,我们认为大规模消除此类漏洞和构建高安全性软件的途径在于安全编码(Safe Coding),这是一种优先过渡到内存安全语言的安全设计方法。
这篇文章说明了为什么对新代码采用安全编码能快速、反直觉地降低代码库的整体安全风险,最终突破内存安全漏洞的顽固高原,并开始指数式下降,同时还具有可扩展性和成本效益。
我们还将分享最新数据,说明随着开发工作转向内存安全语言,安卓系统中内存安全漏洞的比例如何在 6 年内从 76% 降至 24%。
反直觉的结果
考虑到不断增长的代码库主要是用内存不安全语言编写的,内存安全漏洞不断涌现。如果我们在开发新功能时逐步过渡到内存安全语言,而对现有代码除了进行漏洞修复外基本不做改动,会发生什么情况?
我们可以模拟一下结果。若干年后,随着新的内存不安全开发的放缓,新的内存安全开发开始占据上风,代码库的构成如下1:
在我们模拟的最后一年,尽管内存不安全代码的数量有所增加,但内存安全漏洞的数量却大幅下降,这似乎与其他策略的结果背道而驰:
这种减少看似自相矛盾:新内存不安全代码的数量实际上增加了,这怎么可能呢?
数学
答案在于一个重要的观察结果:漏洞以指数形式衰减。它们有一个半衰期。在平均漏洞寿命 λ 的条件下,漏洞寿命的分布遵循指数分布:
2022 年发表在《Usenix Security》上的一项关于漏洞寿命2 的大规模研究证实了这一现象。研究人员发现,绝大多数漏洞都存在于新代码或最近修改过的代码中:
这证实并概括了我们在 2021 年发表的观察结果,即随着代码年龄的增长,Android 内存安全漏洞的密度也在下降,主要集中在最近的更改中。
由此得出两个重要启示:
- 问题主要出现在新代码上,因此我们必须从根本上改变代码开发方式。
- 随着时间的推移,代码会以指数形式逐渐成熟并变得更加安全,这就使得重写代码等投资的回报会随着时间的推移而减少。
例如,根据平均漏洞寿命,使用 5 年的代码比新代码的漏洞密度低 3.4 倍(使用研究中的寿命)到 7.4 倍(使用在 Android 和 Chromium 中观察到的寿命)。
在现实生活中,与我们的模拟一样,当我们开始优先预防时,情况就会开始迅速改善。
在 Android 上的实践
大约在 2019 年,Android 团队开始优先将新开发过渡到内存安全语言。这一决定是由于管理内存安全漏洞的成本和复杂性不断增加。虽然还有很多工作要做,但已经取得了积极的成果。以下是 2024 年的总体情况,从全部代码来看:
尽管大部分代码仍然不安全(但关键是,代码的年龄在逐渐增长),但我们看到内存安全漏洞在持续大幅下降。这一结果与我们上面模拟的结果一致,甚至更好,这可能是我们同时努力提高内存不安全代码安全性的结果。我们在 2022 年首次报告了这一下降趋势,现在我们继续看到内存安全漏洞的总数在下降3。请注意,2024 年的数据是根据全年数据推算出来的(表示为 36 个,但 9 月份安全公告发布后目前为 27 个)。
内存安全问题导致的漏洞百分比继续与新代码所使用的开发语言密切相关。内存安全问题在 2019 年占 Android 漏洞的 76%,目前在 2024 年占 24%,远低于 70% 的行业标准,而且还在继续下降。
正如我们在上一篇文章中指出的,与其他漏洞类型相比,内存安全漏洞往往更加严重、更有可能被远程访问、用途更广、更容易被恶意利用。随着内存安全漏洞数量的减少,整体安全风险也随之降低。
内存安全策略的演变
在过去的几十年中,业界在应对内存安全漏洞方面取得了重大进展,每一代进展都提供了宝贵的工具和技术,明显改善了软件的安全性。然而,事后看来,我们显然还没有找到真正可扩展、可持续的解决方案,以达到可接受的风险水平:
第一代:被动打补丁。最初的重点主要是被动地修复漏洞。对于像内存安全这样猖獗的问题,这会给企业及其用户带来持续的成本。软件制造商必须投入大量资源,以应对频繁发生的事件。这就导致了不断的安全更新,使用户容易受到未知问题的影响,并经常(尽管是暂时)受到已知问题的影响,而这些问题被利用的速度越来越快。
第二代:主动缓解。下一种方法是降低易受攻击软件的风险,包括一系列提高漏洞利用成本的漏洞利用缓解策略。然而,这些缓解措施(如堆栈金丝雀和控制流完整性)通常会给产品和开发团队带来经常性成本,往往会使安全性与其他产品要求发生冲突:
它们会带来性能开销,影响执行速度、电池寿命、尾部延迟和内存使用,有时甚至无法部署。
攻击者的创造力似乎无穷无尽,导致与防御者的猫鼠游戏。此外,通过更好的工具和其他进步,开发漏洞并将其武器化的门槛也在不断降低。
第三代:主动发现漏洞。下一代的重点是发现漏洞。这包括 sanitizers,通常与 libfuzzer 这样的模糊工具搭配使用,其中很多都是由谷歌开发的。虽然这些方法很有帮助,但它们解决的是内存不安全的症状,而不是根本原因。它们通常需要不断施加压力,让团队进行模糊测试、分流和修复发现的问题,因此覆盖率很低。即使全面应用模糊测试,也无法提供高保障,在大量模糊测试代码中发现的漏洞就证明了这一点。
通过这些方法,整个行业的产品都得到了显著增强,我们也将继续致力于应对、缓解和主动查找漏洞。尽管如此,我们也越来越清楚地认识到,这些方法不仅不足以在内存安全领域达到可接受的风险水平,而且还会给开发人员、用户、企业和产品带来持续和不断增加的成本。正如包括 CISA 在内的众多政府机构在其安全设计报告中所强调的,“只有采用安全设计实践,我们才能打破不断创建和应用修复程序的恶性循环”。
第四代:高保障预防
向内存安全语言的转变不仅仅是技术上的变化,更是安全方法上的根本转变。这种转变并非史无前例,而是对成熟方法的重大扩展。这种方法已经在消除 XSS 等其他漏洞类别方面取得了显著的成功。
安全编码(Safe Coding)是这一转变的基础,它通过语言特性、静态分析和应用程序接口(API)设计,在开发平台中直接执行安全变量。其结果是设计出一个安全的生态系统,可大规模提供持续保证,避免意外引入漏洞的风险。
从开发代码时提出的可量化断言中,我们可以看到从上一代到安全编码的转变。安全编码允许我们对代码的属性以及基于这些属性可能发生或不可能发生的事情做出强有力的断言,而不是关注所应用的干预措施(缓解措施、模糊处理),或试图利用过去的性能来预测未来的安全性。
安全编码的可扩展性在于它能通过以下方式降低成本:
- 打破军备竞赛: 防御者试图通过提高自己的成本来提高攻击者的成本,而不是无休止的军备竞赛,安全编码利用我们对开发者生态系统的控制,从一开始就专注于主动构建安全软件,从而打破这种循环。
- 将高保证内存安全商品化: Safe Coding 不需要根据每项资产的评估风险精确定制干预措施,同时还要管理重新评估不断变化的风险和应用不同干预措施的成本和开销,而是建立一个高基准的商品化安全体系,如内存安全语言,从而以可承受的价格全面降低漏洞密度。现代内存安全语言(尤其是 Rust)将这些原则从内存安全扩展到了其他漏洞类别。
- 提高生产力: 安全编码通过将错误查找进一步左移,甚至在代码检查之前,提高了代码的正确性和开发人员的工作效率。我们看到这种转变体现在一些重要的指标上,例如回退率(由于意外错误导致的紧急代码回退)。据 Android 团队观察,Rust 更改的回退率不到 C++ 的一半。
从经验到行动
互操作性是新的重写
根据我们所学到的知识,很明显我们不需要丢弃或重写所有现有的内存不安全代码。相反,Android 将重点放在使互操作性变得安全和方便上,并将其作为我们内存安全之旅中的一项主要功能。互操作性为采用内存安全语言提供了一种实用、渐进的方法,使企业能够利用现有的代码和系统投资,同时加快新功能的开发。
我们建议将投资重点放在提高互操作性上,正如我们在以下方面所做的那样
Rust ↔︎ C++ 和 Rust ↔︎ Kotlin。为此,今年早些时候,谷歌向 Rust 基金会提供了 100 万美元的资助,此外还开发了 Crubit 和 autocxx 等互操作性工具。
前人的作用
随着安全编码不断降低风险,缓解措施和主动检测将发挥什么作用?我们在 Android 中还没有明确的答案,但预计会出现以下情况:
- 更有选择性地使用主动缓解措施: 随着我们过渡到内存安全代码,我们预计对漏洞缓解的依赖会减少,这不仅会使软件更安全,而且会使软件更高效。例如,在移除不必要的沙箱后,Chromium 的 Rust QR 码生成器速度提高了 95%。
- 减少使用,但提高主动检测的有效性: 我们预计对模糊检测等主动检测方法的依赖会减少,但有效性会提高,因为对封装良好的小型代码片段实现全面覆盖变得更加可行。
最后的想法
与漏洞生命周期的数学计算作斗争是一场失败的战斗。在新代码中采用安全编码提供了一种范式转变,使我们能够利用漏洞固有的衰减优势,即使在大型现有系统中也是如此。这个概念很简单:一旦我们关闭了新漏洞的水龙头,它们就会以指数形式减少,从而使我们所有的代码更加安全,提高安全设计的有效性,并减轻与现有内存安全策略相关的可扩展性挑战,从而可以更有效地有针对性地应用这些策略。
事实证明,这种方法成功地消除了整个漏洞类别,而且根据在 Android 系统中长期使用的结果,它在解决内存安全问题方面的有效性也日益明显。
在接下来的几个月中,我们将与大家分享更多有关安全设计的信息。
鸣谢
感谢 Alice Ryhl 编写模拟代码。感谢 Emilia Kasper、Adrian Taylor、Manish Goregaokar、Christoph Kern 和 Lars Bergstrom 对本篇文章的有益反馈。
注释
1.模拟基于与 Android 和其他 Google 项目类似的数据。代码库每 6 年翻一番。漏洞的平均寿命为 2.5 年。新代码过渡到内存安全语言需要 10 年时间,我们使用西格码函数来表示这种过渡。请注意,由于使用了正余弦函数,所以第二张图表最初看起来并不是指数型的。
2.Alexopoulos 等人, “How Long Do Vulnerabilities Live in the Code? A Large-Scale Empirical Measurement Study on FOSS Vulnerability Lifetimes”。USENIX Security 22.
3.与我们的模拟不同,这些是真实代码库中的漏洞,其变异性较高,从 2023 年的轻微增长可以看出这一点。这一年的漏洞报告异常多,但鉴于代码的增长,符合预期,因此虽然内存安全漏洞的百分比继续下降,但绝对数量略有增加。
本文文字及图片出自 Eliminating Memory Safety Vulnerabilities at the Source
你也许感兴趣的:
- 如何在linux下检测内存泄漏
- Linus正面回应Linux内核“Rust之争”:未来必定使用,完全生产级别尚需时日!
- 【外评】在 RiSC-V 上运行《巫师 3》游戏
- 【外评】法官驳回大部分 GitHub Copilot 版权索赔要求
- Node.js之父ry“摇人”——要求Oracle放弃JavaScript商标
- 【外评】一年的 Rust 开发总结
- “革命性”「Safe C++」扩展提案:质疑Rust、理解Rust、成为Rust?
- Rust 的崛起: 这种编程语言为何越来越受欢迎
- 谷歌内部推出 SQL 中的管道(Pipe)语法
- JavaScript 之父联手近万名开发者集体讨伐 Oracle:给 JavaScript 一条活路吧!
> 提高生产力: 安全编码通过将错误查找进一步左移,甚至在代码检查之前,提高了代码的正确性和开发人员的工作效率。我们可以从回滚率(由于意外错误导致的代码紧急回退)等重要指标中看到这种转变。
> 据 Android 团队观察,Rust 更改的回退率不到 C++ 的一半。
20 年来,我一直在用这样或那样的语言编写大规模生产代码。但当我在 2016 年发现 Rust 时,我就知道这就是我的目标。我打算加倍努力。我当天就拿到了克拉布尼克和卡罗尔的书。我还留着我的死树副本。
老实说,它重新激发了我对编程的热爱。
这很有道理,因为我不得不回滚自己的 C++ 提交的首要原因,就是因为检查空指针是否为空的愚蠢失败而导致崩溃。如果 Rust 能够避免这个问题和其他类似的愚蠢编码问题,那么你就会期待整个回滚类的消失。
您真好,谢谢。
你是个传奇人物。感谢你撰写了这本书。它真的以一种非常积极的方式影响了我的生活。
我也有同感。当我需要选择另一种语言时,我会非常想念拉斯特语。
这是一篇非常有趣的文章!其中一个启示是,你不需要重写世界。将新的开发过渡到内存安全语言可以带来有意义的改进。这比为了获得效果而移植所有东西要容易得多(也便宜得多)。
事实上,这些结果表明,重写世界在安全性方面的好处是有限的。这就提高了保留成熟的遗留代码和只在新代码中使用内存安全语言的成本效益比。
这也意味着,在与不安全的遗留代码集成方面具有强大支持的语言和工具更为可取。
从安全角度看,我同意,但如果你想摆脱 GC 或只是减少总体资源消耗呢?
工程师通常是最昂贵的资源。使用 Typescript 或甚至 Ruby 开发软件是一种更快获得收益的方法,而且开发成本更低。对于缺陷率不需要极低的项目(如飞机控制固件)来说,开发成本和时间(即机会成本)通常是最重要的限制因素。Rust 可以节省你的开发时间,因为花在修复 bug 上的时间更少,但通常会选择它并不是因为它能节省你的 RAM 和 CPU 周期;如果你能挥洒自如,Haskell 或,嗯,Ada/Spark 可能会有不相上下的效率。
这是事实,但工程师在理解现有代码比编写新代码花费更多时间时,会出现一个交叉点。在这个交叉点上,具有更多内置检查功能的静态语言通常比动态代码更便宜。根据我的经验,达到这一点大约需要一年的时间,如果雇佣更多的人,时间会更短。
有道理。比方说,你在亚马逊上优化一个用 TS 编写的后台,当然,雇人来优化会更便宜,但到了一定时候就不是这样了。要么你需要一些真正的高级人才开始优化 TS 中的垃圾,要么你就无法像以前那样快速扩展。
Discord 不也发生过类似的事情吗?如果我还记得的话,那就是 Go。
学习如何真正使用不依赖 GC 的语言特性,比如 Swift、D、C#、Linear Haskell、Go、Eiffel、OCaml 特效……已经是很大的进步了。
很多人总是把 GC 语言放在同一个篮子里,却不明白自己在说什么。
那么如果是一个由于执行期限、或内存可用性等原因,任何形式的自动资源管理都不可能实现的领域,Rust 就是一个选择。
Rust 也有 “自动资源管理 ”的某些方面。在 Rust 中,你可能会遇到分配或重新分配(drop)的执行期限问题。在你列出的任何一种语言中,在关键领域避免出现这种情况的模式大致相同。
虽然我喜欢使用 Nim 或 Ocaml 中的效果系统来防止特定区域的分配。
使用 malloc()/free() 时甚至会遇到执行期限,这就是为什么有公司通过销售专门版本的 malloc()/free() 来赚取利润,而如今,由于有了类似的 FOSS 实现,这种生意已经不那么难做了。
问题的关键不在于赢得微基准测试游戏,而是对资源消耗和执行期限有具体的 SLA(服务水平协议),当语言功能被合理使用时,语言工具链是否能满足这些要求,或者它是否耗尽了实现这些目标的能量?
最近的 Guile 性能讨论主题就是一个很好的例子。
如果 X 语言确实能达到目标,但人们仍然选择 “将 XYZ 重写成 ZYW ”的方法,那么我们就已经超越了纯粹的技术考量。
那你就应该使用没有 GC 的内存安全语言。
是的,我是说在这些情况下,用 Rust 重写确实有些意义。
我想说的是,本文的图表非常清晰简洁。它很好地展示了精心挑选的数据和标签是如何毫不费力地传达想要表达的思想,以至于它们几乎消失在散文中。
因此,漏洞会以指数形式递减这一事实的结果就是,我们应该把重点放在新代码上。把精力花在大量不加区分的RiiR项目上,即使是为了实现最大内存安全性的目标,也是对资源的浪费。事实上,最简单的策略,也是所有务实的 Rust 专家都推荐的策略,实际上也是根据数据将内存漏洞最小化的最佳策略,这即使不是偶然,也是显著的趋同。
> Android 团队观察到,Rust 更改的回滚率不到 C++ 的一半。
哇哦
> 答案在于一个重要的观察结果:漏洞会以指数形式衰减。它们有一个半衰期。[……]2022 年发表在《Usenix Security》上的一项关于漏洞寿命的大规模研究2 证实了这一现象。研究人员发现,绝大多数漏洞都存在于新代码或最近修改过的代码中。
因此,在没有绝对必要的情况下,停止添加新功能对安全性来说是更好的选择。Windows LTSC 大概是最安全的 Windows 版本了。
在正常运行中触发的单个错误应该会随着时间的推移在维护的软件上逐渐减少。如果错误导致了问题,有人可能会报告它们,其中的一部分就会被修复。这就是一种衰减机制。
不过,尚未被利用的漏洞没有这种衰减机制。它们不会引起用户的不满和漏洞报告。它们只是静静地躺在那里,直到拥有足够资源和动力的敌人发现并利用它们。
这个联盟中的敌人比以前更多了。
你的断言与 TFA 中引用的 Usenix 研究相矛盾,该研究发现漏洞的寿命_do_遵循指数衰减。如果发现一个漏洞需要更长的时间,那么它的生命周期就会更长。
文章中所说的 “漏洞 ”是他们内部发现的。
而从攻击中发现的漏洞则不同。[1] 大多数漏洞会在最初几周或几个月内被修复。但那些在一年内没有被修复的漏洞会持续很长时间。大约 18% 的报告漏洞从未被修复。
[1] https://www.tenable.com/blog/what-is-the-lifespan-of-a-vulne…
或者另一种方法:只编译你明确需要的功能子集。
显然,这在任何地方的实用性都存在很大差异,但这种方法并不常见。
这可能是一个非常糟糕的主意,因为它大大增加了用户运行未经测试的编译功能组合的风险。
请允许我介绍一套全新的 Bug,当功能 A 存在而功能 B 不存在时就会出现这些 Bug。
恭喜你,你又回到了起点 1!
> 请允许我介绍一套全新的错误,当功能 A 存在而功能 B 不存在时,就会出现这些错误。
是的,但这些错误是安全错误吗?内存安全漏洞是一大焦点,因为它们是最常见的漏洞,可以被有意义地利用。
禁用整个代码段不太可能引入新的内存安全漏洞。当然也有可能发现竞赛条件,这些竞赛条件有时会导致安全漏洞,但其可能性远不及内存安全漏洞。
> 是的,但这些漏洞是安全漏洞吗?
如果软件无法使用,那么它是否也有安全漏洞就不重要了。或者换句话说,最安全的软件就是没人使用的软件。
> 在没有绝对必要的情况下,停止添加新功能对安全性会更好
即使新功能不是销售软件所必需的,但新硬件和更好的安全算法或现有算法的全面淘汰仍会发生。这将引入新的代码。
因此,我们的论点是,由于漏洞的生命周期是呈指数分布的,因此在新代码中关注安全默认值(如内存安全)具有不成比例的价值。
太神奇了,我从未见过有人用这个论点来支持 shift/left 安全护栏,但这很好。特别是对于那些拥有较大的传统代码库的人来说,他们可能会说:”何必呢,我们的 1 亿行 C++ 永远不会从内存安全中受益。
我认为,这也意味着任何轻量级的漏洞检测都会带来不成比例的好处–即使它只针对新代码和依赖关系与积压代码。
他们说的 “内存安全语言(MSL)”是复数,好像不止一种,但只明确指出 Rust 是他们正在过渡到并改进互操作性的 MSL。他们还在改善 Rust<>Kotlin 互操作性的背景下提到了 Kotlin,Kotlin 也有一些内存安全特性,但程度可能不如 Rust。Google 仅使用了这两种语言,还是还提到了其他语言?
几点想法:
关心这个问题的人,尤其是最近几年,一直倾向于 “内存安全语言 ”与 “非内存安全语言 ”的框架。这是因为它抓住了问题的根本,即默认安全与默认不安全。它试图避免对特定语言指手画脚或提出建议,而是将重点放在根本原因上。
就这篇文章的主题 Android 而言,我不知道是否有人试图转而使用除这些语言之外的其他 MSL。不过,我一般也不关注 Android 开发,但我确实很关注这些帖子,我不记得他们中有人谈论过 Rust 或 Kotlin 以外的东西。
> 我不记得他们中有谁谈论过 Rust 或 Kotlin 以外的东西。
别忘了那个又老又无聊的: Java。
我想,Go 之所以不常出现,是因为大多数 Android 进程都有一个可管理、可 GC 的 Java-ish-virtual-machine(Java-ish-virtual-machine)世界和一个本地 C/C++ 世界。Kotlin 适合前者,Rust 适合后者。Go 有点自成体系。
Android 在枯燥的托管 Java 代码中拥有大量令人惊讶的核心操作系统功能。ART/Dalvik与其他一些巧妙的技巧相结合,使系统的运行占用空间非常小,令人印象深刻。
Android 在之前的一篇博文中详细介绍了他们使用的内存安全语言:https://security.googleblog.com/2022/12/memory-safe-language…
谷歌也在 https://security.googleblog.com/2024/03/secure-by-design-goo… 中发表了他们对内存安全的看法,其中也介绍了一些正在使用的内存安全语言,如 Java、Go 和 Rust。
不仅仅是 Rust,将 C 语言网络服务改写成 Java、Python 或 Go 也是过渡到内存安全语言的一个例子。问题的关键在于,你不会在自己的代码中暴露内存安全 bug。可以说,在没有绝对需要的时候,选择一种没有类似 Rust 的手动内存管理的语言要好得多。
有很多,谷歌也使用了几种–Rust、python、java 和 go 都在其中。但 Android 的底层代码历来都是用 c++ 编写的,而 Rust 是他们正在构建的东西的主要内存安全替代品。
这就是抽象考虑与特定项目限制之间的分离。在一个给定的项目中,选择可能很少,但如果谈论基本现象,就必须对任意项目进行推理。当然,Rust 也有很多替代方案。即使仅限于 Android,也有多种选择,即使它们可能是一个较小的集合。
Java 和 Kotlin 用于应用程序。Rust 用于新的系统软件。
在整个谷歌,Go 被用于某些系统软件,但我还没见过它被用于 Android。
Kotlin 是内存安全的。它使用 GC 运行。
GC 对内存是安全的。
他们就是这么说的。
我不识字。
谷歌一直尝试使用少量语言。在 Android 系统中,他们尝试使用 C/C++、Java/Kotlin,现在又开始使用 Rust。不过,同样的经验仍然适用于多语言环境。
也许他们甚至认为碳是内存安全的
我对这里得出的结论有点不安,因为没有人提出一个明显的反驳观点–如果旧代码没有被仔细检查,因此漏洞没有被发现呢?
查看最近的提交日志远比查看某个 20 年未变的库要常见得多。
> 如果旧代码没有被仔细检查,因此漏洞没有被发现呢?
以前也没有这么严格。我认为这一点没有改变。
他们没有给出为什么旧代码的漏洞较少的理论,但我有一个:它们已经被发现了。
如果我们假定任何一段代码每 1000 行都有固定数量的未知错误,那么超时运行的次数越多,错误被发现的可能性就越大。在修复它们和修复它们时进行代码审查之间,我们希望平均而言,事情会变得更好。
因此,随着时间的推移,现有代码中每千行的错误会越来越少。经过实战检验。
正如帖子中所说,如果你继续以相同的速度引入新的错误,你就不会取得进步。但如果使用内存安全语言意味着在新功能中引入更少的错误,那么随着时间的推移,错误总数应该会下降。
我一直认为这等同于 “工作加固”。
我更关心的是使用当时的标准和工具编写的合法的旧代码(安卓已经有 20 年左右的历史,因此可以合理地归入此类)。
要使这些代码保持最新,需要持续的工程努力。而且老代码通常不那么容易理解。
此外,旧代码(尤其是系统编程中的旧代码)往往与旧需求相关联,其中一些需求可能随着时间的推移而变得过时。
这些不常使用的旧代码的长尾巴,让人感觉它的尾巴很可能有刺。
半衰期/工作硬化模型依赖于代码被强调以发现错误
我不明白这一点。受审查的项目是 Android,人们根据源代码/二进制代码手动或自动检测漏洞,而不是提交日志。为什么提交日志与查找漏洞完全无关?
提交日志只是用于归因。如果有某个 20 年未修改的旧库在 20 年未更新的情况下通过了模糊测试和手动代码检查,那么它很可能是可靠的。
漏洞利用者查看提交日志是因为新功能中存在漏洞,而按照提交日志查找漏洞比深入代码库查找已有漏洞要容易得多。
我对旧代码漏洞较少的说法也不完全满意。我觉得除了年龄之外,还有其他原因可以解释这种差异。
例如:也许工程师们在过去几年中一直专注于重写 MSL 中风险最高的部分,而不太可能修改风险较低的旧代码。
或者……也许是流程或人事变动导致了更多的缺陷。
话虽如此,但在我看来,任何给定的缺陷在单位时间内都有被发现的概率,随着时间的推移,被发现的缺陷也会越来越少。当然,只要维护者修复的漏洞比引入的漏洞多,旧代码中的漏洞就会减少,剩下的漏洞也可能很难被发现。
我很好奇这对 Mac 和 Windows 的适用情况,Mac 的大多数新代码都是用内存安全的 swift 编写的,而 Windows 仍然主要使用 C 或 C++。
苹果仍在每个新的 macOS 版本中添加大量新的 Objective-C 代码[0]。
我没有找到 Windows 最近版本的语言使用数字,但微软在新开发和重写旧功能时都在使用 Rust [1] [2]。
[0] 参见 “编程语言的演变 ”部分 https://blog.timac.org/2023/1128-state-of-appkit-catalyst-sw…
[1] https://www.theregister.com/2023/04/27/microsoft_windows_rus…
[2] https://www.theregister.com/2024/01/31/microsoft_seeks_rust_…
应该指出的是,Objective-C 代码出现内存安全问题的可能性应该比 C 代码平均要小得多,尤其是在苹果公司引入自动引用计数(ARC)之后。举个例子:
– ARC 避免了使用后释放(use-after-frees
– 取消引用空指针通常是安全的(向 nil 发送信息会返回 nil)
– Objective-C 有一个很棒的标准库(Foundation),其中包含安全集合等许多内容;在性能要求不高的习语 Objective-C 代码中,C 的大部分危险部分都可以轻松避免。
但苹果公司的 Objective-C 代码有很大一部分可能是用来实现底层运行时的,而这是很难做到正确的。
苹果的大部分 Objective-C 代码都在应用层,就像你的代码一样
> 而 Windows 仍主要使用 C 或 C++。
你有这方面的数据吗?在我的印象中,现在很大一部分 Windows 开发都使用 C#。将近 15 年前我在 EA 工作时,我们的内部工具已经非常倾向于使用 C#。
链接到我昨天的主题,那里肯定正在进行内存安全系统编程: https://news.ycombinator.com/item?id=41642788
将 “安全编码 ”作为安全方法的根本转变,这个想法很吸引人。我有兴趣了解更多关于如何在实践中实现这一目标的信息。
有关我们应用于网络领域的安全编码方法的更多信息,请查看本论文 (https://static.googleusercontent.com/media/research.google.c…) 或本讲座 (https://www.youtube.com/watch?v=ccfEu-Jj0as)。
如果我们逐步过渡到内存安全语言来实现新功能,同时保留现有代码,只进行错误修复,那么会发生什么呢?
…
在我们模拟的最后一年,尽管内存不安全代码的数量在增加,但内存安全漏洞的数量却显著下降,这是一个看似违背直觉的结果[……]。
为什么会违背直觉呢?如果你只是为了修复漏洞而接触内存不安全代码,那么内存安全漏洞的数量显然会减少。
我是不是漏掉了什么?
与直觉相反的是,现在用内存不安全语言编写的代码比以前更多了。即使只是修复漏洞。
错误修复并不是没有产生新的内存错误,但显然错误修复的比率要比全新代码的比率低得多。
我认为标准的假设是,你需要开始用内存安全代码替换旧代码才能看到改进。
相反,他们已经证明,只在新代码中使用内存安全语言就足以使错误总数下降。
自帕斯卡以来,半个世纪过去了。Ada 40 年。Java 28 年。Go 十五年。Rust 10 年。但不安全代码仍占大多数。
C/C++ 使用 libc 接口的要求,不是给其他语言在操作系统层面获得超大规模采用带来了巨大的下游挑战吗?
是的,但 Linux(也就是 Android)没有这个问题,因为它的接口是系统调用而不是 libc。
Android 的实用接口是 Binder,它的接口可以修改为更丰富的语言。
但系统调用不是需要 C/C++ 数据结构和类型定义吗?
因此,虽然从技术上讲并不 “需要 ”C/C++,但如果你的语言不能完全映射到 C/C++ 数据结构和类型定义,它就无法工作。
是的,这对竞争者来说是个问题,Linux/UNIX 内核都是用 C 编写的,除非我们想为所有编译语言添加特定语言的系统调用接口。
另一种方法是,用你选择的语言编写内核,然后选择适合该语言的系统调用规范,这样就能获得广泛采用。简单!
你无法真正用 Java 或 Go 编写一个 C 程序可以使用的库。真正的魔力在于无需 GC 和庞大运行时的内存安全。但如果你指出这一点,人们就会说你是狂热爱好者。
Pascal 和 Ada 都不是内存安全的。
在这两种语言中都有可能写出不安全的代码,但在 Pascal 和 Ada 中写出安全的代码要比 C/C++ 容易得多。在 C++ 中编写安全代码比在 C++ 中编写安全代码要容易得多。内存安全是一个范围,并非全有或全无。
是这样吗?
如果把每年编写的 JavaScript、C#、Java、Python 和 PHP 全部加起来,那就有很多代码了。
我们能确定所有这些加起来不比 C/C++ 多吗?或者至少有点接近?
这说明了一些问题……
[删除]
我们喜欢它。可以继续解决其他两百个问题了。
包括 CNE 下一步的发展方向;逻辑和网络 Bug。
说得好!确保新的、内存安全的代码与旧的代码互操作,并得到同样良好的工具支持,这需要大量的工作。
有一个 C 程序。有一群不合格的程序员,他们不使用旧的、有据可查的、稳定的、内存安全的函数和技术。他们编写的代码存在内存安全漏洞。
他们最终被迫过渡到一种新的语言,这使得内存安全错误变得毫无意义。在不解决他们仍然不合格这一事实的情况下,或者在不解决为什么他们一开始就不合格、为什么他们不使用内存安全函数、为什么我们一开始就让他们发布代码的情况下,他们继续制造更多不合格的代码,出现更多可以避免的安全错误。
他们继续编写更多不合格的代码,出现更多可以避免的安全错误。他们只是不再关注内存安全了。黑客们就会转移攻击重点,换一种攻击方式。
与此同时,没有人谈论房间里的粉红色大象。我们过去和现在都完全允许人们编写低劣的代码。我们允许人们不断使用错误的方法,从而导致完全可以避免的安全漏洞。像注入攻击这样的安全漏洞现在占所有 CVE 的 40%,而内存安全只占 25%。
我们是否可以专注于为更大类的安全漏洞提供默认解决方案?可以。有吗?为什么?因为这一切都与安全无关。程序员只是喜欢玩新玩具。安全只是一个幌子,用来证明允许人们继续编写低劣代码和玩新玩具是合理的。
安全问题将继续恶化,因为我们没有解决编写软件的方式问题。我们要处理的将不再是这一大类错误,而是数以百万计的小错误。实际上,处理这些问题会变得更加困难,因为我们再也没有 “内存安全 ”这个大敌可以指点江山了。
不,事实并非如此。你会在每个平台上发现业务逻辑错误、特定领域错误和系统编程安全错误,但你不会像发现内存安全问题那样发现它们的密度和自动严重性。这与 “语言潮人 ”无关。
人们花在代码正确性上的时间和精力是有限的。如果语言中充满了陷阱、规范问题、过时的错误特性、无端的平台差异、支离破碎的构建系统,那么人们就会浪费大量精力去管理这些无用的东西,而这些东西都会对编写健壮的代码产生积极的负面影响,而且除了语言上的纠结之外,人们还需要花费更多的精力去制作高质量的产品。
你不能总是依赖于完美的人。50 年来,我们一直在尝试这样做,但结果却是无休止的 CVE 和要求下次找到更好的程序员的呼声。
区别在于语言如何应对可能发生的错误。它可以做出 “哎呀,你犯了一个错误!在这里,修正这个错误”,然后让程序员应用修正程序,继续编写没有错误的代码。或者,这种语言会悄无声息地将最不有趣的代码中的最小错误放大为导致灾难性安全故障的损坏。
当字符串的连接和数字的安全相加是一件存在的事情,而且是一件需要顶尖技术的程序员才能完成的事情时,你就只能在蠢事上浪费人们的才能了。
我基本同意你的观点,但持相反立场:攻击内存安全错误已经取得了巨大成功,我们应该用同样的模式来攻击其他大类错误。
编写一种合理可用的语言,使注入/换码/污染/数据类型混淆错误几乎不可能发生,这绝对是可能的,但这需要语言支持和重写大多数库–就像内存安全所做的那样。不幸的是,我们正朝着相反的方向前进(我仍在为 javascript 的反标而生气,这一功能似乎只是为了移植 php 风格的 sql 注入错误而设计的)。
> 有一群不合格的程序员,他们不使用古老的、有据可查的、稳定的、内存安全的函数和技术。
…有哪些?
我认为这多少与安全性有关。没人能写出安全的 C/C++ 代码 “这句废话的意思是 ”我们的产品是垃圾,我们没有错,因为根本不可能写出安全的代码”。现在,我们可以假装修复 “一整类错误”(只要没有 “不安全 ”的地方),方法是将 Rust 强加给程序员(以及开源社区,然后他们将免费生产内存安全代码供一些人使用),虽然这可能确实有点帮助,但人们可以希望在未来十年左右的时间里避免真正为产品安全负责。
> 你们有一群不合格的程序员,他们不使用古老的、有据可查的、稳定的、内存安全的函数和技术。他们编写的代码存在内存安全漏洞。
我们真的不应该再把责任推给开发人员了。问题不在于开发人员不合格,而在于他们所使用的工具几乎不可能编写出安全的软件。在使用内存不安全的语言时,每个人都会编写内存安全漏洞。
这里有一个很有帮助的见解,即软件应用程序的安全状况实质上是开发者生态系统产生的一个新兴属性,这包括拥有安全设计的应用程序接口和语言。https://queue.acm.org/detail.cfm?id=3648601 对此有更详细的介绍。
发现应用级安全 bug 比发现内存安全 bug 难 100-1000 倍。
如果不必对任意指针建模,对代码进行形式分析也要容易得多。
Android 有确凿的证据表明,仅仅消除内存安全就能带来显著的不同。
让工具主动防止各类错误是一项值得努力的工作,但我同意,当其他几类大量漏洞不断出现时,它就会被过度关注。不过,在高层次上,让框架对所有开发人员强制执行 “x ”来提高最低标准要容易得多。但是,Rust 的普通开发人员在现实中却很难做到这一点。案例:https://github.com/pyca/cryptography/issues/5771。
我认为,很多关于 C++ 是 “内存不安全 ”的争论都有点可笑,因为编写内存安全的 C++ 实在是微不足道。只需运行 -Wall 并强制使用智能指针,在现代几乎不可能出现直接导致这些 bug 的原始指针或执行偏移量。极少数例外情况是,开发人员有足够的智慧,可以利用现代语言功能安全地处理这些问题。不幸的是,安全团队似乎很少关注这一点,因为他们都在追逐最新的闪亮语言,就像你提到的那样。
> 编写内存安全的 C++ 微不足道
不幸的是,这并非易事。这就是为什么在许多 C 和 C++ 项目中,70% 的严重漏洞都与内存安全有关。
其中一些原因包括 – C++ 在防止越界漏洞方面收效甚微 – 使用智能指针防止 “使用后释放 ”需要大量使用共享指针,这通常会产生性能代价,在使用 C++ 的环境中是不可接受的。
> 遗憾的是,并非如此。这就是为什么在许多 C 和 C++ 项目中,70% 的严重漏洞都与内存安全有关。
我认为这并不能反驳他们想说的话。如果绝大多数 C++ 开发人员都不遵守这两条规则,那么这并不能证明这两条规则提供了内存安全。
没错,但他们不遵循这两条规则的原因是,使用这两条规则就意味着不能使用大多数不遵循这两条规则的 C++ 库,而且会引入性能回归,从而否定了他们当初选择 C++ 的主要原因。
整篇文章都是关于逐步过渡的。你不必回避违反规则的库,你只需接受它们处于安全区之外的事实。
在性能方面,你必须对共享指针的要求更加明确。但我敢打赌,只有极少数 C++ 函数需要绝对最佳的性能,而且在遵循这两条规则的同时无法避免这些性能问题。
> 编写内存安全的 C++ 并不难。
这是非常大胆的说法,因此需要大量证据,因为实际上没有任何有意义的证据来支持这一说法。众所周知,现实世界中有一些非繁琐的 c++ 代码很少有缺陷,但几乎所有这些代码都需要付出极大的努力才能达到目标。