Хакер - Детект песочницы. Учимся определять, работает ли приложение в sandbox-изоляции
nopaywall

Содержание статьи

INFO
Sandbox (песочница) — изолированная программная среда, в которой задается и контролируется набор ресурсов для запущенной внутри нее программы. Как правило, ограничиваются и фильтруются вызовы WinAPI, которые отвечают за доступ к оборудованию, процессорным ядрам, определение размера памяти, а также доступ к сети и привилегированным средствам операционной системы. Сендбоксинг часто используется для запуска небезопасного кода и для анализа программ.
Есть ли надежные средства детекта песочниц и виртуалок?
- Однозначных нет, приходится использовать принцип экспертной системы
- Для некоторых есть, причем заложенные самим производителем
- Да, например, Virtual PC выполняет недействительные для реального процессора команды с опкодами 0x0F, 0x3F и т.д.
Проверяем запущенные процессы
Если виды песочниц нам приблизительно известны, то это облегчит детектирование. Можно просто поискать сендбоксы в списке выполняющихся процессов. Для этого используем функцию, которая перечислит все процессы и определит 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.
Что может дать ложноположительный детект песочницы при выполнении тайминг-атак?
- Высокая загрузка процессора в момент проверки, особенно если он с Hyper Threading
- Наличие антивируса, который фильтрует системные вызовы
- Сырой патч от Meltdown и Spectre
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; } };
Какие еще есть характерные признаки песочницы или виртуалки?
- Названия популярных песочниц и виртуальных машин в идентификаторах BIOS и оборудования
- Специфическое расположение таблиц GDT, IDT, LDT и т.д.
- Специфические расширения и окна (например, VMwareWindow)
Заключение
Вот мы и разобрали, как определять известные изолированные среды разными способами. Это не все из возможных методов, но от такого набора заготовок можно будет отталкиваться, конструируя свои методы определения (а возможно, и обхода) сендбоксинга.
Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall