Apple's XNU Kernel: Finding a memory exposure vulnerability with QL (CVE-2017-13782)

November 01, 2017

Category

Technical Difficulty

Reading time

In this post, I will describe how I used QL to find a security vulnerability in Apple's macOS operating system kernel. The vulnerability gives a local attacker the ability to read any memory address within a 32GB range of the kernel's address space. The proof-of-concept exploit which I sent to Apple uses the vulnerability to crash the kernel by reading from progressively higher addresses until it reads from an illegal address. However, an attacker could also use the vulnerability to read sensitive data from the kernel's address space, without triggering any visible side-effects which might alert the user to the fact that their Mac has been compromised.

Severity and mitigation

Apple released a patch on October 31, 2017 for macOS versions High Sierra (10.13), Sierra (10.12), and El Capitan (10.11). You are strongly advised to install this patch at your earliest convenience.

Note that all versions of Apple macOS Yosemite (including 10.10.5, which was released August 2015) are also affected by this weakness. Apple tends not to formally announce the 'end of life' of their products, but the company stopped providing security updates for macOS Yosemite around September of this year. Although this vulnerability was reported by me back in July 2017 and subsequently confirmed by Apple's Product Security team in August, Apple decided not to release a patch for macOS Yosemite.

The severity of the vulnerability is mitigated by the fact that it can only be triggered by DTrace or Instruments (a GUI wrapper around DTrace), which require root privileges to run. However, as I will explain in this post, it is possible for a malicious application which does not have root privileges to register a malicious "DTrace helper" with the kernel. The malicious DTrace helper will be triggered if anyone who has root privileges tries to use DTrace to get a stack trace of the malicious application. This means that an attacker might be able to use a social engineering strategy to convince a systems administrator to unwittingly trigger a malicious DTrace helper.

About DTrace and finding this vulnerability

To quote dtrace.org:

DTrace is a performance analysis and troubleshooting tool that is included by default with various operating systems, including Solaris, Mac OS X and FreeBSD.

I first became aware of DTrace when I analyzed Apple's open source XNU kernel, which has been used for all versions of macOS since 1996. At the time of writing this post, the XNU source code is not analyzed by LGTM.com, because compiling the code requires a Mac build environment. However, the technology that powers the analysis on LGTM.com can also be run locally, which is what I did. For more information: see Semmle.com.

Looking at the analysis results for the XNU kernel, I noticed that there are a lot of alerts in dtrace.c. Many of these results are of a low severity that don't pose a security risk (e.g., unused variables). But when I took a closer look, I noticed that this file contains an interpreter. Needless to say, if you are going to put an interpreter in the kernel then you need to be exceptionally careful to avoid creating any security holes. So it is a bit worrying that this file has so many issues, even if they are only minor code quality issues.

DTrace is used to trace things like system calls and networking events. DTrace scripts are compiled by user-space tools into bytecode programs which are registered with the kernel so that they can be executed when such events occur. The idea is that the bytecode is validated at registration time, so that it can be executed with minimal overhead when an event occurs. The validation ensures (in theory) that the bytecode cannot do anything malicious when it is executed.

DTrace uses its own custom bytecode format. The main interpreter loop for the bytecode is in the function dtracedifemulate. Validation is done by dtracedifovalidate.

To register a DTrace bytecode program with the kernel you have to open the file /dev/dtrace and call ioctl on it. Only root has permission to open /dev/dtrace, so this is how unprivileged users are prevented from accessing DTrace. However, there is a second file named /dev/dtracehelper which any user can open. This second interface is used to register "DTrace helpers". The main motivation for the DTrace helper feature is to enable JIT compilers to produce better stack traces. For an excellent explanation of DTrace helpers, see this blog post by David Pacheco. Ironically, the ustack feature doesn't actually work on macOS! But it works well enough for an attacker to plant a malicious DTrace helper in the kernel.

Using QL to find a vulnerability

DTrace bytecode uses 8 virtual registers, which are stored in an array named regs. The instruction set includes a full range of arithmetic operators such as addition, multiplication, and bitwise operators, so the bytecode program has complete control over the values stored in regs. It is therefore important to make sure that the code never uses a register value to do something dangerous, such as indexing an array, unless it has first checked that the value in the register is safe.

First of all, we can define a QL class to find all the register accesses:

class RegisterAccess extends ArrayExpr {
  RegisterAccess() {
    exists (LocalScopeVariable regs, Function emulate |
      regs.getName() = "regs" and
      emulate.getName() = "dtrace_dif_emulate" and
      regs.getFunction() = emulate and
      this.getArrayBase() = regs.getAnAccess())
  }
}

This definition says that a RegisterAccess is an ArrayExpr that accesses an element of the array named regs in the function named dtrace_dif_emulate. For example, this use of regs[rd] is a typical instance of a RegisterAccess.

Secondly, we can define a QL class for potentially dangerous uses, such as indexing an array or deferencing a pointer:

class PointerUse extends Expr {
  PointerUse() {
    exists (ArrayExpr ae | this = ae.getArrayOffset()) or
    exists (PointerDereferenceExpr deref | this = deref.getOperand()) or
    exists (PointerAddExpr add | this = add.getAnOperand())
  }
}

We are interested to know if there are any dataflow paths from a RegisterAccess to a PointerUse. We can use the DataFlow library for this. Below is the complete query:

/**
 * @name DTrace unsafe index
 * @description DTrace registers are user-controllable, so they must not be
 *              used to index an array without a bounds check.
 * @kind path-problem
 * @problem.severity warning
 * @id apple-xnu/cpp/dtrace-unsafe-index
 */

import cpp
import semmle.code.cpp.dataflow.DataFlow
import DataFlow::PathGraph

class RegisterAccess extends ArrayExpr {
  RegisterAccess() {
    exists (LocalScopeVariable regs, Function emulate
    | regs.getName() = "regs" and
      emulate.getName() = "dtrace_dif_emulate" and
      regs.getFunction() = emulate and
      this.getArrayBase() = regs.getAnAccess())
  }
}

class PointerUse extends Expr {
  PointerUse() {
    exists (ArrayExpr ae | this = ae.getArrayOffset()) or
    exists (PointerDereferenceExpr deref | this = deref.getOperand()) or
    exists (PointerAddExpr add | this = add.getAnOperand())
  }
}

class DTraceUnsafeIndexConfig extends DataFlow::Configuration {
  DTraceUnsafeIndexConfig() {
    this = "DTraceUnsafeIndexConfig"
  }

  override predicate isSource(DataFlow::Node node) {
    node.asExpr() instanceof RegisterAccess
  }

  override predicate isSink(DataFlow::Node node) {
    node.asExpr() instanceof PointerUse
  }
}

from DTraceUnsafeIndexConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink, source, sink, "DTrace unsafe index"

This query produces 16 results, one of which is this pointer dereference which does not have a bounds check. The other 15 results are uninteresting. If we wanted to, we could further refine the query to reduce the number of false positives. For example, this result is a false positive, because the call to dtrace_canstore on line 5699 is a bounds check.

The vulnerability

The bug is reachable via the following call path: dtracedifemulatedtracedifvariabledtrace_getarg. An attacker can use the DIF_OP_LDGA instruction to call dtracedifvariable with complete control over the values of the v and ndx parameters. If v == DIF_VAR_ARGS (that is, v == 0) then line 3197 will call dtrace_getarg with the value of the arg parameter completely under the control of the attacker. This means that the pointer deference on line 817 will load a uint64_t from an address controlled by the attacker and return it as the result of the instruction. Since arg is a 32-bit int and the indexing operation gets scaled by sizeof(uint64_t), this means that the attacker has access to a 32GB range of memory centered around the address stored in stack, which is somewhere in the kernel's address space.

Proof-of-concept exploit

As I explained earlier, there are two interfaces to DTrace: /dev/dtrace and /dev/dtracehelper. The former interface requires root privileges, but the latter does not, so I decided to create a proof-of-concept exploit that uses the latter. To create a proof-of-concept, I needed to figure out how to create a valid DTrace program. DTrace uses a binary format similar to ELF, with multiple sections for things like the string table, integer table, and code. I did not manage to find any documentation on this format, so I reverse engineered it from the parsing code in dtracehelperslurp.

You can download the source code of the proof-of-concept here. The interesting bit is in the function mkprog. Most of the rest of the code is the boilerplate that is needed to create a correctly formatted DTrace program.

To run the POC, first compile and run the above program:

cc -o cve cve-2017-13782-poc.c
./cve

Then, from another terminal, run the following command:

sudo dtrace -n 'profile-97/execname == "cve"/{ jstack(); }'

This DTrace command attempts to get a stack trace for the cve program, thereby triggering the malicious DTrace helper that was registered by cve. The effect of this proof-of-concept is to hard-reboot the machine. Note that there is nothing malicious in the above DTrace command. It is just a simplified version of a typical DTrace command. For example, the instructions in the README file for node-stackvis use a very similar DTrace command to profile a program. Any DTrace command that uses jstack or ustack on cve will trigger the bug.

Conclusion

Using QL, I have found a serious vulnerability in Apple's macOS operating system kernel. First, the results from the queries in our default suite led me to take a closer look at dtrace.c. Then I used a more targeted query to find a vulnerable memory access which could be exploited by an attacker.

Vendor response timeline

  • July 10, 2017: privately reported this vulnerability to Apple
  • August 18, 2017: vulnerability confirmed by product-security@apple.com
  • October 28, 2017: Apple confirmed having assigned CVE-2017-13782 to this vulnerability
  • October 31, 2017: patch released by Apple

Note: Post originally published on LGTM.com on 11/01/2017

Join us in securing the software that runs the world!

Enter your email address below to stay up-to-date with Semmle news, security announcements and product updates.

Loading...