Detecting Sandboxes Without Syscalls
19.04.2024 ยท dadevel
The PEB, TEB and KUSER_SHARED_DATA structs are mapped into the memory space of every process. They provide a wealth of information to the process and can be accessed without having to perform syscalls. Using them for anti-debugging is widely known and documented for example by CheckPoint. But they can also be used for stealthy anti-sandbox and anti-vm checks.
The following sandbox indicators might be interesting for both malware developers and sandbox vendors. The code assumes the struct definitions from VX-API.
constexpr uint32_t TICKS_PER_SECOND = 10'000'000;
const PEB* peb = GetPeb();
const KUSER_SHARED_DATA* ksd = GetKUserSharedData();
const uint32_t boot_count = ksd->BootId;
const uint32_t cpu_core_count1 = peb->NumberOfProcessors;
const uint32_t cpu_core_count2 = ksd->ActiveProcessorCount;
const double ram_size = static_cast<double>(ksd->NumberOfPhysicalPages) * 4096 / 1024 / 1024 / 1024; // in gigabyte
const LARGE_INTEGER time1 = { .LowPart = ksd->InterruptTime.LowPart, .HighPart = ksd->InterruptTime.High2Time };
const uint32_t uptime = time1.QuadPart / TICKS_PER_SECOND / 60 / 60; // in hours
const uint32_t os_major_version1 = ksd->NtMajorVersion;
const uint32_t os_major_version2 = peb->OSMajorVersion;
const bool license_valid = ksd->SystemExpirationDate.QuadPart == 0;
const bool secure_boot_enabled = ksd->DbgSecureBootEnabled;
const wchar_t* filepath = peb->ProcessParameters->ImagePathName.Buffer;
const LARGE_INTEGER time2 = { .LowPart = ksd->TimeZoneBias.LowPart, .HighPart = ksd->TimeZoneBias.High2Time };
const double timezone_offset = -1 * static_cast<double>(time2.QuadPart) / TICKS_PER_SECOND / 60 / 60; // in hours from UTC
const wchar_t* env = peb->ProcessParameters->Environment;
const wchar_t* computername = GetEnvVar(env, L"COMPUTERNAME");
const wchar_t* userdomain = GetEnvVar(env, L"USERDOMAIN");
const wchar_t* username = GetEnvVar(env, L"USERNAME");
const wchar_t* workdir = peb->ProcessParameters->CurrentDirectory.DosPath.Buffer;
I implemented a small proof of concept that collects these indicators and performs a DNS query for each, so that they show up in the sandbox network log. The results from VirusTotal are shown below.
Sandbox | Boot Count | CPU Cores | RAM Size | OS Version | Licensed | Secure Boot | File Renamed | Uptime | Timezone | COMPUTERNAME | USERDOMAIN | USERNAME | Workdir |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
My Gaming PC | 523 | 16 | 32GB | 10 | yes | yes | no | 74h | GMT+2 | gamingstation | gamingstation | nobody | downloads |
VirusTotal CAPE | 64 | 4 | 4GB | 10 | yes | no | no | 0h | GMT-7 | desktop-RANDOM | desktop-RANDOM | bruno | temp |
VirusTotal Zenbox | 62-76 | 4 | 8GB | 10 | yes | no | no | 0h | GMT-7 | desktop-RANDOM | desktop-RANDOM | george | desktop |
Unknown Sandbox 1 | 24 | 2 | 2GB | 10 | yes | no | no | 0h | GMT-7 | laptop-RANDOM | laptop-RANDOM | jen | downloads |
Unknown Sandbox 2 | 5 | 1 | 4GB | 10 | yes | no | no | 0h | GMT+2 | horst-pc | horst-pc | horst | downloads |
Note: The OS version can not be used to differentiate between Windows 10 and 11. It is set to 10 in both cases.
After testing various sandboxes it seems that boot count, Secure Boot status and uptime are strong generic sandbox indicators. Besides that there is still T1480/001 aka Environmental Keying to hide the payload itself from analysis.
The overall bottom line: Don't trust the analysis results of sandboxes too much.