Погружаемся в PEB. Подмена аргументов командной строки в запущенных и suspended процессах

—

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/808787/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *