简单的字符串比较对定时攻击不安全

信息安全 密码学 编程 定时攻击
2021-08-11 13:57:22

正如我在关于如何在 PHP 中正确加密的评论中了解到的那样?,有人告诉我在 PHP 中使用如下字符串比较容易受到定时攻击。所以它不应该用于比较两个 MAC 或哈希(也是密码哈希)是否相等。

if ($hash1 === $hash2) {
   //mac verification is OK
   echo "hashs are equal"
} else {
  //something bad happenend
  echo "hashs verification failed!";
}

有人可以详细说明问题到底是什么,攻击的样子,并可能提供避免此特定问题的安全解决方案。应该如何正确完成?这是 PHP 的一个特殊问题,还是 Python、Java、C++、C 等其他语言有同样的问题?

3个回答

这里的问题是通用字符串比较函数一旦发现字符串之间的差异就会返回。如果第一个字节不同,它们仅在查看两个字符串的一个字节后返回。如果唯一的区别在于最后一个字节,它们会在返回之前处理整个字符串。这通常会加快速度,这通常是好的。但这也意味着能够判断比较字符串需要多长时间的人可以很好地猜测第一个差异在哪里。

在攻击场景中,攻击者完全控制$mac1(从攻击者制造的消息中获取),同时$mac2是攻击者消息的真正有效 MAC。$mac2必须对攻击者保密,否则他可以将其粘贴在他的消息上,从而伪造一个有效的消息。攻击者通过分析获得响应所需的时间,可能可以找出他的 MAC 和真实 MAC 之间的第一个区别在哪里。他可以尝试该字节的所有可能性,找到正确的字节,然后在知道第一个k字节是正确的情况下安全地处理下一个字节。最后,他只尝试了 256*len MAC(如果 len 是 MAC 的长度),而不是他应该尝试的 256^len。

我将添加一个包含不同语言的时间常数函数的列表:

PHP

讨论:https ://wiki.php.net/rfc/timing_attack

bool hash_equals ( string $known_string , string $user_string )

http://php.net/manual/en/function.hash-equals.php

Java 讨论:http ://codahale.com/a-lesson-in-timing-attacks/

public static boolean  MessageDigest.isEqual(byte[] digesta, byte[] digestb)

http://docs.oracle.com/javase/7/docs/api/java/security/MessageDigest.html#isEqual(byte[],%20byte[])

C/C++ 讨论:https ://cryptocoding.net/index.php/Coding_rules

int util_cmp_const(const void * a, const void *b, const size_t size) 
{
  const unsigned char *_a = (const unsigned char *) a;
  const unsigned char *_b = (const unsigned char *) b;
  unsigned char result = 0;
  size_t i;

  for (i = 0; i < size; i++) {
    result |= _a[i] ^ _b[i];
  }

  return result; /* returns 0 if equal, nonzero otherwise */
}

我在这里找到更多:http: //www.levigross.com/2014/02/07/constant-time-comparison-functions-in-python-haskell-clojure-java-etc/

蟒蛇(2.x):

#Taken from Django Source Code

def constant_time_compare(val1, val2):
    """
    Returns True if the two strings are equal, False otherwise.

    The time taken is independent of the number of characters that match.

    For the sake of simplicity, this function executes in constant time only
    when the two strings have the same length. It short-circuits when they
    have different lengths.
    """
    if len(val1) != len(val2):
        return False
    result = 0
    for x, y in zip(val1, val2):
        result |= ord(x) ^ ord(y)
    return result == 0

Python 3.x

#This is included within the stdlib in Py3k for an C alternative for Python 2.7.x see https://github.com/levigross/constant_time_compare/
from operator import _compare_digest as constant_time_compare

# Or you can use this function taken from Django Source Code

def constant_time_compare(val1, val2):
    """
    Returns True if the two strings are equal, False otherwise.

    The time taken is independent of the number of characters that match.

    For the sake of simplicity, this function executes in constant time only
    when the two strings have the same length. It short-circuits when they
    have different lengths.
    """
    if len(val1) != len(val2):
        return False
    result = 0
    for x, y in zip(val1, val2):
        result |= x ^ y
    return result == 0

哈斯克尔

import Data.Bits
import Data.Char
import Data.List
import Data.Function

-- Thank you Yan for this snippet 

constantTimeCompare a b =
  ((==) `on` length) a b && 0 == (foldl1 (.|.) joined)
  where
    joined = zipWith (xor `on` ord) a b

红宝石

def secure_compare(a, b)
     return false if a.empty? || b.empty? || a.bytesize != b.bytesize
     l = a.unpack "C#{a.bytesize}"

     res = 0
     b.each_byte { |byte| res |= byte ^ l.shift }
     res == 0
   end

Java(一般)

// Taken from http://codahale.com/a-lesson-in-timing-attacks/
public static boolean isEqual(byte[] a, byte[] b) {
    if (a.length != b.length) {
        return false;
    }

    int result = 0;
    for (int i = 0; i < a.length; i++) {
      result |= a[i] ^ b[i]
    }
    return result == 0;
}

针对字符串比较的定时攻击不是特定于 PHP 的。它们适用于使用标准“短路”比较算法检查用户提供的字符串与秘密字符串的任何上下文(检查在第一个不匹配的字节处停止)。这适用于 PHP、Python、C 甚至是 MySQL 等数据库系统。

解决此问题的标准方法是始终遍历字符串的所有字节,而不管内容如何。作为伪代码:

function safe_string_comp(str_1, str_2):
    if byte_length(str_1) =/= byte_length(str_2):
        return FALSE
    else:
        comparison_bit := 0  // 0 if the strings match, 1 otherwise
        for i := 0, i < byte_length(str_1), i := i + 1:
           comparison_bit := comparison_bit | (str_1[i] ^ str_2[i])

        return comparison_bit == 0

该符号|表示按位OR运算符,并且^是按位的XOR.

最近的 PHP 版本 (>= 5.6.0) 已经有一个名为hash_equals. 如果不可用,则需要实现上述算法。所以一个时间安全的字符串比较函数可能看起来像这样:

<?php

/**
 * Count the number of bytes in a string.
 *
 * Note that the strlen() function is ambiguous, because it will either return the number of *bytes* or the
 * number of *characters* with regard to mb_internal_encoding(), depending on whether the Mbstring extension
 * has overloaded the string functions:
 * http://php.net/manual/en/mbstring.overload.php
 *
 * For example, the non-overloaded strlen() function returns 2 for the string "\xC3\x84". However, if the
 * function is overloaded and the internal encoding set to UTF-8, the same string is interpreted as a single
 * character, namely the "Ä" umlaut. So the function returns 1 in this case.
 */
function byte_length($binary_string)
{
    if (extension_loaded('mbstring'))
        return mb_strlen($binary_string, '8bit');
    else
        return strlen($binary_string);
}



/**
 * Timing-safe string comparison.
 *
 * The standard string comparison algorithm stops as soon as it finds a non-matching byte. This leaks information
 * about the string contents through time differences, because the longer the common prefix, the longer the
 * comparison takes (e. g. checking "aaax" against "aaaa" theoretically requires slightly more time than checking
 * "xaaa" against "aaaa").

 * To avoid this problem in security contexts like MAC verification, iterate over *all* bytes of the strings
 * regardless of the content.
 */
function secure_string_equals($string_1, $string_2)
{
    // Use built-in hash_equals() function if available (PHP >= 5.6.0)
    if (function_exists('hash_equals'))
    {
        return hash_equals($string_1, $string_2);
    }
    else
    {
        $equals = false;

        if (!is_string($string_1) || !is_string($string_2))
        {
            trigger_error('One of the arguments is not a string.', E_USER_ERROR);
        }

        if (byte_length($string_1) == byte_length($string_2))
        {
            // 0 if the strings are equal, 1 otherwise
            $comparison_bit = 0;
            for ($byte_index = 0; $byte_index < byte_length($string_1); $byte_index++)
            {
                $comparison_bit |= ord($string_1[$byte_index]) ^ ord($string_2[$byte_index]);
            }

            $equals = ($comparison_bit == 0);
        }

        return $equals;
    }
}