随机睡眠可以防止定时攻击吗?

Could a random sleep prevent timing attacks?

来自Wikipedia

In cryptography, a timing attack is a side channel attack in which the attacker attempts to compromise a cryptosystem by analyzing the time taken to execute cryptographic algorithms.

实际上,为了防止定时攻击,我使用了以下取自 this answer 的函数:

function timingSafeCompare($safe, $user) {
    // Prevent issues if string length is 0
    $safe .= chr(0);
    $user .= chr(0);

    $safeLen = strlen($safe);
    $userLen = strlen($user);

    // Set the result to the difference between the lengths
    $result = $safeLen - $userLen;

    // Note that we ALWAYS iterate over the user-supplied length
    // This is to prevent leaking length information
    for ($i = 0; $i < $userLen; $i++) {
        // Using % here is a trick to prevent notices
        // It's safe, since if the lengths are different
        // $result is already non-0
        $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
    }

    // They are only identical strings if $result is exactly 0...
    return $result === 0;
}

但我在想是否有可能使用像

这样的随机睡眠来防止这种攻击
function timingSafeCompare($a,$b) {
    sleep(rand(0,100));
    if ($a === $b) {
        return true;
    } else {
        return false;
    }
}

或者增加睡眠的随机性

sleep(rand(1,10)+rand(1,10)+rand(1,10)+rand(1,10));

这种方法完全可以防止定时攻击?或者只是让工作更难?

这对于单个请求没问题,如果攻击者唯一可观察到的边信道是响应时间的话。

但是,如果攻击者发出足够多的请求,这个随机延迟可能会平均,如 citing ircmaxell's blog post 中所述:

So if we needed to run 49,000 tests to get an accuracy of 15ns [without a random delay], then we would need perhaps 100,000 or 1,000,000 tests for the same accuracy with a random delay. Or perhaps 100,000,000. But the data is still there.

举个例子,让我们估计一次定时攻击需要多少请求才能获得有效的 160 位会话 ID,例如 PHP at 6 bits per character which gives a length of 27 characters. Assume, like the linked answer 一次攻击只能对一个用户进行一次(因为他们正在存储用户以在 cookie 中查找)。

从博客 post 中取最好的情况,100,000,排列数为 100,000 * 2^6 * 27.

平均而言,攻击者会在排列数的一半处找到值。

这使得从定时攻击中发现会话 ID 所需的请求数为 86,400,000。这与没有您建议的时序保护的 42,336,000 个请求相比(假设像博客 post 那样的 15ns 精度)。

在博客post中,取最长的测试长度,14,平均耗时0.01171秒,这意味着86,400,000将耗时1,011,744秒,相当于11天17小时2分24秒。

Could a random sleep prevent timing attacks?

这取决于使用随机休眠的上下文,以及它所保护的字符串的位强度。如果它是针对链接问题中上下文的 "keep me logged in" 功能,那么攻击者花 11 天时间使用计时攻击来暴力破解一个值可能是值得的。但是,这是假设完美的条件(即您的应用程序对测试的每个字符串位置的响应时间相当一致,并且没有重置或翻转 ID)。此外,来自攻击者的此类 activity 会产生大量噪音,并且很可能会通过 IDS 和 IPS 被发现。

它不能完全阻止它们,但可以使攻击者更难执行它们。使用 hash-equals 之类的东西会更容易也更好,它可以在假设字符串长度相等的情况下完全防止计时攻击。

您建议的代码

function timingSafeCompare($a,$b) {
    sleep(rand(0,100));
    if ($a === $b) {
        return true;
    } else {
        return false;
    }
}

请注意 PHP rand 函数 不是 加密安全的:

Caution This function does not generate cryptographically secure values, and should not be used for cryptographic purposes. If you need a cryptographically secure value, consider using openssl_random_pseudo_bytes() instead.

这意味着理论上攻击者可以预测 rand 将要生成什么,然后使用此信息来确定您的应用程序的响应时间延迟是否是由于随机睡眠造成的。

接近安全的最佳方法是假设攻击者知道您的源代码 - 攻击者唯一的秘密应该是密钥和密码之类的东西 - 假设他们知道所使用的算法和功能。如果你仍然可以说你的系统是安全的,即使攻击者确切地知道它是如何工作的,那么你就成功了。 rand 之类的函数通常设置为以当前时间作为种子,因此攻击者只需确保他们的系统时钟设置为与您的服务器相同,然后发出请求以验证他们的生成器是否与您的匹配。

因此,最好避免使用 rand 等不安全的随机函数,并将您的实现更改为使用 openssl_random_pseudo_bytes,这将是不可预测的。

此外,根据 ircmaxell 的评论,sleep 不够精细,因为它只接受一个整数来表示秒数。如果您打算尝试这种方法,请查看具有随机纳秒数的 time_nanosleep

这些指示应该有助于确保您的实施免受此类定时攻击。

Anthony Ferrara 在他的博客 post、It's All About Time 中回答了这个问题。我强烈推荐这篇文章。

Many people, when they hear about timing attacks, think "Well, I'll just add a random delay! That'll work!". And it doesn't.

This kind of approach can totally prevent timing attacks? Or just make the work harder?

都没有。它不会阻止计时攻击,也不会使它们变得更加困难。

要了解原因,请查看 docs for sleep。具体第一个参数的含义:

Halt time in seconds.

因此您的应用需要 0.3 秒才能在不休眠的情况下做出响应。睡眠需要 0.3、1.3、2.3 等...

所以真的,为了得到我们关心的部分(时间差异),我们只需要砍掉整数部分:

$real_time = $time - floor($time);

但让我们更进一步。假设您使用 usleep 随机入睡。这要细化得多。那就是在微秒内休眠。

好吧,测量是在 15-50 纳米秒的范围内进行的。因此,睡眠仍然比正在进行的测量 粒度大约 100 倍。所以我们可以平均到单个微秒:

$microseconds = $time * 1000000;
$real_microseconds = $microseconds - floor($microseconds);

而且还有有意义的数据。

您可以更进一步并使用 time_nanosleep,它可以休眠到纳秒级精度。

然后你就可以开始胡编乱造了。

但是数据还在。随机性的美妙之处在于你可以将它取平均:

$x = 15 + rand(1, 10000);

运行 足够多的时间,你会得到一个漂亮的图表。你会发现大约有 10000 个不同的数字,因此你可以平均掉随机性并推断出 "private" 15.

因为良好的随机性是无偏的,所以很容易在足够大的样本上进行统计检测。

所以我想问的问题是:

当您可以正确解决问题时,为什么还要费心进行类似睡眠的黑客攻击?

This kind of approach can totally prevent timing attacks? Or just make the work harder?

ircmaxell 已经回答了为什么 这只会让工作变得更难,

但是在 PHP 中防止定时攻击的解决方案通常是

/**
 * execute callback function in constant-time,
 * or throw an exception if callback was too slow
 *
 * @param callable $cb
 * @param float $target_time_seconds
 * @throws \LogicException if the callback was too slow
 * @return whatever $cb returns.
 */
function execute_in_constant_time(callable $cb, float $target_time_seconds = 0.01)
{
    $start_time = microtime(true);
    $ret = ($cb)();
    $success = time_sleep_until($start_time + $target_time_seconds);
    if ($success) {
        return $ret;
    }
    // dammit!
    $time_used = microtime(true) - $start_time;
    throw new \LogicException("callback function was too slow! time expired! target_time_seconds: {$target_time_seconds} actual time used: {$time_used}");
}


使用这种方法,您的代码可以是

function timingSafeCompare($a,$b, float $target_time_seconds = 0.01) {
    return execute_in_constant_time(fn() => $a === $b, $target_time_seconds);
}

缺点是你应该选择一个有很大余量的数字,这意味着相对较多的时间会浪费在睡眠上。首先在我的笔记本电脑上我不得不使用 0.2(200 毫秒)来比较 2x exactly-1-GiB 字符串, Core i7-8565U(我从未听说过的奇怪的 2018 年中档笔记本电脑 cpu)

和这个循环:

ini_set("memory_limit", "-1");
$s1 = "a";
$s2 = "a";
$append = str_repeat("a",100*1024);
try {
    for (;;) {
        $res = timingSafeCompare($s1, $s2, 0.01);
        $s1 .= $append;
        $s2 .= $append;
    }
} catch (\Throwable $e) {
    var_dump(strlen($s1));
}

大约 65 岁时就开始大便 megabytes/int(65126401)

(但是您需要多久对 65MB 以上的字符串进行恒定时间比较?我想这不经常)

  • 你可能会想“然后攻击者可以发送一个巨大的字符串进行比较,并检查抛出异常需要多长时间”但我认为这行不通,=== 首先检查两个字符串是否具有相同的长度,如果它们具有不同的长度则短路,这种攻击只有在攻击者可以设置 both 字符串的长度时才有效大到足以超时

  • 今天我们有原生 hash_equals() 函数来比较具有完全相同长度 的字符串 ,但是 hash_equals( ) 不会保护您免受不同长度的字符串的影响,而上面的函数会。