Basically we will build a minimal kernel to support initramfs, user permissions and custom modules. Than we will package Busybox in a cpio archive and script init and module installation. Then we will launch it with QEMU and containerize it as a xinetd service in docker (described here).
This step is not required if you are targeting a standard kernel that you can grab from some distro. Most of the time however, we need to disable some exploit mitigations so we need to compile it ourselves. For this example I will use 4.20. There are multiple tutorials online about how to compile a kernel. To make a minimal config use allnoconfig make configuration and enable the following options.
64-bit kernel ---> yes
Enable loadable module support ---> yes
General setup ---> Initial RAM filesystem and RAM disk (initramfs/initrd) support ---> yes
General setup ---> Configure standard kernel features ---> Multiple users, groups and capabilities support ---> yes
General setup ---> Configure standard kernel features ---> Sysfs syscall support ---> yes
General setup ---> Configure standard kernel features ---> Enable support for printk ---> yes
General setup ---> Configure standard kernel features ---> Load all symbols for debugging/ksymoops ---> yes
General setup ---> Configure standard kernel features ---> Include all symbols in kallsyms ---> yes
Executable file formats / Emulations ---> Kernel support for ELF binaries ---> yes
Executable file formats / Emulations ---> Kernel support for scripts starting with #! ---> yes
Binary Emulations ---> IA32 Emulations ---> yes
Binary Emulations ---> IA32 a.out support ---> yes
Binary Emulations ---> IA32 ABI for 64-bit mode ---> yes
Device Drivers ---> Generic Driver Options ---> Maintain a devtmpfs filesystem to mount at /dev ---> yes
Device Drivers ---> Generic Driver Options ---> Automount devtmpfs at /dev, after the kernel mounted the rootfs ---> yes
Device Drivers ---> Character devices ---> Enable TTY ---> yes
Device Drivers ---> Character devices ---> Serial drivers ---> 8250/16550 and compatible serial support ---> yes
Device Drivers ---> Character devices ---> Serial drivers ---> Console on 8250/16550 and compatible serial port ---> yes
File systems ---> Pseudo filesystems ---> /proc file system support ---> yes
File systems ---> Pseudo filesystems ---> sysfs file system support ---> yes
Here is a list of exploit mitigations that might need to be disabled/enabled for your particular challenge.
Ensure /proc/kallsyms is readable by non-root users
Allocations at NULL to allow nullptr de-reference (DEFAULT_MMAP_MIN_ADDR=0)
SMAP (Superuser Mode Access Prevention) to allow kernel space code accessing and executing user-space data directly
KASLR (RANDOMIZE_BASE) to ensure the kernel is mapped at the same address after every reboot
Stack Protector buffer overflow detection to disable/enable stack canaries
To easily find where these options are in the menuconfig you can use the search / just like in vim. If multiple results are found you can select the right one by pressing the number assigned to it (1/2/3/4/5…).
Now let’s build and test it with the following start.sh script.
Seems like it’s booting fine but unable to start the init process (PID 1). Since we enabled initramfs and #! scripts support, let’s create /init bash script that would mount the appropriate Pseudo Filesystems and drop us into a shell.
To be able to execute a bash script we need to install bash itself as well the other bin/sbin utils. Busybox is the perfect collection of statically compiled utils for this job. You can compile it yourself or like me you can download the precompiled binary from here
We can get an example init script from our own linux distro and cherry-pick the commands we need.
Here is what I’ll use for this example
Now let’s compress it into a cpio format and test it out.
Append the -initrd option to the qemu-system command line arguments.
As you have figured out if you are paying attention to details (from the commented out modprobe command), I will package MBE lab10C as an example vulnerable module.
The module’s destination folder needs to match the kernel version or modprobe won’t be able to find the module. Let’s compile it.
Uncomment the modprobe commands from the init script and rebuild the cpio archive. We can confirm the module is loaded successfully.
Don’t forget to clean up the directory where we build the module, this was just an example after all, I would normally not build there. You can place the flag in /root and ensure proper permissions are set, make sure you don’t redistribute the initramfs with the real flag to your users. Now you can launch qemu-system-x86 from xinetd packaged in a docker container and your challenge is ready. Because xinetd won’t terminate the connection when users close theirs (with ctrl+d for example) and your docker container doesnt end up with hundreds of stale qemu-system processes, prefix the qemu-system-x86 command with the timeout utility as such.