利用LLVM的optnone属性改进Rust密码学代码实现
重温恒定时间
恒定时间密码算法无论输入如何,总是在相同时间内执行。并非所有操作都需要相同时间执行,但时间变化不能依赖于秘密数据。如果依赖,攻击者可能推断出秘密数据。然而,在编译恒定时间密码代码时保持秘密数据安全很困难。编译器需要保留程序的"可观察行为",但由于没有时间概念(除专用编译器外),它也可以自由改变代码的恒定时间属性。
实践中,为防止编译器改变精心实现的恒定时间代码,我们必须欺骗编译器。我们需要告诉它我们知道代码的某些信息——我们将以它无法看到的方式读写内存,类似于多线程代码。我们将探索的选项是告诉编译器不要使用所有技能生成高效代码。
我们从第1部分的示例开始:一个选择函数,根据choice参数选择参数a或b。以下是不考虑恒定时间执行的实现:
|
|
通过添加#[inline(never)]
属性,更容易看到更改的效果。首先编译代码并生成LLVM-IR:
|
|
生成test.ll文件,其中包含conditional_select的IR:
|
|
可以看到存在依赖于choice变量的分支。我们需要避免分支以保持choice值的信息秘密。
恒定时间实现
|
|
根据Rust文档,“bool表示值,只能是true或false。如果将bool转换为整数,true将为1,false将为0。“这里我们使用此属性,将bool转换为整数。
变量mask现在将是0xffffff或0,取决于choice是true还是false。
xor和and操作将确保返回值在choice为true时为a,choice为false时为b。
完美!让我们看看是否能够生成无分支代码:
|
|
哎呀!发生了什么?核心操作的分支已被移除(见bb1)。这是成功,但还有更多内容。现在有到panic的条件分支,取决于choice的值。此条件分支由rustc在调试构建中添加以检测有符号整数溢出。在生产构建中,不会发出有符号整数溢出检查,分支将不存在。
然而,一个令人担忧的细节是存在对@llvm.assume的调用,它依赖于choice的值。根据LLVM文档:
“此内在函数允许优化器假设每当控制流到达内在函数调用时,提供的条件始终为真。不会为此内在函数生成代码,并且仅对提供条件有贡献的指令不用于代码生成。如果在执行期间违反条件,行为未定义。”
代码生成可能受choice值的影响。更仔细地检查条件,条件断言choice的值范围假定为[0,1]。真是松了一口气!没有秘密信息泄漏,因为它仅揭示choice的范围(已知信息),而不是其特定值。
似乎我们已经达到了目标。让我们确保在优化构建中仍然看起来正常:
|
|
|
|
根据目标架构,编译器可能将select语句降低为不同指令。在x86上,它可能降低为cmov指令,而在其他架构上,它成为条件分支。更糟糕的是,如果编译我们开始的非恒定时间版本,将得到完全相同的IR。所有工作都白费了!
我们可以看到,只要代码未启用优化,最终结果就是我们所期望的。另一方面,启用优化可能破坏代码的恒定时间属性。这引出了问题:我们能否影响编译器不优化conditional_select函数?Cargo和rustc接受全局禁用优化的参数,但通常不可能在整个系统上这样做。一个可能的解决方案可能是防止特定函数的优化。(这以前曾被建议作为改进情况的方法。)
对抗帮助编译器
既然我们在调试构建中有所需的IR代码,让我们探索如何使用LLVM属性optnone在函数级别禁用优化。LLVM文档说明:
“此函数属性指示大多数优化过程将跳过此函数,但过程间优化过程除外。代码生成默认为’快速’指令选择器。此属性不能与alwaysinline属性一起使用;此属性也与minsize属性和optsize属性不兼容。
此属性要求函数上也指定noinline属性,因此函数永远不会内联到任何调用者中。只有具有alwaysinline属性的函数才是内联到此函数体内的有效候选者。”
我们的下一个目标是用optnone属性标记conditional_select函数。根据文档,函数还需要noinline属性。碰巧的是,我们已经用#[inline(never)]
标记了函数。
我们将在Rust中实现一个属性,编译时将为函数生成optnone和noinline属性。
构建Rust编译器
要构建和运行Rust编译器,请参考此指南。从现在开始,我们将假设用于编译的命令是rustc +stage1。要验证是否使用自定义编译器,使用额外的-vV标志调用它。应该看到类似于以下的输出:
|
|
注意-dev版本字符串,表示自定义构建。
实现optnone属性
在这方面已经做了工作;Rust优化属性已经实现,可以优化程序的速度或大小。我们的目标是为优化属性实现"never"选项。目标是像这样编写conditional_select。(关于命名"never"属性的讨论。命名很重要,但出于我们的目的,我们不需要关注它。)
|
|
在非优化构建中注释函数将没有效果。在优化构建中,它将确保优化器不接触函数。
要实现这样的选项,第一步是用Never成员扩展OptimizeAttr属性。我们将使用此值作为信息载体,从解析到代码生成。
|
|
当在优化属性中找到符号never时,我们应该将以下行添加到codegen_fn_attr以发出先前添加的OptimizeAttr::Never成员:
|
|
此时,我们可以在Rust编译器内部用OptimizeAttr::Never注释函数。剩下的工作是确保它也应用于LLVM IR。
为此,我们向from_fn_attrs添加以下内容。当rustc发现具有#[optimize(never)]
属性的函数时,此代码实际上用所需属性标记LLVM函数。
|
|
现在,我们可以从#[optimize(never)]
Rust属性向LLVM IR添加optnone和noinline属性。但仍然有一些簿记工作要做。
我们需要更新功能门以包括优化属性中never选项的信息。
|
|
我们可以构建stage1编译器来测试更改。
|
|
结果
最后,我们准备好测试新属性。让我们用#[optimize(never)]
属性标记conditional_select函数并为opt-level=3编译。要启用优化属性,我们向test.rs文件添加#![feature(optimize_attribute)]
。
|
|
|
|
你会发现相应的IR现在是:
|
|
成功!optnone和noinline属性正在使用中,IR指令符合预期。我们现在完成了吗?只需创建拉取请求并合并?等等!在此之前,我们当然应该实现测试(感兴趣的读者可以在这里找到它们)。
但我们现在暂时搁置这一点。相反,让我们转向我们刚刚完成的不同方面:代码生成的指令选择阶段。
总是有"但是”
似乎我们取得了巨大进展甚至解决了生成恒定时间代码的问题。这部分是正确的,但就像密码学(和编译器)常见的那样,事情并不那么简单。虽然我们阻止了优化器重写函数,但在代码生成过程中仍然有指令选择阶段。在此阶段,编译器后端选择它认为合适的任何目标指令。这是我们简要讨论过的一个方面。我们隐含地假设LLVM IR中的指令(如xor)将成为目标指令集中的等效指令(如x86 xor指令)。虽然IR xor指令很可能在目标架构中实现为xor,但不能保证它会。代码生成也可能随时间演变,曾经成为特定指令的内容可能随不同版本的编译器而改变。
更糟糕的是,机器代码生成过程中存在优化。x86的一个例子是X86CmovConverterPass,它将在某些情况下将cmov转换为条件分支。这基本上将恒定时间操作(cmov)转换为非恒定时间条件分支,可能重新启用基于时间的侧信道攻击。
事情还不止于此。一旦我们达到实际的特定目标操作,仍然可能存在数据依赖的时间,例如在AMD上执行div:
“硬件整数除法单元的典型延迟为8个周期加上商每9位1个周期。除法器允许两个连续独立除法操作之间的有限重叠。‘典型'64位除法允许每8个周期一次除法的吞吐量(实际吞吐量是数据依赖的)。”
总结
当用高级语言(如Rust)编写时,恒定时间执行代码的主张变得薄弱。对于C和C++等语言也是如此。有太多我们无法控制的因素。
这是否意味着一切都失去了?是否每个未用特定目标汇编语言编写的密码实现都被破坏了?可能不是,但这些实现必须依赖技巧和对合理代码生成的希望。
几乎总是存在权衡,就像许多领域一样——大小与速度,上市时间与质量等。在用内存安全、现代语言和强大分析工具可用的情况下实现密码学有巨大收益。然而,手写的、特定目标的汇编语言可以对恒定时间属性做出更强的主张,缺点是可能引入内存安全问题。
为了能够对用Rust编写的代码做出这样的主张,需要编译器的强大支持,从前端一直到后端的目标机器代码生成。我们可能需要恒定时间成为编译器意识到的属性,以便它保留它。这是一项重大任务,有几个正在进行的讨论和建议来实现这一目标。
目前,我们必须依赖现有的东西。一个小的进步可能是合并never优化选项来帮助。
如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News
页面内容
让我们实现密码学! 重温恒定时间 恒定时间实现 对抗帮助编译器 构建Rust编译器 实现optnone属性 结果 总是有"但是” 总结 最近的帖子 非传统创新者奖学金 劫持你的PajaMAS中的多代理系统 我们构建了MCP一直需要的安全层 利用废弃硬件中的零日漏洞 Inside EthCC[8]:成为智能合约审计员 © 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。