На заметку Детали реализации CFG в Windows 11


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 101
Репутация
8 223
1715508645356.png


Перевод:

CFG в контексте Windows означает Control Flow Guard. Это функция безопасности, предназначенная для предотвращения атак, основанных на нарушении управления потоком выполнения программы.

Как работает Control Flow Guard:
  • Регистрация допустимых точек вызова: При компиляции программы компилятор анализирует код и создает список всех допустимых точек вызова функций (адресов, по которым допустимо передавать управление). Этот список включается в исполняемые файлы и загружаемые модули.
  • Проверка во время выполнения: Когда программа выполняется, операционная система использует этот список для проверки корректности адресов передачи управления перед их выполнением. Если программа пытается выполнить переход на адрес, который не находится в списке разрешенных точек вызова, CFG блокирует такой вызов и предотвращает возможное выполнение вредоносного кода.
Hotpatching долгое время был в центре внимания Windows 11, уже использовавшийся в серверных и облачных системах. В марте стало известно, что первой крупной версией, включающей эту функцию, будет 24H2, что теперь можно подтвердить всего за несколько минут анализа ядра или ntdll.

Hotpatching — это техника обновления программного обеспечения на лету, которая позволяет исправлять ошибки или обновлять систему без необходимости перезагружать её или останавливать работающие приложения. Эта технология особенно полезна в серверных средах, где требуется высокая доступность и минимальное прерывание работы системы.

Процесс hotpatching включает внесение изменений в исполняемые файлы или активные процессы путем динамической подмены инструкций или функций. В Windows, например, это может быть реализовано через специальные механизмы, такие как hotpatch-совместимые точки в функциях, которые начинаются с NOP-слайдов (пустых операций), позволяющих безопасно внедрить новый код.

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

Оказывается, что это функция довольно обширна и встраивается во многие ключевые функциональные возможности ОС. Как следствие, она может нарушить работу некоторых программ, и одной из неудачных жертв на этот раз стала x64dbg.

На прошлой неделе я проводил отладку на сборке 24H2 для участников инсайдерской программы и обнаружил, что представление карты памяти в x64dbg сломалось. Вместо того, чтобы показывать отдельную запись для каждого раздела каждого модуля, некоторые модули объединяли свои разделы в одну запись.

Например, память, относящаяся к kernel32.dll, выглядела так:

Адрес: 00007FFD053C0000
Размер: 00000000000C8000
Область: Система
Информация: kernel32.dll, ".text", "fothk", ".rdata", ".data", ".pdata", ".didat", ".rsrc", ".reloc"

В то время как обычно это выглядело бы так:

Адрес: 00007FF975E90000
Размер: 0000000000001000
Область: Система
Информация: kernel32.dll

Адрес: 00007FF975E91000
Размер: 000000000007E000
Область: Система
Информация: ".text"

и так далее для других секций.

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

Но странности на этом не закончились, и вскоре я заметил, что с этой страницей мало что можно было сделать, поскольку VirtualProtect не работала с ней, возвращая STATUS_NOT_COMMITTED. Тем не менее, страница считалась замапленной в x64dbg, а также другими инструментами инспекции памяти и отладчиками.

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

Кратко:

С появлением hotpatching на Windows 11 появилась функция SCP, цель которой, похоже, состоит в том, чтобы предоставить переносимые, независимые от позиции функции, которые впоследствии можно будет безболезненно подключить к процессам и отдельным модулям.

Хотя изменения затрагивают не только код CFG, большая часть их, кажется, сосредоточена на обеспечении независимости функций CFG от внешнего кода и данных. Основное изменение заключается в реализации новых (но функционально идентичных) функций CFG в их собственных разделах в ntdll (usermode) и функций kCFG в ntoskrnl (ядро).

Разделы копируются и исправляются в их собственные выделенные страницы во время выполнения, и эти страницы затем отображаются как в процессах, так и в отдельных модулях, которые удовлетворяют некоторым условиям, связанным с hotpatching. Похоже, что не было существенных улучшений в области безопасности, и изменения, вероятно, сосредоточены только на обеспечении совместимости с hotpatching.

Предварительные замечания:

Этот пост не о hotpatching. Это очень обширная функция, и для её анализа потребуется больше усилий, чем я вложил сюда. Цель этого поста — поделиться деталями реализации CFG в версии 24H2, включая пользовательский режим и ядро.

Новая реализация в некоторой степени связана с hotpatching, поэтому мы не сможем ответить на некоторые вопросы, и некоторые детали могут остаться неясными. Тем не менее, я считаю, что пост должен дать хороший обзор изменений и указания для дальнейших исследований.
Все отладка и реверс-инжиниринг проводились на сборке Windows 11 Pro 26100.268 для инсайдеров.

В этом посте я предполагаю, что читатель знаком с CFG на Windows и его историей и имеет умеренные знания о внутреннем устройстве Windows NT.

CFG имеет долгую историю на Windows, и вы можете узнать о нем больше из следующих источников:




Также рекомендуется прочитать:



Ядро содержит системное отображение, каждый бит которого указывает, является ли диапазон из 4 байтов допустимой целевой точкой вызова.

Это отображение обновляется каждый раз, когда образ загружается или выгружается в пользовательском пространстве.
Чтобы предоставить доступ к отображению в пользовательском пространстве, адрес отображения записывается в экспортированный блок инициализации системы DllSystemInitBlock в ntdll. Для использования CFG модуль предоставляет конфигурацию в своём каталоге отладки IMAGE_LOAD_CONFIG_DIRECTORY, в формате, описанном в документации.

Кроме того, в одном из разделов предоставляется таблица допустимых целевых точек вызова. Загрузчик ядра читает GuardCFFunctionTable и GuardCFFunctionCount из каталога, проходит по таблице и устанавливает соответствующие биты в системном отображении. Загрузчик в пользовательском режиме читает GuardCFCheckFunctionPointer и GuardCFDispatchFunctionPointer и патчит их, чтобы они указывали на одну из классических функций CFG, например, если CFG включен для процесса, то LdrpValidateUserCallTarget / LdrpDispatchUserCallTarget или их аналоги для подавления экспорта, в противном случае — функции nop.

Классические функции CFG проверяют (или проверяют и направляют) аргумент целевой точки вызова, переданный в RAX. Для этого они используют системное отображение, доступ к которому они получают, найдя его адрес в DllSystemInitBlock, который был ранее записан ядром.

Kernel CFG (kCFG) работает аналогичным образом, но только если включена безопасность на основе виртуализации. Это связано с тем, что VBS — единственный способ действительно защитить отображение kCFG от перезаписывания.

Новые разделы ntdll

Первое, что бросается в глаза в сборке 24H2, — это новые разделы в ntdll.

Их четыре: SCPCFG, SCPCFGFP, SCPCFGNP, SCPCFGES; и они помечены как RX, каждый из них занимает только одну страницу.

Структура содержимого одинакова для каждого раздела:

Код:
[+0x00] SCPCFG header
    [+0x00][0x04] Offset to dispatch (no es) function
    [+0x04][0x04] Offset to dispatch (es) function
    [+0x08][0x04] Offset to validate (no es) function
    [+0x0C][0x04] Offset to validate (es) function
    [+0x10][0x04] Offset to invalid call handler
    [+0x14][0x04] Offset to rtl function table
    [+0x18][0x28] Unknown // I didn't bother reversing
[+dynamic] Dispatch (no es) function
[+dynamic] Dispatch (es) function
[+dynamic] Validate (no es) function
[+dynamic] Validate (es) function
[+dynamic] Invalid call handler
[+dynamic] Icall handler
[+dynamic] Unwind table
    [+0x00][0x0C] Unwind table stuff
[+dynamic] Rtl function table
    [+0x00][dynamic] array of RUNTIME_FUNCTION entries

Например, секция SCPCFG выглядит так:

Код:
[+0x00] Заголовок SCPCFG
[+0x00][0x04] = 0x40
[+0x04][0x04] = 0xC0
[+0x08][0x04] = 0x140
[+0x0C][0x04] = 0x1C0
[+0x10][0x04] = 0x240
[+0x14][0x04] = 0x2A4
[+0x18][0x28] = {66 66 66 66 66 66 66 0F 1F 84 00 00 00 00 00 66 66 66 66 66 66 66 0F 1F 84 00 00 00 00 00 66 66 0F 1F 84 00 00 00 00 00}
[+0x40] = ScpCfgDispatchUserCallTarget
[+0xC0] = ScpCfgDispatchUserCallTargetES
[+0x140] = ScpCfgValidateUserCallTarget
[+0x1C0] = ScpCfgValidateUserCallTargetES
[+0x240] = ScpCfgHandleInvalidCallTarget
[+0x280] = ScpCfgICallHandler
[+0x298] = Таблица раскрутки
[+0x00][0x0C] = {19 00 00 00 80 02 00 00 00 00 00 00}
[+0x2A4] = Таблица функций Rtl
[+0x00][0x0C] {.SectionBegin = 0x00, .SectionEnd = 0x280, .UnwindData = 0x298}

Названия функций во всех секциях похожи на те, что используются в классическом CFG.

Поэтому несложно определить назначение каждой функции:

- Функции проверки и отправки являются аналогами классических функций проверки и отправки CFG.

- Различие между ES / NO ES связано с настройками подавления экспорта, о которых говорится

- Функция обработки недопустимых вызовов активируется из функции отправки, если цель вызова недопустима.

- Функция icall предназначена для работы в сочетании с обработкой исключений, но точные механизмы мне неизвестны. Помимо этих функций, секции также содержат таблицу функций с таблицей раскрутки, которые мы рассмотрим позже. Также есть 40 неизвестных байт в конце заголовка, которые я не смог понять, так как не видел кода, обращающегося к ним.

На первый взгляд функции в различных секциях выглядят похоже друг на друга, а также очень похоже на их классические аналоги CFG. Первое отличие, которое можно увидеть, заключается в том, что там, где классические функции CFG используют адрес карты cfg, записанный в DllSystemInitBlock, новые функции имеют жестко закодированное значение 0x0123456789ABCDEF. Похоже, это значение-заполнитель, которое должно быть исправлено чем-то позже.

Например:

ntdll!LdrpDispatchUserCall:
Код:
mov r11, cs:qword_1801D94F8 <------ адрес карты хранится в DllSystemInitBlock ntdll в секции .mrdata
mov r10, rax
shr r10, 9
mov r11, [r11+r10*8]
mov r10, rax
shr r10, 3
test al, 0Fh
jnz short loc_fail
bt r11, r10
jnb short loc_fail
jmp rax
...
ntdll!LdrpDispatchUserCall

ntdll!ScpCfgDispatchUserCall:
Код:
mov r11, 123456789ABCDEFh <------ жестко закодированное значение-заполнитель
mov r10, rax
shr r10, 9
mov r11, [r11+r10*8]
mov r10, rax
shr r10, 3
test al, 0Fh
jnz short loc_fail
bt r11, r10
jnb short loc_fail
jmp rax
...
ntdll!ScpCfgDispatchUserCall

Также существуют значимые различия между секциями. Чтобы продемонстрировать это, давайте посмотрим на реализацию функции отправки в каждой из секций:
ntdll!ScpCfgDispatchUserCallTarget_Nop (SCPCFGNP):
jmp     rax

ntdll!ScpCfgDispatchUserCall (SCPCFG):
mov     r11, 123456789ABCDEFh
mov     r10, rax
shr     r10, 9
mov     r11, [r11+r10*8]
mov     r10, rax
shr     r10, 3
test    al, 0Fh
jnz     short loc_fail
bt      r11, r10
jnb     short loc_fail
jmp     rax
...

ntdll!ScpCfgDispatchUserCallTarget_ES (SCPCFGES):
mov     r11, 123456789ABCDEFh
mov     r10, rax
shr     r10, 9
mov     r11, [r11+r10*8]
mov     r10, rax
shr     r10, 3
test    al, 0Fh
jnz     short loc_180160069
bt      r11, r10
jnb     short loc_180160074
jmp     rax
...

ntdll!ScpCfgDispatchUserCallTarget_Fptr (SCPCFGFP):
mov     r11, 123456789ABCDEFh
mov     r11, [r11]
jmp     r11

Реализация функции отправки в SCPCFG и SCPCFGES одинакова, но две другие секции работают по-другому. SCPCFGNP реализует функцию nop, немедленно переходя к косвенному указателю функции. С другой стороны, реализация в SCPCFGFP переходит к указателю функции, считываемому из адреса, который, кажется, неизвестен на момент компиляции.

Имея это в виду, несложно определить намерения каждой секции:

SCPCFG и SCPCFGES представляют обычные реализации CFG, имеющие доступ к карте, которая хранит информацию о том, является ли адрес допустимой целевой точкой вызова.

SCPCFGES использует подавление экспорта, в то время как SCPCFG — нет. SCPCFGNP представляет отсутствие CFG. Его реализации определены так, чтобы пропускать любую целевую точку вызова. (NP = NOP) SCPCFGFP вызывает реализацию, расположенную в другом месте, через указатель функции. (FP = функциональный указатель).

Еще одна вещь, которую стоит рассмотреть, это обработчик недопустимых целевых вызовов. Для всех секций, кроме SCPCFGNP, его реализация выглядит так:

Код:
mov     r11, 123456789ABCDEFh
jmp     r11

Это означает, что он также должен переходить в еще неопределенное место. Как мы увидим позже, все значения-заполнители будут исправлены ядром.

Единственные ссылки на новые секции внутри ntdll находятся в новом экспорте - RtlpScpCfgntdllExports, который указывает на заголовки и концы разных секций. Экспорт также упоминается только в таблице экспорта.

Код:
.rdata:00000001801654A0 RtlpScpCfgntdllExports
.rdata:00000001801654A0      dq offset ScpCfgHeader_Nop
.rdata:00000001801654A8      dq offset ScpCfgEnd_Nop
.rdata:00000001801654B0      dq offset ScpCfgHeader
.rdata:00000001801654B8      dq offset ScpCfgEnd
.rdata:00000001801654C0      dq offset ScpCfgHeader_ES
.rdata:00000001801654C8      dq offset ScpCfgEnd_ES
.rdata:00000001801654D0      dq offset ScpCfgHeader_Fptr
.rdata:00000001801654D8      dq offset ScpCfgEnd_Fptr
.rdata:00000001801654E0      dq offset LdrpGuardDispatchIcallNoESFptr
.rdata:00000001801654E8      dq offset __guard_dispatch_icall_fptr
.rdata:00000001801654F0      dq offset LdrpGuardCheckIcallNoESFptr
.rdata:00000001801654F8      dq offset __guard_check_icall_fptr
.rdata:0000000180165500      dq offset LdrpHandleInvalidUserCallTarget

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

Инициализация ядра
Подобно классическому CFG, большая часть логики, связанной с SCPCFG, происходит в ядре.

Классический CFG инициализируется в функции MiInitializeCfg, и эта функция остается неизменной между версиями 23H2 и 24H2.

SCPCFG инициализируется в другой функции — MiInitializeImageViewExtension. Это происходит во время первой фазы инициализации, и её цель заключается в том, чтобы отобразить четыре секции из ntdll в ядро и настроить их так, чтобы позже они могли быть перемещены в любое место в пользовательском пространстве и использоваться "из коробки".

Следующие шаги составляют алгоритм процесса:

[MmInitializeImageViewExtension]
Ядро получает представление глобальной карты cfg для начального системного процесса, вызывая MiMapSecurePureReserveView с PsInitialSystemProcess. Внутренне это реализуется через MmMapViewOfSectionEx, и представление дополнительно защищается через MiSecureVad, который запрещает изменение защиты представления. Это представление затем сохраняется в глобальной переменной.

[MiInitializeImageViewExtensionCfg] Выделяются четыре новые объединенные страницы, и соответствующие блоки объединения сохраняются в глобальном массиве. Страницы изначально пусты и будут заполнены позже. Мы назовем их страницами SCPCFG, поскольку они будут хранить данные, взятые из секций SCPCFG ntdll.

[PsInitializeScpCfgPages / PspLocateNtdllAddressesForScpCfg] Экспортированный RtlpScpCfgntdllExports находится в ntdll через его таблицу экспорта и используется для определения смещения четырех секций SCPCFG в файле. Содержимое каждой секции затем копируется в соответствующую страницу SCPCFG, и выполняется ряд проверок на целостность данных.

[PspLocateNtdllAddressesForScpCfg] Указатели на функции SCPCFG ntdll сохраняются в глобальном массиве (PspNtdllScpFunctions). Здесь указатели не указывают ни на одну из новых страниц, ни на секции, встроенные в ntdll. Они рассчитываются как ntdllBaseAddress + ntdllImageSizeInMemory + fixedOffsetToTheFunction, что предполагает, что одна из страниц SCPCFG будет отображена в конце области изображения ntdll в пользовательском пространстве. Позже мы увидим, что это действительно так.

[PspFinalizeScpCfgPage] Значения-заполнители (т.е. 0x0123456789ABCDEF) заменяются на всех четырех страницах SCPCFG. В частности, заполнитель адреса карты cfg заменяется на адрес представления к карте cfg, полученный на первом шаге. Заполнитель в обработчике недопустимых вызовов заменяется на указатель, сохраненный в последнем поле RtlpScpCfgntdllExports, т.е. LdrpHandleInvalidUserCallTarget. Заполнительные указатели функций в секции SCPCFGFP заменяются на указатели, сохраненные в RtlpScpCfgntdllExports, начиная с LdrpGuardDispatchIcallNoESFptr и заканчивая __guard_check_icall_fptr.

Вот как это в коде:

C:
void MmInitializeImageViewExtension(bool doInitialize)
{
    if (doInitialize)
    {
        // Эта ветка достигается во время первой фазы инициализации
        if (SUCCEEDED(MiMapSecurePureReserveView(PsInitialSystemProcess, g_CFGBitmap, g_SCPCFGBitmapView, ...))
        {
            // g_SCPCFGBitmapView теперь содержит только для чтения защищенное представление карты CFG
            ...
            MiInitializeImageViewExtensionCfg(true);
        }
    }
    else
    {
        ... // Эта ветка достигается во время предварительной инициализации и выполняет некоторую инициализацию, связанную с hotpatching
    }
}

void MmInitializeImageViewExtensionCfg(bool arg)
{
    // Операции ядра, такие как выделение PTE
    ...
   
    // Массив страниц, которые будут использоваться для хранения секций SCPCFG
    void* scpCfgPages[4];
   
    for (uint32_t sectionIndex = 0; sectionIndex < 4; ++sectionIndex)
    {
        // Выделение страницы, инициализация pfn, создание допустимого pte
        ...
        void* newPage = ...;
        scpCfgPages[sectionIndex] = newPage;
       
        // Выделение нового блока объединения для страницы
        auto combineBlock = MiAllocateCombineBlock(...);
       
        // Заполнение информации в блоке объединения и отображение страницы
        ...
       
        // Запись блока объединения в массив в глобальном MiState, мы назовем этот массив g_SCPCFGSectionBlocks
        // поскольку arg == 1 во время инициализации, используемое смещение равно 0xD78
        *(void**)(MiState + 4 * sectionIndex + (arg ? 0xD78 : 0xD98)) = combineBlock;
    }
   
    // Инициализация недавно отображенных страниц
    PsInitializeScpCfgPages(scpCfgPages, ..., g_SCPCFGBitmapView, ...);
}

NTSTATUS PsInitializeScpCfgPages(void** pages, ..., void* cfgBitmapView, ...)
{
    // Определение адресов ntdll для scpcfg
    RTL_SCP_CFG_NTDLL_EXPORTS sections;
    RTL_SCP_CFG_NTDLL_EXPORTS_ARM64EC arm64EcSections;
    if (SUCCEEDED(PspLocateNtdllAddressesForScpCfg(..., &sections, &arm64EcSections))
    {
        // Копирование каждой секции в соответствующую страницу
        for (uint32_t sectionIndex = 0; sectionIndex < 4; ++sectionIndex)
        {
            auto section = &sections.headers[sectionIndex];   // смещение sectionIndex * 0x10
            memcpy(pages[sectionIndex], section->Begin, section->End - section->Begin);
            ...
        }
       
        // Убедиться, что смещения функций имеют фиксированное значение в заголовке, т.е.
        // [0] = 0x40, [1] = 0xC0, [2] = 0x140, [3] = 0x1C0
        ...
       
        // Финализация каждой отображенной секции
        for (uint32_t sectionIndex = 0; sectionIndex < 4; ++sectionIndex)
        {
            if (SUCCEEDED(PspFinalizeScpCfgPage(pages[sectionIndex], sectionIndex, cfgBitmapView, sections))
            {
                ...
            }
        }
    }
}

NTSTATUS PspLocateNtdllAddressesForScpCfg(..., RTL_SCP_CFG_NTDLL_EXPORTS* outSections, RTL_SCP_CFG_NTDLL_EXPORTS_ARM64EC* outArm64Sections)
{
    // Обнуление секций arm64
    memset(outArm64Sections, 0, sizeof(outArm64Sections));
   
    // Найти экспорт SCPCFG в ntdll
   if (SUCCEEDED(PspCopyNtdllExport(..., "RtlpScpCfgNtdllExports", outSections, ...))
   {
      // Проверки целостности и некоторое внутреннее переназначение, которое не влияет на логику
      ...
   }
 
   // Установка глобального PspNtdllScpFunctions
   PspNtdllScpFunctions[0] = MmGetScpCfgFunctionOffset(0x140, ntdllImageSize);
   PspNtdllScpFunctions[1] = MmGetScpCfgFunctionOffset(0x1C0, ntdllImageSize);
   PspNtdllScpFunctions[2] = MmGetScpCfgFunctionOffset(0x40, ntdllImageSize);
   PspNtdllScpFunctions[3] = MmGetScpCfgFunctionOffset(0xC0, ntdllImageSize);
}

void PspFinalizeScpCfgPage(void* mappedSectionPage, uint32_t sectionIndex, void* cfgBitmapView, RTL_SCP_CFG_NTDLL_EXPORTS* sections)
{
    if (pageIndex == 1 || pageIndex == 2) // ветка для SCPCFG и SCPCFGES
    {
        // Ряд проверок целостности для убеждения, что данные в секции выглядят хорошо
        ...
       
        // Замена заполнителя адреса карты cfg в коде каждой из первых четырех функций в секции
        for (uint32_t i = 0; i < 4; ++i)
        {
            *(void**)(mappedSectionPage + mappedSectionPage->func_offset[i] + 0x02) = cfgBitmapView;
        }
       
        // Замена заполнителя указателя функции в CfgScpHandleInvalidUserCallTarget и т.д.
        *(void**)(mappedSectionPage + mappedSectionPage->func_offset[4] + 0x02) = sections->Ptr_HandleInvalidUserCallTarget;
    }
    else if (pageIndex == 3) // ветка для SCPCFGFP
    {
        // Подобные проверки целостности
        ...
       
        // Замена заполнителя указателя функции в коде каждой из первых четырех функций для секции FPTR
        *(void**)(mappedSectionPage + mappedSectionPage->func_offset[0] + 0x02) = sections->Ptr_LdrpGuardDispatchIcallNoESFptr;
        *(void**)(mappedSectionPage + mappedSectionPage->func_offset[1] + 0x02) = sections->Ptr___guard_dispatch_icall_fptr;
        *(void**)(mappedSectionPage + mappedSectionPage->func_offset[2] + 0x02) = sections->Ptr_LdrpGuardCheckIcallNoESFptr;
        *(void**)(mappedSectionPage + mappedSectionPage->func_offset[3] + 0x02) = sections->Ptr__guard_check_icall_fptr;
       
        // И также замена заполнителя указателя функции для HandleInvalidUserCallTarget
        *(void**)(mappedSectionPage + mappedSectionPage->func_offset[4] + 0x02) = sections->Ptr_HandleInvalidUserCallTarget;
    }
}

К концу инициализации ядро имеет следующее:
  1. Защищённое представление карты CFG, сохранённое в глобальной переменной, которую я назвал g_SCPCFGBitmapView.
  2. Четыре объединённые страницы, содержащие соответствующие данные секций SCPCFG, скопированные из ntdll, исправленные для функциональности кода. Соответствующий блок каждой страницы хранится в глобальном массиве, который я назвал g_SCPCFGSectionBlocks.
  3. Четыре адреса функций, сохранённые в глобальном массиве PspntdllScpFunctions, указывающие на функции scpcfg на странице, отображённой в конце области отображения пользовательского пространства ntdll.
Заметки:
  • Смещения первых четырёх функций внутри каждой секции могут быть определены динамически, но код ядра утверждает, что они фактически фиксированы, то есть смещения должны быть 0x40, 0xC0, 0x140 и 0x1C0. Это можно увидеть в PsInitializeScpCfgPages.
  • PspLocateNtdllAddressesForScpCfg принимает указатель на структуру с именем RTL_SCP_CFG_NTDLL_EXPORTS_ARM64EC, однако аргумент просто обнуляется. Я не проверял версию ядра для ARM64, поэтому возможно, что там это правильно заполнено. Конечно, не имеет смысла, что нам нужна информация ARM64EC на x64.
  • Указатели функций в PspntdllScpFunctions указывают на ничто в тот момент, когда они сохраняются, так как ничего не было отображено в пространство в конце области изображения ntdll. Одна из страниц SCPCFG будет отображена немного позже частью кода, ответственной за связывание модуля пользовательского режима. Задержка в инициализации здесь не является проблемой, так как никто не будет обращаться к функциям до того, как страница будет отображена.
  • Теперь ясно, что секция SCPCFGFP предназначена для эмуляции классического CFG. В процедуре исправления заполнители указателей функций заменяются указателями на классические функции CFG. Это не значит, что она всегда будет эмулировать классический CFG, так как указатели функций могут быть легко перезаписаны снова, но это не происходит в данный момент.
Линковка ядра:

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

Линковка процесса:

Точкой, которая нас интересует, является MiMapProcessExecutable. Эта функция вызывается во время выделения процесса в режиме ядра, с цепочкой вызовов PspAllocateProcess -> MiInitializeProcessAddressSpace -> MiMapProcessExecutable.

Затем выполняются следующие шаги для настройки SCPCFG для процесса:

[MiCfgInitializeProcess] Глобальная карта CFG отображается для процесса с использованием MiMapSecurePureReserveView. g_SCPCFGBitmapView хранится во втором аргументе, и если всё проходит хорошо (то есть у процесса есть доступ к представлению), ядро просто возвращает то же значение. В противном случае создаётся новое представление. Затем представление сохраняется в отображениях процесса объекта процесса на смещении 0x3A0.

[MiMapAllImageScpPages] Код проходит через список VAD, принадлежащих процессу, и проверяет, указывают ли флаги VAD на то, что VAD доступен только для чтения и поддерживает hotpatching. Если это так, VAD должен содержать образ/модуль, и тогда проверяется, содержит ли образ исправления переопределения функций. Если это так, MiMapImageScpCfgPages вызывается для отображения секции в модуль, соответствующий образу. Эта функция занимается отдельным модулем и также используется путём связывания модуля, поэтому мы рассмотрим её позже.

Как это в коде:

C:
NTSTATUS MiCfgInitializeProcess(void* process)
{
    // проверяем флаги процесса и совместимость с cfg (например, некоторые архитектуры не поддерживаются)
    ...
   
    void* cfgBitmapView = g_SCPCFGBitmapView;
    uint32_t mapSize = 0;
    if (FAILED(MiMapSecurePureReserveView(process, g_CFGBitmap, &cfgBitmapView, &mapSize, NULL))
    {
           // если не удаётся отобразить существующее глобальное представление на карту, создаём новое
           cfgBitmapView = NULL;
           if (FAILED(MiMapSecurePureReserveView(process, g_CFGBitmap, &cfgBitmapView, &mapSize, NULL))
           {
                // возвращаем ошибку
                ...
           }
    }
   
    // сохраняем cfgBitmapView в отображения процесса
    MiReferenceCfgVad(...)
   
    // специальная обработка для ArmThumb2 и I386
    ...
}

void MiMapAllImageScpPages(void* process)
{
    // устанавливаем флаги, указывающие, что процесс использует scpcfg
    process->someFlags |= 4;
   
    // итерируем по image vads в процессе и отображаем страницы в те, которые их требуют
    for (auto vad = MiGetFirstVad(process); vad != NULL; vad = MiGetNextVad(vad))
    {
        // убеждаемся, что vad защищен с PAGE_EXECUTE_READ и что установлен индикатор hotpatchable (бит 6 в flags2)
        if ((vad->ProtectionFlags & 0x70 == PAGE_EXECUTE_READ) && (MiReadVadFlags2(vad) & 0x20) != 0)
        {
            if (MiDoesImageContainFunctionOverrideFixups(vad->Image))
            {
                MiMapImageScpCfgPages(process, vad);
            }
        }
    }
}


Заметки:

После завершения основной части кода в функции MiCfgInitializeProcess есть дополнительная часть, которая потенциально может быть выполнена. Если архитектура — I386 (0x14C) или ArmThumb2 (0x1C4), карта cfg отображается в другом представлении, но с другими параметрами (я не стал разбирать, что они означают). Новое представление затем сохраняется по смещению 0x3C0 в отображениях процесса.

Условия, необходимые для отображения секции SCPCFG в образ, связаны с hotpatching, т.е. должен быть установлен флаг индикатора hotpatch и функция MiDoesImageContainFunctionOverrideFixups должна возвращать true. Оба эти условия тесно связаны с реализацией hotpatching, поэтому мы не рассматриваем их здесь. Упомяну, что флаг индикатора устанавливается в MiMapViewOfImageSection при первоначальном отображении образа, но способ расчета его значения довольно сложен и выходит за рамки обсуждения.

Линковка модулей

Интересующая нас функция — это громоздкая MiMapViewOfImageSection, которая вызывается, когда образ (модуль) отображается в пользовательское пространство. Принимаемые здесь шаги довольно просты:

[MiMapViewOfImageSection] Сначала определяется, является ли модуль поддерживающим hotpatching. Если да, то размер VAD изображения увеличивается на 0x1000, и в конце области изображения будет доступна дополнительная страница. Если модуль поддерживает hotpatching и, кроме того, содержит исправления переопределения функций, вызывается MiMapImageScpCfgPages для отображения страницы SCPCFG в дополнительную страницу в области изображения.

[MiMapImageScpCfgPages] Сначала проверяем, использует ли процесс, в который загружается модуль, SCPCFG. Если да, вызывается PsGetScpCfgPageTypeForProcess для определения, какая страница была отображена в процесс. После этого страница извлекается из g_SCPCFGSectionBlocks и отображается в дополнительную страницу в конце расширенного VAD изображения. Это делается с помощью MiDecommitPages, который дополнительно освобождает страницу.

Как это в коде:

C:
void MiMapViewOfImageSection(void* imageVad, void* process, ...)
{
    // множество операций, о которых можно написать книгу
    ...
   
    // проверка, поддерживает ли модуль hotpatching, и увеличение размера изображения
    if (...)
    {
        flags2 |= 0x20;  // установка флага hotpatchable
        imageSize += g_HardcodedValueEqualTo4096;  // увеличение размера изображения на 4096 (0x1000)
    }
   
    // дополнительные операции, в этой фазе создается VAD для изображения с использованием переменной imageSize
    ...
    auto vad = ...;  // создание или получение VAD
   
    // те же условия, что и в MiMapAllScpPages
    if ((flags2 & 0x20) != 0 && MiDoesImageContainFunctionOverrideFixups(imageVad))
    {
        MiMapImageScpCfgPages(process, vad);
    }
}

void MiMapImageScpCfgPages(void* process, void* imageVad)
{
    // проверка, включен ли у процесса SCPCFG, это флаг, установленный во время MiMapAllImageScpPages
    if ((process->someFlags & 4) != 0)
    {
        // проверка, какую секцию SCPCFG использует процесс
        const uint32_t scpCfgSectionType = PsGetScpCfgPageTypeForProcess(...);
        if (scpCfgSectionType != 4)
        {
            // получение комбинированной страницы из g_SCPCFGSectionBlocks, создание нового прототипа PTE и т.д.
            ...
           
            // этот вызов отображает комбинированную страницу в адресный диапазон в конце изображения, но также освобождает адресный диапазон
            MiDecommitPages((baseAddrOfImage + MiGetImageExtensionBaseAddress(imageVad)), ...);
        }
    }
}

Заметки:

Условия, необходимые для того, чтобы отображение содержало страницу SCPCFG в конце своей области, здесь такие же, как и в MiMapAllImageScpPages - установлен индикатор hotpatch и присутствуют исправления переопределения функций. Это означает, что системные DLL не получают специальной обработки и проходят через те же кодовые пути, что и обычные DLL.

PsGetScpCfgPageTypeForProcess определяет, какой тип страницы SCPCFG отображается в процесс. Проверка здесь двухэтапная: Вспомните, что MiCfgInitializeProcess сохраняет своё глобальное представление карты cfg в отображениях процесса по смещению 0x3A0. Это поле проверяется в этой функции и сравнивается с глобальным g_SCPCFGBitmapView.
Если они не совпадают, возвращается 3, что соответствует секции SCPCFGFP. Это имеет смысл, если вспомнить, что g_SCPCFGBitmapView жёстко закодирован в ассемблере на страницах, представляющих SCPCFG и SCPCFGES - если это представление не то, которое имеет процесс, то функции работать не будут.

С другой стороны, если они совпадают, проверяется флаг подавления экспорта для процесса, чтобы определить, подходит ли секция SCPCFG или SCPCFGES. Индекс 0, или секция SCPCFGNP, возвращается только если для модуля не установлен определенный флаг, который скорее всего соответствует флагу CFG. Я не стал это выяснять. Теперь мы видим, что в большинстве случаев SCPCFG / SCPCFGES будут отображаться в модуль, в зависимости от того, включено ли подавление экспорта. Единственный способ для модуля использовать SCPCFGFP (который, как мы определили ранее, в настоящее время возвращается к оригинальному CFG) - это если процессу не удаётся отобразить глобальное представление карты cfg, что кажется маловероятным условием, и я не заметил, чтобы это происходило. Секция NOP будет использоваться только если модуль не поддерживает CFG.

MiMapImageScpCfgPages также определяет местоположение отображаемой страницы. Как и ожидалось, она всегда будет отображаться в конце области изображения. MiGetImageExtensionBaseAddress вызывается для определения адреса в пользовательском пространстве, куда должна быть отображена страница, и в настоящее время возвращает сумму базового адреса модуля и исходного размера отображения.

Наконец, поскольку диапазон адресов, к которому принадлежит дополнительная секция, освобождается, это в некоторой степени объясняет, почему VirtualProtect возвращает ошибку.
Это не объясняет, почему страница все еще может быть видна как зафиксированная другими API, и я был слишком ленив, чтобы это исследовать.

Блок системной DLL

Есть ещё одна вещь, которую ядро должно связать для процесса. Вспомните, что классический CFG требует, чтобы адрес карты cfg был записан в DllSystemInitBlock в ntdll, чтобы сделать его известным пользовательскому пространству. Для SCPCFG это не нужно, так как ядро записывает представление на карту непосредственно в ассемблерный код. Однако загрузчику в пользовательском пространстве теперь нужно знать, к каким функциям он должен быть связан, так как это определяется ядром.
Эта информация снова записывается в блок инициализации, теперь по смещениям 0xF0 - 0x120. Поля на этих смещениях будут содержать 6 указателей на соответствующие функции SCPCFG, и они записываются ядром в PspPrepareSystemDllInitBlock.

PspGetScpCfgFunctions вызывается для получения указателей на выбранные функции. Выбор и действия зависят от нескольких условий:

- На этом этапе ntdll уже отображен в процесс, поэтому код проверяет, содержит ли он уже секцию scpcfg - если нет, функция возвращает ноль и ничего не записывается в блок. Если да, определяемая секция вызывается посредством PsGetScpCfgPageTypeForProcess для процесса.
- Если определенный тип секции - SCPCFG или SCPCFGES, записываются значения, хранящиеся в PspntdllScpFunctions.
- Если определенный тип секции - SCPCFGFP, ничего не записывается.

Как это в коде:

C:
void PspPrepareSystemDllInitBlock(...)
{
    // инициализация других переменных в блоке
    ...
    
    // запись функций scpcfg в блок
    void** functions = PspGetScpCfgFunctions(process);
    if (functions)
    {
        *(dllInitBlock + 0xF0) = functions[0];
        *(dllInitBlock + 0xF8) = functions[1];
        *(dllInitBlock + 0x100) = functions[2];
        *(dllInitBlock + 0x108) = functions[3];
        *(dllInitBlock + 0x110) = functions[6];
        *(dllInitBlock + 0x118) = functions[4];
        *(dllInitBlock + 0x120) = functions[5];
    }
}

typedef struct _MEMORY_IMAGE_EXTENSION_INFORMATION
{
    PVOID PageTypeArgs;
    ULONG PageOffset;
    SIZE_T PageSize;
} MEMORY_IMAGE_EXTENSION_INFORMATION;

void** PspGetScpCfgFunctions(void* process)
{
    // проверка наличия отображенной секции scpcfg в области изображения ntdll, если нет, возвращаем NULL
    auto ntdllBaseAddr = PspSystemDlls[0][4];
    MEMORY_IMAGE_EXTENSION_INFORMATION info;
    if (ZwQueryVirtualMemory(-1, ntdllBaseAddr, 0x0E, &info, 0x18ui64, NULL) == 0xC00000BB)
        return NULL;
    if (info.PageSize == 0)
        return NULL;
    
    // получение типа страницы, на этот раз 0 не может быть возвращен, так как последний аргумент *true*
    const int pageType = PsGetScpCfgPageTypeForProcess(process, ..., true);
    switch (pageType)
    {
        // SCPCFG & SCPCFGES
        case 1:
        case 2:
            return &PspNtdllScpFunctions;
        // SCPCFGFP
        case 3:
            return NULL;
        default:
            return NULL;
    }
}

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

Линковка в пользовательском режиме

Как и в случае с классическим CFG, связывание в пользовательском режиме довольно простое и выполняется в ntdll!LdrpCfgProcessLoadConfig загрузчиком:

[LdrpCfgProcessLoadConfig]
Большая часть кода классического CFG все еще актуальна. Читается IMAGE_LOAD_CONFIG_DIRECTORY модуля, чтобы определить, где хранятся косвенные указатели функций вызова. Для каждого из присутствующих в отладочном каталоге косвенных указателей вызывается либо LdrpCfgCheckRoutineCallback, либо LdrpCfgDispatchRoutineCallback.
Эти функции должны решить, следует ли связать указатель с функцией scpcfg или с классической функцией ntdll. Это решение принимается, исходя из соответствующих значений в DllSystemInitBlock.

Например, LdrpCfgDispatchRoutineCallback выглядит так:

C:
void LdrpCfgDispatchRoutineCallback(void** fptr, int flags)
{
  if (LdrControlFlowGuardEnforcedWithExportSuppression() && (flags & IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_PRESENT) != 0)
  {
    if (*(dllInitBlock + 0x108))
        *fptr = *(dllInitBlock + 0x108);        // ядро записало сюда указатель функции ES dispatch
    else
        *fptr = LdrpDispatchUserCallTargetES;
  }
  else
  {
    if (*(dllInitBlock + 0x100))
        *fptr = *(dllInitBlock + 0x100);         // ядро записало сюда указатель функции (no ES) dispatch
    else
        *fptr = LdrpDispatchUserCallTarget;
  }
}

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

Дополнения

Еще одно действие, которое выполняет загрузчик в пользовательском режиме, - это добавление части новой отображенной секции в системную таблицу функций с помощью вызова RtlAddGrowableFunctionTable.
Цель этой функции - пометить определенные части кода как функции для правильного сбора трассировок и обработки исключений. Это делается в RtlpInsertOrRemoveScpCfgFunctionTable, которая вызывается после того, как модуль отображен и cfg инициализирован, или альтернативно, когда модуль выгружается. Чтобы получить информацию о том, какие функции внутри секции следует добавить в таблицу, функция вызывает ZwQueryVirtualMemory с новым классом информации о памяти. Внутренне это обозначено как "расширение представления изображения" и его значение равно 0x0E.

Код выглядит примерно так:

C:
typedef struct _MEMORY_IMAGE_EXTENSION_INFORMATION
{
    PVOID PageTypeArgs;
    ULONG PageOffset;
    SIZE_T PageSize;
} MEMORY_IMAGE_EXTENSION_INFORMATION;

NTSTATUS RtlpInsertOrRemoveScpCfgFunctionTable(void* moduleBase, int, bool insertOrRemove)
{
    MEMORY_IMAGE_EXTENSION_INFORMATION info = {};
    if (SUCCEEDED(NtQueryVirtualMemory(-1, moduleBase, 0x0E, &info, sizeof(info), ...))
    {
        if (info.PageOffset && info.PageSize)
        {
            void* pageBase = moduleBase + info.PageOffset;
            const uint32_t offsetToRtlTable = *(pageBase + 0x20);
            const uint64_t pageSize = info.PageSize;
            if (insertOrRemove)
            {
                RtlAddGrowableFunctionTable(..., pageBase + offsetToRtlTable, 1, 1, pageBase, pageSize);
            }
            else
            {
                RtlDeleteFunctionTable(pageBase + offsetToRtlTable);
            }
        }
    }
}

Ядро CFG также претерпело некоторые изменения, очень похожие на те, которые мы видели в пользовательском режиме. В ntoskrnl.exe теперь есть секция "KSCP", которая оформлена следующим образом:

Код:
[+0x00] KSCP header
    [+0x00][0x04] = length of the section
    [+0x04][0x58] = offsets to individual functions below, sorted, 4 bytes each
    [+0x5C][0x24] = Unknown
[+0x80] __guard_retpoline_icall_handler
[+0xA0] sub_140B570A0
[+0xC0] __guard_retpoline_switchtable_jump_rax
[+0xE0] __guard_retpoline_switchtable_jump_rcx
[+0x100] __guard_retpoline_switchtable_jump_rdx
[+0x120] __guard_retpoline_switchtable_jump_rbx
[+0x140] __guard_retpoline_switchtable_jump_rsp
[+0x160] __guard_retpoline_switchtable_jump_rbp
[+0x180] __guard_retpoline_switchtable_jump_rsi
[+0x1A0] __guard_retpoline_switchtable_jump_rdi
[+0x1C0] __guard_retpoline_switchtable_jump_r8
[+0x1E0] __guard_retpoline_switchtable_jump_r9
[+0x200] __guard_retpoline_switchtable_jump_r10
[+0x220] __guard_retpoline_switchtable_jump_r11
[+0x240] __guard_retpoline_switchtable_jump_r12
[+0x260] __guard_retpoline_switchtable_jump_r13
[+0x280] __guard_retpoline_switchtable_jump_r14
[+0x2A0] __guard_retpoline_switchtable_jump_r15
[+0x2C0] __guard_retpoline_indirect_cfg_rax
[+0x3C0] __guard_retpoline_exit_indirect_rax
[+0x440] __guard_retpoline_import_r10
[+0x4E0] __guard_retpoline_import_r10_do_retpoline
[+0x520] __guard_retpoline_import_r10_log_event
[+0x580] __guard_retpoline_jump_hpat
[+0x5A0] __guard_retpoline_exit
[+0x780] KscpCfgDispatchUserCallTargetEsSmep
[+0x7E0] KscpCfgDispatchUserCallTargetEsNoSmep
[+0x840] KscpCfgHandleInvalidCallTarget

Структура похожа на новые секции в ntdll, но в секции не только новые функции CFG, включены также функции retpoline. Функции retpoline не новы и были присутствовать в предыдущих версиях ядра, хотя в другой секции - RETPOL. Эта секция теперь исчезла. В конце KSCP есть три функции, которые напоминают те, что мы видели в пользовательском пространстве. Однако их реализация отличается. Например:

ntoskrnl!KscpCfgDispatchUserCallTargetEsSmep:
jmp     rax
-------------------------------------------------------------
db 7 dup(0CCh)
-------------------------------------------------------------
mov     r10, rax
shr     r10, 9
mov     r11, [r11+r10*8]
mov     r10, rax
shr     r10, 3
test    al, 0Fh
jnz     short loc_140B577A9
bt      r11, r10
jnb     short loc_140B577C1
jmp     rax
...

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

Инициализация

KSCPSCFG инициализируется вместе с остальной частью KSCP (последняя обозначает все функции в секциях, а первая - только три функции CFG в конце).

Инициализация происходит следующим образом:

[MiPrepareScpFixupsForNtAndHal] Это делается во время подготовительной фазы системной инициализации. Мы начинаем с отображения секции KSCP и сохранения указателя на нее в глобальной переменной, а также ее размера в системных страницах. Назовем их g_ScpBase и g_ScpSectionSizeInPages. Затем вызываем MiApplyDynamicFixupsToKernelAndHal.

[MiApplyDynamicFixupsToKernelAndHal] Эта функция выполняет некоторые исправления для retpolines, а затем вызывает RtlInitializeKscpCfgFunctions для исправления KscpCfgDispatchUserCallTargetEsSmep и KscpCfgDispatchUserCallTargetEsNoSmep. Код исправления довольно прост, в начале обеих функций делается патч, чтобы функция переходила к __guard_retpoline_icall_handler. KSCP дополнительно инициализируется через MiInitializeKernelScp, но здесь ничего особенного не происходит с функциями CFG.

Основная цель этой функции, похоже, заключается в создании таблицы функций, к которой позже можно будет получить доступ через обычные интерфейсы, например, через RtlpxLookupFunctionTable.

Примерный код:

C:
void MiPrepareScpFixupsForNtAndHal(...)
{
    auto kscpSectionDesc = RtlLookupImageSectionByName(..., "KSCP");
    g_ScpBase = kscpSectionDesc->SectionBase;
    g_ScpSectionSizeInPages = PAGE_COUNT(kscpSectionDesc->SectionSize);
    MiApplyDynamicFixupsToKernelAndHal(...);
}

void MiApplyDynamicFixupsToKernelAndHal(...)
{
    // исправления retpoline и прочее
    ...
    
    // исправление kscpcfg
    RtlInitializeKscpCfgFunctions(g_ScpBase, g_ScpSectionSizeInPages * 4096);
}

NTSTATUS RtlInitializeKscpCfgFunctions(void* scpBase, uint32_t scpSize)
{
    // проверки целостности содержимого и размера секции
    ...
    
    // патчинг функций cfg для перехода в __guard_retpoline_icall_handler
    *(scpBase->KscpCfgDispatchUserCallTargetEsSmep) = 0xE9;  // jmp
    *(scpBase->KscpCfgDispatchUserCallTargetEsSmep + 1) = scpBase->__guard_retpoline_icall_handler - scpBase->KscpCfgDispatchUserCallTargetEsSmep;
    *(scpBase->KscpCfgDispatchUserCallTargetEsNoSmep) = 0xE9;  // jmp
    *(scpBase->KscpCfgDispatchUserCallTargetEsNoSmep + 1) = scpBase->__guard_retpoline_icall_handler - scpBase->KscpCfgDispatchUserCallTargetEsNoSmep;
}

Линковка

Остался только один шаг — связать обработчики косвенных вызовов с функциями KSCPCFG каждый раз, когда загружается драйвер. Это делается в знакомом месте, во время выполнения функции MiProcessKernelCfgImageLoadConfig. Эта функция анализирует каталог отладочной конфигурации загрузки в загружаемом exe/драйвере, находит косвенные указатели вызовов и модифицирует их так, чтобы они указывали на функции CFG.

До версии 24H2 использовались функции guard_check_icall и guard_dispatch_icall (в 24H2 они называются guard_check_icall_no_overrides и guard_dispatch_icall_no_overrides).
В 24H2 это изменено, но только для функции диспетчеризации. Вместо использования guard_dispatch_icall_no_overrides, указатели будут связаны с KscpCfgDispatchUserCallTargetEsSmep или KscpCfgDispatchUserCallTargetEsNoSmep, в зависимости от статуса SMEP.

Секция KSCP также дополнительно отображается для каждого модуля, загружаемого в ядро, что снова напоминает о происходящем в пользовательском пространстве. Это делается через MiMapKernelScp, которая вызывается во время загрузки системного образа.

Примерный код:

C:
void MiProcessKernelCfgImageLoadConfig(...)
{
    // обработка функции валидации, очень просто -> устанавливается на guard_check_icall_no_overrides, без kscpcfg
    ...
    
    // обработка функции диспетчеризации, имеет kscpcfg
    void** fptr = *(imageLoadConfig + 0x78);
    *fptr = guard_dispatch_icall_no_overrides;
    if (Mm64BitPhysicalAddress & 1) // проверка SMEP
        *fptr = KscpCfgDispatchUserCallTargetEsSmep;
    else
        *fptr = KscpCfgDispatchUserCallTargetEsNoSmep;
}

Заключительные мысли

Глядя на большую картину, изменения, кажется, предназначены только для совместимости с hotpatching. Мы не видели никаких улучшений безопасности или оптимизаций, которые должен был принести новый код. К сожалению, у меня не хватило мотивации копать глубже и выяснять, где именно новое поведение проявляется наилучшим образом, но кажется довольно ясным, что весь набор изменений был сделан для поддержки hotpatching. Это становится очевидным при рассмотрении изменений в ядре, поскольку, кажется, нет дополнительной пользы для безопасности от нового поведения.
Упоминание "переопределений функций" в нескольких местах также кажется указанием на возможность патчинга функций CFG для модуля или чего-то в этом роде.

Мы выяснили, что вызывает ошибки в x64dbg:

Дополнительная страница в конце областей отображений— это страница SCPCFG. Страница освобождается ядром, поэтому VirtualProtect возвращает ошибку. Я немного поэкспериментировал, но не смог найти способ изменить это из пользовательского пространства, так как VirtualAlloc с MEM_COMMIT, похоже, ничего не делает, и последующие вызовы VirtualProtect не удаются. Но есть несколько вопросов, на которые мне хотелось бы получить ответы, и надеюсь, что они будут ответены в будущем:

Что означает "SCP"? Моё предположение — что-то вроде "Standalone Code Page", но это может быть многое другое, так что, вероятно, не стоит строить догадки.
Почему модулям, поддерживающим hotpatching, нужны собственные секции SCP, когда изначально они будут связаны с секцией SCP ntdll?
Почему освобожденная страница в пользовательском режиме все еще видна как занятая? Этот вопрос может быть очень простым, но я не хотел углубляться в очередную кроличью нору.

Я надеюсь, что в ближайшие месяцы будут раскрыты дополнительные детали, так как 24H2 будет выпущена для широкой аудитории, и мы получим больше информации о коде ядра. В конечном итоге мы не узнаем полную историю, пока MS не решит поделиться некоторой информацией или кто-то не разберет механизм hotpatching. Я не был достаточно смел для этого :)
 
Верх Низ