- Cisco Talos は、市販のハードウェア上で macOS のソフトウェアをテストできるファジングツールを開発しました。
- スナップショットベースのファジングアプローチを利用した、WhatTheFuzz フレームワークを基盤とするファジングツールです。
- VM 状態抽出のサポートが実装され、WhatTheFuzz で VMware 仮想マシンのスナップショットのロードに対応できるようになりました。
- その他、ファジングのトレースのシンボル化とコードカバレッジ分析をサポートするツールもあります。
目次
斬新で特殊な脆弱性を発見するためには、多くの場合、その作業に最も適した特殊なツールを開発する必要があります。通常、使用できるツールや手法は、対象ソフトウェアを実行するプラットフォームやハードウェアによって決まります。特に macOS オペレーティングシステムやカーネルの構成要素については、ソースコードが非公開(クローズドソース)で、高度なデバッグ、イントロスペクト、インストゥルメンテーションに対応したツールがないため、こうした傾向が顕著です。
ほとんどのコードがオープンソースの Linux 上でソフトウェアの脆弱性をファジングするのに比べ、macOS 上のものを対象とするとなると、多少の難しさが伴います。クローズドソースなので、コンパイル時のインストゥルメンテーションは使えません。Dynamorio や TinyInst のような Dynamic Binary Instrumentation ツールは macOS 上で動作しますが、カーネルコンポーネントのインストゥルメンテーションには使えません。
ハードウェアにも懸念事項があります。macOS が動作するのは Apple のハードウェア上だけであり、例外はほとんどありません。もちろん仮想化は可能ですが、このやり方には欠点があります。macOS のコードのテストには市販のサーバーを使えないのです。また、ラップトップ上でのファジングは必ずしも効果的とは言えません。
Talos は、しばらく前からこうした問題の大半の軽減を目指すプロジェクトに取り組んできており、本日、そのコードを公開しています。
スナップショットベースのアプローチを使うことで、カスタムハーネスなしで、正確に対象を絞ってクローズドソースのコードをファジングできるようになりました。エミュレータでテストを実行すれば完全なインストゥルメンテーションとコードカバレッジを入手できるので、既存のハードウェア上でテストを行えます。このアプローチは、Intel のハードウェア上で動作する macOS のテストにしか使えませんが、コードの大半は Intel バージョンと ARM バージョンで共有されています。
スナップショットファジングのこれまで
対象のアプリケーションをファジングする最もシンプルな方法は、入力を変えながらループで実行することです。ただ、このやり方には明らかな欠点があり、アプリケーションの初期化や定型コードの実行に時間がかかり、コードの関連部分の実行に使える CPU 時間が減ってしまいます。
スナップショットベースのファジングアプローチでは、ファジングテストケースを挿入するプロセスの実行ポイント(重要な関数のエントリポイント)を定義します。次に、ブレークポイントなどの手段でプログラムを特定のポイントで中断させ、スナップショットを取得します。このスナップショットには、使用されたすべての仮想メモリと、プロセス実行を復元して再開するのに必要となる CPU やその他のプロセスの状態が含まれています。その後、メモリを変更してファジングテストケースを挿入し、実行を再開します。
あらかじめ定義していたシンク(関数の最後、エラー状態など)に実行が到達したら、プログラムを停止して状態を破棄し、事前に保存していた状態と入れ替えます。
プロセスを前の状態に復元する手間が増えるだけで、1 からプロセスを作成せずに済むのが、この手法のメリットです。さらに、コピーオンライト、ダーティページトラッキング、オンデマンドページングなど、OS や CPU のメカニズムを利用できるとしましょう。そうなれば、プロセス復元の作業は非常に短時間で済むので、ファジングの全体的なスピードにはほとんど影響を及ぼしません。
Cory Duplantis 氏は、かつて Talos が Barbervisor を開発し、スナップショットベースのファジングの活用を試みた際、これを主導した人物です。同氏の研究成果である Barbervisor は、高性能なスナップショットファジングをサポートするために開発されたベアメタルハイパーバイザです。
当時の試みには、完全な(Virtual Box ベースの)VM のスナップショットを取得し、Barbervisor に移植して実行することも含まれていました。高い性能を実現するために、Intel CPU の機能を活用して、変更されたメモリページのみを復元するという手法を採用していました。
スナップショットベースのファジングの潜在的な有用性が垣間見える、大きな可能性を示す試みでしたが、欠点もいくつかありました。同様のアプローチで KVM 上に構築され多くの改良を加えたものが Snapchange に実装され、AWS Labs によってリリースされています。
スナップショットファジングの構成要素
Talos が Barbervisor を公開した頃、Axel Souchet 氏は、また別のアプローチを取る WTF プロジェクトを公開しました。WTF では、既存のツールを活用し、パフォーマンスと引き換えに整った開発環境を保ちます。Hyper-V で仮想マシンを実行し、スナップショットを取得してから、kd(Windows カーネルデバッガ)でそのスナップショットを実行します。なお、状態は Windows のメモリ ダンプ ファイル フォーマットで保存されますが、これはロードに最適な形式です。WTF は C++ で書かれているため、カスタムミューテータやファズジェネレータなど既存のサポートライブラリを活用できます。
実行可能なバックエンドはいくつかありますが、最も機能が充実しているのは x86 エミュレータの Bochs を基盤とするもので、包括的なインストゥルメンテーション フレームワークとなっています。ネイティブな実行より遅くなるため、パフォーマンスの低下を感じることになるはずですが、特別なハードウェア要件なしで、Bochs が動作するあらゆるプラットフォーム(Linux、Windows、仮想マシンなど)で実行できます。
WTF の最大の欠点は、主に Windows の仮想マシンや Windows 上で実行されているソフトウェアを対象として設計されていることです。
WTF を macOS 上の対象ソフトウェアのファジングに対応できるように変更するとなると、元の状態ではサポートされていないメカニズムを処理しなければなりません。ファジング処理前とファジング処理中の段階に分けて列挙してみましょう。まず、ファジング処理前の段階で対処が必要なメカニズムは以下のとおりです。
- ファジング対象の OS とプロセスをデバッグするメカニズム – スナップショットを作成するポイントを的確に選ぶために必要です。
- 物理メモリのコピーを取得するメカニズム – 実行をエミュレータに移植するために必要です。
- CPU の状態のスナップショットの作成 – スナップショットには、すべての制御レジスタ、すべての MSR のほか、汎用レジスタではない CPU 固有のレジスタが含まれている必要があります。
一方、ファジング処理中の段階で必要なものは以下のとおりです。
- 取得したメモリページを復元するメカニズム – 使用する環境に合わせてカスタマイズする必要があります。
- クラッシュを検知する方法 – これが必要となるのは、Windows と macOS ではクラッシュ/違反発生のメカニズムが大きく異なるためです。
CPU の状態、メモリの変更、カバレッジ分析にも調整が必要です。
デバッグ
macOS カーネルを対象とするなら、実際の物理的なマシンのスナップショットを取得したいところです。そうすれば、特別なハードウェアにロードして設定する必要があるカーネル拡張機能をすべて備えた、最も正確な攻撃対象領域を入手できます。仮想化した macOS では、攻撃対象領域が大幅に縮小してしまいます。
しかし、物理的な Mac マシンのデバッグは厄介です。最低でももう 1 台のマシンと複数の特別なネットワークアダプタが必要になります。そのうえ、デバッグのメカニズムも、私たちの目的に完全にはマッチしません。ブレークポイントではなくマスク不能割り込みを使うしかないため、カーネルのコード実行を完全には止められないのです。
これに比べれば、仮想マシンのデバッグは多少簡単です。VMware V5 には、基盤のオペレーティングシステムを問わず使用できる gdbserver スタブが含まれています。VMware のスナップショット作成機能を利用することもできます。
VMware のデバッガスタブは .vmx ファイルで有効化します。
debugStub.listen.guest64 = "TRUE" debugStub.hideBreakpoints = "FALSE"
1 つめのオプションでは、デバッガスタブを有効化しています。2 つめのオプションでは、GDB スタブにハードウェア ブレークポイントではなくソフトウェア ブレークポイントを使用するように指示しています。ハードウェア ブレークポイントは、VMware V5 ではサポートされていません。
VM に接続してデバッグするには、GDB のリモートプロトコルを使用します。
$ lldb (lldb) gdb-remote 8864 Kernel UUID: 3C587984-4004-3C76-8ADF-997822977184 Load Address: 0xffffff8000210000 ... kernel was compiled with optimization - stepping may behave oddly; variables may not be available. Process 1 stopped * thread #1, stop reason = signal SIGTRAP frame #0: 0xffffff80003d2eba kernel`machine_idle at pmCPU.c:181:3 [opt] Target 0: (kernel) stopped. (lldb)
スナップショットの取得
スナップショットファジングの 2 つめの重要な要件は、もちろんスナップショットの作成です。これにも VMware V5 を利用することができます。
VMware のスナップショットを作成する一般的な方法は、VM を一時停止させるか、復旧できる状態の正確なコピーを作るかのどちらかです。これは、私たちのやりたいこととほぼ同じです。
VMware のデバッガでブレークポイントを設定し、実行がそこまで到達するのを待ちます。ブレークポイントに到達すると、仮想マシン全体の実行が一時停止されます。そうしたら、狙った命令のポイントで一時停止したマシンの状態のスナップショットを取得できます。このやり方なら、時間を測定したりセンチネル命令を挿入したりする必要はありません。デバッグしているのは VM なので、完全に制御できます。やや難しいのは、このスナップショットをどう使うかです。再利用するには、VMware V5 がスナップショットを保存しているファイルフォーマットを理解する必要がありました。
VMware V5 のスナップショットは、2 つのファイルで構成されています。メモリの状態を格納している vmem ファイルと、デバイスの状態(CPU、すべてのコントローラ、バス、PCI、ディスクなど)を格納している vmsn ファイルです。この 2 つのファイルさえあれば、VM を復元できます。
まずメモリダンプに関してですが、vmem ファイルは VM の RAM 全体をそのままダンプしたものです。VM の RAM が 2GB なら、vmem ファイルは RAM のコンテンツをバイト単位でコピーした 2GB のファイルになります。これは物理的なメモリレイアウトです。私たちが扱っているのは仮想マシンなので、解析は必要はなく、ローダーがあれば大丈夫です。
これに対し、マシン状態を格納したファイルでは、かなり複雑で文書化もされていないフォーマットが使用されています。このファイルには、関係のない情報も大量に含まれます。完全な VM を復元するつもりはなく、それなりの量のコードを実行できれば十分なので、注意するのは CPU の状態だけです。文書化はされていないものの、Volatility プロジェクトのために大半がリバースエンジニアリングされています。Volatility を拡張すれば、WhatTheFuzz で使用できるフォーマットの CPU 状態のダンプを入手できます。
スナップショットの WTF へのロード
2 つのファイルフォーマットについて把握できたところで、WTF の変更に話を戻します。必要かつ最も重要な変更は、物理的なメモリローダーに対する変更です。
WTF は、Windows の dmp ファイルフォーマットを使用しているため、独自のハンドラが必要になります。メモリダンプファイルは、物理的な RAM をそのまま 1 対 1 でコピーしただけのものなので、これをメモリにマッピングしたうえでページをマッピングするのは非常に簡単です。以下の抜粋を見ていただければわかるでしょう。
あとは、適切なオフセットで構造体を偽造するだけです。
クラッシュの検知
残る問題はあと 1 つ、クラッシュをどう検知するかです。WTF や、Talos による改良版の場合、適切な場所にブレークポイントを設定するだけで検知できます。Windows では nt!KeBugCheck2 のフックがベストであり、macOS カーネルでも同様のポイントが必要になります。
macOS 上でカーネルパニック、例外、違反などが発生すると、複雑なコールスタックを経て、最終的に OS が完全にクラッシュし、再起動に至ります。
検知したいクラッシュのタイプや実行しているカーネルのタイプにもよりますが、ブレークポイントは、exception_triage 関数に設置できます。この関数は、違反が発生してからマシンでカーネルパニックや再起動が起こるまでの間の実行パスに存在します。
これで、macOS カーネルを対象にファジングを行ううえで必要な要件がすべて揃ったことになります。
ケーススタディ:IPv6 スタック
macOS の IPv6 スタックは、スキーム全体の仕組みを説明するのにちょうどよい例です。単純ですが、複雑なコードを見る前の手始めとしては悪くありません。攻撃対象領域は一連の複雑なプロトコルから成り、ネットワーク経由で到達できるステートフルなものです。ネットワーク経由のファジングは時間がかかるため、既存のファジングツールでファジングするのは難しく、カバレッジは入手できないでしょう。そのうえ、macOS カーネルのこの部分はオープンソースなので、意図したとおりに動くかどうかを簡単に確認できます。まず、対象となる仮想マシンを用意する必要があります。
VM の準備
想定する前提は以下のとおりです。
- ホストマシンは、macOS 12 Monterey を実行している MacBook
- 仮想化プラットフォームは VMware V5
- macOS 12 Monterey が動作するゲスト VM のスペックは以下のとおり
- SIP はオフ
- RAM は 2 GB または 4 GB(4 GB が望ましいが、スナップショットのサイズが大きくなる)
- CPU/コアは 1 つ(マルチスレッディングにしても複雑になるだけなので)
VM 上でデバッグを行おうとしている以上、何をするより先に SIP を無効にするのが賢明でしょう。
VM のデバッグに、Apple の KDP ではなく VMware の GDB スタブを使うのは、その方が VM 実行への干渉が少ないからです。VM の側は、GDB が有効化されていることを感知しません(できません)。
有効化は簡単で、VM の .vmx ファイルを編集するだけです。VM パッケージでファイルを探し、次の行を最後に追加します。
debugStub.listen.guest64 = "TRUE" debugStub.hideBreakpoints = "FALSE"
デバッグを簡単にし、手間を減らすために、macOS のブートオプションをいくつか変更しておきます。SIP を無効化しているので、通常の(昇格した)ターミナルからできるはずです。
$ sudo nvram boot-args=”slide=0 debug=0x100 keepsyms=1″
上記のコードで、macOS のブート引数を次のように変更します。
- slide=0 でブート時の KASLR を無効化
- debug=0x100 でウォッチドッグを無効化し、カーネルパニックの際に VM が自動的にリブートするのを防止
- keepsyms=1 は、上の項目との組み合わせにより、カーネルパニック時にシンボルを出力
macOS カーネルの KASAN ビルドのセットアップは、実際にファジングする場合には不可欠な手順ですが、テスト目的であれば必ずしも必要ありません。
対象の関数
ファジングの対象とするのは、ip6_input 関数です。これは、IPv6 の着信パケットを解析するためのエントリポイントです。
パラメータは 1 つで、実際のパケットデータを保持する mbuf を格納します。このデータを、ipv6_input をファジングするために変換して変更します。
mbuf 構造体は XNU の標準的な構造体であり、基本的には、データを含むバッファのリンクリストです。実行を再開する前に、実際のパケットデータの在処(mh_data)を見つけ、それを変換する必要があります。
つまり、WTF のファジングハーネスでポインタを逆参照して実際のパケットデータにアクセスしなければなりません。
スナップショットの作成
スナップショットを作成するために、デバッガを使って ip6_input 関数にブレークポイントを設定します。これが、ファジングを開始するポイントです。
Process 1 stopped * thread #2, name = '0xffffff96db894540', queue = 'cpu-0', stop reason = signal SIGTRAP frame #0: 0xffffff80003d2eba kernel`machine_idle at pmCPU.c:181:3 [opt] Target 0: (kernel) stopped. (lldb) breakpoint set -n ip6_input Breakpoint 1: where = kernel`ip6_input + 44 at ip6_input.c:779:6, address = 0xffffff800078b54c (lldb) c Process 1 resuming (lldb)
次に、VM をそのブレークポイントに到達させる必要があります。VM が IPv6 パケットを受信するまで待ってもよいですし、手動でパケットを送信してもよいでしょう。実際のパケットの送信には、SYN/ACK ではなく、パケットのサイズや内容を簡単に制御できる「ping6」の利用をお勧めします。
実際のコマンドは以下のとおりです。
ping6 fe80::108f:8a2:70be:17ba%en0 -c 1 -p 41 -s 1016 -b 1064
上記のコマンドは、0x41 バイトでパディングされた、できるだけサイズが大きい ICMPv6 の ping 制御パケットを送信するだけのものです。このパケットを en0 インターフェイスに送ります。なお、localhost に送信すると、コールスタックやパケット処理が変わります。こうしてメモリ上に適切なパケットが保持され、その大部分を占める A の羅列は、変換してファジングできます。
ping6 コマンドが実行されると、VM は IPv6 パケットを受信して解析を開始し、すぐに設定したブレークポイントに到達します。
Process 1 stopped * thread #3, name = '0xffffff96dbacd540', queue = 'cpu-0', stop reason = breakpoint 1.1 frame #0: 0xffffff800078b54c kernel`ip6_input(m=0xffffff904e51b000) at ip6_input.c:779:6 [opt] Target 0: (kernel) stopped. (lldb)
VM が一時停止したら、ファジングできるパケットを保持する mbuf のアドレスを取得します。なお、VMware V5 の GDB スタブにはバグがあるようで、int3 がそのままになっています。このままスナップショットを作成しようとすると、実行した最初の命令はその int3 となり、すぐにファジングが中断されてしまいます。そのため、スナップショットを作成する前に、明示的にブレークポイントを無効にする必要があります。
(lldb) disassemble kernel`ip6_input: 0xffffff800078b520 <+0>: pushq %rbp 0xffffff800078b521 <+1>: movq %rsp, %rbp 0xffffff800078b524 <+4>: pushq %r15 0xffffff800078b526 <+6>: pushq %r14 0xffffff800078b528 <+8>: pushq %r13 0xffffff800078b52a <+10>: pushq %r12 0xffffff800078b52c <+12>: pushq %rbx 0xffffff800078b52d <+13>: subq $0x1b8, %rsp ; imm = 0x1B8 0xffffff800078b534 <+20>: movq %rdi, %r12 0xffffff800078b537 <+23>: leaq 0x98ab02(%rip), %rax ; __stack_chk_guard 0xffffff800078b53e <+30>: movq (%rax), %rax 0xffffff800078b541 <+33>: movq %rax, -0x30(%rbp) 0xffffff800078b545 <+37>: movq %rdi, -0xb8(%rbp) -> 0xffffff800078b54c <+44>: int3 0xffffff800078b54d <+45>: testl %ebp, (%rdi,%rdi,8)
このブレークポイントを削除してもまだバグがあり、逆アセンブルリストが更新されない場合もあります。
(lldb) breakpoint disable All breakpoints disabled. (1 breakpoints) (lldb) disassemble kernel`ip6_input: 0xffffff800078b520 <+0>: pushq %rbp 0xffffff800078b521 <+1>: movq %rsp, %rbp 0xffffff800078b524 <+4>: pushq %r15 0xffffff800078b526 <+6>: pushq %r14 0xffffff800078b528 <+8>: pushq %r13 0xffffff800078b52a <+10>: pushq %r12 0xffffff800078b52c <+12>: pushq %rbx 0xffffff800078b52d <+13>: subq $0x1b8, %rsp ; imm = 0x1B8 0xffffff800078b534 <+20>: movq %rdi, %r12 0xffffff800078b537 <+23>: leaq 0x98ab02(%rip), %rax ; __stack_chk_guard 0xffffff800078b53e <+30>: movq (%rax), %rax 0xffffff800078b541 <+33>: movq %rax, -0x30(%rbp) 0xffffff800078b545 <+37>: movq %rdi, -0xb8(%rbp) -> 0xffffff800078b54c <+44>: int3 0xffffff800078b54d <+45>: testl %ebp, (%rdi,%rdi,8)
その場合は、問題のある命令をステップオーバーして確認するとよいでしょう。
(lldb) step Process 1 stopped * thread #3, name = '0xffffff96dbacd540', queue = 'cpu-0', stop reason = step in frame #0: 0xffffff800078b556 kernel`ip6_input(m=0xffffff904e51b000) at ip6_input.c:780:12 [opt] Target 0: (kernel) stopped. (lldb) disassemble kernel`ip6_input: 0xffffff800078b520 <+0>: pushq %rbp 0xffffff800078b521 <+1>: movq %rsp, %rbp 0xffffff800078b524 <+4>: pushq %r15 0xffffff800078b526 <+6>: pushq %r14 0xffffff800078b528 <+8>: pushq %r13 0xffffff800078b52a <+10>: pushq %r12 0xffffff800078b52c <+12>: pushq %rbx 0xffffff800078b52d <+13>: subq $0x1b8, %rsp ; imm = 0x1B8 0xffffff800078b534 <+20>: movq %rdi, %r12 0xffffff800078b537 <+23>: leaq 0x98ab02(%rip), %rax ; __stack_chk_guard 0xffffff800078b53e <+30>: movq (%rax), %rax 0xffffff800078b541 <+33>: movq %rax, -0x30(%rbp) 0xffffff800078b545 <+37>: movq %rdi, -0xb8(%rbp) 0xffffff800078b54c <+44>: movl $0x28, -0xd4(%rbp) -> 0xffffff800078b556 <+54>: movl $0x0, -0xe4(%rbp) 0xffffff800078b560 <+64>: movl $0xffffffff, -0xe8(%rbp) ; imm = 0xFFFFFFFF 0xffffff800078b56a <+74>: leaq -0x1d8(%rbp), %rdi 0xffffff800078b571 <+81>: movl $0xa0, %esi 0xffffff800078b576 <+86>: callq 0xffffff80001010f0 ; __bzero 0xffffff800078b57b <+91>: movq $0x0, -0x100(%rbp) 0xffffff800078b586 <+102>: movq $0x0, -0x108(%rbp) 0xffffff800078b591 <+113>: movq $0x0, -0x110(%rbp) 0xffffff800078b59c <+124>: movq $0x0, -0x118(%rbp) 0xffffff800078b5a7 <+135>: movq $0x0, -0x120(%rbp) 0xffffff800078b5b2 <+146>: movq $0x0, -0x128(%rbp) 0xffffff800078b5bd <+157>: movq $0x0, -0x130(%rbp) 0xffffff800078b5c8 <+168>: movzwl 0x1e(%r12), %r8d 0xffffff800078b5ce <+174>: movl 0x18(%r12), %edx
これで、何らかの問題が発生する前のスナップショットの取得に適した状態になりました。VM がブレークポイントで一時停止している間に、VMware V5 の [スナップショット(Snapshot)] メニューを使えば、スナップショットを作成できます。
VM スナップショットの状態
前述のとおり、.vmsn ファイルには仮想マシンの状態が格納されています。ファイルフォーマットは一部しか文書化されていないので、Volatility の変更版(リポジトリでパッチを入手可能)を使用するとよいでしょう。
Volatility は、正しい「vmsn」ファイルを指定し、以下のように実行するだけです。
$ python2 ./vol.py -d -v -f ~/Virtual\ Machines.localized/macOS\ 11.vmwarevm/macOS\ 11-Snapshot3.vmsn vmwareinfo
Volatility は、関連するマシンの状態を WTF が求める JSON フォーマットで出力します。以下にその例を示します。
上記の出力には、デバッガが表示しているのと同じレジスタの内容がすべて含まれているほか、MSR、制御レジスタ、GDTR なども含まれていることに注意してください。どれも WTF でスナップショットを実行するために必要なものです。
ファジングハーネスとフィックスアップ
Talos のファジングハーネスでは、いくつか行わなければならないことがあります。
- 有用なブレークポイントをいくつか設定する
- 対象の関数の戻り値のブレークポイント(ファジングを終了するポイントを把握するため)
- カーネル例外ハンドラのブレークポイント(クラッシュを検知するため)
- その他の便利なブレークポイント(パッチを適用する、特定の状態になったらテストケースを停止させるなど)
- すべてのテストケースについて、メモリ上の適切な場所を見つけ、そこに書き込み、サイズを調整する
WTF ファジングツールには、最低でも以下の 2 つのメソッドを必ず実装する必要があります。
- bool Init(const Options_t &Opts, const CpuState_t &)
- bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize)
Init
メソッド Init はファジングの初期化ステップを実行します。ここにブレークポイントを登録します。
開始するには、実行の最後として使用している ip6_input 関数が終了する必要があります。
(lldb) disassemble -n ip6_input ... 0xffffff800078cdf2 <+6354>: testl %ecx, %ecx 0xffffff800078cdf4 <+6356>: jle 0xffffff800078cfc9 ; <+6825> at ip6_input.c:1415:2 0xffffff800078cdfa <+6362>: addl $-0x1, %ecx 0xffffff800078cdfd <+6365>: movl %ecx, 0x80(%rax) 0xffffff800078ce03 <+6371>: leaq 0x989236(%rip), %rax ; __stack_chk_guard 0xffffff800078ce0a <+6378>: movq (%rax), %rax 0xffffff800078ce0d <+6381>: cmpq -0x30(%rbp), %rax 0xffffff800078ce11 <+6385>: jne 0xffffff800078d07f ; <+7007> at ip6_input.c 0xffffff800078ce17 <+6391>: addq $0x1b8, %rsp ; imm = 0x1B8 0xffffff800078ce1e <+6398>: popq %rbx 0xffffff800078ce1f <+6399>: popq %r12 0xffffff800078ce21 <+6401>: popq %r13 0xffffff800078ce23 <+6403>: popq %r14 0xffffff800078ce25 <+6405>: popq %r15 0xffffff800078ce27 <+6407>: popq %rbp 0xffffff800078ce28 <+6408>: retq
この関数には戻り値 ret が 1 つしかないので、これを利用すればよいでしょう。テストケースの実行を停止するため、0xffffff800078ce28 にブレークポイントを追加します。
上記のコードは、望ましいアドレスにブレークポイントを設定し、ヒットすると匿名のハンドラ関数を実行するというものです。ハンドラはその後、Ok_t() で実行を停止します。これで、テストケースがクラッシュ以外の原因で終了したことがわかります。
次に、実際の例外やクラッシュ、カーネルパニックを検知してみましょう。macOS カーネルで例外が発生すると、そのたびに exception_triage 関数が呼び出されます。この原因が実際のクラッシュだろうと他の原因だろうと、この関数が呼び出されたらテストケースの実行を止めた方がよいでしょう。
まず、行わなければならないのが、exception_triage のアドレス確認です。
(lldb) p exception_triage (kern_return_t (*)(exception_type_t, mach_exception_data_t, mach_msg_type_number_t)) $4 = 0xffffff8000283cb0 (kernel`exception_triage at exception.c:671) (lldb)
あとは、0xffffff8000283cb0 にブレークポイントを追加するだけです。
クラッシュ時にいくつか情報を収集したいので、このブレークポイントはやや複雑になっています。ブレークポイントがヒットしたら、例外コンテキストに関する情報を含むレジスタをいくつか取得したいところです。この情報は、保存されたテストケースのファイル名形成に使用します。こうすることで、個々のクラッシュを区別できるようになります。
最後に、これはクラッシュのテストケースなので、クラッシュするテストケースを保存する Crash_t() で実行を停止します。
これで、基本の Init 関数は終了です。
InsertTestcase
InsertTestcase 関数は、実行を再開する前に、変更したデータを対象のメモリに挿入するものです。ここで、必要な入力をサニタイズし、変換したデータをメモリのどこに設置するかを決定します。
対象とする関数のシグネチャは ip6_input(struct mbuf *) であり、mbuf 構造体が実際のデータを格納することになります。以下のように最初のブレークポイントで lldb を使えば、データがどこにあるかがわかります。
(lldb) p m->m_hdr (m_hdr) $7 = { mh_next = 0xffffff904e3f4700 mh_nextpkt = NULL mh_data = 0xffffff904e51b0d8 "`\U00000004\U00000003" mh_len = 40 mh_type = 1 mh_flags = 66 } (lldb) memory read 0xffffff904e51b0d8 0xffffff904e51b0d8: 60 04 03 00 04 00 3a 40 fe 80 00 00 00 00 00 00 `.....:@........ 0xffffff904e51b0e8: 10 8f 08 a2 70 be 17 ba fe 80 00 00 00 00 00 00 ....p........... (lldb) p (struct mbuf *)0xffffff904e3f4700 (struct mbuf *) $8 = 0xffffff904e3f4700 (lldb) p ((struct mbuf *)0xffffff904e3f4700)->m_hdr (m_hdr) $9 = { mh_next = NULL mh_nextpkt = NULL mh_data = 0xffffff904e373000 "\x80" mh_len = 1024 mh_type = 1 mh_flags = 1 } (lldb) memory read 0xffffff904e373000 0xffffff904e373000: 80 00 30 d7 02 69 00 00 62 b4 fd 25 00 0a 2f d3 ..0..i..b..%../. 0xffffff904e373010: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA (lldb)
ip6_input 関数の最初で、最初のパラメータである m_hdr のインスペクションを行っています。これにより、標準的な IPv6 ヘッダーのように見える 0xffffff904e51b0d8 に 40 バイトのデータがあることがわかります。さらに、mh_next を取得してインスペクションを行うことで、0xffffff904e373000 にサイズ 1,024 のデータが格納されていること、そのデータは ICMP6 データと A の羅列で構成されていることがわかります。
すべての IPv6 プロトコルを適切にファジングできるよう、IPv6 ヘッダーとカプセル化されたパケットを変換します。40 バイトを最初の mbuf に、残りを 2 つめの mbuf に、別々にコピーします。
2 つめの mbuf(ICMPv6 パケット)の場合、変換したデータを 0xffffff904e373000 に書き込む必要があります。レジスタの読み取りや逆参照、オフセットの処理は必要ないので、これはかなり簡単です。
mbuf のサイズを更新することも可能ですが、ここでは変換して作成するテストケースのサイズを制限することにします。これで、ファジングハーネスの準備はほぼ完了です。
すべてをまとめる
WTF のファジングツールには、必ず state ディレクトリとその中の以下の 3 つのファイルが必要です。
- mem.dmp:RAM の完全なダンプ
- regs.json:CPU の状態が記述された JSON ファイル
- symbol-store.json:実際には必要ないので空でも構いません。ただ、既知のシンボルのアドレスを入力しておくと、ファジングツールでハードコードするアドレスの代わりに使用できます。
次に、スナップショットの .vmm ファイルをファジングを行うマシンにコピーし、mem.dmp にリネームします。Volatility から取得した VM の状態を regs.json ファイルに書き込みます。
状態の設定が済めば、テストを実行できるようになります。ファジングツールをコンパイルし、以下のようにテストを実行します。
c:\work\codes\wtf\targets\ipv6_input>..\..\src\build\wtf.exe run --backend=bochscpu --name IPv6_Input --state state --input inputs\ipv6 --trace-type 1 --trace-path .
デバッガのインスタンスには、0 個のアイテムがロードされます。
load raw mem dump1 Done Setting debug register status to zero. Setting debug register status to zero. Segment with selector 0 has invalid attributes. Segment with selector 0 has invalid attributes. Segment with selector 8 has invalid attributes. Segment with selector 0 has invalid attributes. Segment with selector 10 has invalid attributes. Segment with selector 0 has invalid attributes. Trace file .\ipv6.trace Running inputs\ipv6 -------------------------------------------------- Run stats: Instructions executed: 13001 (4961 unique) Dirty pages: 229376 bytes (0 MB) Memory accesses: 46135 bytes (0 MB) #1 cov: 4961 exec/s: infm lastcov: 0.0s crash: 0 timeout: 0 cr3: 0 uptime: 0.0s c:\work\codes\wtf\targets\ipv6_input>
上記では、トレースを有効にして、run モードで WTF を実行しています。特定の入力でファジングを実行し、Routing Information Protocol(RIP)トレースファイルを保存して、調べられるようにしたいと考えています。出力からわかるように、ファジングツールの実行は正常に終了しました。総命令数は 13,001 件(そのうち 4,961 がユニーク)でした。クラッシュやタイムアウトなしに実行が完了したことは、特に注目すべきでしょう。
カバレッジの分析とシンボル化
WTF のシンボライザは、実行する対象が Windows であり、通常は PDB を備えていることが前提となっています。これを完全にエミュレートするのは大変なので、代わりに LLDB スクリプティングとシンボル化を行うことにしました。
まず、LLDB にすべての既知のシンボルとそのアドレスをダンプさせます。リポジトリで提供されているスクリプトを使えばかなり簡単です。このスクリプトは image dump symtab コマンドの出力を解析し、最も多いシンボルを解決するために追加のクエリを実行します。その結果が symbol-store.json ファイルであり、以下のようなものです。
ファジングツールから取得したトレースファイルは、実行された命令のアドレスを格納した単なるテキストファイルです。サポートツールの 1 つが、以前に生成されたシンボルストアを使用してトレースをシンボル化する symbolize.py スクリプトです。これを ipv6.trace で実行すると、トレースがシンボル化されます。
ip6_input+0x36 ip6_input+0x40 ip6_input+0x4a ip6_input+0x51 ip6_input+0x56 bzero bzero+0x3 bzero+0x5 bzero+0x6 bzero+0x8 ip6_input+0x5b ip6_input+0x66 ip6_input+0x10b ip6_input+0x127 ip6_input+0x129 ip6_input+0x12e ip6_input+0x130 m_tag_locate m_tag_locate+0x1 m_tag_locate+0x4 m_tag_locate+0x8 m_tag_locate+0xa m_tag_locate+0x37 m_tag_locate+0x4b m_tag_locate+0x4d m_tag_locate+0x4e ip6_input+0x135 ip6_input+0x138 ip6_input+0x145 ip6_input+0x148 ip6_input+0x14a ip6_input+0x14f ip6_input+0x151 m_tag_locate m_tag_locate+0x1 m_tag_locate+0x4 m_tag_locate+0x8 m_tag_locate+0xa m_tag_locate+0x14 ... lck_mtx_unlock+0x4e lck_mtx_unlock+0x52 lck_mtx_unlock+0x54 lck_mtx_unlock+0x5a lck_mtx_unlock+0x5c lck_mtx_unlock+0x5e ip6_input+0x1890 ip6_input+0x189b ip6_input+0x18a2 ip6_input+0x18a5 ip6_input+0x18c0 ip6_input+0x18c7 ip6_input+0x18ca ip6_input+0x18e3 ip6_input+0x18ea ip6_input+0x18ed ip6_input+0x18f1 ip6_input+0x18f7 ip6_input+0x18fe ip6_input+0x18ff ip6_input+0x1901 ip6_input+0x1903 ip6_input+0x1905 ip6_input+0x1907 ip6_input+0x1908
トレース全文はもっと長いものですが、最後に関数のオフセットを比較すれば、retq 命令に到達したことが簡単にわかります。
トレースファイルは IDA Lighthouse とも互換性があり、IDA Lighthouse に読み込むだけで、カバレッジの概要を視覚的に把握できます。
緑のノードがヒットを示す
チェックサムの問題を回避
手作業でカバレッジ分析を行わなくとも、IPv6 が対象の場合、フィードバック駆動型のファジングツールではあまりうまくいかないことはすぐにわかります。これは、TCP パケットのチェックサムなど、上位層のプロトコルパケットに存在するさまざまなチェックサムに原因があります。ランダムに変換したデータはチェックサムを無効化するので、パケットは早期に拒否されてしまいます。
この問題への対処方法は 2 つあります。データ変換後にチェックサムを修正するか、インストゥルメンテーションを活用してチェックを実行するコードを無効化するかです。以下のように、チェックサムチェックの戻り値を変更するだけのファジングハーネスに、さらにもう 1 つブレークポイントを設定すれば、簡単に実現できます。
ファジングツールを実行する
これでツールの仕組みがわかったので、ファジングを開始できます。1 つのターミナルで、サーバーを起動します。
c:\work\codes\wtf\targets\ipv6_input>..\..\src\build\wtf.exe master --max_len=1064 --runs=1000000000 --target . Seeded with 3801664353568777264 Iterating through the corpus.. Sorting through the 1 entries.. Running server on tcp://localhost:31337.. And in another, the actual fuzzing node: c:\work\codes\wtf\targets\ipv6_input> ..\..\src\build\wtf.exe fuzz --backend=bochscpu --name IPv6_Input --limit 5000000 The debugger instance is loaded with 0 items load raw mem dump1 Done Setting debug register status to zero. Setting debug register status to zero. Segment with selector 0 has invalid attributes. Segment with selector 0 has invalid attributes. Segment with selector 8 has invalid attributes. Segment with selector 0 has invalid attributes. Segment with selector 10 has invalid attributes. Segment with selector 0 has invalid attributes. Dialing to tcp://localhost:31337/..
サーバーウィンドウで、カバレッジが増加し、新しいテストケースが発見されて保存されていることがすぐに確認できるはずです。
Running server on tcp://localhost:31337.. #0 cov: 0 (+0) corp: 0 (0.0b) exec/s: -nan (1 nodes) lastcov: 8.0s crash: 0 timeout: 0 cr3: 0 uptime: 8.0s Saving output in .\outputs\4b20f7c59a0c1a03d41fc5c3c436db7c Saving output in .\outputs\c6cc17a6c6d8fea0b1323d5acd49377c Saving output in .\outputs\525101cf9ce45d15bbaaa8e05c6b80cd Saving output in .\outputs\26c094dded3cf21cf241e59f5aa42a42 Saving output in .\outputs\97ba1f8d402b01b1475c2a7b4b55bc29 Saving output in .\outputs\cfa5abf0800668a09939456b82f95d36 Saving output in .\outputs\4f63c6e22486381b907daa92daecd007 Saving output in .\outputs\1bd771b2a9a65f2419bce4686cbd1577 Saving output in .\outputs\3f5f966cc9b59e113de5fd31284df198 Saving output in .\outputs\b454d6965f113a025562ac9874446b7a Saving output in .\outputs\00680b75d90e502fd0413c172aeca256 Saving output in .\outputs\51e31306ef681a8db35c74ac845bef7e Saving output in .\outputs\b996cc78a4d3f417dae24b33d197defc Saving output in .\outputs\2f456c73b5cd21fbaf647271e9439572 #10699 cov: 9778 (+9778) corp: 15 (9.1kb) exec/s: 1.1k (1 nodes) lastcov: 0.0s crash: 0 timeout: 0 cr3: 0 uptime: 18.0s Saving output in .\outputs\3b93493ff98cf5e46c23a8b337d8242e Saving output in .\outputs\73100aa4ae076a4cf29469ca70a360d9 #20922 cov: 9781 (+3) corp: 17 (10.0kb) exec/s: 1.0k (1 nodes) lastcov: 3.0s crash: 0 timeout: 0 cr3: 0 uptime: 28.0s #31663 cov: 9781 (+0) corp: 17 (10.0kb) exec/s: 1.1k (1 nodes) lastcov: 13.0s crash: 0 timeout: 0 cr3: 0 uptime: 38.0s #42872 cov: 9781 (+0) corp: 17 (10.0kb) exec/s: 1.1k (1 nodes) lastcov: 23.0s crash: 0 timeout: 0 cr3: 0 uptime: 48.0s #53925 cov: 9781 (+0) corp: 17 (10.0kb) exec/s: 1.1k (1 nodes) lastcov: 33.0s crash: 0 timeout: 0 cr3: 0 uptime: 58.0s #65054 cov: 9781 (+0) corp: 17 (10.0kb) exec/s: 1.1k (1 nodes) lastcov: 43.0s crash: 0 timeout: 0 cr3: 0 uptime: 1.1min #75682 cov: 9781 (+0) corp: 17 (10.0kb) exec/s: 1.1k (1 nodes) lastcov: 53.0s crash: 0 timeout: 0 cr3: 0 uptime: 1.3min Saving output in .\outputs\00f15aa5c6a1c822b36e33afb362e9ec
同様に、ファジングノードにも進行状況が表示されます。
The debugger instance is loaded with 0 items load raw mem dump1 Done Setting debug register status to zero. Setting debug register status to zero. Segment with selector 0 has invalid attributes. Segment with selector 0 has invalid attributes. Segment with selector 8 has invalid attributes. Segment with selector 0 has invalid attributes. Segment with selector 10 has invalid attributes. Segment with selector 0 has invalid attributes. Dialing to tcp://localhost:31337/.. #10437 cov: 9778 exec/s: 1.0k lastcov: 0.0s crash: 0 timeout: 0 cr3: 0 uptime: 10.0s #20682 cov: 9781 exec/s: 1.0k lastcov: 3.0s crash: 0 timeout: 0 cr3: 0 uptime: 20.0s #31402 cov: 9781 exec/s: 1.0k lastcov: 13.0s crash: 0 timeout: 0 cr3: 0 uptime: 30.0s #42667 cov: 9781 exec/s: 1.1k lastcov: 23.0s crash: 0 timeout: 0 cr3: 0 uptime: 40.0s #53698 cov: 9781 exec/s: 1.1k lastcov: 33.0s crash: 0 timeout: 0 cr3: 0 uptime: 50.0s #64867 cov: 9781 exec/s: 1.1k lastcov: 43.0s crash: 0 timeout: 0 cr3: 0 uptime: 60.0s #75446 cov: 9781 exec/s: 1.1k lastcov: 53.0s crash: 0 timeout: 0 cr3: 0 uptime: 1.2min #84790 cov: 10497 exec/s: 1.1k lastcov: 0.0s crash: 0 timeout: 0 cr3: 0 uptime: 1.3min #95497 cov: 11704 exec/s: 1.1k lastcov: 0.0s crash: 0 timeout: 0 cr3: 0 uptime: 1.5min #105469 cov: 11761 exec/s: 1.1k lastcov: 4.0s crash: 0 timeout: 0 cr3: 0 uptime: 1.7min
まとめ
WTF 上にスナップショットファジング環境を構築することには、いくつかのメリットがあります。WTF を使えば、macOS カーネルのチャンクについて、正確に対象を絞ったファジングテストを実行できます。他の方法では、チャンクをピンポイントで対象とするのは困難です。また、市販の CPU で実際のテストを実行できるため、既存のコンピュータリソースを利用できます。少ないコア数にしか対応できないような制限はありません。さらに、Bochs を活用すれば、エミュレートされた実行速度はかなり遅くなるものの、より複雑なインストゥルメンテーションも可能です。Volatility と WTF プロジェクトのパッチやその他のサポートツールは、GitHub リポジトリで入手できます。
本稿は 2024 年 05 月 16 日にTalos Group のブログに投稿された「Talos releases new macOS open-source fuzzer」の抄訳です。