CPUID 命令と RDTSC 命令による簡易 VM 検知プログラムを作ってみた.
こんにちは.あらい大先生です.
私は,AI を用いてマルウェアを検知する手法について研究しています.
そのため,データの前処理や AI の設計/開発が,私の主な作業です.
一方で,「AI にマルウェアか否かを正しく学習させるには,AI の開発者がマルウェアの特徴を理解し,設計へ反映させる必要がある」と私は考えています.
そのため,私はマルウェアを模したプログラムを作ったり解析したりすることもあります.
例えば,以下のプログラムを作りました.
- IsDebuggerPresent によるデバッガの検知
- Metasploit と Code Injection を用いたバックドアの設置
- CPUID 命令と RDTSC 命令による VM の検知
このブログでは 3. を紹介します.
1. と 2. は手順付きで解説されている Web ページ [1, 2] があるので,興味がある方はそちらをご覧ください.
マルウェア解析の関連知識
マルウェア解析は,静的解析と動的解析に大別されます.
以下では,それぞれの手法について概説します.
また,今回のブログでは動的解析を扱うので,その関連知識も説明します.
静的解析
静的解析は,プログラムを実行せずに解析する手法です.
例えば,ソースコードを読み解くことが挙げられます.
ソースコードにはプログラムの動作が全て記述されているので,静的解析によってプログラムの全貌を理解することができます.
しかし,そのソースコードはアセンブリ言語で書かれているうえに,難読化されていることも多いため,解析に時間が掛かりやすいという欠点があります.
動的解析
動的解析は,プログラムを実行して解析する手法です.
例えば,デバッグが挙げられます.
ソースコードが難読化されていても,レジスタやメモリ等の変化からプログラムの動作を推測できるため,静的解析と比べて難読化の影響を受けにくいと言えます.
しかし,動的解析で解析できる部分は,実際にプログラムが実行された部分のみに限定されるという欠点があります.
また,対解析機能が搭載されたマルウェアへの対策が必要になる場合もあります.
デバッガ
デバッガとは,プログラムの欠陥を発見,および修正するためのソフトウェアです.
プログラミング演習で,ソースコードの特定の行でプログラムを一時的に停止させて変数の値を確認したり,書き換えたりする/したと思います.
これを実現しているのがデバッガです.
VM (Virtual Machine)
VM とは,物理マシンの機能をソフトウェアで再現したマシンです.
VM が動的解析で用いられやすい理由として「スナップショットの作成と復元が容易であること」が挙げられます(スナップショットという言葉が分かりづらければ,「手動でセーブとロードがゲームなので,成功するまでリセットし続けられる」と考えると良いでしょう).
これにより,「マルウェアを実行する前のスナップショットを作成しておき,マルウェアの解析を終えたら元の状態へ復元する」という作業を簡単に行えます.
対解析機能
対解析機能とは,マルウェア解析,特に動的解析を妨害するための機能です.
例えば,デバッガや VM を用いて解析されているときは正常なプログラムのように振る舞う機能が挙げられます.
これにより,マルウェアは,セキュリティ製品による検知を回避したり,解析者に自身の特徴を把握させづらくしたりします.
紹介する手法
今回のブログでは,「CPUID 命令と RDTSC 命令による VM の検知」という対解析機能を紹介します.
そのため,CPUID 命令とRDTSC 命令を簡潔に説明します.
[3] によると,CPUID 命令は CPU に関する情報を取得するための命令で,VM か否かで実行時間が大きく異なるという特徴があるそうです.
また,RDTSC 命令は経過時刻を EAX レジスタへ格納する命令のようです.
したがって,下記の手順で VM を検知することができそうです.
- RDTSC 命令の結果を保持する変数を2つ用意する.
- CPUID 命令を実行する前後で RDTSC 命令を実行し,その結果をそれぞれの変数へ保存する.
- 2つの変数の差を実行時間とし,その値からプログラムが VM で実行されているかを判定する.
実装
Cとインラインアセンブラを用いて,先ほどの手法を実現するプログラムを実装しました.
実装にあたり [4, 5, 6] を参考にしました.
プログラムのソースコードを以下に示します.
#include <stdio.h>
int main(void) {
// 1. RDTSC命令の結果を保持する変数を2つ用意する.
unsigned int time1 = 0;
unsigned int time2 = 0;
// 2. CPUID命令を実行する前後でRDTSC命令を実行し,その結果をそれぞれの変数へ保存する.
__asm__(
"rdtsc\n\t"
"mov %%eax, %0\n\t"
"cpuid\n\t"
"rdtsc\n\t"
"mov %%eax, %1\n\t"
: "=r" (time1), "=r" (time2)
:
: "%eax", "%ebx", "%ecx", "%edx"
);
// 3. 2つの変数の差分を実行時間とし,その値からプログラムが VM で実行されているかを判定する.
unsigned int time = time2 - time1;
printf("time2 = %u\n", time2);
printf("time1 = %u\n", time1);
printf("time2 - time1 = %u\n", time);
if (time > 10000) {
printf("%s", "VM detected!\n");
} else {
printf("%s", "VM not detected.\n");
}
return 0;
}
情報理工基礎演習で Processing,計算機科学入門でアセンブリ言語を学習した1回生でも何とか読めそうでしょうか?
とはいえ,__asm__(); の部分が独特なので,この部分の読み方を説明しておきます.
まず,"\n¥t" や "=r",: といった記号は「おまじない」と思っても,このブログを読むうえでは問題ありません(詳細を知りたい方は [4] をご覧ください).
11, 12行目の rdtsc と mov はセットと考えましょう.
rdtsc 命令により,eax レジスタに経過時刻が代入されます.
mov 命令により,%0,今回の場合では time1 に eax レジスタの値が代入されます.
この2つの命令により,経過時刻を変数へ保存する処理を実現しています.
13行目の cpuid は,VM か否かで実行時間が大きく異なるという特徴がある命令です.
14, 15行目に rdtsc と mov のセットが再び登場しています.
これは先ほどと同様です.
実験
実装したプログラムを用いて,実際に VM の検知を試みました.
実行ファイルの作成
ノート PC にインストールした MinGW を用いて,先ほどのプログラムをコンパイルすることで, Windows の 実行ファイルを作成しました.
また,WSL 1 あるいは VM にインストールした gcc を用いること以外は同様の手順で Ubuntu の実行ファイル を作成しました.
実行環境の準備
実験で使用した機器は,Windows 10 Home のノートPCです.
このノート PC に インストールした VirtualBox を用いて,WIndows 10 Home と Ubuntu 20.04 の VM を用意しました.
VirtualBox は CPU のコア数やメモリのサイズを指定して VM を作成することができますが,今回の実験ではデフォルトの設定で作成しました.
また,せっかくなので Windows Subsystem for Linux (WSL 1) を用いた Ubuntu 20.04 と Windows 7 Professional をインストールした Cuckoo Sandbox も用意しました.
それぞれの実行環境のスペックを以下の表に示します.
名称 | VM | CPU | Memory |
Windows 10 Home (Normal) | Core i3 | 12.0 GB | |
Ubuntu 20.04 (WSL 1) | Core i3 | 12.0 GB | |
Windows 10 Home (VM) | 〇 | Core i3 | 2 GB |
Ubuntu 20.04 (VM) | 〇 | Core i3 | 1 GB |
Windows 7 Professional (Cuckoo Sandbox) | 〇 | Core i7 | 1 GB |
結果
それぞれの実行環境での実行時間,および検知の成否を以下の表と画像で示します.
表や画像が示すように,いずれの環境においても VM か否かを正しく判定していました.
したがって,CPUID 命令と RDTSC 命令を用いることで VM の検知が可能と言えます.
名称 | 実行時間 | VM (正解) | 判定 | 成否 |
Windows 10 Home (Normal) | 3524 | Not VM | 成功 | |
Ubuntu 20.04 (WSL 1) | 2294 | Not VM | 成功 | |
Windows 10 Home (VM) | 733888 | 〇 | VM | 成功 |
Ubuntu 20.04 (VM) | 84522 | 〇 | VM | 成功 |
Windows 7 Professional (Cuckoo Sandbox) | 31509 | 〇 | VM | 成功 |
終わりに
今回は,CPUID 命令と RDTSC 命令を用いて VM を検知する手法の紹介,および実装をしました.
「実行時間を計測する」という単純なプログラムだったので,理解しやすかったと思います.
また,マルウェアを模したプログラムを作って解析しようとすることで,マルウェア解析だけでなく,マルウェア解析のための環境構築も勉強することができます.
皆さんも是非とも自分の PC で実験してみましょう.
参考
- 吉川孝志:マルウェア解析チュートリアル<マルウェア解析のはじめかた編>,三井物産セキュアディレクション株式会社(オンライン),入手先〈https://www.mbsd.jp/research/20200910.html〉(参照 2022-05-30).
- cocomelonc(オンライン),入手先〈https://cocomelonc.github.io/〉(参照 2022-07-01).
- 大山恵弘:マルウェアによるRDTSC命令の利用,Qiita(オンライン),入手先〈https://qiita.com/y_oyama/items/9ef61653b69e50bbbeea〉(参照 2022-07-30).
- Hazy Moon:GCC Inline Assembler(オンライン),入手先〈https://www.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html〉(参照 2022-07-30).
- blackwhitebear:rdtsc命令とcpuid命令を使ったVM検知,はてなブログ(オンライン),入手先〈https://blackwhitebear.hateblo.jp/entry/2018/01/05/130709〉(参照 2022-07-30).
- Intel:プロセッサの識別とCPUID命令,pp.15-19(オンライン),入手先〈https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/Processor_Identification_071405_i.pdf〉(参照 2022-07-30).