Awesome
Introduction to macOS - the App sandbox
Continuing with my transition to macOS blogpost series, I'd like to discuss a bit about the macOS App sandbox.
It is highly recommended to read the macOS App structure blogpost first - I will be assuming the reader knows the difference between Apps, processes (tasks), know a little bit about launchd
and its relation to App launching.
Sandbox by example
The first time I learned about the macOS sandbox I naively tried to create a malicious Word Macro.
This (still) is a very common entry vector in the Windows ecosystem, so I wanted to see if I could just launch processes and generally wreck havoc.
Well, things are not so easy on macOS - I was able to run processes, for instance, but it seems they couldn't do much.
Dropping files always got me cryptic Operation not permitted
error - what's going on?
I started reading a bit about macOS and Word and came upon this excellent blogpost by Adam Chester (works at MDSec). I highly recommend reading the blogpost, but I'll summarize the findings here:
- Apps may use a technology called the App Sandbox.
- Once set up, the OS enforces configurable rules on the App, such as what filenames it could create, whether it can use network capabilities and so on.
MacOS used to have a working utility called sandbox-exec
that would execute commands in a sandbox. While deprecated, it could highlight quite a lot. You see in the manual page for it that it gets a profile
, so we can conclude sandbox rules are maintained in profiles. Those profiles can come in various shapes and forms - files, pre-defined names or even as literal strings.
The manual pages also state developers should be using the App Sandbox feature. Reading more about it I understood that the sandbox rules are baked into the binary, in our case, living under /Application/Microsoft Word.app/Contents/MacOS/Microsoft Word
(if this seems alien to you, check out my macOS App Structure blogpost).
While you can easily extract them by hand, it's best to use a tool: codesign
:
```jbo@McJbo ~ % codesign -dv --entitlements - /Applications/Microsoft\ Word.app/Contents/MacOS/Microsoft\ Word
Executable=/Applications/Microsoft Word.app/Contents/MacOS/Microsoft Word
Identifier=com.microsoft.Word
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=351454 flags=0x10000(runtime) hashes=10972+7 location=embedded
Signature size=8980
Timestamp=Apr 10, 2023 at 8:09:50 AM
Info.plist entries=52
TeamIdentifier=UBF8T346G9
Runtime Version=13.1.0
Sealed Resources version=2 rules=13 files=28766
Internal requirements count=1 size=180
[Dict]
[Key] com.apple.application-identifier
[Value]
[String] UBF8T346G9.com.microsoft.Word
[Key] com.apple.developer.aps-environment
[Value]
[String] production
[Key] com.apple.developer.team-identifier
[Value]
[String] UBF8T346G9
[Key] com.apple.security.app-sandbox
[Value]
[Bool] true
...
[Key] com.apple.security.temporary-exception.files.absolute-path.read-only
[Value]
[Array]
[String] /Library/Preferences/com.microsoft.office.licensingV2.plist
[String] /Library/Application Support/Microsoft/
...
[Key] com.apple.security.temporary-exception.sbpl
[Value]
[Array]
[String] (allow file-read* file-write* (require-all (vnode-type REGULAR-FILE) (regex #"(^|/)~\$[^/]+$")) )
[String] (deny file-write* (subpath (string-append (param "_HOME") "/Library/Application Scripts")) (subpath (string-append (param "_HOME") "/Library/LaunchAgents")) )
[Key] com.apple.security.temporary-exception.shared-preference.read-only
[Value]
[Array]
[String] com.ThomsonResearchSoft.EndNote
...
This is a lot to unpack, so let's take some high-level notes:
- First, our command-line used
-dv
flag, which stands fordisplay
andverbose
. Then,--entitlements
presents entitlements associated with the App or binary (yes,codesign
can work on both). We will dive into entitlements in a different blogpost, but for now let's say they reflect capabilities of the App, and one of them states that the App is sandboxed (com.apple.security.app-sandbox
has a Boolean value ofTrue
). - The first few lines of output are general information about the binary, its hash and many other interesting things. They're out-of-scope for the purpose of this blogpost, but are quite interesting!
- Next we get a big dictionary. Those of you who remember me ranting about
plists
(again, in my [macOS App Structure blogpost]) might suspect that the key-value dictionary is a representation of some property list, and they will be correct. - The sandbox rules are states in some of the dictionary keys. For example,
com.apple.security.temporary-exception.files.absolute-path.read-only
mention an array of absolute paths the App is permitted to read from. - The (in)famous regular expression that lives under
com.apple.security.temporary-exception.sbpl
is also here - that's to create those notorious~$whatever.docx
temporary files that Word is so fond of. - Of course, it goes without saying that creating new child processes inherit the sandbox rules, otherwise there is no much point to it. Note how powerful those sandbox rules are!
Sandbox escape with launchd
In MDSec's blogpost from 2018 that I mentioned earlier, the deny file-write*
part under com.apple.security.temporary-exception.sbpl
did not exist, which allowed macros to create files with arbitrary contents such as /Library/LaunchAgents/~$evil.plist
. Why does that escape the sandbox?
LaunchAgents
and LaunchDaemons
are a well known (legitimate) persistence mechanism in macOS. I've mentioned them before, but you can think of them as Services (if you come from the Windows world) - LaunchDaemons
start when the OS boots (and therefore live outside of a user's session), while LaunchAgents
start when a user logs in.
Interestingly, both are described in simple plist
files. Here's one example for my OneDrive updater:
jbo@McJbo ~ % plutil -p /Library/LaunchAgents/com.microsoft.OneDriveStandaloneUpdater.plist
{
"Label" => "com.microsoft.OneDriveStandaloneUpdater"
"Program" => "/Applications/OneDrive.app/Contents/StandaloneUpdater.app/Contents/MacOS/OneDriveStandaloneUpdater"
"ProgramArguments" => [
]
"RunAtLoad" => 1
"StartInterval" => 86400
}
Those LaunchAgents and LaunchDaemons get launched by launchd
(remember that process?) and therefore it escapes the sandbox, as launchd
had no knowledge of whether the plist
was dropped from a sandboxed process or not (and even if it did - how would it know what sandbox rules to apply?).
This concept of using launchd
to escape the macOS sandbox was used extensively, and in fact, I have used that in the past.
Saving you a couple of clicks - here's the idea:
- As you might recall,
launchd
launches macOS Apps. Those Apps might be launched by double-clicking them, or by other means - for example, clicking on azip
file will use theArchive Utility
since it's associated tozip
files. - An alternative way to launch Apps through
launchd
is with theopen
command. - The
open
command is rich - you can use some of its interesting features such as selecting the App, selecting the filename to open or even give full command-line arguments. - I specifically used the builtin
Python
App (which no longer exists on new macOS vanilla devices) to launch Python with anstdin
argument that essentially redirects the standard input from a file I dropped (that file was~$evil.py
due to Word's constaints). - In our case,
launchd
ran an unsandboxedPython
App instance which started reading from~$evil.py
which had arbitrary Python commands, essentially escaping the sandbox.
There have been similar ideas in other disclosures (one nice example lives here) but the idea stays the same. I am pretty sure there are plenty more in plain sight!
One honorable mention goes to a great blogpost by Wojciech Regula - this time focusing on the Terminal
app and an environment variable manipulation. You should give it a read!
The fixes and the quarantine xattr
The one issue found by MDSec was specific to Office - and was fixed with more strict rules.
The ones abusing LaunchServices
(which is the framework name for launching apps with launchd
) are more generic - and hence Apple had to fix them.
One of the things I've noticed was files dropped by Word are now created with the com.apple.quarantine
extended attribute, yes the same one I mentioned in my introduction to Gatekeeper blogpost.
As it turns out, that quarantine attribute is some hardening against certain attacks - for example, the Terminal
App refused to launch shell scripts created with that attribute. That's the reason, by the way, that I had to use the --stdin
option for Python
.
More fixes
As pointed out by Gergely Kalman - Apple has added further checks to the open
binary to harden those kind of exploits. It seems that --stdin
, --args
and other command-line flags are ignored if the calling process is sandboxed. However, open
simply calls LaunchServices
(in launchd
) with an IPC, and conveniently there are APIs for that, e.g. LSOpenURLsWithRole.
I have no investigated whether LaunchServices
itself is hardened too - if not, then I believe similar sandbox escapes could be easily achieved.
Summary
We've briefly discussed another macOS technology - the sandbox. We've seen how powerful and configurable it is, and how it could get broken.
We've also tied some things together - how apps work with sandbox rules, how launchd
launching apps breaks more than just the process trees and how plist
files can be used for good or for evil - this time with persistence (LaunchAgents
and LaunchDaemons
).
Luckily, we've even tied the com.apple.quarantine
extended attribute from the introduction to Gatekeeper blogpost and explained how it might be used as an extra hardening against sandbox escapes. Not too bad!
In the next couple of blogposts, we will explore more security mechanisms in macOS and might talk about strategies of breaking them.
Stay tuned!
Jonathan Bar Or