模块 73 - 反虚拟环境技术 - 多种检测方法
恶意软件开发课程 - 反虚拟环境技术 - 多种检测方法
模块 73 - 反虚拟环境技术 - 多种检测方法#
反虚拟环境技术 - 多种检测方法#
简介#
反虚拟化(Anti-virtualization)在之前的模块中已经有过简单介绍。本模块将深入探讨多种实用的 反虚拟环境(AVE) 技术。
💡 初学者提示:什么是虚拟环境和沙箱?
类比理解: 想象一下你是一个正在研制新型炸药(恶意软件)的科学家。如果你直接在自家的实验室(真实机器)里引爆,万一炸了,你的实验室就毁了。
- 虚拟环境/沙箱:就像是一个加固的、透明的、可瞬间还原的试验室外壳。分析师在这个外壳里运行你的程序,即便程序把系统搞崩溃了,他们只需要点一下“还原快照”,一切就能回到之前的样子。
- 反虚拟化:就是你的程序在运行前先仔细观察:这个实验室墙壁是不是塑料做的?(检测硬件规格);窗外是不是贴的画?(检测显示器分辨率);是不是只有我一个人在?(检测进程数)。如果发现不是真实世界,程序就不真正“引爆”,从而躲过分析。
1. 利用硬件规格进行反虚拟化#
一般来说,虚拟化环境无法完全访问宿主机的硬件。这种访问限制可以被恶意软件利用,用来探测它是否在虚拟环境或沙箱中运行。需要注意的是,这种方法的准确性并不能 100% 保证,因为某些真实的低配物理机也可能具有较低的硬件规格。
我们将检查以下硬件规格:
- CPU - 检查处理器核心数是否少于 2 个。
- RAM - 检查内存是否少于 2 GB。
- 曾挂载的 USB 数量 - 检查系统中记录的 USB 设备是否少于 2 个。
📚 知识扩展:为什么沙箱的规格通常都很低?
成本与效率的平衡: 安全厂商(如 Any.run, Joe Sandbox)每天需要分析成千上万个文件。
- 资源成本:如果每个分析任务都分配 8 核 CPU 和 16GB 内存,服务器成本会极高。
- 效率:分配更少的资源能让一台服务器同时启动更多的虚拟机。
- 典型配置:因此,大多数在线沙箱默认只分配 1-2 个 CPU 核心和 2GB 左右的内存。
CPU 检测#
可以使用 GetSystemInfo WinAPI。它会返回一个 SYSTEM_INFO 结构体,其中包含关于系统的信息,包括处理器数量。
SYSTEM_INFO SysInfo = { 0 };
GetSystemInfo(&SysInfo);
if (SysInfo.dwNumberOfProcessors < 2){
// 可能是虚拟化环境
}cRAM 检测#
可以通过 GlobalMemoryStatusEx WinAPI 来检查。该函数返回一个 MEMORYSTATUSEX 结构体,其 ullTotalPhys 成员包含以字节为单位的物理内存总量。
MEMORYSTATUSEX MemStatus = { .dwLength = sizeof(MEMORYSTATUSEX) };
if (!GlobalMemoryStatusEx(&MemStatus)) {
printf("\n\t[!] GlobalMemoryStatusEx 失败,错误码 : %d \n", GetLastError());
}
// 2 * 1073741824 是 2GB 的字节数
if ((DWORD)MemStatus.ullTotalPhys <= (DWORD)(2 * 1073741824)) {
// 可能是虚拟化环境
}cUSB 曾挂载历史检测#
最后,可以通过访问注册表键 HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USBSTOR 来检查系统曾挂载过的 USB 数量。可以使用 RegOpenKeyExA 和 RegQueryInfoKeyA 来获取子键数量。
HKEY hKey = NULL;
DWORD dwUsbNumber = NULL;
DWORD dwRegErr = NULL;
if ((dwRegErr = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\ControlSet001\\Enum\\USBSTOR", NULL, KEY_READ, &hKey)) != ERROR_SUCCESS) {
// 错误处理...
}
if ((dwRegErr = RegQueryInfoKeyA(hKey, NULL, NULL, NULL, &dwUsbNumber, NULL, NULL, NULL, NULL, NULL, NULL, NULL)) != ERROR_SUCCESS) {
// 错误处理...
}
// 挂载过的 USB 数量少于 2 个
if (dwUsbNumber < 2) {
// 可能是虚拟化环境
}c2. 硬件规格检测完整代码#
IsVenvByHardwareCheck 函数整合了上述逻辑:
BOOL IsVenvByHardwareCheck() {
SYSTEM_INFO SysInfo = { 0 };
MEMORYSTATUSEX MemStatus = { .dwLength = sizeof(MEMORYSTATUSEX) };
HKEY hKey = NULL;
DWORD dwUsbNumber = NULL;
DWORD dwRegErr = NULL;
// 1. CPU 检测
GetSystemInfo(&SysInfo);
if (SysInfo.dwNumberOfProcessors < 2){
return TRUE;
}
// 2. RAM 检测
if (!GlobalMemoryStatusEx(&MemStatus)) {
return FALSE;
}
if ((DWORD)MemStatus.ullTotalPhys < (DWORD)(2 * 1073741824)) {
return TRUE;
}
// 3. 曾挂载 USB 数量检测
if ((dwRegErr = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\ControlSet001\\Enum\\USBSTOR", NULL, KEY_READ, &hKey)) != ERROR_SUCCESS) {
return FALSE;
}
if ((dwRegErr = RegQueryInfoKeyA(hKey, NULL, NULL, NULL, &dwUsbNumber, NULL, NULL, NULL, NULL, NULL, NULL, NULL)) != ERROR_SUCCESS) {
return FALSE;
}
if (dwUsbNumber < 2) {
return TRUE;
}
RegCloseKey(hKey);
return FALSE;
}c3. 利用机器分辨率进行反虚拟化#
在沙箱环境中,分辨率和显示属性通常被设置为标准化的固定值(例如 800x600 或 1024x768),这与真实用户的机器大不相同。
💡 初学者提示:屏幕分辨率检测逻辑
类比理解: 就像你去面试,如果面试官告诉你他在北京,但你发现他的“窗外”显示的分辨率比例极其奇怪,或者甚至没有副显示器(恶意软件通常希望在拥有更高分辨率或多屏环境的真实用户机器上运行),你就会怀疑这是不是一个布景。 程序员通常会使用高清显示器(1080P 或更高)。一个只有 800x600 分辨率且没有 USB 挂载记录的系统,极大概率是用来批量分析程序的“马甲机”。
编程实现:
- 使用
EnumDisplayMonitors枚举显示器。 - 在回调函数中使用
GetMonitorInfoW获取每一个显示器的分辨率。 - 通过
MONITORINFO.rcMonitor.right - MONITORINFO.rcMonitor.left计算宽度 (X)。 - 通过
MI.rcMonitor.top - MI.rcMonitor.bottom计算高度 (Y)。
// 回调函数
BOOL CALLBACK ResolutionCallback(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lpRect, LPARAM ldata) {
int X = 0, Y = 0;
MONITORINFO MI = { .cbSize = sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &MI)) {
return FALSE;
}
X = MI.rcMonitor.right - MI.rcMonitor.left;
Y = MI.rcMonitor.top - MI.rcMonitor.bottom;
// 处理负值
if (X < 0) X = -X;
if (Y < 0) Y = -Y;
// 检查是否不在常见分辨率之列
if ((X != 1920 && X != 2560 && X != 1440) || (Y != 1080 && Y != 1200 && Y != 1600 && Y != 900))
*((BOOL*)ldata) = TRUE; // 探测到沙箱
return TRUE;
}
BOOL CheckMachineResolution() {
BOOL SANDBOX = FALSE;
EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)ResolutionCallback, (LPARAM)(&SANDBOX));
return SANDBOX;
}c4. 利用文件名进行反虚拟化#
沙箱(如 Any.run 或 Cuckoo)经常会重命名文件作为分类手段(例如改为文件的 MD5 哈希值)。这通常会导致文件名包含大量的数字。
ExeDigitsInNameCheck 函数统计当前文件名中的数字数量。
BOOL ExeDigitsInNameCheck() {
CHAR Path [MAX_PATH * 3];
CHAR cName [MAX_PATH];
DWORD dwNumberOfDigits = NULL;
// 获取文件全路径
if (!GetModuleFileNameA(NULL, Path, MAX_PATH * 3)) {
return FALSE;
}
// 从全路径中提取文件名
if (lstrlenA(PathFindFileNameA(Path)) < MAX_PATH)
lstrcpyA(cName, PathFindFileNameA(Path));
// 统计数字个数
for (int i = 0; i < lstrlenA(cName); i++){
if (isdigit(cName[i]))
dwNumberOfDigits++;
}
// 允许的最大数字数量:3
if (dwNumberOfDigits > 3){
return TRUE;
}
return FALSE;
}c5. 利用运行进程数量进行反虚拟化#
沙箱系统通常没有安装太多软件,因此运行的进程数量较少。真实的 Windows 物理机通常至少会有 60-70 个进程。
BOOL CheckMachineProcesses() {
DWORD adwProcesses [1024];
DWORD dwReturnLen = NULL,
dwNmbrOfPids = NULL;
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen)) {
return FALSE;
}
dwNmbrOfPids = dwReturnLen / sizeof(DWORD);
// 如果进程少于 50 个,很可能是沙箱
if (dwNmbrOfPids < 50)
return TRUE;
return FALSE;
}c6. 利用用户交互进行反虚拟化 (核心技术)#
沙箱通常运行在“无头”(Headless)环境中,意味着没有外设交互(键盘、鼠标)。这种缺乏交互的行为是关键特征。我们可以通过监听鼠标点击次数来验证环境真实性。
💡 初学者提示:鼠标钩子(Mouse Hook)是什么?
类比理解: 想象你在门口装了一个红外报警器。只要有人进门(只要鼠标点击了),报警器就会察觉到并告诉你。
- 在本例中,程序在运行真正的“恶意逻辑”前,先保持沉默,并在后台装一个“点击计数器”。
- 如果过了 20 秒,鼠标连一下都没点过,程序就会判定:“这地方没活人,我不能暴露!”
利用 SetWindowsHookExW 安装钩子记录 20 秒内的点击次数。
// 监听时间:20 秒
#define MONITOR_TIME 20000
HHOOK g_hMouseHook = NULL;
DWORD g_dwMouseClicks = NULL;
// 钩子回调
LRESULT CALLBACK HookEvent(int nCode, WPARAM wParam, LPARAM lParam){
// 监听左键、右键、中键点击
if (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN) {
printf("[+] 记录到一次鼠标点击 \n");
g_dwMouseClicks++;
}
return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam);
}
BOOL MouseClicksLogger(){
MSG Msg = { 0 };
// 安装 WH_MOUSE_LL 钩子
g_hMouseHook = SetWindowsHookExW(WH_MOUSE_LL, (HOOKPROC)HookEvent, NULL, NULL);
// 必须运行消息循环
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}
return TRUE;
}
// 在线程中运行监控任务
int main() {
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, NULL);
if (hThread) {
// 监控 MONITOR_TIME 时长
WaitForSingleObject(hThread, MONITOR_TIME);
}
// 卸载钩子并结束线程
UnhookWindowsHookEx(g_hMouseHook);
TerminateThread(hThread, 0);
// 测试结果:如果少于 5 次点击,判定为沙箱
if (g_dwMouseClicks < 5)
printf("[-] 环境异常:由于鼠标交互频率过低,疑似虚拟化环境。\n");
else
printf("[+] 环境正常:通过用户交互测试。\n");
return 0;
}c🎯 总结#
在本模块中,我们学习了:
- 资源基准测试: 利用物理限制(CPU/内存/USB 历史)区分真实物理机与受限 VM。
- 环境指纹: 通过屏幕分辨率、文件名数字分布、进程密集度构建环境画像。
- 行为取证: 利用 Windows 消息钩子异步监控用户行为,识破全自动沙箱的伪装。
📚 下一步学习
下一个模块将探讨 Module 74 - Anti-Virtual Environments - Multiple Delay Execution Techniques。学习如何通过时间战术耗尽分析耐心。