A Brief Look At macOS Detections and Post Infection Analysis

Cedric Owens
Nov 13 · 11 min read

This is a blog post that serves two purposes: 1. provide help with teams looking to jumpstart their macOS detections (i.e., detect a lot of the low hanging fruit and commonly used techniques and tooling by red teams and attackers), and 2. help teams understand what sources to search for post infection analysis on macOS (i.e., discuss some places to look when analyzing compromised macOS endpoints). This blog post is not meant to be an all encompassing resource, as offensive tactics and methods change frequently. But I hope this blog inspires defenders with a broader mindset on how to approach macOS detections and post infection analysis.

First I will discuss some helpful detections and then jump into post infection analysis.

Helpful macOS Detections

Here I will lay out some simple macOS detections that can be rolled out to help identify low hanging fruit and common attack toolkits for macOS hosts. To date, python has been used heavily for post exploitation tasks on macOS hosts, so several of the detections below will be tied to python.

Useful Parent-Child Relationship Detections

Some helpful detections include:

  • a single python parent process spawning several /bin/sh or /bin/bash children over a short period of time:

this will detect command executions on common python-based macOS post exploitation tools, such as EmPyre (https://github.com/EmpireProject/EmPyre) and MacShell (https://github.com/cedowens/MacShell/tree/master/MacShell-master)

  • Any MS Office product spawning /bin/sh or /bin/bash, which usually then executes python:

this will detect office macro executions from common python-based macOS post exploitation tools

  • Any MS Office product spawning curl (example also seen above)
  • python spawning /usr/bin/osascript:

may indicate a python-based post exploitation toolkit is being used to spawn a post exploitation task using osascript

Active Directory Related Hunting Searches/Indicators

Below are some important things to monitor that I pulled from two of Cody Thomas’ posts:



  • use of “dscl “/Active Directory/<AD_DOMAIN>/All Domains” via the command line — returns high level directory structure info
  • use of “dscl . cat /Users/<username>” via the command line — is similar to the Windows net user <username> /domain for a local user account
  • use of “dscl . read /Groups/admin” via the command line — get a list of local accounts that have admin rights on the macOS host
  • use of “dscacheutil -q group -a gid 80” via the command line — dump local admin group info
  • use of “dscl “/Active Directory/<AD_DOMAIN>/All Domains” ls /Computers” via the command line — dumps computernames from AD (similar to net group “Domain Computers” /domain on Windows)
  • use of “dsconfigad -show” via the command line — dumps basic domain info
  • uses of the “klist” command — provides some cached kerberos metadata
  • attempts to access “/etc/krb5.keytab” — this file should be readable only by root; is encrypted and allows authentication to the KDC
  • attempts to access “/etc/krb5.conf” — this file is not always on macOS hosts, but if there provides some configuration info for interacting with AD
  • command line of “launchctl bsexec” with “copy_cred_cache”: copies creds from one cache to another
  • checking process connections to port 88 (kerberos) on domain controllers: can do a stack of all processes communicating out to port 88 and look for the anomalies (least common)
  • Heimdal Kerberos API logging (from Cody Thomas’ blog):

Useful Command Line Detections

Command line detections are certainly brittle and can be trivially bypassed. However, I believe there is still value in having some basic command line detections, in case lower hanging fruit utilizing these tactics is found. Some helpful command line detections are below:

  • searching for “osascript -l JavaScript” along with “eval”:

this will detect launching JXA (javascript for automation) using the osascript binary. Brief description of JXA for command and control: the command and control server is written in JavaScript, which imports Objective C code; the victim uses the osascript engine to connect to the C2 JavaScript code, compile it, and execute it in memory. Apfell (https://github.com/its-a-feature/Apfell) has this capability; note: it is possible to use Swift code to programmatically execute osascript (which would bypass this detection), but this detection will at least detect the command-line invocation of JXA. Link to my post on how to execute JXA programmatically: https://medium.com/red-teaming-with-a-blue-team-mentaility/launching-apfell-programmatically-c90fe54cad89.

  • searching for “osascript -e” with (“password” or “Password”) and with “dialog”:

this will detect attempts to use osascript to launch a fake authentication prompt (ex: osascript -e ‘set popup to display dialog \”Keychain Access wants to use the login keychain\” & return & return & \”Please enter the keychain password\” & return default answer \”\” with icon file \”Applications:Utilities:Keychain Access.app:Contents:Resources:AppIcon.icns\” with title \”Authentication Needed\” with hidden answer’)

  • searching for “osascript -e” with “clipboard”:

this will detect use of osascript to dump the clipboard contents (ex: osascript -e ‘return (the clipboard)’). note: dumping the clipboard can also be done other ways, such as via the nspasteboard class

→ ex:) searching for “bash -i” with “/dev/tcp/” for basic bash tcp reverse shells or searching for “perl” with “exec” and “/bin/sh” or “perl” with “sock” for a basic perl reverse shell

  • searching for “screencapture -x”:

this searches for using the on disk screencapture binary along with the -x option (take screenshot silently)

  • searching for “xattr -d com.apple.quarantine”:

searching for attempts to remove quarantine attributes from downloaded files

  • for older versions of macOS (pre Mojave): searching for “defaults read” with “ShadowHashData”:

this attempts to read password hash data stored in the user’s plist file on disk

  • searching for “launchctl load” (can also pair this search with a parent process of python for higher fidelity):

searches for attempts to load launch agents for persistence via the command line

  • searching for “jamf checkJSSConnection” and “jamf listUsers” recon commands (especially if python is the parent process)

Other Useful Detections

  • a single python process/PID making lots of outbound network connections over time:

basically looking for beaconing activity from python to a C2 server

  • searching for a macOS host connecting to the same destination host and port consistently or intermittently over a period of time

basically looking for beaconing activity to a fixed C2 server

  • searching for unique user agent strings this may seem like an antiquated method, but in some cases this could be useful; example: when JXA is invoked programmatically via an app by default the user-agent string is unique and contains the app’s name. In this case by default the user agent would be formatted as: “<.app_name>/<.app_version> CFNetwork/<version> Darwin/<version>”

searching for user agent strings with Darwin or CFNetwork might yield interesting results

searching for osascript in the user-agent field would also yield interesting results (would indicate that the osascript binary on disk is being used to execute JXA)

  • searching for strange activity with running apps (ex: apps dropping files to disk, prolonged network connections from apps, beaconing-like activity from apps)
  • searching for apps with the hidden attribute set to true; an example of how to do this in Swift code is below:

Useful Artifact Sources on macOS

Next I would like to walk through some of the various parts of the macOS system that are useful for blue teamers when analyzing compromised macOS machines. I learned a lot on this subject when I took on the challenge to rewrite Thomas Reed’s PICT (Post Infection Collection Toolkit) over into Swift as a side project. Here’s the link to Thomas Reed’s PICT (written in python):

Here is the slightly different Swift version of PICT that I wrote:

Below is a brief overview of different log sources on macOS that I learned are useful for blue teamers looking to dig in and analyze activity on an infected macOS host. The PICT tool (listed above) will pull most of the artifacts below for you in the event that you have a macOS host you suspect is infected. Below is a quick walkthrough of some of the important artifacts gathered by PICT that would be of use to defenders during post infection analysis:

  • QuarantineEventsV2 database:

→/Users/<username>/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2; **additional info is included in the next section on how to query this sqlite3 database**

— →would have info on any files downloaded via a browser or via email; example of how this could be useful is investigating how/when a malicious app was downloaded to the victim system

  • Browser History Databases:

→Safari sqlite3 history database: /Users/<user>/Library/Safari/History.db

→Chrome sqlite3 history database: /Users/<user>/Library/Application Support/Google/Chrome/Default/History

→Firefox sqlite3 history database: /Users/<user>/Library/Application Support/Firefox/Profiles/<random>.default-release/places.sqlite

→ **additional info is included in the next section on how to query these sqlite3 database**

— →would help with investigating browser activity around the time of a compromise or alert

  • Common Persistence Data:

# osascript -e ‘tell application “System Events” to get the path of every login item’ (note you can also execute this command programmatically in Swift, such as in the example below):

→/Library/StartupItems/: startup items directory

→/System/Library/StartupItems/: startup items directory

→#kextstat: list kernel extensions

#crontab -l: list cron jobs

→#defaults read com.apple.loginwindow LoginHook: check for login hooks

/Library/LaunchAgents: system launch agents directory; you can search each launch agent found for plist dictionary values containing:

— — — →“/.” (i.e., a hidden file or an executable binary)

— — — → “/tmp”

— — — → “/var/folders/”

— — — → “/Users/Shared/”

— — — → “/Library/Containers/”

— — — → “/var/root/

— — — → “python”

— — — → “sh”

— — — → “/bin/sh”

— — — → “java”

— — — → “curl”

— — — → “exec”

— — — → “base64”

/Library/LaunchDaemons: system launch daemons directory; you can search each launch daemon found for plist dictionary values containing:

— — — →“/.”: (i.e., a hidden file or an executable binary)

— — — → “/tmp”

— — — → “/var/folders/”

— — — → “/Users/Shared/”

— — — → “/Library/Containers/”

— — — → “/var/root/

— — — → “python”

— — — → “sh”

— — — → “/bin/sh”

— — — → “java”

— — — → “curl”

— — — → “exec”

— — — → “base64”

/User/<username>/Library/LaunchAgents/: user launch agents directory; you can search each launch agent found for plist dictionary values containing:

— — — →“/.”: (i.e., a hidden file or an executable binary)

— — — → “/tmp”

— — — → “/var/folders/”

— — — → “/Users/Shared/”

— — — → “/Library/Containers/”

— — — → “/var/root/

— — — → “python”

— — — → “sh”

— — — → “/bin/sh”

— — — → “java”

— — — → “curl”

— — — → “exec”

— — — → “base64”

#launchctl list: list launch daemons/agents

/var/at/jobs/: jobs directory

/etc/security/audit_warn: check to see if it has been edited

/etc/launchd.conf: check for suspicious entries

  • Browser Extension Files:

→Chrome: For each user on the system, you can iterate through /Users/<username>/Library/Application Support/Google/Chrome/Default/Extensions/<random>/<version_number>/manifest.json and pull the “name”, “description”, and “permissions” data.

→Firefox: For each user on the system, you can iterate through /Users/<username>/Library/Application Support/Firefox/Profiles/<random>.default-release/extensions and search for .xpi files

→Safari: For each user on the system, you can iterate through /Users/<username>/Library/Safari/Extensions and search for .safariextz extensions.

  • Listing Of All Installs:

/private/var/db/receipts/: this folder contains a list of packages and apps that have been installed and when each was installed

→/Library/Receipts/InstallHistory.plist: plist with a list of installations and install dates for each

  • System Logs:

→/var/log/*, /var/audit/*

→#log collect <time period> — output <out_path>

  • Useful Network Info:

→#scutil — dns

→#scutil — proxy

→#pfctl -s rules

/etc/hosts file

  • Useful Process Info:

→#ps axo user,pid,ppid,start,time,command: list username, process ID, parent process ID, start time, run time, and command info per running process

#ps axo pid,comm

→#lsof -i: process network info

  • Profile Info:

→#profiles show -all

  • Other Places to Check:

/tmp/: search for suspicious scripts/files

/var/folders/: search for suspicious scripts/files

/Users/Shared/: search for suspicious scripts/files

/Library/Containers/: search for suspicious scripts/files

/etc/hosts: (check to see if sites like apple.com, virustotal.com, malwarebytes.com, etc. are blocked)

/etc/sudoers: check users listed

/Users/<user>/.bash_history: search bash command history

  • Enumerate Running App Info:

→ Swift code to list running apps, PID, launch date, hidden attribute, and path:

More Info On Querying the sqlite3 Databases Above

  • Querying QuarantineEventsDatabaseV2:

In a nutshell, the history of any downloads with a quarantine flag is kept here. You can use a sqlite3 editor to run sql statements against this database and pull back information such as timestamps, the source app that downloaded the file with the quarantine flag, the file name, and the source URL. This information could prove to be useful when investigating a macOS host and searching for files downloaded around a certain time. An example of how to query this data using Swift is below:

In the code snippet above, after you enumerate all users on the system and put the users you find into an array, you can then run a for loop for each user where you check for the presence of the QuarantineEventsV2 database and if found read the contents. In my example above, I am reading the timestamp, app (or agent bundle identifier) and , URL.

  • Querying Browser History Databases:

Using Swift, you can use the code segment above to connect to the sqlite3 browser history databases and query info. Just replace the value of the “queryString” variable above with the appropriate query below:

Safari: “select datetime(history_visits.visit_time + 978307200, ‘unixepoch’) as last_visited, history_items.url from history_visits, history_items where history_visits.history_item=history_items.id order by last_visited;”

Chrome: “select datetime(last_visit_time/1000000–11644473600, \”unixepoch\”) as last_visited, url, title from urls order by last_visited;”

Firefox: “select datetime(visit_date/1000000,’unixepoch’) as time, url FROM moz_places, moz_historyvisits where moz_places.id=moz_historyvisits.place_id order by time;”

Tracing Syscalls on macOS

Simple command you can use when analyzing binaries on macOS:

“sudo dtruss ./<binary> <binary arg1> <binary arg2>…”

This can help identify helpful pieces of information such as what files were touched or opened by a binary during run time. This information can be especially helpful when analyzing attack tools to get a better understanding of how it works (especially malware leveraging API calls).

I hope you found the information here useful in helping expand macOS detections and in helping with post infection analysis of macOS hosts. As macOS offensive techniques continue to evolve, I am sure more techniques will need to be covered here.

Red Teaming with a Blue Team Mentaility

Posts from a blue teamer turned red teamer

Cedric Owens

Written by

Blue teamer turned red teamer but blue teamer at heart. Twitter: @cedowens

Red Teaming with a Blue Team Mentaility

Posts from a blue teamer turned red teamer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade