This post covers basic macOS security concepts and hopefully serves as an introduction to red teaming macOS environments.
- macOS Security Overview
- Gatekeeper & XProtect
- Hardened Runtime
- System Integrity Protection (SIP)
- Transparency, Consent, and Control (TCC)
- macOS Sandbox
- Initial Access Payloads & Persistence
- Application Bundles
- Installer Packages
- Disk Images (DMG)
- Privilege Escalation & Post Exploitation
- Situational Awareness
- Stealing Cookies
- Clipboard Monitoring
- Code Injection
- In-Memory Mach-O Loading
Credits/Resources for Learning
Nothing within this post is “new” research, but instead consolidated information about the above topics, from the below resources, in a single place for easier reading and introduction.
- Patrick Wardle/Objective See
- Adam Chester
- Justin Bui
- Csaba Fitzl
- Wojciech Reguła
- Cedric Owens
- Chris Ross
- Leo Pitt
- Cedric Owens’ DEFCON 29: Red Teaming macOS
- Cody Thomas’ OBTS: Red Teaming macOS
- Wojciech Reguła’s x33fcon: Red Teaming macOS
- Cody Thomas’ OBTS: Lock Picking the macOS Keychain
- Patrick Wardle’s The Art of Mac Malware
- Offensive Security’s OSMR
- All-in-one references
macOS Security Overview
Gatekeeper & XProtect
Gatekeeper is a security mechanism designed to only allow trusted applications to run on systems, similarly to SmartScreen and MOTW on Windows. When executable files are downloaded from the internet they are marked with the
com.apple.quarantine attribute, which triggers gatekeeper when the file is ran.
If the executable is notarized, Gatekeeper verifies the code signature of the file and presents a prompt to the user to confirm they want to run the file.
If the executable is not notarized Gatekeeper will present a prompt to the user informing them the file cannot be ran as it is unsigned. To run unsigned executables the user must right-click the file, then click open, instead of double-clicking.
Following the verification of the code signature, XProtect, which is macOS’ built-in antivirus will scan the file for malware. XProtect utilizes YARA rules which can can be found via
locate XProtect.yara and are for the most part useless unless you are running default Metasploit payloads. Additionally, XProtect only scans for malware under three conditions
- The file is being ran for the first time
- The file’s hash has changed
- XProtect’s YARA rules have been updated
Lastly, if you would like to get an app notarized, you can sign up for the Apple Developer Program to get a code signing certificate which costs $99/year. Once you’re signed up, you can then sign your app and submit it to Apple where it will undergo a security scan and check that other requirements such as the hardened runtime (covered in the next section) is enabled. If you submit malware it is possible that Apple’s security scan may overlook it and still notarize your app, however, your certificate will likely be revoked shortly after.
Hardened runtime is a feature which can be enabled on apps to protect against code injection via dylib hijacking, environment variables, and task port injection. One example of how hardened runtime protects against injection is by requiring that any dylib loaded into the process is signed with the same certificate as the app itself. This of course causes issues when an application needs to load 3rd party dylibs, thus, apps can have certain entitlements to remove some of these restrictions.
We can list apps entitlements via
codesign -d --entitlements :- <file> or the
list_entitlements command in Poseidon, and if any of the below are present we may be able to inject into the app:
com.apple.security.cs.disable-library-validationentitlement allows any dylib, signed or unsigned, to be loaded into the process. Depending on the location of the dylibs the app loads, this may open the door for dylib hijacking.
com.apple.security.cs.allow-dyld-environment-variablesentitlement allows dylibs to be loaded from the
DYLD_INSERT_LIBRARIESenvironment variable. However, this entitlement alone does not remove the previously mentioned code signing requirements. If this entitlement is present along with the disable-library-validation entitlement, you can inject into the app via
DYLD_INSERT_LIBRARIES=malicious.dylib ./appsimilarly to
com.apple.security.get-task-allowentitlement allows other apps to get the task port of your app, which is similar to obtaining a handle to a process on Windows. It should be noted that in order to access a task port your code must run as root. With the task port you can then perform your classic process injection via writing shellcode and creating a thread.
From a red team perspective being able to inject code into another application is particularly useful as it can allow you to inherit entitlements (covered more in the TCC section) such as access to certain folders you previously did not have.
System Integrity Protection (SIP)
SIP aka “rootless” protects system files from modifications. Essentially even as the root user we cannot alter certain system files such as the executables within
/bin/ . Files protected by SIP can be identified via the
SIP can be disabled via
csrutil disable , however, it requires restarting the device in recovery mode, thus, this is usually out of the question from a red team perspective.
Transparency, Consent, and Control (TCC)
TCC is macOS privacy feature implemented in v10.14+ which prompts the user to explicitly grant permissions when an application tries to access certain resources such as the camera, and certain folders including
Documents , and drives/volumes.
Attempting to access a TCC protected resource without permissions could lead to accidentally generating a prompt, alerting the user of your presence, and ultimately your engagement ending sooner than expected. Below are some useful files which are not protected by TCC:
- Hidden files & folders in the home directory —
- User application data —
- Cookie files —
TCC permissions can be seen by browsing to “Settings” then “Privacy & Security”. Additionally, what permissions each app has is stored and tracked in TCC databases. The system TCC database is located at
/Library/Application Support/com.apple.TCC/TCC.db and each user has a TCC database located at
~/Library/Application Support/com.apple.TCC/TCC.db .
The user’s TCC database requires full disk access (FDA) to interact with, however, if we can interact with it, we can then modify it to grant ourselves access to other resources. We can check if we have FDA without prompting the user by running
file against the user’s TCC database.
TCC bypasses are not particularly “rare” and I recommend checking out Csaba Fitzl’s blog to stay up to date with old and new bypasses.
The macOS sandbox is a feature which developers can opt their apps into if they are released outside of the App Store, and mandatory for all apps downloaded directly from the App Store. Apps which have opted-in to the sandbox feature will have the
com.apple.security.app-sandbox entitlement, and as the name implies, will force them to run inside a sandbox. Apps running in a sandbox have extremely limited access to the system such as the ability to read or write files to the disk. For this reason, certain attack paths on macOS such as phishing via macros are not enticing. As Microsoft Office apps opt-in to the sandbox and will prevent you from persisting and looting.
The keychain can be compared to LSASS on Windows and holds secrets such as passwords and keys for apps. For example, an application may encrypt the files it stores on disk, and the decryption key for these files is stored within the keychain. The system keychain can be found at
/Library/Keychains/System.keychain and user’s keychain can be found at
You can download the user’s keychain, although, secrets within will be encrypted with the user’s password. Depending on if you have the user’s password you can leverage Chainbreaker to either extract secrets, or dump the hash of the keychain password via
--dump-keychain-password-hash into a format for hashcat which is extremely slow to crack.
Lastly, each entry within the keychain has associated ACLs and permissions which can be viewed within the Keychain Access app. Trusted applications are apps which can perform operations on the keychain entry without prompting the user. This list can be found under the “Access Control” tab, and an interesting case is where the list is “NULL”, meaning that all apps are trusted, not to be confused with an empty list which simply means no apps are trusted.
If you are running in the context of a trusted app or the list is NULL, and you have any of the below authorizations, you may be able to extract secrets without prompting the user:
For more information on abusing misconfigured keychain ACLs I recommend watching Cody Thomas’ OBTS talk.
Initial Access Payloads & Persistence
Additionally, it should be noted that EDR products on macOS are not mature and essentially function as only a logging product. Therefore, you can typically use any default C2 agent besides from Metasploit, and not have to worry about the EDR catching your payload.
Application bundles (.app) are one of the most common program types and are the format almost all end-user software is released as. App bundles are stored within
/Applications/ and displayed to you when you click into Launchpad. Referencing Apple’s documentation, a minimal app bundle typically has the following structure:
Info.plist # App config info
MacOS/ # Contains the app's executable file
Resources/ # Fonts, images, sounds, icons, etc.
Let’s start with creating a basic app bundle payload. For this demo we’ll use Poseidon to create a fake Zoom app, however, you can alternatively use any of the Mythic agents with macOS support.
1 - Create the app bundle folder structure
2 - Create the Poseidon payload
Next, since both Intel and Silicon macs are now common, we can create both an AMD and ARM Poseidon payload, and then combine them into a universal Mach-O which can run on either architecture via
lipo -create <arm-payload> <amd-payload> -output <universal-payload> and then place it into the
3 - Create the
Referring to Apple’s developer docs we’ll need to specify a few keys:
- CFBundleExecutable — The name of the main executable within the MacOS directory (PoseidonPayload)
- CFBundleIconFile — The name of the application image (ZoomImageForPayload)
- CFBundleIdentifier — The string which unique identifies your application on the system (com.examplepayload.Zoom)
- CFBundleName — The name of your app (FakeZoom)
- CFBundleVersion — The build version number of your app (1.0.0)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
There are many additional keys you can specify in your
Info.plist file for different effects such as the
LSUIElement key which determines whether or not the application will appear in the dock, however, to keep this demo simple we’ll stick with the above.
4 - Select an icon for your app
Since we are creating a fake Zoom app, we can just copy Zoom’s icns file out of the
/Applications/ directory (if it’s installed) and into
Resources/ZoomImageForPayload, however, you can alternatively use any icon you want.
Installer packages (.pkg) are xar archives and another common program type seen when software needs to be installed with root privileges. While installer packages can contain a main payload to run when opened, an alternative is to leverage “preinstall” and “postinstall” scripts, which the name implies, will run a script before and after installing.
For this example we’ll create an installer package which uses these scripts to register a LaunchDaemon for persistence as the root user when opened.
1 - Create the installer package folder structure
chmod +x scripts/preinstall
chmod +x scripts/postinstall
Be sure to make your pre and postinstall scripts executable before packaging them otherwise your payload will fail.
2 - Create a plist file to register as a LaunchDaemon
There’s really only one value needed within this plist file which is
ProgramArguments and essentially specifies what to run when the LaunchDaemon is triggered. Take note that
ProgramArguments is also an array, so you could pass additional string values such as flags if needed.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
We’ll reuse the universal Poseidon payload we created from the app bundle section, and we can move this plist file into
scripts/files/com.examplepayload.persistence.plist along with the payload to
3 - Create the pre and postinstall scripts
These scripts are just regular bash scripts, however, they must start with
#!/bin/bash and end with
exit 0 . We’ll use the preinstall script to copy our plist and payload to the correct locations on the disk, and then the postinstall script to register the LaunchDaemon.
cp files/PoseidonPayload "/Library/Application Support/PoseidonPayload"
cp files/com.examplepayload.persistence.plist "/Library/LaunchDaemons/com.examplepayload.persistence.plist"
launchctl load "/Library/LaunchDaemons/com.examplepayload.persistence.plist"
4 - Create the installer package
To create the installer package from these scripts we can simply run
pkgbuild --identifier <identifier> --nopayload --scripts <path to scripts folder> <output>.pkg
We can unload the LaunchDaemon by running
launchctl unload com.examplepayload.persistence.plist and it should be noted that dropping the payload to
/Library/Application Support/ was purely preference, and it can be dropped elsewhere.
LaunchDaemons represent only one form of persistence on macOS and if you’re interested in other forms, I recommend checking out SentinelOne’s blog.
Disk Images (DMG)
DMGs are compressed volumes which can contain other payload formats. The benefit of delivering your payload within a DMG file is that you can add graphical elements to aid in socially engineering the user to right-click, then open your payload to bypass Gatekeeper, as the DMG itself does not invoke Gatekeeper.
To build a genuine looking DMG we’ll need to create a custom background image, I prefer to leverage legitimate software or company wallpaper along with draw.io, but you can create your own as you like.
For this demo, I went with this image as the base, and created this:
To start we can create a disk image by opening
Disk Utility and selecting “File” then “New Image” then “Blank Image…”
We can select the name of our volume, and then change the “Format” to “Mac OS Extended (Journaled)”.
Open the .dmg file, which is “example-payload.dmg” in my case, and a volume will appear in your Desktop, then open the volume, which is “example” in my case. To show hidden files on macOS you can click “[shift] + [command] + [period]”.
Move your background image into the volume
/Volumes/<volume-name>/ as a hidden file, along with your payload. For this example we can reuse the app bundle from the above section.
Next, we can click into the volume and select “View” followed by “Show View Options”.
Under “Background” select “Picture”, then double click the “Drag image here” box. A new Finder window will open and you can select the background image from within the volume. If you do not choose an image contained within the volume it may cause errors. Additionally, we can resize the icon via “Icon Size”.
Resize the window to fit your background, and center the app bundle.
Once you are content with the layout, and while the Finder window is still open, right click the volume on the Desktop, and click “Eject”.
Disk Utility and select “Images” then “Convert…”
Select your DMG file, then select “Encryption: none” and “Image Format: compressed”
Of course this demo doesn’t make too much sense given it is a Chrome background, with a Zoom application (lol), however, hopefully you get the point.
One final thing to take note of regarding payloads is that files downloaded via your browser will append the
com.apple.metadata:kMDItemWhereFroms attribute, which lists where the file was downloaded from. If you are downloading your Poseidon payload directly from Mythic in your browser before packaging it, this can lead to accidentally revealing the location of your team server. You can remove this attribute via
xattr -d com.apple.metadata:kMDItemWhereFroms <file> .
Privilege Escalation & Post Exploitation
This section is not exhaustive of all activities you can perform after getting a shell and is intended to only give a general idea of post-ex on macOS.
You can start with some situational awareness checks on the system, by either running HealthInspector or SwiftBelt, or manually browsing config files. Many of these files will be plist files in binary format and to make them readable you can use
plutil . Some files of interest could be:
- What apps the user has in the dock? —
plutil -p ~/Library/Preferences/com.apple.dock.plist
- What files the user recently interacted with? —
plutil -p ~/Library/Preferences/com.apple.finder.plist
- What version of macOS your on? —
plutil -p /System/Library/CoreServices/SystemVersion.plist
Next, you can start downloading some files which are not protected by TCC from the disk, including the user’s bash or zsh history, ssh keys, cloud keys (AWS, GCP, etc), keychain, and cookies.
Given you are on a macOS it is likely the organization for your engagement may be a tech company or startup, and Slack is common to see in these environments. If so, you can download the following files:
With these two files downloaded, you can then copy them to your local mac, open Slack, and you will be authenticated as the user. From here you can either manually search Slack or use SlackPirate.
Next, you can target browser cookies, for this example we’ll use Google Chrome. If you have the user’s password either from finding it within their bash or zsh history or maybe running a password prompter, you can download
~/Library/Application Support/Google/Chrome/Default/Cookies and decrypt the file offline. Alternatively, if you do not have the user’s password, you can use tools such as WhiteChocolateMacademiaNut to start Chrome with remote debugging enabled and then dump the cookies over a websocket. Again, since mac EDRs are not mature, dropping garbled tools to disk shouldn’t be much of a concern.
With access to browser cookies you can then open a SOCKS via Poseidon’s built-in command or drop chisel, and browse internal sites. Specifically, you may want to target internal documentation sites such as their Confluence and their internal code repositories for finding privileged credentials.
On macOS every time the clipboard contents have changed a counter gets incremented, thus, we can monitor the value of the counter in an interval and fetch the clipboard’s contents if it has changed. Poseidon has a built in command
clipboard_monitor which we can leverage for this.
Some additional things to note:
clipboard_monitorcommand filters to only retrieve plain text, so if the user copies a large binary to their clipboard we do not have to worry about sending large amounts of traffic over our C2.
- The root user does not have access to the clipboard, so any clipboard monitoring tools must be ran in a user context.
As previously mentioned in the harden runtime section, being able to inject code into an application is useful from a red team perspective as it can potentially grant you additional entitlements and access to resources you previously did not have.
For example, your initial payload may not have access to the Downloads folder, however, if you can inject code into Chrome which might have the
com.apple.security.files.downloads.read-write entitlement, you would then be able to browse the Downloads folder.
To find targets for code injection you can run Poseidon’s
list_entitlements command which will search for running processes with the entitlements mentioned in the hardened runtime section.
From here you can then inspect each process to find a potential target.
One other form of code injection not previously mentioned is “electron hijacking”, which is covered extensively by Adam Chester in this blog. Essentially you can leverage the
ELECTRON_RUN_AS_NODE environment variable to spawn a process as a child of an electron app, and because child processes inherit the TCC permissions of their parent, you can potentially gain their entitlements.
In-Memory Mach-O Loading
While definitely not needed with the current state of macOS EDRs, it is possible to run Mach-Os in-memory. The APIs used to perform in-memory loading vary between OS versions so I will not cover this, but instead recommend checking out Justin Bui’s post and Adam Chester’s post.