わずか11バイトのデータで、脆弱なOpenSSLサーバーに最大131KBものRAMを消費させることができます。バッファを確保した後、サーバーは決して届くことのないメッセージを待ち続けます。接続が突然切断されると、コードはバッファを解放します。しかし、glibcを使用するLinux環境では、このメモリが実行中のタスク内に留まってしまう場合があります。この短いリクエストを繰り返すだけで、リソース使用量はじわじわと増加していきます。結果として、このメモリリークはサーバー全体のクラッシュを引き起こす可能性があります。
HollowByte脆弱性の発見経緯
この脆弱性を最初に発見したのは、Oktaのレッドチームの専門家たちで、「HollowByte」と名付けられました。この欠陥は、TLS接続ハンドラーの開始処理の奥深くに潜んでいます。奇妙なことに、この修正は正式な告知が一切ないまま、OpenSSLの6月ビルドに組み込まれていました。CVEタグも付与されておらず、主要なバグ修正リストにも記載がありません。
この深刻な問題は、ログイン処理や鍵交換、セキュアなセットアップ作業よりもずっと前の段階で発生します。そのため、攻撃者はパスワードや有効な鍵を一切必要とせず、この攻撃を仕掛けることができます。単純な接続を開き、細工したTLSメッセージの冒頭部分を送信するだけで済んでしまうのです。
TLSハンドシェイクが破綻する仕組み
一般に、TLSはWeb通信やネットワーク処理を安全に保つための仕組みです。実際のデータを送信する前に、クライアントとサーバーは短いハンドシェイクを行い、ルールと鍵について合意します。次に、クライアントは通常、自身の主な設定情報を含むClientHelloメッセージを最初に送信します。
すべてのハンドシェイクメッセージは、シンプルな4バイトのヘッダーから始まります。最初の1バイトはメッセージの種類を示し、続く3バイトには本体部分の申告された長さが格納されています。脆弱なOpenSSLビルドでは、コードはこのサイズ値を無条件に信頼していました。そのため、実際のデータが届く前に、受信バッファを即座に拡張してしまっていたのです。
新規のClientHelloの場合、バッファサイズは容易に131KBに達することがあります。短いヘッダーを読み取った後、実行中のタスクは残りのデータをひたすら待ち続けます。その間、攻撃者は一切データを送信しません。接続を開いたままにするか、メモリの確保を強制した直後に接続を切断するかのどちらかです。
メモリの断片化とglibcの問題
この巧妙な手口は、古典的なSlowloris攻撃の手法とよく似ています。Slowloris攻撃では、サーバーは遅いデータの到着を待ちながら接続を保持し続けます。しかし、HollowByteが引き起こす被害は、単なる開いている接続数の問題をはるかに超えています。主な被害は、システムのメモリ管理ツールが解放されたバッファをどう扱うかという点から生じています。
多くのLinux環境では、標準のglibcツールがシステムメモリを管理しています。解放コマンドが実行された後も、小・中規模のメモリチャンクは必ずしもすぐにOSカーネルへ返却されるわけではありません。代わりに、このツールは将来のメモリ処理を高速化するため、それらをプロセス内に保持し続けます。
標準的な防御策が効かない理由
実際のところ、この仕組みは通常であれば理にかなった賢い最適化として機能します。本当の問題が表面化するのは、リクエストがOpenSSLに次々と異なるサイズのバッファを構築させる場合に限られます。やがて、解放されたメモリブロックが新しいタスクに適合しなくなっていきます。その結果、使用中の領域の間に空白の隙間が生まれ、メモリは徐々に断片化していきます。
形式上、OpenSSLはすでに未使用のバッファを解放しています。しかし、サーバーは残された断片をうまく利用できません。さらに、それらをメインシステムに返却することもできません。結果として、RSS指標は増加を続けます。攻撃が止んだ後でさえ、この値は下がることがありません。ユーザーがメモリを完全に取り戻すには、プロセスを再起動する以外に方法がないのです。
1GBのRAMを搭載したサーバーでNGINXの実地テストを行ったところ、空き容量不足によりシステムがプロセスを強制終了させました。この時点で、約547MBが断片化したヒープ内に閉じ込められたままになっていました。より大規模な16GBのサーバーでは、この攻撃によってアクティブな接続数の上限を一切超えることなく、全メモリの約4分の1が瞬く間に消費されました。
そのため、標準的な防御ツールではHollowByteを食い止められない可能性があります。たとえば、IPごとの接続数制限やクライアント総数の制限では、この脅威をブロックできません。攻撃者は多数の接続を同時に保持し続ける必要すらないのです。完了した各リクエストが、そのたびに新しく厄介なメモリの断片を残していきます。
脆弱性への対処
現時点で、公表されているこれらの事実はOkta独自のテストのみに基づくものです。外部による検証はまだ行われていません。また、同チームは実際の攻撃コードを公開していません。glibc以外のメモリ管理ツールに対して、この脆弱性がどの程度深刻な影響を及ぼすかについても、まだ明らかになっていません。
OpenSSLはこのTLSメッセージの欠陥を、バージョン4.0.1、3.6.3、3.5.7、3.4.6、3.0.21で速やかに修正しました。これらのセキュアなビルドは6月9日にリリースされました。一方で、これらの各ブランチにおける、それより前のリリースには依然として脆弱なコードが残ったままです。
この新しいパッチの適用後は、クライアントが申告したサイズどおりにバッファを即座に拡張することはなくなりました。代わりに、実際のデータが到着するにつれて、メモリは段階的に拡張されるようになっています。そのため、クライアントが長いメッセージを送ると偽って申告するだけで、サーバーに巨大なメモリブロックを強制的に確保させることはもうできません。
サイレントアップデートの危険性
驚くべきことに、OpenSSLの開発者たちはこの変更を通常のバグ修正として扱いました。CVEタグも公式な警告通知も一切発行されませんでした。この判断の結果、企業システムの更新は格段に難しくなっています。セキュリティスキャナーは通常、CVE番号を使ってインストール済みアプリケーションを追跡します。一方、管理者はベンダーからの公式発表に大きく依存しています。明確なIDが存在しないため、標準的なツールではサーバーにパッチが必要であることを検知できない可能性があります。
さらに、OpenSSLのバージョン番号のみに基づいて安全性を判断するのは危険です。一部のLinuxディストリビューションでは、ツール自体を完全に置き換えるのではなく、古いブランチに修正をバックポートする形を取っています。そのため、パッケージのバージョン表記だけを見ると脆弱に見えても、実際には重要な修正がすでに適用済みであるケースがあります。
したがって、管理者はパッケージの変更履歴を確認するか、ベンダーに直接問い合わせる必要があります。開発者たちは、メインブランチ向けのマージリクエスト30792、新しいブランチ向けの30793、バージョン3.0向けの30794でこの修正を追加しました。手動でビルドしているユーザーは、直ちに最新のセキュアなOpenSSLリリースへアップグレードすべきです。セットアップ完了後は、脆弱性のあるコードを実行し続けないよう、旧バージョンのツールを使用しているすべてのサービスを再起動する必要があります。
未解決のDTLSリスク
重要な点として、この修正はあくまで標準的なTLSのみを対象としています。DTLSプロトコルのバリアントでは、バッファ処理の際に依然としてクライアントが申告した長さがそのまま使用されています。パッチの作成者によれば、DTLSの修正にはより大がかりなコード変更が必要になるとのことです。そのため、この部分については現時点で未修正のまま残されています。プロジェクト側は、今後この点を修正する予定があるかどうかについて、まだ言及していません。
今回のケースは、これまでのOpenSSLのメモリ関連脆弱性の中でも異彩を放っています。1月には、複雑な条件を必要とする同様のTLS 1.3証明書に関する問題に対し、低リスクのCVEタグが付与されました。6月には、QUICハンドラーにおける急速なメモリ増加の問題にも、固有のIDが付与されています。
一方、HollowByteは特別な機能や複雑な設定を一切必要としません。OpenSSL HollowByte DoSのレポートで詳しく説明されているとおり、通常のTLSハンドシェイクの開始処理の中で、リクエストがこの脆弱な箇所を直撃します。これほど単純で直接的な攻撃経路があるにもかかわらず、OpenSSLはこのバグを低リスクとしてすら評価していませんでした。
幸いなことに、6月9日のアップデートでは、これと同時に既知の脆弱性18件も修正されました。そのため、最新のビルドを稼働させているユーザーは、HollowByteの影響を完全に免れています。しかし、リリースノートにはこの詳細が一切記載されていません。最大のリスクが残るのは、正式なCVEが公表されて初めて更新を適用するという運用をしているチームのサーバーです。
翻訳元: https://meterpreter.org/openssl-hollowbyte-memory-leak-vulnerability/