Сегодня вечером впервые решил зарегестрироваться на Хабре, дабы отписать вот в этом топике о недочётах ilya314, но был предельно удивлён тем, что я оказывается не могу ничего откомментировать, поскольку не являюсь почётным хабропользователем. Какой ужас.
Поэтому я решил набросать код, дабы высказать свои соображения о проблеме дублирования данных в Сишный рантайм из 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
#include
#include
#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'' L'', как документирует 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'';
/* копируем строку с новой переменной после разделителя */
/* и добавляем завершающие 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 == %wsn", Buff);
printf_s("Restoring PEB Environment Table...n");
RestorePEBEnvironmentTable(OriginalEnvironment);
if (!GetEnvironmentVariableW(L"NewVar", Buff, sizeof(Buff)))
printf_s("GetEnvironmentVariableW(): NewVar not foundn");
}
_getch();
}
После чего следует вывод:
Возврат к «родной» таблице кэш не перестраивает. Таким образом, проблема автора с dll решается с помощью использования соответствующих интерфейсов kernel32 для работы с блоком переменных окружения.
Самое интересное, что данный метод будет работать и при подмене данных в стороннем процессе, что можно показать как-нибудь потом.UPDATE: Как правильно заметил CleverMouse, работа с переменными окружения через библиотеку Си не несёт смысловой нагрузки в конкретном примере, тк в случае с функцией main, заполняется рантайм кэш _environ, а не _wenviron, поэтому предлагаю рассматривать это как отладочные печати.
Вечно ваш,
rwx64