skitter-creek-bath-salts README

The project's README from github.com/xoreaxeaxeax/skitter-creek-bath-salts. As one markdown file: README.md.

skitter-creek-bath-salts

Unlocking everything on the CPU with DRAM scrambling — PSP, C6, microcode, SMM, and anything else the specs left out.

&x == &x.

Usually.

Unspaghettifying DRAM

Poke the DRAM controller and an address can be made to land wherever you want in memory. skitter-creek-bath-salts modifies the bottom layers of the memory hierarchy to rewire the physical DRAM address translations. This scrambles platform memory, exposing protected regions of DRAM — carveouts invisible even to the kernel. When the address translations break, so do the security primitives built on them, and we unlock everything.


TL;DR


Target

Developed and tested on AMD Family 16h CPUs, the last generation whose datasheets document the DRAM controller's translation registers — and show that they can't be locked. 17h and beyond simply leave this information out. The odyssey of *p is similar across generations and architectures, and the underlying transforms extend even to ARM, RISC-V, and beyond; skitter-creek-bath-salts shows us only how to begin.


The odyssey of *p

It's a long way down.

Memory is built on layers of abstraction so deep they become almost absurd. When your code dereferences *p, it appears to access the DRAM at p. It does not — p is a virtual address, and before a single bit of DRAM is touched, it must survive the gauntlet below:

         ── 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)

This project works at the deepest levels of the *p pipeline, the MCT/DCT layer — where a physical address from the data fabric/interconnect enters the memory controller and is rewritten one final time into the raw DRAM coordinates that are issued to the DIMM.


Spaghettifying DRAM

Physical addresses are really more of a suggestion.

xor dword [0xf80c2094], 0x00400000

That's the exploit. All of it.

One bit-flip in the DRAM controller rewires the bottom of the *p pipeline, and the data that was at &x is now somewhere else mid-flight. Suddenly &x != &x. Every mechanism the CPU, firmware, uncore, and chipset use to wall off protected memory sits above the memory controller, and none of it sees what happens below. The fences guard physical addresses, not DRAM coordinates; rearrange the coordinates and the barriers above never notice.

But rewiring DRAM is easy. The bit above is the bank-swizzle-mode in the DCT, and it's just one of dozens that control the address remaps at the final layer — all you have to do is poke them to make everything built on top topple. The harder part then is keeping the platform up as the entirety of system memory is scrambled underneath it.

The trick: be fast, and don't touch DRAM. Disable the APs, prime the TLBs, warm the cache, disable interrupts, flush the target, serialize memory accesses, and hope the CPU prefetched the upcoming instructions. Then rewire the MCT/DCT to spaghettify DRAM, grab some data from the protected region, revert the mappings, serialize again, enable interrupts, resume the APs, and the platform is back to normal.

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

With some careful setup of paging, cache states, threading, and the TLBs, the address scrambling can be made to work from C, to illustrate the *p pipeline collapsing, and the platform's corrupted view when suddenly &x != &x:

&x manipulation

So we can rewire the map and restore it without a trace. All that's left is knowing what we rewired it into.


Unlocking everything

Every protected memory region on the platform, reachable with a calculator.

With the above approach, we can reprogram the MCT/DCT transform on a running system — rearranging the lowest stage of the *p pipeline to scramble memory out from underneath every protection built above it.

But there's a challenge: while we can reprogram the translation with a simple xor dword [0xf80c2094], 0x00400000, we have no idea what new transforms the MCT/DCT will use (the datasheets are underspecified here — the xor maps are off, the MMIO subtractive stage is unordered, and details vary across models). Without this, memory scrambles, but we have no way to reconstruct it.

Fortunately, the DRAM controller's address transform is a GF(2) linear map, which means we can reconstruct the scrambled memory with basic linear algebra.

First, consider the normal case: the forward transform of the default MCT/DCT configuration gets applied to some physical address, which lands on a secret in 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

This is the coherent view of memory: the lowest stage of the *p pipeline operates exactly as it should.

Now rewire the MCT/DCT stage of *p with xor dword [0xf80c2094], 0x00400000, and the platform enters a scrambled/spaghettified view of memory where a different transform allows an alias to reach the same DRAM secret:

    ┌                                 ┐   ┌   ┐     ┌   ┐
    │ 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

This alias lets us reach the same secret without hitting the existing platform locks and defenses built for the coherent view. To find the alias, compose the inverse of the attacking/spaghettified hash with the forward of the firmware/coherent hash, to get the translation that will reach any secret from the malicious MCT/DCT configuration:

    ┌                                 ┐   ┌                                 ┐   ┌   ┐     ┌   ┐
    │ 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

The only challenge is that the matrices are unknown, which means we have no idea how memory is actually scrambled, and no transform to use to reach the secret in the first place:

    ┌                                 ┐   ┌                                 ┐   ┌   ┐     ┌   ┐
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 1 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ · │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ · │ 1 │  =  │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 1 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ 0 │     │ ? │
    │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │   │ ? ? ? ? ? 

skitter-creek-bath-salts (xoreaxeaxeax/skitter-creek-bath-salts)

Searching Database…

这是什么?一个安全研究项目,展示如何通过打乱 DRAM 控制器的物理地址到 DRAM 地址转换,来解锁 AMD CPU 上受保护的区域——Platform Security Processor(平台安全处理器)、System Management Mode(系统管理模式)、C6 DRAM 和 CPU 微码。
这是一个能用的工具,还是只是一篇技术文章?两者都是:它既是一篇详细的技术文章,讲解内存控制器地址转换攻击;还附带 C 代码(用 make 构建),演示这个漏洞利用在真实硬件上运行。
这个概念验证实际怎么跑?用 make 构建 C 源码,然后在一台物理的 AMD Family 16h 机器上运行;代码会在运行时直接重编程内存控制器的 MCT/DCT 转换寄存器(例如 `xor dword [0xf80c2094], 0x00400000`)来打乱 DRAM 寻址。
这需要特殊的 AMD 硬件吗?需要——它专门在 AMD Family 16h CPU 上开发和测试,这是最后一代在公开数据手册里记载 DRAM 控制器转换寄存器的产品。
这个攻击对更新的 AMD CPU 也管用吗?不能直接照搬——从 17h 开始,AMD 不再在数据手册里记载这些转换寄存器,所以这里展示的具体技术只适用于 Family 16h,尽管作者相信底层思想可以推广到其他架构。
我得是硬件安全研究者才看得懂这个吗?是的——它假定你深入熟悉物理内存寻址、DRAM 控制器、缓存/TLB 行为和 CPU 微架构;它不是即插即用的安全工具。
翻转 DRAM 控制器的一个位是怎么破坏 CPU 安全的?每个硬件安全机制(PSP、SMM、C6、微码锁)都在物理地址上实施访问控制,但内存控制器会在所有这些机制之下,把这些物理地址再重新映射一次到原始 DRAM 坐标;改动这最后一层重映射,会把受保护的数据挪到安全检查永远看不到的新位置,于是 `&x != &x`,而上面的各道防线毫无察觉。
打乱内存之后,攻击怎么还原数据实际落在了哪里?由于 DRAM 控制器的地址变换在 GF(2) 上是线性的,该项目用线性代数把打乱变换的逆和正常变换的正向组合起来,从而精确算出哪个别名地址能摸到 DRAM 里某个给定的机密。
打乱整个 DRAM 会让机器崩溃吗?不会——代码会先禁用其他 CPU 核心和中断,预热缓存和 TLB,执行重映射和内存访问,然后恢复映射、继续正常运行,所以平台之后会回到可用状态。
这只限于 AMD 吗,还是也适用于其他 CPU 架构?演示的漏洞利用针对 AMD,但作者表示同样的底层地址打乱思想也适用于 ARM、RISC-V 和其他架构,并称本项目只是一个起点。
Size
3.8 MB
Stars
2.1K
Updated
CommitAug 13
Age
2mo
Commits · 90 Days