このシリーズではこれまでに、µC/HTTP サーバー用のファジングツールの開発を取り上げました。前回の記事で説明したように、このファジングツールは AFL++ と互換性を持たせるためにファイルから読み込むようになっています。現在のファジングの実装で一度に処理できるのは単一のリクエストのみです。この単一のリクエストに対応したファジングツールでも、セキュリティの脆弱性が数件明らかになりましたが、1 つのセッションで複数のリクエストを受信すると、複雑で興味深いオブジェクトの相互作用と内部状態の遷移が発生します。
複数のリクエストを含む 1 つのテストケースファイルを用意して、オブジェクトの相互作用と内部状態の遷移をファジングできるようにしたいと考えました。そこで今回は、ソケットファイル記述子を一般的なファイル記述子に単純に置き換えるだけではこのアプローチがうまくいかない理由と、複数のリクエストをサポートするためにオープンソースライブラリ libdesock に追加した機能について説明します。
ソケットからの読み込みとファイルからの読み込み
この問題の解決方法について考えると、少し混乱してしまいます。というのも、ソケット通信がリクエスト/応答パターンによる双方向通信であるのに対し、ファイルからの読み込みは一方向通信だからです。ネットワーククライアント/サーバーモデルでは、通常、クライアントがサーバーにリクエストを送信し、サーバーはクライアントが別のリクエストを送信する前に応答します。
µC/HTTP サーバーは単一のスレッドで動作するので、最初のリクエストを完全に処理して応答してから、ソケット記述子からの 2 回目の読み込みを行います。
目標は、1 つのテストケースファイルを使用して、クライアントからの複数のリクエストをシミュレーションすることです。では、同じテストケースファイル内に 2 つのリクエストを含めるとどうなるでしょうか。サーバーコードは、EOF に達するかバッファがいっぱいになるまで、利用可能なデータをファイルから読み込みます。
たとえば、1 つのテストケースファイルに 2 つの完全な HTTP リクエストが含まれているとします。µC/HTTP サーバーのバッファサイズは 1,460 バイトであり、2 つのリクエストを合わせた長さは 841 バイトしかありません。つまり、このテストケース全体がバッファ内に収まり、一度にメモリに読み込まれます。µC/HTTP サーバーは最初の HTTP リクエストを処理すると、バッファ内の残りのデータを破棄します。その後、サーバーコードが再度テストケースファイルから読み込んだときには、実際に処理されたのは最初のリクエストのみでも EOF に達することになります。
これに対処するには、最初のリクエストを処理した後、ファイルの読み込みを停止するようにサーバーに指示する方法が必要です。これは、テストケースファイルでデリミタを使用することで実現できます。デリミタは、リクエスト内の他の場所に出現する可能性がない一意の値である必要があります。
libdesock
1 つのリクエストが終了し、別のリクエストが始まる場所を示すためにデリミタを使用するというこの戦略を実装するために、libdesock という既存のライブラリに機能を追加しました。このライブラリの目的は、ネットワーク アプリケーションがソケット通信を行わないようにして、ファジングを有効にすることです。これが必要なのは、ファジングツールでは一般的に、stdin 経由でテスト入力を提供するのに対し、ネットワーク アプリケーションではネットワーク接続からの入力を想定しているからです。通常、ネットワーク サーバー アプリケーションは、libc 関数である socket、listen、accept、read/recv を呼び出します。
libdesock は、Linux のダイナミックリンカー機能である LD_PRELOAD を使用して動作します。LD_PRELOAD 環境変数が共有オブジェクトファイルのパスに設定されている場合、ダイナミックリンカーは他のどの共有オブジェクトよりも先に、その共有オブジェクトを読み込むように指示されます。その結果、ダイナミックリンカーは実行ファイルで呼び出されるシンボルを検索する際、まず LD_PRELOAD 環境変数に指定されている共有オブジェクト内のシンボルを検索します。通常、関数 socket、listen、accept、read/recv のシンボルは libc.so 内にあり、そこから実行されます。ただし、LD_PRELOAD が libdesock.so に設定されている場合、代わりに libdesock ライブラリ内のシンボルが検索され、そこで実行されます。これにより、libdesock は呼び出しの動作を変更できます。これらの呼び出しを傍受することで、libdesock は recv/read をネットワークソケットではなく stdin に巧みにリダイレクトします。
複数のリクエスト機能
libdesock コードは、使用されているバッファサイズまでを stdin から読み込むようになっています。このため、先に説明したとおり、入力ファイル(この場合は stdin)から複数のリクエストを読み込もうとした場合に下記の問題が発生します。
「たとえば、1 つのテストケースファイルに 2 つの完全な HTTP リクエストが含まれているとします。µC/HTTP サーバーのバッファサイズは 1,460 バイトであり、2 つのリクエストを合わせた長さは 841 バイトしかありません。つまり、このテストケース全体がバッファ内に収まり、一度にメモリに読み込まれます。µC/HTTP サーバーは最初の HTTP リクエストを処理すると、バッファ内の残りのデータを破棄します。その後、サーバーコードが再度テストケースファイルから読み込んだときには、実際に処理されたのは最初のリクエストのみでも EOF に達することになります」
これに対処するため、読み込み中にリクエストのデリミタを探す機能を libdesock の read コードに追加しました。デリミタが見つかると、デリミタを見つけるまでに読み込んだすべてのデータを返します。次に read が呼び出されると、デリミタの後から読み込みを開始し、次のデリミタを探します。複数のリクエストをファジングする場合、ほとんどのネットワーク アプリケーションではこのアプローチが有効です。その理由は、ネットワーク アプリケーションでは通常、read/recv が呼び出され、データが処理されてから read/recv が再度呼び出されるという処理ループが実行されるからです。このループは、接続が閉じられるまで続きます。最初のブログ記事で取り上げたように、read が 0 を返したときに終了するように、ファジング用の実行プログラムを変更する必要がありました。これにより、実行プログラムがテストケースの処理を完了し、正常に終了したことがわかるため、ファジングにとって非常に重要な変更です。コードを確認したい場合は、こちらで libdesock をご確認ください。
脆弱性のハイライト
テストケースごとに複数のリクエストをサポートするこのバージョンの µC/HTTP サーバーファジングツールでは、さらに 2 件の脆弱性が確認されました。どちらの場合も、最初のリクエストの処理における脆弱性が 2 つ目のリクエストでエクスプロイトされます。
これらの脆弱性を公開したとき、µC/HTTP サーバーのコードベースの多くが複数の製品で共有されていることがわかりました。影響を受けるその他の製品には、Silicon Labs Gecko Platform と Weston Embedded Cesium NET があります。ここで説明しているすべての脆弱性は、シスコの情報開示ポリシーに従って製造元に報告されています。影響を受ける製品の脆弱性は、それぞれ対応する製造元によってパッチが適用されています。
これらの脆弱性に対するエクスプロイト試行は Snort ルール(119:203:1 および 119:282:1)で検出できます。今後、脆弱性に関する新たな情報が追加されるまでの間は、ルールが追加されたり、現行のルールが変更されたりする場合がありますのでご注意ください。最新のルールの詳細については、Cisco Secure Firewall または Snort.org を参照してください。
以下に各脆弱性の概要を示します。
TALOS-2023-1726
HTTP リクエストのプロトコルバージョン部分を処理する際に整数アンダーフローが発生し、接続オブジェクトのバッファ長 p_conn->RxBufLenRem の値が極端に大きくなります。処理ループの次の反復では、receive を呼び出すときにその大きな値が引数として使用されます。つまり、次のリクエストは最大バッファサイズ(1,460 バイト)の制限を受けなくなり、攻撃者は 2 つ目のリクエストで受信バッファを直接オーバーフローさせることができます。
TALOS-2023-1732
この脆弱性では、HTTP リクエストのヘッダーフィールドを処理するときに 1 バイトのバッファオーバーフローが発生します。µC/HTTP サーバーのヒープ実装では、割り当ての最初の 4 バイトに空きメモリチャンクへのポインタが格納されているため、この 1 バイトのオーバーフローは重要です。つまり、1 バイトを上書きするだけで空きチャンクのアドレスが変更され、これが将来割り当てとして使用されることになります。この脆弱性を悪用するには、4 つのリクエストが必要でした。最初の 2 つは無害であり、特定のヒープ割り当てが発生します(「ヒープ操作」と呼ばれる手法)。3 つ目のリクエストで 1 バイトの上書きがトリガーされ、4 つ目のリクエストでヒープの不適切な割り当てが発生します。
どちらの脆弱性も、リクエスト間のグローバルオブジェクトの変更に関係するものです。特定の状況下で関数が呼び出される順序は必ずしも明らかではないため、こうした相互作用は静的コード分析では理解しにくい場合があります。複数リクエストのファジングで明らかにできるこの種の複雑な脆弱性は他にもたくさんあります。このファイルデリミタ手法を使用することで、より多くの脆弱性が発見され、修正されることを願っています。
パート 3 では、μCOS TCP/IP 実装をファジングするために TAP デバイスドライバを作成します。
本稿は 2024 年 08 月 28 日にTalos Group のブログに投稿された「Fuzzing µCOS protocol stacks, Part 2: Handling multiple requests per test case」の抄訳です。