
VirtualBox E1000 ゲストからホストへのエスケープ
私はVirtualBoxが好きですが、0day脆弱性を公開する理由はそれとは関係ありません。その理由は、現在のインフォセック、特にセキュリティリサーチとバグバウンティの状況に対する私の不同意にあります。
最初の2つにうんざりしたので、私の行動は完全開示です。インフォセック、どうか前進してください。
脆弱なソフトウェア: VirtualBox 5.2.20およびそれ以前のバージョン。
ホストOS: 任意。バグは共有コードベースにある。
ゲストOS: 任意。
VM設定: デフォルト(唯一の要件は、ネットワークカードがIntel PRO/1000 MT Desktop(82540EM)であり、モードがNATであること)。
パッチされたVirtualBoxビルドがリリースされるまで、仮想マシンのネットワークカードをPCnet(2つのいずれか)またはParavirtualized Networkに変更できます。できない場合は、モードをNATから別のものに変更してください。前者の方法がより安全です。
デフォルトのVirtualBox仮想ネットワークデバイスはIntel PRO/1000 MT Desktop(82540EM)で、デフォルトのネットワークモードはNATです。これをE1000と呼びます。
E1000には、ゲスト内でroot/管理者権限を持つ攻撃者がホストのring3にエスケープできる脆弱性があります。その後、攻撃者は既存の技術を使用して、/dev/vboxdrvを介してring0に特権を昇格できます。
ネットワークパケットを送信するために、ゲストは一般的なPCと同じことを行います。つまり、ネットワークカードを構成し、ネットワークパケットを供給します。パケットはデータリンク層フレームや他のより高レベルのヘッダーから構成されます。アダプタに供給されるパケットはTxディスクリプタでラップされます(Txは送信を意味します)。Txディスクリプタは、82540EMデータシート(317453006EN.PDF、リビジョン4.0)に記述されたデータ構造です。パケットサイズ、VLANタグ、TCP/IPセグメンテーション有効フラグなどのメタ情報を格納します。
82540EMデータシートは、3つのTxディスクリプタタイプを提供します:レガシー、コンテキスト、データ。レガシーは非推奨だと思います。他の2つは一緒に使用されます。私たちが気にする唯一のことは、コンテキストディスクリプタが最大パケットサイズを設定し、TCP/IPセグメンテーションを切り替えること、そしてデータディスクリプタがネットワークパケットの物理アドレスとそのサイズを保持することです。データディスクリプタのパケットサイズは、コンテキストディスクリプタの最大パケットサイズより小さくなければなりません。通常、コンテキストディスクリプタはデータディスクリプタの前にネットワークカードに供給されます。
Txディスクリプタをネットワークカードに供給するために、ゲストはそれらをTxリングに書き込みます。これは、物理メモリ内の事前定義されたアドレスにあるリングバッファです。すべてのディスクリプタがTxリングに書き込まれると、ゲストはE1000 MMIO TDTレジスタ(Transmit Descriptor Tail)を更新して、処理すべき新しいディスクリプタがあることをホストに通知します。
次のTxディスクリプタの配列を考えてみましょう:``` [context_1, data_2, data_3, context_4, data_5]
それらの構造フィールドを次のように割り当てましょう(フィールド名は人間が読みやすいように仮定的ですが、82540EM仕様に直接マッピングされています):```
context_1.header_length = 0
context_1.maximum_segment_size = 0x3010
context_1.tcp_segmentation_enabled = true
data_2.data_length = 0x10
data_2.end_of_packet = false
data_2.tcp_segmentation_enabled = true
data_3.data_length = 0
data_3.end_of_packet = true
data_3.tcp_segmentation_enabled = true
context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true
data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true
段階的な分析を通じて、なぜそうあるべきかを学びます。
上記のディスクリプタが指定された順序でTx Ringに書き込まれ、TDTレジスタがゲストによって更新されるとします。ホストはsrc/VBox/Devices/Network/DevE1000.cppファイル内のe1kXmitPending関数を実行します(ほとんどのコメントは読みやすくするために削除されています):```c static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread) { ... while (!pThis->fLocked && e1kTxDLazyLoad(pThis)) { while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }
e1kTxDLazyLoad は、Tx リングから5つの Tx ディスクリプタすべてを読み取ります。次に、初めて e1kLocateTxPacket が呼び出されます。この関数はすべてのディスクリプタを反復処理して初期状態を設定しますが、実際にはそれらを処理しません。今回の場合、最初の e1kLocateTxPacket の呼び出しで、context_1、data_2、data_3 ディスクリプタが処理されます。残りの2つのディスクリプタ、context_4 と data_5 は、while ループの2回目の反復で処理されます(2回目の反復については次のセクションで説明します)。この2つの部分への配列分割が脆弱性を発動させるために重要です。その理由を考えてみましょう。
e1kLocateTxPacket は次のようになります:```c
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
...
for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i)
{
E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
switch (e1kGetDescType(pDesc))
{
case E1K_DTYP_CONTEXT:
e1kUpdateTxContext(pThis, pDesc);
continue;
case E1K_DTYP_LEGACY:
...
break;
case E1K_DTYP_DATA:
if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
break;
...
break;
default:
AssertMsgFailed(("Impossible descriptor type!"));
}
最初のディスクリプタ (context_1) は E1K_DTYP_CONTEXT であるため、e1kUpdateTxContext 関数が呼び出されます。この関数は、TCP Segmentation がディスクリプタに対して有効化されている場合、TCP Segmentation Context を更新します。context_1 では有効であるため、TCP Segmentation Context が更新されます。(TCP Segmentation Context Update が実際に何を行うかは重要ではなく、単に以下のコードを参照するために使用します。)
2つ目のディスクリプタ (data_2) は E1K_DTYP_DATA であるため、この議論には不要ないくつかのアクションが実行されます。
3つ目のディスクリプタ (data_3) も E1K_DTYP_DATA ですが、data_3.data_length == 0 であるため、何のアクションも実行されません。
現時点で、3つのディスクリプタが最初に処理され、2つが残っています。ここで重要なのは、switch 文の後に、ディスクリプタの end_of_packet フィールドが設定されているかどうかのチェックがあることです。これは data_3 ディスクリプタで真です (data_3.end_of_packet == true)。コードはいくつかのアクションを実行し、関数から戻ります。```c if (pDesc->legacy.cmd.fEOP) { ... return true; }
data_3.end_of_packet が false だった場合、残りの context_4 と data_5 のディスクリプタが処理され、脆弱性が回避されることになります。以下に、その関数からのリターンがどのようにバグにつながるのかを示します。
e1kLocateTxPacket 関数の最後に、ネットワークパケットをアンラップしてネットワークに送信する準備ができた次のディスクリプタがあります: context_1, data_2, data_3。その後、e1kXmitPending の内部ループが e1kXmitPacket を呼び出します。この関数は、すべてのディスクリプタ(この場合は5つ)を反復処理して、実際に処理します。```c
static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
while (pThis->iTxDCurrent < pThis->nTxDFetched)
{
E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
...
rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
...
if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
break;
}
各記述子に対して e1kXmitDesc 関数が呼び出されます:```c static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr, bool fOnWorkerThread) { ... switch (e1kGetDescType(pDesc)) { case E1K_DTYP_CONTEXT: ... break; case E1K_DTYP_DATA: { ... if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0) { E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf)); } else { if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg))) { ... } else if (!pDesc->data.cmd.fTSE) { ... } else { STAM_COUNTER_INC(&pThis->StatTxPathFallback); rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread); } } ...
e1kXmitDescに渡される最初のディスクリプタはcontext_1です。この関数はコンテキストディスクリプタに対して何もしません。
e1kXmitDescに渡される2番目のディスクリプタはdata_2です。すべてのデータディスクリプタはtcp_segmentation_enable == true(上記のpDesc->data.cmd.fTSE)であるため、e1kFallbackAddToFrameを呼び出します。そこでdata_5が処理されている間に整数アンダーフローが発生します。```c
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
...
uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;
/*
* Carve out segments.
*/
int rc = VINF_SUCCESS;
do
{
/* Calculate how many bytes we have left in this TCP segment */
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
if (cb > pDesc->data.cmd.u20DTALEN)
{
/* This descriptor fits completely into current segment */
cb = pDesc->data.cmd.u20DTALEN;
rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
}
else
{
...
}