Skip to main content

During a client engagement, I performed a security review of a macOS software product and it became apparent to me that privileged operations were being performed from the perspective of a low-privileged OS process. Often in these kinds of scenarios, inter-process communication (IPC) is used to facilitate communications between high- and low-privileged software components. Incorrect use of platform APIs could lead to these IPC mechanisms being inappropriately restricted, which can lead to privilege escalation vulnerabilities, as malicious processes use these interfaces for themselves to gain privileges they otherwise would not be able to.

In this blog post we will:

  • Learn about IPC via distributed objects
  • Learn how to identify usage of distributed objects
  • Create a working PoC for exploiting MacVim versions prior to version 178 – i.e. exploiting CVE-2023-41036

The next section will briefly introduce the concept of distributed objects.

Distributed Objects

Inter-process communication can be achieved in a variety of different ways, such as: files, sockets and pipes; however, on macOS, Apple historically provided a framework known as Distributed Objects, that could be used by programs to “vend” arbitrary classes across the OS process (or even network) boundary. It is worth noting that if we try to create a new XCode project in 2024 and use these interfaces, they will fail fast and the attempted connection to the vended object will never be created.

To avoid this issue, we can start by cloning a repository that is set up for development with distributed objects:

git clone https://github.com/JeremyAgost/IPC-Demo --depth=1

Thanks Jeremy! We can now have a look at an example server implementation.

Server Example

First, we create the class to be vended. For the example code, our vended class, ServiceObject, has one method that can be used, getInfoCommand, which takes in an integer parameter. If the number happens to be equal to 1024, we return a string.

Next we create an AppDelegate, which will create our vended object.


In this file, the actionStart method is responsible for creating and exposing the ServiceObject. Instead of binding to a port, this vended object is bound to a connection name, ie com.tekserve.IPCServ.TestService.

As we will shortly see, any process which is aware of the connection name, can use the ServiceObject by specifying the connection name in its connection attempt.

No server is complete without a corresponding client, so let’s forge ahead and see how distributed object client code functions.

Client Example

The client is very simple; we import the header of the ServiceObject so we are able to refer to it in the client code.

On line 17, we create a ServiceObject to connect to the listening server. When doing this, we only need to specify the name of the connection, i.e. com.tekserve.IPCServ.TestService. If the connection is established, we get back a pointer that we can use to invoke functions on the remote interface. To invoke the getInfoCommand, we could make the following call:


With the understanding that distributed objects have no inherent authentication mechanism for attempted invocations of IPC calls, we can start looking for vulnerable software that make use of these objects.

Indicators That NSConnection Is In Use

From the previous code examples, we can identify information that could be used to find other software which expose sensitive functionality as a vended object:

  • A program that hosts a vended object will make use of [NSConnection serviceConnectionWithName:rootObject:]
    • There are some variants of this, as developers don’t have to set the rootObject immediately using this method; they could call its setter at a later stage instead, as seen below:

  • A program that uses a vended interface will make use of [NSConnection rootProxyForConnectionWithRegisteredName:host:].

It may not immediately be obvious, but once we have identified usage of distributed objects in a macOS program, the only information we need to create a malicious client is the definition of the vended object and connection name. The definition can be found in the corresponding header file:


In our example program, we had access to the source code, but an attacker may need to reverse engineer this interface. There are multiple ways to do this using a combination of static and dynamic approaches.

Static approaches to reverse-engineering a Distributed Object interface

Firstly, let’s explore a static approach and open up the server binary in radare2 and analyze it using r2 -A ./server; we can then issue the command icc to get a list of classes in the binary. This will print lots of information, but it should look something like this:


These results can be filtered to only show the class in question using icc ServiceObject, since we already know that it’s using NSConnection; the output gives us a good idea of what this class does:


Alternatively, if you do not have access to the application source, it is possible to use Hopper to achieve a similar result by executing the following steps:

  • Open the binary in Hopper by clicking on File -> Read Executable To Disassemble
  • Wait for analysis to complete
  • Click on File -> Export Objective-C Header File

This will export a header file, such as the one shown below:


At this point, we can use the source code from the example client and replace the implementation in the header with our reverse-engineered one; then we need only connect to the correct service name, at which point we can execute any of the vended methods.

Digging deeper with dynamic reverse-engineering

From our static analysis, we know that the target service object exposes a few methods, but we don’t know the types or potential values of parameters. While we could continue static analysis until we figure this out, we could also switch to dynamic instrumentation to trace the method and inspect its parameters. In order to do this, we must first disable macOS’ System Integrity Protection temporarily, by booting into the OS recovery terminal and executing csrutil disable.

Frida is a dynamic instrumentation toolkit and one of my favourite pentesting tools. It allows us to modify any program while it is running, by simply writing some JavaScript. It also supports a REPL, which provides quick feedback on changes that we are making to the process.

To get started analysing our target binary with Frida, we have to spawn or attach to our server; in this example, we will spawn the server via frida -f $PATH_TO_BIN. Let’s start by verifying whether the vended object is loaded into memory:

If the service object is in use, the output for the last line should look similar to the following terminal output. Note that if there are multiple instances of the same vended class, with different instances having different service names, multiple values would be returned by this command.


As we can see, the class can be found on the process’ heap, which confirms that it is, in-fact, being used. Now, let’s see if we can create a simple hook to indicate when our vended object is called. This hook should also print the value of arguments, so that we can determine how to use the interface.


Now that our hook is in place, we can run the client application, and we should see the following output in the REPL:


Having gone through this example, we now have enough context to investigate some actual software! In the next section, we will gradually create a Proof of Concept exploit for an unpatched version of MacVim (i.e. a version before 178).

MacVim

The first point of action is to identify a class that is vended, by searching for uses of NSConnection in the codebase. We can do this by cloning the repository and using grep:

We can then inspect the corresponding header file ./src/MacVim/MMAppController.h. While usage of the deprecated interface should raise some eyebrows, it would be of greater concern if the vended methods in the codebase exposed some dangerous functionality.

Try to identify some suitable exploitation candidates from the redacted list below:


The most obvious candidate here is:


Looking at the implementation of this method, in ./src/MacVim/MMAppController.m, it is clear that it executes a binary given a path and arguments, and returns the result as a string:


This raises some questions:

  • Can we connect to this interface?
    • Yes, but only locally. In order to exploit this issue, you would need the user to execute a malicious binary on your behalf, or already have some kind of authenticated access to run your custom client code on the system.
  • Does the connecting process inherit the same rights as the hosting process?
    • Yes. As pointed out by one of the MacVim maintainers, this could allow a malicious process to read the contents of directories it would otherwise not have had access to, such as ~/Documents for other users.
  • Can we escalate to root?
    • Yes, but someone would have to first run MacVim as root.
  • Are there any other vulnerable interfaces?
    • Potentially; however, for this PoC, we focus solely on the executeInLoginShell method.

Since we have access to the codebase and can simply copy the implementation as part of building a custom client, the only other thing we need is the service connection name. There are multiple ways of finding this value, the easiest of which is to use Frida and spawn MacVim using the following script:


After doing this, we can execute the command frida -f $MACVIM_PATH -l hook.js and the output of the hook will reveal the service connection name, as shown below:


The next section makes use of all of the information gathered thus far, to create a Proof of Concept exploit.

Putting It All Together

After we have the basic interface definition and service name, we can create a PoC. Add the following code to the cloned Objective-C project:


Then, we need to specify the service name (/Applications/MacVim.app-connection) and perform our malicious action, as shown below:


Once executed, the program will run a binary located at /tmp/stager, with the same OS permissions as MacVim. In my case, I made this binary a Meterpreter payload, and successful exploitation would result in an active Meterpreter session, as shown below:


Alternatively, as mentioned by Yee Cheng Chin, it would be possible for an attacker to access files in directories that MacVim has permission to read such as ~/Documents.

Conclusion

IPC mechanisms are used heavily within macOS to facilitate communication between multiple processes. Specifically, the distributed objects IPC mechanism has been deprecated since macOS 10.0, Cheetah, which was released in 2001 according to Apple’s documentation. As per the same documentation, developers making use of this specific implementation should migrate to XPC to prevent scenarios such as the above.

Two key take-aways from this research were:

  • Even heavily deprecated APIs still see use in modern software, with some going unnoticed for years. MacVim is one such example, where inspection of the commit history shows that the MMAppController.m file was initially imported into the project in 2007.
  • Deprecation warnings should be taken very seriously and vendors should clearly state when an API is deprecated for security reasons.
    • The screenshot below shows clear warnings that the API is deprecated, but provides no specific reasoning as to why.


If you are interested in reading up more on distributed objects and macOS IPC, Ian Beer from Google’s Project Zero has done some phenomenal work, some of which is referenced below. Finally, I’d like to thank the MacVim maintainers for responding as quickly as they did, and the hard work they do on the project.

Reference List