こんにちは。セキュリティ技術グループのはるぷと申します。
OpenSSLの脆弱性(CVE-2014-0160, JVNU#94401838)が2014年4月7日に公開され、OpenSSLが普及していることや、比較的早く実証コードが流通してしまった背景もあり、巷を賑わせました。
今回は、このOpenSSLの脆弱性をより深く解説したいと思います。
1. OpenSSLの脆弱性(CVE-2014-0160)
このOpenSSLの脆弱性はCodenomiconのDefensicsというセキュリティテストツールの改善中に発見された脆弱性であり、CodenomiconとGoogleのメンバによって公開されました。OpenSSLのハートビート機能に見つかった脆弱性なので、Heartbleedの脆弱性というユニークな名前が付けられたようです。
さて、このHeartbleedの脆弱性ですが、攻撃を受けると、OpenSSLライブラリを利用しているプロセスのメモリの内容が漏洩してしまいます。具体的に漏洩するものは、OpenSSLライブラリを利用しているプログラムにもよるのですが、Apacheであれば、ユーザからのリクエストデータやレスポンスデータ、SSL証明書の秘密鍵などになります。つまり、プログラムが扱うデータ(より正確に言えば、ヒープに格納されるデータ)は、全て漏洩するリスクがあります。例えばゲームであれば、セッション情報やID/パスワードなどの認証情報が漏洩してなりすましが発生し、他のユーザにゲームデータを自由に利用されてしまうといった被害が考えられます。
では、何故Heartbleedの脆弱性によって、データが漏洩してしまうのでしょうか?
今回は、それを技術的な視点で解説するとともに、実際に漏洩する様子を紹介したいと思います。そして、OpenSSLライブラリをアップデートしていない方が危機感を感じ、情報漏洩事故が起きる前に対策を進めていただくことがこの文書の目的です。
2. そもそもSSLハートビートとは?
SSLハートビートは、文字通りSSLの死活監視をするために導入された機能で、最初にOpenSSLに登場したのは、2011年12月です。その後、OpenSSLのVersion 1.0.1として2013年3月に正式にリリースされました。SSLハートビートが利用できるようになったのは、OpenSSLの歴史から見れば、比較的最近ということになります。
このことは、今回の攻撃を受ける対象となるOpenSSLのバージョンが1.0.1から1.0.1f(および1.0.2-beta)と比較的最近のものになっていることからもわかります。
SSLハートビートは、とても単純な機能で、例えばSSLクライアントがデータとしてhelloが入ったハートビートリクエストを投げると、SSLサーバがハートビートレスポンスとしてhelloをエコーバックするというものです。
3. Heartbleedの脆弱性の概要
とても単純な死活監視機能であるSSLハートビートですが、コードの何処に問題があったのでしょうか?
早速、Heartbleedの脆弱性を引き起こしたコードを見ていきましょう・・・、と言いたいところですが、理解の手助けをするために、まずは簡単な図で脆弱性の概要を説明します。
下記の図はSSLハートビートの仕組みを簡易的に表した図です。
正常パターンでは、ハートビートリクエストに”Hello”という文字列とともに、その文字列の長さである5バイトが指定されており、OpenSSLは文字列を正常な長さだけコピーしてレスポンスデータを生成することができています。
一方で情報漏洩パターンでは、ハートビートリクエストに”Hello”という5バイトの文字列が格納されているのにもかかわらず、長さとして、それよりも大きい10バイトが指定されています。Heartbleedの脆弱性を持つOpenSSLは、レスポンスデータを生成する際、正直に10バイトの長さの文字列をリクエストデータからコピーしてしまいます。その結果、情報漏洩が起きてしまうという訳です。
4. Heartbleedの脆弱性を引き起こしたコード
それでは、実際にHeartbleedの脆弱性を引き起こしたコードを見ていきましょう。
下記のコードの断片が、クライアントからのハートビートリクエストメッセージを読み取る部分です。
unsigned char *p = &s->s3->rrec.data[0], *pl; ……(1) unsigned short hbtype; unsigned int payload; unsigned int padding = 16; /* Use minimum padding */ ・・・ hbtype = *p++; ……(2) n2s(p, payload); ……(3) pl = p; ……(4)
サーバがクライアントからハートビートリクエストを受信すると、上記の(1)に示すs->s3->rrec.dataというバッファにリクエストメッセージが入ってきます。このリクエストメッセージのフォーマットは、typeフィールド(1バイト)、payload_lengthフィールド(2バイト)、payloadフィールド(payload_lengthバイト)、paddingフィールド(16バイト)という単純なものです。ちなみに、レスポンスメッセージのフォーマットも全く同じです。
コードを見てみると、リクエストメッセージのtypeフィールドをhbtype変数に保存し(2)、payload_lengthフィールドをpayload変数に保存しています(3)。そして最後にpayloadフィールドの先頭アドレスをplポインタに保存しています(4)。
これでリクエストメッセージの読み込み処理は終わったので、次にレスポンスメッセージ用のメモリ領域を確保します(下記コード)。
unsigned char *buffer, *bp; ・・・ buffer = OPENSSL_malloc(1 + 2 + payload + padding); ……(5) bp = buffer; ……(6)
OPENSSL_malloc()関数によって、1(typeフィールドの長さ)+2(payload_lengthフィールドの長さ)+payload(リクエストメッセージのpayload_lengthフィールドの値)+padding(16バイト)の大きさのメモリが確保されます(5)。そして、確保されたメモリの先頭アドレスがbpポインタに保存されます(6)。
最後に、確保したメモリにレスポンスメッセージを作成します。
*bp++ = TLS1_HB_RESPONSE; ……(7) s2n(payload, bp); ……(8) memcpy(bp, pl, payload); ……(9)
typeフィールドとしてTLS1_HB_RESPONSEを格納します(7)。そして、payload_lengthフィールドとしてpayload変数の値(つまりリクエストメッセージのpayload_lengthフィールドの値)が入ります(8)。最後に、memcpy関数で、plポインタ(リクエストメッセージのpayloadフィールド)から、payload変数の値(リクエストメッセージのpayload_lengthフィールドの値)分だけコピーします。
あとは、パディングを16バイト追加した上で、このレスポンスメッセージを返信しますが、説明は割愛します。
いかがでしたか?とても単純なコードであり、問題無さそうに見えます。
では、もしリクエストメッセージのpayloadフィールドが短いにも関わらず、payload_lengthフィールドに巨大な値が指定されていた場合には一体どうなるでしょうか?
例えば、リクエストメッセージのpayload_lengthフィールドが1000バイト、payloadフィールドがhello(5バイト)だったとします。
先ほど説明したリクエストメッセージの読み込み処理です。クライアントのリクエストメッセージがs->s3->rrec.dataに格納されます。
unsigned char *p = &s->s3->rrec.data[0], *pl; ……(1) unsigned short hbtype; ・・・ hbtype = *p++; ……(2) n2s(p, payload); ……(3) pl = p; ……(4)
payload変数の値が1000バイトになり、plポインタに”hello”の先頭アドレスが入りますね。ここまでは問題が無さそうです。
unsigned char *buffer, *bp; ・・・ buffer = OPENSSL_malloc(1 + 2 + payload + padding); ……(5) bp = buffer; ……(6)
その後、レスポンスデータ用のメモリとして、1+2+1000+16が確保されます。まだ、問題が無さそうにみえますね。
*bp++ = TLS1_HB_RESPONSE; ……(7) s2n(payload, bp); ……(8) memcpy(bp, pl, payload); ……(9)
レスポンスメッセージの作成処理に移ります。レスポンスメッセージのpayload_lengthフィールドに1000が格納されます(8)。ここまでは、まだ問題なさそうですが、問題なのはここからです。(9)でmemcpy()関数を利用して、plポインタから1000バイト読み込んでpayloadフィールドにコピーしようとしますが、plポインタが指し示すメモリは”hello”の5バイト分しかありません。結果的に、関係ないメモリ領域も995バイト分読み込まれてしまうことになります。
まとめ
リクエストメッセージのpayload_lengthフィールドは2バイトなので、最大で65535バイトのメモリ上のデータが漏洩することになります。たまたまリクエストメッセージが確保されたメモリ領域の下方のメモリ領域65535バイト以内に、重要なデータが存在していると、ヤバいことがわかりますね。ただし、攻撃者視点からみると、リクエストメッセージが格納されるメモリアドレスをコントロールできないので、どんなデータが得られるかは運任せということになります。
修正パッチの概要も簡単に紹介しておきましょう。修正パッチの重要パートとして、下記の赤字のコードが追加されました。
if (1 + 2 + 16 > s->s3->rrec.length) ……(10)
return 0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length) ……(11)
return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;
s->s3->rrec.lengthは受信したリクエストメッセージの実際のメッセージ長(改変不可)なので、(11)に示すように、レスポンスメッセージとして返されるメッセージ長が、リクエストメッセージの実際のメッセージ長よりも長い場合は、破棄されるようになっています。ちなみに、(10)は、本脆弱性とは関係ないのですが、SSLハートビートのリクエストメッセージの仕様に従っていない短いメッセージへの対応です。
5. 検証環境でHeartbleedの脆弱性を攻撃してみる
DeNAの社内に検証環境を作成し、Heartbleedの脆弱性を持つOpenSSLライブラリを利用したApache上で、ログイン機能を持つ簡易プログラムを動作させ実際に攻撃をしてみました。ハートビートのリクエストメッセージが確保されるメモリアドレスによって、漏洩するデータが変わるのですが、時間を置いて何度か繰り返している内に、下記の図のように、他のユーザのHTTPSログイン時のリクエストデータ(ログイン名とパスワードや、クッキー内のセッションID)を盗むことができました。
6. Heartbleedの脆弱性の影響
実際の攻撃を見たように、Heartbleedの脆弱性が攻撃されると、プロセスのヒープ領域に格納されているデータは何でも漏洩してしまうリスクがあります。
さらに、攻撃者が能動的に攻撃できるため、脆弱性をもつOpenSSLを利用しているサーバ管理者は、一刻も早い対策が必要になります。
また、漏洩したデータの種類によっては、被害が拡大する恐れがあるため注意が必要となります。例えば、証明書の秘密鍵が漏洩した場合には暗号通信を解読され盗聴に繋がったり、ユーザのログインアカウントが漏洩した場合には、なりすましに繋がったりしてしまいます。
7. Heartbleedの脆弱性の影響有無の確認方法と対応
Heartbleedの脆弱性の影響を受けるOpenSSLのバージョンは、1.0.1から1.0.1f、1.0.2-betaとなります。該当する場合には攻撃を受けた際にプロセスのメモリの内容が漏洩してしまう可能性が存在するため、システム上にインストールされているOpenSSLのバージョンを確認しましょう。システムの管理者であれば、下記のようなopensslコマンドやyum、rpmコマンドを実行することで簡単に確認することができます。
[heart@dena_security ~]$ openssl version OpenSSL 1.0.1e-fips 11 Feb 2013 [heart@dena_security ~]$ yum list installed | grep openssl openssl.i686 1.0.1e-15.el6 @anaconda-CentOS-201311271240.i386/6.5 openssl-devel.i686 1.0.1e-15.el6 @base [heart@dena_security ~]$ rpm -qa |grep openssl openssl-1.0.1e-15.el6.i686 openssl-devel-1.0.1e-15.el6.i686
この例の場合、いずれも「1.0.1e」となっているのでHeartbleedの脆弱性が存在する可能性があり対応が必要なシステムになりますね。バージョンが一致したら必ず影響が出るとは言いきれないものではありますが、影響がないことの確認が難しいまたは時間を要する場合、対応することが望ましいと言えます。
特にこの脆弱性では、対策を行っていない場合、後から影響範囲を調査することが非常に困難であるため早急な対応が強く推奨されます。対応が必要な場合は、各ディストリビューションが配布しているパッチを適用するか、脆弱性のない最新バージョンへの切り替えを行い対応する必要があります。
脆弱性があった場合、メモリ上の情報が漏洩した可能性があるため、先に挙げたようなユーザの認証の漏えいによってなりすましが発生してしまったかを確認し、ユーザへのケアを行う必要があります。また、証明書の秘密鍵についても漏洩する可能性のある範囲の情報になるため、証明書の変更を行うことが推奨されます。
また、監査部門等のシステムにログインはできないものの、Heartbleedの脆弱性の有無を確認する必要がある場合も存在するかと思います。
今回のケースでは、実証コードが公開されているのでチェックツールを利用してシステム外部からの確認も可能です。但し、注意しなければならないのは、Webアプリケーションとして動作しているようなチェックツールの場合、そのサイトが完全に信頼できるものではない限りは利用してはいけません。脆弱性が確認された場合に、チェックツールが脆弱性を利用してチェック対象のメモリ内部の情報を不正に取得する可能性もありますし、チェックツール上に脆弱なサーバとして情報が記録されている可能性も考慮しなければなりません。
また、悪意がない場合でもチェックツールでは、実際にメモリの一部を取得するコードになっているケースも考えられ、たまたまそのメモリの一部に重要な情報(例えば、ユーザの認証情報など)が混入してしまった場合、脆弱性チェック行為がそのまま情報漏洩に繋がってしまうという本末転倒な可能性も否定できません。
もし、チェックツールを利用する場合には、配布されている実証コードを自システム上で動作させて検証することも可能とか考えられますが、危険性を理解し、検証コード自体に悪意のあるコードが含まれていないことを十分に確認した上で利用する必要があります(検証コードの実行を積極的に勧めるものではありません)。実際に検証を行う場合には、対象のサイトが確実に自身の会社の管理するものであることを確認してから自己責任にて行うようにしてください。上記で示したように、チェック行為自体が不正アクセスそのものに繋がる可能性も考慮する必要があります。
8. 最後に
脆弱性情報について正しく理解して、正しく対応・対策を行いましょう。ご不明な点やお問い合わせがありましたら、お気軽にコメントやMobageオープンプラットフォーム事務局までご連絡ください。(パートナー様はこちらから)
長文を読んでくださり、ありがとうございました!