macOS on-disk forensics #

macOS forensics is fundamentally different from Windows: there's no equivalent of the Windows Registry - settings live in thousands of plist (property list) files, and system behavior is logged through the Unified Log, a single structured firehose that replaced syslog/ASL in Sierra (10.12). Most high-value evidence lives in SQLite databases scattered under ~/Library/ and /private/var/db/. Three macOS-specific guardrails shape what attackers can do: SIP (System Integrity Protection) locks down system paths and strips dangerous env vars, Gatekeeper enforces code signing/notarization, and TCC (Transparency, Consent, and Control) gates access to private data (camera, mic, Full Disk Access, Downloads, etc.) via per-app consent prompts stored in TCC.db.

Logs & telemetry

The Unified Log is the backbone of macOS observability, one binary tracestore shared across kernel, launchd, apps, and security subsystems. It's compressed, structured, filterable by predicate language, and rotates fast (default retention is often only days on busy systems), so collecting a .logarchive is a first-24h priority. Supplement it with the per-service text logs (install.log, system.log) and the sysdiagnose bundle, which snapshots system state (process list, netstat, kextstat, powermetrics, running tasks) plus logs into a single tarball.

  • Unified Log - all system logs; log collect --output /tmp/mac.logarchive, then log show --archive /tmp/mac.logarchive --predicate '…' --info --debug --last 7d. Default log show hides info and debug level entries - attackers know this and useful breadcrumbs often live there, so always pass --info --debug. ts: entries store wall-clock timestamps in Mach continuous time internally (ns since boot, like Linux monotonic) + a derived wall-clock. log show output is system local time by default - pass --timezone UTC or use --style ndjson for ISO-8601 UTC. Retention is file-rolling: Persist tracev3 files roll by size; busy systems may retain only 1-7 days. Tools: log (built-in), UnifiedLogReader (mandiant), macos-UnifiedLogs (Mandiant Rust port), Console.app
  • sysdiagnose - sudo sysdiagnose (or ⌃⌥⇧⌘.) - comprehensive triage bundle incl. logs, netstat, kextstat. ts: bundle filename contains the capture timestamp (sysdiagnose_YYYY.MM.DD_HH-MM-SS<TZ>_<model>.tar.gz) and each embedded log carries its native ts. Tools: sysdiagnose-apollo, mac_apt parses the bundle
  • Install.log - /var/log/install.log - package installs incl. PKG payloads. ts: each line prefixed with MMM DD HH:MM:SS<±hhmm>, includes TZ offset (unlike Linux syslog). Rotates infrequently; retention measured in months. Tools: grep, mac_apt, aftermath
  • system.log + DiagnosticReports - crashes may reveal payload names. ts: system.log is RFC3339 with TZ offset. Crash reports (~/Library/Logs/DiagnosticReports/*.ips or legacy .crash) have Date/Time header + Launch Time + per-stack-frame timestamps in Mach continuous time. Tools: mac_apt, Spindump viewer
  • Endpoint Security Framework (ESF) - eslogger (Big Sur+) prints ES events live; the API that Falcon / Jamf Protect / Red Canary / etc. ride. ts: each ES event has a mach_time (continuous ns) and a time (timespec, seconds + ns UTC). Live-only unless you record it; retention is whatever your EDR/collector keeps. Tools: eslogger, Objective-See's ESFPlayground, Red Canary macOS ES utilities
  • Spindump - hang/high-CPU snapshot; useful for stealth miners. ts: report header has Date/Time (local w/ TZ) and End time; stack samples carry per-sample continuous-time offsets. Tools: spindump, Instruments.app

User-activity & filesystem artifacts

These are the "pattern-of-life" sources - what the user ran, when they ran it, what they downloaded, and what the filesystem changed. KnowledgeC and the related biome/DuetKnowledge stores are macOS's hidden crown jewel: a per-user SQLite timeline of app focus, Safari URLs, notifications, Bluetooth pairings, device plug-ins, generated passively by the CoreDuet daemon and often retained for weeks. FSEvents gives you volume-level file change history (not perfect - journal only, no content), Spotlight gives you rich per-file metadata, and the quarantine database + xattr ties downloaded files back to the originating URL and user agent. For TCC, remember: a new entry in TCC.db created outside of a normal consent prompt (e.g., written by root while SIP was disabled, or inherited via com.apple.rootless.install) is a very loud signal.

ArtifactWhat it tells youHow it's generated / key factTimestampsParsers / tools
Quarantine events
~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2 (SQLite)
Downloads - URL, origin, downloading app, dateWhen a quarantine-aware app (browsers, Mail, Messages, AirDrop) saves a downloaded file, LaunchServices adds the com.apple.quarantine xattr (flags;hex-time;agent;UUID) and a row here keyed by the same UUID. Gatekeeper approval rewrites the flag (e.g. 0081 → 00c1) and leaves the xattr in place; a missing xattr on a downloaded file suggests deliberate stripping (xattr -d / -c) or a non-quarantining path (curl, git, scp). Ventura+ also adds com.apple.provenance.LSQuarantineTimeStamp = Mac Absolute Time (seconds since 2001-01-01 UTC), set when the download completed. The xattr's second field is its own hex Unix-epoch time.sqlite3, mac_apt, Aftermath, xattr -p com.apple.quarantine
KnowledgeC
~/Library/Application Support/Knowledge/knowledgeC.db, /private/var/db/CoreDuet/Knowledge/knowledgeC.db
App focus and usage, Safari URLs, notifications, screen lock stateCoreDuet (knowledged) records streams of user and device activity - /app/inFocus, /app/usage, /safari/history, /device/isLocked, notifications - passively, per user and system-wide. On recent macOS much of this has moved to Biome (~/Library/Biome/streams/, protobuf SEGB files) - collect both.ZSTARTDATE, ZENDDATE, ZCREATIONDATE = Mac Absolute Time (double, sub-second). Retention varies by stream - weeks for app focus, longer for some device streams. Biome records also use Mac Absolute Time.APOLLO (Sarah Edwards), mac_apt, sqlite3, knowledgeC parser (Mari DeGrazia)
FSEvents
/.fseventsd/ (hidden, per volume)
File-system change history - what changed and in what orderfseventsd receives change notifications from the kernel and writes them in batches to gzip files on each volume: path, event flags (created, renamed, modified, removed, ...) and an event ID. Not every change is guaranteed to be recorded, and old files roll over.No wall-clock time per event - only a monotonic 64-bit event ID per volume. Anchor time with the gzip file's own timestamps (when the batch was flushed) or by matching paths and IDs to another timed source. Don't write "FSEvents at 14:23" without naming the anchor.FSEventsParser (G-C Partners / David Dym), FSEventer, mac_apt
Spotlight metadata
/.Spotlight-V100/, mdls <file>, mdfind
Rich per-file metadata; can outlive the filemds / mdworker importers index file metadata (and content) into the per-volume store as files are created and changed. The store can keep records of deleted files until it's rebuilt or compacted.kMDItemContentCreationDate, kMDItemContentModificationDate, kMDItemLastUsedDate, kMDItemDateAdded = Mac Absolute Time. kMDItemLastUsedDate updates when the file is opened through LaunchServices and often survives deletion, a strong pivot.mdls, mdfind, SpotlightParser (Yogesh Khatri), mac_apt
TCC
User: ~/Library/Application Support/com.apple.TCC/TCC.db; System: /Library/Application Support/com.apple.TCC/TCC.db
Privacy grants - Full Disk Access, Screen Recording, Mic, Camera, Accessibilitytccd writes a row when the user answers a consent prompt, flips a switch in Privacy & Security, or an MDM PPPC profile grants access. The system TCC.db is SIP-protected; the user TCC.db is not signed and can be written by any process with Full Disk Access, a common abuse path. auth_reason: 2 User Consent, 3 User Set, 4 System Set, 5 Service Policy, 6 MDM Policy.access.last_modified = Unix epoch seconds - not Mac Absolute like most of macOS, so easy to mis-convert. A grant with an odd auth_reason or a last_modified inside the incident window is worth explaining.sqlite3, tccutil (reset, not query), Aftermath
Background Task Management
/private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v*.btm
Ventura+ critical. Every LaunchAgent / Daemon, login item and background helper, the primary persistence sweepbackgroundtaskmanagementd registers each login item, launch agent / daemon and helper it discovers or an app registers, and shows the user a "Background Items Added" notification. Print it with sfltool dumpbtm.Keyed-archive plist; items carry Cocoa dates (Mac Absolute Time) for when they were registered or modified. The .btm file's own timestamps show the most recent update.sfltool dumpbtm (built-in), Aftermath, DumpBTM (Objective-See)
Safari / Chrome
~/Library/Safari/*.db; Chrome in ~/Library/Application Support/Google/Chrome/
History, downloads, cookiesBrowsers write history, downloads and cookies to SQLite as the user browses. Chrome on macOS encrypts cookies and saved passwords (v10 prefix, AES) with a key stored in the Keychain item Chrome Safe Storage; v20 app-bound encryption is Windows-only.Safari history_visits.visit_time = Mac Absolute Time. Chrome visits.visit_time, downloads.start_time/end_time and cookie times = WebKit/Chrome time: µs since 1601-01-01 UTC, easy to mistake for Unix µs because the numbers are a similar size.Hindsight (Chrome, obsidianforensics), mac_apt browser plugins, BrowserHistoryView (NirSoft), sqlite3
Keychain
~/Library/Keychains/login.keychain-db, /Library/Keychains/System.keychain
Stored credentials, keys and certificatessecurityd stores passwords, keys and certificates. login.keychain-db is the file-based (CSSM) keychain, unlocked with the user's password; iCloud and data-protection items live in a separate SQLite keychain (keychain-2.db). Dump with security dump-keychain (needs the user's password).File keychain cdat / mdat = 16-byte YYYYMMDDhhmmssZ strings (UTC); the data-protection keychain uses Mac Absolute Time. Shows when a credential was first stored and last changed.security CLI, chainbreaker (n0fate, offline)
APFS snapshots
tmutil listlocalsnapshots /
Point-in-time copies; deleted evidence may survive insideTime Machine creates local APFS snapshots (about hourly while backups run, kept ~24 hours) and macOS takes one before system updates. Read-only, so files deleted afterwards can still be in them - mount and diff.Name encodes the local creation time (com.apple.TimeMachine.YYYY-MM-DD-HHMMSS.local); the snapshot's own metadata holds create_time in UTC nanoseconds since 1970.tmutil, diskutil apfs, apfs-fuse, mac_apt

AI agent artifacts

AI coding agents and assistants run commands, read files and edit code as the user, so their transcripts are often the best execution log on the host - the prompt, every command, and the output the agent saw. The same files collect any secrets the agent read. Paths below are for macOS; ~ = /Users/<user>.

Trap: agent commands run through a non-interactive shell (zsh -c / bash -c), so they usually don't appear in ~/.zsh_history / ~/.bash_history. If EDR shows shells spawned by node, claude, codex, Cursor Helper or Code Helper, the agent transcript is where the command lines and their output are recorded.
AgentWhereWhat it provesFormat, retention, notes
Terminal agents
Claude Code
(Anthropic)
~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl - full transcripts
~/.claude/history.jsonl - every prompt typed, with project path
~/.claude/file-history/ - pre-edit copies of files it changed
~/.claude/shell-snapshots/
~/.claude/debug/
~/.claude.json - per-project MCP servers, trusted folders, account
~/.claude/settings.json - permission allow-lists, hooks; also <repo>/.claude/settings*.json
Prompts, model replies, every tool call (shell command lines, file reads and writes, web fetches) and the tool output, with working directory and git branch. File-history lets you diff what it changed.JSONL, one event per line, ISO-8601 UTC timestamp on each. <encoded-cwd> is the project path with separators and dots turned into -. Transcripts older than cleanupPeriodDays (default 30) are deleted at startup - collect early. Optional OpenTelemetry export (CLAUDE_CODE_ENABLE_TELEMETRY) can mean a central copy in the SIEM. Enterprise policy: /Library/Application Support/ClaudeCode/managed-settings.json.
Codex CLI
(OpenAI)
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl - session transcripts
~/.codex/history.jsonl - prompt history
~/.codex/log/ - TUI / debug log
~/.codex/config.toml - approval mode, sandbox, MCP servers
Prompts, model output, shell commands and results, patches applied, token use.JSONL with timestamp / type / payload per line; the date folders bound session days. Newer builds add a SQLite index and may compress old rollouts (.jsonl.zst). approval_policy = "never", --yolo or --dangerously-bypass-approvals-and-sandbox = commands ran unreviewed.
Gemini CLI
(Google)
~/.gemini/tmp/<project-hash>/chats/ - auto-saved sessions
~/.gemini/tmp/<project-hash>/logs/ - plus checkpoints/ and /chat save tags
~/.gemini/history/<project-hash>/ - shadow git repo (checkpointing, off by default)
~/.gemini/settings.json - and GEMINI.md context files
Conversation and tool calls per project; the shadow repo holds file states before edits.JSON. The hash comes from the absolute project path - open every folder rather than computing it. --yolo / approvalMode = tool calls ran unprompted.
GitHub Copilot CLI~/.copilot/session-state/<session>/events.jsonl - plus workspace.yaml, plan.md, checkpoints/
~/.copilot/session-store.db - SQLite index (newer builds)
~/.copilot/logs/process-<ms-epoch>-<pid>.log
~/.copilot/config.json
Full session event history, plans and file checkpoints; one debug log per process.Older builds used history-session-state/. Log names carry a millisecond epoch and PID, a ready-made process start time to line up with EDR. COPILOT_HOME can relocate the folder.
Amazon Q Developer / Kiro CLI$TMPDIR/qlog/ - logs
~/.aws/amazonq/, ~/.kiro/ - config
chat.log covers startup, SDK calls and networking; AWS warns it can hold file paths, code, command output and account IDs.Temp-dir logs don't survive reboots or temp cleanup. Full prompt logging is an admin setting that writes to S3 - ask whether it's on. --trust-all-tools = no approval prompts.
AiderIn each repo: .aider.chat.history.md, .aider.input.history
~/.aider.conf.yml
Full chat as Markdown plus input history; aider also commits its own edits to git.Lives in the project, not the home dir - sweep for it, and check git log for aider: commits.
IDE agents
GitHub Copilot Chat / agent mode
(VS Code)
~/Library/Application Support/Code/User/workspaceStorage/<hash>/chatSessions/ - chat sessions
~/Library/Application Support/Code/User/workspaceStorage/<hash>/chatEditingSessions/ - files it changed
~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/
~/Library/Application Support/Code/logs/<YYYYMMDDTHHMMSS>/window<n>/exthost/GitHub.copilot-chat/
Per-workspace chats including agent-mode tool calls and terminal commands; editing sessions show what it changed.JSON (newer builds JSONL). Map <hash> to a project with the workspace.json next to it; renaming or moving a project orphans the old folder, which stays on disk. Log folder names are editor launch times (local time).
Cursor~/Library/Application Support/Cursor/User/globalStorage/state.vscdb - table cursorDiskKV: composerData:<id> (session), bubbleId:<id>:<msg> (messages, tool calls, diffs)
~/Library/Application Support/Cursor/User/workspaceStorage/<hash>/state.vscdb - per-workspace chat list (ItemTable)
~/.cursor/mcp.json - and <repo>/.cursor/ rules and project MCP
~/Library/Application Support/Cursor/logs/
Agent conversations, the terminal commands it ran, the diffs it applied.SQLite with JSON values. Composer IDs are UUIDs, not time-ordered - sort on the timestamps inside the JSON. The schema changes between versions, so list keys first (SELECT key FROM cursorDiskKV). The DB can be gigabytes.
Windsurf
(Cascade)
~/.codeium/windsurf/ - mcp_config.json, memories/, Cascade conversation data
~/Library/Application Support/Windsurf/User/
~/Library/Application Support/Windsurf/logs/
Configured MCP servers, auto-generated memories about the user's projects, Cascade activity logs.Conversation files are a proprietary binary format (reported as encoded protobuf) - expect to work from logs, memories and file timestamps. Verify against the build.
Cline / Roo Code
(VS Code extensions)
~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/<task-id>/ - Cline
~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/tasks/<task-id>/ - Roo; same extensions also appear under the Cursor / Windsurf profiles
api_conversation_history.json = exactly what went to and from the model; ui_messages.json = what the user saw, incl. command approvals; task_metadata.json = files touched.Task IDs are millisecond epochs - the folder name is the task start time. checkpoints/ is a shadow git repo of the workspace.
Continue~/.continue/sessions/ - chat sessions (JSON)
~/.continue/config.yaml
~/.continue/dev_data/
Chat sessions and (if on) local usage data.Config shows which model endpoints, including self-hosted ones, it talked to.
Desktop assistants and local models
Claude Desktop~/Library/Logs/Claude/ - logs
~/Library/Application Support/Claude/claude_desktop_config.json - MCP servers
main.log, mcp.log (MCP connections), mcp-server-<name>.log (each server's stderr); the config lists every local MCP server's command line and env vars.Chat content is server side, not on disk. The MCP config often holds API keys in plaintext env vars - treat them as exposed if the host is compromised.
ChatGPT desktop~/Library/Application Support/com.openai.chat/Cached conversations and app state.Content of record is server side (Enterprise Compliance API). The app stored chats in plaintext until July 2024; current builds encrypt the cache, so expect metadata and timestamps rather than readable chats.
Ollama
(local models)
~/.ollama/logs/server.log - server log
~/.ollama/history - ollama run prompts
~/.ollama/models/
API requests with client IP, model pulls (where models came from), interactive prompts.Listens on 11434; if bound to 0.0.0.0, the server log shows who else on the network used it.
LM Studio~/.lmstudio/conversations/ - saved chats (JSON)
~/.lmstudio/ - models, local server settings
Saved chats; local API server configuration.Like Ollama, an exposed local server is an abuse and data-exfil surface.
Server-side copies: enterprise plans keep their own record, often with longer retention than the endpoint - Claude Enterprise / Team audit logs and Compliance API, ChatGPT Enterprise Compliance API, GitHub org audit log (Copilot policy and seat events, not prompts), Google Workspace admin audit for Gemini, Purview for Microsoft 365 Copilot (CopilotInteraction audit records, prompt copies via eDiscovery), and S3 prompt logs for Amazon Q / Kiro. Ask for these in the first data request.

What to look for in the transcripts:

  • Commands it ran - timeline every shell tool call next to EDR process events.
  • Secrets it read - .env, cloud credential files, SSH keys, kube configs, browser profiles. What was read is in the transcript, so the transcript is now a copy of the secret.
  • Where data went - web fetches, curl, git push to unfamiliar remotes, new MCP servers pointing at external URLs.
  • Safety switches off - --dangerously-skip-permissions, --yolo, --trust-all-tools, broad allow rules, auto-approve. Check the settings file's mtime against the incident window.
  • Instructions the user didn't write - prompt injection arrives in tool output (a web page, issue, README or MCP response). Read the tool results just before an odd action.
  • Persistence - a planted CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules, hook or MCP server re-runs the attacker's instructions every session.
  • Agents driven by malware - the August 2025 Nx "s1ngularity" npm compromise ran claude, gemini and q with permission checks off to hunt for secrets. The parent is a package script, not a terminal, and there's no matching user prompt. Record agent and extension versions too: a poisoned Amazon Q VS Code release (1.84.0, July 2025) shipped a wipe prompt.

Inventory (then copy; take SQLite files with their -wal / -shm files, or close the app first - recent rows often live only in the WAL. Check that your KAPE / Velociraptor / UAC targets actually cover these folders):

for h in /Users/* /var/root; do
  for t in .claude .claude.json .codex .gemini .copilot .continue .cursor .codeium .kiro .aws/amazonq .ollama .lmstudio \
           "Library/Application Support/Claude" "Library/Logs/Claude" "Library/Application Support/com.openai.chat" \
           "Library/Application Support/Code/User" "Library/Application Support/Cursor/User" "Library/Application Support/Windsurf/User"; do
    [ -e "$h/$t" ] && ls -ld "$h/$t"
  done
done 2>/dev/null
find /Users -name .aider.chat.history.md 2>/dev/null
Timestamps: JSONL transcripts (Claude Code, Codex, Copilot CLI) carry an ISO-8601 UTC timestamp on every line. Cline task folders and Copilot CLI log names are millisecond Unix epochs. VS Code-family log folders are local-time YYYYMMDDTHHMMSS. SQLite stores (Cursor, state.vscdb) keep epoch-millisecond values inside JSON blobs - convert them and don't trust row order. Paths in this category change often: verify against the tool version on the host.

Persistence on macOS

macOS persistence is plist-heavy and launchd-centric - virtually every "run my thing at boot or login" path routes through launchd, Apple's unified init/service manager, which reads .plist job definitions from a handful of well-known directories. Since Ventura (13) the single most important sweep is BTM (Background Task Management): sfltool dumpbtm enumerates every LaunchAgent, LaunchDaemon, Login Item, and launch-time dylib the system knows about, and the user receives a notification when a new persistence item is installed, which attackers now actively work around by dropping items that masquerade as Apple/MDM entries, or by abusing SIP-protected directories where BTM trusts the publisher. Everything below assumes you've already pulled a BTM dump.

launchd - the dominant persistence surface

  • User LaunchAgents - ~/Library/LaunchAgents/*.plist. Runs as the logged-in user, loaded at that user's login (Aqua session). No admin required to write, which is why it's the single most common macOS persistence path for commodity malware and infostealers. The plist specifies a Label, ProgramArguments (what to run), and triggers like RunAtLoad, KeepAlive, StartInterval, or WatchPaths. Triage: diff against a clean install, inspect every ProgramArguments value, hunt for Apple-lookalike Labels like com.apple.updateagent.
  • System LaunchAgents - /Library/LaunchAgents/*.plist. Same semantics but applies to all users on the box; requires admin. Common MDM-pushed path; abuse looks like a signed-but-malicious vendor plist.
  • LaunchDaemons - /Library/LaunchDaemons/*.plist. Runs as root at system boot regardless of login. Highest-value persistence - requires admin to write, but attackers who've socially engineered a sudo password (osascript fake prompt is the standard play) will drop here. SIP protects /System/Library/LaunchDaemons/ but not /Library/LaunchDaemons/.
  • System-managed launchd jobs - /System/Library/LaunchDaemons/ and /System/Library/LaunchAgents/ are SIP-protected; a modification here means SIP is disabled or the attacker has an entitlement bypass - treat as near-impossible absent nation-state tooling.
  • launchd triggers to understand - plists can specify RunAtLoad (fires immediately), KeepAlive (respawn if killed), StartInterval/StartCalendarInterval (cron-like), WatchPaths (fire on filesystem change, great for file-drop triggers), QueueDirectories, MachServices (XPC endpoint), and LaunchEvents. Multiple keys = redundant persistence.
  • Modern enumeration - launchctl list shows loaded jobs for the current session; launchctl print system and launchctl print gui/<UID> are the modern (Big Sur+) replacements and expose per-domain state including recent exit codes.

Login / session hooks

  • Login Items (modern, BTM-tracked) - the user-facing "open at login" list from System Settings → Login Items. Modern (Ventura+) items register through SMAppService/SMLoginItemSetEnabled and are reflected in BTM (BackgroundItems-v*.btm). Legacy Login Items were stored in com.apple.backgroundtaskmanagementagent / com.apple.loginitems.plist; aftermath, DumpBTM, and KnockKnock all surface them.
  • Login/Logout hooks - sudo defaults write com.apple.loginwindow LoginHook /path/script and the matching LogoutHook. Deprecated since 10.11 but still functional through at least Sonoma; rarely used by legit software, so a hit is high-signal. Stored in /var/root/Library/Preferences/com.apple.loginwindow.plist.
  • loginwindow AutoLaunchedApplicationDictionary - the old per-user "launch at login" plist (~/Library/Preferences/com.apple.loginwindow.plist, key AutoLaunchedApplicationDictionary). Largely superseded by BTM but attackers writing legacy-style persistence to appear low-risk still land here.

Configuration Profiles & MDM

  • Configuration Profiles - /Library/Managed Preferences/, /var/db/ConfigurationProfiles/, enumerated via profiles list. Profiles can push LaunchAgents, add trusted root CAs, pin SSL, change DNS, install extensions, and gate TCC approvals, effectively a legitimate persistence + policy bypass channel. Scattered Spider / Octo Tempest abuses this by social-engineering MDM enrollment of attacker-controlled devices, and infostealer droppers add rogue root CAs via a user-level profile to man-in-the-middle browser traffic. Any profile not pushed by your known MDM is suspect.
  • Rogue MDM enrollment - profiles under /var/db/ConfigurationProfiles/Store/ with unknown payload identifiers; check profiles -P for installed profiles and profiles status -type enrollment for enrollment state.

Scheduled execution

  • cron - /etc/crontab, /var/at/tabs/<user>, /etc/cron.d/. Still works but Apple has quietly deprecated it (no cron is installed by default on new users). Any live crontab on a corp Mac is suspicious - launchd is the sanctioned path.
  • at jobs - /var/at/jobs/; disabled by default but attackers sometimes re-enable with launchctl load /System/Library/LaunchDaemons/com.apple.atrun.plist.
  • Periodic - /etc/periodic/daily/, weekly/, monthly/, driven by /System/Library/LaunchDaemons/com.apple.periodic-*.plist. The /etc/periodic tree is not SIP-protected and a drop here fires on a predictable cadence as root. Low-prevalence, high-signal.

Dynamic linker & library abuse

  • DYLD_INSERT_LIBRARIES - macOS's analog of LD_PRELOAD; forces a dylib into a target process's address space. SIP strips all DYLD_* env vars from the environment of SIP-protected binaries and hardened-runtime binaries, so it only works against third-party apps without the hardened runtime or against processes started by the attacker. Set via a LaunchAgent plist's EnvironmentVariables dict or via an app's shell wrapper. Great for stealthy credential theft against browsers / password managers that opt out of hardening.
  • Dylib hijacking / proxying - Patrick Wardle's classic attack: planting a malicious dylib at a path an app searches before the legit one (@rpath, @loader_path, @executable_path ambiguity), or exporting the legit symbols and forwarding to the real dylib so the app still works. Detect with DylibHijackScanner / Objective-See's DHS, or diff dylib load lists (otool -L) against a clean baseline. Still works against third-party apps lacking library validation.
  • DYLD_FALLBACK_LIBRARY_PATH / DYLD_FRAMEWORK_PATH - alternative load-path env vars; same SIP caveats as above.

Authentication & authorization plugins

  • AuthorizationPlugins - /Library/Security/SecurityAgentPlugins/*.bundle, invoked by Apple's Security Server during authorization right evaluation (login, unlock, admin prompts). A rogue plugin can observe or capture credentials on every authentication. Legitimate uses are rare (Jamf Connect, Okta Device Access, Kerberos SSO) - anything else needs explanation. Enforced loading rules live in /etc/authorization (deprecated) and the authorization database /var/db/auth.db.
  • PAM on macOS - /etc/pam.d/. Same PAM concept as Linux; sshd, sudo, login, authorization all reference it. Rogue PAM modules under /usr/lib/pam/ intercept credentials. Rare on Macs but devastating when present.
  • Keychain Access Control List (ACL) tampering - attackers who already have user password can modify Keychain ACLs so a malicious binary can read specific items without prompting on each access.

Legacy / niche - check on older or high-value systems

  • emond - removed in Ventura (13). If a client is still on Monterey or older, look at /etc/emond.d/rules/*.plist and /private/var/db/emondClients/. Was a classic because the Apple-shipped com.apple.emond.plist LaunchDaemon ran rules under /etc/emond.d/rules/ as root with no file-signature check. Dead on modern macOS - do not cite without the version caveat.
  • Scripting Additions - /Library/ScriptingAdditions/*.osax or ~/Library/ScriptingAdditions/. Legacy AppleScript extension mechanism; loaded by any process using AppleScript. Rare in modern tradecraft.
  • Login window authorization database tampering - rights at system.login.console can be modified to require a rogue authorization right.
  • launchd overrides - /var/db/com.apple.xpc.launchd/disabled.plist and disabled.<UID>.plist; attackers can re-enable Apple-disabled jobs or disable security daemons.
  • Dock / Finder plist tampering - swap the binary a Dock icon or Finder service points at. Rare but has been seen in highly targeted ops.
Rapid macOS persistence sweep (the actual run-list)
  1. sudo sfltool dumpbtm - the big one. Everything launchd knows about at boot.
  2. Objective-See KnockKnock - GUI/CLI equivalent that enumerates ~40 persistence mechanisms and flags unsigned/mismatched.
  3. ls -laR /Library/LaunchAgents /Library/LaunchDaemons ~/Library/LaunchAgents + diff plutil -p against clean baseline.
  4. profiles -P + profiles status -type enrollment for rogue MDM/config profiles.
  5. launchctl print system and launchctl print gui/<UID> - includes last-exit reason, path, and ownership for every loaded service.
  6. defaults read com.apple.loginwindow for Login/Logout hooks; defaults read ~/Library/Preferences/com.apple.loginwindow for AutoLaunched entries.
  7. Diff otool -L for high-value targets (browsers, password managers) vs. clean image; check EnvironmentVariables in every writable plist.
  8. aftermath or mac_apt for a full offline sweep that captures all of the above at once.

Security subsystems

These are the macOS controls attackers must defeat, bypass, or live with - understanding what each one does and what it doesn't cover is critical for interpreting evidence. SIP protects system paths and strips dangerous env vars from protected binaries; disabling it requires a Recovery-mode reboot and is a bright-line "unauthorized access" indicator. Gatekeeper + notarization enforce that Internet-downloaded code is signed and Apple-reviewed, but a user who clicks "Open Anyway" or an attacker who downloads code outside the quarantine path (via curl, git, or a terminal command) bypasses the prompt entirely. TCC gates sensitive-data access per app; abuses typically involve either social-engineering the consent prompt or writing TCC.db directly while SIP is disabled.

  • SIP (System Integrity Protection) - csrutil status. Disabled only via Recovery; disablement is a bright-line indicator.
  • Gatekeeper - spctl --status; policy at /var/db/SystemPolicy, /var/db/SystemPolicyConfiguration.
  • XProtect - Apple's AV; defs at /Library/Apple/System/Library/CoreServices/XProtect.bundle; XProtect Remediator (XProtectRemediator*) scans in background. Detections logged in Unified Log - subsystem == "com.apple.XProtectFramework.PluginAPI".
  • MRT (Malware Removal Tool) - superseded by XProtect Remediator but still present on older OS.
  • Notarization - code must be notarized to run w/o Gatekeeper prompt; CloudKit tickets stapled to app.
  • FileVault - fdesetup status; Institutional Recovery Key for enterprise. For imaging you'll want the recovery key or user password.
  • APFS encryption is per-volume; snapshots inherit.

Modern macOS threat picture (2024-26)

  • Atomic Stealer (AMOS), Cuckoo Stealer, Poseidon, Banshee Stealer - macOS infostealers growing fast; targets Keychain, browser cookies, crypto wallets, Notes.
  • DPRK clusters - RustBucket, KANDYKORN, SmoothOperator (3CX), ObjCShellz; often lure via LinkedIn/dev interviews, drop AppleScript / Mach-O.
  • Delivery via malicious DMG + osascript to pop a fake sudo prompt and steal password → Keychain dump.
  • Rosetta artifacts (/Library/Apple/usr/share/rosetta) for x86-translated payloads on Apple Silicon.

macOS triage tooling

  • aftermath (Jamf - modern go-to; collects + parses in Swift)
  • mac_apt (Yogesh Khatri - most comprehensive offline analysis framework; parses hundreds of artifacts)
  • AutoMacTC (Palantir - dated but still handy for quick triage)
  • macOS UAC (variant of the Unix-like Artifacts Collector)
  • Velociraptor macOS artifacts (ES-based + plist parsers)
  • Yelp OSXCollector (legacy; still works)
  • Recon ITR (SUMURI, commercial, most polished UI)
  • FTK Imager / MacQuisition / BlackBag MacQuisition for logical/physical acquisition
  • Objective-See suite - KnockKnock (persistence), BlockBlock, LuLu (firewall), DumpBTM, WhatsYourSign, ReiKey - Patrick Wardle's defender toolkit
  • APOLLO (Sarah Edwards) - pattern-of-life timeline across SQLite stores
  • PlistBuddy (/usr/libexec/PlistBuddy), plutil, defaults, plistutil, plist-parse for plist analysis