Heartbleed 2014年 OpenSSL の境界の確かめ漏れ
heartbeat の要求に書かれた長さを、受け取った記録の実際の長さと比べずに、その長さだけ複写して返した。仕様は長すぎる申告を黙って捨てよと書いていたが、コードにその1行が無かった。漏れるのは通信の中身ではなく、相手のプロセスのメモリで、秘密鍵も含み得る。
要点
- 要求の中の payload_length を、記録の長さと比べずに信じた。仕様の RFC 6520 は、長すぎるなら黙って捨てると定めていた。
- 1回の応答で最大64KBのメモリが相手へ渡る。サーバーからも、つないだクライアントからも漏れる。
- 弱点の入った 1.0.1 は2012年3月14日に出て、修正まで2年余り、広く使われる版に入っていた。
- 修正は、記録の長さを確かめる2行と、応答の長さの上限の確かめだった。更新しても、それまでに漏れた鍵は戻らない。
- 根本の守りは、外から来た長さを使う前に実際の長さと比べること、使わない機能を作りから外すこと、漏れた前提で鍵を作り直すこと。
何が起きたか
| 日付 | 出来事 |
|---|---|
| 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になる。
- 勧告は、つないだクライアントにもサーバーにも、最大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 は境界を知らないので、確かめはすべて呼ぶ側の責任になる。ふつうの要求では申告と実際が一致するので、正しい相手との試験はすべて通る。
どこで止められたか
| 止める所 | この件で | 限り |
|---|---|---|
| 仕様の 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] | 漏れたかどうかは、後から確かめられないことが多い。確かめるより作り直す方が早い |
直した後の順
- 共有ライブラリを差し替えても、動いているプロセスは古い版を読み込んだままである。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の出力を確かめる にある。
参照
- OpenSSL Security Advisory 07 Apr 2014, TLS heartbeat read overrun CVE-2014-0160 OpenSSL、2014年4月7日
- CVE-2014-0160 NIST NVD
- RFC 6520 Transport Layer Security and Datagram Transport Layer Security Heartbeat Extension IETF、2012年2月
- Add heartbeat extension bounds check. openssl/openssl、2014年4月7日のコミット
- NEWS openssl/openssl、OpenSSL_1_0_1-stable
- CHANGES.md openssl/openssl
- Support for TLS/DTLS heartbeats. openssl/openssl、2011年12月31日のコミット
- OpenSSL Heartbleed Vulnerability CVE-2014-0160, TA14-098A US-CERT、CISA の保存版、2014年4月8日
- OpenSSL の脆弱性に関する注意喚起 JPCERT-AT-2014-0013 JPCERT/CC、2014年4月8日、11日更新
- Statement from the Interim Privacy Commissioner of Canada regarding Heartbleed カナダ個人情報保護コミッショナー事務局、2014年4月14日
- Form 8-K, Item 8.01 Other Events Community Health Systems、米国証券取引委員会への届け出、2014年8月18日