VMの検知について

マルウェアを解析する際にはVMで解析を行うことが多いので、VMを検知してアンチデバッキングするマルウェアが存在する。それらが用いる手法についてまとめた。

1. TSCを用いる

 TSC(IA32_TIME_STAMP_COUNTER_MSR: TSC_MSR)とは、CPUクロックごとに加算される64bitのタイムスタンプカウンタであり、これを RDTSC(Read Time Stamp Counter)命令を使って読み出すことで、CPUクロックと同じ分解能を持つ精度のタイマを使うことができる。

CPUクロックに基づく相対時刻の計測

 このタイマを用いて、VMとホストマシンで実行した時の時間差を確認することでVMを検知できる。以下のコードはwin上のgccで動作する。

# include <stdio.h>

int main(void) {

    unsigned int time1 = 0;
    unsigned int time2 = 0;
    
    __asm__(
        "RDTSC\n\t"
        "MOV %0, %%EAX\n\t"
        "RDTSC\n\t"
        "MOV %1, %%EAX\n\t"
        : "=&r" (time1)
        : "r" (time2)
    );

    if ((time2 - time1) > 100) {
        printf("%s", "VM detected");
        _exit(-1);
    }

    printf("%s", "VM not present");

    return 0;
}

2. レジストリを見る

 Windows レジストリのHKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Disk\Enum 0の値を見ると、ディスクの名前とIDが分かり、これがVMwareやVBOXなどの値を含んでいるため、VMを検知できる。

 HKLM\SYSTEM\CurrentControlSet\Services\Disk\Enum\0の値に、SCSI\Disk&Ven_VMware_&Prod_VMware_Virtual_S&Rev_1.0\4XXXXXXや、IDE\DiskVBOX_HARDDISK___________________________1.0_____\4XXXXXXを含めば、VM上で動いている。以下のコードはpython2.7.8で動作する。

# coding: UTF-8

import _winreg

handle = _winreg.OpenKey(
        _winreg.HKEY_LOCAL_MACHINE,
        'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'
        )

try:
    reg_val = _winreg.QueryValueEx(handle, '0')[0]

    if "VMware" in reg_val:
        print "Vmware Detected"
    elif "VBOX" in reg_val:
        print "Virtualbox Detected"

finally:
    _winreg.CloseKey(handle)

3. CPUコア数の確認

 VMのデフォルトでのCPUコア数は1つになっている。今どき、CPUコア数が1つの物理マシンなんてないのでCPUコア数を確認することでVMを検知できる。

# coding: UTF-8

import multiprocessing

if multiprocessing.cpu_count() == 1:
    print 'maybe VM'
else:
    print 'real machine?'

設定からVMのコア数は変えられるので、このアンチデバッキングは簡単に回避できる。

4. ディスク容量の確認

 VMのディスク容量は物理マシンに比べすくないことが多いので、ディスク容量を確認することでVMを検知できる。以下のコードは100GB以下の場合はVMとしている。

# coding: UTF-8

import os

def GetDiskSpaceForWin(drive):
    from ctypes import c_ulonglong, windll, byref

    free_bytes_available       = c_ulonglong()
    total_number_of_bytes      = c_ulonglong()
    total_number_of_free_bytes = c_ulonglong()
    
    windll.kernel32.GetDiskFreeSpaceExA(
        drive,
        byref(free_bytes_available),
        byref(total_number_of_bytes),
        byref(total_number_of_free_bytes)
    )

    total_number_of_gigabytes = total_number_of_bytes.value / (1024 ** 3)

    return total_number_of_gigabytes


if __name__ == '__main__':
    disk_space = 0

    if os.name == 'nt':
        disk_space = GetDiskSpaceForWin('C:')

    elif os.name == 'posix':
        statvfs = os.statvfs('/')
        disk_space = statvfs.f_frsize * statvfs.f_blocks / (1024 ** 3)

    if disk_space < 100:
        print 'maybe VM'
    else:
        print 'real machine?'

ディスクの容量上、割り当てられる上限があるので、VMの設定を変更するだけでは検知を逃れるのは厳しい。

5. VMwareバックドアI/Oポートを用いる

 VMwareにはゲストOSとホストOSとの間で情報をやりとりするために存在する VMwareバックドアI/Oポートという仕組みがあるので、その存在を確認することで、VMを検知できる。

 mac上のVirtualBoxを使っているため、環境がないので実装は省略する。アナライジング•マルウェアの3.6章に詳しく載っているので見て欲しい。

6. IDT、LDTアドレスの確認

 CPU1コアにつき、IDT(Interrupt Descriptor Table)という割り込みや例外発生時に呼び出す関数のテーブルが割り当てられる。このアドレスは物理マシンと仮想マシンで違うので、VMの検知につかえる。この手法はシングルプロセッサが用いられていれば有効だが、マルチプロセッサ化が進んだ現在では、IDTはコアごとに存在するので、この手法を使うのはむずかしい。

 LDT(Local Descriptor Table)とは、特権変更のためのメモリーテーブルである。各セグメントのベースアドレスやアクセス権、サイズなど。これのセグメントセレクタの値が物理マシンと仮想マシンで違うので、VMの検知につかえる。物理マシンの場合は0が、仮想マシンの場合はそれ以外の値が入っている。

これらもアナライジング•マルウェアの3.6章に詳しく載っているので見て欲しい。