QEMU脆弱性を利用したVMエスケープ攻撃の検証のまとめ:OSS脆弱性ウォッチ(15)(1/3 ページ)
連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説する。今回は、「QEMU」の脆弱性を悪用したVMエスケープ攻撃に関する事例を紹介するシリーズの最終回。
本連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェア(OSS)の脆弱(ぜいじゃく)性に関する情報を取り上げ、解説しています。
連載第12回から数回にわたりOSSのプロセッサエミュレータである「QEMU(キューエミュ)」の脆弱性を悪用したVM(仮想マシン)エスケープ攻撃に関する事例を、OSSセキュリティ技術の会の佐藤が紹介しています。
このシリーズは今回が最終回です。今回は、連載第13回で紹介した「メモリ情報漏えいの脆弱性」(CVE-2015-5165)と連載第14回で紹介した「ヒープベースのオーバーフロー脆弱性」(CVE-2015-7504)を利用した、VMゲストOSからのエスケープについて解説します。
CVE-2015-5165とCVE-2015-7504に対するエクスプロイトをマージ
VMからエスケープしてホスト上でQEMUの権限によってコードを実行するために、2つのエクスプロイトをマージします。
まず、QEMUのメモリレイアウトを再構築するためにCVE-2015-5165のエクスプロイトを実行します。このエクスプロイトはASLR(Address Space Layout Randomization:アドレス空間配置のランダム化)を迂回(うかい)するために下記のアドレスを取得しようとします。
- ゲスト物理メモリのベースアドレス
この攻撃では、ゲストに対して割り当てた正確なアドレスを、QEMUの仮想アドレス空間内で取得する必要がある - .textセクションのベースアドレス
qemu_set_irq()関数のアドレスを取得するのに役立つ - .pltセクションのベースアドレス
fork()やexecv()のようなシェルコードの構築に使用される関数のアドレスを決定するのに役立つ。ゲスト物理アドレスのパーミッションを変更するには、mprotect()のアドレスも必要。ゲストに割り当てられた物理アドレスは実行可能ではない
RIPコントロール
前回示した通り、%ripレジスタは制御可能です。QEMUを任意のアドレスでクラッシュさせる代わりに、選んだ関数を呼び出す偽のIRQStateを指すアドレスでPCNETバッファーをオーバーフローさせます。
一見すると、system()を実行する偽のIRQStateを構築される可能性があります。ただしQEMUメモリマッピングの一部は、fork()呼び出しを介して保持されないため、この呼び出しは失敗します。
厳密には、mmapped物理メモリはMADV_DONTFORKフラグでマークされています。
qemu_madvise(new_block->host, new_block->max_length, QEMU_MADV_DONTFORK);
execv()の呼び出しは、ゲストマシンを失うことになるので、あまり役に立ちません。qemu_set_irq()はPCNETデバイスエミュレータによって複数回呼び出されます。複数の関数を呼び出すためには、2、3の偽のIRQStateを連鎖させることでシェルコードを構築することもできます。ただし、シェルコードが配置されているページメモリのPROT_EXECフラグを有効にした後にシェルコードを実行する方が便利で、信頼性が高くなります。
今回は、下記2つの偽のIRQState構造体を構築します。
- 最初の構造体はmprotect()を呼び出すためのもの
- 2番目の構造体は、初めにMADV_DONTFORKフラグを元に戻し、次にゲストとホストの間で対話式シェルを実行するシェルコードを呼び出すために使用される
前述した通り、qemu_set_irq()が呼び出されると、入力として2つのパラメータ(irq(IRQstate構造体へのポインタ)とlevel(IRQレベル))を取り、次のようにハンドラを呼び出します。
void qemu_set_irq(qemu_irq irq, int level) { if (!irq) return; irq->handler(irq->opaque, irq->n, level); }
最初の2つのパラメータに対する制御しか行えないことが分かります。それでは、どのようにして3つの引数を持つmprotect()を呼び出すのでしょうか。
まずqemu_set_irq()が次のパラメータを使って自分自身を呼び出すようにします。
- irq:mprotect()関数へのハンドラポインタを設定する偽のIRQStateへのポインタ
- level:PROT_READ | PROT_WRITE | PROT_EXECにセットされたmprotectフラグ
以下のソースコードでは、2つの偽のIRQStateの設定を行います。
struct IRQState { uint8_t _nothing[44]; uint64_t handler; uint64_t arg_1; int32_t arg_2; }; struct IRQState fake_irq[2]; hptr_t fake_irq_mem = gva_to_hva(fake_irq); /* do qemu_set_irq */ fake_irq[0].handler = qemu_set_irq_addr; fake_irq[0].arg_1 = fake_irq_mem + sizeof(struct IRQState); fake_irq[0].arg_2 = PROT_READ | PROT_WRITE | PROT_EXEC; /* do mprotect */ fake_irq[1].handler = mprotec_addrt; fake_irq[1].arg_1 = (fake_irq_mem >> PAGE_SHIFT) << PAGE_SHIFT; fake_irq[1].arg_2 = PAGE_SIZE;
オーバーフローすると、偽のハンドラでqemu_set_irq()が呼び出されます。これはlevelパラメータを「7」に調整した後(mprotectには必須のフラグ)、mprotectを呼び出します。
これでメモリは実行可能です。最初のIRQStateのハンドラをシェルコードのアドレスに書き換えることで、対話型シェルに制御を渡すことができます。
payload.fake_irq[0].handler = shellcode_addr; payload.fake_irq[0].arg_1 = shellcode_data;
対話型シェル
あるポート上のnetcatにシェルをバインドするシェルコードを書いて、それから別のマシンからそのシェルに接続することができます。ファイアウォールの制限を回避するため、ゲストとホスト間の共有メモリを利用してバインドシェルを構築します。
ゲストで書いているコードはすでにQEMUのプロセスメモリで利用可能なので、QEMUの脆弱性をエクスプロイトするには向きません。そのため、シェルコードを挿入する必要はありません。コードを共有し、それをゲストと攻撃されたホスト上で走らせます。
次の図は、ホストとゲストで実行されている共有メモリとプロセス/スレッドをまとめたものです。
2つの共有リングバッファー(inとout)を作成し、それらの共有メモリ領域へのスピンロックアクセスを持つread/writeプリミティブを提供します。
ホストマシン上で、最初にそのstdinとstdoutファイルディスクリプタを複製した後、別のプロセスで「/bin/sh」シェルを起動するシェルコードを実行します。
2つのスレッドも作成します。最初のものは共有メモリからコマンドを読み、それらを、パイプを介してシェルに渡します。2番目のスレッドはシェルの出力を(2番目のパイプから)読み取ってから、それらを共有メモリに書き込みます。これら2つのスレッドは、ゲストマシン上でもインスタンス化されて、専用の共有メモリにユーザー入力コマンドを書き込み、2番目のリングバッファーから読み取られた結果をそれぞれstdoutに出力します。
今回のエクスプロイトでは、標準エラー出力を処理するための3番目のスレッド(および専用の共有領域)があります。
Copyright © ITmedia, Inc. All Rights Reserved.