Creating a Kernel Pwn Challenge : Setup and Deployment

For this article I will be heavily referencing these 2 blogs for their scripts and setup

Building your own Linux Kernel

Installing Buildroot

Head over to Buildroot and install a copy of it and extract it.

Configuration

Inside the directory, run

make qemu_x86_64_defconfig

To change the Buildroot configurations, run

make menuconfig 
  • Go to Kernel > Kernel Version and select the kernel version that you want. If the version that you want is not specified in here, you will need to select Custom Version . Then, go back to Kernel Version and set your custom version. Then, back to main menu and go to Toolchain > Custom kernel header series and set to your desired version.

  • Go to Filesystem images to select the type of filesystem image you want (cpio or ext2/3/4)

Next, we will configure the Linux kernel, run

make linux-menuconfig
  • Enable kernel-level debugging features by selecting Kernel hacking > Kernel debugging

  • Enable kernel debug symbols at Compile-time checks and compiler options -> Debug information -> Rely on the toolchain's implicit default DWARF version

  • Sometimes, you might want to patch the kernel to make it easier to exploit. You should have the headers at /output/build/linux-headers-X where you can patch the code. I have not tested this yet so I will not cover this.

Then, configure some BusyBox user utilities

make busybox-menuconfig
  • Select Runtime utilities -> setuidgid which we will use later

  • Select Shells -> cttyhack

Finally, to finish building your linux kernel, run

make -j$(nproc)

If everything ran successfully, you should see your Linux Kernel (bzImage) and filesystem image (ext or cpio) in <BUILDROOT>/output/images and vmlinux with debugging symbols at `<BUILDROOT>/output/build/linux-<VERSION>/ .

Running your Kernel with Qemu

The scripts from https://r1ru.github.io/posts/0/#how-to-run-the-linux-kernel-with-qemu works fine. Mount the filesystem and create a /init file in mounted fs and add the contents

init
 #!/bin/sh

 if (exec 0</dev/console) 2>/dev/null; then
     exec 0</dev/console
     exec 1>/dev/console
     exec 2>/dev/console
 fi

 mkdir /home
 echo 'root:x:0:0:root:/root:/bin/sh' > /etc/passwd
 echo 'root:x:0:' > /etc/group
 chmod 644 /etc/passwd
 chmod 644 /etc/group

 adduser ctf --disabled-password 2>/dev/null

 chown -R root:root /
 chmod 700 -R /root
 chown ctf:root /home/ctf
 chmod 777 /home/ctf
 chmod 755 /dev
 chmod u+s /bin/su

 mount -t proc -o nodev,noexec,nosuid proc /proc
 mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
 mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run

 ln -sf /proc/mounts /etc/mtab

 echo 1 > /proc/sys/kernel/kptr_restrict
 echo 1 > /proc/sys/kernel/dmesg_restrict
 echo 1 > /proc/sys/kernel/perf_event_paranoid

 setsid cttyhack setuidgid 1000 sh

Remember to make this file executable. Next, create a script to run it

run.sh
#!/bin/sh
qemu-system-x86_64 \
      -m 64M \
      -cpu qemu64 \
      -kernel bzImage \
      -drive file=rootfs.ext3,format=raw \
      -snapshot \
      -nographic \
      -monitor /dev/null \
      -no-reboot \
      -smp 1 \
      -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=3 oops=panic panic=-1"

Writing your own vulnerable Kernel module

This is the part where you write the code for your kernel module. I wont cover how to write this as there are plenty of resouces online. Once you are done writing and ready to compile, create a Makefile

Makefile
obj-m := vuln.o
KERNEL_DIR := ~/buildroot-2025.08/output/build/linux-6.16.5
CROSS_COMPILE := ~/buildroot-2025.08/output/host/bin/x86_64-buildroot-linux-gnu- 

all:
        CROSS_COMPILE=$(CROSS_COMPILE) $(MAKE) -C $(KERNEL_DIR) M=$(shell pwd) modules

clean:
        CROSS_COMPILE=$(CROSS_COMPILE) $(MAKE) -C $(KERNEL_DIR) M=$(shell pwd) clean

This is important to ensure you are compiling against the headers of the linux kernel that is being used. If all is successful, you should have your vuln.ko.

Deployment to Server

Before deploying to server, mount your file system again and modify /etc/shadow . Look for the line root:::::::: and change it to root:*:::::::. This will disable password login to root so players cant do a su root.

Next, add your kernel module into the file system. Then, edit your init file to load the module at startup. Add the lines

insmod /root/vuln.ko
chmod 666 /dev/vuln

That /dev/vuln is if you registered a new device in ur module.

Dockerfile
FROM ubuntu:latest

RUN apt-get update
RUN apt-get install -y qemu-system-x86 qemu-utils socat fuse2fs fuse wget

EXPOSE 5000

RUN useradd -d /home/ctf -m -p ctf -s /bin/bash ctf
RUN echo "ctf:ctf" | chpasswd
RUN ulimit -c 0

WORKDIR /home/ctf

COPY bzImage ./bzImage
COPY deploy.sh ./deploy.sh
COPY rootfs.ext3 ./rootfs.ext3
COPY rootfs.ext2 ./rootfs.ext2
COPY flag.txt ./flag.txt
RUN chmod +x *.sh

CMD socat tcp-l:5000,reuseaddr,fork EXEC:"./deploy.sh",pty,stderr
deploy.sh
#!/bin/sh

read -p "Enter the link to your exploit binary: " link

wget $link -O exploit
chmod 777 ./exploit

mkdir -p mnt
rm -rf mnt/*
fuse2fs -o nonempty rootfs.ext3 ./mnt
chown -R 1000:1000 ./mnt
rm ./mnt/exploit
mv ./exploit ./mnt/exploit
fusermount -u ./mnt 

qemu-system-x86_64 \
    -m 64M \
    -cpu qemu64 \
    -kernel bzImage \
    -drive file=rootfs.ext3,format=raw \
    -drive file=flag.txt,format=raw \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -smp 1 \
    -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1"

This method expects the players to be able to host their binary up to download. This can be easily done using ngrok.

Final words

Thanks for reading! If you notice any misconfigurations or want to suggest improvements, you can drop me a message

Last updated