タイミング攻撃はざっくばらんに言えば、処理にかかる時間を元に秘密となっている文字列(APIトークン、平文で保存されているパスワードなど)を推測する攻撃手法です。例えば 文字列長が異なるか、1 文字目から末尾まで順に比較して異なる文字が見つかるかした時点で処理を打ち切る比較アルゴリズムを考えます。この様な結果が確定した時点で処理を打ち切るのは比較処理の高速化に必須です。このアルゴリズムでは比較対象と一致する前方からの文字列の長さが長くなるにつれて処理時間が長くなります。
compare('bcdcljzacg', 'e'); // 文字列長が異なるため、文字列長の比較のみで処理が終わります。 compare('bcdcljzacg', 'eoivzoczjo');// 文字列長が同じのため、文字列長の比較と文字の比較で処理が終わります。 compare('bcdcljzacg', 'eoivzoczjo');// 1文字目の"b"と"e"が異なるため1文字分の比較で処理が終わります compare('bcdcljzacg', 'bcdcljzxxx');// 8文字目の"a"と"x"が異なるため8文字分の比較で処理が終わります
このため処理時間の前後を見ることで秘密の情報を速く特定できます。早く特定できるとはいえ何度も何度も試す必要があるため回数によるアクセス制限をかける防御は比較的有効です。一方でどのような文字列に対しても現実的な時間で特定が可能であるため、暗号論的に安全な疑似乱数を用いて作ったランダム値であってもあっさり破られます。
対策の方針はいたってシンプルで、比較対象によって処理時間が変わらない比較処理、にすることです。方針は方針であり、現実的にこれを完璧にすることは困難です(無限長の文字列同士の比較と0文字同士の比較の処理時間がどちらも同じ処理というのは現実的でないです)。例えばPHPでは次の様に実装されています。
php-src/hash.c at 96fe8141c397518e4ee10e65a7b921d779d332b6 · php/php-src
/* PHPソースコードより抜粋 */ 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]; }
文字列全体のビットを比較しています。こうすると n 文字目まで一致している、といった情報を得ることが出来なくなります。PHP でこのタイミング攻撃に安全な文字列比較を行うための関数は hash_equals で、次の様に使います。
PHP: hash_equals – Manual
<?php $secret = 'secret string'; $userInput = 'user input'; if(strcmp_safe($secret, $userInput)) { echo 'タイミング攻撃に安全に同じ文字列と識別できました'; } else { echo '異なる文字列です'; } /** * タイミング攻撃に安全な文字列比較例 * @param string $knownString * @param string $userInput * @return bool */ function strcmp_safe(string $knownString, string $userInput): bool { // 比較前に文字列長を一致させます。 // ここで長さを一致させるのはタイミング攻撃対策でしかなく、 // タイミング攻撃にセーフな文字列比較をした後に改めて文字列長比較を行います。 $userInputTrim = substr(str_pad($userInput, strlen($knownString)), 0, strlen($knownString)); // 比較対象の内、既知のもの(データベースからとってきた値や設定ファイルにある値)を第一引数に、 // POSTやGETなどから得たユーザ入力を第二引数にとります。 // hash_equals を用いることで何文字目で異なっていたとしてもタイミング攻撃に安全です $hashEqualsResult = hash_equals($knownString, $userInputTrim); // 既知文字数分の比較が終わった後、必ず文字列長の比較を行います。 // 必ず行うことでタイミング攻撃に安全です。 // 行うことで文字列が完全一致であるかないかを確定させます。 $sameLength = strlen($knownString) === strlen($userInput); // 前方から既知文字列と同じ文字列を持ち、文字列長が既知文字列と同一ならば true // とどのつまり、同じ文字列ならば true return $hashEqualsResult && $sameLength; }