
[CVE-2017-10235] VirtualBox E1000デバイスのバッファオーバーフローに関する説明とPoC
以下の文書では、VirtualBox v5.1.22(v5.1.24 で修正済み)で発見されたバグについて詳しく説明します。このバグは、ゲストデバイスエミュレーションコンポーネント DevE1000(Intel 82540EM イーサネットコントローラエミュレーション)内の関数 e1kFallbackAddToFrame に存在し、攻撃者によって制御されたゲスト OS がホスト上でバッファオーバーフローを引き起こす可能性があります。
このバグは、Oracle によって 2017年7月のCPU で CVE-2017-10235 として認定されました。
この脆弱性は、Linux(Ubuntu 16.04)ゲストを実行する Linux(Ubuntu 16.04)ホストと Windows(v8.1)ホストの両方で確認されましたが、この脆弱性は多くの異なるホスト/ゲストの組み合わせで引き起こされ得ます。すべてのシナリオでデフォルトのネットワーク構成を想定しています:NAT に接続された タイプ Intel PRO/1000 MT Desktop (82540EM) のネットワークアダプタが 1 つのみという構成です。
制御構造(関数ポインタを含む)は攻撃者が制御するデータで上書きされ得るため、多くのシナリオでリモートコード実行が達成され得ると考えて問題ありません。Oracle はこのバグに、機密性へのリスクが None、完全性へのリスクが Low であると判断して低い CVSS スコアを割り当てましたが、我々はこれがこのバグの完全な危険性を反映しているとは考えていません(RCE の可能性についての説明は後述します)。
Intel 82540EM イーサネットコントローラ のエミュレーションを実装する VirtualBox コード(src/VBox/Devices/Network/DevE1000.cpp)では、関数 e1kFallbackAddToFrame がハードウェア TCP セグメンテーションを実装しています:
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc,
bool fOnWorkerThread)
{
#ifdef VBOX_STRICT
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
Assert(e1kGetDescType(pDesc) == E1K_DTYP_DATA);
Assert(pDesc->data.cmd.fTSE);
Assert(!e1kXmitIsGsoBuf(pTxSg));
#endif
uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN +
pThis->contextTSE.dw3.u16MSS;
Assert(u16MaxPktLen != 0);
Assert(u16MaxPktLen < E1K_MAX_TX_PKT_SIZE);
この関数は、最大 TX パケット長(u16MaxPktLen)が標準の最大値である 16288 バイト(E1K_MAX_TX_PKT_SIZE)未満であることを正しくチェックしていますが、それを Assert マクロの形式で行っているため、リリースビルドでは無効化され、結果的にエンドユーザーにとってはチェックが機能しません。これは、Assert の代わりに明示的な if でチェックを強制している類似の関数 e1kAddToFrame とは対照的です:
static bool e1kAddToFrame(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint32_t cbFragment)
{
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
bool const fGso = e1kXmitIsGsoBuf(pTxSg);
uint32_t const cbNewPkt = cbFragment + pThis->u16TxPktLen;
if (RT_UNLIKELY( !fGso && cbNewPkt > E1K_MAX_TX_PKT_SIZE ))
{
E1kLog(("%s Transmit packet is too large: %u > %u(max)\n",
pThis->szPrf, cbNewPkt, E1K_MAX_TX_PKT_SIZE));
return false;
}
通常の関数(e1kAddToFrame)とフォールバック(e1kFallbackAddToFrame)のどちらを使用するかは e1kXmitDesc() 内で決定され、次の 2 つの要因に依存します:データ/コンテキストディスクリプタで TSE フラグが有効になっていること(ゲストマシンを使用する OS によって制御される)と、GSO フラグが無効になっていることです。後者は多くの要因に依存するため、無効化する方法は多数ありますが、最も簡単なのはループバックモードを有効にすることです。ループバックモードは受信制御レジスタ(RCTL.LBM ビット)を通じて構成され、これもゲスト OS によって制御されます。
ループバックモードを有効にすると、関数 e1kXmitAllocBuf は、前述の 16288 バイト(E1K_MAX_TX_PKT_SIZE)の長さを持つ aTxPacketFallback バッファ(TSE フォールバックとループバックに使用される送信パケットバッファ)を PDM スキャッタ/ギャザバッファの割り当てに使用し、GSO が無効であることを通知します(pvUser に NULL を設定することで)。
if (RT_LIKELY(GET_BITS(RCTL, LBM) != RCTL_LBM_TCVR))
{
...
}
else
{
/* Create a loopback using the fallback buffer and preallocated SG. */
AssertCompileMemberSize(E1KSTATE, uTxFallback.Sg, 8 * sizeof(size_t));
pSg = &pThis->uTxFallback.Sg;
pSg->fFlags = PDMSCATTERGATHER_FLAGS_MAGIC |
PDMSCATTERGATHER_FLAGS_OWNER_3;
pSg->cbUsed = 0;
pSg->cbAvailable = 0;
pSg->pvAllocator = pThis;
pSg->pvUser = NULL; /* No GSO here. */
pSg->cSegs = 1;
pSg->aSegs[0].pvSeg = pThis->aTxPacketFallback;
pSg->aSegs[0].cbSeg = sizeof(pThis->aTxPacketFallback);
}
これにより、e1kXmitIsGsoBuf 関数(e1kXmitDesc 内)の呼び出しが False を返すようになり、データディスクリプタで TSE が有効になっている場合、実行フローは(正しいチェックを備えたより安全な関数 e1kAddToFrame の代わりに)e1kFallbackAddToFrame へ進みます。
/*
* Add the descriptor data to the frame. If the frame is complete,
* transmit it and reset the u16TxPktLen field.
*/
if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg)))
{
...
}
else if (!pDesc->data.cmd.fTSE)
{
...
}
else
{
STAM_COUNTER_INC(&pThis->StatTxPathFallback);
rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread);
}
e1kFallbackAddToFrame 内では、前述のチェックがリリースビルドで無効化されているため、MSS を任意に大きな値(最大 64K から HDRLEN を引いた値)に設定でき、その結果、任意に大きな DTALEN を e1kFallbackAddSegment に渡すことが可能になります:
/*
* Carve out segments.
*/
int rc;
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);
関数 e1kFallbackAddSegment はこの値(ここでは引数 u16Len)を使用して、この長さに対する追加のチェックを行うことなく、ゲストメモリからホストメモリ内のバッファ aTxPacketFallback へ(PDMDevHlpPhysRead を通じて)コピーします。これにより、バッファオーバーフロー(容量 16288 バイトのバッファに対して最大 64K のメモリサイズ)が発生します。
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
int rc = VINF_SUCCESS;
/* TCP header being transmitted */
struct E1kTcpHeader *pTcpHdr = (struct E1kTcpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.tu.u8CSS);
/* IP header being transmitted */
struct E1kIpHeader *pIpHdr = (struct E1kIpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.ip.u8CSS);
E1kLog3(("%s e1kFallbackAddSegment: Length=%x, remaining payload=%x,
header=%x, send=%RTbool\n", pThis->szPrf, u16Len,
pThis->u32PayRemain, pThis->u16HdrRemain, fSend));
Assert(pThis->u32PayRemain + pThis->u16HdrRemain > 0);
PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);
この脆弱性を RCE につながりやすくする点として、バッファの直後の変数がそのインデックス(u16TxPktLen)であり、これがバッファへの書き込みに使用される(PDMDevHlpPhysRead の引数のオフセットとして)ことに注目する必要があります。最初のバッファオーバーフロー(長さ E1K_MAX_TX_PKT_SIZE + 2 バイトの最初のデータディスクリプタによって引き起こされる)でこの値を制御できれば、その後の(2 番目のデータディスクリプタによる PDMDevHlpPhysRead の 2 回目の呼び出しで)バッファから最大 64K の距離にある任意のメモリアドレスへの書き込みが可能になります。その際、中間のメモリをすべて上書きする必要はありません(中間のメモリを上書きすると、潜在的なクラッシュを回避しようとして攻撃がより複雑になります)。
ターゲットのバッファ aTxPacketFallback の近く、数行下の 64K 範囲内に g_aE1kRegMap 構造体が定義されています。これには読み取りおよび書き込みハンドラ(pfnRead と pfnWrite)を実装する関数ポインタのベクタが含まれており、RCE を容易にするための 2 回目のバッファオーバーフローの理想的なターゲットとなります。
e1kXmitAllocBuf のバグ完全を期すために、この攻撃ベクトルにおける(軽微な)複雑さについて言及しておく価値があります:関数 e1kXmitAllocBuf にはバグと思われる箇所があり、ループバックモードの場合、cbTxAlloc(次のパケットのバイト数)が通常の場合(if のもう一方の分岐)のようにゼロにリセットされません。これにより、スレッドが e1kLocateTxPacket の while ループ(e1kXmitPending 内)で停止してしまいます:
while (e1kLocateTxPacket(pThis))
{
fIncomplete = false;
/* Found a complete packet, allocate it. */
rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
/* If we're out of bandwidth we'll come back later. */
if (RT_FAILURE(rc))
goto out;
/* Copy the packet to allocated buffer and send it. */
rc = e1kXmitPacket(pThis, fOnWorkerThread);
/* If we're out of bandwidth we'll come back later. */
if (RT_FAILURE(rc))
goto out;
}
これは、e1kLocateTxPacket が cbTxAlloc がゼロでない場合に True を返して早期にリターンし、iTxDCurrent が nTxDFetched と等しいかどうかをチェックするコード(すべてのディスクリプタが処理された通常のケース)に到達しないために発生するようです。通常であればこのチェックにより関数は False を返し、前述のループが効果的に終了します。
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
LogFlow(("%s e1kLocateTxPacket: ENTER cbTxAlloc=%d\n",
pThis->szPrf, pThis->cbTxAlloc));
/* Check if we have located the packet already. */
if (pThis->cbTxAlloc)
{
LogFlow(("%s e1kLocateTxPacket: RET true cbTxAlloc=%d\n",
pThis->szPrf, pThis->cbTxAlloc));
return true;
}
これは、ループバックモードを設定した後にデバイスへ送信される最初のパケットがオーバーフローを引き起こすものでなければならないという要件に帰着します。そうでない場合、VM はハングし(RCE ではなく DoS で終了します)。
ネットワークデバイスのセットアップは簡単とは程遠いため、またそのためのカスタムドライバを構築することを避けるため、汎用 Linux カーネルの E1000 ドライバを改造して、オーバーフローを引き起こすディスクリプタ(コンテキストとデータの両方)を生成するようにしました。この改造されたカーネルは、このリポジトリからダウンロードできます。Ubuntu 16.04 ゲストでテストされ、Linux と Windows の両方のホストでクラッシュを引き起こしました。詳細な説明はこちらで入手できます。
この脆弱性は Changeset 67974(bugref:8881)で修正されました。e1kFallbackAddToFrame で Assert として行われていたチェックは、明示的な if 文によるチェックに変換され、リリースビルドでも有効なままとなりました(e1kAddToFrame ですでに行われていたものと同様です)。また、cbTxAlloc は e1kXmitAllocBuf の両方の分岐(ループバックモードと通常モード)でゼロに設定されるようになりました。
ここで提案する追加の(防御的な)チェックとして、チェンジセットでは実装されていませんが、e1kFallbackAddSegment(および同様に e1kAddToFrame)の PDMDevHlpPhysRead の呼び出しの前に、ゲストメモリによってバッファがオーバーフローしないかを明示的にチェックすることが考えられます(主に u16TxPktLen と u16Len の合計が aTxPacketFallback バッファ長未満であること)。