Heartbleed 2014年 OpenSSL の境界の確かめ漏れ

heartbeat の要求に書かれた長さを、受け取った記録の実際の長さと比べずに、その長さだけ複写して返した。仕様は長すぎる申告を黙って捨てよと書いていたが、コードにその1行が無かった。漏れるのは通信の中身ではなく、相手のプロセスのメモリで、秘密鍵も含み得る。

CVE-2014-0160。CVSS 3.1 の基本値は7.5、CWE-125 範囲外の読み取り[2]。影響は OpenSSL 1.0.1 から 1.0.1f と 1.0.2-beta1[1]。公表と修正の 1.0.1g は2014年4月7日[1][5]。TLS の heartbeat は 1.1.0 で除かれ、DTLS の heartbeat も 3.0.0 で除かれた[6]。2026年11月時点の公開資料で書いています。

要点

  • 要求の中の payload_length を、記録の長さと比べずに信じた。仕様の RFC 6520 は、長すぎるなら黙って捨てると定めていた。
  • 1回の応答で最大64KBのメモリが相手へ渡る。サーバーからも、つないだクライアントからも漏れる。
  • 弱点の入った 1.0.1 は2012年3月14日に出て、修正まで2年余り、広く使われる版に入っていた。
  • 修正は、記録の長さを確かめる2行と、応答の長さの上限の確かめだった。更新しても、それまでに漏れた鍵は戻らない。
  • 根本の守りは、外から来た長さを使う前に実際の長さと比べること、使わない機能を作りから外すこと、漏れた前提で鍵を作り直すこと。

何が起きたか

2011年12月に heartbeat が入り、2012年3月に 1.0.1 で出て、2014年4月7日に公表と修正、2016年と2021年に機能が除かれた 1 取り込む 2011年12月31日 heartbeat の 実装のコミット 2 出す 2012年3月14日 1.0.1 に heartbeat が入る 3 直す 2014年4月7日 公表と 1.0.1g 4 使われる 2014年4月 カナダの国税の 機関で約900人 5 除く 2016年8月 TLS から 2021年 DTLS 2から3まで2年と約3週間、配布された版に弱点があった 修正後も、それまでに漏れた鍵と資格情報は使われ得る
図1 取り込みから機能の除去までの段
日付出来事
2011年12月31日外部の寄稿者の TLS と DTLS の heartbeat の実装が、1人のレビューを経て OpenSSL に取り込まれる[7]
2012年2月RFC 6520 が出る。長すぎる payload_length の要求は黙って捨てると定める[3]
2012年3月14日OpenSSL 1.0.1 が出る。主な変更に TLS と DTLS の heartbeat の対応がある[5]
2014年4月7日OpenSSL が勧告を出し、1.0.1g で直す。見つけたのは Google のセキュリティの担当者[1][5]。同じ日、NVD に載る[2]
2014年4月8日US-CERT が TA14-098A を出す。悪用のコードは公開されている、と書く[8]。JPCERT/CC が注意喚起を出す[9]
2014年4月11日カナダ歳入庁が、同国の個人情報保護の機関に、Heartbleed を使われ約900人の納税者の個人情報が取られたと届け出る。機関は14日に声明を出す[10]。JPCERT/CC が、秘密鍵などが既に漏れている恐れがあるとして、鍵の作り直しと証明書の再発行を足す[9]
2016年8月25日OpenSSL 1.1.0 で、TLS の heartbeat を除き、DTLS では既定で無効にする[6]
2021年9月7日OpenSSL 3.0.0 で、使われることが少なく役に立っていないとして、DTLS の heartbeat も除く[6]
2022年5月4日CISA が、悪用が確認された脆弱性の一覧 KEV に載せる[2]
公表資料に無い話

2014年8月に米国の病院の運営会社 Community Health Systems が公表した約450万人分の流出は、Heartbleed が入口だったと広く報じられた。同社が米国証券取引委員会へ出した届け出には、2014年4月と6月の攻撃、中国から来た APT の集団、調べた会社の名前は書かれているが、Heartbleed の名前は無い[11]。

起きた仕組み

heartbeat は、TLS と DTLS の接続が生きているかを確かめる拡張である。要求を受けた側は、要求の payload をそのまま写して返す[3]。

struct {
   HeartbeatMessageType type;
   uint16 payload_length;
   opaque payload[HeartbeatMessage.payload_length];
   opaque padding[padding_length];
} HeartbeatMessage;

RFC 6520 は、送る側に16バイト以上の乱数の詰め物を求め、全体を2の14乗バイト以下とし、受け取った payload_length が大きすぎれば黙って捨てると定める[3]。payload_length は16ビットなので、申告できる値は最大65535になる。

受け取った記録の payload は短いのに、申告の長さだけ複写するため、記録の後ろにある別のメモリまで応答に入る 受け取った側のメモリ 型 長さ 申告 N payload 実際 n 詰め物 記録の外 ほかの接続の中身、鍵、資格情報があり得る 記録の長さ。ここまでが受け取った物 申告 N の分だけ複写する。N が n より大きいと外まで読む 応答。最大で約64KB 型 N payload 記録の外から写した物 詰め物
図2 申告の長さを信じた複写が、記録の外まで読む
  • 勧告は、つないだクライアントにもサーバーにも、最大64KBのメモリが見える、と書く[1]。NVD は、細工した要求で範囲外の読み取りを起こし、秘密鍵を読めることが示された、と書く[2]。
  • 読まれるのは、同じプロセスのメモリのうち、記録の後ろに続く物である。

架空の最小の例

架空の通信の echo の拡張で、同じ形の誤りを示す。1バイトの型、2バイトの申告の長さ、payload、16バイトの詰め物、の順の記録を受け、payload を写して返す。

直す前 申告の長さを確かめずに複写する

/* rec は受け取った記録の先頭、rec_len は記録の実際の長さ */
int echo_handle(struct conn *c, const unsigned char *rec, size_t rec_len)
{
    unsigned int claimed = (rec[1] << 8) | rec[2];   /* 申告の長さ */
    size_t out_len = 3 + claimed + 16;
    unsigned char *out = malloc(out_len);
    if (out == NULL) return -1;

    out[0] = ECHO_RESPONSE;
    out[1] = rec[1];
    out[2] = rec[2];
    memcpy(out + 3, rec + 3, claimed);   /* rec_len を見ていない */
    fill_random(out + 3 + claimed, 16);
    conn_send(c, out, out_len);
    free(out);
    return 0;
}

直した後 使う前に実際の長さと比べ、合わなければ捨てる

int echo_handle(struct conn *c, const unsigned char *rec, size_t rec_len)
{
    if (rec_len < 1 + 2 + 16) return 0;              /* 型と長さと詰め物も無い */
    unsigned int claimed = (rec[1] << 8) | rec[2];
    if (1 + 2 + (size_t)claimed + 16 > rec_len) return 0;   /* 黙って捨てる */
    size_t out_len = 3 + claimed + 16;
    if (out_len > MAX_RECORD_PLAIN) return 0;       /* 応答の上限 */
    ...
}

OpenSSL の修正も同じ形だった。ssl/t1_lib.c と ssl/d1_both.c で、型と長さを読む前に 1 + 2 + 16 が記録の長さを超えないか、読んだ後に 1 + 2 + payload + 16 が記録の長さを超えないかを確かめ、超えれば RFC 6520 の4節どおり黙って捨てる。応答の長さが記録の平文の上限を超えないかも確かめる[4]。

誤りの型

外から来た長さで複写の量を決め、手元に実際にある量と比べない。C の memcpy は境界を知らないので、確かめはすべて呼ぶ側の責任になる。ふつうの要求では申告と実際が一致するので、正しい相手との試験はすべて通る。

どこで止められたか

仕様、コード、レビューと試験、機能の選択、運用の5つの段に、それぞれ止める所を置く 仕様 コード レビューと試験 機能の選択 運用 1 仕様の MUST を試験に写す 長すぎる申告は捨てる、が既に書いてあった 2 外から来た長さを使う前に比べる この件の根本。修正は確かめの数行だった 3 食い違う入力で試す 申告と実際が合わない記録を試験と検査に入れる 4 使わない機能を作りから外す 後に OpenSSL 自身が heartbeat を除いた 5 漏れた前提で鍵を作り直す 更新だけでは、既に漏れた物は戻らない
図3 段ごとの止める所
止める所この件で限り
仕様の MUST を試験に写すRFC 6520 は、長すぎる payload_length の要求を黙って捨てると定めていた[3]。修正のコードの注は、この節をそのまま指している[4]仕様が書いていない境界は拾えない
外から来た長さを使う前に比べる確かめは、記録の長さとの比較が2つと、応答の上限が1つだった[4]同じ型の誤りは、長さを持つ形式を読むすべての所にあり得る。1か所を直しても、ほかは残る
食い違う入力で試す正しい相手との通信では、申告と実際が必ず一致する。ふつうの試験では誤りが出ない範囲外の読み取りは落ちないことが多く、試験の結果に出ない。AddressSanitizer のような検査の道具と組んで初めて見える
使わない機能を作りから外す勧告は、すぐに更新できない場合、-DOPENSSL_NO_HEARTBEATS で作り直すよう勧めた[1]。OpenSSL は後に、使われることが少ないとして heartbeat を除いた[6]作る側が組み込んだ機能は、使う側が外すまで残る
漏れた前提で鍵を作り直すUS-CERT は、弱点のある版で作った鍵は漏れたものとみなし、修正の後に作り直すよう求めた[8]。JPCERT/CC は、新しい秘密鍵を作りサーバー証明書を再発行するよう求めた[9]漏れたかどうかは、後から確かめられないことが多い。確かめるより作り直す方が早い

直した後の順

更新、再起動、鍵の作り直しと証明書の再発行、古い証明書の失効、資格情報の差し替えの順に進める 1 更新 1.0.1g 以降 2 再起動 読み込み直す 3 鍵を作る 証明書を再発行 4 失効 古い証明書 5 差し替え 資格情報 3を1より先にすると、新しい鍵もまた漏れ得る
図4 修正の後に進める順
  • 共有ライブラリを差し替えても、動いているプロセスは古い版を読み込んだままである。OpenSSL を結ぶサーバーのプロセスは、すべて再起動する。
  • 鍵の作り直しは、修正を当てた後にする[8]。順が逆だと、新しい鍵も同じ穴から読まれ得る。
  • 古い証明書は、作り直しても期限まで有効なまま残る。漏れた前提なら失効させる。
  • セッションのクッキー、API の鍵、利用者のパスワードも、同じプロセスのメモリを通っていたなら漏れた前提で差し替える。

US-CERT は、あわせて Perfect Forward Secrecy を使うよう勧めた[8]。鍵の交換に使い捨ての鍵を使っていれば、サーバーの秘密鍵が後で漏れても、過去に記録された通信は復号できない。

守り方

根本の直し方
  • 外から来た長さ、件数、位置は、使う前に、手元に実際にある量と比べる。合わなければ、仕様に従って捨てるか接続を切る。比べる所は、読む関数の入口の1か所にまとめる。
  • 仕様の MUST と MUST NOT を、1つずつ試験に写す。とくに「黙って捨てる」「切る」と書かれた所は、捨てたことを確かめる試験を置く。
  • 使わない機能は、設定で無効にするより、作りから外す。外せないなら既定を無効にする。勧告がすぐの策として作り直しを示したのも、この形だった[1]。
  • 依存する部品の版を棚卸しし、修正の版を早く入れる仕組みを持つ。部品の版と脆弱性の通知の扱いは A03 ソフトウェアサプライチェーンの不備 の型1にある。
緩和の策
  • 長さを持つ形式を読むコードは、申告と実際を食い違わせた入力で試し、AddressSanitizer のような範囲外の読み取りを見つける検査の道具の下で回す。
  • 鍵の交換は、使い捨ての鍵を使う方式だけを許す[8]。暗号の設定の誤りの型は A04 暗号化の不備 にある。
  • 漏れた鍵と資格情報は、無効化、差し替え、跡の調査の順に扱う。順の考え方は 秘密の鍵の扱い にある。
  • 秘密鍵をサーバーのプロセスのメモリに置かない形を検討する。HSM や、鍵を別のプロセスに持たせる形にすれば、同じプロセスのメモリが読まれても鍵は出ない。

AIが書く読み取りのコード

AIのコーディングの道具は、2進の形式を読む関数を短い指示で書く。申告の長さで複写する1行は、ふつうの入力で正しく動き、試験も通る。この件の誤りは、その形をしていた。

  • 外から来た長さで memcpy、配列の添字、確保の大きさを決める行を、差分の中で探す。その前に実際の長さとの比較が無ければ止める。
  • AIに試験を書かせると、正しい入力の試験に偏る。申告と実際が食い違う入力、上限ちょうど、上限を1超える入力を、人が足すよう指示する。
  • 差分の読み方と、自動の点検を書く側と点検する側で分ける考え方は、AIの出力を確かめる にある。

参照

  1. OpenSSL Security Advisory 07 Apr 2014, TLS heartbeat read overrun CVE-2014-0160 OpenSSL、2014年4月7日
  2. CVE-2014-0160 NIST NVD
  3. RFC 6520 Transport Layer Security and Datagram Transport Layer Security Heartbeat Extension IETF、2012年2月
  4. Add heartbeat extension bounds check. openssl/openssl、2014年4月7日のコミット
  5. NEWS openssl/openssl、OpenSSL_1_0_1-stable
  6. CHANGES.md openssl/openssl
  7. Support for TLS/DTLS heartbeats. openssl/openssl、2011年12月31日のコミット
  8. OpenSSL Heartbleed Vulnerability CVE-2014-0160, TA14-098A US-CERT、CISA の保存版、2014年4月8日
  9. OpenSSL の脆弱性に関する注意喚起 JPCERT-AT-2014-0013 JPCERT/CC、2014年4月8日、11日更新
  10. Statement from the Interim Privacy Commissioner of Canada regarding Heartbleed カナダ個人情報保護コミッショナー事務局、2014年4月14日
  11. Form 8-K, Item 8.01 Other Events Community Health Systems、米国証券取引委員会への届け出、2014年8月18日