Как стать автором
Поиск
Написать публикацию
Обновить

Продолжение истории про переменные окружения, или подменяем PEB

Время на прочтение8 мин
Количество просмотров3.5K
Сегодня вечером впервые решил зарегестрироваться на Хабре, дабы отписать вот в этом топике о недочётах 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 байт. Убедиться в этом можно посмотрев на память процесса с помощью отладчика.

image

image

Теперь собёремся с силами и подменим 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();
}

После чего следует вывод:

image

Возврат к «родной» таблице кэш не перестраивает. Таким образом, проблема автора с dll решается с помощью использования соответствующих интерфейсов kernel32 для работы с блоком переменных окружения.

Самое интересное, что данный метод будет работать и при подмене данных в стороннем процессе, что можно показать как-нибудь потом.

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

Вечно ваш,
rwx64
Теги:
Хабы:
Всего голосов 18: ↑15 и ↓3+12
Комментарии11

Публикации

Ближайшие события