Background

Back in early 2024, I was looking into Apple Archive along with how parts of the OS such as Archive Utility and WorkflowKit use libAppleArchive. I have made some cool discoveries from that front, such as the WorkflowKit extraction race, as well as the Archive Utility race. However, recently, I decided to start looking into the Apple Archive format itself.

libAppleArchive is a great library, but it's closed source and proprietary, and no one to my knowledge has documented enough about the Apple Archive format. This means that on platforms such as Linux, you can't easily work with them. This lead me to start developing libNeoAppleArchive; a (hacked-together) library for dealing with Apple Archive files, that's open source and available on Linux, allowing for (currently limited) use of Apple Archives on Linux.

One thing I had already tried a long time ago was malforming an .aar to have a file name containing .. (a way to traverse to the file's parent) to attempt writing to an arbitrary file path. However, this didn't work, because the libAppleArchive team had already thought of this. They have a pathIsValid() function for determining if a file path contains relative path traversals, which I decompiled and couldn't think of a way to bypass. Thus, I kind of just forgot about this for a while.

Discovery

Fast forward to this month, I was working on implementing code for extracting .aar files and got to symlinks. I realized something interesting; imagine we have a directory, Output-FT-Symlink to archive. In this directory, there is a file Output-FT-Symlink/interesting/foo, and there is a symlink to it at Output-FT-Symlink/interesting/bar. Looking at the header for the archived output filetest directory, I saw that the PAT field key pointed to Output-FT-Symlink/interesting/bar, but the LNK field key just had foo. This got me thinking: if the symlink was to a file in the archive that was outside of the directory, how would shortcuts handle it? Having a symlink at Output-FT-Symlink/interesting/bar that was pointed to Output-FT-Symlink/test would have the LNK field be ../test. This made me realize that the LNK path does not get checked by pathIsValid(), as it would break normal symlinks in an Apple Archive.

This made me wonder: are there any checks at all if the symlink is to a path outside the extraction directory? After testing, I successfully had an archive extract with a symlink to a completely different directory on my system! While I don't think this by itself is an issue, this does open the door to some interesting possibilities. Hence, I thought hey, if we can have a symlink to an arbitrary path, then why couldn't we have a file written to that symlink, allowing us to write to an arbitrary path on the file system WITHOUT using relative directory traversal, thus keeping pathIsValid() happy? So I tried this, and... it didn't work.

At first I was ready to call it quits, but I decided to check if I formatted the header wrong, so I messed around with a hex editor trying various things. After the 4th time, it worked! However, upon reextraction, I saw something confusing: despite not changing anything, it didn't work. After trying to extract it multiple times, I realized that it only worked about 15% of the time. Why is this happening?

The Race

So as it turns out, I was actually bypassing another check that I didn't even know existed in libAppleArchive. I'm not sure if this was because the libAppleArchive team thought about writing to arbitrary symlinks and wanted to protect against it, or if it was meant for another purpose, but in the concatExtractPath() function, it checks if any items in the path aren't directories, which also includes symlinks. Here's what it was doing in code:

struct stat64 st;
int pathDoesExist = lstat64("Output-FT-Symlink/secret/pathToEvilSymlink", &st);
if (pathDoesExist) {
 mkdir("Output-FT-Symlink/secret/pathToEvilSymlink", 755);
} else if ((st.st_mode & ST_IFMT) != S_IFDIR) {
 /* a parent of path is not a direct... */
 /* if we reach here, game over, libAppleArchive will refuse to extract the file */
}

This does protect against it most of the time. However, given that libAppleArchive uses multi-threaded extraction, if you place the header for the symlink DIRECTLY before the header and data for the file that will be written to the symlink in the .aar file, libAppleArchive will try to extract both of them at the same time. In the cases where the file directly follows the symlink creation, there is around a 15% chance that libAppleArchive will race with itself and the symlink will be created after the lstat64() call, but BEFORE the mkdir()! Since the symlink was created in between, the mkdir() call will fail, as the symlink will already exist. But libAppleArchive only checks the success of the lstat64() call, and it will think the directory was created even though a symlink is there instead. This means that when it tries to write to the file, it instead writes it to the symlink pointing at our arbitrary file! Holy cow!

I then tried to make it more reliable, as a 15% success rate isn't too hot. I tried denying the symlink execute permissions to make lstat64 fail, but saw that libAppleArchive was performing the check before it applied file permissions, so this will not work. However, there was one final idea I had to increase exploit reliability: when the initial race fails, libAppleArchive will stop trying to extract that file, but will still move on and attempt to extract other files in the archive. This means that we can just copy our symlink header and the file header and data (but changing the symlink path) over and over to retry the exploit multiple times. This makes the archive's size larger, but it works. Now, we have an arbitrary file write primitive with a high success rate!

Attempting Gatekeeper Bypass

This libAppleArchive vulnerability is triggerable in any process using it for extraction. So, I had an idea of a great place to exploit it.

When I was researching the Archive Utility race, I found this cool post about a different vulnerability in Archive Utility that allows bypassing Gatekeeper. In it, an vulnerability was shown that allows a malicious .aar file to be extracted without com.apple.quarantine.

Archive Utility stores it's extracted files at $TMPDIR/com.apple.desktopservices.ArchiveService/TemporaryItems/NSIRD_ArchiveService_{6-random-characters}. It then calls qtn_file_apply_to_path() to apply the quarantine attribute to NSIRD_ArchiveService_{6-random-characters}. However, with this libAppleArchive exploit, we can have ArchiveService extract the file outside the directory to $TMPDIR/com.apple.desktopservices.ArchiveService/evil.zip, but not quarantine it, thus bypassing Gatekeeper!

The problem is that since ArchiveService.xpc is restricted by the Sandbox, we can only write to $TMPDIR/com.apple.*.ArchiveService, not to directories like /Applications. Thus, the Gatekeeper bypass would mean that a user would have to go to $TMPDIR/com.apple.*.ArchiveService to launch the bypassed file. Now, we could make this better by having the .aar also contain a symlink to $TMPDIR/com.apple.*.ArchiveService/maliciousbinary. With this, even though the symlink itself will have the com.apple.quarantine attribute applied to it, since the file it points to does not, it allows it anyway.

However, this creates a new problem: we need to have the symlink point to $TMPDIR, meaning we have to know the user's $TMPDIR when making the malicious .aar. Not only that, libAppleArchive will not apply the correct mode_t to the arbitrary written file path, so it will be written with the permissions set to 644, so any binaries extracted this way will not automatically be made executable. However, if instead of having the quarantine bypassed file be the binary itself, we make it a zip that contains the actual binary marked as executable, we can bypass this.

When implementing this into an Apple Archive, we would have a symlink to the gatekeeper-bypassed zip file. The symlink will still have the quarantine attribute applied to it, but since the zip itself will not, when trying to extract via the symlink, it will extract the binary without the com.apple.quarantine attribute applied to it. Still, because we need to know the user's $TMPDIR directory AND we also need to have them extract a .aar and a .zip, while this may be a Gatekeeper bypass it still requires some additional human interaction. There probably is a way to improve upon it to make it more subtle, but I can't think of anything at the moment...

Possible Targets

Even if using this against Gatekeeper wasn't as subtle as I hoped it would be, this libAppleArchive vulnerability is still pretty bad. As stated before, it's triggerable in any process relying on it for extraction. This includes WorkflowKit, the backend for Shortcuts that uses libAppleArchive to extract AEA signed shortcut files, which side note, contains LZFSE compressed Apple Archives. You still need to pass signing validation, but I have found other flaws with the signing validation process. Exploiting WorkflowKit would also be less subtle since the AAArchiveStreamClose() call would return a negative value and show an error message saying "Could not find the main shortcut Shortcut.wflow file in the archive". However, despite the warning it still triggers and writes files to an arbitrary directory. This exploit thus grants access to write to any directory Shortcuts has access to.

There are also other Apple utilities that uses libAppleArchive, such as FlexMusicKit and possibly ClipServices, which powers App Clips. Admittedly, I don't know much about these services, but the bug should still be exploitable in them. There are also certain kernel modules dealing with Apple Encrypted Archives, though outside of KDP I don't know what they're used for and I also don't believe libAppleArchive is involved. Regardless, any 3rd party app that uses libAppleArchive to extract Apple Archives can have a malicious archive given to it and made to write files to whatever directories are available in its Sandbox. The Files app on iOS can also extract .aar files, which to my knowledge uses libAppleArchive under the hood, so the exploit should be triggerable from there as well.

Exploit Creator

I have made an exploit creator that uses libNeoAppleArchive to create an aar to bypass Gatekeeper. You can find it in GatekeeperV3.zip down below. This is a modified version of the exploit I originally submitted to Apple Security that uses an updated libNeoAppleArchive. However, it does still need the attacker to know the user's $TMPDIR to create a symlink to the Gatekeeper-bypassed file, as explained previously.

Patch

After submitting this vulnerability, this vulnerability was addressed in the following updates:

(Note: This should theoretically affect tvOS and watchOS as well, but given that their release notes do not list this vulnerability, perhaps there was something different with their codebase that prevented it from working, but unfortunately I don't have an Apple TV or Apple Watch to test it...)

Sadly I did not recieve a bounty for this, as I couldn't demo writing to any sensitive files with this. Hopefully next time I find something that will get me a bounty soon. I'm considering opening up donations in the future for development of libshortcutsign/libNeoAppleArchive, but they are very niche libraries so I don't know anyone that would be willing to donate for them.