タイミング攻撃にNode.jsのイベントループを利用する
2016年2月16日
0 分で読めます3年ほど前、ある 数人の 友人 と私は、pastenというグループを立ち上げ、Chaos Computer Clubの Capture The Flag(CTF)大会に参加しました。これはJeopardy形式のCTFで、参加チームはエクスプロイト、リバースエンジニアリング、Web、フォレンジック、暗号など、さまざまなカテゴリのセキュリティ関連の課題を解く必要があります。CTFは楽しく、学びにもなるので、ぜひ参加することをおすすめします。今年のように優勝できれば、さらに楽しいですよ!
今年の大会では、ほかの課題に加えて、Node.js固有のイベントループを利用した興味深いタイミング攻撃に対して脆弱なNode.jsアプリケーションが出題されました。この記事では、その課題を順を追って解説し、リスクを説明するとともに、攻撃者がこの脆弱性をどのように見つけて悪用するかを紹介します。皆さんが自分のコードで同様の脆弱性を避けるための参考になれば幸いです。
タイミング攻撃
課題に取り組む前に、タイミング攻撃とは何かを説明しましょう。
既存のメールアドレスに対して誤ったパスワードを入力すると、「パスワードの5文字目が間違っています。もう一度お試しください」と返答するサービスを想像してください。ばかげた話ですよね?このような情報を与えると、攻撃者は1文字ずつ総当たりでパスワードを試せるため、簡単に侵入されてしまいます。しかし、パスワードや認証トークンの検証に単純な文字列比較を使うと、まさに同じことが起こります。
JavaScriptの==演算子を含むネイティブの文字列比較は、通常、長さが等しい2つの文字列を1文字ずつ比較し、異なる文字が見つかった時点で処理を止めます。そのため、fooとbarを比較するとループは1回で終わりますが、fooとfoxを比べる場合は3文字を比較するため、処理により時間がかかります。
脆弱な認証関数の例:
1文字の比較は非常に高速ですが、測定できる程度の時間はかかります。調査によると、攻撃者はインターネット経由で15~100µs、ローカルネットワークでは100nsの精度でイベントを測定できます。こうした手法を使えば、わずかな遅延から、どの文字が間違っているかをユーザーに伝えているのと同じような情報を得られます。
この種の攻撃はタイミング攻撃と呼ばれ、入力によって処理時間が変わる場合に実行できます。これを防ぐには、入力されたパスワードにかかわらず、文字列比較に常に同じ時間がかかるようにする必要があります。たとえば、2つのパスワードをxoringし、その結果がゼロかどうかを確認する方法があります。
タイミング攻撃に対して脆弱ではない例:
課題:Sequence Hunt
この情報を踏まえて、課題を詳しく見ていきましょう。まず、次の内容が記載されたページへのリンクがありました。
Sequence Hunt整数列ハントへようこそ。以下のアルゴリズムを実装し、0から100までの5つの数値を計算してください。正しいと思う解が見つかったら、GETパラメータとしてこちらに送信してください。こちらからあなたに関する情報も確認できます。大量のアクセスを想定しているため、非同期IOLoopを使って構築しました。お楽しみください!
[編集]誰かがこちらを教えてくれました。各チェックにかなり時間がかかるため、この攻撃を防ぐ目的で、サーバーはリクエストを受け取ってから3秒が経過するまで待つようになりました。[/編集]
アルゴリズムTODO:アルゴリズムを公開
こちらというテキストは/checkを指しており、正しいパラメータを送信するとフラグを取得できます。また、IOLoopsは/infoへのリンクで、次の内容が表示されます。
あなたのIPアドレスは37.120.106.140あなたのIOLoopでのリクエスト数:1
ここまでで分かったことは何でしょうか?
0から100までの5つの数値を受け取る、未知のアルゴリズムがある
フラグを取得するには、正しい数値を指定する必要がある
アルゴリズムの実行時間は入力によって変わる可能性があり、タイミング攻撃を許すおそれがある
サーバーはタイミング攻撃を防ぐため、応答を返す前に必ず少なくとも3秒待機する
ここからフラグの取得方法を探ります。課題のタイトルはSequence Huntなので、例では仮のドメイン名としてsequencehunt.comを使います。ただし、この(無関係な)ドメインに攻撃を仕掛けないでください!私が作成した脆弱なサンプルアプリケーションを実行できます。CTFのものと同様の動作をするはずです。
調査
簡単に確認したところ、指定する値にかかわらず、checkリクエストの応答には約3秒かかることが分かりました。つまり、すべての組み合わせを総当たりで試すことはできません。100^5(100億)通りを試す必要があり、完了までに何千年もかかります……
/infoページを試してみると、/checkリクエストのたびにIOLoopのカウントが増え、/infoリクエスト自体でも増えることが分かりました。
ランダムな数値を指定して/checkリクエストを送っても、統計的に有意な時間差は見られませんでした。どうやらアルゴリズムの実行時間は3秒未満で、すべてのリクエストが指定された最短時間の3秒後に返ってくるようです。
ところが、/checkリクエストが処理中のときに/infoへリクエストを送ると、応答までにかなり時間がかかることに気づきました。これがタイミングに関する手がかりになります。次のステップを理解するため、Node.jsのイベントループについて少し説明します。
Node.jsのイベントループ
スケーラビリティを重視して設計されたNode.js(およびJavaScript全般)は、非同期のイベント駆動型フレームワークです。ファイルを開いたりネットワークに書き込んだりするなど、ブロックする可能性のある処理をコードが実行するときは、コールバック関数を登録し、該当する処理を開始してから終了します。処理(たとえばファイルを開く処理)が完了すると、イベントループがコールバックイベントを呼び出します。
イベント駆動型のモデルは、スレッドが「待機」状態にならないため、非常に高いスケーラビリティを実現できます。しかし、関数の完了に時間がかかると問題が生じます。Node.jsサーバーは(デフォルトでは)コアごとに1つのスレッドしか実行しないため、時間のかかる関数がコア全体を占有し、ほかの処理は順番が来るまでキューで待つことになります。ブラウザーで「スクリプトの実行に時間がかかりすぎています」というエラーメッセージが表示されるのは、このためです。サーバーでは、単にリクエストが停止したように見えます。
Node.jsのイベントループについて詳しく知るには、JSConfでPhilip Robertsが行った素晴らしい講演を見るか、こちらの記事を読んでみてください。
パズルを解く
今回、イベントループから必要なタイミング情報を得ることができました。/checkの処理中はイベントループに制御が戻らないため、/infoリクエストは応答待ちになります。一方、setTimeout関数はスケジュールされたイベントを登録するだけで制御を返すため、/infoリクエストはすぐに処理されます。この現象をローカルで確認したい場合は、シンプルなサーバーのコードをGitHubに公開しています。
タイミング情報(サイドチャネルとも呼ばれます)を得る方法が分かったので、あとは/checkリクエストを送信し、応答を待たずに/infoリクエストを送り、応答が返るまでの時間を測定するだけです。

検索プロセスを自動化し、ネットワークのジッターの影響を取り除くため、次に、入力値を受け取って以下を行うPythonスクリプトを作成しました。
n個のスレッドを起動するcheckリクエストを送信し、並行して送る/infoリクエストの応答にかかる時間を測定する時間の平均を計算し、結果を返す
n=5を使いましたが、十分でした。インターネット接続の品質やアルゴリズムのタイミング特性によっては、もっと大きな値が必要になるかもしれません。最初の実行結果を示します。値4に大きな差があることに注目してください。
タイミングの差が現れたのは、最初の位置に4を指定した場合だけだったため、次は残りのパラメータを順に調べました。2桁目は12と分かりました。
このように続け、正しいパラメータ[4, 12, 77, 98, 35]をすべて特定しました。
フラグを取得しました。
まとめ
タイミング攻撃は現実の脅威であり、Googleのような大企業から小規模な組織まで、あらゆる組織に影響します。実行時間のわずかな差でも、インターネット規模では比較的少ないサンプルから検出でき、攻撃者はその情報を使って総当たり攻撃を絞り込めます。特にNode.jsでは、イベントループがタイミング攻撃の別のシグナルとして利用される可能性があります。
こうした欠陥からアプリケーションを守るためのヒントをいくつか紹介します。
認証チェックを実装する際は、認証処理に常に一定の時間がかかるようにしてください。一定時間で比較するには、secure-compareやほかのパッケージを利用できます。
それらに既知の脆弱性がないことも必ず確認してください。
最後に、自分のスキルを試してこうした課題に挑戦したい方は、来年、ぜひCCCのCapture The Flagに参加してみてください!
CTFを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。