これは、3 部構成から成るシリーズの最後の記事です。このシリーズでは、2 つの µC/OS プロトコルスタック(µC/TCP-IP と µC/HTTP サーバー)のファジングに使用した手法について詳しく説明しています。
パート 1 では、µC/HTTP サーバーに特化したファジングハーネスの開発に必要なコード変更について説明しました。パート 2 では、ファジングテストケースごとに複数のリクエストを処理する手法について説明しました。シリーズ最後の今回は、µC/TCP-IP スタックのファジングに必要なコード変更について詳しく説明します。リアルタイム オペレーティングシステム(RTOS)の説明と、このリサーチプロジェクトを行うことになった動機については、パート 1 を参照してください。
µC/TCP-IP のファジングにおける目標は µC/HTTP サーバーのときと同じで、次のとおりです。
- 最新のファジングフレームワークを使用する(AFL++ を選択しました)。
- ネットワークコードを変更して、ファイルからの入力を受け入れるようにする。
- テストケースごとに複数のリクエストを処理する。
下記の制約を自ら課しました。
- ソフトウェアのみのソリューションとする。
- Linux 上でネイティブに実行する。
エミュレーションや専用ハードウェアに頼らずに、このコードをファジングする簡単な方法を見つけたいと考えました。
Linux への移植
AFL++ ファジングフレームワークを利用するには、このコードを µC/OS ではなく Linux で実行する必要があります。これは、パート 1 で直面したのと同じ課題です。ただし、今回は独自に開発するのではなく、µC/OS が提供する POSIX KAL 実装を使用することにしました。以下のアーキテクチャ図は、µC/OS のさまざまなソフトウェアコンポーネントのレイアウトを示しています。青で表したものは、アプリケーションを Linux に移植するために変更を加えるコンポーネントです。オレンジで表したものは、ファジングツールの対象となるコードです。
µC/HTTP サーバーのファジングハーネスについては、提供されている POSIX KAL 実装を使用しないことにしました。対象としていた µC/HTTP サーバーコードでは、OS コンポーネントをあまり使用していなかったので、独自の最小限の KAL インターフェイスを実装するのは簡単でした。対照的に、µC/TCP-IP の実装では、別のタスクを使用してデータを受信し、セマフォとメッセージキューを使用するので、より完全な KAL 実装が必要でした。
µC/OS が提供するコードを使用するほうが早い気がしました。特に手間がかかることはなく、わずかな変更を加えるだけですぐに動作しました。ネットワーク側の作業についてはこのように順調に事は進まず、かなりの開発が必要となりました。
ネットワーク機能の移植
TAP デバイス
ファジングツールで主に目指したのは、ネットワークデータをファイルから受け入れるようにすることです。まず、µC/HTTP サーバーのときに使用した戦略を採用しました。この戦略では、実際のネットワークデータを使用してプログラムが機能するようにします。最初にこれを行うことで、標準の Linux ユーティリティを使用して実際のネットワークトラフィックをプログラムに送信できるため、コードのデバッグとテストが簡単になります。
µC/TCP-IP で処理するデータをネットワークから取得するのは、µC/HTTP サーバーの場合と比べ少し複雑でした。これは、TCP/IP が HTTP よりも下位のプロトコルだからです。HTTP はアプリケーション層プロトコルですが、TCP/IP はトランスポート層プロトコルです。通常、ユーザーモード アプリケーションは、POSIX ソケットまたは µC/OS 用の NetSock を介して、アプリケーション層でネットワークとやり取りします。
Linux では、ソケットを使用する場合、カーネルが TCP/IP 処理を管理し、アプリケーション層のデータのみをユーザーモード アプリケーションに渡します。ここで課題となったのは、ユーザーモードの µC/TCP-IP アプリケーションで TCP/IP プロトコルそのものを処理することを目的としている点でした。カーネルの TCP/IP スタックをバイパスするために、TAP デバイス(純粋にソフトウェアベースのカーネル仮想ネットワークデバイス)を使用しました。
このモジュールは µC/TCP-IP と呼ばれてはいるものの、実際には OSI モデルの複数のレイヤをカバーしています。オレンジで囲んでいる部分は、µC/TCP-IP モジュールが処理する OSI モデルのレイヤです。
µC/TCP-IP コードはイーサネットフレームの受信を想定しており、ARP、IP、TCP、UDP のハンドラがコードに含まれています。µC/TCP-IP でイーサネットフレームの受信が想定されているということは、TAP デバイスを使用する必要があります。理由は、TUN デバイスが IP パケットを転送するのに対し、TAP デバイスはイーサネットフレームを転送するからです。
TAP デバイスは、自身に接続するユーザー空間プログラムによって使用されることを想定しています。オペレーティングシステムが TAP デバイスにパケットを送信すると、接続されたユーザー空間プログラムにそのパケットが渡されます。逆に、ユーザー空間プログラムから TAP デバイスに送信されたパケットは、外部ソースから送信されたかのようにカーネル ネットワーク スタックに挿入されます。
TAP デバイスを使用するために、µC/TCP-IP が提供する API を使用して独自のイーサネットデバイス ドライバを作成しました。このドライバを使用して TAP デバイスを作成して設定し、データを送受信する関数を実装する必要がありました。繰り返しになりますが、µC/OS は API が明確に定義され、文書化されているため、この作業に最適でした。さらに、µC/TCP-IP リポジトリには、さまざまなイーサネットハードウェア用のデバイスドライバが多数存在します。サンプルコードとドキュメント一式が揃っており、非常に役立ちました。実装にあたっては、µC/TCP-IP イーサネットドライバ内で TAP デバイスを作成および設定するためのガイドとして、オープンソースの tapip ユーティリティを使用しました。
最も混乱したのは、TAP デバイスを含むネットワークトポロジを視覚化する作業でした。TAP デバイスを作成して IP アドレス 10.10.10.1 を割り当て、Linux カーネルではそれに MAC アドレス 46:31:92:b0:48:5b を割り当てました。リモート通信を行うにはネットワークブリッジを作成する必要がありますが、テストにはローカル通信で十分です。作成した TAP デバイスに割り当てられる名前は tap0 で、ifconfig を実行すると次のように表示されます。
µC/TCP-IP アプリケーションに独自の MAC アドレスと IP アドレスを割り当てる必要もありました。これは実質的に、別の仮想イーサネットデバイスを作成するということです。この仮想イーサネットデバイスは別の仮想イーサネットデバイスを介して動作するので、複雑さが増すことになります。µC/TCP-IP アプリケーション内で、IP アドレス 10.10.10.64、MAC アドレス 12:12:12:34:34:34 の仮想イーサネットデバイスを設定しました。この設定は、Linux システムでは外部から確認できず、µC/TCP-IP アプリケーションに独自のものなので、少し混乱します。アプリケーションが別のマシンで動作しているかのように見え、TAP インターフェイスは Linux ホストと µC/TCP-IP アプリケーション間のリンクとして機能します。
µC/TCP-IP の機能を検証する目的で、ポート 10001 で動作するエコーサーバーを開発しました。設定をテストするために、Linux ユーティリティ netcat を使用して µC/TCP-IP アプリケーションへの TCP 接続を確立しました。Linux アプリケーションから tap0 を使用してトラフィックが送信され、µC/TCP-IP 仮想インターフェイスの MAC アドレスまたは IP アドレスが指定されると、µC/TCP-IP アプリケーションで処理されます。テストメッセージを送信して TCP 接続を確立するには netcat コマンド(例:echo ‘test’ | nc -N 10.10.10.64 10001)を実行します。ここで、netcat ツールは tap0 インターフェイスに接続されたソケットを介して動作します。このソケットは Linux カーネルの TCP/IP スタックを使用します。TCP/IP スタックは、まず tap0 のアドレスから ARP リクエストをブロードキャストして、その IP アドレスに対応する MAC アドレスを取得します。次に、µC/TCP-IP アプリケーションがそのブロードキャストメッセージを受信し、リクエストを処理して、独自の MAC/IP アドレスペアで応答します。その後、µC/TCP-IP アプリケーションの MAC/IP アドレスペアを使用して TCP 接続が開始されます。以下の画像は、tap0 インターフェイスに接続された Wireshark を使用してキャプチャしたものです。
µC/TCP-IP アプリケーションは、tap0 インターフェイスとやり取りするときにソケットを使用しません。代わりに、ファイル記述子を使用してデバイスから直接読み書きを行います。その結果、µC/TCP-IP アプリケーションはパケットデータを変更せずに直接ネットワークに書き込みます。次の図は、上記の TCP 接続を確立するために各コンポーネントがどのように連携しているかを示したものです。
ファイルの読み込み
この時点で、netcat を使用して µC/TCP-IP エコーサーバーが動作することを確認しました。ファジングの次のステップは、イーサネットドライバに変更を加え、TAP デバイスではなくファイルから読み込むようにすることです。イーサネットドライバはソケットを使用していませんが、libdesock の使用は可能であり、libc からの読み込みと書き込みを libdesock でオーバーライドできます。ただしこれには、libdesock で LD_PRELOAD を使用することに加えて、いくつかのコード変更が必要でした。まず、libdesock で stdin にリダイレクトするソケット記述子を作成する必要がありました。µC/TCP-IP アプリケーションは tap0 インターフェイスとやり取りする際にソケットを使用しないため、libdesock のデバッグ関数(_debug_instant_fd)を使用しました。この関数が提供するファイル記述子は自動的に stdin にリダイレクトされます。
通常、libdesock は accept が呼び出されたときにファイル記述子をリダイレクトしますが、これは下位レベルで行われるため、_debug_instant_fd の呼び出し時に libdesock が提供するファイル記述子を使用する必要がありました。ファジング環境を構築するために、TAP デバイスを作成するのではなく、コンパイル時のフラグを使用して次のような libdesock ファイル記述子を作成しました。
次に、ファイルから最後のリクエストが読み込まれたら µC/TCP-IP エコーサーバーを終了するコードを追加しました。
このようにコードを変更した後、libdesock で LD_PRELOAD を使用すると、アプリケーションが stdin からリクエストデータを読み込むようになります。libdesock に機能を追加して利用することで、複数のリクエストをすぐに処理することができました。libdesock の動作の詳細については、このシリーズのパート 2 を参照してください。
メモリエラー
ファジングアプリケーションでは、Address Sanitizer(ASAN)を使用することにしました。解放後のメモリ使用、NULL ポインタの逆参照、バッファオーバーランなど、さまざまな種類のメモリエラーを検出するように設計されているからです。µC/TCP-IP で使用されているヒープ実装は、µC/HTTP サーバーでの実装とは異なりますが、このシリーズのパート 1 で説明したのと同じ手法を使用できました。
脆弱性のハイライト
こうして、µC/TCP-IP コードに変更を加えることにより、ソフトウェアのみで動作し、Linux ネイティブで、ファイルからの入力を処理できるファジングソリューションを開発することに成功しました。ここで説明したアプローチにより、2 件の固有の脆弱性 TALOS-2023-1828 および TALOS-2023-1829 が見つかりました。このうち 1 件は、一度に複数のリクエストをファジングした結果、発見されたものです。
これらの脆弱性を公開したとき、µC/TCP-IP のコードベースの多くが複数の製品で共有されていることがわかりました。Silicon Labs Gecko Platform も TALOS-2023-1828 の影響を受けます。ここで説明しているすべての脆弱性は、シスコの情報開示ポリシーに従って製造元に報告されています。影響を受ける製品の脆弱性は、それぞれ対応する製造元によってパッチが適用されています。
これらの脆弱性に対するエクスプロイト試行は Snort ルール(119:201、116:2、116:151)で検出できます。
まとめ
µCOS プロトコルスタックのファジングに関するこのシリーズをご覧いただき、ありがとうございます。要点をまとめると、µC/HTTP サーバーと µC/TCP-IP をファジングするためにソフトウェアのみのファジングソリューションを作成しました。今回説明したファジングソリューションには、Linux 上でネイティブに動作し、ファイルからの入力を受け入れる機能を持たせています。また、libdesock に追加した機能について説明しました。これにより、アプリケーションが 1 つのテストケースで複数のリクエストを処理できるようになり、より複雑な脆弱性の検出が可能になりました。このシリーズで説明した手法をきっかけに、他の RTOS プラットフォームのファジングを実施し、これらの重要なシステムのセキュリティを高めようとする人たちが現れることを願っています。
以下は、このブログシリーズで紹介した調査に基づいて公開された脆弱性のリストです。
- TALOS-2023-1725:境界外書き込みの脆弱性
- TALOS-2023-1726:バッファオーバーフローの脆弱性
- TALOS-2023-1732:メモリ破損の脆弱性
- TALOS-2023-1733:ヒープベースのバッファオーバーフローの脆弱性
- TALOS-2023-1738:メモリ破損の脆弱性
- TALOS-2023-1746:メモリ破損の脆弱性
- TALOS-2023-1828:サービス妨害の脆弱性
- TALOS-2023-1829:二重解放の脆弱性
- TALOS-2023-1843:ヒープベースのバッファオーバーフローの脆弱性
これらの脆弱性に対するエクスプロイト試行は Snort ルール(119:201、119:281、1:12685、1:39908、119:203:1、119:282:1、116:2、116:151)で検出できます。今後、脆弱性に関する新たな情報が追加されるまでの間は、ルールが追加されたり、現行のルールが変更されたりする場合がありますのでご注意ください。最新のルールの詳細については、Cisco Secure Firewall または Snort.org を参照してください。
本稿は 2024 年 08 月 28 日にTalos Group のブログに投稿された「Fuzzing µC/OS protocol stacks, Part 3: TCP/IP server fuzzing, implementing a TAP driver」の抄訳です。