目录
-
基于时间的沙箱规避技术
-
1. 延迟执行
-
1.1. 简单的延迟操作
-
1.2. 使用任务计划程序延迟执行
-
1.3. 在重新启动之前没有可疑的行动
-
1.4. 只在特定日期运行
-
2. 睡眠跳过检测
-
2.1. 使用不同的方法进行并行延时
-
2.2. 使用不同的方法测量时间间隔
-
2.3. 使用不同的方法获得系统时间
-
2.4. 检查调用延迟函数后延迟值是否发生变化
-
2.5. 使用绝对超时
-
2.6. 从另一个进程获取时间
-
3. 从外部来源(NTP、HTTP)获取当前日期和时间
-
4. 虚拟机和主机中的时间测量差异
-
4.1. RDTSC(使用CPUID强制退出虚拟机)
-
4.2. RDTSC(带有GetProcessHeap和CloseHandle的Locky版本)
-
5. 使用不同的方法检查系统的最后启动时间
-
反制措施
-
归功于
基于时间的沙箱规避技术
沙盒伪装通常持续时间很短,因为沙盒装载了大量的样本。伪装时间很少超过3-5分钟。因此,恶意软件可以利用这一事实来避免被发现:它可能在开始任何恶意活动之前进行长时间的延迟。
为了抵制这种情况,沙盒可能会实现操纵时间和执行延迟的功能。例如,Cuckoo沙箱有一个跳过睡眠的功能,用一个非常短的值取代延迟。这应迫使恶意软件在分析超时前开始其恶意活动。
然而,这也可以用来检测沙盒。
在一些指令和API函数的执行时间上也有一些差异,可以用来检测虚拟环境。
没有为这一类技术提供签名建议,因为执行本章中描述的函数并不意味着它们被用于规避目的。很难区分旨在执行规避代码的代码和以非规避目的使用相同函数的代码。
1. 延迟执行
执行延迟用于避免在伪装期间检测到恶意活动。
1.1. 简单的延迟操作
使用的函数:
-
Sleep, SleepEx, NtDelayExecution
-
WaitForSingleObject, WaitForSingleObjectEx, NtWaitForSingleObject
-
WaitForMultipleObjects, WaitForMultipleObjectsEx, NtWaitForMultipleObjects
-
SetTimer, SetWaitableTimer, CreateTimerQueueTimer
-
timeSetEvent (multimedia timers)
-
IcmpSendEcho
-
select (Windows sockets)
虽然这些函数的大部分使用是显而易见的,但我们展示了使用多媒体API的timeSetEvent函数和Windows套接字API的select函数的例子。
代码示例(使用 “select “函数进行延迟):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
int iResult; DWORD timeout = delay; // delay in milliseconds DWORD OK = TRUE; SOCKADDR_IN sa = { 0 }; SOCKET sock = INVALID_SOCKET; // this code snippet should take around Timeout milliseconds do { memset (&sa, 0, sizeof (sa)); sa.sin_family = AF_INET; sa.sin_addr.s_addr = inet_addr( "8.8.8.8" ); // we should have a route to this IP address sa.sin_port = htons(80); // we should not be able to connect to this port sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock == INVALID_SOCKET) { OK = FALSE; break ; } // setting socket timeout unsigned long iMode = 1; iResult = ioctlsocket(sock, FIONBIO, &iMode); iResult = connect(sock, (SOCKADDR*)&sa, sizeof (sa)); if (iResult == false ) { OK = FALSE; break ; } iMode = 0; iResult = ioctlsocket(sock, FIONBIO, &iMode); if (iResult != NO_ERROR) { OK = FALSE; break ; } // fd set data fd_set Write, Err; FD_ZERO(&Write); FD_ZERO(&Err); FD_SET(sock, &Write); FD_SET(sock, &Err); timeval tv = { 0 }; tv.tv_usec = timeout * 1000; // check if the socket is ready, this call should take Timeout milliseconds select(0, NULL, &Write, &Err, &tv); if (FD_ISSET(sock, &Err)) { OK = FALSE; break ; } } while ( false ); if (sock != INVALID_SOCKET) closesocket(sock); |
代码示例(使用 “timeSetEvent “函数进行延迟):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
VOID CALLBACK TimerFunction( UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2) { bProcessed = TRUE; } VOID timing_timeSetEvent( UINT delayInSeconds) { // Some vars UINT uResolution; TIMECAPS tc; MMRESULT idEvent; // We can obtain this minimum value by calling timeGetDevCaps(&tc, sizeof (TIMECAPS)); uResolution = min(max(tc.wPeriodMin, 0), tc.wPeriodMax); // Create the timer idEvent = timeSetEvent( delayInSeconds, uResolution, TimerFunction, 0, TIME_ONESHOT); while (!bProcessed){ // wait until our function finishes Sleep(0); } // destroy the timer timeKillEvent(idEvent); // reset the timer timeEndPeriod(uResolution); } |
1.2. 使用任务计划程序延迟执行
这种方法既可用于延迟执行,也可用于躲避沙盒追踪。
代码样本(PowerShell):
1
2
3
4
|
$ tm = (get-date).AddMinutes(10).ToString( "HH:mm" ) $action = New-ScheduledTaskAction -Execute "some_malicious_app.exe" $trigger = New-ScheduledTaskTrigger -Once -At $ tm Register-ScheduledTask TaskName -Action $action -Trigger $trigger |
1.3. 在重新启动之前没有可疑的行动
这种技术背后的想法是,在模拟恶意样本的过程中,沙盒不会重启虚拟机。恶意软件可能只是使用任何可用的方法设置了持久性,并悄悄退出。只有在系统重新启动后,才会进行恶意操作。
1.4. 只在特定日期运行
恶意软件样本可能会检查当前日期,只在某些日期执行恶意行动。例如,在Sazoora恶意软件中使用了这种技术,它检查当前日期并验证这一天是否是某个月的第16、17或18号。
示例:
反制措施:
这类规避技术的对策应该是全面的,并且包括所有描述的攻击向量。实现不可能简单,对它的描述应该单独一条。因此,我们在此仅提供一般性建议:
-
实现睡眠跳过。
-
全系统动态时间流速度操纵。
-
在不同日期多次运行仿真。
虽然在Cuckoo沙盒中已经实现了睡眠跳过,但它很容易被欺骗。创建新线程或进程后,将禁用睡眠跳过,以避免检测到睡眠跳过。但是,它仍然可以很容易地检测到,如下所示。
2. 睡眠跳过检测
这种类型的技术一般是针对Cuckoo监控器的睡眠跳过功能和其他时间操纵技术,这些技术可以在沙盒中使用,以跳过恶意软件执行的长时间延迟。
2.1. 使用不同的方法进行并行延时
这些技术背后的想法是并行地执行不同类型的延迟,并测量运行时间。
代码样本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
DWORD StartingTick, TimeElapsedMs; LARGE_INTEGER DueTime; HANDLE hTimer = NULL; TIMER_BASIC_INFORMATION TimerInformation; ULONG ReturnLength; hTimer = CreateWaitableTimer(NULL, TRUE, NULL); DueTime.QuadPart = Timeout * (-10000LL); StartingTick = GetTickCount(); SetWaitableTimer(hTimer, &DueTime, 0, NULL, NULL, 0); do { Sleep(Timeout/10); NtQueryTimer(hTimer, TimerBasicInformation, &TimerInformation, sizeof (TIMER_BASIC_INFORMATION), &ReturnLength); } while (!TimerInformation.TimerState); CloseHandle(hTimer); TimeElapsedMs = GetTickCount() - StartingTick; printf ( "Requested delay: %d, elapsed time: %d\n" , Timeout, TimeElapsedMs); if ( abs (( LONG )(TimeElapsedMs - Timeout)) > Timeout / 2) printf ( "Sleep-skipping DETECTED!\n" ); |
在上面的代码示例中,延迟超时是用SetWaitableTimer()计时器函数设置的。Sleep()函数被循环调用,直到定时器超时。在Cuckoo沙盒中,由Sleep()函数执行的延迟会被跳过(用一个很短的超时来代替),实际运行时间会比要求的超时高很多:
1
2
|
Requested delay: 60000, elapsed time : 1906975 Sleep-skipping DETECTED! |
2.2. 使用不同的方法测量时间间隔
我们需要执行一个将在沙盒中跳过的延迟,并使用不同的方法来测量运行时间。虽然Cuckoo监控器钩住了GetTickCount(), GetLocalTime(), GetSystemTime()并使它们返回跳过的时间,但我们仍然可以找到Cuckoo监控器没有处理的测量时间的方法。
使用的函数:
-
GetTickCount64
-
QueryPerformanceFrequency, QueryPerformanceCounter
-
NtQuerySystemInformation
代码样本(使用“QueryPerformanceCounter”测量运行时间):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
LARGE_INTEGER StartingTime, EndingTime; LARGE_INTEGER Frequency; DWORD TimeElapsedMs; QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&StartingTime); Sleep(Timeout); QueryPerformanceCounter(&EndingTime); TimeElapsedMs = ( DWORD )(1000ll * (EndingTime.QuadPart - StartingTime.QuadPart) / Frequency.QuadPart); printf ( "Requested delay: %d, elapsed time: %d\n" , Timeout, TimeElapsedMs); if ( abs (( LONG )(TimeElapsedMs - Timeout)) > Timeout / 2) printf ( "Sleep-skipping DETECTED!\n" ); |
代码样本(使用 “GetTickCount64 “测量运行时间):
1
2
3
4
5
6
7
8
9
10
11
|
ULONGLONG tick; DWORD TimeElapsedMs; tick = GetTickCount64(); Sleep(Timeout); TimeElapsedMs = GetTickCount64() - tick; printf ( "Requested delay: %d, elapsed time: %d\n" , Timeout, TimeElapsedMs); if ( abs (( LONG )(TimeElapsedMs - Timeout)) > Timeout / 2) printf ( "Sleep-skipping DETECTED!\n" ); |
我们也可以使用我们自己的GetTickCount实现来检测睡眠跳动。在接下来的代码示例中,我们直接从KUSER_SHARED_DATA结构中获取tick计数。这样,即使GetTickCount()函数被拦截了,我们也能得到原始的tick计数值。
代码样本(从KUSER_SHARED_DATA结构中获取tick计数):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#define KI_USER_SHARED_DATA 0x7FFE0000 #define SharedUserData ((KUSER_SHARED_DATA * const) KI_USER_SHARED_DATA) #define MyGetTickCount() ((DWORD)((SharedUserData->TickCountMultiplier * (ULONGLONG)SharedUserData->TickCount.LowPart) >> 24)) // ... StartingTick = MyGetTickCount(); Sleep(Timeout); TimeElapsedMs = MyGetTickCount() - StartingTick; printf ( "Requested delay: %d, elapsed time: %d\n" , Timeout, TimeElapsedMs); if ( abs (( LONG )(TimeElapsedMs - Timeout)) > Timeout / 2) printf ( "Sleep-skipping DETECTED!\n" ); |
2.3. 使用不同的方法获得系统时间
这种方法与前一种方法类似。我们尝试用不同的方法来获得当前的系统时间,而不是测量间隔时间。
代码样本:
1
2
3
4
5
6
7
8
9
10
11
|
SYSTEM_TIME_OF_DAY_INFORMATION SysTimeInfo; ULONGLONG time ; LONGLONG diff; Sleep(60000); // should trigger sleep skipping GetSystemTimeAsFileTime((LPFILETIME)& time ); NtQuerySystemInformation(SystemTimeOfDayInformation, &SysTimeInfo, sizeof (SysTimeInfo), 0); diff = time - SysTimeInfo.CurrentTime.QuadPart; if ( abs (diff) > 10000000) // differ in more than 1 second printf ("Sleep-skipping DETECTED!\n); |
2.4. 检查调用延迟函数后延迟值是否发生变化
睡眠跳过通常被实现为用一个较小的间隔来替换延迟值。让我们看一下NtDelayExecution函数。延迟值是用一个指针传递给这个函数的:
1
2
3
4
|
NTSYSAPI NTSTATUS NTAPI NtDelayExecution( IN BOOLEAN Alertable, IN PLARGE_INTEGER DelayInterval ); |
因此,我们可以检查DelayInterval的值在函数执行后是否改变。如果该值与初始值不同,则跳过了延迟。
代码样本:
1
2
3
4
5
|
LONGLONG SavedTimeout = Timeout * (-10000LL); DelayInterval->QuadPart = SavedTimeout; status = NtDelayExecution(TRUE, DelayInterval); if (DelayInterval->QuadPart != SavedTimeout) printf ( "Sleep-skipping DETECTED!\n" ); |
2.5. 使用绝对超时
对于执行延迟的Nt-函数,我们可以使用相对延迟间隔或绝对超时时间。延迟间隔的负值意味着相对超时,而正值意味着绝对超时。高级别的API函数,如WaitForSingleObject()或Sleep(),都是用相对的时间间隔来操作。因此,沙盒开发人员可能不关心绝对超时,并错误地处理它们。在Cuckoo沙盒中,这种延迟会被跳过,但跳过的时间和刻度会被错误地计算。这可以用来检测睡眠跳过。
代码样本:
1
2
3
4
5
6
7
8
|
void SleepAbs( DWORD ms) { LARGE_INTEGER SleepUntil; GetSystemTimeAsFileTime((LPFILETIME)&SleepUntil); SleepTo.QuadPart += (ms * 10000); NtDelayExecution(TRUE, &SleepTo); } |
2.6. 从另一个进程获取时间
Cuckoo沙盒中的睡眠跳过不是全系统的。因此,如果有执行延迟,时间在不同的进程中以不同的速度移动。在延迟之后,我们应该同步进程并比较两个进程的当前时间。如果测量到的时间值有很大的差异,说明进行了睡眠跳转。
当前版本的Cuckoo监控器在创建新线程或进程后禁用了睡眠跳转。因此,我们应该使用不被Cuckoo监控器跟踪的进程创建方法,例如,使用一个计划任务。
3. 从外部来源(NTP、HTTP)获取当前日期和时间
沙盒可以设置不同的日期,以检查分析样本的行为是如何根据日期而改变的。恶意软件可以使用外部日期和时间源来防止虚拟机内的时间操纵企图。这种方法也可用于测量时间间隔,执行延迟,并检测跳过睡眠的尝试。NTP服务器,以及HTTP头 “Date “可以作为日期和时间的外部来源。例如,恶意软件可以连接到google.com来检查当前日期,并将其作为DGA种子。
反制措施:
实施假的网络基础设施或欺骗NTP数据和由真正的服务器返回的HTTP头。返回/欺骗的日期和时间应与虚拟机中的日期和时间同步。
4. 虚拟机和主机中的时间测量差异
一些API函数和指令的执行在虚拟机和通常的主机系统中可能需要不同的时间。这些特殊性可以用来检测虚拟环境。
4.1. RDTSC(使用CPUID强制退出虚拟机)
代码样本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
BOOL rdtsc_diff_vmexit() { ULONGLONG tsc1 = 0; ULONGLONG tsc2 = 0; ULONGLONG avg = 0; INT cpuInfo[4] = {}; // Try this 10 times in case of small fluctuations for ( INT i = 0; i < 10; i++) { tsc1 = __rdtsc(); __cpuid(cpuInfo, 0); tsc2 = __rdtsc(); // Get the delta of the two RDTSC avg += (tsc2 - tsc1); } // We repeated the process 10 times so we make sure our check is as much reliable as we can avg = avg / 10; return (avg < 1000 && avg > 0) ? FALSE : TRUE; } |
4.2. RDTSC(带有GetProcessHeap和CloseHandle的Locky版本
代码样本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#define LODWORD(_qw) ((DWORD)(_qw)) BOOL rdtsc_diff_locky() { ULONGLONG tsc1; ULONGLONG tsc2; ULONGLONG tsc3; DWORD i = 0; // Try this 10 times in case of small fluctuations for (i = 0; i < 10; i++) { tsc1 = __rdtsc(); // Waste some cycles - should be faster than CloseHandle on bare metal GetProcessHeap(); tsc2 = __rdtsc(); // Waste some cycles - slightly longer than GetProcessHeap() on bare metal CloseHandle(0); tsc3 = __rdtsc(); // Did it take at least 10 times more CPU cycles to perform CloseHandle than it took to perform GetProcessHeap()? if ((LODWORD(tsc3) - LODWORD(tsc2)) / (LODWORD(tsc2) - LODWORD(tsc1)) >= 10) return FALSE; } // We consistently saw a small ratio of difference between GetProcessHeap and CloseHandle execution times // so we're probably in a VM! return TRUE; } |
反制措施
实施RDTSC指令 “拦截”。有可能使RDTSC成为一条特权指令,只能在内核模式下调用。在用户模式下调用 “挂钩 “的RDTSC会导致我们的处理程序的执行,它可以返回任何想要的值。
5. 使用不同的方法检查系统的最后启动时间
这种技术是常规操作系统查询中描述的技术的组合:检查系统的正常运行时间是否很小,以及WMI:检查最后的启动时间部分。根据获取系统最后启动时间的方法,测得的沙盒操作系统运行时间可能太小(几分钟),或者相反,太大(几个月甚至几年),因为系统通常在分析开始后从快照中恢复。
我们可以通过比较通过WMI和NtQuerySystemInformation(SystemTimeOfDayInformation)获得的最后一次启动时间的两个值来检测沙箱。
代码样本:
1
2
3
4
5
6
7
8
9
|
bool check_last_boot_time() { SYSTEM_TIME_OF_DAY_INFORMATION SysTimeInfo; LARGE_INTEGER LastBootTime; NtQuerySystemInformation(SystemTimeOfDayInformation, &SysTimeInfo, sizeof (SysTimeInfo), 0); LastBootTime = wmi_Get_LastBootTime(); return (wmi_LastBootTime.QuadPart - SysTimeInfo.BootTime.QuadPart) / 10000000 != 0; // 0 seconds } |
反制措施:
- 调整KeBootTime值
- 在调整KeBootTime后,重置WMI资源库或重新启动 “winmgmt “服务
暂无评论内容