In this post, I will describe how I used CodeQL 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. The technology that powers the analysis is CodeQL.
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 dtrace_dif_emulate. Validation is done by dtrace_difo_validate.
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 CodeQL 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: dtrace_dif_emulate → dtrace_dif_variable → dtrace_getarg. An attacker can use the DIF_OP_LDGA
instruction to call dtrace_dif_variable 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 dtrace_helper_slurp.
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 CodeQL, 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 November 01, 2017