使用 Cackle 提高 Rust 供应链攻击难度(2023)

Lobsters Hottest 工具

摘要

David Lattimore 介绍了 Cackle,这是一个通过使用访问控制列表(ACL)限制依赖项行为来帮助防止 Rust 供应链攻击的工具,从而降低通过第三方 crate 引入恶意代码的风险。

<p><a href="https://lobste.rs/s/dl0yiv/making_rust_supply_chain_attacks_harder">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/24 07:55

# 用 Cackle 让 Rust 供应链攻击更难 来源:https://davidlattimore.github.io/posts/2023/10/09/making-supply-chain-attacks-harder.html David Lattimore \-2023\-10\-09 ## 用 Cackle 让 Rust 供应链攻击更难 如果你想读得简短一点,可以直接跳到[介绍 Cackle(又名 cargo-acl)](https://davidlattimore.github.io/posts/2023/10/09/making-supply-chain-attacks-harder.html#introducing-cackle-aka-cargo-acl)\。 ## 关于 Alex 的虚构故事 Alex 是一名软件工程师,她构建了一个工具并许可给客户使用。她的客户数量不多,但都很喜欢她的工具。她使用 Rust 构建了这个工具,依赖了来自 crates.io 的约 20 个直接依赖。如果算上间接依赖,她的工具有大约 250 个依赖。 其中一个间接依赖是一个名为 `foobar` 的 crate,作者是 Chris。Chris 几年前写了 `foobar`,但现在已经对维护失去兴趣。 一个叫 Bob 的人给 `foobar` 提交了几个 PR,现在正在询问这些 PR 何时能被合并,以及项目是否还在维护。Bob 表示如果 Chris 太忙,他很乐意接手维护。Bob 看起来很热心,于是 Chris 将 Bob 添加为所有者。 几个月过去了,Alex 正在修复一个客户发现的 bug。修复完成后,她运行了 `cargo update`,运行了测试,做了一些手动测试,然后将更新后的工具版本推送给了客户。这个更新版本中包含 `foobar` crate 的更新版本。 一周后,她接到一位客户的电话,客户非常愤怒。原来客户的用户数据库被泄露了,攻击者威胁说如果不支付赎金就会公开这些数据。客户确定 Alex 的工具正在向互联网上的某个未知地址发送数据。 经过一番调查,Alex 在新版本的 `foobar` 中发现了一些代码正在做这件事。她将 `foobar` crate 固定到一个未受影响的旧版本,并发布了新版本,但她的生意和声誉已经受损。 ## 供应链攻击 关于 Alex 的故事就是一个供应链攻击的例子。这种情况可能以多种方式发生: - 开发者的 crates.io 令牌可能被泄露。 - 精疲力竭的开发者可能将 crate 的控制权交给一个他们并不真正了解的人。 - 某人可以构建一个 crate,然后向其他项目提交 PR 来使用他们的 crate。之后他们可以在自己的 crate 中添加恶意代码。 - 开发者有时会在自己的 crate 中添加抗议软件。即使是为了正当理由,这也可能造成重大损失和信任丧失。 这类事件在 Rust 生态中目前还不多见,但随着生态的发展,预计会越来越频繁。在其他更大的生态系统中,如 node.js 和 Python 的包管理系统,这类事件已经半定期地发生。 ## 有助于防止供应链攻击的做法 你可以采取多种措施来帮助防止供应链攻击: - 审查你使用的 crate 的代码。 - 使用并贡献代码审查数据库,例如 [cargo-vet](https://github.com/mozilla/cargo-vet) 或 [cargo-crev](https://github.com/crev-dev/cargo-crev)。 - 避免依赖那些只能帮你节省几行代码的琐碎 crate(例如 node.js 中的 left-pad)。琐碎的依赖不仅节省不了多少代码,而且由于更容易创建,风险也更高。 - 在其他条件相同的情况下,优先选择下载量更多的 crate。 - 选择由你认识并且社区声誉良好的作者编写的 crate——尽管每个人都是新手起步,所以这是一个权衡。 - 从 crates.io 复制 `cargo add` 命令,而不是手动输入名称。这有助于防止你成为域名仿冒攻击的受害者。 - 对于不需要或不需要新功能、bug 修复等的二进制 crate,可以考虑固定其版本。但如果这样做,你应该监控安全公告。 对于 crate 作者,还有一些额外的步骤: - 在审查 PR 时,仔细查看任何新添加的依赖。 - 谨慎选择将 crate 的控制权交给谁。 - 考虑要求新的维护者 fork 你的 crate,而不是直接移交控制权。 ## 介绍 Cackle,又名 cargo-acl Cackle 是一个代码 ACL 检查器,也是一个额外的工具,用于帮助使供应链攻击更困难。它旨在配合上述方法一起使用。Cackle 通过 `cackle.toml` 配置,该文件与 `Cargo.toml` 放在一起。在配置文件中,你可以定义 API 类别,例如 "net"、"fs"(文件系统)和 "process",这些是你想要限制的。然后你可以指定哪些你依赖的 crate 被允许使用这些 API。运行时,Cackle 会检查你的依赖树中是否有任何 crate 使用了它们不被允许使用的受限 API。 API 定义规定了该 API 包含或排除哪些名称。例如,我们可以如下定义 "process" API: `` [api.process] include = [ "std::process", ] exclude = [ "std::process::exit", ] `` 排除的优先级高于包含,因此应该更具体。这里我们定义了一个名为 "process" 的 API,并将 `std::process` 模块中的所有函数归类为该 API。然后我们排除了 `std::process::exit`。因此,对 `std::process::Command::new` 的引用会被视为使用了 `process` API,但 `std::process::exit` 则不会。 然后我们可以允许特定的包使用这个 API。例如: `` [pkg.rustyline] allow_apis = [ "fs", ] allow_unsafe = true `` 这表示允许 `rustyline` 包使用文件系统 API,也允许使用不安全代码。 在 Alex 的故事中,如果她使用了 Cackle,那么 `foobar` crate 可能会被标记为现在使用了 "net" API,而之前并没有。 ## 安装 Cackle 目前,Cackle 仅支持 Linux。假设你已经安装了 Rust,可以运行: `` cargo install --locked cargo-acl `` ## 构建初始配置 Cackle 有一个内置的终端 UI,可以帮助你创建 `cackle.toml`。我们将使用它来创建初始配置。从包含 `Cargo.toml` 的目录运行: 问题面板显示了到目前为止检测到的与依赖项使用的权限相关的问题。最初,它还会显示有助于创建配置的操作项。 缺少配置的 UI 截图 我们选择一个问题,按 'f' 显示可能的修复。 缺少配置的修复 UI 截图 这里我们可以看到两个可以创建的初始配置选项。我们选择使用推荐的初始配置,然后按 'f' 应用修复。 选择沙箱的 UI 截图 接下来,UI 会要求我们选择想要使用的沙箱类型。目前唯一支持的沙箱是 Bubblewrap。沙箱用于运行构建脚本、运行 rustc(以及因此的过程宏)和运行测试。每个二进制文件使用的 API 会在运行前由 Cackle 检查,因此取决于你检查这些 API 使用的仔细程度,你可能决定不担心沙箱问题。 此时,我们已经导入了通过标准库限制网络访问、文件系统访问和命令访问的 API。但我们还没有限制依赖树中第三方 crate 可能提供的类似 API。例如,tokio 可以用于执行网络访问,但我们尚未将其任何 API 分类为网络访问。 第三方 crate 可以导出 Cackle API 定义。如果有,Cackle UI 会询问我们是否要导入这些 API 定义。然而,由于 Cackle 还比较新,我们可能需要自己编写这样的 API 定义。例如,我们可以为 `tokio` 编写 "net" API 定义如下: `` [api.net] include = [ "tokio::net", ] `` 这里我们说的是,对 `tokio` crate 的 `net` 模块中任何内容的引用都应被归类为使用了 `net` API。 我们可以手动编辑 `cackle.toml` 来添加这个 API 定义。它将与 Cackle 内置的标准库 `net` API 定义合并,我们在创建初始配置时已经导入了这个定义。 Cackle 现在会继续构建你的 crate 及其依赖。随着构建的进行,Cackle 会分析目标文件、可执行文件和源代码,以查看哪些 API 在哪里被使用,以及哪些 crate 使用了 unsafe。当发现问题时,它们会被添加到 UI 的 "问题" 面板。 问题列表和包详情的 UI 截图 底部面板显示涉及包的详细信息。你可以利用这些信息来理解包的功能,这对于判断它使用特定 API 是否合理很有帮助。 如果你想查看某个包是如何被引入的,可以按 't' 查看从当前问题的包到你的包的依赖树。 包树的 UI 截图 对于 API 使用和不安全代码,你可以按 'd' 查看该 API 或不安全代码的每个使用位置的详细信息。 显示不安全使用位置的 UI 截图 如果你同意该 crate 使用该 API 或不安全代码,可以按 'f' 查看对配置文件的可用的修复。 允许不安全的修复 UI 截图 再次按 'f' 应用选定的修复。 与不安全使用类似,对于不允许的 API 的使用位置,可以按 'd' 显示。 不允许的 API 使用位置的 UI 截图 这里我们可以看到 `quote` crate 的构建脚本正在使用 process API,通过引用 `std::process::Command`。 与其他问题一样,我们可以按 'f' 查看可用的修复。 允许 API 的修复 UI 截图 我们有几种允许 API 使用的选项。`quote` crate 在其构建脚本中使用了 `process` API。我们可以只允许这一点,这是第一个可用的修复。 或者,我们可以选择允许 `quote` crate 使用 `process` API,但仅限于构建脚本部分。这意味着,例如,`quote` 的 `lib.rs` 中的代码如果随后被另一个包的构建脚本使用,也可以使用 `process` API。它也会允许 `quote` 自身的 `build.rs` 使用 `process` API。 最后,我们可以允许 `quote` 无论构建什么类型的二进制文件都可以使用 `process` API。 底部面板显示了正在进行的更改的描述以及 `cackle.toml` 的差异。 只考虑可达代码中的 API 使用。如果你想查看代码是如何可达的,可以按 'b' 获取一个回溯,显示从 main 到使用该 API 的函数的函数引用路径。 API 使用的回溯截图 这里我们可以看到 ripgrep 的构建脚本调用了 clap 的 `App::gen_completions`,进而调用了 `Parser::gen_completions`,而后者使用了 `File::create`。 请注意,这不是运行时回溯——构建脚本尚未执行。而是一个基于函数引用的图构建的假设回溯。按 escape 离开回溯。 Cackle 可能会检测到的其他类型的依赖问题包括: - 构建脚本(build.rs)失败。可能是因为它尝试访问网络,或尝试在其输出目录之外写入文件。可能的修复包括允许网络访问、允许写入特定目录,或针对该特定构建脚本禁用沙箱。 - 构建脚本可能向 cargo 发出指令,请求链接器的额外参数。这可能是我们未检查的代码来源,因此我们需要查看正在做的事情是否合理,如果是,则允许它。 一旦所有问题都已解决,Cackle 将退出。 我们可能希望将 Cackle 集成到 CI 中。为此,我们可以运行: 如果我们还想在 Cackle 下运行测试,可以运行: 在 GitHub Action 中,我们可以这样做: `` name: cackle on: [push, pull_request] jobs: cackle: name: Cackle check and test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable - uses: cackle-rs/cackle-action@latest - run: cargo acl -n - run: cargo acl -n test `` 这将进行非交互式检查依赖项与配置的匹配情况。如果遇到任何问题,将打印详细信息并以失败状态退出。 如果你的 `Cargo.lock` 已检入仓库,你可能希望在运行 `cargo acl` 之前在 CI 中添加 `rm Cargo.lock`,这样我们将检查你的依赖项的最新语义版本兼容版本。 ## 工作原理 当你运行 `cargo acl` 时,Cackle 会调用 `cargo build`,但会包装 rustc、链接器和任何构建脚本。随着构建的进行,这些包装程序会向父 Cackle 进程发送回通信,父进程分析构建产物以确定正在使用哪些 API。它通过解析目标文件来确定哪些函数引用了哪些其他函数。它还解析链接可执行文件中的调试信息,以确定每个引用的源位置。然后,源文件通过 rustc 编写的依赖文件映射到提供该源文件的包。 Cackle 分析最终可执行文件的一个好处是,死代码不会被视为 API 使用。例如,如果你依赖 `image` 包来编码 PNG,但不使用 image crate 中读写文件的函数,那么这些函数不应该进入可执行文件,这意味着 Cackle 不会将 image 包归类为使用文件系统 API。这意味着如果后来 `image` 包开始从一个以前没有的文件系统访问函数中进行操作,它将被标记为使用了不允许的 API。 ## 规避 当然,有一些方法可以绕过 Cackle 并使用 API 而不被发现。 一种方法是如果你的配置不完整。例如,如果你的 crate 依赖 tokio,但你没有将 `tokio::net` 添加到 `api.net` 的包含中,那么你的另一个依赖可以使用 `tokio::net` 进行网络访问而不被检测到。Cackle 尝试通过寻找与你的 API 同名的顶级模块来在一定程度上缓解这个问题。因此,对于 tokio,Cackle 会建议你将 `tokio::net` 添加到 `net` API 中。 一旦你允许一个包使用某个 API,它就可以自由地做任何它想做的事情。Cackle 在那些没有被授予任何特殊权限的 crate 上提供了最强的保护。同样,一旦一个 crate 被允许使用 unsafe,理论上它可以利用 unsafe 做任何事情。话虽如此,使用 unsafe 来完成比如网络访问而不链接 C 代码,比仅仅使用 Rust 的 `std::net` API 更难,所以我们至少让潜在的攻击者更难了。 更成问题的是,平台特定或配置特定的恶意代码可能会被遗漏。例如,仅在 Windows 或 Mac 上存在的恶意代码会被遗漏,因为 Cackle 目前只在 Linux 上工作。 最后,无疑存在一些 bug 可能导致 API 使用未被检测到。 ## 在几个不同的二进制文件上运行的观察 当我在 crates.io 上发布的一些流行二进制文件上运行 Cackle 时,我观察到通常略少于一半的 crate 不需要特殊权限。这很好,因为如果这些 crate 中的任何一个开始使用受限 API 或 unsafe,我们应该会注意到。 在需要特殊权限的 crate 中,最常见的是需要 unsafe。大多数使用 unsafe 的 crate 都提供了一些低级 API,这些 API 无法用安全 Rust 实现……

相似文章

我用Rust构建了一个自托管的上下文赌博机装置,并部署在一个实时的AI交易产品上。在发现运行时错误之前,先找到了自己配置中的两个错误。

Reddit r/ArtificialInteligence

宣布两个开源Rust项目:Lycan(一种用于上下文赌博机的图执行语言)和Syntra(一个自托管的Docker设备,用于服务Lycan胶囊)。作者在自己的实时AI交易产品上自用测试,发现数据管道错误(而非算法问题)主导了适配工作。

hax:一个Rust验证工具

Lobsters Hottest

hax是一个将Rust代码翻译成F*、Rocq和Lean等正式语言以进行高保障验证的工具。