AMD Family 16h 上,同一個虛擬位址理論上永遠指向同一顆 DRAM 電容——研究者 Christopher Domas 在記憶體控制器裡翻一個暫存器位元,PSP 的私有記憶體、SMRAM handler、C6 省電狀態存下的 CPU 暫存器,就從同一顆電容裡被讀了出來。
把位址攪亂,保護區就露了出來
同一支位址,在同一台機器上按理該永遠對應同一格 DRAM。GitHub 上一個叫 skitter-creek-bath-salts 的專案,標題直接寫著「Unlocking everything on the CPU with DRAM scrambling」,示範怎麼讓這件事不成立。作者開篇只留了一行字:「&x == &x. Usually.」多數時候成立,這個專案的重點就在那個「usually」。它不碰 page table 的權限位元,不碰 SMRAM 的鎖定位元,只改記憶體控制器(MCT/DCT)裡決定「這個實體位址要落在 DRAM 哪個 row、哪個 column」的幾個暫存器。README 的說法是:「Poke the DRAM controller and an address can be made to land wherever you want in memory... This scrambles platform memory, exposing protected regions of DRAM」——連 kernel 都看不到的 carveout,就這樣被暴露出來。
為什麼「&x == &x」會失守
從 CPU 發出一個 *p 到 DRAM 真的被讀寫,中間要走過的層數比想像的深。README 畫了一張很長的管線圖:虛擬位址先過 canonical-form 檢查,加上 segment base,查 TLB 或觸發 page walk,拿到一個實體位址;若是 guest 虛擬機還要再過一輪 EPT/NPT 重走。實體位址接著查 MTRR/PAT 決定快取策略,再往下探 L1、L2、LLC,跨核心跨 socket 做一致性廣播,最後才交給 data fabric 送進記憶體控制器,在那裡被 channel interleave、rank interleave、bank interleave、bank swizzle、chip-select normalize 這幾道 XOR 運算重寫一次,變成 DIMM 認得的 DRAM 座標——bank group、bank、row、column。README 把這條路徑稱為 *p 管線,並且明講:「This project works at the deepest levels of the `*p` pipeline, the MCT/DCT layer」——一個實體位址從 data fabric/interconnect 進入記憶體控制器,在這裡被重寫成最終要送進 DIMM 的原始 DRAM 座標。下面這張圖照著這條管線走一遍,每一層都能點開看它管的是什麼、它不知道什麼。
click any layer to read its responsibility · 5 layers of the *p pipeline
CPU 核心/MMU · 負責什麼
虛擬位址先過 canonical-form 檢查、加上 segment base,查 TLB 或觸發 page walk,U/S、R/W、NX、SMEP/SMAP、Protection Keys 這些權限位元逐級檢查,才拿到一個實體位址。
不知道什麼:這個實體位址最後會被記憶體控制器重排到 DRAM 的哪個 row、哪個 column——這一層看不到,也管不到。
MTRR/PAT 型別解析 · 負責什麼
MTRR 的固定與可變範圍,加上 PTE 裡的 PAT/PCD/PWT 位元,決定這段實體位址該用 WB、WT、WC 還是 UC 的快取策略。
不知道什麼:它只管快取行為,不管這個位址在 DRAM 裡實際住在哪一顆 cell。
Uncore 快取與一致性 · 負責什麼
L1、L2、LLC 依序探測,跨核心、跨 socket 用 MESI/MOESI 協定廣播,home-node 目錄決定回應是資料、介入還是中止。
不知道什麼:它管的是哪個快取 line 持有這份資料的副本,不是這份資料實際落在 DRAM 哪個座標。
系統資料結構/interconnect · 負責什麼
data fabric 判斷這個實體位址該走向 MMIO 裝置 BAR,還是繼續往記憶體控制器走——這是最後一段還看得到「原始」實體位址形式的地方。
不知道什麼:接下來實體位址要被怎麼重寫,fabric 本身不參與,只負責把它送過去。
MCT/DCT 記憶體控制器 · 沒有鎖定位元
channel interleave、rank interleave、bank interleave、bank swizzle、chip-select normalize,這幾道 XOR 運算把實體位址重寫成 DRAM 真正認得的座標——bank group、bank、row、column。
關鍵句:在 AMD Family 16h 的資料手冊上,這幾個暫存器沒有鎖定位元;上面四層的所有保護,都不會檢查這一步做了什麼。
先把答案攤開:管線裡有五段,前四段各自有明確的守門機制——page table 的權限位元、MTRR/PAT 的快取策略、快取一致性協定、data fabric 的路由判斷——但只有最後一段,MCT/DCT 把實體位址重寫成 DRAM 座標的那一步,沒有對應的鎖定位元。下面按著兩個「總該擋得住吧」的假設走一遍,看它們各自卡在哪裡。
假設一:MMU 和頁表擋住了吧
第一個直覺是頁表。x86 的頁表項目上掛著 U/S、R/W、NX,還有 CR4 控制的 SMEP、SMAP,以及 IA32_PKRU 管的 Protection Keys——這些位元組合起來,決定一個 ring-3 的行程能不能讀寫、能不能執行某個實體位址。TLB 把查過的結果快取起來,靠 PCID 或 VPID 分開不同的位址空間,invlpg 廣播負責讓失效的項目跟著失效。這一整套機制確實擋住了「用 CPU 指令直接讀寫某個不該碰的實體位址」——但它擋的是 CPU 的 load/store 指令,不是記憶體控制器自己的組態暫存器。skitter-creek-bath-salts 走的不是 mov,而是直接寫 MMIO 位址 0xf80c2094,那是 DCT(DRAM Controller)本身的組態空間,不受頁表管轄。頁表決定「你能不能讀這個實體位址」,但完全不參與「這個實體位址最後對應到 DRAM 的哪裡」——這是後面才發生的事,頁表看不到。
如果目標不是 CPU 直接執行的 load/store,而是裝置發起的 DMA,管線圖裡還多一段 IOMMU page walk:VT-d 或 AMD-Vi 把裝置 ID 對應到一個 domain,再查一次自己的頁表。guest 虛擬機下的存取還要多繞一圈——每一次 guest 端的頁表查詢,都要疊上 EPT 或 NPT 再查一次,README 把這個代價寫成「~5× walks per single guest walk」。這些機制加起來構成了一套相當紮實的存取控制,但它們全部發生在「拿到實體位址 k」之前或同一時刻,跟實體位址之後怎麼被翻譯成 DRAM 座標,完全是兩件事。假設一在這裡就能排除:頁表、TLB、IOMMU 管的是誰能對某個實體位址發出讀寫,不是這個實體位址最後真正落在哪裡。
假設二:SMRAM lock、PSP carveout 這些 fence 總該擋住
那 firmware 層的保護呢?SMM(System Management Mode)理論上靠 SMRAM lock 擋住;PSP(Platform Security Processor)有自己的私有 DRAM carveout,「right past the visible top-of-memory」,連 ring-0 都摸不到;C6 省電狀態把每顆核心的完整 x86 架構狀態存進 DRAM,同樣被認為「inaccessible from the OS」。這些機制的共通點,是它們全部建立在「實體位址」這個座標系上:SMRAM lock 鎖的是一段實體位址範圍,PSP carveout 是一段被防火牆隔開的實體位址範圍,C6 stash 是分配好的另一段實體位址。skitter-creek-bath-salts 的說法直接點出問題所在:「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.」換句話說,這些 fence 比的是門牌號碼,不是門牌背後真正的房子。只要能讓同一顆 DRAM 電容配上兩個不同的門牌號碼,舊門牌上的鎖就跟這顆電容脫鉤了。
這個原則不只適用於 SMRAM 跟 PSP。README 把清單列得更長:「SEV, SGX, TDX, TrustZone, CCA realms, pKVM, CoVE, SEP, the PSP, ME, T-SEG, SMRAM, the C6 stash」全部「sits above」記憶體控制器這一層。機密運算圈子裡幾乎所有耳熟能詳的名字,理論基礎都是同一套:在某個實體位址範圍上蓋一層存取控制,然後相信這個範圍在硬體層面就是安全邊界。假設二在這裡也一起被排除:這些機制彼此獨立、廠商也不同,但共同的前提一旦在記憶體控制器這一層被打破,全部一起失守。README 自己的收束句是:「In the end, everything so carefully walled off — PSP private memory, SMRAM, the C6 idle-state, inaccessible from the OS, ring-0, sometimes the CPU itself — is still sitting in the same DRAM capacitors. But the locks were built around the coherent view of memory, and do nothing against a spaghettified alias reaching the same cell.」
破口:MCT/DCT 那層沒人鎖
真正的操作只有一行:xor dword [0xf80c2094], 0x00400000。README 原話是:「That's the exploit. All of it.」這個位元是 DCT 裡的 bank-swizzle-mode,作者說它只是「dozens」個控制最終位址重排的暫存器之一——翻哪一個都能讓上層建的保護全部失效,難的地方在於翻完之後平台還得撐著別當機。方法是先把可能出問題的環節都關掉:停用其他核心(AP)、預先把 TLB 和快取填好、關中斷、flush 目標位址逼 DRAM 真的被讀、序列化指令流,然後在極短的窗口裡翻 DCT 暫存器、抓資料、翻回去、重新開中斷、喚醒 AP。README 的講法是「be fast, and *don't touch DRAM*」——翻轉前後那一刻,CPU 靠的是已經預取好的指令流撐過去:
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
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
popf ; interrupts back on
翻完之後,同一個實體位址在 DRAM 裡對應的 cell 變了——原本讀得到的東西讀不到,反而在另一個位址上冒出來。要用這招讀到「特定的」protected 位址,得先知道新的對應關係是什麼,但作者的說法是資料手冊在這裡不夠用:「the datasheets are underspecified here」——xor map 對不上、MMIO 的減法定址順序不固定、不同型號之間細節也不一樣。於是變成一個逆向工程問題,不是查手冊就能解決的問題。
解法靠 GF(2) 線性映射這個性質:README 指出「the DRAM controller's address transform is a GF(2) linear map, which means we can reconstruct the scrambled memory with basic linear algebra」。收集資料的手法是在正常(coherent)視角下,把記憶體控制器切到被打散(spaghettified)的視角,丟一個 sentinel 值,再切回正常視角,掃記憶體找這個值冒出來的位置。這樣就拿到一組 (target, alias) 配對——兩個實體位址對應到 DRAM 裡同一顆 cell 的具體證據。重複幾次,把配對餵給 z3,它就解出把兩種視角接起來的轉換矩陣。README 把這個矩陣形容成「a rosetta stone: any target address in the coherent view maps to an alias that reaches the same DRAM in the spaghettified view」——往後只要是想讀的 protected 位址,套進這個矩陣就能算出它在打散視角裡的別名,翻一次 DCT 暫存器讀寫,翻回去,全程不留痕跡。
要讓這套解法真的解得出來,關鍵在於未知的到底是什麼:兩個 16×16 的 GF(2) 矩陣,一個是韌體預設的 firmware/coherent 轉換 M_firmware,一個是攻擊者自己觸發的 spaghettified 轉換 M_attacker,兩者都是把一串位址位元映射到另一串位址位元的線性函式——因為底層做的全部是 channel/rank/bank 這幾種 XOR interleave,才會剛好落在 GF(2) 這個代數結構裡,矩陣乘法退化成 AND、加法退化成 XOR,沒有進位、沒有浮點誤差。README 給的合成公式是:「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」——也就是 M_attacker 的逆矩陣乘上 M_firmware,兩個矩陣接起來剛好抵消掉中間那段誰都不知道的轉換,只留下「正常視角的位址」對「打散視角的別名」這一段真正要的對應。每一組 (target, alias) 配對,都是這個未知矩陣的一條線性方程式;收集夠多獨立的樣本,z3 就能把整個矩陣的每一個位元解出來,用不著資料手冊裡沒寫的那部分。
switch tabs to walk the sentinel-scan loop · 4 stages
在正常視角丟 sentinel
先留在 coherent 視角,在記憶體裡挑一個位址,寫進一個好認的樣式。README 舉的例子:「drop some sentinel value like `0xdeadc0de` into a random address in memory」——這個位址本身就是後面要解的 target。
翻 DCT,切到打散視角
執行 xor dword [0xf80c2094], 0x00400000,「modify the MCT/DCT to switch to the spaghettified view」——channel/rank/bank 的對應整批換掉,剛剛寫入的 sentinel 理論上還在同一顆 DRAM cell 裡,只是換了一個位址才能碰到它。
掃記憶體找 sentinel
翻回 coherent 視角,「sweep memory for where the sentinel resurfaces. This gives a (target, alias) pair」——sentinel 冒出來的新位址就是 alias,跟第一步的 target 湊成一條具體證據:兩個實體位址對應到同一顆 DRAM cell。
餵給 z3,解出矩陣
重複幾輪拿到一批 (target, alias) 配對,「pass it to z3, and it solves the translation matrix」。解出來的矩陣是「a rosetta stone」:往後任何一個想讀的 protected 位址,套進矩陣就能直接算出它在打散視角裡的別名。
GF(2) 線性映射GF(2) 是只有 0 與 1 的有限體,矩陣乘法退化成 AND、加法退化成 XOR——位址重排本來就是一串 XOR 運算,天生長這個形狀。README:「the DRAM controller's address transform is a GF(2) linear map」。
spaghettified viewDCT 暫存器被改過之後,原本的 channel/rank/bank 對應被打散,README 用這個詞稱呼那個被打散的視角,與正常(coherent)視角相對。攻擊時切換進去、讀完再切回來的那一邊。
sentinel 值README 舉的例子是 0xdeadc0de——好認、不會跟正常資料混在一起的位元組樣式,方便切回正常視角後在記憶體裡掃到它。丟進被打散視角,切回正常視角後掃記憶體找它冒出的位置。
z3z3 是微軟研究院開源的 SMT 求解器,這裡拿來解一組線性方程,不是做邏輯證明。吃進一批 (target, alias) 配對,解出轉換矩陣。
驗證:PSP、SMRAM、C6、microcode 四個案例
矩陣解出來之後,README 沒有停在「證明可行」,逐一示範讀出四塊平常摸不到的記憶體。
| 保護區 | 理論上的鎖 | 藏在 DRAM 哪裡 | 作者實際讀到什麼 |
|---|---|---|---|
| PSP 私有記憶體(fTPM) | fenced off at the memory controller, opaque even to ring-0 | visible top-of-memory 之後的 carveout | fTPM 的 RSA 模指數函式 crAmd_ModExp,完整 Thumb-2 組合語言 |
| SMRAM(SMM handler) | SMRAM lock(晶片組鎖定) | SMBASE + 0x8000(SMI entry vector) | ring -2 下執行的 SMI handler 入口:載 GDT、切保護模式、跳進 handler 本體 |
| C6 省電狀態暫存器 stash | inaccessible from the OS | 每核心 16 KiB save area | CR3、IA32_APIC_BASE、saved RIP 等架構暫存器,加上作者形容「沒人討論過」的其他內部狀態 |
| CPU microcode patch 的 DRAM 副本 | SRAM 式 patch 本應隨 C6 斷電消失 | C6 save area 的 +0x1800 | 比對後 68/94 個區塊命中 fam16h 官方 microcode,且能用 dram_poke 直接寫回 |
PSP 那一段最直接:fTPM 本身跑在 PSP 自己的 ARM 核心上,「in a DRAM carveout right past the visible top-of-memory」,套上轉換矩陣讀出來的是 crAmd_ModExp——README 形容它是「the modexp behind every fTPM signature, and behind the Miller-Rabin tests that mint its keys」,反組譯出來的 Thumb-2 組合語言直接貼在 README 裡,函式進出、字串指標、呼叫的子函式全部齊全。SMRAM 那段拿 SMBASE MSR(0xc0010111)算出 SMI handler 入口在 SMBASE+0x8000,讀出的是 ring -2 下執行的第一段指令。README 在這裡下了一句評語:「SMRAM "locked" turns out to be a polite suggestion when we can talk to the DRAM controller directly.」
測試機上量到的具體位置也留在 README 裡:PSP carveout 的 PSP_BASE 是 0x7f800000,大小 PSP_SIZE 是 0x800000;crAmd_ModExp 這段程式碼就釘在 PSP_BASE+0x19d4,函式體只有 0x64 bytes。SMI handler 的組合語言裡也留了一個具體位址:進 SMM 之後,CPU 換上的堆疊頂端是 0x6efe2ff8,這是 SMBASE+0x803a 那段 handler 真正跑起來之前,最後一步準備好的資源。
C6 那部分最意外的是規模:每顆閒置核心的完整 x86 架構狀態存在 16 KiB 的 save area 裡,四核心的資料一次全部讀出來——CR3、IA32_APIC_BASE、GS base、saved RIP,這些其實 ring-0 本來就查得到。具體數字比想像的更細:四核心讀出來後,只有 core 0 的 APIC_BASE 帶著 BSP bit(0xfee00900),其餘三顆是 0xfee00800,README 的講法是「One core with the BSP bit set, three without — the boot processor and its three APs, caught mid-idle with their register state lying in the open.」同一份 dump 裡,core 0 的 GS/per-cpu base 讀到 0xffff9be4e3600000,CR3(頁表根)是 0x0fd46000,saved RIP 停在 0xffffffff8f3a0029——這些原本都是 ring-0 就查得到的架構暫存器,差別在於現在連 CPU 都被凍結在 C6,平常靠指令讀暫存器的路徑根本不通,唯一能碰到這些值的管道只剩 DRAM 裡的這份 stash。作者自己的評語比較誠實:「Of course, those registers are all accessible from ring-0 anyway. The *fun* part is in all the *other* CPU state sitting there」——那些 ring-0 摸不到的內部暫存器。這句話也順帶承認了這個案例的一個限制:目前示範出來的多半是已知暫存器,那些「沒人討論過」的內部狀態,README 自己也只寫了一句「I have no idea what's in here」。microcode 那段則是 C6 附帶的產物:核心進 C6 省電時,原本在 SRAM 裡的 microcode patch 會斷電消失,所以 CPU 會先把它備份進 DRAM(C6 save area 的 +0x1800),甦醒時再重新載入。作者把讀出來的位元組跟 AMD 官方各世代的 microcode 檔案逐塊比對,fam16h 命中 68/94 塊——確認這真的是 CPU 正在跑的那份 patch,不是巧合的雜訊。更進一步,dram_dump 有個姊妹工具 dram_poke,同一組別名位址能讀也能寫,而這份 DRAM 副本正是核心從 C6 醒來後會重新載入的那一份。
這招能走多遠
作者把測試範圍講得很白:「Developed and tested on AMD Family 16h CPUs, the last generation whose datasheets document the DRAM controller's translation registers」——而且手冊本身就證明這些暫存器鎖不住。README 接著補一句:「17h and beyond simply leave this information out.」也就是說,這次示範能成立的一個關鍵前提,是那個世代的資料手冊還願意寫出這些暫存器的位置與語意——後面的世代不是修好了這個洞,是資料手冊不再公開這些細節,逆向工程的門檻變高了,不代表底層的轉換邏輯消失了。README 自己的主張是:「Channel interleave, rank interleave, bank interleave, swizzle, chip-select normalize」——這幾種手法,現代的記憶體控制器多少都做過一部分,AMD、Intel、ARM、RISC-V,手機、伺服器、嵌入式全部算在內;README 的說法是「The same architectural shape sits underneath everything.」以及「skitter-creek-bath-salts shows us only how to begin」。
這段話值得拆成兩半看。「不同世代、不同架構的記憶體控制器都要做 channel/rank/bank interleave」是作者對硬體設計慣例的觀察,合理程度不低——DDR 系記憶體不管掛在哪家 CPU 上,都得靠某種雜湊把連續的實體位址分散到多個 channel/bank 拉高頻寬,這是 DRAM 物理限制逼出來的共同解法,不是巧合。但「同樣的線性代數手法能在別的架構上重建出轉換矩陣」是這個 repo 還沒示範過的部分——README 列出的四個案例全部在 AMD Family 16h 上完成,repo 裡沒有 ARM 或 RISC-V 平台的 (target, alias) 配對,也沒有 Intel 平台的 z3 解算過程。合理的推測是,只要一顆 CPU 的記憶體控制器一樣做 channel/rank/bank 這類 XOR 式的位址重排,同一套「sentinel 值+掃記憶體+GF(2) 反解」的方法論在原理上就有機會套用——但能不能實際翻出等效於 0xf80c2094 的暫存器、翻了之後平台撐不撐得住,取決於個別世代的 MMIO 佈局與微架構細節,這些 README 並沒有給出答案。作者也把這項研究列進了 Black Hat 2026 的議程,條目只寫著「Spaghettifying DRAM」,標註「Coming Soon」,細節要等正式簡報才會補齊。
作者自己把這次示範定位得很清楚:「The exploit demonstrated here is one configuration register on AMD Family 16h, picked because the datasheets gave enough to begin. The *pipeline* it broke is everywhere.」這句話把「選哪顆 CPU」跟「破的是哪一層」分開講:選 AMD Family 16h 只是因為手冊夠用,不是因為只有這顆 CPU 有這個洞;破的那一層——把實體位址重寫成 channel/rank/bank/row/column 的那道轉換——是所有現代記憶體控制器都要做的事,這是 README 自己的說法,語氣上是斷言,但沒有跨架構的實測資料撐著,本文把它當成作者的主張而不是已驗證的結論。對讀者來說,合理的下一步不是去找「哪顆 CPU 有同樣位址的暫存器」,而是去問:自己負責的系統裡,有沒有一層類似的「最後一段轉換」,建立在下層座標系上,而上層所有的存取控制都只驗證了轉換前的位址——GPU 顯示記憶體的位址轉換、NVMe 的 FTL 邏輯到實體對應、跨機器的分散式儲存路由表,都是同一種形狀的候選。這是這篇 README 沒有寫、但架構上站得住的推論,不是作者自己驗證過的結論。
Take-away:判斷一層保護鎖不鎖得住,不能只看它檢查的是不是正確的實體位址——要接著問這個實體位址在真正落地 DRAM 之前,還要不要經過一段沒人管的轉換。MCT/DCT 在這個案例裡,就是那一段。