Содержание статьи
INFO
Sandbox (песочница) — изолированная программная среда, в которой задается и контролируется набор ресурсов для запущенной внутри нее программы. Как правило, ограничиваются и фильтруются вызовы WinAPI, которые отвечают за доступ к оборудованию, процессорным ядрам, определение размера памяти, а также доступ к сети и привилегированным средствам операционной системы. Сендбоксинг часто используется для запуска небезопасного кода и для анализа программ.
Проверяем запущенные процессы
Если виды песочниц нам приблизительно известны, то это облегчит детектирование. Можно просто поискать сендбоксы в списке выполняющихся процессов. Для этого используем функцию, которая перечислит все процессы и определит PID нужного по названию.
DWORD getPIDproc(char * pProcName)
{
HANDLE pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(pHandle == NULL) return 0;
PROCESSENTRY32 ProcessEntry;
DWORD pid;
ProcessEntry.dwSize = sizeof(ProcessEntry);
bool Loop = Process32First(pHandle, &ProcessEntry);
if(Loop == NULL) return 0;
while (Loop)
{
if (strstr(ProcessEntry.szExeFile, pProcName))
{
pid = ProcessEntry.th32ProcessID;
CloseHandle(pHandle);
return pid;
}
Loop = Process32Next(pHandle, &ProcessEntry);
}
return 0;
}
Детект песочницы Comodo Internet Security:
if(getPIDproc("cmdvirth.exe")) std::cout << "Comodo sandbox detected!\n";
Процесс cmdvirth.exe
обеспечивает виртуализацию в Comodo Internet Security. По тому же принципу можно задетектить песочницу Sandboxie:
if(getPIDproc("SbieSvc.exe")) std::cout << "Sandboxie detected!\n";
Я думаю, что принцип понятен. 🙂 Если процесс не будет найден, функция вернет 0, и условие if
не будет выполнено. Также функция вернет 0, если возникнут проблемы с получением первого процесса в системном снапшоте функцией Process32First
либо с получением самого снапшота функцией CreateToolhelp32Snapshot
.
Проверяем подключенные модули в нашем адресном пространстве
Другая интересная идея — просмотреть подключенные модули в адресном пространстве нашей программы на предмет известных модулей песочниц. Сделаем это напрямую из нашего процесса при помощи WinAPI-функции GetModuleHandle
:
BOOL checkLoadedDll(LPCWSTR pDllName)
{
HMODULE hDll = GetModuleHandle(pDllName);
if(hDll) return TRUE;
}
Проверка на песочницу Comodo Internet Security:
if (checkLoadedDll(L"cmdvrt64.dll")) std::cout << "Comodo sandbox detected!\n";
Или проверим на Sandboxie:
if (checkLoadedDll(L"sbiedll.dll")) std::cout << "Sandboxie detected!\n";
Функция GetModuleHandle()
проверяет наличие DLL в адресном пространстве вызывающего процесса. Если функция не находит модуль, то возвращает ноль, условие не срабатывает.
Чтобы узнать список подключенных модулей в стороннем процессе, нужно получить его хендл, вызвав функцию WinAPI OpenProcess
, затем перечислить все подключенные к процессу модули с помощью функции EnumProcessModules
(в нее следует передать полученный хендл) и, наконец, получить название модуля с помощью функции WinAPI GetModuleFileNameEx
.
Человеческий фактор
Что делать, если мы не знаем даже примерно, какая именно программа-песочница будет использована? Надо сказать, что очень сложно на 100% задетектить грамотно написанную песочницу, ведь она будет работать на уровне ядра, может прятать все свои процессы, подключенные модули, может даже скрывать собственный драйвер. Техники, которые будут перечислены далее, я рекомендую использовать по принципу экспертной системы для анализа результатов: чем больше будет срабатываний, тем более вероятно, что мы находимся в изолированной среде.
Первый способ косвенного обнаружения sandbox-изоляции основан на человеческом поведении. Мы допускаем, что после запуска нашей программы указатель мыши перемещался, и если это происходит, то можно предположить, что песочница не используется. Сравним координаты указателя через некоторый промежуток времени: если координаты не меняются, можно ставить один балл в пользу того, что нас исследуют внутри песочницы.
BOOL mouse_motion()
{
int count = 0;
POINT mouse_coordinate1 = {};
POINT mouse_coordinate2 = {};
GetCursorPos(&mouse_coordinate1);
Sleep(1500);
GetCursorPos(&mouse_coordinate2);
if ((mouse_coordinate1.x == mouse_coordinate2.x) &&
(mouse_coordinate1.y == mouse_coordinate2.y))
++count;
GetCursorPos(&mouse_coordinate1);
Sleep(1500);
GetCursorPos(&mouse_coordinate2);
if ((mouse_coordinate1.x == mouse_coordinate2.x) &&
(mouse_coordinate1.y == mouse_coordinate2.y))
++count;
if(count > 0) return TRUE;
else return FALSE;
}
В этой функции мы берем два отрезка времени по полторы секунды каждый и, если в каком-то отрезке времени не было перемещения указателя мыши, делаем вывод, что, скорее всего, выполнение идет под бдительным взором изолированной среды.
PEB → NumberOfProcessors
Изолированные среды часто усекают число процессоров, чтобы не занять все ресурсы компьютера. Например, песочница может эмулировать одноядерный процессор. Но на дворе 2018 год, и даже в мобильных гаджетах зачастую камни с четырьмя ядрами, так что смело можем проверять, сколько ядер процессора видит наша программа. Если ядро всего одно, то это повод для подозрений.
Код для архитектуры x64:
PULONG procNum = (PULONG)(__readgsqword(0x60) + 0xB8); // DWORD NumberOfProcessors;
Для x86:
PULONG procNum = (PULONG)(__readfsdword(0x30) + 0x64); // DWORD NumberOfProcessors;
Этот код получает содержимое поля NumberOfProcessors
из PEB (Process Environment Block). Теперь легко проверить число процессоров:
if (*procNum < 2) std::cout << "NumberOfProcessors == 1, may be sandboxed!\n";
Выясняем размер оперативной памяти
Другой побочный признак сендбоксинга — это малое количество оперативной памяти. В наши дни редко можно увидеть ПК с одним-двумя гигабайтами оперативки, теперь это скорее нормальный объем для смартфонов. Вот как будет выглядеть реализация проверки.
BOOL check_memory()
{
MEMORYSTATUSEX mem_stat = { 0 };
statex.dwLength = sizeof(mem_stat);
GlobalMemoryStatusEx(&mem_stat);
if(mem_stat.ullTotalPhys < (1024LL * (1024LL * (1024LL * 1LL)))) return TRUE;
else return FALSE;
}
Проверяем свободное место
Мало места на жестком диске? Возможно, и правда все забито под завязку, но есть вероятность того, что это очередной признак песочницы. В этом примере мы предполагаем, что программа работает в изоляции, если доступно меньше 30 Гбайт.
BOOL check_freespace()
{
LPCWSTR lpDirectoryName = NULL;
ULARGE_INTEGER lpTotalNumberOfBytes;
BOOL bStat = GetDiskFreeSpaceEx(lpDirectoryName, NULL, &lpTotalNumberOfBytes, NULL);
if (bStat)
{
if (lpTotalNumberOfBytes.QuadPart < (30ULL * (1024ULL * (1024ULL * (1024ULL)))))
return TRUE;
else return FALSE;
}
}
Простые тайминг-атаки
Песочницы зачастую весьма требовательны к ресурсам и нередко сильно замедляют работу программы. Мы можем использовать тайминг-атаки для того, чтобы понять, используется эмуляция оборудования или нет. Один из вариантов — выполнять какой-нибудь системный вызов, который на чистой системе будет работать моментально, а в эмулируемой среде замедлен из-за драйвера песочницы. Если разница велика, то можно предположить, что используется песочница. На такой трюк ловится, например, песочница Sandboxie.
BOOL checkTiming1()
{
unsigned __int64 counter1, counter2, counter3;
int i = 0;
do
{
counter1 = __rdtsc();
GetProcessHeap();
counter2 = __rdtsc();
CloseHandle(0);
counter3 = __rdtsc();
// Замеряем отношение времени выполнения CloseHandle и GetProcessHeap()
if ( ( LODWORD(counter3) - LODWORD(counter2) ) /
( LODWORD(counter2) - LODWORD(counter1) ) >= 10)
return TRUE;
} while (i = 0; i < 10; i++);
return FALSE;
}
Отношение времени выполнения функции CloseHandle
и GetProcessHeap()
должно быть около 1 к 10. Если отношение меньше, делаем вывод о наличии фильтрации вызовов в драйвере песочницы.
Также песочницы могут оптимизировать течение времени — например, игнорировать вызовы функции Sleep()
. Если сделать замеры времени выполнения до вызова Sleep()
и после, то мы это увидим.
BOOL check_sleep()
{
// Инициализируем пустые метки
DWORD counterStart = 0;
DWORD counterEnd = 0;
DWORD difference = 0;
counterStart = GetTickCount(); // Засекаем время до вызова Sleep();
Sleep(100000); // Засыпаем на 100 секунд
counterEnd = GetTickCount(); // Проверяем время после вызова
difference = counterEnd - counterStart; // Сравниваем временные интервалы
if difference > 99000) // Корректируем на одну секунду, допуская погрешность
return FALSE;
else return TRUE;
}
Быстрый детект гипервизоров
Мы рассмотрели детектирование известных и неизвестных программ-песочниц, но нельзя ли определить гипервизор? Здесь нам поможет инструкция __cpuid
. Она запрашивает у процессора его тип и функции, которые он поддерживает. На выходе мы получаем заполненную структуру cpuInfo
, которая состоит из четырех чисел, передаваемых в регистрах процессора EAX, EBX, ECX и EDX. На вход нужно подать чистую структуру cpuInfo
и число function_id
, которое говорит команде, какая именно информация нас интересует. Функция поддерживает как x64-, так и x86-архитектуры. Ее прототип выглядит следующим образом:
void __cpuid(
int cpuInfo[4],
int function_id
);
Если передать число 1 в поле function_id
и посмотреть, выставлен ли 31-й бит в регистре ECX структуры cpuInfo
, то можно узнать о присутствии гипервизора. Итак, код:
BOOL check_cpuid()
{
INT cpuInfo[4] = { -1 }; // Объявляем структуру cpuInfo и инициализируем ее
__cpuid(cpuInfo, 1); // Запрашиваем данные
if ((cpuInfo[2] >> 31) & 1)
return TRUE; // Проверяем нужный бит в структуре cpuInfo
}
Помимо этого, известные гипервизоры детектируются так же, как и песочницы: можно просто посмотреть их процессы или характерные записи в реестре. Давай попробуем задетектить гипервизор Virtual PC при помощи функции, которую мы написали выше, — DWORD getPIDproc(char * pProcName)
. Просто передадим в нее имя нужных процессов, характерных для Virtual PC:
if(getPIDproc("VMSrvc.exe") || getPIDproc("VMUSrvc.exe")) std::cout << "Virtual PC detected!\n";
Так же легко найдем гипервизор Citrix Xen:
if(getPIDproc("xenservice.exe")) std::cout << "Citrix Xen detected!\n";
Теперь посмотрим, как задетектить гипервизоры по характерным записям в реестре. Для этого напишем небольшую функцию, которая поможет нам определить, что программа исполняется внутри Wine.
BOOL check_wine_registry_key()
{
HKEY phkResult = FALSE;
if (RegOpenKeyEx(HKEY_CURRENT_USER, _T("SOFTWARE\\Wine"), NULL, KEY_READ, &phkResult) == ERROR_SUCCESS)
{
RegCloseKey(phkResult);
return TRUE;
}
};
Заключение
Вот мы и разобрали, как определять известные изолированные среды разными способами. Это не все из возможных методов, но от такого набора заготовок можно будет отталкиваться, конструируя свои методы определения (а возможно, и обхода) сендбоксинга.