Introduction
This article discusses a vulnerability, CVE-2024-54471, that was patched as part of the Apple security releases: macOS Sequoia 15.1, macOS Sonoma 14.7.1, and macOS Ventura 13.7.1 (all released on October 28th, 2024). If you use a macOS device and are not on one of these updated versions: update now!
This article is going to start with a lot of setup. I need to lay out some definitions and explain several concepts before jumping into the actual exploitation details. If you want, you can skip to the juicy exploitation info. For everyone else, thank’s for coming along for the ride! Let me start by explaining inter-process communication on macOS.
What is a Kernel?
In an operating system, the code responsible for communicating with hardware and presenting a multi-tasking model to the applications (among many other things) is called the kernel. When code is executed in the kernel, it is said to be in kernel space, while code that is executed outside of the kernel (i.e. most applications) is said to be in user space. The separation between user space and kernel space is often an important security barrier.
The kernel for macOS (and pretty much all Apple OS’s) is known as XNU. XNU is a hybrid kernel, containing parts of the BSD kernel and its variants, as well as a (now heavily-modified) variant of the Mach kernel. Interestingly, it appears that Apple is one of the only organizations out there that is still actively maintaining a Mach kernel variant. While the Free Software Foundation’s GNU Hurd kernel is based on their own variant called GNU Mach, development on the GNU Hurd kernel project is very minimal today.
A (Not-So)-Brief History of Mach
The history of the Mach kernel is deeply entangled with the Unix wars of the 80’s and 90’s, with multiple organizations and groups working on it and using it, often in overlapping time periods. As such, there is not really a clean well-delineated timeline from the start of Mach to now. Additionally, certain historical notes don’t have easily-found primary sources, but are repeated often enough in secondary and tertiary sources to be considered trustworthy. I have done my best to fact-check this section while also linking to primary sources (or as close as I could get) where important.
Mach started life as an operating systems research project of the Carnegie Mellon University School of Computer Science from 1985 to 1994.
In 1989, the Open Software Foundation (now The Open Group) announced it would be using Mach in their upcoming OSF/1 operating system. Unfortunately, I have been unable to find a direct link to this announcement, but I did find a few sentences of coverage of the announcement in an archive of a late-December issue of a online magazine called CPU NewsWire Online Magazine?? (almost immediately after some coverage of late-1980’s ransomware). The coverage of the announcement reads:
Cambridge, MA The Open Systems Foundation, an organization funded by ------------- several Unix vendors to develop a new Unix standard, has announced that they may use the Mach OS (currently used in the NeXT System) as the foundation for OSF/1, their new systems software platform, instead of using A/IX, IBM's version of Unix. Mach provides better data security measures, inherent support for multiprocessing, and compatibility with Berkeley Unix. But given that IBM's support of the OSF was partly based on the OSF's use of A/IX, and that much of the OSF's credibility depends on OSF/1 shipping by the announced date of July 1990....
It is unclear if the use of Open Systems Foundation
is an error, or simply another name the OSF was known by at the time. I’m also not sure why the last sentence ends the way that it does, as despite the ellipses, it does appear to be the end of the coverage. More pertinent to the current topic, though, is the reference to the NeXT System.
This is likely referring to NeXTSTEP, the operating system from NeXT (the company that Steve Jobs founded after originally being ousted from Apple). This is the link that would ultimately bring Mach into what is now macOS.
To say that NeXTSTEP simply used Mach would not tell the whole story. One of the original developers of Mach (and longtime friend of Steve Jobs) Avie Tevanian worked with Steve as an executive at NeXT. When NeXT was later acquired by Apple, both Steve and Avie were given executive positions at their new parent company. Their NeXTSTEP operating system was developed into Darwin the operating system basis for the next commercial release of Apple’s Macintosh operating system: Mac OS X (now macOS).
Why Mach?
As mentioned previously, Mach was developed during the Unix wars of the 80’s and 90’s. Operating system vendors were all competing with each other to provide what they saw as the best way to design and use a Unix system. So what was it about Mach’s Unix that was so special? What made it stand out amongst all the others? Really, it was the fact that it wasn’t Unix… at least not completely.
In a paper submitted to the USENIX 1986 Summer Technical Conference & Exhibition (one of the earliest sources I could find), the developers laid out their vision and reasoning for creating Mach. They describe a landscape where inter-process communication had become frustratingly complex in Unix. What had started with the lowly file descriptor (a single handle that could allow a process to read, write, or seek) had turned into a confusing mess of streams, sockets, shared memory, and more. In an effort to simplify, they designed a system around Unix based on four basic abstractions.
The Architecture of Mach
The Four Abstractions
The four basic abstractions of Mach, as explain by the 1986 USENIX paper, are as follows (emphasis theirs):
- A task is an execution environment in which threads may run. It is the basic unit of resource allocation. A task includes a paged virtual address space and protected access to system resources (such as processors, port capabilities and virtual memory). The UNIX notion of a process is, in Mach, represented by a task with a single thread of control.
- A thread is the basic unit of CPU utilization. It is roughly equivalent to an independent program counter operating within a task. All threads within a task share access to all task resources.
- A port is a communication channel — logically a queue for messages protected by the kernel. Ports are the reference objects of the Mach design. They are used in much the same way that object references could be used in an object oriented system. Send and Receive are the fundamental primitive operations on ports.
- A message is a typed collection of data objects used in communication between threads. Messages may be of any size and may contain pointers and typed capabilities for ports.
Things certainly have changed in the several decades since this paper’s release. For example, Mach threads actually pre-date POSIX threads by nearly a decade (this fact has lead to difficulty when attempting shellcode injection on macOS). However, despite decades of other software changes, these four abstractions still underpin what Mach is today in modern macOS (and all other XNU-based Apple OS’s).
Tasks, Ports, and Port Rights
Ports in Mach are interesting as the queues themselves really only exist in kernel space. Ports are exposed to user space as integers, similar to file descriptors. Except not really. What is actually exposed are port rights, with each task having a port name space
containing named port rights (the integers themselves being referred to as the names
of these rights). In some cases, two different rights to the same port may have the same name
and are thus exposed to the owning task in user space with the same integer.
Despite all this, in what appears to be an effort to use similarly-named API’s in both kernel space and user space, these named port rights
are often referred to simply as ports
in user space, despite that being technically incorrect. It can all be very confusing, and only really starts to make sense after practice and immersing one’s self in the world of Mach.
Regarding rights themselves, the two main types of rights are send rights and receive rights. The kernel will allow multiple tasks to hold a send right to a port, but will only allow a single task to hold a receive right. This essentially creates a client-server model with a single server task receiving messages from multiple client tasks. As alluded to by the Mach paper above, a task is basically synonymous with a process. However, there is one special task in Mach, the kernel itself (more on that later).
The Structure of a Message
Conceptually, each Mach message contains, in order:
- A header,
- an optional body of descriptors,
- an arbitrary payload of bytes, and
- a kernel-appended trailer (only on received messages).
Descriptors allow tasks to share out-of-line memory and even port rights with each other, with the kernel mapping addresses and manipulating port name spaces as necessary. The data in the arbitrary payload, on the other hand, is transferred as-is from the sending task to the receiving task.
How Tasks Get Send Rights
One might wonder how a task gets send rights to start with. macOS includes a bootstrap server, a Mach task that holds the receive right to a port to which every task holds a send right. The bootstrap server exposes the concept of Mach services, which are Mach servers registered with the bootstrap port with specific string names. Clients can ask the bootstrap server for send rights to these Mach services by name.
The Mach Interface Generator (MIG)
Introduction
While Mach messages seem simple on their face, in practice they can involve a lot of manual memory management, which can be prone to issues. Perhaps in an effort to combat this, the authors of Mach included MIG, which provides a way to create functional interfaces around the sending and receiving of Mach messages.
MIG is two parts: a pseudo-C IDL (interface definition language) and a compiler that takes in an IDL file and outputs multiple C files:
- a C source file to run on clients,
- a C source file to run on the server, and
- a C header file to use for both.
These files define functions that handle the messages for the clients and server, exposing an RPC-style interface wherein a client needs only to call a function on its end and a server needs only to implement that function on its end. This makes for a much more memory-safe messaging experience.
The Technical Details
On a technical level, MIG is really just a wrapper around Mach messages. Each function is referred to as a routine, with a collection of routines being referred to as a subsystem. Each subsystem has a subsystem number
of off which the rout
6 Comments
janandonly
A very interesting article this is. Never knew there was so much lore in the making of the Mach and Darwin kernels .
junon
Well written article. It reminds me of the zero day that Apple tried to cover up somewhat – the "empty password tried twice" root login bypass. This was ca. 2017 or so, maybe 2018.
You were able to type in an administrator username in any root sign in box (e.g. in the settings panel via the padlock icon) with an empty password. Hitting the Sign In button the first time told you that the password was incorrect. Dismissing that alert box and hitting sign in a second time signed you in as that user.
We were able to reproduce it 100% of the time day-of, and of course was patched pretty shortly after making the rounds on social media. Still seems like a massive oversight though.
Seems there's still some cruft around the auth mechanisms in Mac. Interesting to see the port system mentioned – it's not a well known fact of Mach kernels.
turnsout
At this point it feels like Mach is a reliable source of bugs in macOS. I know Apple is working hard to lock it all down, but is there any path to shifting away from Mach completely?
biofunsf
Does the author provide the actual PoC code anywhere? I want to do some testing for mitigations. I see the example code but it seems incomplete.
Realistically what are the risks?
unit149
[dead]
nmgycombinator
A minor correction was made to the article:
Entitlement checks are not in the Mach layer of the kernel.
https://github.com/nmggithub/wts/commit/2bdce1c0c76c7adc360e…
Just a one word change, fixing a factual inaccuracy when talking about how XNU works.