Cisco Japan Blog

WTF を活用した Direct Composition のスナップショットファジング

1 min read



  • Cisco Talos は、Windows の Direct Composition を対象とする人気のスナップショット ファジング ツール「WTF」を使用して、カスタマイズしたファジングツールを開発しました。
  • Talos の脆弱性調査チームは Google 社が開発した Protocol Buffer を使用して、ユースケースをシリアル化および逆シリアル化しました。
  • スナップショットファジングが正しく機能するよう、WTF の Bochscpu バックエンドにパッチを適用したほか、他の技術も活用しています。
  • スナップショットファジングの実装の詳細を公開したことで、新しいスナップショットファジングのアイデアが皆さんに伝わり、より安全に Direct Composition を使えるようになっていただければと考えています。

Direct Composition は Windows 8 で初めて導入された機能で、カーネルオブジェクトとして抽象化された変換、効果、アニメーションを含む高性能ビットマップ合成を実現します。これらのカーネルオブジェクトは後でシリアル化され、DWM(デスクトップ ウィンドウ マネージャ)に送信されて画面にレンダリングされます。

カーネルオブジェクトはシステムコールを使って直接作成および操作できるため、格好の攻撃対象となります。研究者たちは Pwn2Own のようなコンテストpopup_iconの場で、Direct Composition の脆弱性がどのようにエクスプロイトされるかを実演してきました。

Direct Composition に関しては、公的研究は行われているものの、当該機能のファジングについて論じたものはごくわずかです。スナップショットファジングに至っては、私たちが知る限り全く取り上げられていません。そこで、スナップショットファジングを Direct Composition に適用し、公開されている WTF スナップショット ファジング ツールを使った新たなファジングツールを導入する方法を検討しました。

Direct Composition のファジングに関する先行研究

Qihoo 360 Vulcan チームのセキュリティ研究者 Peng Qiu 氏と SheFang Zhong 氏は、「Win32k Dark Composition:Attacking the Shadow Part of Graphic Subsystem」という会議で Direct Composition の脆弱性に関する研究を初めて発表しました。この研究では、Direct Composition 関連のコードをトリガーし、最終的にファジングする方法が説明されています。公開されたコードをファジングすることにより、セキュリティ研究者は、それまで公式には解析されたことがなかった Direct Composition に潜む潜在的な脆弱性を発見できるようになりました。

最近では 2023 年の HITB カンファレンスpopup_iconで、Hillstone Networks 社の研究者が、ファジングフレームワーク syzkallerpopup_icon の改造版を使って Direct Composition をファジングした方法について講演しました。まず、コードカバレッジを使用してカーネルコンポーネントをファジングします。次に、生成されたコーパスをもとに、各テストケースの最後に、ユーザーランド DWM によるデータ処理をトリガーする NtDCompositionCommitChannel への呼び出しを追加します。こうして変更したコーパスで再びファジングしたところ、DWM に脆弱性が見つかったのです。ただ、syzkaller は完全なシステム仮想化環境で動作しますが、スナップショット ファジング ツールではないため、このアプローチでは DWM のカバレッジは収集できませんでした。

スナップショットベースのファジングツール「What The Fuzz」

WTFpopup_icon(「what the fuzz」)は Axel Souchet 氏という研究者が開発した、Windows を対象とするスナップショットベースのファジングツールです。このツールがリリースされるとすぐに、研究者はさまざまな対象をファジングし始めました。WTF はスナップショットを実行するバックエンドに複数対応しています。Bochscpu、Windows Hypervisor Framework、KVM などです。さらに hongfuzz や libfuzzer といったミューテータにもデフォルトで対応しています。

研究者はファジングする前に、メモリスナップショットとカスタマイズしたファジングツールを作成します。このファジングツールはテストケースを変異させ、ロードしたスナップショットのゲストメモリ空間にコピーし、クラッシュ、コンテキストの切り替え、スナップショットの復元を検出する戦略的なブレークポイントをインストールします。後は WTF が処理してくれます。

Talos は WTF を使用して潜在的に重大性の高い対象をファジングすることにも興味を抱きました。そこで、これまでの研究を基に WTF を使った Direct Composition のファジングについて検討することにしました。

Direct Composition のファジングにおける課題

Direct Composition はカーネルコンポーネントとユーザーランド コンポーネントで構成されます。ユーザーはシステムコールを実行してカーネルコンポーネントとやり取りできます。カーネルを対象にしたファジングの場合、360 Vulcan Team による研究が示していたように、文法をカスタマイズすれば、従来の Windows システムコールファジングを使用できます。

ただし、ユーザーランド コンポーネントをファジングするのは少し厄介です。システムコールによって作成される Direct Composition に関連する内部カーネルオブジェクトは、NtDCompositionCommitChannel システムコールが実行されると、シリアル化されてからユーザーランド DWM に送信され処理されます。ファジングツールは、カーネルから送信されたデータの処理中に dwm.exe がクラッシュしたかどうかを検出する必要があります。ですが、この検出処理が、スナップショットファジングの観点で考えると厄介なのです。ゲスト内部で実行されるファジングハーネスは、dwm.exe プロセスとは異なるアドレス空間に存在します。よって WTF はゲストのファジングハーネス自体のクラッシュ(ファジングツール自体のバグ)は把握できますが、dwm.exe プロセスのクラッシュは検出できません。回避策として、テストケース実行(PID の変更など)のたびにゲストのファジングハーネスに dwm.exe の状態をモニタリングさせ、ハーネス自体をクラッシュさせて WTF に通知するという方法があります。ただしこれは非常に効率が悪く、特に遅いバックエンド(Bochscpu など)の場合には使えない方法です。

別の問題は、カーネルコンポーネントとユーザーランド コンポーネントの実行の間にコンテキストが切り替わるものの、dwm.exe がデータを処理するまでは無視してファジングを継続する必要があるということです。たとえば Bochscpu バックエンドの場合、CR3 の値が変更されたかどうかをチェックし、変更された場合はスナップショットを自動的に復元してテストケースの実行を終了します。

Talos のファジング手法

Talos は、Direct Composition のスナップショットファジングの課題を克服するために工夫を凝らし、WTF を使用して Direct Composition 用にカスタマイズされたファジングツールを開発しました。次の図は、WTF を使ったファジング手法の仕組みを説明したものです。

カスタムファジングツールの各段階については、次のセクションで詳しく説明します。この図で説明されているすべてのコンポーネントは WTF の一部です。

テストケースの生成と変異

Dcgen(Direct Composition ジェネレータ)にはさまざまなコードジェネレータが実装されています。各ジェネレータは、関連する Direct Composition システムコールにマッピングされる dcpreter(Direct Composition インタープリタ)というインタープリタ向けの命令を生成します。WTF が新しいテストケースを要求すると、一連のコードジェネレータがランダムに選択され、新しいテストケースが生成されます。テストケースはゲスト内で実行される dcpreter によって解釈されます。

コードジェネレータの実行中に新たに生成された命令は、新しいリソースがハンドル(その後の参照用に保存する必要があり、再利用してリソースを作成してはならない)を使って生成されていることなどの副次的な影響を把握している抽象的インタープリタで解釈されます。解釈された情報は保存され、後でコードジェネレータが決定を下す場合に使用されます。次のコードは、NtDCompositionCreateChannel システムコールに関連する命令を生成するためのシンプルなコードジェネレータの例です。

まず、新しい命令を現在のテストケースに追加し、命令のオペコードを OP_CREATE_CHANNEL(次のセクションで説明するプロトコルバッファファイルで定義されたオペコード)として設定します。これは、NtDCompositionCreateChannel が解釈されるときに dcpreter によって呼び出されます。

さらに複雑な例もご紹介しましょう。

リソースマーシャラで整数プロパティを設定するための命令を生成

このコードジェネレータは、特定のリソースマーシャラがサポートする整数プロパティタイプをランダムに選択して値を設定する命令を生成します。まず、テストケースがスナップショットで順次実行されるときに使用できるリソースハンドルをランダムに選択し、ハンドルが表す特定のリソースタイプに関する情報を格納する CMarshaler オブジェクトへのポインタを取得します。このオブジェクトを使用すると、dcgen はどの整数プロパティタイプが有効かを認識し、その中から 1 つを選択します。

変異の処理は、生成と非常によく似ています。dcmutate(Direct Composition ミューテータ)は最初、命令を生成するのではなく、テストケース内の命令を順番に抽象解釈します。しかし、この処理の途中で、ミューテータは現在処理している命令を変異させるかどうかをランダムに決定します。変異させることにした場合、命令を変異させ、変異した命令を抽象的に解釈します。その後、テストケース内の残りの命令を引き続き処理します。変異としては、命令のオペランド(システムコールの引数)の変更や、まったく別のタイプの命令への置き換えなどが行われます。

整数プロパティを設定する命令を変異

これで、変異された命令の一部を表すオブジェクトへのポインタが取得されます。dcmutate は、このオブジェクトから参照されるフィールドを変更して命令内のオペランドを変更します。この例では、ミューテータは、整数プロパティタイプを変更する、整数プロパティタイプと代入値を変更する、代入値を変更するという 3 つのケースの中からランダムに選択します。

テストケースのシリアル化と逆シリアル化

Talos は、テストケースをゲストに渡すために、Protocol Buffers を使用してテストケースをシリアル化および逆シリアル化し、Direct Composition の各システムコールをインタープリタ命令でモデル化する「inst.proto」ファイルを定義しました。このファイルに Protocol Buffers で定義されたメッセージフォーマットは、テストケース内で命令を生成および変更するときに使用します。

dcpreter の命令を定義する proto ファイル

先に説明したように、dcpreter はゲスト内で実行され、Protocol Buffers ライブラリを使用してテストケースを逆シリアル化し解釈を開始します。

メインの解釈のループ

上に挙げた長い switch ステートメントを見れば、各命令がどのように解釈されるかがわかります。OP_CREATE_CHANNEL では、ここで実際に NtDCompositionCreateChannel が呼び出され、作成されたチャネルのハンドルが保存されていることがわかります。

Bochscpu バックエンドへのパッチ適用

NtDCompositionCommitChannel を呼び出すと、シリアル化されたカーネルデータ構造がユーザーランド DWM に送信され、さらに処理が行われます。この処理中にコンテキストの切り替えが発生する可能性があります。発生すると、前のセクションで説明したように、Bochscpu エミュレーションが停止してスナップショットが復元されます。これは Direct Composition のカーネルコンポーネントだけをファジングするのであれば問題ありませんが、dwm.exe もファジングしたいのであれば問題になります。

この問題を回避するには、CR3 の変更を検出してエミュレーションを停止する Bochscpu バックエンドのコードを変更します。このためには、Bochscpu がいつ元のプロセスコンテキストに切り替わったかを知るために現在の CR3 の値を追跡する必要があります。

CR3 の変更の無視

TlbControlHook では、最初の CR3 と新しい CR3 の値が一致しない場合、スナップショットを復元するのではなく、新しい CR3 の値を保存してエミュレーションを続行します。

CR3 が一致しない場合にはブレークポイントを無視

BeforeExecutionHook は現在の命令ポインタにブレークポイントが設定されているかどうかをチェックし、それに応じてブレークポイントハンドラをディスパッチします。ブレークポイントは初期の CR3 値が存在する場合には仮想アドレスとして保存されるため、bochscpu が異なるコンテキストで実行されている場合、保存されたブレークポイントのアドレスは無効になります。そのため、現在の CR3 と初期の CR3 の値が一致しない場合は何もしないというチェックが追加されています。

エッジカバレッジのトラッキングのオーバーヘッドを減らすため、現在の CR3 がスナップショット生成時の CR3 と一致しない場合、ファジングに直接関係しないコードのトラッキングを回避するためにエッジカバレッジをログに記録しないようにするパッチを作成しました。

エッジの記録を無視

最後にスナップショットを復元するときには、現在の CR3 値が格納されている変数を初期の CR3 に復元する必要があります。

CR3 を復元

dwm.exe への dcpreter の挿入

ユーザープロセスのクラッシュを検出するには、ユーザーモードのクラッシュハンドラにブレークポイントを設定する必要があります。しかし、dcpreter.exe が別のプロセスとして実行され、そのプロセスコンテキストでスナップショットが作成された場合、WTF は dwm.exe のクラッシュをどのように検出するのでしょうか?

この問題を解決するには、dcpreter を dwm.exe と同じプロセスコンテキストで実行しなければなりません。これを実現するため、dcpreter を DLL として実装し、インジェクタを使用して dwm.exe に挿入します。dcpreter がメモリからテストケースの逆シリアル化を開始する前に、スナップショットが作成されるようにします。また、dwm.exe コンテキストのユーザーモードのクラッシュハンドラにブレークポイントを設定します。これで、dwm.exe がクラッシュすると、ファジング中にブレークポイントがトリガーされるようになります。

ただし、解決しなければならない実装関係の問題は他にもありました。dcpreter に静的にリンクされている Libprotobuf は内部でスレッドローカルストレージ(TLS)を使用しているため、dwm.exe に挿入されると dcpreter がクラッシュしてしまいます。特定のマクロ GOOGLE_PROTOBUF_NO_THREADLOCAL を定義して TLS の使用を無効にすることはできますが、その場合は Windows に対応していない pthread を使用することになります。これは、pthread-win32 と呼ばれる pthread ライブラリの Windows ポートを使用してコンパイルすることで解決できます。

ブレークポイントの設定

WTF には、ユーザーランドのクラッシュが発生したときにトリガーされるブレークポイントを設定する SetupUsermodeCrashDetectionHooks() という関数があります。カスタムファジングツールの初期化段階でこの関数を呼び出すと、dwm.exe のクラッシュを検出し、テストケースを保存したうえですぐにスナップショットを復元してファジングを続行できます。

さらに、単一のテストケースの実行が終了したことを示す適切なブレークポイントを設置する必要があります。そうしないと、テストケースが常にタイムアウトすることになり、ファジング全体の速度が低下します。設置するのは dwmcore.dll 内部で、カーネルからのデータ処理が終了した場所でなければなりません。公開されている脆弱性のコールスタックを調べたうえで、dwmcore!CKernelTransport::DispatchBatches の末尾に withsetting ブレークポイントを設定することにしました。

その他

ファジングの設定をテストしているときに、D3D10Warp!ProcessorThreadSpecificData::ExecuteProgram_JIT という関数がスナップショット内で実行されると必ずクラッシュすることに気づきました。WTF のバックエンド SimulateReturnFromFunction() を使用して、実際には何もせずにこの関数から戻ることでスナップショットでクラッシュすることはなくなりました。

結果

カスタムファジングツールを使用して Direct Composition をファジングし、生成されたテストケースを使用して dwm.exe のクラッシュを検出することに成功しました。

各テストケースのサイズは実行されるシステムコールの数と関連しているため、実行速度に大きく影響します。システムコールでは、Direct Composition のファジングとは直接関係のない多数のマシン命令が Bochscpu でエミュレートされます。パフォーマンスを向上させるにはこれを減らす必要があります。パッチが適用されていないサービス妨害のカーネルバグがいくつか存在しており、システムコールの小さな組み合わせによってトリガーできる状態になっています。このため、テストケースが大きければテストケースが途中で終了する可能性はそれだけ高くなります。Talos が開発したファジングツールは意図したとおりに機能していますが、改善の余地はまだ多く残されています。

今後の研究

バッチ処理

Direct Composition には、いわゆる「バッチコマンド」と呼ばれるコマンドでしか実行できない機能がいくつかあります。たとえばカーネルオブジェクト内でのプロパティ値の設定などです。バッチコマンドは NtDCompositionProcessChannelBatchBuffer を呼び出すことで処理されます。複数のバッチコマンドを 1 つのシステムコールで実行できるため、カーネルとユーザーランド間の切り替えのボトルネックが軽減されます。現時点では、ファジングツールを簡単に実装できるようにするため、すべてのバッチ処理システムコールには 1 つのバッチコマンドしか含まれていません。1 つの NtDCompositionProcessChannelBatchBuffer 呼び出しで複数のバッチコマンドを実行できるようにすれば、テストケースを生成および変異できるため、ファジング速度は大幅に向上します。

競合状態を対象に入れる

Direct Composition で使用されるマッピングされた共有メモリは、カーネルおよびユーザーランドから操作できるため、競合状態に対して脆弱である可能性があります。Hillstone Networks 社の研究者によって発見されたいくつかの脆弱性がこれに該当します。Talos のファジングツールはこうしたテストケースを生成できますが、特に競合状態を対象とするテストケースの生成にも役立つというのは興味深いことです。

変異の高速化と改良

基本的な変異はすでに利用可能ですが、改良の余地があります。たとえば一部のリソースタイプは構造化データをプロパティ値として受け取りますが、その生成および変異の仕組みはファジングの品質に影響します。リソースの種類が非常に多いため、精度を向上させるにはさらに多くの作業が必要です。さらにテストケースの各命令は抽象的に再解釈されるため、変異処理そのものが非常に重くなります。各命令の抽象的な解釈を減らすことができれば、全体的なパフォーマンスも大幅に向上するでしょう。

メンテナンス

新しい Windows ビルドがリリースされると、Direct Composition コンポーネントが更新される可能性があります。削除または追加されたリソースタイプをチェックするのは手間がかかる作業です。利用可能なリソースタイプとそのプロパティを自動抽出できれば、ファジングツールのメンテナンスはとても楽になるでしょう。現在リソースマーシャラは 200 種類以上あり、手作業でのメンテナンスにはかなりの労力が求められます。一般的な逆アセンブラでスクリプトを使用することがこの研究に役立つかもしれません。

 

本稿は 2023 年 10 月 17 日に Talos Grouppopup_icon のブログに投稿された「Snapshot fuzzing direct composition with WTFpopup_icon」の抄訳です。

 

コメントを書く