比较 MySQL 中的字符串容易受到定时攻击


Is comparing strings in MySQL vulnerable to timing attacks?

我正在为用户密码部署经典的哈希保护。登录时提交的密码经过加盐、散列,然后与数据库中已存储的散列进行比较。

但是,不是使用 PHP 函数调用来比较现在散列的用户输入和存储的散列,而是在数据库中完成比较 - 更准确地说,使用 WHERE 子句(注意:在比较开始时,由于各种原因已经知道盐,但密码不是)。

由于用户名是唯一的,因此以下查询可以有效地告知用户名 + 密码对是否匹配:

SELECT * FROM `users` WHERE `password`='$password_hash' AND `username`='$username';

这种方法是否容易受到定时攻击?


编辑:SQL注入不是问题,它得到了照顾。

是的,字符串比较(和/或索引查找)原则上可能会泄漏数据库中存储的密码哈希和从输入的密码共享计算出的相同前导字节数。

原则上,攻击者可以使用它来逐字节迭代学习密码哈希的前缀:首先,他们找到一个与数据库中的哈希共享其第一个字节的哈希,然后找到一个共享其前两个字节的哈希,依此类推。

不,这几乎肯定无关紧要。

为什么? 嗯,原因有很多:

  1. 定时攻击可能允许攻击者了解用户密码哈希的一部分。 然而,设计良好的密码散列方案(使用盐和密钥拉伸)应该保持安全(当然,假设密码本身不容易猜到),即使攻击者知道整个密码哈希。 因此,即使定时攻击成功,密码本身也是安全的。

  2. 要进行攻击,攻击者必须提交他们知道其哈希值的密码。 哈希值取决于盐。 因此,除非攻击者以某种方式已经知道盐,否则这种攻击是不可能的。

    (的确,在密码散列方案的大多数安全分析中,盐被认为是公共信息。 但是,这只是因为此类分析假设了上述最坏情况,即攻击者已经获得了整个用户数据库,盐和哈希的副本以及所有内容。 如果攻击者还不知道哈希值,则没有理由假设他们会知道盐。

  3. 即使攻击者知道盐,为了执行上述迭代攻击,他们也需要生成哈希为具有所需前缀的值的密码。 对于任何安全哈希函数,唯一可行的方法是通过试验错误,这意味着这样做所需的时间与前缀的长度呈指数级增长。

    这在实践中意味着,为了提取足够多的哈希位,以便能够对其进行离线暴力攻击(不一定是全部;只是超过密码中的有效熵量),攻击者需要执行与破解密码本身所需的计算一样多的计算。 对于设计良好的密码哈希方案和安全选择的密码,这是不可行的。

  4. 原则上,迭代攻击可以给攻击者带来的能力是能够在其末端本地执行大部分暴力计算,同时只向系统提交相当少量的密码。 但是,即使这样也只有在他们从提交的每个密码中收到详细可靠的计时信息时才成立。 在实践中,实时定时攻击效率极低,需要许多(通常是数千或数百万)查询才能产生任何有用的信息。 这很可能会抵消定时攻击可能为攻击者提供的任何潜在性能优势。

    这一点被放大了,

    您使用适当的密钥拉伸密码哈希方案,因为此类方案被故意设计为缓慢。 因此,与首先对密码进行哈希处理相比,数据库中的字符串比较可能需要的时间可以忽略不计,因此由它引起的任何时间变化都将在噪音中丢失。

如果您在users表中(username, password)上放置复合索引并将查询从 SELECT * 更改为 SELECT COUNT(*) AS matching_user_count ,您将在进行匹配的查询和花费大致相同的时间的查询方面大有帮助。如果您的哈希长度都相同,那也会有所帮助。

如果所有这些查询花费相同的时间,这将使定时攻击变得更加困难。显然,您可以通过在每个查询上休眠伪随机时间来做更多的事情来击败定时攻击。试试这个:

SELECT COUNT(*) AS matching_user_count, SLEEP(RAND()*0.20) AS junk
  FROM `users` 
 WHERE `password`='$password_hash'
   AND `username`='$username'

它将为每个查询添加 0 到 0.2 秒之间的随机时间。这种随机性将主导执行索引WHERE子句的近乎恒定的时间。

一般

情况下防止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}");
}

有了这个你可以做

$rows = execute_in_constant_time(function () use ($db, $password_hash, $username) {
    return $db->query("SELECT * FROM `users` WHERE `password`='$password_hash' AND `username`='$username';")->fetchAll();
}, 0.01);
  • 从那里你可以调整 0.01 10 毫秒,或 0.1 调整 100 毫秒,依此类推,无论有多少字节正确或不正确,函数都会使用那么长的时间......除非你选择了一个太短的时间来执行,在这种情况下,你会得到一个 LogicException...