USD: One File Format, Many Vulnerabilities
This post covers my project of identifying a series of vulnerabilities (CVE-2020-9878, CVE-2020-9880, CVE-2020-9881, CVE-2020-9882, CVE-2020-9940, CVE-2020-9985) in the processing of USD (Universal Scene Description) files within Apple’s iOS operating system. It also covers the high-level approach to exploit one of these vulnerabilities.
Although this project was being done in 2020, the methodology and techniques used are still relevant today. I’m sure it’s possible to copy the general approach and apply it to other areas to find interesting vulnerabilities.
Path to the Vulnerability
In early 2020, I challenged myself to find and exploit a vulnerability in Apple’s iOS operating system, the operating system that runs on all iPhones. My goal was to perform a more comprehensive, in-depth security assessment than I usually have the opportunity to do. I was also encouraged by the fact that Apple had opened its bug bounty program in December 2019, offering a potential reward for my work (spoiler: it worked out very well).
At the time of conducting the project, I did not expect to find an easy-to-find vulnerability in iOS. But as it turned out, there was such a “low-hanging fruit” in iOS 13, a fact that surprised me. I became aware of it when I came across a certain page in Apple’s documentation:
|Hint about the usdz file format in the Apple documentation
When I first saw this page, I was immediately excited! USDZ - I had never heard of it before, and maybe many others hadn’t either. I saw a chance that this could be an exotic file format that might not have been considered much from a security perspective. A quick search revealed that Apple had built support for this file format into iOS 12, and that it is a format for displaying 3D models.
I also found out that the display of USDZ files can be triggered by many different apps, even from Safari - just visiting a website is enough! The display is always handled by the ASVAssetViewer program. This program has some interesting permissions:
$ launchctl procinfo <pid-of-ASVAssetViewer>
"com.apple.private.tcc.allow" = (
These entitlements are special because they are specific permissions that are exclusively granted by Apple to system programs or Apple apps. This enables the program to always have access to the camera and photos without requiring the user’s explicit consent, unlike regular apps.
Overall, the USDZ file format looked very promising to me as an attack vector (By the way, the file format is actually called USD, USDZ is just a zipped variant). So my first approach was to do a quick test to see if there were any evidence that the file format was really that promising. A simple 1-bit flip-fuzzer caused the Mac OSX Preview component to crash within a few minutes during my initial tests, and I was able to reproduce the crash in the ASVAssetViewer program. From then on I was relatively sure that I would find a “low hanging fruit”!
Finding the Vulnerability
So, I had my promising attack vector, but now I needed a well-exploitable vulnerability. For this, I first had to find a way to investigate the processing of USD with a proper fuzzer like AFL. The problem was that for a fuzzer like AFL, you need access to the source code, which I did not have for the ASVAssetViewer component. However, this is only half true - from error messages that occurred during my initial fuzzing on Mac OSX, I was able to find out that Apple uses the USD library from Pixar, which is open source. So, the next steps were simple: download the library, compile it for AFL, and fuzz a simple test program that comes with the library with AFL. This was enough to find a number of interesting crashes. From these crashes, I identified five as separate vulnerabilities, and ultimately decided on the vulnerability in the following code:
_ReadCompressedInts(Reader reader, Int *out, size_t size)
using Compressor = typename std::conditional<
sizeof(Int) == 4,
std::unique_ptr<char> compBuffer(new char[Compressor::GetCompressedBufferSize(size)]);
auto compSize = reader.template Read<uint64_t>();
Compressor::DecompressFromBuffer(compBuffer.get(), compSize, out, size);
The vulnerability is a heap overflow in which data is written beyond the area of a memory allocation. Data from the USD file is written to a buffer (compBuffer), with the size of this buffer being indirectly determined and the amount of bytes written (and read from the file) directly determined by the file and therefore determinable by an attacker. From the perspective of an exploit developer, this is a very nice vulnerability because so much can be controlled by the attacker.
At this point, I had completed 20% of the journey, and at the latest, a regular security assessment would have stopped. But I had set myself the goal of developing an exploit, so the real work was just beginning.
Exploiting the Vulnerability
My goal was to exploit the vulnerability to run my own code in the vulnerable ASVAssetViewer program to use the program’s privileges to access user photos.
Since the vulnerability could also be triggered via Safari, a remote exploit was theoretically possible. However, to do this, I would have had to remotly bypass ASLR, which randomizes the allocation of code addresses, among other things. To make things easier, I instead set myself the goal of developing an exploit that would allow a developer of a malicious app to access photos on an iPhone 8, even though the app didn’t have this permission. ASLR wasn’t a problem here because certain parts of the code in iOS are located at the same memory address for all apps under the same operating system instance. Restricting the target to an iPhone 8 was important because modern devices use PAC (Pointer Authentication Codes), which make developing an exploit even more difficult (At least, that’s what I thought in 2020 - today I know that back then with iOS 13, it was actually easier in a case like this than I thought).
However, there was still the problem of how to exploit the vulnerability to execute code that would give me access to the photos. Below, I’ll outline how my exploit achieves this (For anyone familiar with exploit development, it should be obvious that from step 2 onwards the approach is more or less standard, as it is necessary for many other heap overflows).
1. The Basic Structure
My exploit takes advantage of the flexibility of the USD file format. While such a file is being parsed, it’s possible to build a key-value data structure in which various objects can be stored during parsing without the file format specifying a fixed structure. Overall, this data structure allows the exploit to be “programmed” by creating a USD file with a specially constructed key-value data structure. The following basic building blocks are used by the exploit:
- Allocating memory of a specific size and filling it with specific data (
dict[new_key] = array[size])
- Deleting a specific previous allocted memory region (
dict[already_used_key] = dummy_data)
- Allocating an object that contains a code pointer that will be used when the object is deleted (
dict[new_key] = object_with_code_pointer)
- Triggering the vulnerability (
dict[new_key] = corrupted_array)
2. The Heapspray
At the beginning, 13,000 memory allocations of size 0x4000 are created and filled with data from the file. The data represents a ROP chain. Simply put, it contains the code I want to execute, but not as machine code, but rather encoded via return addresses. The reason for this is that it’s not allowed for the ASVAssetViewer component to create and execute its new code at runtime, so you have to resort to a series of references to existing code components. The reason why I let 13,000 such memory areas be created is that I execute a heapspray at this point. The goal of a heapspray is to allocate data at a specific address, and my goal was the address 0x150000000. My tests had shown that my data was most likely located at this address after 13,000 allocations of size 0x4000. This is important because I need a known address to be able to reference this data later.
3. Heap Grooming
Next, my exploit allocates 2000 memory blocks of size 96. At first, one might think this is another heap spray, but my goal here was heap grooming. The goal of heap grooming is to bring the heap from an unknown state to a state where an attacker can anticipate how further memory allocations will behave. My heap grooming aims to make further allocations of size 96 lie neatly next to each other in memory, and when one allocation is freed, the address is immediately re-used for the next allocation. This is exactly what my exploit achieves by triggering the allocation of 2000 memory blocks of size 96. Simply put, this will most likely fill all the “holes” in the heap, so that the heap allocator will assign memory blocks sequentially in the future. By the way, all subsequent objects have a size of approximately 96 bytes, because the heap grooming only prepares the heap for memory allocations of this size. The reason for this is that memory blocks of different sizes are managed differently.
4. Heap Preparation & Triggering the Actual Vulnerability
Now it is time to do some final steps and finally trigger the vulnerability:
- First, a placeholder memory block of size 96 is allocated, but its content is irrelevant:
| Placeholder |
- Then, a memory block is allocated that contains an object for which I found out that it has a code pointer that is used when the object is deleted. This code pointer is the one that will be overwritten by the vulnerability. Due to heap grooming (and because the code pointer object has a similar size as the placeholder), the object is placed neatly in memory behind the previous allocation:
| Placeholder | Object with code pointer |
- Next, I let the placeholder object be deleted and trigger the vulnerability, choosing the parameters so that the memory block just freed is used, allowing me to write beyond it into the object with the code pointer:
< Placeholder is beeing deleted > | deleted_Placeholder | Object with code pointer < Vulnerability is triggered > | Buffer which overflowed | Object with overwritten code pointer
- At this point, I have manipulated a code pointer. Since this code pointer is used when the object is deleted, the exploit only needs to trigger the deletion of this object at this point, and the code pointer will be used. This triggers the ROP chain that I constructed, which is designed to send all of the device’s photos to a server of my choosing via HTTP.
So, I achieved my goal of finding and exploiting a vulnerability in iOS. In the end, I found the vulnerability easier to find than I expected, and I did not get the feeling that anyone really thought about security when USD support was added to iOS. After reporting the discovered vulnerabilities to Apple, I received a six-digit bug bounty reward, which was a great motivator to continue my work in the security field of iOS. In fact, in 2021, after USD file processing in iOS had received more attention and many vulnerabilities had been fixed, I could still find another USDZ file processing vulnerability using a different approach.
Holger Fuhrmannek (firstname.lastname@example.org)