While assessing the potential impact of the latest BLASTPASS Zero-Click, Zero-Day Exploit on our Family of Apps, we discovered a feature in ImageIO that moves image parsing to an out-of-process sandbox. This feature mitigates the effects of vulnerabilities related to image parsing on macOS similar to BLASTPASS. App developers can enable this feature on macOS by setting the IIOEnableOOP preference true. Anyone can enable this feature by setting the environment variable IIOEnableOOP=YES before launching an app. It is not available on iOS.

Background

In light of the BLASTPASS 0-day being exploited in the wild, we sought to understand how image parsing was performed on Apple devices.

Apple provides the CGImage* set of APIs that enable developers to conveniently work with various image formats. Although these APIs have a prefix indicating the CoreGraphics framework, the underlying parser code resides in ImageIO.framework. Developers may not use CoreGraphics APIs directly and may instead use UIKit’s UIImage class which wraps CGImage* APIs and can be used to easily render an image in a UIKit application.

Information about Apple’s image parsing practices is scarce, with the exception of a 2020 article by Project Zero which mentioned that some formats like PSD were parsed out-of-process, while the majority were done in-process. Out-of-process sandboxing of media parsers raise attacker costs by requiring a sandbox escape before exploit code can gain access to app data. This is desirable from a defense perspective.

We wanted to understand which media formats were sandboxed out-of-process, if any.

ImageIOXPCService

According to the Project Zero post, out-of-process image parsing is handled by the ImageIOXPCService service /System/Library/Frameworks/ImageIO.framework/Versions/A/XPCServices/ImageIOXPCService.xpc/Contents/MacOS/ImageIOXPCService.

Examining its exports, we discovered that it provides the same CGImage* APIs as ImageIO. A comparison of the decompiled code for these APIs revealed that they are very similar.

Additionally, both ImageIO and XPCService import libraries like libPNG, libJPEG, etc., suggesting that they share the same capabilities to parse these formats. The list of imports for ImageIO (left) and ImageIOXPCService (right) is provided below:

"Symbol trees showing ImageIO exports on the left and ImageIOXPCService on the right, listings are identical"

The sandbox config for ImageIOXPCService is located at /System/Library/Sandbox/Profiles/com.apple.ImageIOXPCService.sb.

Tracing XPC calls using XPoCe, we didn’t see any calls to com.apple.imageioxpcservice, but we did see telemetry from all the calls to ImageIO APIs, hinting that image parsing is being done in-process. We also did not see the ImageIOXPCService process show up in Activity Monitor, further confirming that no out-of-process was happening.

Despite signs that ImageIO is capable of out-of-process parsing, all of this pointed towards in-process execution being the default.

Debugging

At this point we needed clarity and reached for a debugger. There were a couple APIs that were good starting points:

  • CGImageSourceCreateWithData saves the reference to an image buffer, parses the header, and initializes a reader plugin based on the format of the image. The reader plugin is a format-specific plugin that knows how to handle each image format.
  • CGImageSourceCreateImageAtIndex lazily parses the image buffer when pixel data is accessed.

Stepping through CGImageSourceCreateWithData we see reader initialization determining the image format.

The following code snippet shows IIO_ReaderHandler::readerForBytes initializing an XPC client before deciding whether to use an XPC server (out-of-process) or not (in-process) for parsing image header data. This pattern repeats when reader plugins prepare to parse image contents.

"Reader plugin initializing an XPC client before deciding whether to use the XPC server or do parsing in-proc"

In-Proc or Out-of-Proc?

We have determined that ImageIO is capable of parsing media both in-process and out-of-process. However, the question remains: how does the library decide where to parse an image? There exists a set of functions beginning with “useServerFor*” which determines whether the library will use a remote XPC server (out-of-process) for a specific task.

"Listing of various useServer* functions that decide whether the sandbox is enabled for different parsing stages"

Let’s examine IIOXPCClient::useServerForIdentification, which determines whether header parsing will be performed locally or in a sandbox. Other useServerFor* functions are similar:

"Ghidra decompilation of macOS' useServerForIdentification method, which is similar to the other useServer* funcs"

In our experience, param_1 is always 0xffffffff, and the deciding factor is the byte at an offset of 0x24. By default, the byte at this offset was set, which caused useServerForIdentification to return false and made all parsing in-process on macOS.

Forcing Out-of-Proc

Modifying this byte in the debugger forced out-of-process parsing, causing ImageIOXPCService to appear in the Activity Monitor. Success!

"macOS' Activity Monitor showing the ImageIOXPCService process running, proving ImageIO is sandboxed"

Tracing what initialized that byte, we discovered IIOXPCClient_block_invoke:

"The block_invoke function's decompilation, showing where the IOPreference is fetched"

It sets the value at offset 0x24 into IIOXPCClientObject based on the IIOEnableOOP preference. Let’s take a look at how it works:

"IOPreferences GetBoolean internals, decompilation showing how it first looks for an env var then pulls from CFPreferences"

First, it checks if an environment variable called IIOEnableOOP is set. If so, this takes precedence over any other preference. We can test our application by setting IIOEnableOOP=YES to verify that parsing is now occurring out-of-process.

If the environment variable is not set, it falls back to reading a CFPreferences value from an app-specific key-value preference store that our application can write to.

To test this, we can call the following to set that app-specific preference:

CFPreferencesSetAppValue(CFSTR("IIOEnableOOP"), kCFBooleanTrue, kCFPreferencesCurrentApplication);

And it works! Now our application is doing OOP sandboxed parsing for all images that use ImageIO APIs. Note that this preference is persistent and will be saved between different application runs.

Does this work on iOS?

Unfortunately, this does not work on iOS. The ImageIOXPCService binary is not present on iOS, and we can see that the useServerForIdentification function body is #ifdef’d blank. The IIOEnableOOP preference has no effect either.

"Decompilation of iOS' useServer* function showing it is an empty stub, not yet implemented"

Update as of Nov 2023 (iOS 17):

As of iOS 17 we see this feature show up in code, however, it is gated behind IIO_OSAppleInternalBuild().

"Decompilation of iOS 17 showing implementation gated behind apple internal build check"

Conclusion

While this is an undocumented feature, setting the IIOEnableOOP preference true appears to correctly sandbox ImageIO on macOS and falls back gracefully to in-process parsing on iOS.

Examining the image parsing code in ImageIOXPCService, it resembles the code found in ImageIO itself, and it imports the same libraries used for image parsing

From our testing on macOS 13.5.1, we have not encountered issues with out-of-process parsing. We have not measured the performance impact, but if your application is not heavily reliant on images, enabling this feature is a meaningful security improvement.