
CVE-2021-26708 の背後にあるバグの調査
このリポジトリには、CVE-2021-26708に関する小規模な解説と、このバグをUse After Free(解放後使用)の書き込みプリミティブに変換する方法が含まれています。ここにあるPoCは完全なエクスプロイトではなく、このバグを調査する際に使用した単なるテスト用プログラムです。kmalloc-64キャッシュから解放されたエントリを正常に使用できますが、メモリをグルーミングしてスロットに関心のある何かを配置するコードは含まれていません。
これは @a13xp0p0v によって報告された楽しいバグです。パッチが非常にシンプルで、5箇所でロックの外から vsk->transport への参照が取得されるのを防ぐだけだったので、私の目を引きました。
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c518adafa39f37858697ac9309c6cf1805581446
以下は、パッチからエクスプロイトに使用可能なuse-after-freeプリミティブに至るプロセスの簡潔なウォークスルーです。このウォークスルーは、このバグを探究したい他の人々にとって役立つはずです。
Linuxカーネル5.10.13をダウンロードし、上記のパッチを手動で元に戻しました。カーネルの構築と実行に関する詳細については、以下の資料が参考になります。
https://fedoraproject.org/wiki/Building_a_custom_kernel
また、kgdbによるカーネルデバッグを有効にするためにブートパラメータを変更しました。以前に構築したvmlinuxファイルをgdbで使用すると、メインカーネルのすべてのシンボルは取得できましたが、ロード可能なカーネルモジュールのシンボルは取得できませんでした。脆弱性に関連するコードはデフォルトではロードされませんが、PF_VSOCKファミリーが使用されると(カーネルのビルド方法に依存します)、カーネルにロードされます。
ロードされたモジュールのkgdbでシンボルを取得するために、少なくとも一度vsockソケットを使用した後、sudo cat /proc/modules | grep vsock を使って関連モジュールのベースアドレスを取得しました。次にgdbで (gdb) add-symbol-file ./net/vmw_vsock/vsock.ko 0xffffffffc0567000 のようにして、そのkoファイルのシンボルがメモリ上のどこにあるかをgdbに認識させました。vsock.ko と vmw_vsock_virtio_transport_common.ko が最も関連性の高い2つのモジュールでした。
パッチから逆方向に作業するのは楽しいです。なぜなら、多くの脆弱性調査とは異なり、すでに正しい場所を見ていることが確実だからです。このケースでは、パッチから、sock_lock が取得される前にトランスポートへの参照が保存されることが分かっています。脆弱性はトランスポートが変更されるが、古い参照が使用されることにあると安全に推測できます。
このようなシナリオでは、トランスポート自体が動的に割り当てられたオブジェクトであり、参照の取得とロックの保持の間に解放され、他のオブジェクトに置き換えられることを期待します。残念ながら、他のモジュールによって実装された関連トランスポートのライフタイムを追跡すると、それらはすべてグローバルメモリ上にあるように見えます。そのため、スコープ外で使用されるアイテムを探すために、もう一段深く調べる必要があります。
af_vsock.c を見ると、vsk->transport が変更される2箇所が見つかります。vsock_assign_transport と vsock_deassign_transport です。vsock_assign_transport では、異なる既存のトランスポートがある場合、新しいトランスポートを配置する前に vsock_deassign_transport が呼び出されることがわかります。
vsk->transport->destruct(vsk) 呼び出しの可能性を こちら で見ると、ループバックとvirtioの両方のトランスポートが vsk->trans パラメータを単に kfree していることがわかります こちら。ビンゴ!(1) この呼び出しへのパスで、(2) 破棄される前のトランスポート参照を使用して vsk->trans にアクセスする脆弱な関数と競合できるものを見つければ、プリミティブが得られます。
vsock_deassign_transport へのパスを探すと、vsock_sk_destruct または vsock_assign_transport から呼び出されているのがわかります。vsock_sk_destruct は sock->destruct 関数として設定されているため、__sys_close への呼び出しや、破棄パスに沿った他の利用可能な呼び出し(sock_put、sock_close、vsock_release など)がここに到達する可能性があります。
vsock_assign_transport への最も関連性の高いパスは vsock_stream_connect 経由ですが、ソケットが特定のいくつかの状態にある必要があり、トランスポートが変更される場合にのみ vsock_deassign_transport を呼び出します。また、新しいトランスポートがNULLにならない場合、vsk->trans パラメータを置き換えます。
どの解放パスが適切かを見つける前に、破棄されたトランスポートへの無効な参照を持つ vsk->trans メンバーを使用する有効なパスが存在するかどうかを確認する必要があります。トランスポートがおそらく無効な参照で使用されるすべての箇所を系統的にチェックできます。それらの穴を追跡すると、vsk->trans が使用される箇所が見つかります。最良のパスは vsock_stream_setsockopt こちら を通るもので、transport->notify_buffer_size がループバックおよびvirtioトランスポートの vsk->trans 内のオフセットに書き込むとき まさにこちら です。使用時にtransがすでに解放されている場合、kmalloc-64割り当ての0x28オフセットにu32の nice な書き込みが得られます。
vsock_stream_setsockopt をプリミティブとして使用するには、トランスポートへの参照を取得してから sock_lock を取得するまでの間の競合に依存します。これは小さなウィンドウであり、そこに到達するまでに多くの命令があります。そこで、Linuxのクールな機能であるuserfaultfdを使用して、自分たちのチャンスを高めることができます。このメカニズムにより、ユーザーモードでページフォールトを好きなように処理できます。
参照: https://man7.org/linux/man-pages/man2/ioctl_userfaultfd.2.html および https://man7.org/linux/man-pages/man2/userfaultfd.2.html。
これにより、1つのスレッド(ゲーター)が sock_lock を取得し、次にユーザーメモリにアクセスしてページフォールトを発生させることができます。そのスレッドを(ロックを保持したまま)好きなだけ一時停止させることができます。そのロックを取得しようとする他のスレッドは、ゲーターを解放してロックを解放するまでそこで待機します。破棄を実行するスレッドと、無効な参照を使用するスレッドを並べることができます。両方とも sock_lock で待機し、これでレースに勝つ絶好のチャンスが得られます。破棄を実行するスレッドが次にロックを取得するように選択された場合、その後に setsockopt 呼び出しが完了します。それは、解放された(そして置き換えられた)後の vsk->trans ポインタを使用します。
代わりに setsockopt 呼び出しが先に進んだ場合、レースには負けますが、プロセス全体を安全に再試行できます。
これを構築しようとしたとき、しばらく間違った方向に進みました。close やタイムアウトを介して vsock_deassign_transport を呼び出そうとしましたが、多くの参照カウントチェックに遭遇し、実際の破棄が手遅れになるまで遅延しました。
余談ですが、これらのパスをデバッグするのは難しい場合があります。想像できるように、システムコール close にブレークポイントを設定すると、頻繁にヒットします。条件付きブレークポイント を使用して適切なスレッドでのみ停止するようにしても、マシンの速度が極端に低下します。これに対する興味深い回避策は、条件が正しい場合にのみ bpf_trace_printk を呼び出すトレースポイントとともに eBPF を使用することです。その後、bpf_trace_printk に kgdb ブレークポイントを設定でき、正しい場所の近くに到達できます。kprobes では、すでにブレークポイントハンドラ内にいるため、これは機能しません。eBPF に bpf_trace_kgdb_break 呼び出しを追加することは、カーネルへの良い追加になると思います。
やっと vsock_assign_transport パスに注目を切り替えると、すぐにうまくいきました。要件を満たすために、最初にリッスンしているサーバーがない状態で VM_ADDR_CID_LOCAL に接続します。これによりループバックトランスポートが得られますが、接続がタイムアウトまたは失敗すると、状態は SS_UNCONNECTED に戻ります。これにより、VM_ADDR_CID_HOST より大きいアドレスに別の接続を行うことができ、トランスポートが変更され、既存のトランスポートが破棄されて解放が発生します。重要なのは、実際に登録された transport_g2h または transport_h2g がない状態でこれを行うことです。そうすれば、新しいトランスポートはNULLになり、解放された参照が vsk->trans に残ります。
これらすべてを整えることで、権限昇格に使用できる信頼性の高いuse-after-freeが得られます。
このリポジトリは、最初のuse-after-freeに到達するまでの話だけです。しかし、これで virtio_vsock_sock が以前に割り当てられていたkmalloc-64キャッシュ内のオフセットに値を書き込むプリミティブが得られました。私はそこでウォークスルーを止めることにしました。なぜなら、なんでもかんでも私がやらなきゃいけないんですか?ということで。