Каждый процесс в ОС 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.
DenisYahnovec
Не хило так раскатали), вообще конечно через командную строку секретную информацию не желательно передавать, тем более что ее видно через ProcessHacker и другие, но тем не менее попортить жизнь программе в принципе можно. Другое дело что такие функции как
ReadProcessMemory WriteProcessMemory
не так часто вызываются в обычных приложениях тем более что еще нужно и самим это подопытный процесс и стартанутьCreateProcessA,
ну то ладно в общем есть подозрения что антивирусы могут сильно ругаться на такую новую чистую программу с таким функционалом, вот что будет если ее прогнать через ВирусТотал пару раз с интервалом в неделю....