计算机业界相关安全话题
2015-03-11 11:22:13 阿炯

DDR3内存存在硬件漏洞-可被修改数据
美国国家安全局敦促机构和企业采用内存安全编程语言
美国CISA建议放弃C/C++以消除内存安全漏洞
白宫敦促开发者改用内存安全的编程语言
Java是最容易出现第三方漏洞的语言
25年经验资深安全从业者:从安全看Rust与C
苹果称Swift是C++的最佳继任者
美国CISA等机构再发警告:Linux中95%没用内存安全代码
美政府机构再想新招:开发AI工具将旧的C代码转为Rust
美政府给出最强硬要求:2026年前关键软件必须开始全面去C系编程语言


DDR3内存存在硬件漏洞-可被修改数据

在通常,黑客都是研究系统和软件等的漏洞,不过这次黑客把目标瞄准了硬件。根据外媒报道,黑客发现在某些类型的DDR芯片上存在设计缺陷,可以用来提升Linux系统权限。谷歌的Project Zero博客周一发表文章称,已经将去年科学家发现的在DDR3内存中存在的“位翻转”多次访问内存缺陷变为实际可行的攻击。

为了保护整个系统的安全性与完整性,DDR3内存每个大的内存块仅由一个给定的应用程序或操作系统进程访问的”沙盒”。黑客现在有能力来修改禁止的内存区域的内容,这种硬件级别的修改具有深远的影响。它可以允许一个用户或应用程序直接从内存中提升权限而无视系统的限制;黑客能够执行恶意代码或劫持用户或其它软件的数据,此漏洞只适用于更新的DDR3内存的类型。不过这个攻击对DDR4内存或者包含ECC纠错功能的内存无效。

安全专家马克西伯恩,称此攻击为”沙盒生成器和断路器”。攻击者并没有给出具体是哪些型号的DDR3内存容易受到攻击(理论上存在“位翻转”漏洞的DDR3内存都可能易被攻击),攻击演示的平台是运行X86-X64 CPU的Linux计算机,可能在其他系统平台下也可以进行攻击。虽然说起来很可怕的样子,但由于种种原因,现在这些攻击似乎是理论多于实际,这是因为:攻击只允许本地,而不是远程。更重要的是,攻击需要在短短的64毫秒进行超过54万次的内存访问。

安全专家表示如果想要真正解决这个问题,需要更换内存条,比如使用带有ECC功能的DDR3内存,或者升级平台使用DDR4内存。从攻击实施的代价来看,似乎这个漏洞只是看起来吓人而实际可能不会发生。

美国国家安全局敦促机构和企业采用内存安全编程语言

美国国家安全局(NSA)发布软件内存安全指南,敦促机构和企业弃用C和C++等老派编程语言,转向内存安全的编程语言,比如C#、Rust、Go、Java、Ruby和Swift。该机构中最大的情报部门在指南中写道:“NSA建议机构和企业尽可能使用内存安全语言,并通过编译选项、工具选项和操作系统配置等代码强化防御措施来加强保护。”

NSA的主要关切在于不法之徒可能会利用代码中内存管理不善的漏洞,而这种漏洞常见于能赋予程序员更多选择和灵活性的编程语言。在示例方面,NSA举出了黑客通过缓冲区溢出或利用软件内存分配缺陷入侵系统的例子。同时,内存安全语言综合采用编译时和运行时检查来自动封锁程序员失误所造成的漏洞。注意,尽管不能完全杜绝错误,但每减少一个都有帮助。比如说,涉及内存指针的不安全使用或并发线程间竞争的漏洞就能被此类编程语言捕获。NSA表示:“恶意网络攻击者可能利用这些漏洞发起远程代码执行或形成其他有害影响,常可损害设备并成为大规模网络入侵的第一步。”

很明显,我们最好避免这种情况。C和C++等通常使用的语言在内存管理方面提供了很大的自由度和灵活性,同时又严重依赖程序员对内存引用执行所需的检查。简单的错误可能导致可被人利用的基于内存的漏洞……NSA建议尽可能使用内存安全语言。使用静态应用程序安全测试和动态应用程序安全测试来分析软件[无法]使非内存安全的代码完全做到内存安全。[而且]对于恶意威胁分子说,绕过地址空间布局随机化(ASLR)和数据执行保护(DEP)并非不可实现……如果使用内存安全语言以及现有的代码强化防御措施,许多内存漏洞是可以被防止、被缓解或者很难被网络威胁分子利用的。

NSA网络安全技术总监Neal Ziring称,想要杜绝此类漏洞,开发软件时就必须始终使用内存安全语言和其他保护措施。不过,NSA确实认识到“内存安全”有点儿用词不当,这个概念范围太广,各人理解不同。

内存安全也有其自身的挑战。比如说,由于特定工具链不会生成内存不安全代码,额外的固有保护可能会先拖慢开发,尽管减少漏洞和增强代码可维护性终能值回票价。从一种语言转换到另一种语言可谓ASCII的老大难问题了,有时候甚至都没法换。例如Rust尽管功能强大,其学习曲线却很陡峭。

分析师公司SlashData的数据表明,2020年第一季度到2022年第一季度,Rust用户数量增长了两倍。Go语言也不遑多让,据称其开发者社区规模达到了330万人之巨。JavaScript则以1750万开发者在长达十年的时间里一直荣膺最流行编程语言称号。

至于C和C++,尽管普遍存在于当今计算机世界,但NSA与大多数人观点一致,都认为这两种编程语言很有问题。2022年9月,微软Azure首席技术官Mark Russinovich摆明态度,认为现在就应该停止使用这两种久经考验的语言编写任何新项目了。他承认,尽管自己偏向用Rust开发新工具,但“未来几十年(或更长时间里)仍有大量C/C++代码需要维护或发展”。就在发出此条推特的前一晚,Russinovich才往自己已有8.5万行的Sysinternals C/C++代码里又加进了几行。

网络安全公司Acronis首席信息安全官Kevin Reed向媒体透露:“我觉得NSA做得对。”“某种程度上而言,地址空间布局随机化(ASLR)和堆栈保护等缓解措施不过是个创可贴,谈不上是完整解决方案;转向内存安全语言是好得多的选择。”Reed补充道,然后附和了Russinovich的看法。“由于这些年来产出了大量C和C++代码,我不认为我们能立马看到效果,而且即使我们全都明天就开始使用Rust和Go,估计也得花上个几十年才能收拾干净这烂摊子。”

NSA《软件内存安全指南

安全的第一大“杀手”——内存漏洞

一直以来,内存安全的漏洞引起多个企业与开发者的警觉。根据长期关注内存漏洞的开发者 LazyFishBarrel 的统计,苹果公司的 iOS 和 macOS 系统中 60%-70% 的漏洞是内存安全漏洞。微软在 2019 年的一次会议上透露,从 2006 年到 2018 年,其发现的 70% 的漏洞都是因内存安全问题造成的。据 Google 估计,Chrome 中存在了类似比例的内存安全漏洞,另外 90% 的 Android 系统漏洞也都是内存安全问题。

为此NSA认为,黑客极有可能会利用代码中管理不善的内存漏洞,而这种漏洞在程序员使用灵活性更高的编程语言时更容易出现。于是,其最新发布了《软件内存安全之网络安全信息指南》时,写道,「黑客可以利用这些漏洞进行远程代码执行或其他不利影响,这通常会危及设备,并且成为大规模网络入侵的第一步」,因此 NSA 建议各个组织尽可能使用内存安全语言,并通过代码强化防御(如编译器选项、工具选项和操作系统配置)来增强保护。然而,众所周知,C 和 C++ 是编写核心系统软件的默认语言。这两门编程语言速度快,而且源代码可以直接汇编成机器语言。虽然一边有很多企业高管呼吁不要用,但另一边也有很多人不信邪,不愿相信 C、C++ 语言的不足之处。当然也有人提出质疑,“为什么非要弃用 C、C++ 呢,有什么理由不能在 C、C++ 编译器中强制执行内存安全吗?”

针对这一点,有开发者进行回应:这在以前就已经尝试过了。但挑战是双重的。首先,如果在编译器强制执行内存安全,范围也只能局限在编译器上。然而,真正的内存安全实际上是(至少)线程安全、空值安全和类型安全,以及大多数人所想的原始边界检查等各个方面。除非你打算进入托管语言领域(Managed Language)并引入 GC,否则你需要语言级别的结构来允许程序员在这些新的边界内有效工作。例如在 Rust 中,这就是 "所有权 "系统。

第二点是,如果总是把语言功能限制在一些有限的、更安全的范围内,或者用一些自定义的东西取代核心功能(例如 malloc 或编译器)。这就把你能使用的库限制在那些使用功能集的库上,并要求你无限期地维护这些核心功能。即便如此,你也不会得到 "真正的 "安全,因为这取决于每个人都很小心、不使用错误的功能、编译器,而你又非常确定你的核心实现本身是安全的。

因此,如果你全力以赴,与其需要一个特定的编译器,再加上一套不同的核心语言特性,再加上你需要确保所有的支持库都符合要求,再加上需要为静态分析和编译工具链定制支持工具,倒不如直接用一种新的语言,Rust 便是不错的选择。


美国CISA建议放弃C/C++以消除内存安全漏洞

美国网络安全与基础设施安全局 (CISA) 在2023年12月上旬发布了一份名为 “The Case for Memory Safe Roadmaps” 的文件,详细介绍了软件厂商应如何过渡到内存安全编程语言 (MSL),以消除内存安全漏洞。

CISA 认为,内存安全错误经常会造成重大损失,需要加以杜绝;因此敦促企业和技术领导者密切关注软件开发中的内存安全。而 C 和 C++ 等编程语言就是内存不安全编程语言的典范;它们可能导致内存不安全代码,但仍然是当今使用最广泛的语言之一。该指南文件由美国网络安全与基础设施安全局 (CISA)、国家安全局 (NSA)、联邦调查局 (FBI),以及澳大利亚、加拿大、英国和新西兰的网络安全机构联合制定。旨在敦促每家软件厂商的高级管理人员通过优先考虑实施 MSL 的设计和开发实践来降低客户风险;同时敦促他们创建并发布内存安全路线图,以便客户了解所面临的内存安全风险。

“MSL 可以消除内存安全漏洞。因此,过渡到 MSL 可能会大大降低投资于旨在减少这些漏洞或将其影响降至最低的活动的必要性。此外,将不安全的代码库迁移到 MSL 的投资将以更安全的产品的形式带来长期回报 -- 这也抵消了过渡到 MSL 的部分前期成本。”

文件指出,微软约 70% 的 CVE 是内存安全漏洞(基于 2006-2018 年的 CVE),Mozilla 的 34 个关键/高危漏洞中有 32 个是内存安全漏洞。谷歌 Chromium 项目中所发现的漏洞,有约 70% 是内存安全漏洞;且 2021 年的零日漏洞中也有 67% 是内存安全漏洞。虽然一些组织为了减少 C/C++ 内存不安全代码引入的风险,投入了大量资金用于开发人员培训。但 CISA 等机构认为,“虽然培训可以减少程序员可能引入的漏洞数量,但考虑到内存安全缺陷的普遍性,内存安全漏洞的出现几乎是不可避免的。”


鉴于此,其建议组织放弃 C/C++,转而使用 C#、Go、Java、Python、Rust 和 Swift 等 “内存安全语言”。

白宫敦促开发者改用内存安全的编程语言


白宫国家网络主任办公室 (ONCD) 于2024年2月发布了一份报告,呼吁科技界主动减少网络空间的攻击面;通过改用 Rust 等内存安全编程语言,减少内存安全漏洞的数量来提高软件安全性。同时鼓励研究界解决软件可测量性问题,以便开发出更好的测量网络安全质量的诊断方法。


ONCD 例举了历史上一些著名的网络攻击事件,包括:1988 年的 Morris 蠕虫病毒、2003 年的 Slammer 蠕虫病毒、2014 年的 Heartbleed 漏洞、2016 年的 Trident 漏洞、2023 年的 Blastpass 漏洞。并指出所有这些问题的背后都有一个共同的根本原因,即内存安全漏洞。报告称:“35年来,内存安全漏洞一直困扰着数字生态系统,但情况本不必如此。消除整类软件漏洞的挑战是一个紧迫而复杂的问题。展望未来,必须采取新方法来减轻这种风险。”“减少内存安全漏洞的最高杠杆方法是保护网络空间的构建模块之一:编程语言。使用内存安全编程语言可以消除大多数内存安全错误。”

在此之前,美国国家安全局 (NSA) 曾于 2022 年 11 月发布了关于软件开发人员如何防止软件内存安全问题的指南。美国网络安全与基础设施安全局 (CISA) 也在 2023 年 12 月发布了类似报告,要求过渡到内存安全编程语言,通过消除与内存相关的漏洞来减少软件产品的攻击面。ONCD 报告以美国总统拜登于 2023 年 3 月签署的国家网络安全战略为基础,将网络安全的责任从个人和小型企业转移到技术公司和联邦政府等更有能力管理不断变化的威胁的大型组织身上。并在与整个联邦政府的安全设计计划和研发工作保持一致的同时更进一步,涵盖了由 CISA、NSA、FBI 和 NIST 领导的计划和研发工作。

报告中有关内存安全的工作还补充了美国国会对此主题的兴趣。此外,美国参议院国土安全和政府事务委员会主席 Gary Peters (D-MI) 和美国参议员 Ron Wyden (D-OR) 也向 ONCD 强调了他们在内存安全方面的立法努力。

更多详情可查看完整报告

C++ 之父反驳白宫观点

白宫国家网络主任办公室 (ONCD) 此前曾发布了一份报告,呼吁科技界主动减少网络空间的攻击面;通过改用 Rust 等内存安全编程语言、避免使用 C++ 和 C 语言等易受攻击的语言,以减少内存安全漏洞的数量来提高软件安全性。C++ 之父 Bjarne Stroustrup 在2024年3月与 InfoWorld的采访中针对白宫的这些言论进行了反驳。“我感到惊讶的是,这些政府文件的作者似乎对当代 C++ 的优势和提供强大安全保证的努力视而不见。另一方面,他们似乎已经意识到,编程语言只是工具链的一部分,因此改进工具和开发流程至关重要。”

Stroustrup 指出,安全性改进始终是 C++ 开发工作的目标。从 C++ 诞生的第一天起,提高安全性就一直是 C++ 的目标。只要将 K&R C 语言与最早的 C++、早期的 C++ 以及当代的 C++ 进行就能看出差别。许多高质量的 C++ 都是使用基于 RAII (Resource Acquisition Is Initialization)、容器和资源管理指针的技术编写的,而不是传统的 C-style pointer messes。他还列举了为改善 C++ 安全所做的一系列努力。与安全有关的问题有两个。在数十亿行的 C++ 中,很少有完全遵循现代准则的,而且人们对安全的哪些方面是重要的概念也不尽相同。我和 C++ 标准委员会正在努力解决这个问题。

Profiles 是一个框架,用于指定一段代码需要什么保证,并启用实现来验证它们。委员会网站上有描述这一点的文件(可查看 WG21),并且还会有更多文件。然而,我们中的一些人没有心情等待委员会必然缓慢的进展。其允许我们逐步改进 guarantees —— 例如,相对较快地消除大多数 range errors,并通过本地静态分析和最少的运行时检查逐步将 guarantees 引入大型代码库。我对 C++ 的长期目标一直是在需要的时候提供类型和资源安全。也许当前对内存安全的推动 —— 我想要的 guarantees 的一个子集 —— 将有助于我的努力,C++ 标准委员会中的许多人也认同这一点。

Java是最容易出现第三方漏洞的语言

云应用监控和安全平台 Datadog 于2024年4月中旬发布了“2024 年 DevSecOps 状况”报告,称通过分析以各种编程语言编写的一系列应用程序的安全状况发现,Java服务受第三方漏洞的影响最大。90% 的 Java 服务容易受到第三方库引入的一个或多个严重或高严重性漏洞的影响。而其他编程语言如 JavaScript 的这一比例约为 75%、Python 的为 64%、.NET 为 50%,平均数则为 47%;且 Java 服务也更有可能被积极利用。报告发现在 CISA 公布的一份被威胁者利用的漏洞运行列表,即已知漏洞利用(KEV)目录中,Java 服务所占比例过高:55% 的 Java 服务受到影响,而使用其他语言构建的服务只有 7%。


即使在关注特定类别的漏洞时,这种比例过高的模式也仍然存在。例如 23% 的 Java 服务容易受到远程代码执行 (RCE) 的攻击,影响了 42% 的组织。Datadog 认为这一高比例可能是由于流行的 Java 库中存在许多普遍存在的漏洞,例如 Tomcat、Spring Framework、Apache Struts、Log4j 和 ActiveMQ。

“当我们检查这些漏洞通常起源于何处时,这一假设得到了加强。在 Java 中,63% 的高危漏洞源自间接依赖项,即间接与应用程序打包在一起的第三方库。这些漏洞通常更难识别,因为它们出现的附加库通常会在不知不觉中引入到应用程序中。”

鉴于此,报告建议开发人员在扫描应用程序漏洞时需要考虑完整的依赖关系树,而不仅仅是直接依赖关系。了解添加到应用程序中的任何新依赖项是否维护良好并经常升级其自身的依赖项也很重要。报告的一些其他主要发现还包括:
1.虽然来自自动安全扫描器的攻击代表了最大数量的利用尝试,但其中绝大多数攻击是无害的。在发现来自此类扫描程序的数千万个恶意请求中,只有 0.0065% 成功触发了漏洞。
2.即使短期凭证更实用、更安全,但仍有许多组织在其 CI/CD 管道中继续依赖长期凭证(这是造成数据泄露的最常见原因之一)。 63% 的人至少使用过一次长期凭证来验证 GitHub Actions pipelines。

更多详情可查看完整报告

这份报告无疑为 Java 社区敲响了警钟,其作为一种广泛使用的编程语言,其安全性直接关系到大量应用程序和服务的稳定运行。因此,开发者和安全团队需要更加重视 Java 服务的安全漏洞管理,尤其是在第三方库的选择和管理上。此外,这也提醒了整个行业,在快速发展的技术环境中,持续的安全审查和及时的更新补丁是保护关键基础设施不受攻击的关键。对于企业和组织来说,这意味着需要加强对 Java 服务的安全监控和漏洞修复工作,同时也要考虑多元化的编程语言策略,以降低单一语言漏洞对整个系统安全的影响。总之,Datadog 的这份报告不仅揭示了 Java 服务在安全方面的薄弱环节,也为整个 IT 行业敲响了安全警钟。


25年经验资深安全从业者:从安全看Rust与C

作者 | John Viega;译者 | 王强

本节作者 John Viega 拥有 25 年安全和开发经验,喜欢研究编译器,是 AES-GCM 加密算法的联合作者,曾是 McAfee 首席安全架构师、SaaS 业务部门的 CTO。并在纽约大学担任兼职教授至今。撰写了多本关于安全的书籍,包括《安全神话》、《构建安全软件》和《使用 OpenSSL 进行网络安全》等。是 IEEE 安全与隐私杂志的前主编。

他在本节中围绕行业长期存在的,关于 C/C++ 安全性不足及其应该被 Rust 等语言替代的话题展开了讨论,并发表了一些独特而有价值的观点。原文内容较长,译文删减了一部分不重要的内容以方便阅读。

前不久我又看到了一篇非常轻视内存安全性,觉得这方面没什么必要做改变的文章,然后我又看到一些安全专家对这篇文章回应说,想要保障安全、负起责任就得尽快放弃 C 和 C++ 才行。这篇文章就是我对这个话题的分析,我会尽可能覆盖所有方面,尽量让行业内的读者都能理解我的意思。


太长不看版本

1.安全问题确实比很多人想的要严重得多,很多人应该立刻拒绝在新项目中使用 C/C++ ,理由也不仅是安全性考虑。但是!

2.让我们的应用程序摆脱已有的所有 C 代码的成本和风险远高于许多人的想象;某些关键软件的替代品需要十年或更久才能成为主流,而且新软件的整体效益并不明显。

3.安全性这个领域有许多隐藏的复杂性,所以“Rust 比 C 更安全”这句话可能是正确的,但因为那些复杂性,实际情况没那么简单。

4.选择编程语言这种事情看似简单,经济层面实际上非常复杂。安全性不是唯一的非功能性考虑因素,而且无论你做什么,总会在某个地方存在内存不安全的代码(只要底层架构本身是不安全的),而且试图快速摆脱 C 代码会带来许多负面后果。

5.系统语言被过度使用了;C vs Rust 的二选一其实是伪命题,因为像 Go 这样的编译语言在经济上往往是一个更好的全方位答案。特别是其有足够好的性能,足以满足绝大多数用例,可以是安全的,并且可以很好地访问底层系统 API。


一些安全人员已经怒气冲冲了

我曾看到一位安全人员和业务部门争吵不休,于是我问他:如果你认为安全是首要问题,那你为什么还要使用电脑?

人们每天都愿意承担风险。我们知道,只要我们外出,就有可能感染某种病毒。我们知道,只要我们上车,就有可能发生事故并丧命。

但众所周知,我们倾向于大大高估或低估自己的风险水平。

一般来说,安全行业可能认为普通人大大低估了风险。过去这个观点可能是正确的,但现在的世界已经不同了。曾几何时,大多数行业都大大低估了风险,网络连接可能被轻易篡改、代码可能被轻易攻破,没有例行补丁,也没有广泛的隔离或其他有效的权限机制。

但由于整个安全行业的辛勤工作,其他科技界从业人员最终承认他们错了。安全行业产生了巨大的影响力,从硬件架构到网络协议,再到编程语言设计都开始重视安全性。这条路走下来并不轻松,因为我们常常无法真正理解来自其他领域的专业观点。

如果我们能牢记这一点,行业就能取得更快的进步,获得更好的信誉。我们需要这样做,因为正如我们所看到的,要做的事情还有很多,而且还有很多重要的变化根本无法快速实现。


内存安全问题有多严重?

内存安全往往被认为是最严重的漏洞类别,因为这个层面的漏洞经常可以获得最高权限。漏洞经常可以被远程利用,有时甚至不需要任何身份验证。但有观点认为内存安全漏洞数量众多,老练的攻击者很容易找到并利用它们,这种说法是错误的。

世纪初的时候情况的确是那样,但现在不一样了。从安全角度来看,内存不安全代码的影响肯定还是非常大,但还没有高到你要顶着经济压力为它换成内存安全语言的程度。


风险状况已经变了

我坚决承认相比 C/C++,其他语言本质上更安全;我只是在质疑“它们的安全性到底强多少?尤其考虑到我们已有的很多安全措施时。”

世界发生了许多变化,直接影响了风险水平(双向),包括:

1.我们使用的硬件架构和操作系统都很好地帮助我们阻止了漏洞利用行为,同时又不牺牲太多性能。

2.C++ 专心围绕其标准库打造用户界面,试图确保普通 C++ 用户不太可能使用内存安全可能有问题的 API。另一方面,虽然 C 已经发展成为一种语言,但在这方面却更加保守。

3.全面披露运动发生了,漏洞研究成为一个职业领域,结果最常见的 C 组件受到了相当多的审查,大大提高了 C 程序员对这类问题的认知。

4.学术界不再教授 C++,转向 Java,然后是 Python。

5.越来越多适合做系统语言的新语言出现了,其中较为关注内存安全的 Rust 最引人注目,还有 Zig、Nim 和其他几种语言也比较重视内存安全。

6.向云端迁移的趋势以及现代技术栈的其他很多组成部分都是很好的抽象,但它们确实会增加攻击面。另一方面,它们也让分区隔离更加常见,从而减少了攻击影响。由于我们不再将那么多可逆二进制代码交到人们手中,专有代码往往会因为对外部没那么透明而获得更好的安全性(尽管容错设计确实经常让攻击者可以根据需要自动执行尽可能多的“攻击”)。

考虑上述情况:

1.C 和 C++ 代码中的许多内存问题被报告为可利用的,即使在实践中确实存在相关漏洞无法被利用的情况。很久以前,如果我看到一个明显的内存错误,那么它不仅很有可能被利用,而且我还可以很容易地构建一个有效的漏洞。我同意,对于我们这个行业来说,最好只是假设我们发现的任何内存问题都是可以利用的。然而,想要有效利用漏洞变得非常困难,并且已经从一种轻松获得的技能变成了非常罕见的技能。

2.漏洞研究领域的经济学往往会扭曲风险认知。这是因为此类错误在经济层面是最有价值的,但也意味着对比不同语言的 CVE 可能没什么意义。此外,有价值的漏洞要么出现在不太可能被利用的地方,要么会进入“负责任的披露”周期,被修补从而消除风险。

3.有时,人们之所以写 C 代码是有自己理由的。理解这些理由很重要。很少有新手觉得自己必须学 C 才能成为出色的程序员,但在嵌入式系统等场合,C 往往是更为实用的选择。

为什么对比不同语言的 CVE 数量容易误导人呢?举个例子——Linux 内核最近正式获得了为自己的代码库发布 CVE 的能力。但在他们看来,任何错误都可能带来他们不了解的安全隐患,因此 Linux 内核中发现的每一个错误现在都有自己的 CVE,尽管它们大多不是可利用的内存问题。


漏洞的利用可能性降低了

一方面,找到好的漏洞变得越来越难,这一事实并不重要,因为如果你不再用 C,这类问题就不再是问题了。另一方面,漏洞研究人员比从前更努力,找到的漏洞却少很多,这表明实际的风险比以前更低了(尤其是在有良好的补偿控制措施的情况下)。如今,我倾向于认为许多 C 程序中存在问题的几率很高,但如果你有正确的设计,并花钱请合适的人来审查你的代码,那么找到下一个错误的经济成本就足够高了。

我刚开始进入这个领域时,工作往往很简单。如果你能找到一个数组形式的局部变量,那么你很可能可以诱使程序在数组之外写入。而且内存布局很容易预测,因此那时候我们很容易利用这种情况。

具体来说,局部变量经常保存在程序栈中。当你进入一个函数时,数据会被推送到堆栈上,你退出时,数据会从堆栈中弹出。这与函数调用后仍存在的长期内存分配(堆存储)是不一样的。

例如,过去经常看到这样的代码:

void open_tmp_file(char *filename){
char full_path[PATH_MAX] = {0,};
strcpy(full_path, base_path);
strcat(full_path, filename);
// Do something with the file.
}

对外行来说上述代码可能看起来没什么问题。它创建一个数组,该数组初始化为零,其大小为操作系统支持的路径的最大大小。然后,它将某个 base_path 名复制到该数组中,最后将文件名附加到该数组的末尾。但即使在今天的系统上,如果攻击者可以控制这个函数或 base_path 的输入,也很容易让程序崩溃。

部分原因是 C 不跟踪内容的长度。在 C 中,strcpy 逐字节复制,直到遇到值为 0 的字节(所谓的 NULLbyte)。类似地,strcat 的工作方式是向前扫描到 full_path 中的第一个空字节,并从 filename 中复制,直到找到 NULL。在任何情况下,这两个函数都不会根据 fullpath 的长度来检查它们正在做什么。因此,如果你可以传入超过 PATH_MAX 减去 len(base_path) 个字符,你就能写入超出缓冲区末尾的数据。

过去,程序栈将自己的运行时数据与用户的数据混合在一起,这就是传统的栈溢出如此容易实现的原因所在。

每次调用一个新函数时,堆栈都会获取程序应该返回的位置的内存地址。因此一旦你发现了其中一个条件,你要做的就是制作一个利用漏洞的有效载荷(你的恶意数据),让它覆盖这个返回地址,用指向你自己的有效载荷的指针替换它……载荷里一般还包括可以执行任何操作的可执行指令。有效载荷的可执行部分通常称为 shell 代码,尽管获得交互式登录权限(即 shell)不一定是你的目标。无论用哪种方式,当漏洞成功被利用时,攻击者往往能够从这里运行他们想要的任何代码。

从技术角度来看,当时最复杂的东西是 shell 代码本身,因为它一般至少需要汇编级知识。但你用不着自己编写 shell 代码,有很多现成代码可用。


为什么不总是执行边界检查?

好问题。有人可能会认为,我们只需让编程语言每时每刻都生成代码来检查所有访问边界,就能解决问题。

但如果到处都做代码检查,绝对会对性能产生显著影响,而且肯定会有些领域是无法忽视这种影响的。

例如,如果你是 CDN,并试图以经济高效的方式处理大量连接,那么代码检查的额外硬件成本很可能是无法承受的。

而且,用 Python 编写的单个应用程序经常“足够快”,但如果 Python 运行的每一段代码都经过完全的边界检查,它还会那么快吗?

我们使用的大多数软件都大量使用 C 或 C++ 编写的底层系统代码。不仅操作系统是用这样的语言编写,常见的编程语言在运行时利用的许多库也是如此。

当然,可以“用 Rust 重写它”。但即使我们应该这样做,显然这也是一段漫长而艰巨的旅程。

请注意,Rust 能够接近 C 的速度,部分原因是编译器可以在编译时“证明”它可以跳过大多数边界检查。

实际上,基于 C 设计 API 并不难,严格使用的话同样可以避免内存错误,同时最大限度地减少生成的运行时代码。例如,在上面的例子中,很容易为字符串提供一个不一样的 C API,始终跟踪长度并进行全面检查。我们可以为数组和其他数据类型提供类似的 API。我们甚至可以为指针提供这样的严格性。

这实际上是 C++ 做的很成功的事情。从 API 文档来看,它确实很容易做到永远不出现内存问题。

但实际上,经济因素通常是软件领域最重要的考虑因素,经济上的投资更关心的是软件使用者的体验(比如说有些情况下使用者最关心的是性能)。

因此,如果你正在编写底层系统代码,那么它必须有广泛的适用性,并且在大规模使用的场景中成本不能很高。所以我们需要权衡成本和风险,经济因素无法被忽略。


越界内存错误:缓解措施的发展历史

我们应该愿意接受多大的风险这个问题,引出了下一个问题“我们目前接受多大的风险?”

因为如果答案是“不多”,那么我们就需要考虑是否值得付出代价来增加边界检查。

实际的风险水平很难准确量化。共识是,世纪初的 C/C++ 程序绝对满身筛子。但漏洞缓解措施经过了四分之一世纪的发展,世界已经完全不同了。证明一个漏洞可以被利用变得非常困难,标准确实越来越高了。

拿前面提到的内存漏洞代码示例来说,虽然它的确是内存错误,但这并不意味着它很容易被利用,连有可能被利用都不一定。尽管上面的代码很糟糕,但 StackGuard 早在 1998 年就很好地解决了这个问题。其基本思想是,当程序启动时,选择一个至少 64 位的随机数。每次调用函数时将其推送到堆栈上。然后,每次从函数返回时,检查随机金丝雀值是否完好无损。如果不是,则崩溃而不是返回。除非程序以某种方式泄露了它的金丝雀,否则漏洞就不会被轻松利用了。

软件开发社区(包括“坏人”和漏洞研究社区)不得不更加努力地研究绕过技术,但如果情况合适,他们至少会找到一些可以绕过某些缓解措施的情况。例如,如果内存是动态分配的,上述缓解措施就不起作用,因为这块内存是单独保存在堆中的。

当然,程序不会将函数返回地址保存在堆中。但许多真实程序,尤其是使用 C++ 动态调度的程序,会将指向函数的指针保存在堆中,并使用它们动态选择要调用的函数。

地址空间布局随机化(ALSR)是一种更有效和广为人知的防御措施,它是在操作系统级别实现的。基本上,每次程序启动时,操作系统都会尽可能随机化数据所在的位置。有了 ASLR,如果注入了足够的随机性,攻击成功的概率将非常低,可能需要像宇宙中的原子一样多的尝试次数才能成功。

你可能会问两个重要的问题:

1.系统难道不应该让用户的程序数据远离其内部状态吗?

2.如果有效载荷必须存在于堆或堆栈中(其他内存通常不可写),我们难道不能阻止这些区域运行代码吗?

对于问题 1,每个线程一个堆栈不仅更容易实现,而且通常速度更快,因为硬件通常会直接支持程序栈。最后,虽然进程有虚拟地址空间可以保护其免受其他进程的影响,但在一个进程中,进程中的任何代码都可以寻址进程中的任何内存单元。

不过,注入随机性还是很有价值的。例如,在堆溢出中,函数指针是诱人的目标。将所有函数指针存储在静态分配的表或单独的内存堆中,绝对比将函数指针随意散布在任何地方的典型方法要好。

至于第二个问题,在系统级别上绝对可以防止代码在堆栈或堆外执行,这是一种非常值得采取的缓解措施。但是,某些环境(包括某些完整的编程语言)需要使用其中一种区域来实现自己的动态功能(例如 lambda 函数,它们是闭包)。不过对于大多数程序而言,这种缓解措施基本上是免费的,并且进一步提高了安全性。

如今,当出现内存问题时,攻击者通常不能指望自己可以直接运行代码。但想象一下你正在攻击用 Python 编写的程序,你能以某种方式利用 Python 底层 C 实现中的内存错误来写入内存中的任意位置。在底层,Python 实现了一个虚拟机。堆或堆栈中可以存在一些“指令”,这些指令由 Python 的内置代码检查,并且该代码根据指令执行不同的操作。

事实证明,当我们谈论内存“不可执行”时,实际上指的只是直接在底层系统处理器上执行的内容,而不是在应用程序级虚拟机中发生的事情。因此,即使你攻击的程序的可执行代码段不可写,并且你可以写入的所有数据都不可执行,你仍可以更改控制可执行代码执行操作的数据。

作为攻击者,如果没有虚拟机,你可以使用一种称为“返回导向编程”(又名 ROP)的技术自己创建一个。基本上,你可以利用内存错误尝试整理程序的数据,让它在程序的内存中跳转,执行你希望它执行的操作(通常目的是让它生成登录 shell,然后你可以合法地再次运行任何你想要的操作)。

ROP 不简单,因为它通常要求攻击者在堆栈和堆上整理数据,这本身就很难。添加地址布局随机化后,你会发现大多数涉及越界写入的内存错误实际上都难以利用,通常需要将多个错误链接在一起,并且往往还需要应用 ROP 才能用上漏洞。

最近,英特尔推出了控制流完整性(CFI)这个选项来显式阻止 ROP。我们说过将返回地址移出堆栈通常是没有意义的。英特尔认为让大家在实践中不再这样做太难了,但相反,如果将返回地址复制到一个影子堆栈上;然后,当函数返回时,它会确保返回位置是一致的——这显然可以有效防止堆栈溢出。

如果攻击者不去直接写入堆栈怎么办?例如,使用 ROP 时,人们通常只会以一种导致跳转失败的方式操纵数据,这将运行他们喜欢的代码。当该代码遇到“返回”语句时,它可能会返回到 CFI 期望的位置上。但 CFI 还可以验证调用站点。ROP 经常会跳转到函数中间,而 CFI 可以阻止这种情况。而且,它可以确保函数只从应该被调用的地方被调用。

CFI 不会阻止我们对 Python 虚拟机的攻击。但对于没有嵌入虚拟机的程序,使用 CFI 很可能会让 ROP 式攻击变得更加困难。

总之,虽然我们可能无法很好地量化现代缓解措施阻止了多少百分比的内存漏洞,但如果程序应用了所有容易获得的系统缓解措施,很可能很多这样的错误根本就无法利用了(大多数应用程序中的许多缓解措施都是默认启用的,但 CFI 足够新,并且没有广泛使用,有些地方没有应用这些缓解措施)。

的确也有水平很高、拥有创新精神的专家可以绕开缓解措施,但他们中的大多数要么会向政府提供漏洞信息,要么会负责任地披露漏洞,这通常意味着如果你能及时打补丁,风险就可以得到很好的缓解。

我预计随着时间的推移,硬件平台将继续提高标准。如果我们足够幸运,再过十年,这些缓解措施的效果甚至可能达到与完整边界检查一样好的水平,但成本却要低得多。而在此之前,人们有理由相信,尽管这些问题确实令人担忧,但补偿控制方面的投资已经非常充足了。


其他内存错误

在 C 和 C++ 中,越界访问并不是唯一符合“内存错误”定义的情况。

这些语言需要用户手动承担分配和释放内存的责任。它们确实带有用于帮助用户管理内存的库,但与其他许多语言不同,你仍然需要决定何时以及如何释放堆内存。除了纯粹的数组边界错误之外,还有许多问题,包括:

1.如果你释放了仍在被其他对象使用的内存,则可能会泄露敏感信息(例如指向 ROP 目标的指针),部分原因是 C 不会将内存清零或确保这些内存在使用前写入内容。这类“释放后使用”错误也很容易导致任意数据写入。

2.如果你可以让程序释放已经释放的内存,就可以破坏内部的内存管理数据结构,这也可能带来严重后果。在分配内存之前,只需在同一内存上调用两次 free() 即可结束游戏。

3.如果程序要做数学运算来确定要分配多少内存,并且攻击者可以强制这个运算得出足够大的结果,那么这个数字可能会“回绕”,导致分配的内存比需要的小,并强制缓冲区溢出(整数溢出问题)。

此类问题在代码中相当常见,因为我们通常很难推断何时释放内存,而手动执行这种操作的 C 程序员经常会出错。自动内存管理(例如垃圾收集)通常有助于解决大多数此类问题……除非你可以利用这种内存管理机制来发起攻击。确实,垃圾收集语言中的内存管理器往往非常复杂,并且有多个垃圾收集器出过几次严重漏洞,其中包括了大多数浏览器 JavaScript 引擎。


现代缓解措施

如上所述,C++ 已经做了很多工作来帮助程序员避开上述问题:

1.标准库的编写方式是,当函数返回时,大多数资源(特别是堆栈分配的内存)将自动释放,而无需用户执行任何操作。

2.类似地,C++ 的库完全避免使用原始指针,并使用了通过“引用计数”自动管理内存的包装器。

3.C++ 标准数据结构的 API 可防止数组边界错误。

4.C++ 有一个深层类型系统,对于这些类型,我们通常可以对程序的安全性充满信心。这意味着如果你一直处于这些类型的防护范围内,就可以捕获所有类型错误。

5.C++ 也早有垃圾回收了。Boehm Collector 于 1988 年问世,至今仍在维护。

6.C++ 有大量静态分析工具可以识别代码何时不遵循 C++ 核心指南。

许多 C++ 程序员都从上述措施中受益。相比之下,C 程序员往往没有这些好处可以享用。虽然 C 标准与 C++ 非常像,但 C 在更改方面更加保守,并且不会像 C++ 那样添加很多细节。

C 程序员也有能用的:

1.C 有垃圾回收器。其实 C++ 的 Boehm 垃圾收集器在 C 里一样好用。

2.C 有相当不错的静态分析工具,主要以 Clang 编译器生态系统为中心。

但 C 更多将其定位为一种可移植的汇编语言。我们稍后会回到这个问题。


为什么你的观点如此偏颇?

对于局外人来说,本文目前的结论可能与你听到的“证据”相悖。例如,我听到很多人声称“大多数 CVE 都是由内存安全问题引起的”。我最近又读到一篇文章给出了一个有趣的统计数据:当时(2024 年 3 月),C/C++ 代码中有 61 个 CVE,但 Rust 代码中只有 6 个 CVE。

当然,所有这些数据都是真的。但它们并不能准确反映风险水平:

1.我们已经确定,内存错误与可利用的内存错误是不一样的,后者更为重要。

2.C/C++ 就像小强一样,你可能不会每天都看到它,但如果你知道在哪儿找小强,你会发现它无处不在。虽然 C/C++ 一直不是最受欢迎的语言,但旧的构建块仍被广泛使用和维护。而且这两种语言对于新的系统级代码来说仍然很常见。因此,对于我们使用的大多数软件,无论它是用什么语言编写的,底层可能都存在用 C/C++ 编写的关键隐藏依赖关系。因此,如果说 Rust 的 CVE 总数是 C/C++ 的 10%,实际上这个数字感觉要么对 Rust 来说高得惊人,要么对 C/C++ 来说低得惊人,因为与 C 和 C++ 相比,Rust 的生产代码实际上非常少。

3.即使所有存在 CVE 的 C/C++ 错误都很容易被利用,由于经济因素,数据也肯定会偏向于内存漏洞。

玩这个游戏的漏洞研究人员通常只用一个漏洞就能赚到普通技术工人一年以上的薪水。而且绝对有人每年能卖出不止一个这样的漏洞。

一般来说,如果我是购买漏洞的一方,对我来说最重要的考虑因素有:

1.可靠性。由于很多因素(包括随机性),许多漏洞只有在极少数情况下才会起作用。然而,有些漏洞绝对每次都能奏效。

2.普及性。我会关心要攻击的目标主要使用哪些软件。

3.隐身。一般来说,我希望任务不被发现,并将丢失工具的风险降至最低。

4.执行。一般来说,完全执行能力是必须的。如果我有完整的执行能力,就可以做各种各样的事情。

5.持久性。理想情况下,错误应该是不太可能被发现和修复的。

有一些错误类型(比如命令注入攻击漏洞)会影响大多数编程语言,它们在上述许多方面得分都很高。但一个真正好的内存错误通常会得分更高。由于内存错误的固有价值,以及实际查找和利用此类错误的技术挑战,内存错误是漏洞研究界最负盛名的错误类型。因此,它们比更平凡的问题受到更多的关注。这意味着在其他语言编写的代码中很容易出现大量相当容易实现的漏洞,你更有可能看到这种漏洞出现在你正在使用的代码中,让你面临更大的风险。


人们为什么不换成其他选项呢?

到目前为止,我们已经看到 C 确实比任何高级语言都更容易受到内存错误的影响。虽然缓解措施相当有效,但它们还不足以成为人们不换语言的唯一原因。那么阻止人们切换语言的关键因素是什么?

为了讨论,我们会像经济学家一样假设人们是理性的。

首先,即使每个人都同意 C 应该消亡,这也需要很长时间。连 COBOL 应用程序都还在继续使用,很多公司会觉得替换 COBOL 程序要付出大量成本、面临很大风险,经济因素让这种语言继续存在下去。而 C/C++ 比 COBOL 更普及,近 50 年来它们一直是最重要的软件技术基石。

每个主流操作系统都是主要用 C 语言编写的。我们每天使用的大多数网络服务主要用 C 或 C++ 实现。在嵌入式系统中,使用 C 或 C++ 以外的语言几乎闻所未闻。即使在 Rust 领域,你也会发现一些 C。

其中一些原因包括:

1.多年来,许多 C 代码已被广泛使用和信任,世界将不得不承受巨大的痛苦才能迁移。

2.在更安全的语言中,许多底层任务需要做的工作超出必需,特别是因为它们更具防御性。

3.许多嵌入式环境的约束太大,以至于将更多具有大量依赖关系的编译系统语言引入桌面是不可行的。与 Rust 相比,C 非常紧凑和简单。

4.谈到市场上有资格进行底层编程的开发人员时,你会发现 C 和 C++ 程序员是最多的。

5.Rust 不是特别容易阅读或编写,而且目前还没有其他值得一提的选项。要求那些花了 20-30 年时间磨练 C 或 C++ 技能的人们抛弃自己的知识积累是不现实的。

如果你是谷歌或微软,你会抛弃几十年来一直表现得很强大的代码,并且相信替代方案可以涵盖过去几十年的所有极端情况吗?尤其是你还知道新方案并没有很好的知识库积累?我们今天运行的软件已经建立了一些信任度。当然,在糟糕的过去,Sendmail 很受欢迎,但漏洞百出,因为它不是防御性编写的。在 Unix 方面,Postfix 已经积极开发了 25 年,并且是用 C 编写的。虽然它的安全记录并不完美,但它一直很好用,并且从一开始就充分利用了最小特权原则。

尽管 Postfix 从一开始就更加注重安全,而且更容易配置和使用,但它花了至少十年(可能更长)的时间才基本取代 Sendmail。它不仅花了很长时间重新发现那些可能破坏业务的重要异常,还用了很久向担心业务中断的人们证明它足够成熟,不会造成这种风险。

因此,为了“用 Rust 重写它”,并成功取代 Postfix(更不用说 Exchange),我们必须:

1.从头开始构建符合所有标准的东西。

2.获得足够的实际用户群,以找到并处理大量在成熟的软件中已经解决掉的“陷阱”。

3.构建足够的可用性,让人们可以考虑大规模使用它。

4.跟上邮件世界中的一大堆新趋势(多年来已经有很多新增功能,特别是在安全领域)。

5.慢慢说服世界迁移过来。

在最好的情况下,这个过程也需要很久,结果要十年后才能看到。人们很难对一个长达十年的项目感到兴奋,因为它取代的东西并没有那么大的风险。上述论点也同样适用于大量用 C 编写的传统基础设施。


C 的长期优势

在我看来,Rust 走进内核是一项令人印象深刻的成就。多年来,许多人一直在游说,希望大家允许 C++ 进入内核,但这个目标从未实现。反对它的论点很简单——C++ 有太多的抽象。内核基本上位于软件栈的底部,不应产生任何不必要的成本,因此内核团队需要能够推理性能问题,这意味着他们需要能够看到 C 代码如何映射到生成的汇编代码上。

从经验上讲,Rust 在这方面确实更接近 C。然而,Rust 在这方面肯定不比 C 好,而且在某些任务上 Rust 非常碍事。

许多任务足够底层,是真正的系统级任务,例如内存管理、设备驱动程序、公开新硬件功能等。是的,你可以在 Rust 中做这些事情,但与 C 相比这很费力,而且经常需要利用 Rust 的“不安全”功能,于是会承担同样的风险。那么为什么不用 C 来编写呢?

此外,就像 Linux 内核不包含标准 C API(因为它们在这种情况下没有意义;它们在需要时提供自己的内部 API)一样,Rust 不能使用自己的 API;它必须使用内核的。

我们使用的硬件架构在指令级别提供的内置安全性非常少。当然,即使是 C 的可怕的基本类型系统也比直接向架构写入代码要好得多。在任何现实软件系统中,如果你深入到最底层,总会有代码需要针对这些不安全的平台来编写。

此外,绝大多数嵌入式系统都只用 C。资源有限的不仅是 CPU:

1.内存可能非常宝贵,包括堆栈空间、磁盘空间、缓存、寄存器等。编译后的可执行文件的大小可能是一个问题,运行时垃圾或肥胖抽象(如果存在)占用的任何不必要的空间也可能是一个问题。

2.这种环境的工具通常不适合其他语言。仅仅获得一个为平台生成代码的编译器可能就是一个巨大挑战,更不用说提供专门针对该平台的工具了。

3.由于许多因素,包括空间限制和测试周期可能涉及的难度,这样的环境通常几乎没有外部依赖项。即使是这些环境的标准库也可能完全缺乏线程等基础内容。如今,较新的系统语言一直在努力让它们的标准库瘦身,但它们还是太大了(而且构建时间也太长,尤其是 Rust),在嵌入式领域“不够好”。

C 在这个世界中蓬勃发展,这是 C 标准积极瘦身常用 API 的众多原因之一,它在这方面的努力和成果远超其他语言。

不幸的是,嵌入式世界往往不支持许多让 C 变得安全的常见缓解措施。从安全角度来看,它们还有其他缺点,例如没有足够的资源来做基础的加密工作,升级打补丁也没那么容易。然而,这些限制更多是商业权衡。大部分嵌入式软件出于某种原因都运行在低端硬件上,如果它们承担得太多,产品就没竞争力了。而且更强大的硬件需要更多的电量,对于可穿戴设备或其他可能需要长时间使用电池供电的设备来说,能耗比安全性可能更重要。而 C 语言实际上是唯一一种愿意正确服务此类环境的非汇编语言(而 C++ 是唯一一种在嵌入式领域真正有吸引力的替代选项)。

人们确实喜欢称 C 为“便携式汇编程序”。但在我看来,C 比汇编高级得多。只有无法用 C 正确完成(或无法轻松正确完成)时,我才会使用汇编语言,但这时一般也就涉及几条指令。当我不得不使用汇编语言时,我也会浪费很多时间,因为这种情况很少见(而且每个现代架构都非常复杂),我不得不在文档上花费更多时间。

C 比汇编语言更高级,但比其他系统编程语言(C++ 和 Rust)都更底层。它基本上介于两者之间,抽象出了许多平台可移植性问题,但仍然足够基础。如果你了解架构和编译器,就可以通过查看 C 源代码来可靠地预测它将生成什么代码。

有很多任务最好在这个级别完成。用 Rust 完成此类任务不会像汇编语言那样困难,但无论如何,换掉“不安全”的代码块仍需要很多额外的工作。对此类“不安全”块的替代需求越多,越有可能亏本。

我认为我们最好使用有意义的轴来对语言进行分类,尽可能定义它们。通常,使用 Rust 或 C 的人们非常关心性能。在我看来,轴的一端是性能,另一端是用户体验。按照这个方法来分:

1.汇编语言:本质上是底层的,缺乏有用的抽象,因此我认为人们应该只在少数特殊情况下直接处理它。

2.预汇编语言:试图让所有东西尽可能靠近硬件,同时让程序员尽可能多地控制性能。在这里,我们可能会在性能上稍微牺牲一点,换取可移植性和可维护性。但我们仍然应该在提供一些基本功能的地方编写代码。操作系统内核肯定属于这一类,也许一些基本的底层库设施也算,但大多数任务可能不需要在这个级别处理。实际上这个类别只有 C。Rust 可能有一天会达到这个水平,但我觉得它现在还远远不够。

3.系统语言:对我来说,这些语言专注于提供尽可能合理的性能,同时在大多数情况下仍提供安全性(允许对不安全机制进行受保护的访问)。通常,这些语言会竭尽全力避免传统的垃圾收集(或有选择地退出)。这意味着你可能需要在内存管理方面做一些手动工作。这已成为语言开发的“热门”类别,其中包括 C++、Rust、Zig、D 和 Nim 以及许多鲜为人知的语言。

4.编译语言:这些语言也会在意性能,但同样会重视整体开发体验。这里不需要担心内存管理,并且应该有一个相当丰富的生态系统,通常也非常重视类型安全,并有不错的类型系统。Go、Java 和 C# 是这一类别中的长期中坚力量,我还会把 Typescript 放在这里。

5.脚本语言:在这些语言中,性能在优先级列表中相当低。快速开发受到高度重视。通常人们不想等待结果慢慢编译出来,甚至可能希望在不重新运行程序的情况下就能看到变化。在这里,动态特性往往比类型安全更受重视,尽管这种情况已经开始发生变化。

C和汇编语言在内存安全性方面肯定处于绝对劣势,但正如我们一直在讨论的那样,安全性差距并不像人们想象的那么大,因为:

1.在桌面/服务器世界中,安全性更有可能是高优先级,通常有非常有效的缓解措施可以减轻风险。。

2.其他系统级语言仍会暴露很多不安全的特性,最后也会出问题。

3.大多数语言都有依赖项(通常包括编译器),这些依赖项会用内存不安全的语言编写。

4.此外,正如我们将在最后讨论的那样,上面这条至少是其他语言往往比 C 更不安全的一个非常重要的原因。

也就是说,我们很难想象在未来几年内,你选择的操作系统和浏览器会完全用 Rust 重写,同时完全解决安全隐患。但可以肯定的是,人们会付出大量努力慢慢转向这一方向。我们已经看到,不仅 Linux 接受了 Rust,微软也非常重视它。


语言的过早优化

我一直在说,C 在生态系统中的地位很有说服力,而且它不会很快消失,因为没有合适的东西可以取代它。我认为更令人担忧的是,许多程序员系统性地高估了性能的重要性。我的观察:

1.尽管 Python 在大部分运行时间里比 C 慢 50-80 倍(而系统和编译类别中的语言通常在 2 倍到 5 倍范围内,很少超过 10 倍),但它还是非常受欢迎。

2.大多数语言设计决策制定时都没有性能参考数据。

3.事实上,人们很少收集性能数据,尽管以性能为名做出的决策很常见。

4.经常会听到人们说“最好信任编译器”,但许多人前脚这么说,后脚就忘了这条原则。

我认为任何诚实的系统程序员都会说过早优化是一个巨大的陷阱,而且他们已经多次陷入其中。人们在估计性能(和风险)水平方面表现非常差,对于那些会高估性能需求的人来说,他们最终会选择系统语言(或 C,尤其在他们不怎么在乎风险的时候)。事实上,在很多情况下,即使是 Python 也很好用。例如,Dropbox 的大多数关键任务都用 Python,但表现也非常出色。我认为作为一个行业,我们应该更关心人们在性能(而非安全性)方面做出的错误选择,因为:

1.如果你能让人们不要高估对性能的需求,那么安全性自然会变得更好。

2.高估性能带来的间接经济损失对企业的影响可能远大于低估安全性的成本。例如,与编译语言甚至最高级别的语言相比,C 和 Rust 程序员都倾向于在内存管理上花费大量时间,并且发现自己在出现底层问题时花费了更多时间试图理解这些问题。你真的需要放弃垃圾收集器吗?因为仅凭这一点就会对开发时间产生重大不利影响,更不用说在标准库中提供更丰富的抽象可以降低成本的好处了。

这并不是说系统语言甚至预汇编语言不是正确的选择。我只是认为我们应该经常思考这个问题,并让人们客观地思考影响他们决策的方方面面:

1.真的可以证明我们很需要性能吗?如果是这样,请证明这一点。

2.否则,可能应该考虑迁移到高性能语言的成本是否值得。

3.如果没法证明,那么应该考虑其他哪些重要目标?构建速度?代码质量?


我们依旧需要学习 C 语言的人才

在人们选择系统语言时,大多数情况下我们应该推荐他们选择更高级的语言,而不是在没有数据的情况下过早为性能做优化。然而,整个行业确实需要继续以某种方式培养 C 程序员,因为:

1.信不信由你,尽管 COBOL 已经存在多年,但它比 C 语言的级别高得多,也更容易理解,所以要让用 C 写的关键系统在 50 年后还能继续维护会是更困难的事情。

2.毫无疑问,C 仍将得到广泛使用(很可能包括操作系统内核)。

3.它有助于推动系统级的创新。

我最后一点的意思是,50 年后,我们仍然需要能够成为底层架构专家的人才,帮助软件开发人员利用各种硬件改进。

在过去 30 多年里,大量新人涌入编程领域,其中一些人已经走上了这条道路。但是:

1.我们还为人们提供了比 30 年前更高级别的语言抽象。Javascript、Python、Go 和 Java 在抽象硬件细节方面做得非常出色,只有对学习系统内容非常感兴趣的人才能做到这一点。

2.如果人工智能辅助开发广泛流行,那么大多数开发人员会对系统编程非常陌生(这不全是坏事)。

3.如果我们以某种方式消灭大多数的 C 语言开发项目,并在没有任何合适替代品的情况下阻止大多数人学习它(我向你保证 Rust 不是合适的替代品),我们将大大加深系统语言和实际硬件之间的鸿沟。

C 语言虽然很可怕,但相对其他语言而言,它绝对是从编程角度理解底层架构的更好的基石。如果没有这个垫脚石,那么愿意一路向下推进来促进软件利用硬件改进的专家会快速减少,因为学习基础知识和完成简单任务所需的努力程度,最终会高到让更多感兴趣的人们要么认为他们没有能力,要么不想经历那些痛苦,然后放弃。人类是以目标为导向的生物,为了我们自己的心理健康,我们倾向于不追求那些自己认为太难实现的目标。

如果有一种预汇编语言可以纠正 C 语言的一些最严重的错误(在我看来最大的错误是 C 语言对数组的处理),并且在开发过程中始终执行尽可能多的分析(不仅是通过 Clang 项目提供的清理程序,还包括 Valgrind 等运行时安全工具),我会感到更安心。

Rust 目前是最接近这种替代品的语言,但在我看来,过分强调函数式范式会影响它更好地阐明底层的冯·诺依曼架构。


人们不选择 Rust 的理由

总体而言,人们谈论的理由有:

1.“我们的应用程序不需要用系统级语言编写;我们对垃圾收集很满意。”

2.“我们对自己熟悉的语言和生态系统很满意,不想花太多时间学习新的生态系统。”

3.“我们觉得 Rust 的学习曲线很陡,很难编写。”

4.“我们没有足够的人才熟练掌握它(大家一起学习它似乎是在浪费资源)。”

5.“其他人用 Rust 编写的代码往往很难理解。”

6.“构建时间往往非常长,Rust 经常有太多外部依赖项。”

我意识到 Rust 在很短的时间内就变得非常受极客欢迎,这是有充分理由的。我非常欣赏 Rust 的一些成就,尽管如此,我已经亲身感受了上面列出的一些因素。三年前,我读了一篇写得很差的学术论文,想看看是否有人已经实现了它。结果我发现了同一篇论文的两种不同实现,但只有两种,而且它们恰好都是用 Rust 编写的。这两种实现都非常简洁,很难理解。如果我不知道它们实现了相同的算法,我永远也不会猜到它们做的是一样的事情,因为它们使用了截然不同的习语,而且看起来一点也不像。

我已经编写了足够多的 Rust 代码,并与许多 Rust 开发人员交流过,我可以自信地说,相当多的人会发现它很难采用,而且需要很长时间才能感觉到它与他们目前选择的语言相比一样高效。

谷歌的一篇博客文章试图反驳 Rust“难以学习”的论点。我读过这篇文章,但除了他们没有分享任何真实数据之外,这篇文章还存在一些问题:

1.那些发现自己“在 Rust 中同样高效”的人们的情绪被谷歌员工严重扭曲了,这些谷歌人习惯构建内部了 C++ 工具栈,这些工具栈有足够的复杂性和多年来积累的控制力,只要摆脱这些限制,他们就会感到高效。

2.调查用户情绪(而不是围绕代码本身的指标客观分析)会带来难以纠正的隐性偏见。谷歌员工不想在调查他们对 Rust 看法的人们面前显得软弱。

3.即使数据有点用处,也无法与其他语言直接对比。那篇文章声称他们的结果与其他语言相同,但他们明确使用了“轶事”一词。

4.谷歌的员工也往往是最熟练的。说“我们谷歌人觉得很容易”没什么代表性。

从所看到的情况来看,Rust 主要只在极客圈子流行。我觉得 Rust 明确针对极客,也就是那些对数学函数和递归有直观理解的人们打造,这一事实让我无法接受它。

我希望编程更加平等。我认为世界上有很多聪明、能干的人不在我们的专业圈子里,如果他们能更轻松地将想法转化为计算机代码,就能为世界做出惊人的贡献。我非常希望降低编程的入门门槛(在我看来,Python 在这方面为世界做出了最大的贡献)。从根本上讲,Rust 是一种很棒的语言,熟悉它的人应该在有意义的地方使用它。然而,我认为人们应该尽量更诚实地对待经济学:

1.我们应该更全面地思考各种选项的经济要素,尽可能地使用编译语言而非系统语言,因为前者的总体经济效益在大多数情况下可能更好。

2.有预汇编需求时,我们不应该逼人们放弃使用 C/C++。对这种选择的质疑是可以的,但世界的那么多需求永远不会只用一种编程语言就能满足,即使在系统领域也是如此。

3.我们应该促进 Zig 等系统语言的发展,甚至推动有潜力真正取代 C 的语言崛起,这样有一天我们也许能够用一种在 COBOL 时代做不到的方式给 C 的棺材钉上钉子。

目前,Zig 及其生态系统在满足许多系统编程需求方面远远落后 Rust,但前者对问题采取了更加平等的态度。相比 Rust,Zig 将是一种更容易被大多数使用编译甚至脚本语言编写程序的人们接受的语言。

部分原因是 Rust 的根基牢牢扎根于函数式编程世界,其基础原则是围绕数学的纯函数构建的。有些人可以凭直觉理解这些东西,但他们往往是有着深厚的数学背景。

相比之下,Zig 依旧是一种常规的命令式语言。它的基础原则本质上是“给某人详细的指示”,小孩都能理解。事实上,我见过的每一个成功的预编程项目(例如 Scratch)都是命令式的。

函数式编程被普遍认为是晦涩难懂的,并且在过去 65 年中它都没能流行起来的事实表明,每类编程语言都应该有一种强大的过程语言。

但是,我对函数式范式和函数式语言大多持积极态度,因为它们确实有自己的巨大优势,尤其是它们可以更好地鼓励程序员编写更可靠、更易分析的代码。函数式范式的价值非常大,我相信在每个抽象级别(直到预汇编语言)上都应该有一个好的、流行的函数式语言。

另一方面,我认为面向对象编程范式的实用性要小得多,它最好完全消失,或者最多成为一个不那么突出的特性。


Rust(目前)可能存在比 C 更大的安全风险

当知名的安全思想领袖发表“用非内存安全语言构建关键应用程序是不负责任的”这样的言论时,我感到很失望。这不仅仅是因为他们忽略了经济复杂性,将复杂的决策简单化,还因为即使我们只考虑安全性也能发现,C 中的内存安全问题虽然很严重,但风险并不一定比其他语言更严重。

具体来说,C 程序一般只有少量外部依赖项,而这些依赖项往往是最常用的软件(例如 C 标准库)。其他多数语言中,程序员更容易利用其他人的工作成果。从商业角度来看这是一件好事。但从安全角度来看,更多的依赖项不仅会增加我们的攻击面,还会让我们更容易受到供应链攻击。xz 事件是此类供应链攻击的最新知名案例之一,但它绝非孤例。

Rust 很容易引入外部依赖,就像在 JavaScript 生态系统中一样,它似乎鼓励人们做出来大量很小的依赖项。这使问题监控和管理起来更困难了。Rust 的情况还比大多数语言更糟糕,因为核心 Rust 库(Rust 项目正式维护的主要库)大量使用第三方依赖。这个项目需要承担起该负的责任,监督他们使用的库。对我来说,这一直是软件中最大的风险之一。我可以编写颇具防御性的 C 代码,但我很难信任自己使用的任何依赖项,更不用说大规模使用了。

正确保护依赖项供应链比编写安全的 C 代码要困难得多。在这方面,C 比 Rust 好很多,但并不是特别好。部分原因是 C 标准库用得没那么多。编写大量 C 代码的程序员总会自己构建一些代码,维护上几十年。

我个人一直更关心尽量减少依赖项的主题,而非缓冲区溢出。有一些简单的方法可以尽可能减少内存安全问题,而且在大多数应用程序中它们并不难使用。但深入研究每一个依赖项?即使是供应链安全领域的从业者迄今为止做出的最大努力,也无法很好地帮助我们应对最近的 xz 事件等攻击。一旦开发人员建立了对某些下游依赖项的信任,那么以一种可能被视为意外错误的方式引入后门并不难。比如 xz 事件中,后门并没有直接进入源代码树,因此它更像是一个后门。但我们早就知道,隐秘的后门与错误是无法区分的。尽管我们比以前更加注重同行评审文化,但很多“经过评审”的代码并没有得到那些严谨的人们的充分审查。

此外,代码审查比编写要难得多,这也是我不指望在不久的将来使用基于 LLM 的代码生成工具的原因之一——它将程序员变成了编写需求的“产品经理”,以及代码审查员。目前,我觉得“只”做一名工程师更容易。

无论如何,依赖关系越多,隐含信任圈就越大,攻击面就越大,你承担的供应链风险就越大。这使得 Rust 在供应链安全方面的风险特别大,而 C 在这方面得分相当高。考虑到所有经济因素,选择 Rust 可能还是更明智的,但我认为安全性的理由还不够有说服力。我认为 Rust(以及几乎所有编程语言)如果能有自己的标准库,那就再好不过了。它们应该引入所有依赖项,并愿意承担责任。

此外,我一般会主张语言将更多功能纳入其标准库,尽管最近的趋势恰恰相反。是的,从安全角度来看,这在技术上增加了你的攻击面。但事实并非如此:

1.如果人们觉得他们可能需要从外部引入某些通用功能,那么无论标准库是否导出它,他们都很可能会这样做。

2.删除语言标准库中实际上没有用到的部分并不难(特别是有了链接时优化),这会将攻击面降低到大致相同的程度(尽管许多非系统语言并不担心链接时优化)。

3.通过承担责任,语言维护者不仅可以更专注于正确审查人们可能使用的解决方案中的安全风险,他们也更可能找到有助于减少攻击面的架构设计。

Rust 这样的语言应该对人们可能需要的功能负责,尤其是当安全性被视为人们使用它的主要动机之一时。Go 和 Python 这样的语言有丰富的标准库,由语言维护者负责,这实际上是最好的情况。

的确,Python 已经变得如此流行,以至于很多人都使用外部依赖项,而且有几种流行的包管理器。但从供应链的角度来看,它在这方面还是比 JavaScript 强。


建议

虽然我认为如今供应链问题可能让 C 比 Rust 更胜一筹,但 Rust 很容易让这种优势消失。当这种情况发生时,C 将面临内存管理方面的担忧。我的目的不是要说 C 比 Rust 更好,而是要表明围绕语言选择的决策远比人们情绪化的结论要复杂许多。

对于团队

1.探索各种选项的整体经济效益。在选择技术时,思考它们各自更广泛的经济影响,并尝试获得支持你假设的数字证据。人们往往会凭直觉行事,经常高估性能的重要性。

2.如果你不想预先分析,请不要先考虑系统语言。相反,请先看看 Go、Swift、Java、C# 或其他优秀的编译语言。如果你感觉应该提高性能,那么 Python 对于各种用例都有可接受的性能,并且所有编译语言的效率都会远高于 Python。

3.一定要考虑安全性。你选择了 Rust 并不意味着你没有代价。坏事仍可能发生在你身上。

4.避免不必要的依赖。你需要思考并判断所有经济因素。但请注意,更少的依赖关系通常还有其他好处,包括更短的构建时间、更少的测试面、更少的 API 更改风险或下游依赖项的错误。

5.注意你已有的依赖项。除了尝试掌握依赖项扫描工具提供的传递图之外,还应该深入研究工具没发现的东西。你的运行时中可能链接了一些 C 库,当下一次 xz 事件出现时,这有助于你更轻松、更诚实地评估风险。

6.尝试确保外部安全审查。对于企业软件来说,这几乎是理所当然的,因为大买家会要求提供证据。但大家都应该考虑如何定期进行此类审查,并将其视为一个机会,而不仅仅是一个需要勾选的框。

7.如果你选择 C,请提供文档支持你的决策。我这样说是因为安全问题绝对是真正要考虑的问题,人们应该知道你在这方面做得很好。

8.如果你选择 C,请主动有效地解决内存安全问题。

请注意,许多 C 开发人员选择使用不安全原语的主要原因不是他们缺乏安全风险方面的教育,而是他们通常有一些大型依赖项(例如 OpenSSL 或其他加密库),并且不清楚如何以简单、可移植的方式将 Boehm 垃圾收集器应用于这些第三方库。

对于安全行业

1.最重要的是要记住,仅仅因为你个人认为这是一个糟糕的安全决策,并不意味着它总体上真的是一个糟糕的决策。试着倾听非安全人士的意见,了解他们的优先事项。你可能反对“FUD”(恐惧、不确定性和怀疑),但过度简化非平凡问题,不惜一切代价推动安全增强措施本质上就是在传播 FUD。请记住,即使是安全影响也比我们现在可能想到的要复杂和微妙得多。

2.确保行业考虑更广泛的经济因素。当然,在行业之外,安全性远不如在行业内部重要。但我认为问题要深远得多——如果安全行业试图将过多的工作推给行业的其他部分,我们至少会破坏可信度,还可能会造成很大的经济损失。比如说,如果小型企业和个人的成本和责任风险太高,他们就会完全忽略它了。

3.为解决我们遗留的软件问题做出贡献。“用安全的语言重写所有内容,并尽快迁移所有内容”这样的口号并不是真正的解决方案。它充其量只是一种理想,从风险管理的角度来看完全不切实际。我们可以取得进展,但需要循序渐进,我们必须更加务实。

对于其他行业

一般来说,软件行业的其他行业应该与安全行业合作应对风险。具体来说:

1.帮助我们弄清楚如何才能留住一批能够理解软件和硬件界限的新鲜人才。这意味着,我们最终需要让熟练的人员有效地完成预组装任务,而这种方式的风险要比现在用 C 语言完成任务低得多。如果答案不是“另一种编程语言”,那肯定很好,但如果我们需要另一种语言,双方都需要认真对待这个决策。

2.请推动其他人在更多涉及经济因素的决策中展示他们的工作,不要以为什么事只靠“老人言”就能搞定。这是一个快速变化的世界。

3.语言设计者,除了你已经为安全性做的工作外,还请更多地考虑和关注第三方依赖项的影响,意识到平衡贡献与安全性是很难的。我们需要更务实的解决方案。

4.在所有情况下,试着假设你所做的各种估计(比如性能和安全性)都是错的。假设你在任何一个方向上都可能是错的!现在,试着收集一些硬数据,让你更好地了解现实到底是什么情况。

苹果称Swift是C++的最佳继任者

Swift 是苹果公司在 2014 年推出的,一款旨在替代 Objective-C 的编程语言。但苹果语言和运行时总监 Ted Kremenek 在 WWDC24 的主题演讲中表示苹果不会放弃吃带血馒头的时机:Swift 也将取代 C++。

“Swift 的安全性、速度和易用性,加上内置的 C 和 C++ 互操作性,意味着 Swift 是继任 C++ 的最佳选择。”

C++ 和 C 在前段时间曾受到了美国各大政府机构的抨击,白宫国家网络主任办公室 (ONCD) 还在2024年2月份发布了一份报告,敦促开发人员转向内存安全的编程语言(上文有述)。一直以来,苹果设备上的软件都是用采用 C、C++ 和 Objective-C 编写的。但现在,该公司已经在致力于用 Swift 重写其 C++ 代码。

在此次大会上,苹果公司还特别介绍了将于今年发布的全新版本 Swift 6。新的编码时数据安全保障功能,可以在编码时诊断开发者项目中并发访问内存的情况。这一优化可以在对整个代码库进行最小改动的情况下发现和修复错误,增强了未来代码的安全性和可维护性。


Swift 6 还计划扩大对 Linux 的支持,包括 Debian 和 Fedora Linux 发行版,并改进对 Windows 的支持。此外,Swift 6 还将改进泛型,计划为 OS 内核和微控制器等受限环境提供一个新的子集。苹果还在投资为微软的 Visual Studio 代码编辑器和其他利用语言服务器协议的编辑器提供 Swift 支持。

WWDC24 也标志着 Swift 问世 10 周年。在接下来的发展中,苹果公司宣布计划将 Swift 迁移到一个专门的 全新 GitHub 组织;能托管一系列对 Swift 生态系统至关重要的项目,包括 Swift 编译器、Foundation 以及其他关键的资料库。此外还有新的 Swift 测试框架为开发者提供了简洁明了的 API,让编写测试更加简单。


剑指C/C++,美国CISA等机构再发警告:Linux中95%没用内存安全代码

本节转自:CSDN,发表于2024年7月上旬。

如同微软想尽办法让消费者尽可能地升级到最新的 Windows 11 系统一样,美国安全机构无时无刻也不在发力,希望广大程序员可以使用 Rust 等更安全的语言替代掉无法自动防止内存错误的语言如 C、C++ 等。

美国安全机构再发 22 页报告,“剑指”开源软件

近日,美国网络安全部门(CISA)联合美国联邦调查局(FBI)、澳大利亚信号局(ASD)、澳大利亚网络安全中心(ACSC)和加拿大网络安全中心(CCCS)共五大机构又发布了一份 22 页的调查报告——《探索关键开源项目中的内存安全》,总结了他们对 OSS 中使用内存不安全代码的调查结果。



以 GitHub 代码托管平台和 OpenSSF(开源安全基金会)为调研平台,CISA 这些机构此番分析了全球 172 个关键开源项目,包含了 Chromium、Linux、MySQL Server、TensorFlow、JDK、Node、KVM、GCC 等主流的浏览器、操作系统、数据库、框架项目。

同时,CISA 等机构使用的内存不安全语言定义如下,覆盖 Assembly、C、C++、Cython 等内存不安全编程语言。



根据调查,其得出以下一些结论:

172 个项目中有 52% 是使用 C、C++ 和其他所谓“内存不安全”的语言编写的。

所有项目的总代码行(LoC)中有 55% 是用内存不安全的语言编写的。

最主流的一些项目在很大程度上是用内存不安全的编程语言编写的。在按 LoC 总数计算的十大项目中,每个项目使用内存不安全 LoC 的比例都超过了 26%。在这十个项目中,使用内存不安全语言的比例中位数为 62.5%,十个项目中有四个项目的比例超过 94%。

对用内存安全语言编写的三个项目进行的依赖性分析表明,每个项目都依赖于用内存不安全语言编写的其他组件。



具体来看,根据调研数据显示,总共有 26023 KLoC(千行代码)的 Linux 项目中,约 95% 的代码行都是内存不安全的;对于 MySQL Server,这一比例为 84%;对于 TensorFlow,这个数字是 64%;对于 Zephyr,这个数字是 84%;对于 Chromium,这个数字是 51%。

平均而言,10 个最大的开源项目中,总代码行中有 26% 是内存不安全的代码。即使是用内存安全语言编写的项目也因依赖不安全的组件而面临风险。



与此同时,报告称,「我们确定所分析的大多数关键开源项目,甚至那些用内存安全语言编写的项目,都可能包含内存安全漏洞。这可能是由于直接使用内存不安全的语言或外部依赖使用内存不安全性的语言的项目造成的。此外,禁用内存安全的低级功能需求可能会在以其他内存安全语言编写的代码中造成内存安全漏洞。这些限制凸显了继续认真使用内存安全编程语言、安全编码实践和安全测试的必要性。」

多份报告相继发布:呼吁弃用 C/C++ 等内存不安全的编程语言

事实上,近几年来,业界愈发关注内存安全问题。据安全组织 Security Planner 于 2022 年 10 月发布的一份《消费者报告》指出,“大约 60%-70% 的浏览器和内核漏洞——以及在 C/C++ 代码库中发现的安全漏洞——是由于内存不安全导致的。”该报告还引用了一段关于使用内存安全语言代替内存不安全语言的安全成本和收益的有趣讨论。



自此,使用更加安全的内存语言便成为美国多个政府机构引导的主要方向,这也是其连发多个文件、呼吁项目迁移到更安全编程语言的原因:

1、2022 年 11 月,美国国家安全局(NSA)发布了关于保护软件开发者和运营商免受内存安全问题影响的指南,鼓励多个组织将编程语言从 C/C++ 转为使用内存安全的语言,如 C#、Rust、Go、Java、Ruby 和 Swift,主要原因是这样可以帮助软件开发者和使用者预防并缓解软件内存安全问题,这些问题占可利用漏洞的很大一部分;

2、2023 年 3 月发布的《2023年国家网络安全战略》和2023年7月发布的相应实施计划均讨论了投资于内存安全并与开源社区合作的内容。《国家网络安全战略实施计划》的第4.1.2项倡议“促进开源软件安全和内存安全编程语言的采用”指示建立“开源软件安全倡议(OS3I)以推动内存安全编程语言和开源软件安全的采用。” 同一文件中的第4.2.1项倡议“加速内存安全编程语言的成熟、采用和安全性”也呼吁投资于内存安全编程语言,并寻求新成立的OS3I的参与。

3、2023 年 12 月,美国网络安全和基础设施局 (CISA)也开始联合 NSA、美国联邦调查局 (FBI) 以及澳大利亚、加拿大、英国和新西兰的网络安全机构发布了一份 23 页的《内存安全路线图指南》,这份文件指出,内存安全漏洞是最常见的软件漏洞类型之一,并给软件制造商和消费者带来修复、及时响应和其他努力相关的巨大成本。建议软件制造商创建内存安全路线图,包括解决外部依赖(通常包括开放源代码软件)中的内存安全的计划。

4、2024 年 2 月,美国白宫国家网络主任办公室 (ONCD)在一份主题为《回到基础构件:通往安全软件之路》的 19 页 PDF 报告中强烈呼吁道,“C、C++ 不安全,新应用开发时就别用了,旧应用应该采取迁移行动”。

时下,CISA 联合其他国际安全组织发布的报告直接揭晓了一些主流开源项目中使用的不安全内存语言编写的代码行数,其也遵循了以上战略路线,希望引发开发者、企业的注意,有效避免内存安全错误。

各大科技公司已经开始“重写”、“迁移”等行动

这份报告也再次强调,像 C 和 C++ 这样的内存不安全编程语言允许程序员对代码中的内存相关功能进行更直接的控制,这通常会导致非常常见的应用程序安全问题,例如缓冲区溢出、使用未初始化内存、类型混淆和使用后释放漏洞。这类缺陷在现代应用软件的所有漏洞中占了很大比例。

相比之下,内存安全语言(最常见包括 Rust、Python、Java 和 Go)提供了如内置运行时和编译时检查等保护措施,以减轻常见的内存相关错误。最近几年,在政策引导下,不少大厂、项目确实开始发力使用内存安全的编程语言:

2021 年,由 AWS、华为、谷歌、微软和 Mozilla 等创始成员支持,Rust 基金会成立。

Linux 是从 2022 年 12 月开始合并 Rust 代码。Linux 之父 Linus Torvalds 在去年 12 月出席 Open Source Summit Japan 2023 上透露:“Rust 一直在成长,但我们还没有内核的任何部分真正依赖 Rust。对我来说,Rust 是技术上有意义的事情之一,但对我个人来说,更重要的是,作为内核和开发人员,我们不能停滞不前。”

尽管如此,Torvalds 继续说道:“Rust 还没有真正成为下一个伟大的事物。但我认为,在明年,我们将开始集成驱动程序,甚至一些主要的子系统也将开始积极使用 Rust。因此,要让它成为内核的重要组成部分,还需要数年时间。但它肯定会成为内核的一部分。”

互联网安全研究小组(ISRG)发起的 Prossimo 项目,它的目标是通过使用具有内存安全属性的语言来解决 C 和 C++ 代码中的内存安全问题,从而改善互联网敏感的软件基础设施,而这种基础设施的代表就是 Linux 内核。参与 Prossimo 项目的开发者们一直在用 Rust 重写关键的开源库,以减少遗留代码中内存缺陷的风险。就在本周,Let’s Encrypt 表示它已经部署了 ntpd-rs,这是 Prossimo 用 Rust 重写的网络时间协议(NTP)守护进程。

同时,在这一趋势下,Google 和微软都开始向内存安全语言迈进,最初是用于新项目,最近则用于应用程序重写。不久前,Google 的工程总监 Lars Bergstrom 在伦敦的 Rust Nation UK 大会上分享了 Google 将 Go 或 C++ 编写的项目迁移到 Rust 语言的经验。他表示,「使用 Rust 的开发团队相比于使用 C++ 的团队,在工作效率上大约高出两倍。」

对于另一大科技公司微软,此前其 Windows 操作系统安全总监 David “dwizzle” Weston 透露,微软正在用 Rust 编程语言重写核心 Windows 库。

迁移之路,任重而道远

然而,鉴于将代码库完全用内存安全语言进行重写这一任务过于复杂、成本高昂,因此仅想要通过政策的引导就能实现业界做出大规模的改变,显然不太现实。

更何况为了应对内存安全编程语言的挑战,争议之中的编程语言如 C++ 社区也正在尝试通过 C++ 配置文件使开发者能够编写更安全的代码,这些配置文件在编译时提供安全保证。

针对这一点,CISA 在报告中也指出,还有很多工作需要完成。

“在性能和资源约束的地方,我们预计内存不安全的语言会继续使用,包括操作系统内核和驱动程序、密码学和网络,尤其是嵌入式应用程序”,CISA 报告指出,“同时,即使在使用内存安全语言时,开发人员也可能禁用内存安全特性。AWS 在对‘开放源码软件安全信息请求’的回应中,建议‘使用像 Rust 这样的内存安全语言’编写新代码,但要注意并非所有名义上使用内存安全语言写成的代码实际上都是内存安全的...开发人员禁用使 Rust 内存安全的编译器特性是容易的,而且相当普遍。”

因此,CISA 等组织也没有特别好的办法,只是表示,“我们鼓励其他人在此分析的基础上进一步扩大我们对开源软件内存不安全风险的集体理解,评估降低这种风险的方法(例如用内存安全语言有针对性地重写关键组件),并继续努力推动软件制造商采取降低风险的行动。”

业界看法

对此,来自 Synopsys 软件完整性小组技术经理 Gunnar Braun 表示,“该报告及其之前的‘内存安全路线图指南’将这个问题提交给了公司 C 级高管——这是理所应当的。软件安全与保障不再是纯粹的技术问题。”

Braun 表示,许多开源程序运行在资源受限的嵌入式系统中。如果无法改变编程语言,那么使用静态代码分析和模糊测试工具来降低内存安全风险就显得尤为重要。

“一些内存安全语言,例如 Rust 或 Go,已经进入嵌入式系统,因此我乐观地认为 C/C++ 终有一天会被大量取代——但不是今天,也不是明天,”他说道。

OpenSSF 总经理 Omkhar Arasaratnam 认为,「内存安全问题并不是开源或闭源软件特有的问题。它是所有现代软件的普遍问题。如今有许多内存安全的语言,比如 JavaScript、Python 和 Java,但软件工程师经常使用内存不安全的旧语言,比如 C/C++,以实现性能或低级硬件访问。此外,尽管 Rust 近年来已成为低级系统编程中 C/C++ 的可行替代品,但有许多嵌入式系统和安全关键型应用程序并不适合 Rust。」

他补充道,“虽然用内存不安全的语言编写内存安全的代码是完全有可能的,但 25 年的 CVE 告诉我们,这种情况极不可能发生,这并不是说人们是糟糕的程序员,而是用内存不安全的语言编写内存安全的代码非常困难。”

在部分开发者看来,“这的确很好地提醒了我们,非内存安全的遗留语言不会消失。确实如此!虽然最好不要用低级语言编写某些项目,但它们已经存在,重写是一个巨大的工程,可能会引入其他逻辑错误。更好的开发方式是使用工具,并实际使用它们来修复一些关键的现有代码。”

也有人认为:“内存安全语言虽然被设计来减少内存泄漏的风险,但实际上仍然可能发生内存泄漏。有时候,像 C 语言(或 C++)往往会出现一些简单的问题,但这些问题相对容易解决。当然仍需要一个良好的测试套件来确保代码质量,但一旦通过了类似 valgrind 的工具进行了内存清理,保持代码在可控的范围内并处理新的 Bug 相对较容易。尽管不太熟悉 Rust,但 Java 程序员曾经在使用他们能找到的库时遇到过问题。他们可能没有充分了解自己使用的具体库,因此可能会出现严重的内存泄漏问题。虽然他们的测试用例可能在他们自己的机器上运行正常,但对于企业级软件来说,这些问题可能会倍增,过多的内存使用仍然可能导致系统崩溃。”

在 AI 时代下,甚至有人建议,“与其用内存安全的编程语言重写程序,倒不如可以让 AI 帮忙捕捉一些安全漏洞。”

对此,该如何看待美国 CISA 等机构发布的报告?编程语言安全性是否可以直接提升程序安全性?

参考:
cisa_open_source

cisa-memory-unsafe-code-open-source-projects


joint-guidance-exploring-memory-safety-in-critical-open-source-projects-508c


美政府机构再想新招:开发AI工具将旧的C代码转为Rust

当呼吁起不到实际的效果时,美国政府机构又想出一个提高内存安全编程语言使用率的新方法。2024年8月上旬有不少人发现,美国国防高级研究计划局(DARPA)正在启动一项资助计划,即推动一款程序代码转换工具 TRACTOR(全称为 Translating All C to Rust)的开发,旨在借助 AI 大模型技术独立地将传统的 C 和 C++ 代码直接转换为可用的 Rust 代码。同时,DARPA 最终希望这款 AI 工具达到的水平能够与经验丰富的 Rust 程序员带来的结果相当,实现“一劳永逸地消除内存安全漏洞”。

1、呼吁开发者放弃 C、C++ 的机构们

毋庸置疑,将 C、C++ 和 Rust 这几种语言摆在一起,DARPA 必然要提到内存安全问题。在公告中,DARPA 指出,内存安全漏洞是已披露的软件漏洞中最常见的类型,主要通过两种方式影响计算机内存:
1.首先,C 语言等编程语言允许程序员直接操作内存,因此很容易在程序中意外引入错误,使看似常规的操作破坏内存状态。

2.其次,当我们在编写代码时,有时候会遇到一种叫做“未定义行为”的情况。就是说,编程语言的规则(或者标准)没有明确说明在某些特定情况下程序该怎么运行。所以,如果我们写的代码触发了这些不明确的情况,程序可能会以一种我们意想不到的方式运行,甚至可能导致内存安全问题(比如程序崩溃或产生错误结果)。

正因此,过去几年间,美国各大组织的呼吁动作不断,如此前报道的:
1.2022 年 11 月,美国国家安全局(NSA)发布了关于保护软件开发者和运营商免受内存安全问题影响的指南,列举了微软在 2006 年到 2018 年期间,发现的 70% 的漏洞都是因内存安全问题造成的;Google Chrome 中存在了类似比例的内存安全漏洞...鼓励多个组织将编程语言从 C/C++ 转为使用内存安全的语言,如 C#、Rust、Go、Java、Ruby 和 Swift,称这样可以帮助软件开发者和使用者预防并缓解软件内存安全问题。

2.彼时,微软 Azure CTO Mark Russinovich 也呼吁,「是时候停止使用  C/C++ 启动任何新项目」。

3.2023 年 12 月,美国网络安全和基础设施局 (CISA)也开始联合 NSA、美国联邦调查局 (FBI) 以及澳大利亚、加拿大、英国和新西兰的网络安全机构发布了一份 23 页的《内存安全路线图指南》,直接点名 C 和 C++ 是内存不安全的语言代表,软件开发商应该放弃使用,从而迅速采用 Rust、C#、Go、Java、Python 和 Swift 等其他内存安全编程语言 (MSL)。

4.2024 年 2 月,美国白宫国家网络主任办公室 (ONCD)在一份主题为《回到基础构件:通往安全软件之路》的 19 页 PDF 报告中再次强烈呼吁道,“C、C++ 不安全,新应用开发时就别用了,旧应用应该采取迁移行动”。

5.2024 年 7 月,美国网络安全部门(CISA)联合美国联邦调查局(FBI)、澳大利亚信号局(ASD)、澳大利亚网络安全中心(ACSC)和加拿大网络安全中心(CCCS)共五大机构发布了一份 22 页的调查报告——《探索关键开源项目中的内存安全》,发现 172 个项目中有 52% 是使用 C、C++ 和其他所谓“内存不安全”的语言编写的,希望引发众人的重视。

在连番呼吁之下,内存安全问题确实引起了一定的重视。为此,一些组织也开始了行动,如:
1.互联网安全研究小组的 Prossimo 项目与 weedegolf 合作,用 Rust 重写 Network Time Protocol (NTP) ,以提供内存安全的 NTP。

2.早些时候,苹果公司修改了用于构建 iBoot 引导载入程序的 C 编译器工具链,以减少内存和类型安全问题。

3.有消息称,微软已经用 3.6 万行 Rust 代码改写了 Windows 内核;

4.2022 年 12 月,Linux 内核 6.1 发布,包含了初始 Rust 支持...

然而,摆在现实中的问题是,即使当代开发者深谙要在编写程序的时候尽可能地使用内存安全的语言,也做了如上所述的一些尝试,但要知道 C 语言诞生于 20 世纪 70 年代,发展至今已经五十多年的历史,早已变得无处不在,无论是现代智能手机,还是太空飞行器等各种应用程序,都有它的身影。就连 DARPA 也坦言,美国国防部的长寿命系统对 C 语言等编程语言的依赖程度更高。手动地一行一行改写代码,亦或者正确遵守 ISO 标准并认真应用测试工具就可以避免安全漏洞,显然有些不太现实。DARPA 也得出了一个结论:经过二十多年对 C 和 C++ 内存安全问题的努力,软件工程社区已达成共识,仅仅依靠错误查找工具是不够的。

2、迁移是巨大的问题

然而此前,美国政府机构在发出呼吁的同时也指出,向 MSL(内存安全语言)过渡将涉及重大投资和高管关注,此外任何此类过渡都需要多年的精心规划。

因此,虽然内存安全编程语言可以消除内存安全漏洞已经不是什么秘密,但面临的挑战一直是如何大规模重写传统代码,以应对庞大的问题。要想弃用,不谈迁移代码带来的技术难度问题,仅是外在的人力、财力、精力成本都大到难以想象。

时下,始于大型语言模型 (LLM) 等机器学习技术的最新突破,DARPA 表示这创造了一种新解决方案的环境,即“通过大规模自动化,将世界上高度脆弱的遗留 C 代码自动转换成本质上更安全的 Rust 编程语言。”

“你可以访问任何 LLM 网站,开始与其中一个 AI 聊天机器人聊天,你只需要说‘这是一些 C 代码,请将其转换成安全的 Rust 代码’,然后剪切、粘贴,就会出现一些结果,而且通常效果很好。”DARPA TRACTOR 项目经理 Dan Wallach 博士说。不过这是理想状态下的情况,Dan Wallach 表示,“但现实并非总是如此,研究挑战在于大幅改进从 C 到 Rust 的自动转换,特别是对于最相关的程序结构。”

TRACTOR 的目标不仅是实现代码转换的自动化,而且要实现熟练开发人员手动编写 Rust 代码的高质量和风格。通过这样做,该计划旨在消除 C 程序中固有的内存安全漏洞。除了利用软件分析方法(包括静态和动态分析)外,TRACTOR 还将采用 LLM 支持的解决方案并举办公开竞赛来展示和测试这些创新。

Wallach 表示,“Rust 迫使程序员把事情做好,处理它强制执行的所有规则可能会让人感到束缚,但当你适应它们时,这些规则会给你自由。它们就像护栏;一旦你意识到它们是用来保护你的,你就会自由地专注于更重要的事情。”

当前,DARPA 已经公开发布了这一计划,也希望有更多的参赛者参与进来提交关于 LLM 支持的解决方案。DARPA 将于 2024 年 8 月 26 日举办一场活动,针对计划为 TRACTOR 项目提交提案的人。从内存安全出发,借助 AI 大模型的潜力,实现不同的语言代码一键转换,确实是一个不错的想法。

据外媒 The Register 报道,Prossimo 项目执行董事 Josh Aas 对此认为,“当今互联网基础设施中运行的大量 C 代码使得使用翻译工具变得很有吸引力。我们已经对此进行了尝试,例如我们最近将基于 C 语言的 AV1 实现转换为 Rust。虽然目前的工具还需要不少手动调整才能让结果正确并符合惯用法,但我们相信,通过进一步投资,这些工具将会变得更加高效。”

理想情况下,只要用自然语言告诉 AI 将 C、C++ 遗留代码转为 Rust 代码,它确实会帮助转换,但是这样生成的结果是否可以直接使用?我们距离 TRACTOR 项目成为现实又还有多远?

正在负责 TRACTOR 项目的工作 Wallach 坦言,该项目的目标是实现高度自动化,但这需要克服一些棘手的技术挑战。“其中一个挑战是,当你让大型语言模型(LLMs)转换代码时,它们有时能给出出乎意料的好答案,但也可能会产生错误的答案。另一个挑战是 C 语言允许代码对指针进行操作,包括算术运算,而 Rust 禁止这些操作。要弥合这一差距,不仅仅是把 C 语言直接转换成 Rust 这么简单。”对于这一计划,开发者的看法不一。

美政府给出最强硬要求:2026年前关键软件必须开始全面去C系编程语言

2024年10月消息,此次意见可谓美国政府在软件安全方面表达的最强硬立场,消息一出很快引起软件开发商关注:若不对编码实践做出修复,则意味着需要承担过失风险。这是美国政府迄今在软件安全问题上最强硬的立场,提醒软件开发商:修正危险的编程做法,否则有可能被扣上玩忽职守的帽子。

美联邦政府正在加强关于危险软件开发实践的警告,2024年10月下旬美国网络安全与基础设施安全局(CISA)和联邦调查局(FBI)陆续发布关于困扰关键基础设施的基本安全问题的明确处置信号。CISA 和 FBI 在关于产品安全性不良实践的最新报告中,提醒软件开发商应高度关注使用非内存安全编程语言等不良行为,而 C 和 C++ 更是在其中被列为反面典型。报告指出,“在支持关键基础设施或国家关键职能(NCF)的新产品线的开发过程中,使用非内存安全语言(例如 C 或 C++)可能引发风险,即显著提高了国家安全、国家经济安全以及国家公共卫生及安全所面临的风险。”其联合发布了一份《Product Security Bad Practices》指南,旨在警示软件开发商在整个产品开发过程中优先考虑安全性、规避不良实践;其中还再次提及了使用 C/C++ 等 “内存不安全” 的编程语言风险性。该文件主要面向开发用于支持关键基础设施或 NCF 的软件产品和服务(包括内部部署软件、云服务和软件即服务 (SaaS))的软件制造商。


规避不良实践,遵循建议方针

这份报告还将不良实践具体分为三大类别:
1.产品属性,即描述软件产品中肉眼可见与安全相关的质量属性。
2.安全功能,描述产品所支持的安全功能。
3.组织流程和政策,描述软件开发商在确保安全方法的透明度等方面采取的实际行动。

这份报告主要面向负责为关键基础设施或者国家关键职能等应用场景开发软件产品及服务的各软件开发商(包括本地软件、云服务以及 SaaS 软件即服务)。此外,这份报告还鼓励全体软件开发商避免采取这些可能影响产品安全性的不良实践。报告提到,“通过遵循本指南中的建议,开发商将向客户发出信号,表明他们高度关注向客户交付成果的安全因素、牢牢秉持在设计之初就重视安全的基本原则。”

Omdia 分析师 Brad Shimmin 表示,“这项指南是对美国政府此前关于软件安全问题的早期声明的延续,当时的声明可以追溯到 2022 年,意在提醒技术提供商和企业用户尽量使用或迁移至内存安全语言。”他解释称,“抛开新增代码不谈,当时的文件和美国政府表达的立场相对比较和缓,没有立即要求从 C/C++ 迁移至 Rust。CISA 的设计文档中也提到,软件维护者根本不可能在短时间内完成如此规模的代码库迁移。”当时的指南虽然属于自愿性质,但也代表着 CISA 在基准安全实践方面的最强立场,包括提醒企业注意到可能被疏忽的不良软件开发实践,特别是其中触及基础设施的部分。

但时间的洪流一刻不停向前奔涌,最新报告已经要求企业必须在 2026 年 1 月 1 日之前建立内存安全发展路线图;报告指出,“对于以非内存安全语言编写的现有产品,到 2026 年 之前仍缺少明确内存安全迁移路线图的情况,将被视为存在风险,即显著提高了国家安全、国家经济安全以及国家公共卫生及安全所面临的风险。”

CISA 战略合作伙伴关系及漏洞项目开发负责人 Rina Rakipi 表示,CISA 已获得超过 230 家软件厂商的自愿承诺。加入“安全设计”计划,意味着这些厂商承诺在一年内达成一系列网络安全目标,包括减少产品中的默认密码、增加多因素身份验证的使用,以及消除特定类别的漏洞。Rakipi 说:“我们非常高兴有超过 230 家软件厂商自愿参与这一承诺。展望未来,我们期待在接下来的2024年中看到这 230 家公司取得的实质性进展。”

来源:secure-design-pledge-signers

此外,报告还要求企业在同一日期之前移除管理员账户中使用的全部默认密码。这一截止日期已经将指南内容从建议升级为预期标准。报告同时指出,内存安全路线图应要搞开发商在主要代码组件(例如面向网络的代码或者处理加密操作等敏感功能的代码)当中将要采取的首选内存安全漏洞消除方法。

报告指出,“开发商应当证明,其内存安全路线图将如何优先缓解其所开发产品中内存安全性的脆弱性,同时证明他们正做出合理努力以实施并推进其内存安全路线图。”Shimmin 在采访中解释称,“但有两个现实理由会迫使企业继续维护由 COBOL 和 Fortran 编写的成规模代码——成本和风险。首先,对数百万行旧代码进行迁移在财务上不具备可行性,而且任何负责任的组织都无法承担由此带来的业务风险。”此外,根据报告内容,关键基础设施还面临着以下“异常风险”的困扰:
1.默认密码。
2.直接 SQL 注入漏洞。
3.缺少基本注入检测机制。
4.缺少多因素身份验证机制。

开源难题
对于开源软件,该报告称应特别关注开源漏洞。其他建议还包括:
1.企业必须维护明确的软件物料清单(SBOM)。
2.应当缓存依赖项,而非直接从公共来源处提取。
3.需要以负责任方式为其依赖的开源项目做出贡献。

报告提到,“软件开发商应当以负责任的方式使用并持续为其所依赖的开源软件做出贡献。”报告还敦促提高软件开发透明度,包括:
1.企业必须发布漏洞披露政策。
2.应当为所有关键漏洞发布 CVE。
3.必须提供关于安全问题的清晰说明文档。
4.预计将安全日志维护并保存六个月。

尽管手段严厉,但影响无疑非常积极。


一些其他不良实践具体还包括:
在 SQL 查询字符串中包含用户提供的输入内容
在操作系统命令字符串中包含用户提供的输入
存在默认密码与已知被利用的漏洞
存在已知可利用漏洞的开源软件
缺乏多因素身份验证
缺乏收集入侵证据的能力
未能及时向 CWE 发布 CVE
未发布漏洞披露政策

CISA 鼓励组织遵循指南中的建议,但该指南并不具有约束力。CISA 已就其指南开启了公众意见征询期,截止日期为 2024 年 12 月 16 日。

最后,Shimmin 总结称,CISA 建议掌握关键软件的企业在 2026 年初之前制定出明确的安全计划无疑是件好事。因为这能让软件行业有更多时间探索出理想的方法,确保我们的关键软件资产免遭威胁侵扰。“这些方法能够包括在硬件制造层面消除潜在的攻击面,以及由编程语言维护者提出方案。以 Safe C++ 提案为例,其呼吁为 C++ 创建一个超集以解决内存安全问题,借此避免强制进行大规模代码重写。”