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

Сегодня вечером впервые решил зарегестрироваться на Хабре, дабы отписать вот в этом топике о недочётах 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
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 11
  • +1
    3-я нулевыми байтами.
    Один из этих «нулевых байт» — обрезок первого символа, а разделителем служит нулевой символ (они там по два байта на символ, UCS-2 же).
    • 0
      Приношу свои извинения, скорректировал. Это был огрызок символа. Всё верно, два нулевых байта как widechar.
    • +1
      Рантайм не следит за PEB, так что подмена PEB не заставляет кэш обновляться. Наблюдаемый эффект объясняется следующим образом: на момент первого вызова _wdupenv_s unicode-кэша просто нет, потому что до этого никакие unicode-функции работы с окружением не вызывались. Следовательно, первый вызов _wdupenv_s лезет в текущее окружение — в котором NewVar есть — кэширует его и возвращает информацию из текущего окружения. Следующий вызов возвращает кэшированную информацию, поэтому не видит, что PEB изменён.

      Пример программы нетипичен. С одной стороны, используется функция main, а не wmain, так что компоновщик выбирает startup-код в ANSI-версии, который заполняет кэш _environ, но не _wenviron. С другой стороны, сама функция main вызывает w-функции. Если заменить main на wmain — дописать одну букву в коде, ага, — эффект исчезнет.
      • 0
        Ну соль вообщем в том, чтобы использовать SetEnvironmentVariable/GetEnvironmentVariable etc и не париться с getenv.
        • 0
          Я делал упор на замену таблицы в целом и предложил вариант с использованием соответствующих интерфейсов. Этого достаточно, чтобы решить проблему, возникшую у исторического автора.

          Остальное просто банальный интерес.
          • 0
            Если уже так принципиально, то можно попробовать найти в хипе рантайма кэш этот и вставить туда свою переменную.
          • +4
            Ради доступа к gs: не нужно играть с ассемблером, он для более серьёзных вещей нужен. Для доступа к gs: есть intrinsic-псевдофункции __readgs{byte,word,dword,qword} и __writegs{byte,word,dword,qword}.
            • 0
              Спасибо, учту на будущее.
              • 0
                К сожалению у меня не так много кармы, чтобы заплюсовать Ваш комент про «интринсики». Вы реально облегчили мне жизнь только что!!!
              • 0
                А можно код дополнить комментариями? Или мне одному он неясен? :)

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.