Каждый процесс в ОС 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.

Комментарии (3)


  1. DenisYahnovec
    19.04.2024 11:43

    Не хило так раскатали), вообще конечно через командную строку секретную информацию не желательно передавать, тем более что ее видно через ProcessHacker и другие, но тем не менее попортить жизнь программе в принципе можно. Другое дело что такие функции как ReadProcessMemory WriteProcessMemory не так часто вызываются в обычных приложениях тем более что еще нужно и самим это подопытный процесс и стартануть CreateProcessA, ну то ладно в общем есть подозрения что антивирусы могут сильно ругаться на такую новую чистую программу с таким функционалом, вот что будет если ее прогнать через ВирусТотал пару раз с интервалом в неделю....


  1. LoadRunner
    19.04.2024 11:43

    А будет рассказ, как менять адреса DLL в PEB? Интересно было бы почитать.


  1. rivitna
    19.04.2024 11:43
    +2

    Тема совершенно не новая, статей по данной технике достаточно. Реализация и на C, и на C# с исходниками. Но за русский спасибо, конечно, только за это и плюсану :-)