概要
システムが起動してからの時間をミリ秒単位で取得する関数です。GetTickCount関数は、最大49.7日間まで経過時間を取得できます。49.7日以上の経過時間を取得するためには、GetTickCount64関数を使用します。
C++
DWORD GetTickCount(void);
C++
ULONGLONG GetTickCount64(void);
利用方法は2つあります。1つ目は、関数を1回実行しシステム起動時間をあらかじめ用意した値と比較する方法です。システム起動時間が小さくなる仮想環境の検知で使われます。2つ目は、関数を2回実行し得た2つの時間の差分(動作の処理時間)をあらかじめ用意した値と比較する方法です。ブレークポイントの設定やステップ実行によって通常処理時より時間が掛かるデバッガーの検知で使われます。このように、攻撃者は、基準値以上か基準値未満かによって作成したマルウェアが解析されているかどうかを判断して、動作を分岐させます。
ほかにも、指定した時間だけ処理を停止するSleep関数と組み合わせることで、サンドボックス型の解析システムがスリープ時間を短縮させる動作を検出することも可能です。
実装の例
C++/x86 Assemblyのコード例を以下に示します。今回は基準値を120,000ミリ秒と1,500ミリ秒の2つ用意しています。
(1) GetTickCountのサンプルコード(仮想環境かどうかを確認する場合)
C++
DWORD bGetTickCount = GetTickCount();
if ( bGetTickCount < 120000 ) {
exit(1);
}
else
{
//任意の動作
}
Assembly(x86)
push ebp
mov ebp, esp
push ecx
call ds:GetTickCount
mov [ebp+var_4], eax
cmp [ebp+var_4], 120000
jnb short loc_virtualmachine_not_found
push 1
call exit
(2) GetTickCountのサンプルコード(デバッグ実行中かどうかを確認する場合)
C++
DWORD bGetTickCount = GetTickCount();
DWORD aGetTickCount = GetTickCount();
if ( aGetTickCount-bGetTickCount > 1500 ) {
exit(1);
}
else {
//任意の動作
}
Assembly(x86)
push ebp
mov ebp, esp
sub esp, 8
call ds:GetTickCount
mov [ebp+var_8], eax
call ds:GetTickCount
mov [ebp+var_4], eax
mov eax, [ebp+var_4]
sub eax, [ebp+var_8]
cmp eax, 1500
jbe short loc_debugger_not_found
push 1
call exit
(3) GetTickCount64のサンプルコード(仮想環境かどうかを確認する場合)
C++
ULONGLONG bGetTickCount64 = GetTickCount64();
if( bGetTickCount64 < 120000 ) {
exit(1);
}
else {
//任意の動作
}
Assembly(x86)
push ebp
mov ebp, esp
sub esp, 8
call ds:GetTickCount64
mov [ebp+var_8], eax
mov [ebp+var_4], edx
cmp [ebp+var_4], 0
ja short loc_virtualmachine_not_found
jb short loc_virtualmachine_found
cmp [ebp+var_8], 120000
jnb short loc_virtualmachine_not_found
push 1
call exit
(4) GetTickCount64のサンプルコード(デバッグ実行中かどうかを確認する場合)
C++
ULONGLONG bGetTickCount64 = GetTickCount64();
ULONGLONG aGetTickCount64 = GetTickCount64();
if( aGetTickCount64-bGetTickCount64 > 1500 ) {
exit(1);
}
else {
//任意の動作
}
Assembly(x86)
push ebp
mov ebp, esp
sub esp, 24
call ds:GetTickCount64
mov [ebp+var_10], eax
mov [ebp+var_C], edx
call ds:GetTickCount64
mov [ebp+var_8], eax
mov [ebp+var_4], edx
mov eax, [ebp+var_8]
sub eax, [ebp+var_10]
mov ecx, [ebp+var_4]
sbb ecx, [ebp+var_C]
mov [ebp+var_18], eax
mov [ebp+var_14], ecx
cmp [ebp+var_14], 0
ja short loc_debbuger_found
cmp [ebp+var_10], 1500
jbe short loc_debugger_not_found
push 1
call exit
回避手法
GetTickCount/GetTickCount64によるデバッガー/仮想環境の検出を回避し、プログラムを解析する手法を以下に示します。一時的に検出を回避するときには1、2の手法を、恒久的に検出を回避するときには3の手法を使います。
- デバッグ中にレジスタの値を変更する
GetTickCount/GetTickCount64を呼び出した後に、CMP命令の結果が0になるように値を変更します。または、ZFの値を1にします。 - プラグインを利用する
ScyllaHide(Ollydbg, x64dbgなどで利用可能なプラグイン)の”GetTickCount”又は”GetTickCount64”の項目にチェックを入れることで回避できます。実装例のような最低限の分岐では、プラグインがうまく機能しない場合があります。(2)の手法については、回避を確認しています。 - プログラムを書き換える
GetTickCount/ GetTickCount64を呼び出した後の分岐命令を、ja命令またはjae命令に書き換え、変更を保存します。
検出手法
GetTickCountとGetTickCount64を検出するためのYARA ルールを以下に紹介します。
YARAルール
rule Win32API_GetTickCount_GetTickCount64
{
strings:
$str1 = "GetTickCount"
$str2 = "GetTickCount64"
condition:
any of them
}