检测是否存在于沙箱中

检测运行时间

CPUID

上述的几种方法都是基于延时执行或者由其引出的一些特性来进行沙箱检测的。由于虚拟化自身的特性,也会造成某些指令的执行在虚拟环境和真实环境中有时间差异。

得益于Hypervisor的普及,许多作弊软件制作者利用虚拟化技术逃避了反作弊引擎的检测。与此对应,反作弊引擎的厂商也没有坐以待毙,研究出了一系列检测虚拟化的方法。接下来要介绍的就是来自BattlEye的反沙箱方法。

原理:
首先我们要知道CPUID是什么,它为什么能够作为检测沙箱的指标:CPUID 在真实硬件上是一条相对快捷的指令,通常只需要 200 个周期,而在虚拟化环境中,由于内省引擎产生的开销,它可能需要十倍甚至更多的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 old_priority = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); 
unsigned __int64 rdtsc_first = __rdtsc();
Sleep(2000);
unsigned __int64 rdtsc_time_delta = __rdtsc() - rdtsc_first;
DWORD64 result_time = 0;
int cpuid_data[4] = { 0 };
for (std::size_t count = 0; count < 0x6694; count++)
{
auto rdtsc_iter_time = __rdtsc();

__cpuid((int*)cpuid_data, 0);

result_time += __rdtsc() - rdtsc_iter_time;
}
unsigned __int64 _time = 10000000 * result_time / rdtsc_time_delta / 0x65;
if (_time > 400 || _time < 10)
exit(0);

SetThreadPriority(GetCurrentThread(), old_priority);

上面这段代码就是这种方法的关键部分,通过计时器计算cpuid执行的平均CPU周期,假如与真实环境的CPU周期差距过大则判断为沙箱,退出程序。

正如一开始说的,CPUID在虚拟化环境中,占用的CPU周期会大幅上升。本质原因就在于这类指令会触发vmexit,在调用一些虚拟机无法完成指令的时候,就会触发它退出虚拟化,然后再执行指令。假如可以绕过vmexit也就绕过了这种检测方式。

不得不说逆向出battleye反沙箱代码的国外大佬不仅技术厉害,在产品方面更有独到的见解。虽然没有直接给出改进的代码,但是给出了反反沙箱的方法和更新建议。

基于FYL2XP1的CPUID执行时间检测

说白了,就是以FYL2XP1这条命令的执行时间作为参照物来分析CPUID。

对于FYL2XP1这条命令其实不需要了解太多,我们只需要知道两点:

1.FYL2XP1是一条算术运算指令,平均执行时间正常情况下比CPUID长。

2.这条指令可以可靠的计时。

所以当CPUID指令的执行时间比FYL2XP1指令的平均执行时间长,就可以确定该系统是虚拟化的。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
constexpr aubool take_time()
{

constexpr auto measure_time = 5;

long long __cpuid_time = 0;
long long __fyl2xp1_time = 0;

LARGE_INTEGER frequency = {};
LARGE_INTEGER start = {};
LARGE_INTEGER end = {};

QueryPerformanceFrequency(&frequency);

// count the average time it takes to execute a CPUID instruction
for (std::size_t i = 0; i < measure_time; ++i)
{
QueryPerformanceCounter(&start);
_cpuid_buffer_t cpuid_data;
__cpuid(reinterpret_cast<int*>(&cpuid_data), 1);
QueryPerformanceCounter(&end);

auto delta = end.QuadPart - start.QuadPart;

delta *= 1000000000;
delta /= frequency.QuadPart;

__cpuid_time += delta;
}

// count the average time it takes to execute a FYL2XP1 instruction
for (std::size_t i = 0; i < measure_time; ++i)
{
QueryPerformanceCounter(&start);
#ifdef _WIN64
_asm_fyl2xp1();
#else
_asm FYL2XP1
#endif
QueryPerformanceCounter(&end);

auto delta = end.QuadPart - start.QuadPart;

delta *= 1000000000;
delta /= frequency.QuadPart;

__fyl2xp1_time += delta;
}

return __fyl2xp1_time <= __cpuid_time;
}

bool take_time_cpuid_against_fyl2xp1()
{
constexpr auto measure_times = 5;
auto positives = 0;
auto negatives = 0;

// run the internal VM check multiple times to get an average result
for (auto i = measure_times; i != 0; --i)
take_time() ? ++positives : ++negatives;

// if there are more positive results than negative results, the
// process is likely running inside a VM
const bool decision = (positives >= negatives);

return decision;
}to measure_time = 5;

long long __cpuid_time = 0;
long long __fyl2xp1_time = 0;

LARGE_INTEGER frequency = {};
LARGE_INTEGER start = {};
LARGE_INTEGER end = {};

QueryPerformanceFrequency(&frequency);

// count the average time it takes to execute a CPUID instruction
for (std::size_t i = 0; i < measure_time; ++i)
{
QueryPerformanceCounter(&start);
_cpuid_buffer_t cpuid_data;
__cpuid(reinterpret_cast<int*>(&cpuid_data), 1);
QueryPerformanceCounter(&end);

auto delta = end.QuadPart - start.QuadPart;

delta *= 1000000000;
delta /= frequency.QuadPart;

__cpuid_time += delta;
}

// count the average time it takes to execute a FYL2XP1 instruction
for (std::size_t i = 0; i < measure_time; ++i)
{
QueryPerformanceCounter(&start);
#ifdef _WIN64
_asm_fyl2xp1();
#else
_asm FYL2XP1
#endif
QueryPerformanceCounter(&end);

auto delta = end.QuadPart - start.QuadPart;

delta *= 1000000000;
delta /= frequency.QuadPart;

__fyl2xp1_time += delta;
}

return __fyl2xp1_time <= __cpuid_time;

这段代码截取自github项目Hypervisor-Detection,经过了多组循环测试。take_time循环计算了fy12xp1和cpuid的周期大小并返回平均CPU周期的对比结果。take_time_cpuid_against_fyl2xp1为了确保准确,进行了多组测试。

当然CPUID不止如此,在反沙箱领域中还有其它妙用,后面的篇章我会陆续讲到。

环境类

屏幕分辨率

检查屏幕分辨率是否是常用的分辨率,下面给出一个示例代码

1
2
3
4
5
6
7
8
9
10
void GetMonitorRealResolution(HMONITOR monitor, int* pixelsWidth, int* pixelsHeight)
{
MONITORINFOEX info = { sizeof(MONITORINFOEX) };
winrt::check_bool(GetMonitorInfo(monitor, &info));
DEVMODE devmode = {};
devmode.dmSize = sizeof(DEVMODE);
winrt::check_bool(EnumDisplaySettings(info.szDevice, ENUM_CURRENT_SETTINGS, &devmode));
*pixelsWidth = devmode.dmPelsWidth;
*pixelsHeight = devmode.dmPelsHeight;
}

修改屏幕分辨率就可以很好的针对这种检测,现在应该很少有沙箱会犯这种低级错误。

首选语言

1
2
3
4
5
6
func IFlanguage() {
a, _ := windows.GetUserPreferredUILanguages(windows.MUI_LANGUAGE_NAME)
if a[0] != "zh-CN" {
os.Exit(1)
}
}

正常情况下,我们接触到的都是国内项目,系统都是中文的,但是许多沙箱都是默认配置搭建起来的,所以使用英文系统。获取当前系统首选语言也是一种有效的检测方法。

主机用户/主机名

某些沙箱厂商会使用几个固定的用户名/主机名,在后面的信息搜集类模块会细讲。

检测硬盘大小

2014年首次发现的Emotet银行木马,最近就改头换面重现江湖了。该新版本是带.doc后缀的XML文档,利用大多数沙箱要求真实文件类型的特性规避检测。即便真实文件类型是XML,终端上还是在Word中打开。

一旦在Word中打开,XML文件中的宏就会触发一段PowerShell脚本,连接攻击第二阶段的URL,下载Emotet攻击载荷。Emotet会枚举系统上安装的应用程序并检查磁盘空间以确定自身是否处于沙箱环境。如果判断自身身处沙箱环境,攻击载荷就会停止执行。而且,Emotet还有长期睡眠和延迟机制以阻碍动态分析技术,让沙箱无法检测恶意行为。很聪明的做法!

进程

可以细分为两个方向,一个是检测正常个人电脑不该有进程,比如Vmtoolsd.exe等。另一个是检测正常个人电脑该有的进程,比如微信、QQ、浏览器等。

这个方法可以更进一步的延伸,检测进程地址空间中有没有特定的libraries。比如sbiedll.dll、dbghelp.dll、api_log.dll、vmcheck.dll等。

最近文件

Win+R输入Recent就可以看到当前主机的最近文件,下图是一个正常使用的主机,满满的都是使用痕迹:

图片

如果最近文件夹中的数量少于n,那就可以断定这台主机大概率是沙箱。

临时文件

与最近文件同理。

特定文件

虚拟环境中存在一些真实环境中没有的文件,以Vmware举例。

很明显可以看到多了一些vm开头的文件。但不一定都能作为依据判断虚拟机,因为电脑只要装了Vmware这个工具,也会出现许多vm开头的文件,假如你选择了一个不恰当的文件作为依据,只能判断出这可能是一台虚拟机也可能是安装了Vmware的真实物理机。

注册表

不同的沙箱厂商会有一些特定的注册表路径,还有一些特殊的注册表项值。依旧以我们常见的Vmware举例:

image-20240416124506089

上次开机时间

用过虚拟机的同学,应该都知道有一个很好用的功能就是快照,还原快照后会将当前虚拟机的状态还原到拍摄快照时的状态。而WMI数据库可能包含创建VM快照时的开机时间,如果快照是在一年前创建的,且沙箱仅更新了上次启动的时间,但是系统正常运行时间还是一年,这无疑是不合理的。

这一特性可以检测出从快照恢复的虚拟机,如果命中下面任何一条规则,都很可疑:

1.系统正常运行时间过长。

2.系统正常运行时间太短。(听说有的同学拿到样本现开虚拟机?)

3.多种方式获得的上次开机时间不一致。

CPU运行时间

具体原理见上一条,这里只给出一个简单的demo。

1
2
3
4
5
6
#define MIN_UPTIME_MINUTES 12
BOOL uptime_check()
{
ULONGLONG uptime_minutes = GetTickCount64() / (60 * 1000);
return uptime_minutes < MIN_UPTIME_MINUTES;
}

这里使用的API是不是很眼熟?所以绕过方式应该不用我多说了。

wifi检测

Windows虚拟机有一个特性,就是将所有网络连接识别为有线连接。所以一台没有任何WIFI连接记录的主机是很可疑的

实体机中执行命令 netsh wlan show profiles

检查窗口数量

如果你经常使用的云沙箱,会发现返回测试截图的时候除了我们提交的文件,没有别的窗口存在。从性能角度考虑这是合理的,但同时提升了可疑性。