PHP 5.6からタイミング攻撃に対する対策が導入されます。メジャーなアプリケーションはタイミング攻撃対策が導入されていますが、PHP 5.6から簡単に対策できるようになります。
タイミングセーフな文字列比較関数はhash_equalsとして実装されました。
http://php.net/manual/es/function.hash-equals.php
タイミング攻撃とは
タイミング攻撃とは、コンピュータが動作する時間の違いを測って攻撃する、サイドチャネル攻撃(副作用攻撃)と呼ばれる攻撃手法の1つです。HTTPSの圧縮の副作用を利用したサイドチャネル攻撃が有名です。
コンピュータの動作時間、温度、音、電子ノイズ、電力使用量など、アルゴリズム自体の脆弱性を攻撃するのではなく副産物を利用する攻撃方法でサイドチャネル攻撃の一種です。例えば、ブランドSQLインジェクションではタイミング攻撃が非常に有用であることが昔から知られています。
文字列比較のタイミング攻撃
以下がタイミング攻撃に脆弱なコードです。
<?php if ($secret_password === $_POST['password']) { $authenticated = TRUE; }
このコードは平文の秘密のパスワード($secret_password)と ユーザーが送信したパスワード($_POST[‘password’])が一致しているか確認し、一致している場合は認証フラグ($authenticated)をTRUEに設定しています。
一見なんの変哲もないコードですが、このコードがタイミング攻撃に脆弱です。なぜ脆弱になるのかはPHP内部でどのような動作になっているのか、知る必要があります。PHPは
文字列 === 文字列
または
文字列 == 文字列
と比較されると、文字列の長さが違う場合は即座にFALSEを返します。長さが異なる場合は標準Cライブラリのstrncmpを利用して文字列を比較します。
例えば、平文パスワードの長さは簡単に分かります。長さが異なる場合、strncmpは呼ばれないので長さが一致した場合と一致しない場合では微妙にレスポンス時間が異なります。レスポンス時間の違いを統計的に処理すると、パスワードの長さが分かってしまいます。
平文パスワードの長さが分ったら、今度はstrncmpの動作時間の違いを計測します。strncmpは比較する文字列が異なると、即座に結果を返します。つまり、最初の一文字が一致する場合と一致しない場合では微妙に処理時間が異なります。これを繰り返せば、一文字ずつパスワードを解析でき簡単にパスワードが分ってしまいます。パスワードがランダム文字列であっても推測の難易度はそれほど変わりません。
解りやすいようにパスワードには数字のみが利用されているとします。パスワードの長さが6である場合、大雑把にいって(普通は長さ制限や禁止パターンがあり、その分は少なくなります)
10^6 = 1,000,000
の組み合わせがあります。タイミング攻撃を利用しない場合、この組み合わせの中から一致するパスワードを推測しなければなりません。タイミング攻撃を利用した場合、一文字ずつ解析できるので
10*6 = 60
で解析できます。100万対60であり圧倒的に簡単に解析できることが分かります。
とは言ってもタイミング攻撃を行う場合、処理時間の違いを統計的に処理して一文字ずつ解析するので60アクセスではなく、実際には複数回のアクセスが必要です。仮に一文字の解析に1000回のアクセスが必要だとすると、それでも6万回のアクセスしか必要ありません。よく映画で機械を使ってパスワードやパスコードを一文字ずつ解析するシーンがありますが、それと全く同じ要領です。
タイミング攻撃が脅威となる理由
ランダムな値のハッシュ値を推測する事はほぼ不可能と考えられますが、タイミング攻撃を利用すれば平文パスワードの解析と同様に比較的簡単に解析できます。ランダムな値から生成されたハッシュ値をAPIキーなどに利用していても、推測は不可能でもタイミング攻撃を利用すれば解析可能になります。
このような微妙な実行時間の違いを利用して、実際にWebアプリに攻撃が可能なのか?と疑問に思うかも知れませんが、既に可能であると実証されています。ネットワークの遅延などはランダムに発生しますが、完全にランダムに発生する遅延は統計処理を行うと除外できます。つまり除外すべきランダムな要素が少ないほど解析が簡単になります。最近ではデータセンターにサーバーを置くことが当たり前になっていますが、ネットワーク遅延の要素はタイミング攻撃を行うコンピュータを同じデータセンターに置けばランダム性が少なくなり、解析が容易になります。
ログインパスワードなどであれば、閾値を用いてアクセス回数(比較回数)を制限する事も可能ですが、APIキーなどのように大量のアクセスを前提としているものでは、アクセス回数を小さな値に限定する事は現実的ではありません。仮に一度に解析できないよう一日あたりの最大クエリ数を限定しても、長い時間をかければ解析できる可能性があります。
言語やOSでの対策
例えば、Python 3.4からは文字列の比較にハッシュ値を利用して比較していると聞いています。OSもライブラリを提供しています。例えば、OpenBSDのtimingsafe_bcmpなどがあります。
int timingsafe_bcmp(const void *b1, const void *b2, size_t n) { const unsigned char *p1 = b1, *p2 = b2; int ret = 0; for (; n > 0; n--) ret |= *p1++ ^ *p2++; return (ret != 0); }
C言語を知らなくても何を行っているのか分かると思います。固定長のデータに対して一バイトずつ全てのバイトを比較しています。入力データに関わらず、常に同じ処理を行う事で「一定の時間」で処理することでタイミング攻撃を無効化できます。
Python 3.4ではSipHashという64ビットのハッシュを生成するアルゴリズムを利用しハッシュを生成し、64ビット整数として比較しているようです。ハッシュ関数を利用し、その結果を整数として比較すればタイミング攻撃を無効化できます。(Pythonの実装は聞いた話なので間違っていたら教えて下さい)
PHPでの対策
PHP 5.6からタイミング攻撃に対する対策が導入されることは決まっていますが、どのような実装にするのかは決まっていません。timingsafe_bcmpのような関数を作ることになりそうですが、PHPの比較演算子(==、===)で対応される可能性もないわけではありません。
タイミング攻撃に対する対策にはハッシュを使う、全てのバイトを比較するなどの方法があります。色々あるので作ってみました。
https://github.com/yohgaki/php-src/compare/PHP-5.6-rfc-hash-compare
以下の関数が追加されています。
- bool str_siphash_compare(string $str, string $str) – SipHash(64bit)を利用
- bool str_xxhash32_compare(string $str, string $str) – xxHash32(32bit)を利用
- bool str_md5_compare(string $str, string $str) – MD5(128bit)を利用
- bool str_byte_compare(string $str, string $str) – バイト単位で比較(固定長)
- bool str_byte_compare2(string $str, string $str) – バイト単位で比較
- bool str_word_compare(string $str, string $str) – 可能な限りワード単位で比較
- bool str_compare(string $str, string $str) – strncmpで比較(非タイミングセーフ)
ベンチマークスクリプトはここにあります。パフォーマンスは入力データの大きさによって異なりますが、性能的にはstr_word_compare()が最も良いようです。
[yohgaki@dev github-php-src]$ ./php-bin t.php str_siphash_compare Elapsed: 9.450211 Iterations: 1000000 DataSize: 1024 str_xxhash32_compare Elapsed: 3.849933 Iterations: 1000000 DataSize: 1024 str_md5_compare Elapsed: 27.174614 Iterations: 1000000 DataSize: 1024 str_byte_compare Elapsed: 5.874092 Iterations: 1000000 DataSize: 1024 str_byte_compare2 Elapsed: 8.761152 Iterations: 1000000 DataSize: 1024 str_word_compare Elapsed: 1.867914 Iterations: 1000000 DataSize: 1024 str_compare Elapsed: 1.178738 Iterations: 1000000 DataSize: 1024
PHPスクリプトでの対策
自分で対策する事も可能ですが、フレームワークが対応している場合もあります。
PHPのhash_password関数を利用している場合、タイミング攻撃に注意する必要はありません。パスワードは普通、ハッシュ化されたパスワードを比較するのでタイミング攻撃は行えません。APIキーなどを文字列として保存し、文字列として比較している場合には注意が必要です。上記のフレームワークが利用しているタイミングセーフな文字列比較を行うか、直接比較せずハッシュ値を比較するようにして下さい。
APIキーなどの鍵となるハッシュ値が特定されやすい条件は、以下の通りです。
- 鍵が特定のユーザーIDに紐付けて保存されている
- 鍵をメモリなどにキャッシュしている
- 鍵を一文字ずつ比較している
このような場合、文字列比較の微妙な遅延時間を解析され攻略される可能性が高くなります。一文字ずつの文字列比較を排除しなければ攻略される可能性は残るので、文字列比較時間を一定にする事が重要です。既に書いた、ハッシュ関数を利用してユーザーが送信した文字列を一文字ずつの比較を行わない方法でも安全です。
「鍵が特定のユーザーIDに紐付けて保存されている」とはユーザーIDと鍵が一緒に送信されるような場合を言っています。例えば、以下のようなリクエストです。
http://example.com/get_data/?userid=1234&apikey=1234567890
タイミング攻撃に脆弱な比較の場合、攻撃者はuserid=1234のapikeyがどのような文字列なのかタイミング攻撃を使って解析可能になります。ユーザーIDは盗んだり推測することが比較的容易な場合が多いでしょう。長いapikeyのみで認証している場合、特定のユーザーの鍵を盗む事は困難になります。しかし、どのユーザーでも構わないのでapikeyを盗む場合には同様にタイミング攻撃で盗まれる可能性があります。
タイミングセーフな比較であれば、鍵の文字列の長さの暴露についてはあまり気にしなくても構いません。 文字列の長さを完全に隠蔽することはほぼ不可能です。鍵の長さが分っても安全である鍵(つまり十分長い鍵)を利用すれば鍵の長さを知られることによるリスクは緩和できます。
まとめ
タイミング攻撃は言語やOSなどを問わず行える攻撃方法です。PHP以外の言語でも同じです。セキュリティが重要なシステムではタイミング攻撃のリスクを無視するべきではありません。普通は保護されている環境に置くアプリケーションだから大丈夫と安心するのは良くありません。保護された環境であってもSSRFで攻撃される可能性は十分にあります。保護されたシステムの場合、一般に公開されているシステムに比べランダム性が少なく、タイミング攻撃が容易である可能性が高くなることが多いと考えられます。
最後に、ランダム性を高くする事で対策する事はお薦めできません。ランダム性を高めても緩和策として機能しません。完全にランダムだと統計処理で解析できます。擬似的ランダム性なら緩和策としてある程度機能します。根本的な対策だけがセキュリティ対策ではありませんが、一定の比較時間にするか一文字ずつの比較を行わない方法の方が格段に優れ、かつ現実的対策です。