• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья PTM - Управление таблицей страниц из пользовательского режима

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
PTM - это библиотека C++ для Windows 10, которая позволяет программисту управлять всей памятью, физической и виртуальной, из пользовательского режима. Проект наследует интерфейс от VDM, позволяющий использовать примитив чтения-записи физической памяти для поддержки этого проекта. VDM используется исключительно для настройки таблиц страниц таким образом, чтобы PTM мог управлять ими из пользовательского режима. После того, как таблицы страниц настроены для PTM, VDM больше не требуется. Однако VDM может наследовать экземпляр PTM как средство чтения и записи физической памяти. И VDM, и PTM очень хорошо работают вместе и независимо друг от друга.

Введение

Управление таблицей страниц - чрезвычайно мощный примитив. Тот, который позволяет создавать новаторские проекты, такие как патч ядра только в определенных контекстах процесса или отображение адресного пространства исходного процесса в адресное пространство целевого процесса. PTM - это библиотека пользовательского режима, которая позволяет программисту управлять таблицами страниц из пользовательского режима в 64-битных системах Windows 10. PTM делает это с помощью VDM; проект, предназначенный для злоупотребления уязвимыми драйверами, выставляя примитив чтения-записи физической памяти (RWP) для повышения до произвольного выполнения ядра. VDM используется для настройки таблиц страниц таким образом, чтобы ими можно было управлять из пользовательского режима без необходимости загрузки уязвимого драйвера VDM в ядро после инициализации. Затем PTM можно использовать для получения и установки всех уровней записей таблицы страниц, преобразования линейных виртуальных адресов из пользовательского режима, отображения физической памяти в виртуальную память и даже создания новых таблиц страниц. PTM также может использоваться как средство для прямого чтения и записи в физическую память, поэтому его можно использовать с VDM для использования произвольного выполнения ядра без необходимости загрузки уязвимого драйвера VDM в ядро.

Основы виртуальной памяти и подкачки

Пейджинг - это концепция разбиения памяти на блоки фиксированного размера, называемые страницами. Страницы можно перемещать в физическую память и из нее, что позволяет перемещать на диск память, к которой нечасто обращаются. Чтобы это работало, ЦП не может напрямую взаимодействовать с физической памятью, вместо этого ЦП взаимодействует с виртуальной памятью. Виртуальные адреса преобразуются в физические адреса с помощью набора таблиц, называемых таблицами страниц. В 64-битной системе с процессором в длинном режиме существует четыре уровня таблиц страниц: PML4(s), PDPT(s), PD(s) и, наконец, PT(s). Все таблицы страниц имеют одинаковый размер (1000h байт), если не указано иное. Каждая запись таблицы страниц имеет размер восемь байтов. Это означает, что каждая таблица содержит 512 записей (8 * 512 = 1000h). Последние двенадцать бит каждого виртуального адреса называются смещением страницы и представляют собой смещение на физической странице. Смещение страницы виртуального адреса может быть больше 12 бит в зависимости от конфигурации структуры подкачки для данного виртуального адреса. Длина поля смещения страницы может составлять 12 бит (физическая страница 4 КБ), 21 бит (физическая страница 2 МБ) или 30 бит (страница 1 ГБ).

IqB4B22.png



Чтобы преобразовать линейные виртуальные адреса в линейные физические адреса, необходимо пройти по таблицам страниц. Как показано на рисунке 1, каждое виртуальное адресное пространство имеет свой собственный PML4, физический адрес этой таблицы хранится в CR3.

Взаимодействие с таблицами страниц в Windows

В Windows планировщик потоков использует KPROCESS.DirectoryTableBase при планировании потоков. Структура KPROCESS является подструктурой структуры EPROCESS и содержит DirectoryTableBase по смещению 28h. Программист, использующий VDM, может легко получить линейный физический адрес PML4 процесса, задав DKOM структуру KPROCESS желаемого процесса.

kd> dt !_KPROCESS ffffc38759d9e080
nt!_KPROCESS
+0x000 Header : _DISPATCHER_HEADER
+0x018 ProfileListHead : _LIST_ENTRY
+0x028 DirectoryTableBase : 0x00000001`15684000
+0x030 ThreadListHead : _LIST_ENTRY
+0x040 ProcessLock : 0
+0x044 ProcessTimerDelay : 0
+0x048 DeepFreezeStartTime : 0

Как только физический адрес желаемых процессов PML4 получен, уловка заключается в взаимодействии со структурами поискового вызова. Хотя VDM позволяет читать и писать в физическую память, имейте в виду, что MmMapIoSpace не может использоваться для отображения структур подкачки в виртуальную память. Однако драйверы, использующие MmCopyMemory и ZwMapViewOfSection для взаимодействия с физической памятью, могут использоваться для непосредственного управления таблицами страниц. Для правильной поддержки VDM, которую PTM наследует как кодовую базу, проект не полагается на физические примитивы чтения и записи, предоставляемые драйвером. Вместо этого PTM выделяет свой собственный набор таблиц страниц и вставляет PML4E в текущие процессы PML4, указывая на такие таблицы. Это позволяет программисту по желанию отображать физическую память в текущее адресное пространство виртуальной памяти в пользовательском режиме. Другими словами, как только таблицы распределены и настроены, необходимость в VDM отпадает, поскольку таблицами подкачки можно полностью управлять из пользовательского режима.

Что такое TLB

Буфер трансляции - это аппаратный кэш, который помогает преобразовывать линейные виртуальные адреса в линейные физические адреса. TLB кэширует преобразования виртуальных адресов в физические, а также другую информацию, такую как права доступа к страницам и информацию о типе кеша. Хотя TLB чрезвычайно важен для эффективности, он сделал PTM интересной задачей. Например, когда физическая память отображается в виртуальное адресное пространство, записи таблицы страниц будут вставлены или изменены. Эта вставка или изменение существующей записи таблицы страниц может относиться к кэшированной записи в TLB. Это означает, что эффекты, применяемые к записи таблицы страниц, не будут видны до тех пор, пока запись TLB для данной виртуальной страницы не будет признана недействительной, вместе с изменениями, записанными в основную память. Чтобы противодействовать этому, в ЦП есть инструкция, которая позволяет программисту аннулировать запись таблицы страниц в кэше TLB. Эта инструкция называется INVLPG и является привилегированной инструкцией. Это не то, что может использовать PTM, поскольку библиотека предназначена для работы полностью в пользовательском режиме. Непосредственная аннулирование TLB - не единственный способ сделать записи недействительными. Если возникает ошибка страницы, TLB аннулирует записи для данного адреса, который вызвал ошибку (адрес в CR2). Это эффективный метод аннулирования желаемых виртуальных адресов из пользовательского режима, но он очень медленный. Переключение контекста по своей сути не вызывает сброс TLB, скорее, PCID изменяется на другой PCID. Это позволяет TLB сохранять записи из нескольких адресных пространств и улучшать производительность. Однако выполнение может сделать записи TLB недействительными, потому что планировщик перепрограммирует логический процессор для выполнения в другом месте в течение некоторого времени, возможно, заполняя TLB другими записями и удаляя те, которые ранее были кэшированы.

TLB - Outrun

Хотя TLB представляет собой эффективный аппаратный кеш, он не может кэшировать линейные виртуальные адреса, к которым ранее не было доступа, этот простой факт означает, что программист может создавать новый линейный виртуальный адрес каждый раз, когда ему нужно отобразить новую физическую страницу в виртуальная память. Однако это не надежное решение, которое надежно работает на всех современных процессорах. По мере того, как отрасль продвигает технологию виртуализации, расширение TLB продолжается. Таким образом, единственное создание нового линейного виртуального адреса каждый раз, когда вы хотите взаимодействовать с физической страницей, не является разумным решением и уже нестабильно на большинстве современных чипов AMD. Вместо этого идеально сочетать эту технику с другими техниками.

C++:
auto ptm_ctx::map_page(void* addr) -> void*
{
    ++pte_index;
    if (pte_index > 511)
    {
        ++pde_index;
        pte_index = 0;
    }

    if (pde_index > 511)
    {
        ++pdpte_index;
        pde_index = 0;
    }

    if (pdpte_index > 511)
        pdpte_index = 0;

    // insert paging table entries down here…
    //... (refer to PTM repo to see that code)...
    // returns the newly generated virtual address...
    return get_virtual_address();
}

Приведенный выше код генерирует новый линейный виртуальный адрес, к которому раньше не обращались. Этот линейный виртуальный адрес указывает на физическую страницу запросов в памяти. Это позволяет программисту обходить TLB, обращаясь к новым линейным виртуальным адресам, вместо того, чтобы пытаться аннулировать запись TLB существующей и уже кэшированной страницы. Однако это имеет ограничения, поскольку код предоставляет только 512^3 различных возможных виртуальных страниц.

TLB - Преимущество сомнения

Хотя опережение TLB является самым быстрым решением для отображения физической памяти в виртуальную память без необходимости отменять какие-либо записи TLB, это не самое стабильное решение на современном оборудовании. Вместо этого предпочтительнее сочетание генерации нового виртуального адреса и цикла SEH try/except. Предоставляя новому виртуальному адресу benefit of the doubt в том, что он еще не был кэширован, выполняется попытка доступа к вновь созданной странице. Если доступ успешен, новый линейный виртуальный адрес возвращается вызывающей стороне ptm::ptm_ctx::map_page. Однако, если доступ вызывает сбой страницы, TLB аннулирует записи, связанные с этим вновь созданным линейным виртуальным адресом. Затем блок except пытается получить доступ к новой странице в цикле, обеспечивая выполнение при каждой неудаче доступа к новому виртуальному адресу. Этот метод обеспечивает наиболее эффективное решение для работы с TLB из пользовательского режима. Этот метод гарантирует, что сгенерированный линейный виртуальный адрес доступен перед его возвратом вызывающей стороне.

C++:
auto ptm_ctx::get_virtual_address() const -> void*
{
    //...
   
    // start off by making sure that
    // the address is accessible...
    __try
    {
        *(std::uint8_t*)new_addr.value = *(std::uint8_t*)new_addr.value;
        return new_addr.value;
    }

    // if its not accessible then the
    // TLB just invalidated its entry...
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        // loop until this new address is accessible…
        // do not return until this new virtual
        // address is accessible....
        while (true)
        {
            // try again to access the page again
            // and it should return...
            __try
            {
                *(std::uint8_t*)new_addr.value =
                    *(std::uint8_t*)new_addr.value;
                return new_addr.value;
            }

            // if its still not accessible, then we are
            // going to let the core get
            // rescheduled for a little...
            __except (EXCEPTION_EXECUTE_HANDLER)
            {
                while (!SwitchToThread())
                    continue;
            }
        }
    }
    return new_addr.value;
}

Этот код выше выполняется каждый раз, когда новая страница отображается в виртуальную память через PTM. Это гарантирует, что новый созданный виртуальный адрес доступен перед возвратом нового виртуального адреса.

Осложнения с пейджингом

Хотя PTM управляет своим собственным набором таблиц подкачки из пользовательского режима, физический адрес этих виртуальных таблиц страниц может изменяться. Таблицы страниц, выделенные VirtualAlloc, могут сами быть выгружены на диск, если к ним не часто обращаются. Когда они возвращаются обратно при повторном доступе, весьма вероятно, что они будут размещены на новой физической странице. Диспетчер рабочего набора играет большую роль в подкачке памяти на диск. Этот поток повторяется бесконечно и создается при запуске операционной системы. Этот поток диспетчера рабочего набора оказался весьма утомительным, поэтому он приостановлен PTM.

C++:
auto get_setmgr_pethread(vdm::vdm_ctx& v_ctx) -> PETHREAD
{
    ULONG return_len = 0u;
    std::size_t alloc_size = 0x1000u;
    auto process_info =
        reinterpret_cast<SYSTEM_PROCESS_INFORMATION*>(
            malloc(alloc_size));

    while (NtQuerySystemInformation
    (
        SystemProcessInformation,
        process_info,
        alloc_size,
        &return_len
    ) == STATUS_INFO_LENGTH_MISMATCH)
        process_info =
        reinterpret_cast<SYSTEM_PROCESS_INFORMATION*>(
            realloc(process_info, alloc_size += 0x1000));

    const auto og_ptr = process_info;
    while (process_info &&
        process_info->UniqueProcessId != (HANDLE)4)
        process_info =
            reinterpret_cast<SYSTEM_PROCESS_INFORMATION*>(
                reinterpret_cast<std::uintptr_t>(process_info)
                    + process_info->NextEntryOffset);

    auto thread_info =
        reinterpret_cast<SYSTEM_THREAD_INFORMATION*>(
            reinterpret_cast<std::uintptr_t>(process_info)
                + sizeof SYSTEM_PROCESS_INFORMATION);

    static const auto ntoskrnl_base =
        util::get_kmodule_base("ntoskrnl.exe");

    const auto [ke_balance_um, ke_balance_rva] =
        util::memory::sig_scan(
            KE_BALANCE_SIG, KE_BALANCE_MASK);

    auto rip_rva =
        *reinterpret_cast<std::uint32_t*>(ke_balance_um + 19);
       
    const auto ke_balance_set =
        ntoskrnl_base + ke_balance_rva + 23 + rip_rva;

    const auto [suspend_in_um, suspend_rva] =
        util::memory::sig_scan(
            SUSPEND_THREAD_SIG, SUSPEND_THREAD_MASK);

    rip_rva =
        *reinterpret_cast<std::uint32_t*>(
            suspend_in_um + 1);
           
    const auto ps_suspend_thread =
        reinterpret_cast<void*>(
            ntoskrnl_base + rip_rva + 5 + suspend_rva);

    static const auto lookup_pethread =
        util::get_kmodule_export(
            "ntoskrnl.exe", "PsLookupThreadByThreadId");

    for (auto idx = 0u; idx < process_info->NumberOfThreads; ++idx)
    {
        if (thread_info[idx].StartAddress ==
            reinterpret_cast<void*>(ke_balance_set))
        {
            PETHREAD pethread;
            auto result = v_ctx.syscall<PsLookupThreadByThreadId>(
                lookup_pethread, thread_info[idx]
                    .ClientId.UniqueThread, &pethread);

            free(og_ptr);
            return pethread;
        }
    }

    free(og_ptr);
    return {};
}

Код, определенный выше, получит PETHREAD диспетчера рабочего набора с помощью VDM и вернет его вызывающей стороне. Затем вызывающий код может использовать VDM для системного вызова PsSuspendThread, чтобы приостановить поток диспетчера рабочих наборов.

C++:
auto stop_setmgr(vdm::vdm_ctx& v_ctx, PETHREAD pethread) -> NTSTATUS
{
    static const auto ntoskrnl_base =
        util::get_kmodule_base("ntoskrnl.exe");

    const auto [suspend_in_um, suspend_rva] =
        util::memory::sig_scan(
            SUSPEND_THREAD_SIG, SUSPEND_THREAD_MASK);

    const auto rip_rva =
        *reinterpret_cast<std::uint32_t*>(
            suspend_in_um + 1);
       
    const auto ps_suspend_thread =
        reinterpret_cast<void*>(
            ntoskrnl_base + rip_rva + 5 + suspend_rva);
       
    return v_ctx.syscall<PsSuspendThread>(
        ps_suspend_thread, pethread, nullptr);
}

Использование PTM и примеры

PTM - очень обширная библиотека, которая предлагает все стандартные операции с таблицами подкачки, такие как вставка записей и получение записей. PTM имеет предопределенные процедуры для получения и установки всех записей таблицы подкачки на всех уровнях таблицы подкачки. PTM может преобразовывать линейные виртуальные адреса двумя предопределенными способами. Один способ использует отображения гиперпространства и может использоваться только для преобразования линейных виртуальных адресов текущего адресного пространства. Другая подпрограмма вручную отображает таблицы страниц в текущее адресное пространство и вручную просматривает таблицы страниц.

Создание объекта PTM

Чтобы создать объект PTM, вы должны сначала создать объект VDM. Это легко сделать, все, что требуется, - это две лямбда-выражения, которые предоставляют метод для чтения и записи физической памяти соответственно. После создания объекта VDM вам понадобится PID процесса, из которого вы хотите сделать объект PTM. Просто укажите объект VDM по ссылке и PID процесса.

C++:
// describe the method of reading/writing physical memory.
vdm::read_phys_t _read_phys =
    [&](void* addr, void* buffer, std::size_t size) -> bool
{
    return vdm::read_phys(addr, buffer, size);
};

vdm::write_phys_t _write_phys =
    [&](void* addr, void* buffer, std::size_t size) -> bool
{
    return vdm::write_phys(addr, buffer, size);
};

vdm::vdm_ctx vdm(_read_phys, _write_phys);
nasa::mem_ctx my_proc(&vdm);

Если вы работаете с текущим контекстом памяти процессов, вам не нужно указывать PID.

Получение заголовков PDE ntoskrnl.exe

Как описано в приведенном ниже коде, ptm::ptm_ctx::get_pde получает PDE данного виртуального адреса. Эта процедура работает со всеми виртуальными адресами как в пользовательском режиме, так и в адресах ядра.

C++:
// describe the method of reading/writing physical memory.
vdm::read_phys_t _read_phys =
    [&](void* addr, void* buffer, std::size_t size) -> bool
{
    return vdm::read_phys(addr, buffer, size);
};

vdm::write_phys_t _write_phys =
    [&](void* addr, void* buffer, std::size_t size) -> bool
{
    return vdm::write_phys(addr, buffer, size);
};

vdm::vdm_ctx vdm(_read_phys, _write_phys);
nasa::mem_ctx my_proc(&vdm);

const auto ntoskrnl_base =
    reinterpret_cast<void*>(
        util::get_kmodule_base("ntoskrnl.exe"));

const auto ntoskrnl_pde = my_proc.get_pde(ntoskrnl_base);
std::printf("[+] pde.present -> %d\n", ntoskrnl_pde.second.present);
std::printf("[+] pde.pfn -> 0x%x\n", ntoskrnl_pde.second.pfn);
std::printf("[+] pde.large_page -> %d\n", ntoskrnl_pde.second.large_page);

Переворот NX бита

В этом примере первая страница основного модуля в текущем процессе становится исполняемой без вызова каких-либо функций Windows API, таких как VirtualProtect(Ex). Это не изменит значения VAD, поэтому, если бы вы открыли NtQueryVirtualMemory на этой странице, это было бы PAGE_READONLY, но если бы ЦП попытался получить инструкции с этой страницы, он бы это сделал успешно.


C++:
// describe the method of reading/writing physical memory.
vdm::read_phys_t _read_phys =
    [&](void* addr, void* buffer, std::size_t size) -> bool
{
    return vdm::read_phys(addr, buffer, size);
};

vdm::write_phys_t _write_phys =
    [&](void* addr, void* buffer, std::size_t size) -> bool
{
    return vdm::write_phys(addr, buffer, size);
};

vdm::vdm_ctx vdm(_read_phys, _write_phys);
nasa::mem_ctx my_proc(&vdm);

const auto baseaddr_pte =
my_proc.get_pte(GetModuleHandleA(NULL));

baseaddr_pte.nx = false;
my_proc.set_pte(GetModuleHandleA(NULL), baseaddr_ptr);

Заключение

В заключение, PTM - это еще одна высокомодульная библиотека, которая позволяет программисту управлять всей памятью, виртуальной и физической, из пользовательского режима. PTM используется во всех других моих недавних проектах, таких как reverse-injector, PSKP, PSKDM и kmem. Хотя этот проект идеально подходит для работы с таблицей страниц, у него есть свои недостатки. Как описано в разделе 6, таблицы страниц VirtualAlloc могут быть выгружены на диск, и, таким образом, при подкачке обратно в физическую память они, скорее всего, будут размещены на новой физической странице. Однако это оказалось незначительной проблемой после приостановки потока диспетчера рабочего набора, который отвечает за подкачку виртуальной памяти на диск.


Источник: https://back.engineering/01/12/2020/
Автор перевода: yashechka
Переведено специально для https://xss.pro
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх