Skip to content

macOS Device Forensics: A DFIR Guide to Artifacts and Threat Hunting

An expert guide to macOS device forensics, covering APFS file system architecture, critical artifacts like TCC databases, Keychains, plist configurations, persistence mechanisms, Unified Logging analysis, and DFIR methodologies.

/ ARTICLE
[ FIG. 1 ]
macOS Forensics and Incident Response

Introduction to macOS Forensics

The fastest way to spot someone who’s never done Mac DFIR: they open a terminal and reach for /var/log/system.log. It’s been effectively empty since Sierra. Apple moved logging to a binary store in 2016, and that one change tells you most of what you need to know about this platform — the knowledge transfers from Linux, the muscle memory does not.

Then stack the rest of it. APFS with copy-on-write and snapshots everywhere. SIP locking off directories that root used to own. Secure Enclave holding keys you cannot extract. Apple Silicon, where the boot chain is a different animal entirely and DFU-based acquisition became the norm. Treat a Mac like a BSD box with a nicer window manager and you’ll miss most of the evidence.

Breach response, persistence hunt on an employee laptop, or a formal exam that ends up in front of a lawyer — the workflow below is roughly the same shape. Where it forks is early, on whether the machine is still running:

%%{init: {'theme': 'base', 'themeVariables': {'background': 'transparent', 'mainBkg': 'transparent'}}}%% graph TD A[macOS System Incident Alert] --> B{Investigation Type} B -->|Live Response & Triage| C[Collect Volatile Data: nettop, lsof, active users] B -->|Dead-Box Analysis| D[APFS Imaging via Write-Blocker] C --> E[Extract Artifacts: TCC.db, Unified Logs, plist configs, shell history] D --> E E --> F[Parse Browser Caches, Email, and Keychain Details] F --> G[Timeline Analysis and Correlation] G --> H[Incident Containment and Remediation]

Key Analysis Areas

Seven areas cover the overwhelming majority of macOS cases. Work them in roughly this order and you’ll rarely be surprised:

  1. Unified Logging — the binary replacement for syslog. This is your primary timeline source, and it holds far more than the old system ever did. It also rolls over faster than you’d like, which is why you collect it early.
  2. Persistence Mechanisms — LaunchAgents, LaunchDaemons, login items, shell profiles. Something has to survive the reboot, and macOS offers a short enough list of ways to do it that you can check them all in twenty minutes.
  3. TCC Security Permissions — the consent database. Which app asked for the microphone, which one got full disk access, and when. Underused, and frequently the artefact that breaks a case open.
  4. Plist Configuration Files — binary property lists, scattered everywhere, recording interface history, recent documents, connection records. Tedious to work through. Worth it.
  5. Web Browser Activity — SQLite all the way down. History, downloads, extensions, across Safari, Chrome, and Firefox.
  6. User Sessions and Shell History — who logged in, when, and what they typed.
  7. Email and Communication Caches — Apple Mail keeps .emlx files on disk, so you can often reconstruct correspondence with no server-side access and no legal process against a mail provider.

Advertisement

macOS Forensic Artifacts

The TCC Database

TCC produces every “wants to access your camera” dialog you’ve ever clicked through. What makes it forensically valuable is that the answer gets written to SQLite and stays there — so when a user was talked into granting Full Disk Access to something calling itself a printer utility, there’s a row with a timestamp.

Two databases, and check both. The per-user one is where most of the interesting grants live:

  • System-wide: /Library/Application Support/com.apple.TCC/TCC.db
  • Per-user: ~/Library/Application Support/com.apple.TCC/TCC.db

Pull the grant table straight out of it. service names the capability (kTCCServiceScreenCapture, kTCCServiceMicrophone, kTCCServiceSystemPolicyAllFiles), client is the requesting bundle ID, and a bundle ID that doesn’t match anything you’d expect on a corporate laptop is where you start pulling:

sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db "SELECT client, service, allowed FROM access;"

(Your terminal needs Full Disk Access to read this on a live system — grant it to Terminal or iTerm in System Settings first, or you’ll get a permission error and waste ten minutes assuming the path is wrong. On a mounted image it’s a non-issue.)

Property List (Plist) Files

Plists hold settings, preferences, and a surprising amount of historical state. Most are binary, so cat gives you garbage and a colleague walks past while you’re staring at it. plutil converts:

plutil -convert xml1 /path/to/file.plist -o output.xml

plutil -p prints a readable dump without writing a file, which is usually what you want when you’re just looking. The ones that earn their keep:

  • Network and interface config: /Library/Preferences/SystemConfiguration/preferences.plist
  • Bluetooth connection history: /Library/Preferences/com.apple.Bluetooth.plist
  • Accessibility access grants: ~/Library/Preferences/com.apple.universalaccessAuthWarning.plist

Application Directories

Three locations, and the split matters since Catalina moved the OS onto a sealed read-only volume. Anything under /System is signed and immutable, so it’s the two user-writable paths you’re actually interested in:

  • /Applications — system-wide user-installed apps
  • ~/Applications — apps scoped to the current user
  • /System/Applications — Apple’s core apps on the read-only system volume

Persistence Mechanisms

A payload that dies on reboot is a payload that wasted the operator’s access. So something, somewhere, has to reference it — and macOS gives attackers a fairly short menu. Check all of it; the whole sweep is a coffee’s worth of time.

Launch Daemons and Launch Agents

The workhorse. Agents fire at user login in the user’s context; daemons fire at boot as root. That distinction tells you what privilege the attacker had when they planted it, which is a useful thing to know before you write the timeline.

  • User LaunchAgents: ~/Library/LaunchAgents
  • System-wide LaunchAgents: /Library/LaunchAgents
  • System LaunchDaemons (boot-time, root): /Library/LaunchDaemons
  • Apple-protected (read-only): /System/Library/LaunchAgents and /System/Library/LaunchDaemons

Go straight to ProgramArguments. You’re looking for a path into /tmp, /private/var/folders, or a user’s home directory, and for a binary that codesign -dv refuses to verify. Also check RunAtLoad and KeepAlive set together — that combination means “start me and keep restarting me,” which is what an implant wants and what almost nothing legitimate needs.

One more habit worth building: compare the plist’s filename against its Label key. Legitimate software keeps them consistent. Something naming itself com.apple.softwareupdate.plist while pointing at a binary in a Downloads folder is not subtle once you look.

Shell Profiles

Cheap persistence: append a line, get executed every time a terminal opens. zsh is the default since Catalina, so:

  • ~/.zshrc, ~/.zprofile, ~/.zshenv
  • Legacy bash: ~/.bash_profile, ~/.bashrc, ~/.profile
  • System-wide: /etc/zshrc, /etc/zprofile, /etc/profile

Cron Jobs

Deprecated in favour of launchd, still perfectly functional, and consequently ignored by plenty of checklists. Attackers read the same checklists you do:

  • /etc/crontab and /private/etc/crontab
  • Per-user cron files: /var/at/tabs/<username>

Login Items

Ventura and later track login items through Background Task Management, in a binary plist:

  • ~/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm

It’s an obnoxious format to read by hand. Patrick Wardle’s DumpBTM parses it in a second and is what most people use.

Kernel and System Extensions

Kexts used to be the way drivers and security tools got into the kernel — and the way sophisticated malware did too. Big Sur pushed that into user space via System Extensions and the Endpoint Security framework, which is a real security win and means a .kext on a modern system deserves a hard look rather than a shrug.

  • Third-party kexts: /Library/Extensions
  • System kexts: /System/Library/Extensions
  • System Extensions state: /Library/SystemExtensions/

macOS Forensic Evidence Analysis

Browser Artifacts

Google Chrome

Each Chrome profile gets its own directory, and the interesting parts are SQLite. Copy the files before you query them — SQLite locks, and a live Chrome will either block you or leave you reading stale data with an unmerged WAL sitting alongside. Grab the -wal and -shm files too, or you’ll miss the most recent activity, which is usually the activity you care about.

  • Profile paths:
    • ~/Library/Application Support/Google/Chrome/Default
    • ~/Library/Application Support/Google/Chrome/Profile N (N = profile number)
  • Key databases:
    • History — contains the urls, visits, and downloads tables
    • Downloads — records file paths, source URLs, sizes, and timestamps
  • Extensions directory:
    • ~/Library/Application Support/Google/Chrome/Default/Extensions

Apple Safari

Safari moved into a sandbox container at some point depending on version, so check both paths rather than assuming:

  • ~/Library/Safari
  • Or containerised: ~/Library/Containers/com.apple.Safari/Data/Library/Safari

Key files to examine:

  • History.db — SQLite database with URLs, visit counts, and timestamps
  • LastSession.plist — snapshots the open tabs from the last Safari session
  • Extensions/ — installed Safari extensions

Last ten Chrome URLs with timestamps you can actually read. That /1000000-11644473600 is converting Chrome’s WebKit epoch — microseconds since 1601 — into Unix time. Get this arithmetic wrong and your timeline lands somewhere in the 17th century, which is an easy mistake to make and an embarrassing one to have caught in review:

sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/History "SELECT url, title, datetime(last_visit_time/1000000-11644473600,'unixepoch') FROM urls ORDER BY last_visit_time DESC LIMIT 10;"

For a visual interface to query browser databases, DB Browser for SQLite works well on macOS.

Mozilla Firefox

  • Profile directory: ~/Library/Application Support/Firefox/Profiles/<profile_id>.default
  • Primary database: places.sqlite — history, bookmarks, and downloads all in one file. Firefox uses PRTime here: microseconds since the Unix epoch, not the WebKit one. Different browser, different epoch, same opportunity to produce a wrong timeline.

Email Artifacts

Apple Mail caches messages on disk as individual files. That means you can often reconstruct someone’s correspondence entirely from the endpoint — no warrant to a mail provider, no waiting on legal, no tipping off anyone that you’re looking.

  • Base path: ~/Library/Mail/
  • Subfolders like V9 or V10 contain .mbox mailbox directories
  • Individual messages are stored as .emlx files containing raw headers, message body, and attachment metadata
  • Attachment cache: ~/Library/Mail/V10/<Mailbox-ID>/Attachments/

Useful Forensic Scripts

Finding Encrypted Archives

Staging before exfil almost always involves an archive, and a password-protected one defeats content inspection on the way out. You can’t read what’s inside, but the existence of an encrypted archive in a Downloads folder is itself a finding worth chasing:

#!/bin/bash
# Scan a directory to identify encrypted ZIP archives
root_dir="/Users/username/Downloads"

find "$root_dir" -type f -name "*.zip" | while read -r zip_file; do
    echo "Scanning: $zip_file"
    if 7z l -slt "$zip_file" 2>/dev/null | grep -q "Encrypted = +"; then
        echo "[!] ENCRYPTED ARCHIVE IDENTIFIED: $zip_file"
    fi
done

Detecting Macros in Office Files

Office macros are less dominant than they were once Microsoft started blocking them by default on internet-sourced files, but they haven’t gone away — and on macOS the sandbox escape chain through Office has its own history. OOXML files are just zip archives, so you unzip and look for the VBA container:

#!/bin/bash
# Check Office files for VBA macros
root_dir="/path/to/suspicious/folder"
output_dir="./extracted_payloads"
mkdir -p "$output_dir"

idx=0
find "$root_dir" -type f \( -name "*.xls*" -o -name "*.doc*" \) | while read -r doc_file; do
    echo "Processing: $doc_file"
    unzip -q -o "$doc_file" -d "$output_dir/doc_$idx"
    
    if find "$output_dir/doc_$idx" -name "vbaProject.bin" | grep -q .; then
        echo "[!] WARNING: VBA Macro container detected in $doc_file"
    fi
    ((idx++))
done

User Activity Analysis

Active Users and Running Processes

Activity Monitor works if you’re sitting at the machine, but on a live response you want the command line — scriptable, capturable, and it doesn’t require you to explain to the user why you’re clicking around their laptop:

CommandWhat it shows
whoCurrent active user sessions
wLogged-in users plus their current terminal activity
lastFull login history read from /var/log/utmpx, including reboots
last | grep usernameFiltered login history for a specific account

Shell History Logs

  • Zsh history: ~/.zsh_history
  • Bash history: ~/.bash_history

Read the absence as carefully as the content. An empty .zsh_history on an account with weeks of terminal activity in the Unified Log, a file whose mtime is recent but whose contents stop three months ago, a symlink to /dev/null — all of these are findings. Someone cleared it, and clearing it is a decision.

Also check HISTFILE and HISTSIZE in the shell profiles. Setting HISTSIZE=0 disables logging silently and looks nothing like tampering unless you go looking for it.


Advertisement

macOS System Logging and Unified Log Analysis

Back to the thing I opened with. Since Sierra, logging lives in a binary tracev3 format under /var/db/diagnostics/ and /var/db/uuidtext/, and neither grep nor cat will help you. Apple’s log tool or Console, or a third-party parser like mandiant’s macos-UnifiedLogs if you’re working an image offline.

The trade is worth understanding. ULS captures vastly more than syslog ever did — nearly every process and subsystem, at a level of detail that makes real timeline work possible. It also rotates aggressively, and on a busy machine you may only have days of history, not weeks. Collect the log archive first, before you start poking at the system and generating your own noise in it.

Essential Log Queries

  • Stream live events as they happen:
    log stream --level debug
    
  • Search historical logs for login events:
    log show --predicate 'eventMessage contains "login"' --info --style syslog
    
  • Find privilege escalation and sudo activity:
    log show --predicate 'process == "sudo" or process == "authorizationhost"' --style syslog
    
  • Export logs to a portable archive for off-system analysis:
    sudo log collect --output ~/Desktop/Evidence.logarchive
    
    Do this one first, not last. Add --last 7d if you only need a window; the full archive can run to gigabytes.

Network Forensics and Incident Indicators

Live connections are the most perishable evidence on the box. If there’s an active C2 channel or an exfil transfer running while you’re standing there, that’s a socket, a PID, and a remote address — and all three are gone the moment the process exits or someone pulls the cable. Capture this before anything else in a live response.

Live Network Commands

CommandDescription
nettopReal-time network usage broken down per process and socket
lsof -iOpen file handles tied to network sockets (e.g., lsof -i :443)
tcpdumpPacket capture (e.g., sudo tcpdump -i en0 -w dump.pcap)
iftopTerminal-based interface bandwidth monitor

DNS Hardening During an Incident

Pointing the host at a filtering resolver mid-incident can sever C2 that resolves by name, which is most of it. Understand the cost, though: you’ve just changed the environment you’re investigating, and you’ve potentially told the operator something is wrong. Note the time you did it and put it in the report. Anything hardcoded to an IP sails straight past this regardless.

  • Cloudflare (blocks malware domains):
    • Primary: 1.1.1.2 / Secondary: 1.0.0.2
  • Cloudflare (blocks malware and adult content):
    • Primary: 1.1.1.3 / Secondary: 1.0.0.3
  • Quad9 (threat intelligence-backed blocking):
    • 9.9.9.9

Securing Evidence and Chain of Custody

You can do everything above perfectly and still have it thrown out. Evidence handling is the part that survives a lawyer, and the habits are worth keeping even on internal work where nobody’s going to court — because “internal” has a way of becoming “litigation” eighteen months later.

  1. Use a write-blocker. Hardware where you have it. Read-only mounting is the software fallback, and it’s genuinely weaker — macOS has been known to touch a volume during automount before you get a chance to intervene. Disable automount first if the case is serious:
    diskutil mount readOnly /dev/diskXsY
    
  2. Hash everything immediately. SHA-256 at acquisition, then again before analysis, then again before you hand anything over. Matching hashes are the entire argument that the image you analysed is the image you took. Record them somewhere outside the evidence:
    shasum -a 256 /path/to/image.dmg > checksum.sha256
    
  3. APFS Local Snapshots are the quiet win on this platform. Time Machine drops them hourly whether or not a backup disk is attached, they’re copy-on-write, and they routinely contain files a user deleted days ago believing they were gone. Always check — this has salvaged more cases than any tool on the list below:
    • List available snapshots: tmutil listlocalsnapshots /
    • Mount a snapshot read-only: mount_apfs -s <snapshot_name> / /tmp/snapshot_mount

Forensic and Hardening Software

Host Protection and Encryption

ToolPurpose
MalwarebytesLive malware detection, signature scanning, and cleanup
VeraCryptOn-the-fly encryption for volumes and sensitive storage
GNU Privacy GuardOpen-source OpenPGP implementation for file and email encryption

Dedicated macOS DFIR Tools

ToolWhat it does
mac_apt (macOS Artifact Parser Tool)Open-source Python suite for extracting TCC records, Spotlight data, Safari history, and Unified Logs
Cellebrite BlackLightEnterprise forensics platform for full-disk imaging, volume analysis, and timeline generation
SUMURI RECON LABmacOS-native automated analysis platform for deleted artefact recovery and plist parsing
Objective-See SuiteCollection of open-source tools including KnockKnock (persistence scanner) and BlockBlock (runtime monitor)
ChainbreakerExtracts and decrypts keys and secrets from macOS Keychain database files

Application Cleanup Trails and Deinstallation Auditing

Here’s the part people skip: what isn’t there anymore. Someone installs a remote access tool, uses it, drags it to the Trash, and considers the matter closed. macOS scatters application state across half a dozen directories, and dragging the bundle to the Trash removes exactly one of them. Caches, preferences, logs, and package receipts all stay put.

The two examples below use developer tooling because the footprint is well documented and easy to reason about, but the method is what matters — the same logic finds the RAT that was installed on a Tuesday and deleted on a Thursday.

Case Study: Visual Studio for Mac Cleanup Footprints

Even after the bundle is gone, these paths carry timestamps that place the software on the machine:

sudo rm -rf "/Applications/Visual Studio.app"
rm -rf ~/Library/Caches/VisualStudio
rm -rf ~/Library/Preferences/VisualStudio
rm -rf "~/Library/Preferences/Visual Studio"
rm -rf ~/Library/Logs/VisualStudio

Case Study: Xamarin Development Trails

pkgutil is the one to remember. The receipt database records what was installed and when, and it survives the application being deleted — pkgutil --pkgs on a suspect system is a thirty-second query that frequently contradicts what someone told you during the interview:

sudo rm -rf /Developer/MonoDroid
rm -rf ~/Library/MonoAndroid
# Check whether a package receipt is still registered
sudo pkgutil --forget com.xamarin.android.pkg

Further Learning and Resources

  1. SANS macOS Artifact Hunting Cheat Sheet
  2. DFIR Training Portal
  3. The DFIR Report — Threat Analysis Cases
  4. Objective-See Foundation — macOS Security Tools
  5. VirusTotal

Mac work punishes generalists and rewards anyone willing to learn the platform properly. The formats are proprietary, the tooling is thinner than the Windows ecosystem, and half the public DFIR material still assumes /var/log/system.log means something.

The compensation is that macOS is unusually chatty once you know the paths. Unified Logging records more than any Windows event log ever has. TCC keeps a permanent record of every consent decision. APFS snapshots quietly retain files people are certain they deleted. Learn the seven areas at the top, build the collection into a script so you’re not improvising at 2 a.m., and most Mac cases become straightforward — you’re reading a system that has already written down what happened.


Share article

Subscribe to my newsletter

Receive my case study and the latest articles on my WhatsApp Channel.

Warning

Ask CyberROX AI