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, thenlog show --archive /tmp/mac.logarchive --predicate '…' --info --debug --last 7d. Defaultlog showhides 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 showoutput is system local time by default - pass--timezone UTCor use--style ndjsonfor ISO-8601 UTC. Retention is file-rolling:Persisttracev3 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 withMMM 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/*.ipsor legacy.crash) haveDate/Timeheader +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 amach_time(continuous ns) and atime(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) andEnd 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.
| Artifact | What it tells you | How it's generated / key fact | Timestamps | Parsers / tools |
|---|---|---|---|---|
Quarantine events~/ (SQLite) | Downloads - URL, origin, downloading app, date | When 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~/, / | App focus and usage, Safari URLs, notifications, screen lock state | CoreDuet (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/ (hidden, per volume) | File-system change history - what changed and in what order | fseventsd 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/, mdls <file>, mdfind | Rich per-file metadata; can outlive the file | mds / 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: ~/; System: / | Privacy grants - Full Disk Access, Screen Recording, Mic, Camera, Accessibility | tccd 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/ | Ventura+ critical. Every LaunchAgent / Daemon, login item and background helper, the primary persistence sweep | backgroundtaskmanagementd 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~/; Chrome in ~/ | History, downloads, cookies | Browsers 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~/, / | Stored credentials, keys and certificates | securityd 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 snapshotstmutil listlocalsnapshots / | Point-in-time copies; deleted evidence may survive inside | Time 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>.
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.| Agent | Where | What it proves | Format, 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. |
| Aider | In 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. |
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 pushto 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,geminiandqwith 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
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 (Aquasession). 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 aLabel,ProgramArguments(what to run), and triggers likeRunAtLoad,KeepAlive,StartInterval, orWatchPaths. Triage: diff against a clean install, inspect everyProgramArgumentsvalue, hunt for Apple-lookalikeLabels likecom.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), andLaunchEvents. Multiple keys = redundant persistence. - Modern enumeration -
launchctl listshows loaded jobs for the current session;launchctl print systemandlaunchctl 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/SMLoginItemSetEnabledand are reflected in BTM (BackgroundItems-v*.btm). Legacy Login Items were stored incom.apple.backgroundtaskmanagementagent/com.apple.loginitems.plist; aftermath, DumpBTM, and KnockKnock all surface them. - Login/Logout hooks -
sudo defaults write com.apple.loginwindow LoginHook /path/scriptand the matchingLogoutHook. 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, keyAutoLaunchedApplicationDictionary). 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 viaprofiles 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; checkprofiles -Pfor installed profiles andprofiles status -type enrollmentfor 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 withlaunchctl 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/periodictree 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 allDYLD_*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'sEnvironmentVariablesdict 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_pathambiguity), 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,authorizationall 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/*.plistand/private/var/db/emondClients/. Was a classic because the Apple-shippedcom.apple.emond.plistLaunchDaemon 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/*.osaxor~/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.consolecan be modified to require a rogue authorization right. - launchd overrides -
/var/db/com.apple.xpc.launchd/disabled.plistanddisabled.<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.
sudo sfltool dumpbtm- the big one. Everything launchd knows about at boot.- Objective-See KnockKnock - GUI/CLI equivalent that enumerates ~40 persistence mechanisms and flags unsigned/mismatched.
ls -laR /Library/LaunchAgents /Library/LaunchDaemons ~/Library/LaunchAgents+ diffplutil -pagainst clean baseline.profiles -P+profiles status -type enrollmentfor rogue MDM/config profiles.launchctl print systemandlaunchctl print gui/<UID>- includes last-exit reason, path, and ownership for every loaded service.defaults read com.apple.loginwindowfor Login/Logout hooks;defaults read ~/Library/Preferences/com.apple.loginwindowfor AutoLaunched entries.- Diff
otool -Lfor high-value targets (browsers, password managers) vs. clean image; checkEnvironmentVariablesin every writable plist. - 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 +
osascriptto 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