文字列(ハッシュ)の安全な比較方法 – hash_equals

(Last Updated On: 2018年11月2日)

映画などでPINコードを一桁づつ解析してドアを開錠する、といったシーンがあると思います。こんなのは”映画の世界だけ”と思っている方も多いと思います。しかし、タイミング攻撃を利用すると”実際にこれと全く同じ方法”で鍵となる情報を解析できます。

タイミング攻撃とはサイドチャネル攻撃の一種で、鍵情報を比較的簡単に解析する方法です。PHP 5.6からはタイミング攻撃に脆弱でない比較方法を提供しています。

結論から書くと、秘密の文字列を比較する場合

if ($secret_str !== $user_input) {
  // ユーザー提供の秘密情報不一致
  die('不正なアクセスです');
}

といった通常の文字列は比較は危険です。

if (!hash_equals($secret_st, $user_input)) {
  die('不正なアクセスです');
}

上記のようにhash_equals関数を利用しなければなりません。

文字列比較タイミング攻撃の原理

文字列データを比較する場合、通常はstrncmp/memcmpなどのライブラリ関数が利用されます。これらの比較関数は以下のように動作します。1

  • 1バイト毎に比較を行い、違いがあった場合、即座に結果を返す

つまり、秘密鍵が

abcdef

の場合で

XXXXXXX

aXXXXXXX

を比較した場合、前者の方が一文字分の比較が少なくなります。

少しだけですが秘密鍵の文字と攻撃者が送信した文字が一致する場合としない場合で実行時間の差が生まれます

こんな微妙な実行時間差で秘密情報が解析できるのか?と思うかも知れませんが、統計的な処理を行うと比較的簡単に鍵情報を解析できることが実証されています。PHPアプリケーションでも、アプリケーションレベルで対策が取られていました。(WordPress, Joomlaなど)

文字列比較タイミング攻撃対策

タイミング攻撃の原理は「文字列に違いがあった場合に、処理が早く終了する」ことにあります。対策は比較的簡単です。

  • 文字列に違いがあった場合でも、無かった場合でも、同じ時間で処理する

これを行うだけです。

PHPの場合、hash_equals関数を利用するだけです。この関数の中では、文字列全体が比較されるよう、以下のように記述(C言語)されています。

    /* We only allow comparing string to prevent unexpected results. */
    if (Z_TYPE_P(known_zval) != IS_STRING) {
        php_error_docref(NULL, E_WARNING, "Expected known_string to be a string, %s given", zend_zval_type_name(known_zval));
        RETURN_FALSE;
    }

    if (Z_TYPE_P(user_zval) != IS_STRING) {
        php_error_docref(NULL, E_WARNING, "Expected user_string to be a string, %s given", zend_zval_type_name(user_zval));
        RETURN_FALSE;
    }

    if (Z_STRLEN_P(known_zval) != Z_STRLEN_P(user_zval)) {
        RETURN_FALSE;
    }

    known_str = Z_STRVAL_P(known_zval);
    user_str = Z_STRVAL_P(user_zval);

    /* This is security sensitive code. Do not optimize this for speed. */
    for (j = 0; j < Z_STRLEN_P(known_zval); j++) {
        result |= known_str[j] ^ user_str[j];
    }

このコードから以下のことが解ります。

  • 文字列比較による解析攻撃は防止される
  • 秘密鍵の長さ情報の解析は防止していない

この事から

  • 鍵となるハッシュ値の長さを変えてもセキュリティ対策として意味はあまりない

ことが解ります。特別な理由がない限り、ハッシュ値の値は、短くしたり、長くしたりせず、そのまま使うことをお勧めします。2

まとめ

安全な文字列比較関数であるhash_equals()には”hash”と名前が付いていますが、秘密の文字列を比較する場合は必ずhash_equeals()を利用しなければなりません

例えば、何らかの理由でユーザーが入力した平文パスワードを比較しなければならない、といった場合にもhash_equals()を利用しないとパスワードを解析されるリスクがあります。秘密の文字列比較には、比較対象がハッシュ値であるかどうかに関わらず、hash_equals関数を利用してください。

HMACハッシュの使い方のまとめ


    1. SSEなど大きなレジスタを持っているハードウェアの場合、この限りではありません。参考 
  1. ビット数の多いハッシュを使わなくて良いという意味ではなく、利用しているハッシュ関数のデフォルトサイズを変更する必要はない、という意味です。ハッシュ関数は可能な限り強力な物を利用することが好ましいです。 

投稿者: yohgaki