一篇文章带你全面了解 Rust 与 安全
“P.S 最近报名参加了开放原子基金的一个比赛,所以公众号一直没有更新,等 15 号决赛以后,开始继续更新「Rust 研学 : Rust 与 LLM 系列」,正好这次比赛也用到了 Rust 实现了一个 AI Agent 来解决特定问题,到时候可以分享一下我的心得。
最近又是 xz 后门事件,又是 Rust 标准库发现 Windows 平台漏洞,也发现一些朋友可能对 Rust 的安全承诺有所误解,所以就打算写一篇文章,再谈一谈 Rust 与安全。
目录
安全的分类 :Safety 与 Security Rust 的安全承诺 但 Rust 不保证 100% 安全 Rust 基金会和 Rust 安全工作组的努力 Memory Safety 典型的 Rust Security : "BatBadBut" 关键安全漏洞 xz 后门启示录 Rust 安全策略防范 Rust 供应链安全解决方案:cargo vet 安全开发策略:减少依赖库 后记
安全的分类 :Safety 与 Security
在技术和工程领域中,"Safety"(功能安全性)和 "Security"(信息安全保障)是两个关键概念,它们虽然听起来相似,但代表着不同的关注点。尤其是中文翻译,这两个英文单词都被翻译为「安全」一词,所以会让一些人造成一些困惑。
其实这两个术语有着不同的内涵。
Safety(功能安全性):
Safety 特指功能安全性。通常指的是保护系统、设备或程序免受意外的、非故意的错误和故障的能力。这包括防止由系统故障、操作失误或外部事件引起的伤害或损害。例如,在编程中,功能安全性关注的是如何避免程序崩溃、数据损坏或意外行为,如缓冲区溢出、空指针访问、数据竞争、越界写入等。因为这些漏洞会直接影响系统功能本身。
Security(信息安全保障):
Security 特指信息安全,它属于一种安全保障。侧重于保护系统免受恶意攻击和威胁。这涉及到预防未授权访问、数据泄露、系统入侵、注入、DDOS 等其他形式的恶意行为。在同样的编程场景中,安全保障措施可能包括使用加密技术、实现复杂的用户认证机制、防御网络攻击等。相对于 Safety 来说, Security 更注重系统外侧的安全防护和保障。
总之,Safety 功能安全性的提升通常是通过增强系统的鲁棒性和错误处理能力来实现的,而Security 信息安全保障则需要考虑潜在的恶意行为,采取主动防御措施。
在实际应用中,Safety 和 Security 是相辅相成的。例如,内存安全漏洞(Safety 问题)可能被利用来执行恶意代码(Security 问题)。因此,编写安全的代码不仅需要关注代码本身的稳定性和防错性(提升 Safety),也必须考虑到潜在的安全威胁和防护措施(增强 Security)。
Rust 的安全承诺
很多人只听说 Rust 安全,但不知道 Rust 的安全承诺是什么,也不明白 Rust 的安全保障边界在哪里。以至于看到 Rust 语言曝出 CVE 就会说,「号称安全的 Rust 语言又不安全了」之类的“胡话”。
应用 Rust 语言,理解 Rust 语言的安全承诺很重要。Rust 编程语言的设计核心在于提供一种安全且高效的方式来编写系统级软件。其安全承诺主要围绕以下几个方面:
Memory Safety
Rust 最为人称道的特性之一是其内存安全性。Rust 通过所有权(ownership)、借用(borrowing)和生命周期(lifetime)的概念,防止了空指针异常和数据竞争等常见的内存错误。这种机制确保了在编译时就能捕捉到潜在的内存错误,极大地提高了软件的可靠性和安全性。
所有权系统:在 Rust 中,每个值都有一个称为其“所有者”的变量。值在任何时候只能有一个所有者。这个机制也同时阻止了内存泄漏的风险,因为当所有者变量离开作用域时,值和它占用的内存就会自动被清理。 借用规则:Rust 允许值的借用,但是有严格的规则:要么只能有一个可变借用(可以改变数据),要么有多个不可变借用(只读访问),这两者不能同时存在。这避免了数据竞争,保证了线程安全。 生命周期标注:Rust 要求开发者在某些情况下标明内存数据的使用期限(生命周期),这有助于编译器理解引用何时仍然有效,何时则可能导致悬挂引用。
以上内存安全规则,都是借由 Rust 语言静心设计的类型系统来保障的,由编译器在编译期根据类型系统来进行检查,从而达到内存安全目标。
但 Rust 不保证 100% 安全
理解 Rust 的安全承诺也意味着要认识到它的界限。
尽管 Rust 提供了强大的安全保障,但它并不声称能100%保证软件安全。安全性依旧依赖于开发者正确使用语言提供的功能。例如,Rust 不能自动防范逻辑错误或算法缺陷,开发者需要对其代码逻辑进行彻底的测试和审查。
另外,如果开发者使用 Unsafe Rust ,则安全保障的义务和责任也将落到每个开发者身上,不仅仅是 Unsafe Rust 代码编写者,还有 Unsafe Rust 代码调用者。通过 unsafe
代码块,开发者可以选择绕过 Rust 的安全检查,直接操作内存。这为高级优化提供了可能,但同时也带来了风险。
虽然官方没有给出一个统一的 Unsafe Rust 编码规范,但是业内还是有一套约定俗成的 Unsafe Rust 安全抽象规范的。这方面可以参考 Google Android/ Rust for Linux/ Rust std 这些内部实现。
关于这一点,我在 《Rust 编码规范》的 Unsafe Rust 部分[1]也有总结,供大家参考。
另外,虽然 Rust 努力提供内存安全,但它不直接处理其他类型的安全问题,如网络安全或用户认证等。因为这属于 Security 信息安全保障范围。对于 Security 问题,是很多语言都会面临的问题。
几个比较典型的 Security 问题就是:
DDoS 问题。比如你采用了错误的 Hash 算法,是有可能导致这种问题。 字符类问题。比如 2021 年 Rust 编译器也发过安全公告 (CVE-2021-42574),公开了一个利用 Unicode 漏洞攻击方法「特洛伊之源」。本公众号也专门介绍过这个漏洞 : 特洛伊之源 | 在 Rust 代码中隐藏的无形漏洞
本公众号历史文章里也介绍过多个安全问题。包括也介绍了用 Safe Rust 如何构造安全问题的 cve-rs 相关代码解读。
你要明白,Rust 语言不是万能的,也不是安全银弹,它只是软件安全发展路上的一个比较进步的解决方案而已。尤其是,Security 问题,不能仅仅依赖语言。
Rust 基金会和 Rust 安全工作组的努力
Rust 官方安全工作组正致力于扩展 Rust 的安全特性,通过推广更安全的编程实践和改进现有的工具支持来提升 Rust 程序的安全性。例如,他们推出了如 cargo-audit
这样的工具,帮助开发者检测已知的依赖库漏洞,及时进行修补。并且维护一个 https://rustsec.org/[2] 来跟踪 Rust 及其生态库中被发现的 CVE 和 软件缺陷等问题。
Rust 基金会在进一步扩大 Rust security 安全保障也在努力行动中。 包括雇佣了安全专家,对 Rust 生态库做威胁情报分析等等。
通过这些机制和社区的持续努力,Rust 希望能够在系统编程领域提供一个既安全又高效的选择,减少常见的安全漏洞,同时提升开发效率和程序性能。
典型的 Rust Security : "BatBadBut" 关键安全漏洞
这两天在 Rust 标准库中发现了一个名为 "BatBadBut" 的关键安全漏洞,影响所有在 Windows 上 1.77.2 版本之前的版本。该漏洞被标识为 CVE-2024-24576,CVSS 分数为 10.0,允许攻击者通过绕过调用批处理文件时的转义机制来执行任意的 shell 命令。
近期 Rust 安全响应工作组收到通知,Rust 标准库在 1.77.2 版本之前,在 Windows 上使用Command
调用批处理文件(带有bat
和cmd
扩展名)时,没有正确转义参数。
能够控制传递给生成的进程的参数的攻击者可以通过绕过转义来执行任意的 shell 命令。
对于在 Windows 上使用不受信任的参数调用批处理文件的人来说,这个漏洞的严重程度是关键的。其他平台或用途不受影响。
Command::arg
和Command::args
的API在文档中声明,无论参数的内容如何,参数都将原样传递给生成的进程,并且不会被 shell 评估。这意味着可以安全地将不受信任的输入作为参数传递。
“这个函数不属于 Rust 内存安全承诺范畴,所以将函数命名为 unsafe 也无济于事。
在 Windows 上,这个实现比其他平台更复杂,因为 Windows API 只提供一个包含所有参数的字符串,并且由生成的进程来拆分它们。大多数程序使用标准的 C 运行时 argv,实际上导致参数被拆分的方式基本一致。 有一个例外,即 cmd.exe
(用于执行批处理文件等其他任务),它具有自己的参数拆分逻辑。这迫使标准库为传递给批处理文件的参数实现自定义转义。
所以,有人报告说 Rust 的转义逻辑不够严谨,可能会传递恶意参数导致任意的 shell 执行。由于 cmd.exe
的复杂性,Rust 团队也没有找到一个能够正确转义所有情况下参数的解决方案。为了保持标准库的 API 保证,官方团队改进了转义代码的鲁棒性,并将 Command
API 更改为在无法安全转义参数时返回 InvalidInput
错误。在生成进程时将发出此错误。
其实这种问题,很多语言都有,但就是因为 Rust 语言主打安全,所以,它就当了出头鸟。 不明所以之人,就开始攻击 Rust 的安全性了。"BatBadBut" 漏洞是由安全研究员 RyotaK 发现,并负责向 Rust 安全团队进行了负责任的披露。
虽然最初的关注点是 Rust 编程语言,但现在已经发现 "BatBadBut" 漏洞不仅仅局限于一个 CVE 标识符。该漏洞影响多种编程语言和工具,每种都分配了不同的 CVE ID,具体取决于实现和影响。
除了与 Rust 标准库相关的 CVE-2024-24576 外,"BatBadBut" 还包括 CVE-2024-1874、CVE-2024-22423(影响 yt-dlp,风险评分为 8.3)和 CVE-2024-3566(影响 Haskell、Node.js、Rust、PHP 和 yt-dlp)。这凸显了该漏洞的广泛性质,以及开发人员需要评估各种编程语言和工具中的应用程序和依赖关系的需求。
本着负责任的态度,Rust 官方团队还是在 Rust 1.77.2 中修复了这个问题(其他语言不一定给你修复)。请注意,批处理文件的新转义逻辑偏向保守一些,可能会拒绝有效的参数。那些自己实现转义或仅处理受信任的 Windows 输入的人也可以使用 CommandExt::raw_arg
方法绕过标准库的转义逻辑。
xz 后门启示录
xz 后门事件[3],官方标记为 CVE-2024-3094,揭露了在广泛使用的开源库中植入恶意代码的潜在危害。攻击者通过精心计划和执行,逐步获得了项目的维护权,最终在xz/liblzma
库中引入后门。
这种攻击不仅影响了Linux的多个发行版,还可能对使用这些库的应用程序造成间接影响。特别是在此事件中,后门被设计为难以检测,它不直接修改代码库的文件,而是在发布的压缩包中植入,这使得它能够在不引起立即怀疑的情况下传播。
Rust 安全策略防范
Rust 社区的 Guillaume Endignoux[4] 从他自己的 lzma-rs 项目(一个纯 Rust 实现的 XZ 压缩格式库)的视角出发,分析了 Rust 社区安全策略防范 xz 后门事件所起到的作用。
RustSec advisory[5] 安全策略对于标记 Rust crates 为未维护状态有严格定义,强调了在开源项目中关于软件组件维护状态的透明性。这一过程涉及提交包含特定细节的 pull request,有助于库的使用者做出知情决策,避免使用可能存在安全漏洞的过时或未维护的组件。 crates.io 也严格限制了 crate 所有者的身份,是由 token 和 个人邮箱绑定的。
这种策略对于防御类似 xz 后门这样的人为攻击具有潜在帮助。通过公开和及时更新组件的维护状态,可以提高社区的警觉性,从而减少因使用不安全或弃用的依赖而导致的安全风险。这有助于及时识别和替换那些可能被植入恶意代码的组件。
然而,对于 xz 后门这类属于社会工程学层面的攻击,安全策略作用也及其有限。
Rust 供应链安全解决方案:cargo vet
Rust 在提供供应链安全方面也有另外一种解决方案。
其实为应对此类供应链问题,Mozilla 两年前就开发了 cargo vet
工具,用于帮助开发者审核其项目的依赖。这个工具检查依赖项的安全性记录,帮助识别和防范可能的安全威胁。虽然这不能保证完全防止所有供应链攻击,但它提供了一种机制,通过增加代码和依赖的透明度来降低风险。这个工具也被 Google 内部采用,Google 也发布了经过他们团队审计过的 Rust crate 列表[6]。
Cargo vet 动机
cargo-vet 的主旨是确保项目的第三方依赖已经由可信的实体审计,力求轻巧和易于集成。
运行时,cargo-vet 会将一个项目的所有第三方依赖关系与项目作者或他们信任的实体进行的一系列审计进行匹配。如果有任何差距,该工具在执行和记录审计方面提供辅助。
人们通常不审计开源依赖关系的主要原因是,它的工作量太大。cargo-vet的目的是将开发者的工作降低到一个可管理的水平,有几个关键的方法。
共享。公开的 crate 经常会被很多项目使用,这些项目可以共享他们的发现,以避免重复工作。 相对审计。同一crate 的不同版本往往是非常相似的。开发人员可以检查两个版本之间的差异,并记录如果第一个版本被审核过,第二个版本也可以被认为是被审核过的。 延迟审计。要实现全覆盖并不总是实际的。依赖关系可以被添加到一个例外列表中,这个列表可以随着时间的推移而逐渐缩小。这使得在一个新的项目中引入货真价实的审核,并防范未来的漏洞,同时在时间允许的情况下逐步审核预先存在的代码,这是很琐碎的。
在 Rust 代码中减少第三方代码安全风险相比于其他语言较为容易,Rust有以下两个独有的特点创造了系统分析的条件:
首先,审计 Rust 代码相对容易。与 C/C++ 不同,Rust 代码默认是内存安全的,与 JavaScript 不同的是,没有高度动态的共享全局环境。这意味着开发者通常可以在高层次上推断模块潜在行为的范围,而无需仔细研究其所有内部不变量。例如,一个复杂的字符串解析器,具有适当的接口、没有Unsafe 代码,也没有强大的导入,其损害程序其他部分的手段是有限的。这也使得我们更容易根据与之前可信版本的差异来断定新版本是安全的。 其次,Rust生态系统中几乎每个人都依赖同一套基本工具--Cargo和crates.io--来导入和管理第三方组件,而且依赖集的重叠度很高。
Cargo-vet 工作机制
大多数开发人员都是忙碌的人,他们致力于供应链完整性的精力有限。因此,cargo-vet 背后的驱动原理是尽量减少摩擦并尽可能轻松地做正确的事情。它旨在简化设置,不显眼地融入现有工作流程,引导人们完成每一步,并允许整个生态系统共享审计广泛使用的软件包的工作。
具体工作流为:
初始设置 :Cargo-vet 可以通过将工具添加为 linter 并运行来启用
cargo vet init
,这会在存储库中创建一些元数据。这大约需要五分钟,而且至关重要的是,不需要审核现有的依赖项。这些会自动添加到豁免列表中。添加新的第三方crate。一段时间后,开发人员尝试将新的第三方代码拉入项目。这可能是一个新的依赖,或者是对现有依赖的更新。作为持续集成的一部分,cargo-vet 分析更新的构建图,以验证新代码是否已由受信任的组织审核。如果没有,补丁将被拒绝。
如果被拒绝,cargo-vet会帮助开发者解决问题:
首先,它会扫描注册表以查看是否有任何知名组织之前审核过该包。 如果匹配,cargo-vet 会通知开发人员并提供将该组织添加到项目受信任导入的选项。 导入和审计提交的批准自动落入 supply-chain/
目录的代码所有者手中,该所有者应由项目领导或专门的安全团队组成。如果没有匹配,开发人员可以自己审计。 cargo-vet 简化了这个过程。通常有人已经审核过同一个 crate 的不同版本,在这种情况下,cargo-vet 会计算相关的差异并确定最小的差异1[7]。在引导开发人员完成确定要审计什么的过程之后,它会在本地或在 Sourcegraph[8]上呈现相关工件以供检查。 共享审计结果。Cargo-vet 的共享和发现机制建立在这种去中心化存储之上。导入是通过直接指向外部存储库中的审计文件来实现的,而注册表只是来自知名组织的此类文件的索引。这也意味着攻击者没有中央基础设施可以攻破。
看得出来,Cargo-vet 将复杂的审计工作通过统计分析和巧妙的流程设计给简化了,让开发者可以「懒洋洋」地一步步完成这份工作。
Cargo-vet 具有许多高级功能——它支持自定义审计标准、构建图中不同子树的可配置策略以及过滤特定于平台的代码。
安全开发策略:减少依赖库
在实际操作中,减少不必要的外部依赖是降低被攻击风险的有效策略之一。例如,sudo-rs 项目通过精简其依赖库来减少潜在的安全威胁[9]。这种方法有助于控制项目的攻击面,从而提高整体的安全性。这种策略强调了在软件开发中,依赖管理的重要性,特别是在供应链攻击越来越多的当下,通过控制和审计依赖项可以显著提升项目的安全性。
然而,无论 Rust 社区采用多么严格的安全策略,也无法避免底层 Linux 库被攻击的风险。我在 Rust 接棒 C 语言 :Rust for Linux 中正在发生的技术变革 一文中写到过:
“上文说到,在网络方面,Rust 开发人员不得不要求网络维护者减慢合并 Rust 代码的速度[3]。 具体情况是,目前 Tomonori Fujita 正在为物理层(PHY)驱动程序添加一些 Rust 抽象。已经进行了大量的审查,并且根据这些审查意见频繁地重新制定了补丁集。不幸的是,Rust-for-Linux 开发人员在跟上这个速度方面遇到了困难。两个社区的开发实践似乎存在一些脱节。 Andrew Lunn(该补丁的审查者)指出,网络补丁不需要经过审核就可以合并;“如果在三天内没有反馈,并且通过了CI(持续集成)测试,那么很可能会被合并。”但 Ojeda (Rust for Linux 核心开发者)表示,CI 测试无法确定抽象是否经过良好和合理的设计,这是 Rust 抽象(重点是安全抽象)所需的关键属性;他希望有人参与其中。Lunn 回答说,最终是人决定是否合并代码,但 API 问题只是像其他 bug 一样,如果发现问题,可以稍后修复。
我是认同 Ojeda 的观点。因为Rust 抽象,尤其是 Unsafe Rust 安全抽象是需要专门设计的;但是 Linux 中某些模块开发和合并速度太快,人手严重不足,以及 C 那边的人认为 API 后面修改也可以,这是有误解的。其实 Rust 安全抽象在后面修改就已经来不及了,必须在前期就为其做好安全抽象。
“然而,网络维护者Jakub Kicinski 表示, "更长的审查周期将使跟踪补丁和讨论变得难以管理。" 他想知道在初始阶段之后,Rust-for-Linux 项目是否会减少对补丁审查的参与。Ojeda 同意这是目标,但初始的抽象集将需要更多的审查时间。
因此,我觉得 Linux 过快的发布节奏,对于供应链安全的保障是脆弱的。 所以,供应链安全还是任重道远啊。作为开发者,不可能把你代码里所有的依赖库都审查一遍,也许未来 AI 在供应链安全上能做出一些突破。
后记
希望本文能让读者朋友们对 Rust 与 安全建立一个「系统且健康」的认知。
感谢阅读。
《Rust 编码规范》的 Unsafe Rust 部分: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
[2]https://rustsec.org/: https://rustsec.org/
[3]xz 后门事件: https://www.wired.com/story/xz-backdoor-everything-you-need-to-know/
[4]Guillaume Endignoux: https://gendignoux.com/blog/2024/04/08/xz-backdoor.html
[5]RustSec advisory: https://rustsec.org/
[6]Google 也发布了经过他们团队审计过的 Rust crate 列表: https://opensource.googleblog.com/2023/05/open-sourcing-our-rust-crate-audits.html
[7]1: https://mozilla.github.io/cargo-vet/how-it-works.html#1
[8]Sourcegraph: https://sourcegraph.com/
[9]sudo-rs 项目通过精简其依赖库来减少潜在的安全威胁: https://www.memorysafety.org/blog/reducing-dependencies-in-sudo/