連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説する。今回は、「QEMU」の脆弱性を悪用したVMエスケープ攻撃に関する事例を紹介するシリーズの最終回。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェア(OSS)の脆弱(ぜいじゃく)性に関する情報を取り上げ、解説しています。
連載第12回から数回にわたりOSSのプロセッサエミュレータである「QEMU(キューエミュ)」の脆弱性を悪用したVM(仮想マシン)エスケープ攻撃に関する事例を、OSSセキュリティ技術の会の佐藤が紹介しています。
このシリーズは今回が最終回です。今回は、連載第13回で紹介した「メモリ情報漏えいの脆弱性」(CVE-2015-5165)と連載第14回で紹介した「ヒープベースのオーバーフロー脆弱性」(CVE-2015-7504)を利用した、VMゲストOSからのエスケープについて解説します。
VMからエスケープしてホスト上でQEMUの権限によってコードを実行するために、2つのエクスプロイトをマージします。
まず、QEMUのメモリレイアウトを再構築するためにCVE-2015-5165のエクスプロイトを実行します。このエクスプロイトはASLR(Address Space Layout Randomization:アドレス空間配置のランダム化)を迂回(うかい)するために下記のアドレスを取得しようとします。
前回示した通り、%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構造体を構築します。
前述した通り、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()が次のパラメータを使って自分自身を呼び出すようにします。
以下のソースコードでは、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.