A mini-article about CVE-2017-18344 — an arbitrary-read vulnerability I found in the Linux kernel timer subsystem. Contains a brief description of the /etc/shadow leak exploit I wrote for this bug. Originally posted as an announcement on the OSS-Security mailing list.

Overview

In November 2017, syzbot reported a global-out-of-bounds bug in the timer subsystem of the Linux kernel. This bug is exploitable and allows reading arbitrary kernel memory to leak keys, credentials, or other sensitive information. The impact of this bug is thus similar to Meltdown.

The bug was introduced in commit 57b8015e (“posix-timers: Show sigevent info in proc file”) in 3.10 and fixed by commit cef31d9a (“posix-timer: Properly check sigevent->sigev_notify”) in 4.15-rc4.

The bug only affects kernels that have CONFIG_POSIX_TIMERS and CONFIG_CHECKPOINT_RESTORE enabled. Many modern distributions enable these configs.

This bug has been fixed in Ubuntu 16.04 but still affects at least CentOS 7 at the time of this writing (at least 3.10.0-862.9.1.el7.x86_64, which I’ve checked). I haven’t checked any other distros.

I initially sent a bug report to linux-distros@ but was asked to post it to oss-security@ right away, as the issue was already public (and has been for the last 8 months, see the timeline below).

Bug details

« The timer_create syscall implementation in kernel/time/posix-timers.c in the Linux kernel before 4.14.8 doesn’t properly validate the sigevent->sigev_notify field, which leads to out-of-bounds access in the show_timer function (called when /proc/$PID/timers is read). This allows userspace applications to read arbitrary kernel memory (on a kernel built with CONFIG_POSIX_TIMERS and CONFIG_CHECKPOINT_RESTORE).
CVE-2017-18344 — MITRE

Exploitation

I wrote a proof-of-concent exploit that allows:

  1. Reading arbitrary virtual or physical (within the physmap) memory,
  2. Dumping virtual memory that belongs to a particular process by its pid, and
  3. Searching physical memory for a pattern (only the start of each page, which is enough to locate /etc/shadow).

See the comment in the exploit source code for a usage example that shows how to leak /etc/shadow on Ubuntu Xenial 4.13.0-38-generic. The exploit bypasses KASLR and SMEP but doesn’t bypass SMAP.

The bug is that the timer_create syscall doesn’t validate the sigevent->sigev_notify value but uses it to address a global array of strings in show_timer() when /proc/PID/timers is read. By providing a large sigev_notify value, I caused a global-out-of-bounds access that results in nstr[notify & ~SIGEV_THREAD_ID] overflowing 8 bytes and ending up in the userspace. I then mmaped the accessed userspace page and put a contolled address there, which allowed reading arbitrary kernel memory. Since the kernel accessed the userspace there, the exploit attempt would be caught by SMAP.

Since kernel image location is randomized due to KASLR, I couldn’t know in advance which page exactly I should have mmapped. However, since the kernel usually lies within a known address range [0xffffffff81000000, ...), I could mmap a huge chunk of userspace memory that would catch the access wherever the kernel image is placed.

By filling half of the mapped memory with one pointer and the other half with another and reading /proc/PID/timers to see which one got accessed, I could then bisect the exact location in the userspace where I could place a pointer to read kernel data. To limit the RAM usage of the exploit, I chose to use the memfd_create syscall to mmap two distinct chunks of memory over and over.

I could now calculate the kernel location based on this userspace address, but instead I decided to leak the first IDT entry (which is divide_error) and calculate the kernel image address based on that. The location of physmap is also randomized on the kernels built with CONFIG_RANDOMIZE_MEMORY=y, so I read the page_offset_base global variable value to find out the physmap location. It should be possible to find the location of all required kernel symbols heuristically instead of hard coding offsets, but I haven’t explored this.

Now, I could read arbitrary physical memory through physmap. The /etc/shadow content always seems to be page-aligned, so I could search the beginning of each page for something like root:!: and locate it in the physical memory. I could also walk the list of running tasks starting with init_task and dump memory that belongs to a particular task by walking its page tables. Dumping and inspecting memory for gnome-keyring-daemon, for example, allows finding out the user’s password.

Alternative approach

See CVE-2017-18344 analysis & exploitation notes for a detailed write-up of an alternative approach to exploiting this bug.

Timeline

I thought it would be interesting to see when certain Linux distros fixed this bug, as there was no CVE requested and assigned for a while after the bug was fixed.

Initially, I was only looking at Ubuntu 16.04. Here’s the related timeline:

  • Nov 30, 2017 — Bug reported by syzbot
  • Dec 15, 2017 — Mainline fix committed
  • Feb 17, 2018 — Fix backported to the 4.4 stable kernel branch
  • Mar 15, 2018 — Fix added to the Ubuntu Xenial 4.4 kernel branch
  • Jul 25, 2018 — CVE requested
  • Aug 2, 2018 — Notified linux-distros@
  • Aug 2, 2018 — Posted on oss-security@

In this particular case of a somewhat “scary” bug, there was a window of 3.5 months between the bug being reported and the fixing commit reaching the Ubuntu Xenial 4.4 kernel branch. This gives some insight into how much time it usually takes for a fix to travel from upstream through stable into a distro kernel when there’s no CVE. Compared to the 14 days that distros are usually given to fix a security bug reported through linux-distros@, the 3.5 months seem rather long.

Then, I decided to take a look at the CentOS kernel. I was quite surprised to find out that this bug hasn’t been fixed there at all. I was under the impression that most Linux distros either follow stable kernel branches or monitor upstream commits for security-related fixes themselves. It seems that this is not the case. Perhaps this fix was missed because CentOS 7 kernel is based on the 3.10 kernel version, and the 3.10 stable kernel release stopped being supported just before the bug was found in November 2017.

And this is just this single bug. Right now, there are 700+ fixed bugs reported by syzbot and 200+ more that are still not fixed. Almost none of them have CVEs (so if anybody wants to practice requesting CVEs, go for it). There are also ~9000 fixes backported to the 4.4 stable kernel. Some of them are security-relevant and don’t have CVEs. On top of that, apparently, there are ~700 fixes that are missing in the 4.4 stable kernel.

It seems that a CVE is required for a particular security-related fix to end up in distro kernels, but there are no CVEs requested for most of the bugs that are being fixed. So there’s this inconsistency between the Linux kernel community that fixes the bugs without bothering about CVEs and the distros, which require CVEs to apply fixes to their kernels.

Just sharing some thoughts 🙂

💜 Thank you for reading!

🐱 About me

I’m a security researcher and a software engineer focusing on the Linux kernel.

I contributed to several security-related Linux kernel subsystems and tools: KASAN — a fast dynamic bug detector, syzkaller — a production-grade kernel fuzzer, and Arm Memory Tagging Extension — an exploit mitigation.

I also wrote a few Linux kernel exploits for the bugs I found.

Occasionally, I’m having fun with hardware hacking, teaching, and other random stuff.

Follow me @andreyknvl on Twitter or @xairy on LinkedIn for notifications about new articles and talks.