CVE-2020-7460: FreeBSD Kernel Privilege Escalation

September 01, 2020 | Guest Blogger

In August, an update to FreeBSD was released to address a time-of-check to time-of-use (TOCTOU) bug that could be exploited by an unprivileged malicious userspace program for privilege escalation. This vulnerability was reported to the ZDI program by a researcher who goes by the name m00nbsd. He has graciously provided this write-up and proof-of-concept code detailing ZDI-20-949/CVE-2020-7460.


The goal is to achieve kernel code execution on FreeBSD starting from an unprivileged user, using a TOCTOU vulnerability present in the 32-bit sendmsg() system call. This vulnerability has been assigned CVE-2020-7460 and affects all FreeBSD kernels since 2014. Before we get into the details, here’s a quick video showing the exploit in action.

The Vulnerability

Let’s jump right in where the vulnerability is located: the freebsd32_copyin_control() function. This function is made of two loops, annotated below:

Let’s see what’s going on here. The first loop fetches data from userland. This data is a set of contiguous cmsghdr structures:

The first loop performs length checks and makes sure that the total length of the buffer (len bytes) can fit the kernel buffer subsequently allocated, which will be of size MLEN bytes.

Once the length check has passed, said kernel buffer is allocated, and the second loop copies the userland data into that buffer. The kernel performs some processing to convert the structures from 32-bit to 64-bit, but this is not relevant here, so we will omit that.

After the second loop has finished, the kernel buffer ends up in this format:

However, there is a TOCTOU vulnerability here: between the first loop and the second loop, it is possible that userland modified the cmsg_len fields of its cmsghdr structures, and that the resulting total length now exceeds MLEN.

Suppose that right after the first loop, userland increased the value of the last cmsg_len field:

A kernel heap overflow ensues.

Triggering the vulnerability

We can trigger this heap overflow via the 32-bit sendmsg() syscall, which has the following format:

msg_control is where our user memory buffer begins. That is where the contiguous cmsghdr structures are located.

To trigger this vulnerability, we:

-- Create a contiguous block of cmsghdr structures, which are initially valid.
-- Spawn a thread that calls sendmsg() in a loop with our block.
-- Spawn another thread that increases the value of the last cmsg_len field, and then reverts back to the correct value, in a loop.

Once done, we just wait a few seconds for the two threads to race. Very quickly, the kernel panics. We’re off to a good start.

Improving the Primitive

We are interested in knowing when the heap overflow triggered, so that we know when to stop the two racing threads. That is, we don’t want to keep the threads running for too long and thereby overflow kernel memory in a too extreme manner that could result in a panic. Rather, we would prefer to stop immediately after the first successful overflow to increase the reliability of the exploit and limit damage to the kernel.

To accomplish this, we can use the following trick:

The idea consists of leveraging the behavior of copyin(), a function used to copy user data into kernel memory. To protect against userland giving an unmapped page (or just garbage in the general sense), copyin() gracefully handles page faults in the kernel and safely returns EFAULT when an unmapped page is encountered during the copy. EFAULT in turn gets returned by the syscall.

In the case of our heap overflow, we can just put an unmapped page exactly where we want the copy to end. copyin() will then copy everything up until this unmapped page, will fault on it, and will then return EFAULT, which causes sendmsg() to return EFAULT as well.

Therefore, if sendmsg() returned EFAULT, then the unmapped page was hit, and we know the overflow triggered.

So, we can choose what to write, how much to write, and know exactly when the overflow triggered. More progress.

Mbufs and heap layout in FreeBSD

The kernel buffer we overflow is an mbuf. An mbuf is a special structure, allocated using FreeBSD’s regular zone allocator. Without going into unnecessary detail, mbufs are located one after the other contiguously on large pages. This means that with our heap overflow, there is a very high chance that we are overwriting the next mbuf in memory.

mbuf structures have interesting fields from the point of view of exploitation. Here, we are going to use the m->m_ext.ext_free field, which is a pointer to a function and has the following prototype:

          void (*ext_free)(struct mbuf *the_current_mbuf_being_freed);

When the kernel wishes to free an mbuf, it checks the mbuf for certain flags. If this check succeeds, the kernel calls the mbuf’s ext_free function and expects this function to free the mbuf.

We will use that in our exploit.

Controlling Deallocation of mbufs

The current state of affairs is that we have a heap overflow that allows us to overwrite the next mbuf in memory, and, therefore, we can patch the ext_free field of that next mbuf.

How exactly can we cause ext_free to be called? Once we detect that the heap overflow succeeded, we must quickly trigger a deallocation of the next mbuf in memory.

This can be achieved using a simple UDP server/client pair launched locally:

-- The client sends packets to the server using the regular sendto(). This causes mbufs to be allocated in the kernel. This can be seen as a PushMbuf() primitive that basically allocates mbufs.
-- The server receives these packets using the regular recvfrom(). This causes the allocated mbufs to be freed, and their ext_free to be called. This can be seen as a PopMbuf() primitive that frees mbufs.

So now let’s see how we can use these primitives:

  1. We use PushMbuf() to push many mbufs into the kernel. This fills the heap.
  2. We use PopMbuf() to pop 50% (chosen experimentally) of the mbufs we just pushed. This creates holes in the allocation map.
  3. We trigger the heap overflow and overwrite the ext_free of some mbuf located right after ours.
  4. Once we know the heap overflow triggered, we use PopMbuf() to pop the remaining 50% of the mbufs we pushed.
  5. If we are lucky, this will free the mbuf whose ext_free we just overwrote. ext_free therefore gets called and jumps to an address we control. If we are unlucky, we go back to step 1 and try again until this succeeds.

This procedure typically succeeds in less than 1 second, and the kernel jumps into an ext_free address we fully control. Here, a COP/JOP/ROP chain begins.

Chaining Gadgets

Where are we at? We have just managed to have the kernel jump into an address we control. The state of the registers is rather simple:

          %rdi = address of mbuf being freed

Here, %rdi points to the mbuf we overwrote. If we base our COP/JOP chain on someoffset(%rdi), we actually are on a buffer whose content we control since we managed to overflow into it previously.

Unfortunately, there are few useful gadgets that can be chained together, and we are short on space as the mbuf content is partly consumed already by fields that need to remain valid.

One can see that:

  1. Given the apparent lack of nice gadgets, we have to resort to some twisted COP/JOP gadgets.
  2. We quickly need to turn to an ROP, because ROP is easier, and we cannot maintain COP/JOP for too long.
  3. Given the space shortage, the ROP will have to quickly get out of the kernel and jump to a userland shellcode where we can gain complete execution without constraints.

With that in mind, chainers gonna chain. On my side, I ended up with a chain of 13 gadgets, which do the following:

  1. Save the register context in order to restore it correctly afterward.
  2. Mark the userland page tables as executable. This is an interesting constraint that appeared with the Meltdown mitigation on FreeBSD. When the kernel executes, the user page tables have the no-execute (NX) bit, so the kernel cannot jump into any user address. Therefore, the chain has to patch the page tables to drop NX, which is not very complicated given that FreeBSD does not randomize its recursive PML4 slot.
  3. Disable SMEP and SMAP on the CPU.
  4. Finally, jump into a userland shellcode I built.

Not exactly the most straightforward exploit on the planet, but it works.

What now?

We are finally executing our shellcode in kernel mode. We now have free hands on all of the systems.

As a simple move, I just patch my thread’s UID field and give it UID zero. I then go on to restore the state of the kernel that I initially saved as part of my chain, and the kernel peacefully continues execution as if nothing had happened. Finally, I call setuid(0) and this succeeds, which means I’m root.

Of course, to do just that, it is easier to just patch that UID directly as part of a write-what-where without actual kernel code exec, but I wanted direct code exec to give to ZDI.

Full steps

Let’s recap:

  1. We create a UDP server/client pair locally.
  2. We create a userland shellcode.
  3. Using the UDP client, we allocate many mbufs and use the server to create holes in the kernel heap allocation map.
  4. We race two threads against one another to trigger a heap overflow in sendmsg(), which allows us to overwrite an mbuf. In the mbuf we overflow into, we write both a new ext_free pointer and the data for a COP/JOP/ROP chain.
  5. We detect successful overflow and use the UDP server to free the mbufs we allocated, which causes our ext_free to be called and starts the chain.
  6. Our chain executes, disables kernel/CPU protections, and jumps into our userland shellcode.
  7. We won.

Exploit

The proof-of-concept code found here and takes you from an unprivileged user to a root shell. It has an observed reliability of 90%.

Conclusion

According to FreeBSD, i386 and other 32-bit platforms are not vulnerable. For those systems that are vulnerable, you need to upgrade to a supported FreeBSD stable or release / security branch (releng) dated after the correction date, and reboot. That should take care of the bug. The team at FreeBSD needed just two weeks to build and release the patch. Nice work indeed.


Thanks again to m00nbsd for providing this great write-up and PoC. This was his first FreeBSD submission to the ZDI program, and we certainly hope to see more submissions from him in the future. Until then, follow the team for the latest in exploit techniques and security patches.