Сегодня вечером впервые решил зарегестрироваться на Хабре, дабы отписать вот в этом топике о недочётах ilya314, но был предельно удивлён тем, что я оказывается не могу ничего откомментировать, поскольку не являюсь почётным хабропользователем. Какой ужас.
Поэтому я решил набросать код, дабы высказать свои соображения о проблеме дублирования данных в Сишный рантайм из PEB-a процесса.
Собственно, для решения проблемы, возникшей у автора, есть несколько путей,- самый простой из них заключается в отказе от библиотечных функций рантайма getenv и использование интерфейсов kernel32.GetEnvironmentVariableW или kernel32.GetEnvironmentStringsW. Но развивая тему дальше, мне захотелось найти переменные окружения и попробовать просто подменить их для конкретного процесса.
Глянем на недокументированное объявление PEB одним глазком (M$, понятное дело, предоставляет нам обрезок в пяток свойств, но при должном использовании гугла, либо отладчика — всё становится на свои места):
Нас интересует свойство ProcessParameters, которое и содержит в себе CommandLine; Environment; и прочие вкусняшки, которые дублируются в ring3 из ring0 и кэшируются Сишным рантаймом на лету прямо отсюда. Вероятно, рантайм іспользует стандартные интерфейсы kernel32->ntdll и их можно было просто хукнуть, но я решил вытянуть PEB через сегментный регистр и заменить данные в наглую в памяти. Просто подменить и посмотреть как на это будет реагировать Винда. В последнее время я собираюсь под AMD64, поэтому компилировать будем именно под эту платформу.
Благодаря замечательному решению M$, которое выпилило возможность inline asm вставок для 64-х битных платформ — нас ждёт занятный квест поприсиранию ассемблерных функций в проект и линковки с отдельным объектником (об этом можно написать полноценную статью, поэтому останавливаться на этом не буду, скажу лишь что я порядком запарился, пока всё заработало как надо).
Кстати, стоит отметить, что в отличии от 32-х битной версий семейства ОС Windows, в 64-х битной PEB мапится по смещению относительно gs регистра, в 32-битной версии получить его можно так:
Глядя на PEB, легко высчитать смещение ProcessParameters, для 32-х бит. С выравниванием в 4 байта, оно укладывается в 16 байт. Для 64-х бит- с учетом 8-байтовых указателей и выравненных на 4 байта первых BOOLEAN-ов — выйдет 28 + 4 байт. Убедиться в этом можно посмотрев на память процесса с помощью отладчика.


Теперь собёремся с силами и подменим ProcessParameters в PEB нашего процесса, а конкретнее — его переменные окружения. Но прежде, давайте посмотрим на формат хранения строк в Environment блоке. M$ настойчиво утверждает, что строки имеют вид VAR=VALUE и разделены нулевым байтом, признаком конца блока является два нулевых байта подряд. Убедимся в этом своими глазами и выделим подводные камни:
После чего следует вывод:

Возврат к «родной» таблице кэш не перестраивает. Таким образом, проблема автора с dll решается с помощью использования соответствующих интерфейсов kernel32 для работы с блоком переменных окружения.
Самое интересное, что данный метод будет работать и при подмене данных в стороннем процессе, что можно показать как-нибудь потом.
UPDATE: Как правильно заметил CleverMouse, работа с переменными окружения через библиотеку Си не несёт смысловой нагрузки в конкретном примере, тк в случае с функцией main, заполняется рантайм кэш _environ, а не _wenviron, поэтому предлагаю рассматривать это как отладочные печати.
Вечно ваш,
rwx64
Поэтому я решил набросать код, дабы высказать свои соображения о проблеме дублирования данных в Сишный рантайм из PEB-a процесса.
Собственно, для решения проблемы, возникшей у автора, есть несколько путей,- самый простой из них заключается в отказе от библиотечных функций рантайма getenv и использование интерфейсов kernel32.GetEnvironmentVariableW или kernel32.GetEnvironmentStringsW. Но развивая тему дальше, мне захотелось найти переменные окружения и попробовать просто подменить их для конкретного процесса.
Глянем на недокументированное объявление PEB одним глазком (M$, понятное дело, предоставляет нам обрезок в пяток свойств, но при должном использовании гугла, либо отладчика — всё становится на свои места):
typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; BOOLEAN Spare; HANDLE Mutant; PVOID ImageBaseAddress; PPEB_LDR_DATA LoaderData; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PPEBLOCKROUTINE FastPebLockRoutine; PPEBLOCKROUTINE FastPebUnlockRoutine; ULONG EnvironmentUpdateCount; PPVOID KernelCallbackTable; PVOID EventLogSection; PVOID EventLog; PPEB_FREE_BLOCK FreeList; ULONG TlsExpansionCounter; PVOID TlsBitmap; ULONG TlsBitmapBits[0x2]; PVOID ReadOnlySharedMemoryBase; PVOID ReadOnlySharedMemoryHeap; PPVOID ReadOnlyStaticServerData; PVOID AnsiCodePageData; PVOID OemCodePageData; PVOID UnicodeCaseTableData; ULONG NumberOfProcessors; ULONG NtGlobalFlag; BYTE Spare2[0x4]; LARGE_INTEGER CriticalSectionTimeout; ULONG HeapSegmentReserve; ULONG HeapSegmentCommit; ULONG HeapDeCommitTotalFreeThreshold; ULONG HeapDeCommitFreeBlockThreshold; ULONG NumberOfHeaps; ULONG MaximumNumberOfHeaps; PPVOID *ProcessHeaps; PVOID GdiSharedHandleTable; PVOID ProcessStarterHelper; PVOID GdiDCAttributeList; PVOID LoaderLock; ULONG OSMajorVersion; ULONG OSMinorVersion; ULONG OSBuildNumber; ULONG OSPlatformId; ULONG ImageSubSystem; ULONG ImageSubSystemMajorVersion; ULONG ImageSubSystemMinorVersion; ULONG GdiHandleBuffer[0x22]; ULONG PostProcessInitRoutine; ULONG TlsExpansionBitmap; BYTE TlsExpansionBitmapBits[0x80]; ULONG SessionId; } PEB, *PPEB;
Нас интересует свойство ProcessParameters, которое и содержит в себе CommandLine; Environment; и прочие вкусняшки, которые дублируются в ring3 из ring0 и кэшируются Сишным рантаймом на лету прямо отсюда. Вероятно, рантайм іспользует стандартные интерфейсы kernel32->ntdll и их можно было просто хукнуть, но я решил вытянуть PEB через сегментный регистр и заменить данные в наглую в памяти. Просто подменить и посмотреть как на это будет реагировать Винда. В последнее время я собираюсь под AMD64, поэтому компилировать будем именно под эту платформу.
Благодаря замечательному решению M$, которое выпилило возможность inline asm вставок для 64-х битных платформ — нас ждёт занятный квест по
; ; Utils.asm ; INCLUDE Utils.inc .code GetCurrentUserProcessParameters PROC mov rax, gs:[60h] mov rax, [rax + 20h]; ret; GetCurrentUserProcessParameters ENDP END
Кстати, стоит отметить, что в отличии от 32-х битной версий семейства ОС Windows, в 64-х битной PEB мапится по смещению относительно gs регистра, в 32-битной версии получить его можно так:
__declspec(naked) PVOID GetCurrentUserProcessParameters() { __asm { mov eax, fs:[30h]; mov eax, [eax + 10h]; ret; } }
Глядя на PEB, легко высчитать смещение ProcessParameters, для 32-х бит. С выравниванием в 4 байта, оно укладывается в 16 байт. Для 64-х бит- с учетом 8-байтовых указателей и выравненных на 4 байта первых BOOLEAN-ов — выйдет 28 + 4 байт. Убедиться в этом можно посмотрев на память процесса с помощью отладчика.


Теперь собёремся с силами и подменим ProcessParameters в PEB нашего процесса, а конкретнее — его переменные окружения. Но прежде, давайте посмотрим на формат хранения строк в Environment блоке. M$ настойчиво утверждает, что строки имеют вид VAR=VALUE и разделены нулевым байтом, признаком конца блока является два нулевых байта подряд. Убедимся в этом своими глазами и выделим подводные камни:
- Надо следить за memory protection для текущей страницы и восстанавливать их после подмены и записи в служебные структуры
- Стоит всегда предполагать, что на этой же странице может размещаться исполняемый код образа, либо инжектнутый код, поэтому надо выделять права на исполнение. Конечно, это маловероятно, с учётом выравнивания образов и гранулярности выделения памяти, но перестраховаться не будет лишним, особенно, если хучим/подменяем в многопоточной среде
- После окончания работы не забываем восстанавливать, заблаговременно сохранённую оригинальную таблицу (указатель на таблицу) и освобождать память фэйковой таблицы.
- Данный пример носит чисто академический характер, поскольку должен выполняться одним из первых в точке входа, чтобы избежать обращения системы к PEB-у (из другого рабочего потока) в момент подмены. По хорошему, стоит «заэнамить» все потоки в процессе и приостановить их на момент подмены.
#include <windows.h> #include <stdio.h> #include <conio.h> #include "Utils.h" typedef struct _LSA_UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING; typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; PVOID ConsoleHandle; ULONG ConsoleFlags; HANDLE StdInputHandle; HANDLE StdOutputHandle; HANDLE StdErrorHandle; UNICODE_STRING CurrentDirectoryPath; HANDLE CurrentDirectoryHandle; UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; PVOID Environment; ULONG StartingPositionLeft; ULONG StartingPositionTop; ULONG Width; ULONG Height; ULONG CharWidth; ULONG CharHeight; ULONG ConsoleTextAttributes; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; UNICODE_STRING DesktopName; UNICODE_STRING ShellInfo; UNICODE_STRING RuntimeData; PVOID DLCurrentDirectory; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; PVOID ReplacePEBEnvironmentTableAndAddValue(LPCWSTR Variable, LPCWSTR Value) { PRTL_USER_PROCESS_PARAMETERS ProcessParams; MEMORY_BASIC_INFORMATION MemoryInformation; PBYTE NewEnvironment; PWCHAR Token; size_t EnvironmentSize; if (!Variable || !Value || !*Variable || !*Value) return NULL; /* получаем указатель на RTL_USER_PROCESS_PARAMETERS в PEB и считаем */ /* размер блока Environment в байтах */ /* для этого проходимся по блоку, до тех пор, пока не встретим */ /* последовательность L'\0' L'\0', как документирует MSDN */ /* те 4 байта нулей */ ProcessParams = GetCurrentUserProcessParameters(); Token = (PWCHAR)ProcessParams->Environment; while (!(!*Token && !*(Token + 1))) ++Token; EnvironmentSize = (ULONG_PTR)Token - (ULONG_PTR)ProcessParams->Environment; /* выясняем атрибуты защиты для блока Environment и сохраняем их, чтобы вернуть к */ /* первоначальному состоянию, после подмены */ MemoryInformation.AllocationProtect = PAGE_EXECUTE_READWRITE; VirtualQuery(ProcessParams->Environment, &MemoryInformation, sizeof(MEMORY_BASIC_INFORMATION)); /* выделяем новую память размером с оригинальный Environment + страница */ /* с атрибутами для записи чтения и выполнения в виртуальном */ /* адресном пространстве процесса */ /* для того, чтобы скопировать туда обновлённый блок Environment, */ /* куда мы добавим новые переменные окружения */ NewEnvironment = (PBYTE)VirtualAlloc(0, EnvironmentSize + 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (NewEnvironment) { /* высчитываем сколько нам надо места под строку с новой переменной */ /* вида Var=Value (+ 2 widechar для L'=' и нулевого байта) */ DWORD OldProtect = PAGE_EXECUTE_READWRITE, OldProtect2 = PAGE_EXECUTE_READWRITE; size_t Size = (wcslen(Variable) + wcslen(Value)) * sizeof(WCHAR) + 2 * sizeof(WCHAR); PWCHAR EnvironmentString = malloc(Size); if (EnvironmentString) { /* формируем строку с новой переменной и копируем оригинальное */ /* окружение в начало выделенного буфера */ PVOID OldEnvironment = ProcessParams->Environment; UINT EndOfEnvironment = 0; _snwprintf_s(EnvironmentString, Size / sizeof(WCHAR), _TRUNCATE, L"%ws=%ws", Variable, Value); memcpy(NewEnvironment, ProcessParams->Environment, EnvironmentSize); /* добавляем разделительный нулевой байт - см описание формата окружения в MSDN */ *((PWCHAR)(NewEnvironment + EnvironmentSize)) = L'\0'; /* копируем строку с новой переменной после разделителя */ /* и добавляем завершающие 4 байта нулей */ memcpy(NewEnvironment + EnvironmentSize + 2, EnvironmentString, Size - sizeof(WCHAR)); memcpy(NewEnvironment + EnvironmentSize + 2 + Size - sizeof(WCHAR), &EndOfEnvironment, 4); /* выставляем атрибуты защиты новому буферу идентичные оригинальной странице */ VirtualProtect(NewEnvironment, EnvironmentSize + 0x1000, MemoryInformation.AllocationProtect, &OldProtect); /* выставляем странице со свойством RTL_USER_PROCESS_PARAMETERS в PEB */ /* права на запись чтение и исполнение */ /* подменяем Environment block и возвращаем все права на Родину */ VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), PAGE_EXECUTE_READWRITE, &OldProtect); ProcessParams->Environment = NewEnvironment; VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), OldProtect, &OldProtect2); /* освобождаем память под буфер для формирования строки с новой переменной */ /* и возвращаем старый адрес Environment block */ free(EnvironmentString); return OldEnvironment; } VirtualFree(NewEnvironment, 0, MEM_RELEASE); } return NULL; } void RestorePEBEnvironmentTable(PVOID OriginalEnvironment) { PRTL_USER_PROCESS_PARAMETERS ProcessParams; DWORD OldProtect = PAGE_EXECUTE_READWRITE, OldProtect2; PVOID OldEnvironment; if (!OriginalEnvironment) return; /* Получаем PEB процесса и восстанавливаем таблицу с переменными окружения на оригинальную */ ProcessParams = GetCurrentUserProcessParameters(); VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), PAGE_EXECUTE_READWRITE, &OldProtect); OldEnvironment = ProcessParams->Environment; ProcessParams->Environment = OriginalEnvironment; VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), OldProtect, &OldProtect2); /* Фэйковый блок переменных окружения больше не нужен, освобождаем память */ VirtualFree(OldEnvironment, 0, MEM_RELEASE); } void main(int argc, char *argv[]) { PVOID OriginalEnvironment; UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); OriginalEnvironment = ReplacePEBEnvironmentTableAndAddValue(L"NewVar", L"NewValue"); if (OriginalEnvironment) { WCHAR Buff[1024] = {0}; if (GetEnvironmentVariableW(L"NewVar", Buff, sizeof(Buff))) wprintf_s(L"GetEnvironmentVariableW(): NewVar == %ws\n", Buff); printf_s("Restoring PEB Environment Table...\n"); RestorePEBEnvironmentTable(OriginalEnvironment); if (!GetEnvironmentVariableW(L"NewVar", Buff, sizeof(Buff))) printf_s("GetEnvironmentVariableW(): NewVar not found\n"); } _getch(); }
После чего следует вывод:

Возврат к «родной» таблице кэш не перестраивает. Таким образом, проблема автора с dll решается с помощью использования соответствующих интерфейсов kernel32 для работы с блоком переменных окружения.
Самое интересное, что данный метод будет работать и при подмене данных в стороннем процессе, что можно показать как-нибудь потом.
UPDATE: Как правильно заметил CleverMouse, работа с переменными окружения через библиотеку Си не несёт смысловой нагрузки в конкретном примере, тк в случае с функцией main, заполняется рантайм кэш _environ, а не _wenviron, поэтому предлагаю рассматривать это как отладочные печати.
Вечно ваш,
rwx64
