Каждый процесс в ОС Windows имеет описательный блок PEB (Process Environment Block) размером в 2000 байт, содержащий информацию об этом процессе.
Начало этого блока выглядит так:
BYTE InheritedAddressSpace; BYTE ReadImageFileExecOptions; BYTE BeingDebugged; BYTE BitField; union { BYTE BitField; struct { BYTE ImageUsesLargePages:1; BYTE IsProtectedProcess:1; BYTE IsImageDynamicallyRelocated:1; BYTE SkipPatchingUser32Forwarders:1; BYTE IsPackagedProcess:1; BYTE IsAppContainer:1; BYTE IsProtectedProcessLight:1; BYTE IsLongPathAwareProcess:1; }; }; UCHAR Padding0[4]; VOID* Mutant; VOID* ImageBaseAddress; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
Полную структуру PEB можно посмотреть здесь
При создании процесса, его CommandLine записывается в PEB структуры EPROCESS в ядре операционной системы.
В свою очередь, процессы пользовательского режима имеют доступ к PEB, например, чтобы получать адреса dll, загруженных в процесс, проверять, подключен ли отладчик и так далее.
Благодаря этому, существует возможность перезаписи PEB прямо из пользовательского режима.
Это может использоваться для подмены аргументов командной строки.
Зачем это может понадобиться злоумышленникам?
В случае, когда на скомпрометированном узле фиксируются события системами SIEM, EDR и т.д., создание процесса с подозрительными аргументами будет подсвечено, что в итоге приведет к потере контроля над скомпрометированным узлом.
Помимо этого, подмена аргументов может быть использована для изменения логики выполнения процесса, так как многие системные функции обращаются к PEB за аргументами командной строки.
Для начала давайте напишем простое приложение и посмотрим на него в IDA Pro:

Добавим в настройках отладчика аргументы:

Поставим точку останова на функции main и запустим приложение:

Во вкладке Modules найдем kernelbase.dll:

В этом модуле найдем функцию kernelbase_IsDebuggerPresent:

Здесь видно, что в регистр rax помещается адрес, находящийся в сегменте gs по смещению 0x60(это и есть адрес PEB в 64-разрядных приложениях).
Затем берется смещение в 2 байта от этого адреса - флаг, отлаживается ли приложение (смотри структуру PEB выше).
Если перейти на этот адрес и посмотреть смещение, то будет видно, что флаг выставлен в 1, потому что сейчас мы подключены отладчиком

Перейдем на смещение 0x20 - тут находится адрес структуры RTL_USER_PROCESS_PARAMETERS

RTL_USER_PROCESS_PARAMETERS имеет следующую структуру:
struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; //0x0 ULONG Length; //0x4 ULONG Flags; //0x8 ULONG DebugFlags; //0xc VOID* ConsoleHandle; //0x10 ULONG ConsoleFlags; //0x18 VOID* StandardInput; //0x20 VOID* StandardOutput; //0x28 VOID* StandardError; //0x30 struct _CURDIR CurrentDirectory; //0x38 struct _UNICODE_STRING DllPath; //0x50 struct _UNICODE_STRING ImagePathName; //0x60 struct _UNICODE_STRING CommandLine; //0x70 VOID* Environment; //0x80 ULONG StartingX; //0x88 ULONG StartingY; //0x8c ULONG CountX; //0x90 ULONG CountY; //0x94 ULONG CountCharsX; //0x98 ULONG CountCharsY; //0x9c ULONG FillAttribute; //0xa0 ULONG WindowFlags; //0xa4 ULONG ShowWindowFlags; //0xa8 struct _UNICODE_STRING WindowTitle; //0xb0 struct _UNICODE_STRING DesktopInfo; //0xc0 struct _UNICODE_STRING ShellInfo; //0xd0 struct _UNICODE_STRING RuntimeData; //0xe0 struct _RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32]; //0xf0 ULONGLONG EnvironmentSize; //0x3f0 ULONGLONG EnvironmentVersion; //0x3f8 VOID* PackageDependencyData; //0x400 ULONG ProcessGroupId; //0x408 ULONG LoaderThreads; //0x40c struct _UNICODE_STRING RedirectionDllName; //0x410 struct _UNICODE_STRING HeapPartitionName; //0x420 ULONGLONG* DefaultThreadpoolCpuSetMasks; //0x430 ULONG DefaultThreadpoolCpuSetMaskCount; //0x438 ULONG DefaultThreadpoolThreadMaximum; //0x43c ULONG HeapMemoryTypeMask; //0x440 };
Далее перейдем по смещению 0x70 от указанного адреса. Здесь находится CommandLine нашего процесса в виде структуры UNICODE_STRING:

Структура UNICODE_STRING имеет следующий вид:
struct _UNICODE_STRING { USHORT Length; //0x0 USHORT MaximumLength; //0x2 WCHAR* Buffer; //0x8 };
По смещению 0x8 от CommandLine находится указатель на ту самую строку с аргументами:

Переходим по адресу и видим командную строку, переданную ранее:

Отлично. Разобрались, где хранится командная строка в PEB, нашли все смещения и раскопали заветную строку руками. Так будет чуть больше понимания :)
С IDA все понятно, а что с кодингом?
Давайте напишем приложение для доступа к CommandLine в PEB!
Определим несколько структур:
_PEB_LDR_DATA
typedef struct _PEB_LDR_DATA { ULONG Length; //0x0 UCHAR Initialized; //0x4 VOID* SsHandle; //0x8 struct _LIST_ENTRY InLoadOrderModuleList; //0x10 struct _LIST_ENTRY InMemoryOrderModuleList; //0x20 struct _LIST_ENTRY InInitializationOrderModuleList; //0x30 VOID* EntryInProgress; //0x40 UCHAR ShutdownInProgress; //0x48 VOID* ShutdownThreadId; //0x50 };
_UNICODE_STRING
typedef struct _UNICODE_STRING { USHORT Length; //0x0 USHORT MaximumLength; //0x2 WCHAR* Buffer; //0x8 };
_CURDIR
typedef struct _CURDIR { struct _UNICODE_STRING DosPath; //0x0 VOID* Handle; //0x10 };
_STRING
typedef struct _STRING { USHORT Length; //0x0 USHORT MaximumLength; //0x2 CHAR* Buffer; //0x8 };
_RTL_DRIVE_LETTER_CURDIR
typedef struct _RTL_DRIVE_LETTER_CURDIR { USHORT Flags; //0x0 USHORT Length; //0x2 ULONG TimeStamp; //0x4 struct _STRING DosPath; //0x8 };
_RTL_USER_PROCESS_PARAMETERS
typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; //0x0 ULONG Length; //0x4 ULONG Flags; //0x8 ULONG DebugFlags; //0xc VOID* ConsoleHandle; //0x10 ULONG ConsoleFlags; //0x18 VOID* StandardInput; //0x20 VOID* StandardOutput; //0x28 VOID* StandardError; //0x30 struct _CURDIR CurrentDirectory; //0x38 struct _UNICODE_STRING DllPath; //0x50 struct _UNICODE_STRING ImagePathName; //0x60 struct _UNICODE_STRING CommandLine; //0x70 VOID* Environment; //0x80 ULONG StartingX; //0x88 ULONG StartingY; //0x8c ULONG CountX; //0x90 ULONG CountY; //0x94 ULONG CountCharsX; //0x98 ULONG CountCharsY; //0x9c ULONG FillAttribute; //0xa0 ULONG WindowFlags; //0xa4 ULONG ShowWindowFlags; //0xa8 struct _UNICODE_STRING WindowTitle; //0xb0 struct _UNICODE_STRING DesktopInfo; //0xc0 struct _UNICODE_STRING ShellInfo; //0xd0 struct _UNICODE_STRING RuntimeData; //0xe0 struct _RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32]; //0xf0 ULONGLONG EnvironmentSize; //0x3f0 ULONGLONG EnvironmentVersion; //0x3f8 VOID* PackageDependencyData; //0x400 ULONG ProcessGroupId; //0x408 ULONG LoaderThreads; //0x40c struct _UNICODE_STRING RedirectionDllName; //0x410 struct _UNICODE_STRING HeapPartitionName; //0x420 ULONGLONG* DefaultThreadpoolCpuSetMasks; //0x430 ULONG DefaultThreadpoolCpuSetMaskCount; //0x438 ULONG DefaultThreadpoolThreadMaximum; //0x43c ULONG HeapMemoryTypeMask; //0x440 };
_PEB
typedef struct _PEB { UCHAR InheritedAddressSpace; //0x0 UCHAR ReadImageFileExecOptions; //0x1 UCHAR BeingDebugged; //0x2 union { UCHAR BitField; //0x3 struct { UCHAR ImageUsesLargePages : 1; //0x3 UCHAR IsProtectedProcess : 1; //0x3 UCHAR IsImageDynamicallyRelocated : 1; //0x3 UCHAR SkipPatchingUser32Forwarders : 1; //0x3 UCHAR IsPackagedProcess : 1; //0x3 UCHAR IsAppContainer : 1; //0x3 UCHAR IsProtectedProcessLight : 1; //0x3 UCHAR IsLongPathAwareProcess : 1; //0x3 }; }; UCHAR Padding0[4]; //0x4 VOID* Mutant; //0x8 VOID* ImageBaseAddress; //0x10 struct _PEB_LDR_DATA* Ldr; //0x18 struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; //0x20 } PEB, * PPEB;
Получим доступ к сегменту gs с помощью функции __readgsqword() и PEB по смещению 0x60:
PPEB peb = (PPEB)__readgsqword(0x60);
Далее получаем значение RTL_USER_PROCESS_PARAMETERS:
_RTL_USER_PROCESS_PARAMETERS* ProcParameters = peb->ProcessParameters;
Получаем CommandLine:
_UNICODE_STRING cmdLine = ProcParameters->CommandLine;
Выводим значение различными способами:
int main() { PPEB peb = (PPEB)__readgsqword(0x60); _RTL_USER_PROCESS_PARAMETERS* ProcParameters = peb->ProcessParameters; _UNICODE_STRING cmdLine = ProcParameters->CommandLine; std::wcout << cmdLine.Buffer << std::endl; std::wcout.write(cmdLine.Buffer,cmdLine.Length/sizeof(WCHAR)); std::wcout << std::endl; for(int i = 0; i<__argc;i++ ) { std::wcout << __argv[i]<<L" "; } std::wcout << std::endl; std::wcout << GetCommandLineW() << std::endl; std::cout << GetCommandLineA() << std::endl; }
В результате видим совпадение нашего "ручного" получения командной строки с системными функциями GetCommandLine и argv:

Теперь давайте попробуем перезаписать наши аргументы чем-нибудь другим и выведем результат.
WCHAR newCmdLine[] = L"AUTHORITY.EXE NEWARG1"; peb->ProcessParameters->CommandLine.Buffer = newCmdLine; peb->ProcessParameters->CommandLine.Length = sizeof(newCmdLine); std::wcout << peb->ProcessParameters->CommandLine.Buffer << std::endl; std::wcout.write(peb->ProcessParameters->CommandLine.Buffer, peb->ProcessParameters->CommandLine.Length / sizeof(WCHAR)); std::wcout << std::endl; for (int i = 0; i < __argc; i++) { std::wcout << __argv[i] << L" "; } std::wcout << std::endl; std::wcout << GetCommandLineW() << std::endl; std::cout << GetCommandLineA() << std::endl;
Результат:

Результат в ProcessHacker:

Как же так получается, что в PEB мы поменяли commandLine, а все стандартные способы получения аргументов дают тот же результат, что и раньше?
Дело в том, что приложения при старте, до выполнения функции main, копируют значения commandLine из PEB "внутрь" себя.
Поэтому, для изменения аргументов в райнтайме, необходимо их менять также по указателям, которые возвращают GetCommandLine и argv.
Давайте попробуем!
LPWSTR GCLW = GetCommandLineW(); memcpy(GCLW, newCmdLine, sizeof(newCmdLine) * sizeof(WCHAR)); LPSTR GCLA = GetCommandLineA(); memcpy(GCLA, "AUTHORITY.EXE NEWARG1", sizeof("AUTHORITY.EXE NEWARG1")); char newArg0[] = "AUTHORITY.EXE"; char newArg1[] = "NEWARG1"; __argv[0] = newArg0; __argv[1] = newArg1; __argc = 2; for (int i = 0; i < __argc; i++) { std::wcout << __argv[i] << L" "; } std::wcout << std::endl; std::wcout << GetCommandLineW() << std::endl; std::cout << GetCommandLineA() << std::endl;
Смотрим на результат.... БИНГО!

Теперь давайте попробуем изменить аргументы в PEB так, как это обычно делают злоумышленники, чтобы противодействовать своему обнаружению.
Создадим приложение, которое просто будет выводить аргументы командной строки:
int main() { for (int i = 0; i < __argc; i++) { std::wcout << __argv[i] << L" "; } std::wcout << std::endl; std::wcout << GetCommandLineW() << std::endl; std::cout << GetCommandLineA() << std::endl; }
Научимся создавать процесс в suspended режиме:
CreateProcessA(NULL, (LPSTR)"ReadCommandLine.exe arg1 arg2", NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
Получим информацию о PEB созданного процесса и перезапишем аргументы.
Прототип функции:
typedef NTSTATUS(WINAPI* NtQueryInformationProcess_t)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
Получении информации о процессе:
NtQueryInformationProcess_t NtQueryInformationProcess_p = (NtQueryInformationProcess_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess"); NtQueryInformationProcess_p(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
Чтение памяти процесса:
PEB peb; SIZE_T bytesRead; ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &peb, sizeof(PEB), &bytesRead);
Перезапись аргументов:
SIZE_T bytesWritten; WCHAR newArgs[] = L"AUTHORITY.exe NEWARG1\0"; WriteProcessMemory(pi.hProcess, parameters.CommandLine.Buffer, (void*)newArgs, sizeof(newArgs), &bytesWritten);
Перезапись длины командной строки
DWORD newUnicodeLen = sizeof(newArgs); WriteProcessMemory(pi.hProcess, (char*)pebLocal.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length), (void*)&newUnicodeLen, 4, &bytesWritten);
Выводим процесс из приостановленного состояния:
ResumeThread(pi.hThread);
Проверяем результат.... БИНГО:

В итоге мы получили возможность манипуляции аргументами командной строки как в рантайме, так и при старте приложения.
Такая технология позволяет злоумышленникам избегать обнаружения при логировании событий создания процесса и менять поведение легитимных программ, зависящих от переданных им аргументов.
Существует еще много способов манипуляций PEB, о которых мы когда-нибудь обязательно расскажем!
Напоминаем, что хакинг является незаконной деятельностью и допустим только в случае тестирования на проникновение в согласовании с заказчиком.
Подписывайтесь на наш telegram-канал AUTHORITY.
