时序安全字符串比较 - 避免长度泄漏

信息安全 保密 编程 定时攻击 侧信道
2021-08-19 14:49:04

假设我们正在构建一个通用的时序安全比较函数以供通用使用。众所周知,当两个字符串长度相等时,它是安全的。但是,我不确定的是,如果字符串长度不同(任意),我们如何使其安全。

一个字符串将被视为“已知”,而另一个字符串将被视为“未知”。我们假设攻击者只能控制“未知”值。理想情况下,此函数不应泄漏有关已知字符串长度的任何信息。

一个简单的实现,例如:

// returns 1 is same, 0 otherwise
int timing_safe_compare(char *known, size_t known_len, char *unknown, size_t unknown_len) {
    // Safe since all strings **will** be null terminated
    size_t mod_len = known_len + 1;
    size_t i;
    int result = 0;

    result = known_len - unknown_len;
    for (i = 0; i < unknown_len; i++) {
        result |= known[i % mod_len] ^ unknown[i];
    }

    return result == 0 ? 1 : 0;
}

但这里的问题是可能存在缓存信息泄漏。

例如,x64 中的字长为 64 位。所以我们可以在一个寄存器中容纳 8 个字符。如果已知值是一个不超过 7 个字符的字符串(因为我们将 known_len 加 1),则比较永远不需要对已知字符串进行另一次加载操作,即使未知字符串也会这样做。

换句话说,如果未知字符串的大小与已知字符串相差一个或多个单词边界,则正在完成的“工作”总量可能会发生变化。

我的第一直觉是只比较大小相等的字符串,但是长度信息会泄露(因为执行会遵循几个不同的分支)。

所以,这留下了两个问题:

  1. 这个微小的差异是否足以引起人们的关注?

  2. 可以在不泄露已知大小的信息的情况下防止这种差异吗?

4个回答

由于缓存的原因,能够处理任意长度的字符串而不泄露其长度信息似乎非常困难(即我不知道该怎么做)根据定义,很长的字符串会占用大量空间,因此读取字符串会导致与缓存的交互。从 RAM 访问字符串将触发缓存未命中,并且还会从缓存中逐出其他数据元素,从而影响应用程序代码的未来行为。缓存未命中要花费数十甚至数百个时钟周期:从外部看,它比分支错误预测至少要多十倍。如果您担心分支,那么您应该更多地担心缓存。

但是,我们可以用padding作弊假设您可以安排将要比较的两个字符串写入两个大小相等且全为零的大缓冲区的开头;此外,我们假设值 0 的字节不能出现在普通字符串中(例如,这些是 C 字符串)。然后,您只需对长度相同的两个缓冲区进行无泄漏比较。缓冲区长度会泄漏,但它是一个固定、恒定且众所周知的参数,所以这不是问题。

这并不能解决问题;它移动它。现在,您必须确保生成的字符串可以将它们写入缓冲区而不会泄漏大小信息。一般来说,你不再有strings您有给定固定长度的二进制值,您可以使用 big memcpy(); 这些值恰好有一个字符串解释,其中字节被认为是字符,直到值为零的第一个字节。


从更高的角度来看,拥有“安全字符串比较功能”就像在泰坦尼克号上带了一个水桶。如果您的代码正在处理秘密数据,那么您对数据所做的一切都可能受到时间攻击。通常,您的应用程序可以分为两种:

  • 如果唯一的秘密部分是单个加密元素,而其他所有内容都是公开的,那么使用一些无泄漏原语是有意义的,并且会提高整体安全性。一个典型的例子是证书颁发机构,其中唯一的秘密部分是 CA 私钥;只要签名算法不泄露机密,整个系统就可以抵抗定时攻击。同样,进行基于密码的身份验证但仅包含公共数据的网站也可以。

  • 如果机密性分布在整个系统中,例如进行基于密码的身份验证以访问某些机密数据的网站,那么专注于字符串比较就没有抓住重点。整个服务器代码必须做到无泄漏,这是一项相当困难的工作(我们真的不知道该怎么做)。

无论如何,当语言更“高级”时,试图保护任何给定的代码免受侧信道攻击变得更加困难。诸如 PHP 之类的语言,其自动内存管理(垃圾收集器)和字符串管理(字符串是与整数一样的)根本无济于事。这就是为什么必须提供用 C 实现的低级原语(例如无泄漏字符串比较函数)的原因,但问题要大得多,并且还包含大量 PHP 代码。

如果你假设一个对手可以通过缓存泄漏观察内存访问模式,那么试图防止对手学习秘密的长度是愚蠢的。他总会知道的。防止这种情况的唯一方法是保证您可以在没有段错误的情况下访问字符串的末尾 - 如果不过度分配编程语言中的每个字符串,您几乎肯定无法做到这一点。

您是否研究过想要此功能的 PHP 程序员的需求?

在我能想到的实际应用中——验证密码、会话令牌等。已知字符串会相对较小,比如 < 64 字节;在一个英特尔高速缓存行内。因此,您的琐碎实现实际上不会导致不同的缓存访问模式。

如果您确实需要比较长字符串而不泄漏长度,则应该考虑比较哈希值。

简单地 ...

...不要比较字符串,比较它们的哈希值

是的,我的意思是为了计时安全(密码安全的副作用,放在一边)。

这是做什么的

比较哈希时,您无需担心跟随(一开始可能并不明显)

  • 对于更长的字符串,哈希过程需要更多时间

    为什么?您正在比较用户输入的正确散列(显然)是固定长度的,用户可能获得的唯一信息是程序散列他的输入需要多长时间(最坏的情况,这可能会提供一些关于底层散列算法的提示,无论如何都不应该依赖哪个保密性)

    唯一的例外是如果您无法在某个地方存储正确的哈希值。必须首先计算它,也就是直接处理密码,再次带来完全相同的密码/长度问题。

  • 分支预测错误或缓存未命中

    显然,如上所述,无论任何给定用户的正确密码有多长,正确的哈希值总是相同的长度。

说真的,这个简单的过程使一个非常困难的问题变得微不足道。

关于哈希强度

如果您使用的是弱散列过程(或具有荒谬熵的过程),您可以考虑另外检查直接密码是否相等(在比较散列得到肯定结果之后),以保护自己免受冲突。

然而,当遇到碰撞时,这会泄漏计时信息/花费更长的时间。

底线:不要使用弱散列算法,应用一些盐,你应该没问题!;-)