A Deep Dive into Penetration Testing of macOS Applications (Part 3)

February 13, 2024 Julia Minin and Daniel Rabinovich

Deep dive into penetration testing

Introduction

This is the final installment of the blog series “A Deep Dive into Penetration Testing of macOS Applications.” Previously, we discussed the structure of macOS applications and their analysis techniques. Now, we will focus on client-side attacks in macOS applications. In penetration testing, the goal is to identify vulnerabilities in the app. To do that effectively, it’s important to understand how these attacks work. So, let’s dive in and learn more!

TL;DR

We will explore client-side attacks in macOS apps, focusing on three main areas: dylib hijacking, code injection (dylib injection and injection via mach task port) and XPC attacks. Dylib hijacking involves replacing legitimate dynamic link library files with malicious ones. Code injection techniques include injecting code via the DYLD_INSERT_LIBRARIES environment variable and injecting code into another process using the mach task port. XPC attacks target vulnerabilities in the implementation or configuration of XPC services.

Dylib Hijacking

Dylib hijacking is a well-known attack on macOS. Although implemented differently, it is similar to DLL Hijacking on Windows. It occurs when an attacker replaces the legitimate dynamic link library files (dylib) with a malicious one.

A little bit about dyld and dylibs:

To understand this technique, let’s first delve into how applications load dynamic libraries. MacOS, like other operating systems, use a dynamic linker to load dynamic libraries (also known as shared libraries or dylibs).

When an application is launched, the Mach-O Loader loads the application’s code and data into the address space of a new process. It also loads the dynamic linker (also known as dyld) into the process and passes control to it.

The macOS executable files use the Mach-O (Mach Object) format. The dynamic linker examines the application’s Mach-O header to determine its dependencies on dynamic libraries. The Mach-O header contains information such as the list of shared libraries required by the application and their install names. The install name is the complete path to the library, which is stored in the binary’s LC_LOAD_DYLIB  or dylib  LC_ID_DYLIB  load commands (Figure 1).

Loading and linking simple flowFigure 1: Loading and linking simple flow

These load commands are a data structure that provides information to the dynamic loader about how to load and link the binary or library at runtime. The Mach-O format uses multiple types of “load commands,” including the following:

  • LC_LOAD_DYLIB specifies a dynamic library to be loaded at runtime and the dylib must be loaded when the binary or library is executed.
  • LC_LOAD_WEAK_DYLIB specifies a weakly linked dynamic library. If the dylib is not found, the binary or library will still be executed without interruption.
  • LC_REEXPORT_DYLIB specifies a dynamic library to be reexported by the binary or library.

The dynamic loader uses the install names of dependent libraries to locate them in the file system. In some cases, the complete install name of libraries may not be known at the creation time. Developers can use the @<shortcut>/ to specify a full path to dylib relative to the main executable. There are three variables that can be used as a path prefix:

  • @executable_pathThis variable is replaced with the path to the directory containing the main executable for the process, for example, /Applications/Dummy.app/Contents/MacOS.
  • @loader_pathThis variable is replaced with the path to the directory containing the mach-o binary, which contains the load command.
  • @rpath – is a variable that will be replaced with one or more paths specified by the LC_RPATH command at runtime.

Libraries with dynamic (relative to application) paths are also known as “run-path dependent libraries. Developers can use the install_name_tool command to configure the @rpath. For example, the following command sets “@rpath/custom.dylib” as the install name of a library named “custom.dylib”:

 
install_name_tool -id @rpath/custom.dylib custom.dylib

At runtime, an executable provides a list of runtime search paths using the LC_RPATH  load command in the Mach-O binary. The LC_RPATH   is filled by the programmer during the development cycle if needed (Figure 2).

Configuration in Xcode

Figure 2: Runpath Search Paths configuration in Xcode

The dynamic loader performs the following steps when loading an executable:

  • For every dependent library (a separate file containing additional code and resources required by the application), dyld checks if the dependent library is already in the dyld shared cache (a prebuilt cache of system-provided dylibs and frameworks). The dyld shared cache is located at /System/Library/dyld/dyld_shared_cache_*.
  • If the library is not in the shared cache, dyld searches in the location specified in the LC_LOAD_*DYLIB load command.
    • If this location is configured as @rpath, dyld searches for this dependent library in the runtime search path embedded in the executable or dylib itself.
  • If the library is still not found, the dynamic loader searches for dependent libraries in the standard location like: /usr/local/lib, /usr/lib, and /System/Library/Frameworks.
  • In case there is a missing dependency or a mismatch with its versions, and they are not marked as weak (optional), the launch process is terminated.

Let’s Hijack Dylib

In 2015, security researcher Patrick Wardle published a great post about abusing weak and run-path-dependent libraries to dylib hijacking on macOS. We’ll take a closer look at these techniques shortly.

However, it is important to note that since 2015, Apple has added some security features that can prevent this vulnerability. When an executable file or dylib is loaded into memory, AMFI (Apple Mobile File Integrity) checks the code signature to verify that it has been signed by a trusted developer and has not been tampered with or modified since it was signed.

In addition, Library Validation, a hardened runtime feature, prevents a program from loading frameworks, plug-ins or libraries unless they’re either signed by Apple or signed with the same Team ID as the main executable.

However, there are some cases in which dylib hijacking can still be abused. Let’s take a closer look at these cases:

  • The app is not restricted with a hardened runtime or having the com.apple.security.cs.disable-library-validation entitlement.
  • One of the files in the application path [app/Contents] is not properly signed (Figure 3). We can run the codesign –verify –verbose dummy command. If we see an error message in the output, it indicates that the signature is invalid.

Not signed dylib in dummy app

Figure 3: Not signed dylib in dummy app

To check if an application is vulnerable to dylib hijacking, we can use Patrick’s Dylib Hijack Scanner or manually check using the otool command. Using the otool command with the “-l” flag, we can display all load commands of the file.

To identify weak dylibs, we can use the command otool -l dummy | grep LC_LOAD_WEAK_DYLIB -A5 (Figure 4). If the application loads a weak library from a user-writable directory, it’s vulnerable, and we can place or replace a malicious dylib in that directory.

Dummy application weak dylibs

Figure 4: Dummy application weak dylibs

Similarly, we can use the command otool -l dummy | grep LC_LOAD_DYLIB -A5  to check if any dylibs are loaded from @rpath (Figure 5).

Dummy application dylib

Figure 5: Dummy application dylib loaded from @rpath

If any dylibs have @rpath, we need to check if LC_RPATH  points to a writable directory (Figure 6). If multiple LC_RPATH  load commands are present and the run-path-dependent library is not found in the primary run-path search path, we can place a malicious dylib in the primary path, such as @executable_path/../Frameworks in our example.

load command

Figure 6: Rpath load command

In our example, the dylib called “custom.dylib” is loaded from the @rpath (Figure 5). The intended location for the legitimate dylib is @executable_path/../Test directory. However, we also discover the presence of the @executable_path/../Frameworks run-path search before the @executable_path/../Test (Figure 6). This allows us to place our dylib at @executable_path/../Frameworks and hijack the legitimate dylib.

Let’s create a simple dylib from the following code:

#include <stdio.h>
#include <syslog.h>


__attribute__((constructor))
static void customConstructor(int argc)
{
syslog(LOG_ERR, "Dylib loaded successful!");
}
  • To compile this code, we can use the gcc command. Also, we need to set the current_version and compatibility_version flags to be compatible with the versions of the legitimate dylib. The hijacker dylib also needs to export the correct symbols to avoid crashes at loading time. We can do that by using a proxy or ‘re-exporter’ dylib (Figure 7) that creates exports to a legitimate dylib using the -reexport_library  flag.

Reexport load command

Figure 7: Reexport load command

Here is an example of the compilation command:

gcc -dynamiclib -current_version 6.8.0 -compatibility_version 6.8.0 custom.c -o custom.dylib Wl,-reexport_library "legitimate dylib"
  • Setting the install name to match the name specified in the LOAD_*DYLIB command
install_name_tool -id @rpath/custom.dylib custom.dylib
  • Change the install name to the full path of legitimate dylib (resolving @<shortcut>/ path)
install_name_tool -change <old> <new> <target dylib>
  • Finally, copy the dylib to the @executable_path/../Test directory and run the application.

We can check in the Console application if the dylib is loaded (Figure 8).

Console application

Figure 8: Log message in the Console application

In addition to hijacking, there are other methods of running malicious code in a target process called “dylib injection” (also known as “code injection”). Unlike hijacking, which manipulates the dynamic library search mechanism to load a malicious dylib in place of a legitimate one, dylib injects a dylib into a running process to modify its behavior or extend its functionality.

Code Injection

Code injection is a technique used to insert or execute arbitrary code within the context of a running application. Here we will talk about two techniques:

  • Dylib injection
  • Injection via mach task port

What is a dylib injection?

Dylib injection is the technique of injecting code into a macOS application by setting the DYLD_INSERT_LIBRARIES  environment variable to the path of a dynamic library (dylib), which performs execution of the library code within the process of the launched application. This technique is similar to the LD_PRELOAD  method used in Linux.

The injected library is executed with the same privileges as the target process. Important to note that by using this technique, we can only inject code into the application that we launched.

The purpose of DYLD_INSERT_LIBRARIES is to specify a dynamic library that should be loaded into a process at runtime (Figure 9). When a process starts, the dynamic linker looks for the value of the DYLD_INSERT_LIBRARIES environment variable and loads the specified dylibs before loading the program’s dependencies.

definition from dyld manual

Figure 9: DYLD_INSERT_LIBRARIES definition from dyld manual

If System Integrity Protection (SIP) is enabled, this environment variable is ignored when executing binaries protected by SIP.

In our case, SIP enforces restrictions on applications that are built with the Hardened Runtime enabled. By default, certain environment variables related to dynamic linking, including DYLD_INSERT_LIBRARIES, are not allowed.

However, there are exceptions to these restrictions, as demonstrated in part 2 of this series. Some applications may have Runtime Exceptions. There are two exceptions that can lead to dylib injection (Figure 10).

  • Allowing DYLD environment variables
  • Disable Library Validation – in case the injected library isn’t signed with the expected team ID.

We can check that by viewing the entitlements. If the com.apple.security.cs.allow-dyld-environment-variables  and com.apple.security.cs.disable-library-validation  rights are set to true, then these exceptions are enabled, and the application can still be vulnerable to dylib injection.

 Simple flow of SIP

Figure 10:  Simple flow of SIP rules for DYLD_INSERT_LIBRARIES

To demonstrate this technique, we can start by creating a simple dylib using the following code:

#include <stdio.h>
#include <syslog.h>


__attribute__((constructor))
static void customConstructor(int argc, const char **argv)
{
    printf("!\n");
    syslog(LOG_ERR, "Dylib injection successful in %s\n", argv[0]);
}

The next step is to compile it: gcc -dynamiclib inject.c -o inject.dylib

Once we have the compiled dylib, we can proceed by setting the environment variable to our dylib and running the application (Figure 11):

DYLD_INSERT_LIBRARIES=inject.dylib ~/dummy.app/Contents/MacOS/dummy

dylib injection

Figure 11: Successful dylib injection

Afterward, we can verify in the console application that the log has been created (Figure 12).

the Console application

Figure 12: Log message in the Console application

Another Technique: Injection via Mach Task Port

Another technique involves injecting code into the process using the mach task port. The com.apple.security.get-task-allow entitlement allows an app to read or modify the memory of other processes running on the same machine. This can be exploited to inject code into another process.

For example, the following code attempts to obtain the task Mach port for a specified process ID. Mach port is usually used for inter-process communication (IPC) of the process. Note that the code needs to be executed with root privileges.

#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>
#include <unistd.h>


int main() {
   setuid(0);
   pid_t pid = 4570;
   mach_port_t task;
   kern_return_t kr = task_for_pid(mach_task_self(), pid, &task);
   if (kr != KERN_SUCCESS) {
       printf("Failed to get task port for PID %d: %s\n", pid, mach_error_string(kr));
       return 1;
   }
   printf("Got task port 0x%x for PID %d\n", task, pid);
   // use the task port to read and write memory within the target process
   // ...
   return 0;
}

When we tried to execute this program with a dummy PID, we received an AMFI error that our program is not a declared read-only debugger (Figure 13). After some testing, we understood that the program must also have had a com.apple.security.cs.debugger entitlement, allowing the app to attach a debugger to another process.

AMFI error
Figure 13: AMFI error in a Console application

The program above is a simple example of using task_for_pid functionality. If someone wants to try that technique by themselves, you can find useful programs published by Jonathan Levin and Scott Knight about remote thread injection.

Apple has restricted the use of com.apple.security.get-task-allow entitlement. The entitlement is now only granted to apps installed through Xcode or marked as development builds. This means that the entitlement is not available to apps distributed through the App Store or downloaded from third-party sources.

XPC Attacks

Let’s explore another type of attack known as an XPC attack. Although we won’t delve into the specifics of this attack in this blog, we believe it’s important to mention it as understanding XPC attacks can provide valuable insights and help uncover previously unknown security risks.

An XPC attack involves exploiting vulnerabilities in the implementation or configuration of XPC (Cross-Process Communication) services, which can lead to unauthorized access, privilege escalation or even remote code execution. Attackers may attempt to manipulate XPC messages, forge identities or abuse XPC endpoints to gain control over the targeted application.

XPC is an interprocess communication (IPC) mechanism provided by macOS that enable processes to securely communicate with each other using a client-server model.

XPC services are often used to implement privileged helper tools, also known as privileged daemons or helper agents, that provide elevated functionality to the main application or other applications on the system. These tools are designed to perform tasks that require elevated privileges, such as network management, system-level configuration or hardware control. The privileged helper tools service can be found at /Library/PrivilegedHelperTools location.

For more detailed information and references on XPC attacks, you can visit the following blogs:

Summary

We have discussed three types of attacks: dylib hijacking, code injection and XPC attacks. We also examined the restrictions imposed by System Integrity Protection (SIP) and how certain runtime exceptions can still make an application vulnerable to these attacks. We hope that throughout this series, we have provided insights and tools for identifying and exploiting vulnerabilities, and we believe that this information will be helpful for penetration testers and security professionals working with macOS applications.

References:

 

 

 

 

 

Previous Video
Why You Need a Battle-Tested PAM Solution
Why You Need a Battle-Tested PAM Solution

CyberArk experts discuss why you need a battle-tested PAM solution.

Next Video
Safeguarding Digital Entities
Safeguarding Digital Entities

An Exclusive Panel Discussion by CyberArk & CNBC-TV18