December 19, 2019

Ubuntu apport PID recycling vulnerability (CVE-2019-15790)

Kevin Backhouse

In this third post of my four-part series about several vulnerabilities, I'll focus on apport CVE-2019-15790, a vulnerability which enables a local attacker to obtain the ASLR offsets for any process they can start (or restart).

The bug

The bug is not a coding error that is easy to pinpoint in the source code. Instead, it's a mistake related to accidentally using PIDs as authorization tokens. Apport is invoked by the kernel to generate a crash report for a process that's crashed. One of apport's command-line arguments is the PID of the process. Although the process crashed, it hasn't been removed from the PID table, since that doesn't happen until after apport is finished generating the report. This enables apport to read extra information about the crashed process in /proc/[pid] and include it in the crash report. But what happens if I kill the process sooner by sending it a SIGKILL signal?

  • Since PIDs get recycled, is it possible that a new process will be assigned the same PID?
  • If a new process is assigned the same PID, can apport detect that this has occurred?

The answer to the first question is yes, and the answer to the second questions is no. The reason is that the pool of available PIDs is very small. There's only 32768 on a typical Ubuntu installation, so the PID is extremely weak as an identifying token. This enables me to trick Apport into leaking information about a process that doesn't belong to me.

Exploit plan

I had two ideas for how to exploit this bug.

Plan A (unsuccessful)

My first idea was to use the bug to dodge the problem that I encountered in my exploit for CVE-2019-7307, which I covered in the second post of the series. The problem there was that apport looks at the ownership of /proc/[pid]/stat to determine who should own the crash report. But /proc/[pid]/stat was owned by root, so I could not read the crash report. What would happen if I use the PID recycling trick to replace /proc/[pid]/stat with a file that I own? Would this enable me to read the crash report?

I wrote an exploit to do this and it almost worked. The problem is that apport checks the ownership of /proc/[pid]/stat quite early on. This means that the exploit needs to send a SIGKILL to the crashed process before apport checks the ownership. The kernel uses a pipe to send the core dump to apport's stdin. Unfortunately, apport doesn't start reading its stdin until after checking the ownership of /proc/[pid]/stat. Because the process was removed from the process table, the kernel aborts sending the rest of the core dump. The pipe does have a buffer though. I found that the first 64KB of the core dump is successfully delivered and written to the crash report. Unfortunately, that isn't enough of the core dump to include the section containing the information that I want to steal. If the pipe buffer was bigger, this would work. I was frustratingly close to having a working exploit.

Plan B

Plan A didn't work, but I realized that I could flip things around and use the bug in an almost opposite way. The main concept of Plan A was to take ownership of the PID of a privileged process after it crashed. Instead, Plan B is about deliberately crashing an innocuous process belonging to myself, before waiting for a privileged process to get assigned its recycled PID. This doesn't enable me to access a core dump of the privileged process, because the core dump will be from the innocuous process that I deliberately crashed. But it enables me to access the extra information that apport reads from /proc/[pid] and includes in the crash report. The most valuable of these is /proc/[pid]/maps, which contains the ASLR offsets of the process.

Here's a diagram of Plan B:

The diagram shows time flowing downwards. Starting from the top, these are the events that happen:

  1. Start a /bin/sleep process, which gets assigned PID 1234 (for example).
  2. Send a SIGSEGV to PID 1234, which triggers the kernel to start apport and send it a core dump for /bin/sleep.
  3. Pause apport. (How?)
  4. Wait for the privileged process to start (and get assigned PID 1234).
  5. Resume apport.
  6. Apport creates a crash report for /bin/sleep, containing the ASLR offsets for the privileged process.

Clearly, there are a few details that still need to be worked out. The exploit depends on the privileged process getting assigned the same PID as /bin/sleep. The simplest way to ensure that is by repeatedly starting and stopping the process until it gets the correct PID, but that might take a while. So I need to pause apport while that is happening. Otherwise apport will finish too quickly and read the ASLR offsets for /bin/sleep, which are not interesting to me.

Exploit implementation

The source code for my proof-of-concept exploit is posted on GitHub. The vulnerability could be used to obtain the ASLR offsets of any process, provided that I can control when the process is created. But, for the purposes of the proof-of-concept, I'm targeting whoopsie, since it will help me complete the exploit chain that I started in my previous blog post and will finish in my next (to be published on December 23, 2019). For the purpose of this post, what you need to know about the whoopsie daemon is that I can restart it on demand because it has a heap overflow vulnerability which I can use to trigger a SIGSEGV. The next blog post covers exploiting the heap overflow to gain code execution as the whoopsie user, but first I need to know the ASLR offsets.

The exploit works according to the plan that I've outlined, and reuses many of the same techniques I described in my previous post. For example, it detects when apport starts up by using inotify to watch for /var/crash/.lock getting opened. But I need to use a new technique to pause apport while whoopsie restarts. Note that it takes whoopsie roughly 15 seconds to crash and restart, which makes pausing essential. The exploit needs to pause apport between the call to get_pid_info on line 452 and the call to add_proc_info on line 500:

get_pid_info(pid)

# Partially drop privs to gain proper os.access() checks
drop_privileges(True)

# *** Lines 454-499 omitted ***

info.add_proc_info(pid)

That's because get_pid_info reads the files in /proc/[pid] to determine who owns the process, and add_proc_info reads /proc/[pid]/maps. This means that the PID needs to belong to a process owned by me when get_pid_info is called, but by the time that add_proc_info is called, the PID needs to have been recycled and reassigned to the whoopsie daemon.

The reason I'm able to pause apport is due to the call to drop_privileges on line 455, which moves apport into the "partially drop privs" state. See the previous post for more information. That state is awesome, because it enables me to send apport signals, even though it still has a root EUID, which gives it privileges to read any file on the system! So I can send apport a SIGSTOP, which pauses the process while whoopsie crashes and restarts. Later, I send apport a SIGCONT and it resumes. Unfortunately, I wasn't able to time the sending of the SIGCONT perfectly because inotify doesn't work on files in /proc. Instead, I had to use the crude technique of looping until the signal is accepted.

PID Feng Shui

How do I ensure that whoopsie receives the same PID as the /bin/sleep that I deliberately crashed? As mentioned earlier, PIDs are assigned sequentially from a pool of 32768 on a typical Ubuntu installation. This number wraps around to zero when it reaches 32768. Suppose I want whoopsie to start with PID 10000. The basic idea is to keep starting and stopping innocuous processes until I get a process with PID 9999. The next PID that gets assigned will be 10000, so that's the PID that whoopsie will get, provided that I don't get unlucky with another random process starting up just before whoopsie starts. I have written a utility named fork_child_with_pid which starts and stops innocuous processes for this purpose. The main difficulty that I encountered is that approximately 15 /lib/systemd/systemd-udevd processes always seem to get forked at exactly the same time that the new whoopsie process starts. This means that there's a race between whoopsie and the udev processes to get a PID, which adds some non-determinism and reduces the chance that whoopsie will get the PID that I want. Fortunately, it's not a huge problem since whoopsie gets assigned the correct PID approximately 33% of the time.

To be continued ...

The exploit that I described in this post enables me to get the ASLR offsets for the whoopsie daemon. In the final post in the series, I'll use those offsets to successfully exploit a heap buffer overflow in whoopsie and gain code execution as the whoopsie user: