NFLabs. エンジニアブログ

セキュリティやソフトウェア開発に関する情報を発信する技術者向けのブログです。

KVMとQEMUを活用したマルウェア解析環境の作り方


概要

本記事では、QEMUとlibvirtを使って仮想マシンをCLIで制御する方法を解説します。特に以下のポイントに焦点を当てています。

  • スナップショットの作成・リバート方法
  • 差分ディスクを使った効率的な環境構築

はじめに

この記事は、NFLaboratories Advent Calendar 2024 3日目の記事です*1。

こんにちは。教育ソリューション担当の吉武、斎藤、神です。
皆さんは普段の生活の中でどれくらい仮想マシンに依存しているでしょうか。
我々セキュリティエンジニアにとって、仮想マシンはマルウェアの解析環境や、やられ環境の構築など様々な用途で利用する必要不可欠な存在です。セキュリティエンジニアでなくとも、某仮想化製品の値上げの影響により某製品からの脱却を検討しているという声を聴いた方は多いかもしれません。自社サーバを抱える多くの組織にとって、仮想マシンの運用方針をどうするかという課題は避けられないものとなっています。

そのような時代の中で、NFLabs.も解析環境を"KVM+QEMU"を使って構築してみることにしました。そして実際に使用してみますと、解析環境として非常に有用な点が多々見つかりました。

  • (体感的には)某製品よりも高速な起動・リバートができる
  • 起動から終了までの差分を差分ディスクとして保存できるため、とても軽量で効率的にシグネチャマッチングができる
  • 異なるCPUアーキテクチャの環境も構築することができる

上記のことから、本記事では"KVM+QEMU"を布教する意味も込めて、QEMUで構築した仮想マシンの操作方法(起動・停止、スナップショットの取得)について紹介します。特に差分ディスクの作成とリバートまでの一連の流れについては、マルウェア解析環境を構築するうえでの一番の重要なポイントだと思いますので、類似の解析環境構築を考えている方の参考になれば幸いです。

なお、本記事では文章量の都合上、KVMやQEMU、libvirtのインストール方法などは含まれていません。インストール方法については、すでに有識者の記事が多数存在するため、そちらをご参照ください*2, *3。

KVMとQEMU

QEMUは仮想マシンを実現するためのオープンソースのエミュレータです*4。異なるCPUアーキテクチャのOSを仮想的に動作させることができるので、VMwareやVirtualBoxとは異なりx86_64のCPU上でARM OSを動作させることも可能です。

この機能だけでも他の仮想化製品とは異なる強みがあります。しかしながら、エミュレーションを行う特性上、動作が重くなりがちな点があり、WindowsのGUI操作などもユーザー体験が損なわれることがあります。

そのため、CPUの仮想化支援機能を使用し、QEMUをハイパーバイザー上で動作させることが一般的です。これを実現するために、QEMUと組み合わせてよく使用されるのがKVMです。KVM(Kernel-based Virtual Machine)はLinuxカーネルに組み込まれた仮想化機構で、QEMUと組み合わせることで、ハードウェア支援型の高速な仮想化を実現します。 "KVM+QEMU"の構成にすることで、ユーザー体験を落とすことなく異なるCPUアーキテクチャのOS環境を複数動作させることができます。これは解析者にとって非常に便利な機能といえます。

ここまでお読みなられた方はQEMUの良さを理解して頂けたと思います。ここからは、実際にQEMUの仮想マシンを操作していきましょう。今回の解析環境では自動で起動やリバートをさせたいので、libvirtを使っていきます。

libvirtを使った基本操作

libvirtは仮想化プラットフォーム(QEMU/KVMやVMwareなど)を統一的に管理するためのオープンソースです*5。
QEMUやVMwareなどの仮想化技術を抽象化したAPIを提供し、仮想マシンの作成や起動、停止といったライフサイクルを管理することができます。 また、virshなどのCLIツールやvirt-managerといったGUIのクライアントツールも、そのコンポーネントの一部であり、今回は主にvirshコマンド(一部はqemu-img)を使って仮想マシンの操作を行っています。

virshコマンドを使った仮想マシンの操作

ここで用語について補足しますが、特定の仮想マシンのことをドメインと呼びます。
コマンドライン上で"ドメイン名"を引数として渡す場合は、特定の仮想マシンを指すことに注意してください。

動作環境
  • OS: Ubuntu 24.04 LTS
  • QEMU: 8.2.2
  • libvirt: 10.0.0
  • python: 3.12.3

なお、以降の操作で使用しているUbuntuのユーザー(workerユーザー)はあらかじめKVMグループに追加しています。
また、QEMU仮想マシンでは、Windows 11を元に作成した qcow2 形式のディスクイメージ(以降の"windows11-0712-clone.qcow2"です)を使用し、仮想マシンの名前は win11-test に設定しています(これが VM のドメイン名になります)。

$ ls /var/lib/libvirt/images
windows11-0712-clone.qcow2

仮想マシンをGUI上で確認するにはVirtual Machine Managerを使います*6。
Virtual Machine Managerは仮想マシンを管理できるGUIツールであり、libvirtを使用して仮想化技術を簡単に操作することが可能です。
今回はCLIでの制御がメインなのでこちらのツールに関しては、実行結果の確認程度に利用しています。

Virtual Machine Managerをインストールした状態で、ターミナルで以下のコマンドを実行することで起動できます。

$ virt-manager
Virtual Machine Managerの起動

GUIでは、既に作成された仮想マシンとそのステータスを確認することができます。

準備が整ったところで、さっそくlibvirtのvirshコマンドを使用していきます。
まずは仮想マシンの一覧を表示させてみます。

$ virsh list --all
 Id    Name              State
-----------------------------------
 1     windows11-sample  paused
 109   win11-yo-2        paused
 -     win11-0712        shut off
 -     win11-test        shut off

listサブコマンドは、作成済みの仮想マシンとそのステータスの一覧を表示させることができるため、仮想マシンの状態を確認する際に役立ちます。また、domstateサブコマンドを使えば特定の仮想マシンの状態を出力させることが可能です。

$ virsh domstate win11-test
 shut off

起動する際は、startサブコマンドを使います。

$ virsh start win11-test
Domain 'win11-test' started
仮想マシンの起動

停止する際には、shutdownサブコマンドが利用できます。

$ # virsh shutdown "VMドメイン名"  
$ virsh shutdown win11-test
Domain 'win11-test' is being shutdown

$ # 仮想マシンが完全に停止した後にdomstateサブコマンドを実行
$ virsh domstate win11-test
shut off
仮想マシンの停止

このようにして、libvirtで仮想マシンの制御を簡単に行うことができます。
次はスナップショットの作成や仮想マシンのリストア処理についてです。

スナップショット作成からリバートまで

仮想マシン内でマルウェアの検体を実行し、実行後のメモリ・ディスクの状態を保存するといったことを想定して、メモリ・ディスクスナップショットの取得とリバートをlibvirtを使って行います。処理の全体的な流れは以下の通りです。

仮想マシンの操作の流れ

スナップショットの作成

スナップショットを作成するには、virshのsnapshot-create-asサブコマンドを使います*7。
snapshot-create-asサブコマンドでは、ディスクの状態のみを保存することや、メモリの状態を一緒に保存することも可能です。なお、QEMUには内部スナップショットと外部スナップショットという2種類のスナップショットがあります*8。内部スナップショットではベースのqcow2形式のイメージファイルに仮想マシンのディスクの状態を保存することができます。しかし、環境(ファームウェア等)によっては使用できなかったり、そもそも使用が非推奨とされていることがあるということで、今回は外部スナップショット機能を使用していきます*9 ,*10。 以下では--memspecオプションを使用して、メモリイメージを保存するファイル名を指定し、snapshot=externalで外部にイメージファイルを保存するように指定しています。作成されたスナップショットは検体実行後にリバートでクリーンな状態に仮想マシンを戻すために使用していきます。

$ # virsh snapshot-create-as "VMドメイン名" "スナップショット名" --memspec file="ファイルパス",snapshot="no, internal or external" --atomic
$ virsh snapshot-create-as win11-test clean --memspec file=/var/lib/libvirt/images/win11-test-clean.memory,snapshot=external --atomic
Domain snapshot clean created

$ # スナップショットが作成されたことを確認する
$ virsh snapshot-list win11-test
 Name    Creation Time               State
----------------------------------------------
 clean   2024-12-07 07:44:13 +0000   running

スナップショットを作成すると、上記の例では/var/lib/libvirt/images/にメモリイメージファイルと差分ディスクのファイルが生成されます。
差分ディスクはベースとなるディスクイメージ(qcow2形式で、今回の場合は"windows11-0712-clone.qcow2"がそれに該当します)は変更せずに、別のファイルに変更分を保存するものとなります。そのため、ホストのディスク容量を節約できるというのが大きなメリットです。また、マルウェア解析では、マルウェアが残した痕跡(どんなファイルを作成・削除したのかなど)を効率よく見つけられるためフォレンジックによるアーティファクトの収集や、シグネチャの作成に役立ちます。保存先のディスク形式はベースのディスクイメージ(今回の場合はqcow2形式)に合わせる必要がある点に注意してください。

ここで、マルウェアの検体を実行したと仮定します。
検体実行後の汚染されたディスクや実行後のメモリイメージを保存するため、再びスナップショットを作成します。
このとき、メモリとディスクの状態の整合性を保ちつつ安全にデータを保存するため、仮想マシンを一時停止しておきます。仮想マシンの一時停止にはsuspendサブコマンドが使用できます。

また、仮にメモリイメージのみを保存したいという場合は、snapshot-create-asサブコマンドの代わりに、saveサブコマンドが使用できます。saveサブコマンドは、実行中の仮想マシンのメモリの状態を保存したあと、仮想マシンを停止する機能を持ちます。仮想マシンの停止後は、restoreサブコマンドを使用することで仮想マシンをリストア(メモリの状態を基に戻す)することができますが、ディスクの状態は復元されずに最新のディスクの状態(XMLの設定ファイルに記載してある差分ディスクイメージ)で起動するため、リバートとは異なる動作をすることには注意してください。一方、差分ディスクのみを保存したい場合には、snapshot-create-asサブコマンドのオプション--disk-onlyを指定できます。

以下の例では、snapshot-create-asサブコマンドでメモリイメージと差分ディスクを取得しています。運用方針に応じて、saveやsnapshot-create-asを使い分けると良いでしょう。

$ # 仮想マシンを一時停止
$ virsh suspend win11-test
Domain 'win11-test' suspended

$ # マルウェア検体実行後のスナップショットを作成 
$ virsh snapshot-create-as win11-test dirty --memspec file=/var/lib/libvirt/images/win11-test-clean.memory,snapshot=external --atomic
Domain snapshot dirty created

リバートしてみる

snapshot-revertサブコマンドを使用して仮想マシンをリバートすることができます。 --runnigオプションを付けることで、仮想マシンを起動状態でリバートできます。

$ # 現在のスナップショットリストを確認
$ virsh snapshot-list win11-test
 Name     Creation Time               State
-----------------------------------------------------
 clean    2024-12-07 07:44:13 +0000   running
 dirty    2024-12-07 07:49:37 +0000   paused

$ # virsh snapshot-revert "VMドメイン名" "リバートしたいスナップショット名" --running
$virsh snapshot-revert win11-test clean --running
Domain snapshot clean reverted

これでリバートが完了し、クリーンな状態で仮想マシンを起動することができました。マルウェア検体を実行するたびに以下の処理を行うことで、汚染されたディスクの保存とクリーンな状態での起動を繰り返し行うことができます。
1. 仮想マシンの一時停止
2. 差分ディスク、メモリイメージの保存
3. リバート

スナップショットの作成やリバート処理を行うと、QEMUは新しい差分ディスクを自動的に生成し、それに関連する情報を仮想マシンのXML形式の設定ファイル(今回の場合は/etc/libvirt/qemu/win11-test.xml)に反映します。この差分ディスクはランダムな名前で保存され、例えば以下のようになります。

<!--
設定ファイル (一部抜粋)
/etc/libvirt/qemu/win11-test.xml
-->

...
<devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='/var/lib/libvirt/images/windows11-0712-clone.1733554667'/>
      <backingStore type='file'>
        <format type='qcow2'/>
        <source file='/var/lib/libvirt/images/windows11-0712-clone.qcow2'/>
      </backingStore>
      <target dev='sda' bus='sata'/>
      <address type='drive' controller='0' bus='0' target='0' unit='0'/>
    </disk>
...

クリーンな状態のスナップショット(clean)にリバートして設定ファイルを確認すると、上記のように"/var/lib/libvirt/images/windows11-0712-clone.1733554667" という差分ディスクが作成されていることが確認できました。スナップショットを続けて作成していくと、"backingStore"タグが入れ子になり、関連する差分ディスクの情報が記録されていきます*11。仮想マシンを起動する際には、依存関係にあるすべての差分ディスクが正しい場所に存在していることを事前に確認しておくことをお勧めします。

libvirt-python

これまでvirshコマンドで仮想マシンの操作を行ってきましたが、libvirtのAPIをPythonで利用可能にするバインディングであるlibvirt-pythonを使用すると、スクリプトによる仮想マシン操作の自動化が簡単に行えます*12。
以下のスクリプトは、今回のスナップショットの作成を行うcreate_snapshot.pyとリバートを行うrevert.pyの例です

# create_snapshot.py
# スナップショットを作成するスクリプト

import libvirt
from xml.etree import ElementTree

vm_name = "win11-test"
snapshot_name = "dirty"
memory_file = "/var/lib/libvirt/images/dirty.memory"
disk_file = "/var/lib/libvirt/images/windows11-0712-clone.dirty"

conn = libvirt.open("qemu:///system")
if not conn:
    raise SystemExit("Failed to open connection to qemu:///system")

dom = conn.lookupByName(vm_name)
if not dom:
    raise SystemExit(f"Cannot find guest: {vm_name}")

snapshot_xml = f"""
<domainsnapshot>
  <name>{snapshot_name}</name>
  <memory file='{memory_file}' snapshot='external'/>
  <disks>
    <disk name='sda' snapshot='external' file='{disk_file}'/>
  </disks>
</domainsnapshot>
"""

try:
    snapshot = dom.snapshotCreateXML(snapshot_xml, libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_ATOMIC)
    if not snapshot:
        raise SystemExit("Failed to create snapshot")
    print(f"Snapshot '{snapshot_name}' created successfully.")
except libvirt.libvirtError as e:
    print(f"Failed to create snapshot: {e}")
finally:
    conn.close()
# revert.py
# リバートを行うスクリプト
import libvirt

vm_name = "win11-test"
snapshot_name = "clean"

conn = libvirt.open("qemu:///system")
if not conn:
    raise SystemExit("Failed to open connection to qemu:///system")

dom = conn.lookupByName(vm_name)
if not dom:
    raise SystemExit(f"Cannot find guest: {vm_name}")

try:
    snapshot = dom.snapshotLookupByName(snapshot_name, 0)
    if not snapshot:
        raise SystemExit(f"Cannot find snapshot: {snapshot_name}")
    
    dom.revertToSnapshot(snapshot, libvirt.VIR_DOMAIN_SNAPSHOT_REVERT_RUNNING)
    print(f"Reverted to snapshot '{snapshot_name}' and VM is now running.")

except libvirt.libvirtError as e:
    print(f"Failed to revert to snapshot: {e}")
finally:
    conn.close()

おわりに

初めてQEMUやlibvirtに触れましたが、操作はシンプルで、豊富なリファレンスが揃っているため非常に取り組みやすい印象を受けました。
基本的にはvirshのリファレンスに従ってコマンドを実行するだけで済みますが、スナップショット・差分ディスクの作成などでは、運用に適したサブコマンドを使用する必要があり、複雑な内容も多かったですが、とても勉強になりました。

最後に、ここまでお読みいただきありがとうございました。
この記事が少しでも仮想化管理の理解を深める助けになれば幸いです。

参考リンク