Heartbleed(CVE-2014-0160)
たった1つの長さ検証漏れが世界中のTLSサーバーから秘密鍵まで抜いた事件を、未検証memcpyの原理・境界チェックの欠落・修正差分・境界検証の一般原則まで一気に理解できる。
- OpenSSLのHeartbeat拡張で、自己申告のペイロード長を実データ長と照合せずmemcpyへ渡した。過大な長さを指定すると隣接ヒープが応答へ混入し、1回最大64KBを読み出せた。
- 書き込みではなくバッファ over-read。プロセスのヒープ上にあれば秘密鍵・セッションチケット・Cookie・平文パスワードが漏れうるうえ、TLSハンドシェイク完了前でも成立し、ログにも残りにくい。
- 対象はOpenSSL 1.0.1〜1.0.1f、修正は1.0.1g(2014年4月7日公開)。パッチは応答前に長さの境界チェックとゼロ長拒否を追加した。教訓は、外部由来の長さは必ず実バッファ長で検証し、コピー前に境界を確かめること。
何が起きたか(影響範囲・深刻度)
2014年4月7日、OpenSSLはTLS Heartbeat拡張の実装に重大な欠陥があると公表しました。CVE-2014-0160、通称Heartbleedです。攻撃者は正規のTLS接続を張るだけで、サーバープロセスのメモリを1リクエストあたり最大64KB読み出せます。要求は何度でも繰り返せるため、時間をかければ広範囲のヒープを走査できました。
深刻なのは、漏れる可能性のあるデータの中身です。同じプロセスのヒープ上にあれば、サーバーのTLS秘密鍵、セッションチケットの鍵、他ユーザーのHTTPリクエスト(Cookieやパスワード)まで混入しえます。秘密鍵が漏れれば、過去に記録された通信の復号や、正規サーバーへのなりすましが可能になります。攻撃はTLSハンドシェイクの完了前でも成立し、通常はアプリケーション層のログに残りません。
影響は甚大でした。当時のインターネット上のHTTPSサーバーの相当数がOpenSSL 1.0.1系を使っており、証明書の失効・再発行、全ユーザーのパスワード変更という大規模な対応が世界的に発生しました。NVDのCVSS v2基本値は5.0(AV:N/AC:L/Au:N/C:P/I:N/A:N)で、機密性のみへの影響として評価されています(可用性・完全性は損なわない読み取り専用の欠陥のため)が、鍵漏洩の二次被害まで含めた実質的な危険度は極めて高いと広く受け止められました。
脆弱なのはOpenSSL 1.0.1から1.0.1fまで、および1.0.2-beta1。修正版は1.0.1g(2014年4月7日)です。1.0.0系や0.9.8系はHeartbeat拡張自体を含まず影響を受けません。
脆弱性の原理(なぜ起きるか)
Heartbeatは接続を維持するためのキープアライブ機構で、RFC 6520が定義します。一方がHeartbeatRequestを送ると、相手は受け取ったペイロードをそのままHeartbeatResponseに載せて返します。メッセージは「タイプ(1バイト)」「ペイロード長(2バイト)」「ペイロード本体」「パディング」で構成されます。ここで問題になるのが、ペイロード長がパケット内で自己申告される点です。
OpenSSLのtls1_process_heartbeat(DTLS側はdtls1_process_heartbeat)は、受信データからこの長さを取り出して応答を組み立てます。欠陥コードの骨子は次のとおりです。
/* p は受信メッセージの先頭を指すポインタ */
hbtype = *p++;
n2s(p, payload); /* 2バイトの申告長を payload に読み込む(実長と未照合) */
pl = p; /* pl は申告されたペイロードの開始位置 */
/* ... 応答バッファ bp を確保 ... */
memcpy(bp, pl, payload); /* 申告長 payload ぶんを丸ごとコピー */
n2sはネットワークバイトオーダーの2バイトを整数payloadに変換するマクロで、値域は0〜65535です。ここでpayloadは攻撃者が送ったパケットの中の数字にすぎず、実際に受信したペイロードの長さとは無関係です。にもかかわらず、コードはこの申告長を信じてmemcpyのコピー長に使います。攻撃者が本体を1バイトしか送っていなくてもpayloadに65535を書いておけば、memcpyはplの位置から65535バイトを読み進め、本来のペイロードを越えて隣接するヒープメモリまで応答バッファへ写し取ってしまいます。
これが典型的なバッファ over-read(境界を越えた読み出し)です。書き込み側は破壊しないためクラッシュしにくく、だからこそ静かに情報だけが漏れ続けます。根本原因は一言でいえば「外部から与えられた長さフィールドを、実バッファ長で検証しないままmemcpyに渡した」ことに尽きます。
オーバーフロー(書き込み)と違い、over-read はメモリを壊さないため検知が難しい一方、読み出した内容がそのまま攻撃者へ返る本件では、機密情報の直接的な漏洩に直結します。
エクスプロイトの流れ(概念レベル)
横にスクロール
悪用の筋道は単純です。攻撃者はまず対象へTLS接続を開始します。ハンドシェイクの途中、鍵交換や証明書検証が終わる前の段階でも、Heartbeatメッセージは処理されます。ここで攻撃者は、ごく短い(あるいは空の)ペイロードを載せつつ、長さフィールドには上限に近い値を書いた不正なHeartbeatRequestを送ります。
サーバーは申告された長さを信じ、その長さぶんのメモリを応答として返します。返ってくる内容には、直前に同じヒープ領域で扱われた他の接続の断片が含まれえます。攻撃者はこの読み出しを繰り返し、返却されたバイト列から秘密鍵の候補やセッション情報を機械的に抽出していきます。証明書の秘密鍵はメモリ上で特徴的な構造を持つため、大量のダンプから探索する手法が知られていました。
ここでは仕組みの理解に必要な概念のみを示し、そのまま実行できる完全なペイロードや攻撃ツールのコードは記載しません。目的は、なぜ検証漏れが致命傷になるのかを設計・実装の観点で学ぶことにあります。
修正と対策
1.0.1gのパッチが加えたのは、応答を組み立てる前の2つの防御です。第一に、申告長が実際に受信したレコード長に収まっているかを確認します。第二に、意味のないゼロ長のHeartbeatを拒否します。修正後のチェックの骨子は次のとおりです。
/* 0長のリクエストは無視する */
if (1 + 2 + 16 > s->s3->rrec.length)
return 0;
/* 申告長 payload が実レコード長に収まるか検証する */
if (1 + 2 + payload + 16 > s->s3->rrec.length)
return 0; /* 収まらなければ黙って破棄 */
s->s3->rrec.lengthはTLSレコードとして実際に受信したバイト数です。1 + 2 + payload + 16は「タイプ1バイト+長さ2バイト+申告ペイロード+パディング16バイト」の合計で、これが実受信長を超えるなら、そのpayloadは嘘なので処理を打ち切ります。これでmemcpyは必ず実データの範囲内に収まります。
運用側の対策は多層でした。まずOpenSSLを1.0.1g以降へ更新する。次に、鍵が漏れた前提で全証明書を再発行し、旧証明書を失効させる(前方秘匿性のない鍵交換では過去通信の復号も想定して更新する)。さらに、ユーザーのパスワードやセッショントークンを無効化して張り替える。ビルド時オプション-DOPENSSL_NO_HEARTBEATSでHeartbeat機能を無効化する緊急回避も用いられました。
| 観点 | 修正前(1.0.1〜1.0.1f) | 修正後(1.0.1g) |
|---|---|---|
| 申告長の扱い | 無検証でmemcpyのコピー長に使用 | 実レコード長との境界を検証 |
| ゼロ長リクエスト | 処理してしまう | 明示的に破棄 |
| 不正パケット | 隣接ヒープを応答に混入 | 黙って破棄し応答しない |
| 最悪の読み出し量 | 1要求あたり最大64KB | 申告分は実データ内に限定 |
教訓(一般化できる原則)
Heartbleedの核心は、暗号の弱さでも複雑な論理ミスでもなく、「入力に含まれる長さを信じてメモリをコピーした」という古典的な過ちです。ここから一般化できる原則があります。
- 外部由来の長さは必ず実体の長さで検証する: パケットやファイルが自己申告するサイズは攻撃者の制御下にあります。コピーやアクセスの前に、実際に確保・受信したバッファ長と突き合わせ、はみ出す要求は拒否します。
- コピーは境界チェックとセットで書く:
memcpyや配列アクセスの直前に上限を確認する規律を、レビューや静的解析で強制します。C/C++のように境界検査が自動でない言語では、この一手間の欠落がそのまま脆弱性になります。 - over-read も脆弱性として扱う: 書き込み破壊がなくクラッシュしないからと軽視できません。読み出した内容が外部へ返る経路があれば、機密性の重大な侵害です。
- 重要ライブラリほど監査と資金を厚くする: 世界の通信基盤を支えるコードが少人数の善意で保守されていた事実は、供給網としての脆さを露呈しました。この反省はOpenSSLの再監査やメモリ安全な言語での再実装、基盤OSSへの継続的支援につながりました。
境界検証やメモリ安全の考え方はセキュリティ、C言語のメモリモデルと未定義動作はプログラミング、プロセスのアドレス空間とヒープの扱いはOSの各トピックも参照してください。ハードウェア起因のメモリ読み出しについてはSpectre / Meltdownが対になる事例です。
脆弱性の解剖の記事ガイド
Heartbleed(CVE-2014-0160)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Heartbleed
比較で見る軸
難易度: advanced / カテゴリ: 脆弱性の解剖 / タグ数: 6
導入後に効く点
書き込みではなくバッファ over-read。プロセスのヒープ上にあれば秘密鍵・セッションチケット・Cookie・平文パスワードが漏れうるうえ、TLSハンドシェイク完了前でも成立し、ログにも残りにくい。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 脆弱性の解剖
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Heartbleed / OpenSSL」に近いか確認する。
- 強みである「OpenSSLのHeartbeat拡張で、自己申告のペイロード長を実データ長と照合せずmemcpyへ渡した。過大な長さを指定すると隣接ヒープが応答へ混入し、1回最大64KBを読み出せた。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。