Каждая загрузка Windows расставляет ядро, драйверы и системные структуры по новым адресам. Эта перестановка, известная как Address Space Layout Randomization, задумана как доспех: атакующий код, не знающий адресов, вынужден действовать вслепую. За годы доспех не раз трещал, и история его трещин поучительна сама по себе: она учит, что рандомизация - не стена, а подвижная дюна, и её прочность измеряется количеством бит энтропии и дисциплиной защиты указателей.

Как устроена рандомизация карт памяти в системе

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

dumpbin /headers mydriver.sys | findstr /C:"Dynamic base"
link /dump /headers mydriver.sys | findstr /C:"DLL characteristics"

Ответ выглядит коротко и однозначно:

Dynamic base    - DLL can move.

Если этой строки в выводе нет, образ собран без поддержки перемещения и ляжет по фиксированному адресу. Тот же флаг виден из отладчика по загруженному модулю:

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

Ключевой жеребьёбки - источник случайности. Ранние реализации брали зерна, полученные до полного поднятия подсистемы питания энтропии, что ставило верхнюю границу качества случайности; с годами цепочка усилилась: накопители энтропии системы кормятся событиями оборудования, а загрузчик имеет выделенный доступ к генераторам случайных чисел процессора (инструкция RDRAND при наличии). Энтропия в битах - бюджет защиты, и считается он из ширины диапазона и гранулярности выравнивания:

Число вариантов базы   N = диапазон_адресов / гранулярность_выравнивания
Энтропия в битах       E = log2(N)

пример: диапазон 512 ГБ, выравнивание образа кратно 1 МБ
N = 512 * 1024 = 524288 вариантов
E = log2(524288) = 19 бит

пример: тот же диапазон, выравнивание кратно 64 КБ
N = 512 * 1024 * 16 = 8388608 вариантов
E = log2(8388608) = 23 бита

Разница в четыре бита означает шестнадцатикратный рост стоимости перебора, и именно выравнивание чаще всего съедает энтропию там, где её ждут. Если диапазон возможных баз составляет порядка 2 в степени 19 вариантов, перебор для целеустремлённого наблюдателя уже не фантастика, особенно когда попытка не ограничена одной загрузкой.

Помимо баз модулей, рандомизации подлежат и внутренние структуры: таблица системных вызовов, адреса глобальных объектов ядра, расположение стеков потоков в пространстве ядра. Общий принцип неизменен: чем меньше фиксированных ориентиров остаётся на карте памяти, тем дороже исследование.

Откуда берутся утечки адресов и классы слабостей

Практика показала: сама рандомизация ломается редко, а вот её разоблачение - сплошь и рядом. Класс слабостей номер один - утечки указателей из ядра в пользовательский режим. Сервис системного вызова возвращает структуру, где мимоходом лежит указатель ядра; отладочный вывод показывает адрес объекта; счётчик производительности отражает адрес внутренней структуры. За годы аудиторы выгребли из системных интерфейсов десятки таких мелких стукачей, и каждую находку система залатывала, заменяя указатель идентификатором из таблицы, недоступной без привилегий.

Второй класс - предсказуемость задатчика. Если зерно генератора можно вычислить или подсмотреть (дефект реализации, побочный доступ к состоянию), вся рандомизация раскрывается как расписание киосков. Отсюда строгие требования к источнику энтропии загрузчика и регулярные правки в этой области.

Третий класс - побочные каналы, та область, где железо подводит софт: по времени доступа к кэшу и по ветвлению предсказателя удаётся с высокой вероятностью угадать, как расположено ядро в памяти. Нашумевшие исследования класса обхода KASLR через временные различия кэша и через буфер адресов ветвлений показали: даже без единой прямой утечки указателя размещение модулей восстанавливается с точностью, достаточной для практических целей, за секунды наблюдений. На этом фоне индустрия массово внедрила разделение таблиц страниц (называется оно kernel page-table isolation), когда ядро во время работы пользовательского кода вовсе убирается из карты: искать адрес, которого нет на карте, куда сложнее.

Четвёртый класс - слабые точки самой схемы: фиксированные области, выжившие по историческим причинам, области с пониженной энтропией из-за выравнивания, крупные неперемещаемые модули. Аудит этой категории - перманентная задача разработчиков ОС.

Как измеряют защиту и что показывают проверки

Исследовательская практика оценки KASLR неизменно начинается с переписи неподвижного. В каждой новой версии системы принято проверять: все ли системные модули собраны с флагом динамического основания; сколь широк диапазон перебазировки каждого класса областей; какие системные вызовы и интерфейсы всё ещё возвращают значения, коррелирующие с адресами. Инструменты из набора Sysinternals и собственные скрипты сопоставления наборов загруженных модулей между перезагрузками дают быстрый ответ на вопрос, насколько случайно выглядит карта. Сбор исходных данных делается журналом отладчика, а сравнение - штатной командой:

kd> .logopen C:\Traces\lm-boot-01.txt
kd> lm
kd> .logclose
fc C:\Traces\lm-boot-01.txt C:\Traces\lm-boot-02.txt

После десятка перезагрузок в руках оказывается набор баз каждого модуля, и по числу различных значений для одного и того же модуля вычисляется фактический диапазон, а по нему - реальная энтропия в битах. Пригодная для отчёта методика проста:

  1. Снять снимок карты адресов модулей ядра до перезагрузки и после неё, зафиксировав базу каждого модуля;
  2. Повторить измерение десятки раз и вычислить эффективный диапазон каждого класса областей;
  3. Перечислить модули без флага перемещения и зафиксировать их фиксированные точки;
  4. Пройтись по списку системных вызовов, возвращающих структуры, и проверить, не содержат ли поля указателей ядра;
  5. Сопоставить результат с версией и настройками механизмов изоляции таблиц страниц.

Состояние этих механизмов видно в реестре и в сводке по системе, обе проверки занимают секунды:

reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverride
reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverrideMask
systeminfo | findstr /C:"Hyper-V"

Картина, возникающая после такой проверки, честнее любого буклета: ровно видно, сколько бит энтропии реально остаётся у защиты.

Почему рандомизация лишь один из этажей защиты

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

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

Как эволюционировала рандомизация от первых версий системы до современных механизмов защиты

Полезно проследить, как менялась сама концепция. Первые внедрения рандомизации в систему относились к эпохе Vista и затрагивали в основном пользовательский режим: базы библиотек, кучи и стеки. Ядерная разновидность пришла позже и изначально была довольно скромной: энтропия базы ядра исчислялась единицами десятков тысяч вариантов, а драйверы подгружались последовательно со сдвигом. С каждым поколением добавлялись новые классы: рандомизация страничных пулов, областей сессий, стеков ядра, отдельных структур HAL. Появилась и специальная забота о совместимости: процессорная рандомизация вынуждена координироваться с механизмами отладки, гипервизором и механизмами гибернации, где состояние памяти должно восстанавливаться предсказуемо - файл гибернации хранит адреса, и выбор новой раскладки при пробуждении не разрешён.

Отдельной строкой идёт динамическое перемещение, известное как периодическая перерандомизация. Исследовательские прототипы предлагали переставлять ядро на ходу, чтобы срок годности любой утечки исчислялся минутами; в продуктивной системе полная версия такой механики до сих пор не стала массовой, но отдельные её элементы - например, периодические изменения размещения некоторых структур - постепенно проникают в реализацию.

Взаимодействие с гипервизором и изоляцией на базе виртуализации

Появление виртуализационной безопасности переставило акценты в самой схеме защиты. Чувствительные секреты и механизмы доверия были вынесены за пределы обычного ядра в защищённый уровень, куда даже скомпрометированное кольцо-ноль не имеет доступа; адреса там не просто случайны - они недоступны по определению. KASLR в такой схеме продолжает работать, но её роль смещается: она защищает не корону, а придворных. Не случайно современные публикации о слабостях рандомизации всё чаще касаются периферии: сторонних драйверов, которые загружаются по фиксированным адресам по причине устаревшей сборки и становятся предсказуемыми островками на случайной карте. Проверка сторонних модулей на флаг перемещения - простая, но регулярно приносящая находки процедура, и в зрелых аудиторских отчётах ей отведён обязательный раздел.

Частые заблуждения вокруг рандомизации

Отдельного разговора заслуживают три устойчивых мифа, которые регулярно всплывают в обсуждениях. Первый гласит, что KASLR "защищает от уязвимостей": нет, она защищает от простой эксплуатации, уязвимость от этого никуда не девается, и единственное настоящее лекарство - исправленный код. Второй миф утверждает, что рандомизация мешает легальной отладке: на деле отладчик ядра с корректными символами работает независимо от картографии, а вот средства, написанные на коленке с жёсткими адресами, наказаны справедливо. Третий миф касается производительности: цена механизма измеряется аккуратной работой загрузчика и ничтожна по сравнению с ценой изоляции таблиц страниц, которую, впрочем, с каждым поколением железа тоже умеют отдавать всё дешевле. Карта памяти, которая меняется при каждом старте, - уже не экзотика, а фон, и представить современную систему без этого фона так же трудно, как автомобиль без ремней.

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

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