| Mechanism | Required privilege | Source of Truth | Collection / Triage | Trigger | What to review |
|---|---|---|---|---|---|
|
Launch Agents
[S1]
| Mixed | Both |
Artifact / Path
/System/Library/LaunchAgents/Library/LaunchAgents~/Library/LaunchAgentsResolves to Program / ProgramArguments[0]Referenced MachServices / Sockets if presentEnumerate
launchctl print gui/<uid>find ~/Library/LaunchAgents /Library/LaunchAgents -maxdepth 1 -name '*.plist'Inspect
launchctl print gui/<uid>/<label>launchctl blame gui/<uid>/<label>plutil -p <plist> | User login / GUI session | Parse Label, Program, ProgramArguments, RunAtLoad, KeepAlive, WatchPaths, QueueDirectories, StartCalendarInterval, PathState, Sockets, and MachServices. Use launchctl blame to identify the actual trigger that loaded a service: this distinguishes a job started by RunAtLoad from one started by WatchPaths, Sockets, or MachServices, which matters for hunting on-demand persistence. Prioritize targets outside Apple or vendor-controlled paths and jobs present on disk but absent or odd in live launchctl state. Scope and required privilege vary by path: user-scoped in ~/Library, system-scoped in /Library. |
|
Launch Daemons
[S2]
| Root | Both |
Artifact / Path
/System/Library/LaunchDaemons/Library/LaunchDaemonsResolves to Program / ProgramArguments[0]Referenced service endpoints and loaded binary pathEnumerate
launchctl print systemfind /Library/LaunchDaemons -maxdepth 1 -name '*.plist'Inspect
launchctl print system/<label>launchctl blame system/<label>plutil -p <plist> | Boot / pre-login | Very high-value persistence. Review launchd keys such as RunAtLoad, KeepAlive, StartCalendarInterval, and PathState, plus ownership, signer, Team ID, and whether the target binary lives in a user-writable or otherwise weak path. Use launchctl blame on the live service-target to surface the real trigger (RunAtLoad vs Sockets / MachServices / WatchPaths) instead of relying on plist intent alone. |
|
launchd overrides / disabled state
[S3]
| Mixed | State |
Artifact / Path
Persistent disabled / override stateResolves to Effective enabled / disabled state for a labelFinal live state shown by launchctl, not just plist intentEnumerate
launchctl print-disabled systemlaunchctl print-disabled gui/<uid>Inspect
launchctl print system/<label>launchctl print gui/<uid>/<label> | Persistent state across reboot | All versions; storage details vary. Do not trust plist review alone: disabled or override state can persist independently of the plist, and the exact storage location has changed over time. For offline triage, the backing files are typically /var/db/com.apple.xpc.launchd/disabled.plist (system domain) and /var/db/com.apple.xpc.launchd/disabled.<UID>.plist (user domain), though these paths are subject to change across OS versions. |
|
BTM / SMAppService
[S4]
| Mixed | Both |
Artifact / Path
~/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm (legacy / pre-Ventura)/private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v*.btm (Ventura+, versioned BTM store)Resolves to Registered helper binary or app bundle in BTMOwning app bundle, Team ID, and registration recordEnumerate
sudo sfltool dumpbtmInspect
plutil -p ~/Library/Application\ Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btmcodesign -dv --verbose=4 <helper_or_owner_app>BackgroundItems-v*.btm stores are not simple text plists. plutil -p can be useful but incomplete depending on format; prefer sudo sfltool dumpbtm or a dedicated BTM parser for triage.
| User login / helper startup | macOS 13+ Background Task Management / SMAppService model. Prefer live/state collection with sudo sfltool dumpbtm; parse on-disk BTM stores as supporting artifacts, not as the only source of truth. Correlate each item with the owning app bundle, embedded helper, launchd registration when present, signer, Team ID, and user-facing Background Items state. Treated here as mixed scope because modern SMAppService can register login items as well as launch-item style services. |
|
Login Items (legacy / classic)
[S4a]
| User | Both |
Artifact / Path
Legacy login item records / shared file list stateResolved app or helper path launched at user loginResolves to App bundle or helper registered for loginOwning app, signer, Team ID, and user-scoped registrationEnumerate
osascript -e 'tell application "System Events" to get the properties of every login item'plutil -p <legacy_login_item_store_when_present>Inspect
codesign -dv --verbose=4 <login_item_or_owner_app>ls -la <resolved_login_item_path> | User login | Legacy / classic login items predate BTM / SMAppService. Backing files and exact storage vary by OS generation, so verify on the target OS and map each registration to the real app or helper on disk. |
|
Embedded Login Helpers
[S5]
| User | Both |
Artifact / Path
<App>.app/Contents/Library/LoginItemsResolves to Embedded helper app / binaryOwner app bundle, signer, Team ID, and expected install locationEnumerate
find /Applications ~/Applications -path '*/Contents/Library/LoginItems/*'Inspect
codesign -dv --verbose=4 <helper>plutil -p <owner_app>/Contents/Info.plist | Login / app-managed helper start | Common in legitimate software too. Treat as modern app-bundled persistence and baseline Bundle ID, Team ID, signer, and owner app. Focus on signer mismatches, stale paths, and helpers embedded in unusual app locations. On disk confirms helper presence; live or login-item/BTM state confirms enablement. |
|
Privileged Helper Tools
[S6]
| Admin | Both |
Artifact / Path
/Library/PrivilegedHelperTools/Library/LaunchDaemons<App>.app/Contents/Library/LaunchDaemons<App>.app/Contents/Library/LaunchAgentsResolves to Root-privileged helper binary and its corresponding launchd registrationOwning app, embedded helper source, Bundle ID, and signer relationshipEnumerate
ls -la /Library/PrivilegedHelperTools /Library/LaunchDaemons 2>/dev/nullfind /Applications ~/Applications \( -path '*/Contents/Library/LaunchDaemons/*' -o -path '*/Contents/Library/LaunchAgents/*' \) 2>/dev/nullInspect
plutil -p <App>.app/Contents/Library/LaunchDaemons/*.plist <App>.app/Contents/Library/LaunchAgents/*.plist 2>/dev/nullcodesign -dv --verbose=4 /Library/PrivilegedHelperTools/<helper>launchctl print system/<label> | Boot / on-demand XPC activation | These helpers are high-value because they bridge a user-facing app to a root-context service. Apple's legacy install path is SMJobBless, which drops the helper in /Library/PrivilegedHelperTools and registers a matching LaunchDaemon; on macOS 13+ this API is deprecated in favor of SMAppService.daemon(plistName:), which registers the daemon directly from the app bundle without writing to /Library/PrivilegedHelperTools. Baseline each helper against its owning app, verify the helper binary, the matching LaunchDaemon plist (or SMAppService registration), and whether the signer / Team ID chain is coherent end-to-end. Stale helpers without a present owner app are especially suspicious. |
| Mechanism | Required privilege | Source of Truth | Collection / Triage | Trigger | What to review |
|---|---|---|---|---|---|
|
SSH authorized_keys
[S7][S7a]
| Mixed | Both |
Artifact / Path
~/.ssh/authorized_keys/Users/*/.ssh/authorized_keys/var/root/.ssh/authorized_keys/etc/ssh/sshd_config/etc/ssh/sshd_config.d/AuthorizedKeysFile / AuthorizedKeysCommand effective policyResolves to Authorized key entries actually accepted by sshdAuthorizedKeysFile path(s), AuthorizedKeysCommand, forced command= options, principals, source restrictions, and signer contextWhether Remote Login / sshd is enabled and which users or groups can authenticateEnumerate
sudo find /Users /var/root -path '*/.ssh/authorized_keys' -type f -print 2>/dev/nullsudo grep -R -nE 'AuthorizedKeysFile|AuthorizedKeysCommand|AuthorizedKeysCommandUser|AuthorizedPrincipalsFile|PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|Include|Match' /etc/ssh 2>/dev/nullsshd -T 2>/dev/null | grep -Ei 'authorizedkeys|authorizedprincipals|permitrootlogin|passwordauthentication|pubkeyauthentication'sudo systemsetup -getremotelogin 2>/dev/nullInspect
while IFS= read -r f; do echo "### $f"; ssh-keygen -lf "$f" 2>/dev/null; sudo grep -nEv '^\s*(#|$)' "$f" 2>/dev/null; done < <(sudo find /Users /var/root -path '*/.ssh/authorized_keys' -type f -print 2>/dev/null)sudo stat -f '%Sp %Su %Sg %N' /Users/*/.ssh /Users/*/.ssh/authorized_keys /var/root/.ssh /var/root/.ssh/authorized_keys 2>/dev/nullsudo grep -R -nEv '^\s*(#|$)' /etc/ssh 2>/dev/null | SSH connection | Access persistence rather than local autostart. Inspect every user's authorized_keys, including /var/root, not just the current shell user's ~/.ssh. Also inspect effective sshd configuration with sshd -T because AuthorizedKeysFile, AuthorizedKeysCommand, Include, and Match blocks can redirect key lookup away from the default path. Review key options such as command=, from=, environment=, permitopen=, principals/cert-authority markers, and unusual comments or reused keys. High priority findings include keys on shared admin accounts, root keys, custom AuthorizedKeysCommand, broad PermitRootLogin, recently modified authorized_keys files, weak ownership or permissions, and keys whose provenance does not match the user's normal administration workflow. |
|
SSH rc
[S8]
| User | Disk |
Artifact / Path
~/.ssh/rc~/.security/ (common chained helper drop)Resolves to Command or script executed at SSH session startHidden helper scripts or background launchers chained from the rc fileEnumerate
find /Users -path '*/.ssh/rc' 2>/dev/nullfind /Users -path '*/.security/*' 2>/dev/nullInspect
grep -n '' ~/.ssh/rc 2>/dev/nullgrep -R -n '' ~/.security 2>/dev/null | SSH login / session start | Separate from authorized_keys. .ssh/rc runs after authentication and can quietly chain a hidden helper, nohup launcher, or process-guard wrapper. Hunt for appended shell, ps | grep checks, and per-user hidden directories that only exist to support the rc path. |
|
PAM stack / pam.d configuration
[S9][S9a]
| Root | Disk |
Artifact / Path
/etc/pam.d/sudo/etc/pam.d/sshd/etc/pam.d/login/etc/pam.d/su/etc/pam.d/screensaver/etc/pam.d/ (full directory)/usr/lib/pam/ (SIP-protected module location)Resolves to PAM service config that loads a module on auth eventsModule path referenced (.so / .so.2), often pam_exec.so.2 with a script argumentScript or binary invoked via pam_exec on each matching authenticationEnumerate
ls -la /etc/pam.d/grep -nE '^(auth|account|password|session)' /etc/pam.d/* 2>/dev/nullgrep -rn 'pam_exec' /etc/pam.d/ 2>/dev/nullInspect
cat /etc/pam.d/sudo /etc/pam.d/sshd /etc/pam.d/loginshasum -a 256 /etc/pam.d/*ls -la /usr/lib/pam/codesign -dv --verbose=4 /usr/lib/pam/<module>.so.2 2>/dev/null | Authentication event for the matching service (sudo / su / login / sshd / screensaver) | macOS uses OpenPAM (not Linux-PAM). Module references in pam.d files accept both pam_module.so and pam_module.so.2. /usr/lib/pam is SIP-protected on modern macOS, so dropping a new module file there is not realistic without breaking SIP; the practical attack surface is editing /etc/pam.d/* (admin-writable) to add a pam_exec.so.2 line that runs an attacker-controlled script in any admin-writable location. Highest-value config files for DFIR are /etc/pam.d/sudo (executes on every sudo) and /etc/pam.d/sshd (executes on every SSH auth). Examine each line for unfamiliar modules, paths to user-writable directories, or pam_exec invocations referencing non-Apple scripts. Compare against a clean macOS baseline of the same release: most files are short and stable across releases, so any new line, any non-Apple module path, and any pam_exec are immediate priorities. Note that codesign on /usr/lib/pam/*.so.2 only validates Apple-shipped modules under SIP; the realistic attack surface is the /etc/pam.d/* config files themselves, so prefer hash-based comparison (shasum -a 256) against a clean baseline of the same macOS release. |
|
sudoers / sudo policy
[S10][S10a]
| Root | Disk |
Artifact / Path
/etc/sudoers/etc/sudoers.d//private/etc/sudoers/private/etc/sudoers.d/Resolves to Effective sudo policy, command aliases, environment inheritance, and password prompt behaviorUser, group, host, runas, command, and Defaults entries that can grant privilege persistenceEnumerate
sudo visudo -csudo ls -la /etc/sudoers /etc/sudoers.d 2>/dev/nullsudo grep -R -nE 'NOPASSWD|SETENV|env_keep|secure_path|timestamp_timeout|!authenticate|Cmnd_Alias|Runas_Alias|User_Alias|includedir|#include' /etc/sudoers /etc/sudoers.d 2>/dev/nullInspect
sudo sed -n '1,220p' /etc/sudoerssudo find /etc/sudoers.d -maxdepth 1 -type f -print -exec sed -n '1,220p' {} \; 2>/dev/nullsudo stat -f '%Sp %Su %Sg %N' /etc/sudoers /etc/sudoers.d/* 2>/dev/null | sudo invocation / privilege elevation | Access and privilege persistence rather than local autostart. Review sudoers entries for NOPASSWD, SETENV, env_keep, secure_path weakening, timestamp_timeout changes, !authenticate, broad command aliases, unexpected includedir/include files, and commands that point to user-writable scripts or directories. Treat NOPASSWD for administrative users, writable command targets, and SETENV combined with preserved loader/interpreter variables as high priority. Validate syntax with visudo -c before interpreting unusual policy, and compare against a clean baseline for the same macOS release and enterprise build standard. Keep this row separate from AuthorizationDB: AuthorizationDB governs Authorization Services / SecurityAgent rights, while sudoers governs sudo policy. |
|
AuthorizationDB rules
[S11][S11a]
| Admin | Both |
Artifact / Path
/var/db/auth.db (modern, SQLite-backed authorization rules)/System/Library/Security/authorization.plist (Apple-shipped reference defaults)/etc/authorization (legacy single-file plist; absent on modern macOS)/Library/Security/SecurityAgentPlugins/ (custom mechanism plug-ins referenced by rights)Resolves to Authorization right rule (e.g. system.login.console, authenticate, system.login.screensaver) and the mechanism chain it invokesSecurityAgent plug-in bundle in /Library/Security/SecurityAgentPlugins/ that runs when a referencing right is evaluatedEnumerate
security authorizationdb read system.login.consolesecurity authorizationdb read authenticatesecurity authorizationdb read system.login.screensaversudo sqlite3 /var/db/auth.db '.tables'sudo sqlite3 /var/db/auth.db "SELECT name FROM rules ORDER BY name;"Inspect
security authorizationdb read <right>ls -la /Library/Security/SecurityAgentPlugins/codesign -dv --verbose=4 /Library/Security/SecurityAgentPlugins/<bundle>plutil -p /System/Library/Security/authorization.plist | Authorization right evaluation (login, screen unlock, GUI authorization prompts, custom rights) | AuthorizationDB rights govern when and how SecurityAgent plug-ins (authorization plug-ins in /Library/Security/SecurityAgentPlugins/) are invoked. Pair this row with the SecurityAgentPlugins coverage in the Application / daemon plug-ins row in section 04: this row is the policy that decides when, that row is the bundle that gets loaded. Modern storage is /var/db/auth.db (SQLite); the legacy /etc/authorization plist is no longer used on current macOS. Highest-value rights for persistence are system.login.console (every console login), system.login.screensaver (every screen unlock), authenticate (every authenticate call), and any custom right that adds a non-Apple mechanism. The strongest indicator is a mechanisms array on a high-traffic right that points to a third-party SecurityAgent plug-in. Compare current right definitions against /System/Library/Security/authorization.plist (Apple's shipped reference) to identify rights with non-default mechanisms. SIP protects /System/Library/Security/authorization.plist but admin can still modify the live rules via security authorizationdb write, so absence of file changes does not rule out tampering. Do not treat AuthorizationDB as the primary sudo policy surface: sudo policy is handled separately through sudoers, sudoers.d, PAM, and sudo configuration. |
|
Local accounts / hidden users / Remote Login
[S12][S12a][S12b]
| Mixed | Both |
Artifact / Path
Directory Services local user records/var/db/dslocal/nodes/Default/users/*.plist/var/db/dslocal/nodes/Default/groups/*.plist/Library/Preferences/com.apple.loginwindow.plist/etc/ssh/sshd_configRemote Login / system sshd stateResolves to Local users, hidden users, UID/GID, shell, home directory, admin group membership, and SSH-accessible accountsSecureToken / volume ownership context that can materially affect access persistence and recovery capabilityRemote Login state and sshd launchd stateEnumerate
dscl . list /Users UniqueIDdscl . list /Users UserShelldscl . list /Groups GroupMembershipdseditgroup -o read adminsudo defaults read /Library/Preferences/com.apple.loginwindow HiddenUsersList 2>/dev/nullsudo systemsetup -getremotelogin 2>/dev/nullsudo launchctl print system/com.openssh.sshd 2>/dev/nullInspect
for u in $(dscl . list /Users | grep -v '^_'); do echo "### $u"; dscl . read /Users/$u UniqueID PrimaryGroupID NFSHomeDirectory UserShell IsHidden AuthenticationAuthority 2>/dev/null; donefor u in $(dscl . list /Users | grep -v '^_'); do sysadminctl -secureTokenStatus "$u" 2>/dev/null; donesudo plutil -p /var/db/dslocal/nodes/Default/users/<user>.plistsudo grep -R -nEv '^\s*(#|$)' /etc/ssh 2>/dev/null | Interactive login / SSH login / admin authorization | Access persistence rather than autostart. Hunt for newly created local users, hidden accounts, unexpected admin group members, root-equivalent UIDs, unusual shells, homes in nonstandard paths, Remote Login enabled outside policy, SSH access for accounts that should not be remotely reachable, and SecureToken / AuthenticationAuthority state that gives an account durable recovery or FileVault-adjacent control. HiddenUsersList and IsHidden affect UI visibility, not authentication by themselves, so pair loginwindow preferences with dscl, group membership, sshd policy, and actual login evidence. Treat service accounts and enterprise break-glass accounts as baseline-required rather than automatically malicious, but require provenance. |
| Mechanism | Required privilege | Source of Truth | Collection / Triage | Trigger | What to review |
|---|---|---|---|---|---|
|
Shell init (zsh)
[S13]
| Mixed | Disk |
Artifact / Path
/etc/zshenv/etc/zprofile/etc/zshrc/etc/zlogin/etc/zlogout~/.zshenv~/.zprofile~/.zshrc~/.zlogin~/.zlogoutResolves to Commands sourced by zsh startup filesZDOTDIR-resolved files and downstream scripts / binariesEnumerate
echo $ZDOTDIRls -la ~/.z* /etc/z*Inspect
grep -nEv '^\s*(#|$)' ~/.zshenv ~/.zprofile ~/.zshrc ~/.zlogin ~/.zlogout /etc/zshenv /etc/zprofile /etc/zshrc /etc/zlogin /etc/zlogout | Login shell / interactive shell / logout | zsh is the default shell on current macOS, but not exclusive to a single release boundary. Resolve ZDOTDIR before concluding nothing changed, or you can miss relocated per-user dotfiles and hidden sourced files. Also follow chained helper scripts in hidden per-user directories such as ~/.security/ when the init file only launches a second stage. |
|
Shell init (bash / sh)
[S14]
| Mixed | Disk |
Artifact / Path
/etc/profile/etc/bashrc~/.bash_profile~/.bash_login~/.bashrc~/.bash_logout~/.profileResolves to Commands sourced by bash / sh startup filesReferenced scripts, PATH entries, and ENV / BASH_ENV targetsEnumerate
env | grep -E 'PATH|ENV|BASH_ENV'ls -la ~/.bash* ~/.profile /etc/profile /etc/bashrcInspect
grep -nEv '^\s*(#|$)' ~/.bash_profile ~/.bash_login ~/.bashrc ~/.bash_logout ~/.profile /etc/profile /etc/bashrc | Login shell / interactive shell | bash remains present and is still common on older, migrated, admin, CI, and developer estates. Look for hidden sourcing, PATH abuse, curl or osascript launchers, and suspicious environment exports. Also inspect hidden helper directories such as ~/.security/ when the profile simply hands execution off to another script. |
|
Cron
[S15]
| Mixed | Disk |
Artifact / Path
/etc/crontab/usr/lib/cron/tabs/Resolves to Command field and invoked script pathPer-user or system crontab entry actually executedEnumerate
crontab -lsudo ls -la /usr/lib/cron/tabs/ /etc/crontabfor u in $(dscl . list /Users | grep -v '^_'); do sudo crontab -u "$u" -l 2>/dev/null && echo "[$u]"; doneInspect
sudo grep -nEv '^\s*(#|$)' /etc/crontab /usr/lib/cron/tabs/* | Schedule / periodic | More common on older or mixed estates than on launchd-first modern deployments. macOS-specific quirk: per-user crontabs live in /usr/lib/cron/tabs/, not /var/cron/tabs as on most BSD/Linux references. crontab -l only shows the current user's jobs, so iterate over local accounts (or list the spool directly with root) to avoid missing entries. Verify targets for user-writable paths, temp directories, masquerading names, and hidden payloads staged in odd locations such as ~/Public/Drop Box/.share.sh. |
|
At / atrun
[S16]
| Mixed | Both |
Artifact / Path
at queue / spoolat.allow / at.denyResolves to Queued command / script in the at jobatrun-driven one-shot execution targetEnumerate
atqInspect
at -c <job>grep -n '' /etc/at.allow /etc/at.deny | One-shot deferred execution | Largely theoretical on default macOS: /usr/libexec/atrun is disabled out of the box, so submitted at jobs do not actually fire until an attacker (or admin) enables it with root via sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist. On Ventura+, prefer the modern verbs: sudo launchctl enable system/com.apple.atrun followed by sudo launchctl bootstrap system /System/Library/LaunchDaemons/com.apple.atrun.plist. The legacy load -w flag is deprecated and may behave inconsistently on modern launchctl. Treat any host where atrun is loaded and at jobs are queued as a strong, version-independent indicator. Prefer command-based review (atq, at -c <job>) over a single hardcoded spool path and focus on jobs that launch from temp, shared, hidden, or user-writable locations. |
|
Periodic Jobs
[S17]
| Root | Disk |
Artifact / Path
/etc/periodic/daily/etc/periodic/weekly/etc/periodic/monthly/usr/local/etc/periodic/daily/usr/local/etc/periodic/weekly/usr/local/etc/periodic/monthly/etc/defaults/periodic.conf/etc/periodic.conf (override; absent by default)Resolves to Script executed from periodic dirsAny custom path introduced through periodic.conf or local periodic tree overridesEnumerate
ls -la /etc/periodic/daily /etc/periodic/weekly /etc/periodic/monthly /usr/local/etc/periodic/daily /usr/local/etc/periodic/weekly /usr/local/etc/periodic/monthly 2>/dev/nullInspect
grep -R '.' /etc/periodic /usr/local/etc/periodic /etc/defaults/periodic.conf /etc/periodic.conf 2>/dev/null | Daily / weekly / monthly maintenance | Low-frequency but worth keeping for exhaustive modern macOS triage. Note that /etc/defaults/periodic.conf is the shipped default; /etc/periodic.conf does not exist by default and is only read if created as a local override, so its mere presence is itself worth investigating. Also check locally installed periodic trees such as /usr/local/etc/periodic on mixed admin / Homebrew-heavy estates. This path is the Homebrew-convention local override tree and is the most common source of non-Apple periodic scripts on developer and mixed-admin estates. Review inherited script paths, shellouts, and permissions rather than assuming stock maintenance content. |
|
Calendar Alerts / EventKit
[S17a]
| User | Disk |
Artifact / Path
~/Library/Calendars/~/Library/Calendars/Calendar Cache~/Library/Preferences/com.apple.iCal.plistResolves to Event alarm / procedure target and schedule stored in the user's calendar databaseCalendar hide state such as DisabledCalendars and the app path or bookmark tied to the alertEnumerate
find ~/Library/Calendars -maxdepth 2 -type f 2>/dev/nulldefaults read com.apple.iCal DisabledCalendars 2>/dev/nullInspect
plutil -p ~/Library/Preferences/com.apple.iCal.plist 2>/dev/nullsqlite3 ~/Library/Calendars/Calendar\ Cache '.tables' 2>/dev/null | Event alert / recurrence | Calendar persistence is noisy because many users have legitimate events, but procedure alarms and hidden calendars are still worth checking when execution appears time- or reminder-driven. Focus on alarms tied to unexpected apps or bookmarks, recurring events with no business context, and calendars hidden through DisabledCalendars. Important: the user-facing Run AppleScript alarm action was dropped around Mojave and EventKit programmatic alarms are constrained on current macOS; verify that alarm-driven execution actually fires on the target OS before treating this as an active autostart. |
|
Emond
[S18]
| Root | Disk |
Artifact / Path
/etc/emond.d/rules//private/var/db/emondClients/System/Library/LaunchDaemons/com.apple.emond.plistResolves to Action command or script referenced by the ruleCondition-to-action chain that leads to code executionEnumerate
ls -la /etc/emond.d/rules /private/var/db/emondClientsInspect
plutil -p /System/Library/LaunchDaemons/com.apple.emond.plistgrep -R '.' /etc/emond.d/rules | Event-driven (startup, auth, policy) | Apple removed emond entirely in macOS 13 Ventura: the /sbin/emond binary and /System/Library/LaunchDaemons/com.apple.emond.plist no longer ship. On any modern fleet (13+), the presence of /etc/emond.d/rules/*, /private/var/db/emondClients, or any emond-related plist is itself a strong indicator because the mechanism does not exist natively. On ≤12 hosts, treat unexplained rules, clients, or odd actions as immediate priority. |
| Mechanism | Required privilege | Source of Truth | Collection / Triage | Trigger | What to review |
|---|---|---|---|---|---|
|
System Extensions
[S19]
| AdminApproval | State |
Artifact / Path
<App>.app/Contents/Library/SystemExtensions/Library/SystemExtensionsOwning app bundleCurrent activation stateResolves to Activated system extension and owning appBundle ID, Team ID, and activation state shown by systemextensionsctlEnumerate
systemextensionsctl listOffline partial
find /Applications /Library /Users -path '*/Contents/Library/SystemExtensions/*' -print 2>/dev/nullInspect
codesign -dv --verbose=4 <owning_app>systemextensionsctl list | grep -E 'enabled|active' | Boot / activation / app-managed load | macOS 10.15+ current model, distinct from legacy KEXTs. Live state from systemextensionsctl list remains the preferred source for activation/enabled status. Offline review is partial: use app bundles, embedded system extension bundles, /Library/SystemExtensions when present, signatures, Team IDs, entitlements, bundle IDs, and MDM approval policy to reconstruct intent, but do not infer active/enabled state from disk artifacts alone. |
|
KEXTs
[S20]
| RootApproval | Both |
Artifact / Path
/Library/Extensions/System/Library/ExtensionsResolves to Loaded kext bundle ID and pathActual loaded extension plus the on-disk bundle, whether user-installed or Apple-shippedEnumerate
kmutil showloadedkextstatfind /Library/Extensions -maxdepth 1 -name '*.kext' -printInspect
codesign -dv --verbose=4 <kext>kmutil inspect -b <bundle_id> | Boot / extension load | Legacy mechanism. Kernel extensions are deprecated from macOS 10.15 onward in favor of System Extensions / DriverKit, and their use is increasingly restricted on 11+, but they are still possible under constrained approval and security-policy conditions. On Apple silicon, third-party KEXT loading normally requires Reduced Security and explicit administrative approval. A non-Apple KEXT that is loaded or approved on modern Apple silicon should be treated as a high-value DFIR finding and correlated with Startup Security Utility policy, MDM KEXT policy, Team ID, bundle ID, notarization, and install timeline. |
|
Chromium Extensions
[S21][S21e][S21f]
| Mixed | Both |
Artifact / Path
~/Library/Application Support/Google/Chrome/*/Extensions~/Library/Application Support/Google/Chrome/*/Preferences/Library/Managed Preferences/com.google.Chrome.plistResolves to Extension ID, manifest, and effective policy sourceForce-installed or managed extension config tied to a real profileEnumerate
find ~/Library/Application\ Support/Google/Chrome -path '*/Extensions/*'plutil -p /Library/Managed\ Preferences/com.google.Chrome.plist 2>/dev/null | grep -E 'ExtensionInstallForcelist|ExtensionSettings'Inspect
grep -n 'extensions' ~/Library/Application\ Support/Google/Chrome/*/Preferencescodesign -dv --verbose=4 <extension_host_app_if_app_mode> | Browser launch / profile load | All versions with Chromium-based browsers; noisy on managed fleets. Review extension IDs, policy-forced installs, managed profiles, update URLs, and any extension granted unusual permissions or loaded from a nonstandard path. Treat Native Messaging Hosts as adjacent high-value pivots when a Chromium extension declares nativeMessaging or calls runtime.connectNative() / runtime.sendNativeMessage(). On macOS, inspect browser-specific host manifests such as /Library/Google/Chrome/NativeMessagingHosts/*.json, ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/*.json, /Library/Application Support/Chromium/NativeMessagingHosts/*.json, ~/Library/Application Support/Chromium/NativeMessagingHosts/*.json, /Library/Microsoft/Edge/NativeMessagingHosts/*.json, and Edge user-data NativeMessagingHosts directories. Parse name, absolute path, type, and allowed_origins; map allowed_origins back to installed extension IDs, then verify the native binary signer, Team ID, notarization, xattrs, receipts, parent app, and path writability. Native hosts are not extensions themselves, but they can turn a browser extension into a durable native execution bridge. Treated as mixed scope because this row covers per-profile installs, user-level native hosts, and managed / policy-backed installs. |
|
Firefox Extensions
[S21][S21a][S21b][S21c][S21d]
| Mixed | Both |
Artifact / Path
~/Library/Application Support/Firefox/Profiles/*/extensions~/Library/Application Support/Firefox/Profiles/*/extensions.json~/Library/Application Support/Firefox/Profiles/*/prefs.js/Applications/Firefox.app/Contents/Resources/distribution/policies.json/Applications/Firefox.app/Contents/Resources/distribution/extensions/Library/Preferences/org.mozilla.firefox.plistFirefox configuration profiles / org.mozilla.firefox payloadsAdjacent pivot only: /Library/Application Support/Mozilla/NativeMessagingHosts/*.jsonAdjacent pivot only: ~/Library/Application Support/Mozilla/NativeMessagingHosts/*.jsonAdjacent pivot only: /Library/Application Support/Mozilla/ManagedStorage/*.jsonAdjacent pivot only: ~/Library/Application Support/Mozilla/ManagedStorage/*.jsonAdjacent pivot only: /Library/Application Support/Mozilla/PKCS11Modules/*.jsonAdjacent pivot only: ~/Library/Application Support/Mozilla/PKCS11Modules/*.jsonResolves to Firefox add-on ID, XPI or unpacked extension, manifest.json, effective profile statePolicy-forced / blocked extension state from ExtensionSettings or distribution policiesOptional native manifest path mapped to an extension ID when nativeMessaging / managed storage / PKCS#11 is usedEnumerate
find ~/Library/Application\ Support/Firefox/Profiles -maxdepth 3 \( -name extensions.json -o -name prefs.js -o -path '*/extensions/*' \) 2>/dev/nullfind /Applications ~/Applications -path '*/Firefox.app/Contents/Resources/distribution/*' 2>/dev/nulldefaults read /Library/Preferences/org.mozilla.firefox 2>/dev/nullprofiles show -type configuration 2>/dev/null | grep -A60 -Ei 'org\.mozilla\.firefox|ExtensionSettings|Extensions'find /Library/Application\ Support/Mozilla ~/Library/Application\ Support/Mozilla -maxdepth 3 \( -path '*NativeMessagingHosts*' -o -path '*ManagedStorage*' -o -path '*PKCS11Modules*' \) 2>/dev/nullInspect
python3 -m json.tool ~/Library/Application\ Support/Firefox/Profiles/<profile>/extensions.jsonunzip -p <extension.xpi> manifest.json | python3 -m json.toolpython3 -m json.tool /Applications/Firefox.app/Contents/Resources/distribution/policies.jsonplutil -p /Library/Preferences/org.mozilla.firefox.plist 2>/dev/nullcat <native_manifest.json> | python3 -m json.tool | Browser launch / profile load | All current Firefox; noisy on managed fleets. Treat privilege as mixed: profile extensions are user-scoped, while bundle distribution, global policies, and MDM-backed org.mozilla.firefox payloads require admin/root or management control. Review extension ID, version, install location, sourceURI/updateURL, signedState when present, active/visible state, profile path, XPI manifest permissions, and browser_specific_settings.gecko.id. Prioritize risky permissions such as <all_urls>, webRequest, webRequestBlocking, cookies, downloads, history, tabs, proxy, management, nativeMessaging, privacy, clipboardWrite, and clipboardRead. Correlate user-installed extensions with ExtensionSettings, Extensions, InstallAddonsPermission, BlockAboutAddons, DisableFirefoxStudies, and related policies when present. Native manifests are not the extension itself, but they are high-value pivots when an extension declares or uses native messaging, managed storage, or PKCS#11. Do not automatically classify a policy-forced extension as malicious; verify MDM, configuration profile, app owner, deployment source, and business need. Offline artifacts usually carry the useful evidence; validate effective state live with Firefox, about:policies, and about:support when available. |
|
Safari App Extensions
[S22]
| UserApproval | Both |
Artifact / Path
Owning app bundle / appexSafari extension state / preferencesResolves to appex / owner app bundle actually providing the extensionStored extension state tied to the installed owner appEnumerate
pluginkit -mAv | grep -i safarifind /Applications ~/Applications -name '*.appex'Inspect
codesign -dv --verbose=4 <appex_or_owner_app>find ~/Library/Containers/com.apple.Safari/Data/Library/Preferences -name "*.plist" -exec plutil -p {} \; 2>/dev/null | Browser launch / extension activation | Safari App Extensions are the older app/appex-backed model, introduced with Safari 10 (macOS 10.12 Sierra; also available as a Safari 10 update on 10.11.6 El Capitan). Review the owner app, signer, Team ID, and whether stored state matches the app actually present on disk. Treated as mixed scope because extension state is user-specific while the owning app may live in either user or global application space. |
|
Safari Web Extensions
[S22a][S22b][S22c]
| UserApproval | Both |
Artifact / Path
Owning app bundle / web extension payloadSafari extension state / preferencesResolves to Safari web extension actually providing the browser codeStored enablement / permissions tied to the installed owner appEnumerate
pluginkit -mAv | grep -i safarifind /Applications ~/Applications -name '*.appex'Inspect
codesign -dv --verbose=4 <appex_or_owner_app>find ~/Library/Containers/com.apple.Safari/Data/Library/Preferences -name "*.plist" -exec plutil -p {} \; 2>/dev/null | Browser launch / extension activation | Safari Web Extensions are the current model on Safari 14+. Review the owner app, signer, Team ID, extension permissions, and whether the stored state matches the installed app and expected browser profile. Treat Safari native-app messaging as an adjacent pivot: Safari Web Extensions can communicate with their containing native app, but this is not a Chromium-style external NativeMessagingHosts manifest directory. Inspect the owner app and appex bundle, Info.plist, entitlements, extension resources, native app messaging code paths, package receipts, provenance, and Safari enablement / permission state. Treated as mixed scope because extension state is user-specific while the owning app may live in either user or global application space. |
|
Finder Sync Extensions
[S23]
| UserApproval | Both |
Artifact / Path
<App>.app/Contents/PlugIns/*.appexpluginkit registration / enablement stateResolves to Registered Finder Sync appex actually loaded by FinderOwning app, Bundle ID, Team ID, and enablement stateEnumerate
pluginkit -mAv | grep -i FinderSyncfind /Applications ~/Applications -path '*/Contents/PlugIns/*.appex' 2>/dev/nullInspect
plutil -p <appex>/Contents/Info.plistcodesign -dv --verbose=4 <appex_or_owner_app> | Login / Finder launch / extension activation | Finder Sync extensions sit in a useful middle ground for persistence: user-approved, app-backed, and easy to hide among legitimate file-management tools. Baseline the owner app and signer, then verify that the appex present on disk matches the pluginkit state and the Finder-facing functionality the app claims to provide. |
|
Application / daemon plug-ins
[S24]
| Mixed | Both |
Artifact / Path
App / daemon-specific plug-in directories/Library/Security/SecurityAgentPlugins/ (authorization plug-ins)/Library/DirectoryServices/PlugIns/ (Open Directory)~/Library/iTunes/iTunes Plug-ins (historical; iTunes removed in 10.15)~/Library/QuickLook / /Library/QuickLook/Library/Spotlight~/Library/Application Support/Sublime Text */Packages~/.vim/plugin~/Library/Application Support/xbar/pluginsResolves to Actual plug-in bundle, script, or dylib loaded by the host app or daemonOwning host, signer, and execution context inherited from that hostEnumerate
pluginkit -mAvqlmanage -m plugins 2>/dev/nullmdimport -L 2>/dev/nullfind ~/.vim/plugin ~/Library/Application\ Support \( -path '*/Sublime Text */Packages/*' -o -path '*/xbar/plugins/*' \) 2>/dev/nullInspect
codesign -dv --verbose=4 <plugin_bundle>find ~/Library /Library -type d \( -name '*Plug-Ins*' -o -name 'QuickLook' -o -name 'Spotlight' \) 2>/dev/nullgrep -R -n '' ~/.vim/plugin ~/Library/Application\ Support/Sublime\ Text* ~/Library/Application\ Support/xbar/plugins 2>/dev/null | Host app / daemon launch or plug-in discovery | Covers non-browser plug-in persistence such as app-specific plug-ins and daemon plug-ins, plus user-space script/plugin loaders such as Sublime packages, Vim plug-ins, and xbar plug-ins. Two especially sensitive subclasses deserve dedicated attention: authorization plug-ins in /Library/Security/SecurityAgentPlugins/ (loaded by authorizationhost / SecurityAgent, can intercept credential prompts and run very early in the auth chain). On modern macOS, authorization plug-ins are subject to notarization and signing expectations; a non-Apple, non-matching-Team-ID plug-in in this directory is essentially always an investigation-priority finding regardless of other context. The second subclass is Open Directory plug-ins in /Library/DirectoryServices/PlugIns/ (loaded by opendirectoryd, root-context, very high impact). For all rows, focus on writable plug-in directories, unexpected host / signer pairings, bundles that inherit sensitive host privileges or sandbox exceptions, and script-based plug-ins that quietly shell out when the host app starts. |
|
Application Support helpers
[S25]
| User | Disk |
Artifact / Path
~/Library/Application Support/Suspicious executables, scripts, or persistence-style plists under app support subdirsResolves to Helper, launcher, updater, monitor, or config file that causes a second-stage autostart pathOwning app-support directory and any executable or plist it referencesEnumerate
find ~/Library/Application\ Support -maxdepth 4 \( -perm -111 -o -name '*.plist' -o -name '*.sh' -o -name '*.py' \) 2>/dev/nullInspect
grep -R -nE 'Program|ProgramArguments|RunAtLoad|KeepAlive|StartOnMount|LaunchOnlyOnce' ~/Library/Application\ Support 2>/dev/nullcodesign -dv --verbose=4 <suspect_binary> | App launch / helper launch / chained autostart | This is a broad but useful DFIR sweep for user-space helpers hidden in Application Support. Most entries will be benign app data, so prioritize executable files, shell / Python scripts, and plists that reference launch-style keys or suspicious helper names such as updater, daemon, loader, injector, monitor, or launcher. |
|
Application startup scripts
[S26]
| Mixed | Disk |
Artifact / Path
~/.atom/init.coffee~/Library/Application Support/iTerm2/Scripts/AutoLaunch/iTerm.py~/Library/Application Support/Sublime Text/Packages/User/~/Library/Application Support/Sublime Text/Installed Packages/Resolves to Shell / interpreter command the host app launches on openApp resource or user script that re-executes the payload when the app startsEnumerate
ls -la ~/.atom/init.coffee ~/Library/Application\ Support/iTerm2/Scripts/AutoLaunch ~/Library/Application\ Support/Sublime\ Text/Packages/User 2>/dev/nullInspect
grep -n '' ~/.atom/init.coffee ~/Library/Application\ Support/iTerm2/Scripts/AutoLaunch/iTerm.py ~/Library/Application\ Support/Sublime\ Text/Packages/User/*.py 2>/dev/null | App launch / first window | Some userland persistence modifies startup scripts instead of registering with launchd. Focus on appended shellouts, Python os.system usage, spawned child processes, and unexpected edits inside otherwise legitimate app resources or profile-local script folders. Note: Atom was sunset by GitHub in December 2022; the presence of ~/.atom/ on a 2025+ host is itself an anomaly worth noting. |
|
App preference triggers
[S27]
| User | Disk |
Artifact / Path
~/Library/Preferences/com.apple.dock.plist~/Library/Preferences/com.apple.Terminal.plistResolves to Dock tile target or Terminal CommandString actually reached by user actionBundle path or script path hidden behind the preference stateEnumerate
defaults read com.apple.dock persistent-apps 2>/dev/nulldefaults read com.apple.Terminal 2>/dev/nullInspect
plutil -p ~/Library/Preferences/com.apple.Terminal.plist ~/Library/Preferences/com.apple.dock.plist 2>/dev/null | Dock click / new Terminal window | These are user-space preference surfaces rather than classic autostarts. Review Dock persistent-apps bookmark data and bundle IDs, and Terminal Window Settings command strings. Unexpected killall Dock, killall -HUP Terminal, or killall -HUP cfprefsd activity nearby can help explain when the malicious state was applied. Screen saver bundles and selection state are tracked separately in the dedicated Screen Saver bundles row. |
|
Python Startup Hooks
[S30]
| Mixed | Disk |
Artifact / Path
.pthsitecustomize.pyusercustomize.pyResolves to Imported module or code run on interpreter start.pth executable lines, sitecustomize, or usercustomize targetsEnumerate
python3 -m sitefind ~/Library /usr/local /opt/homebrew \( -name '*.pth' -o -name 'sitecustomize.py' -o -name 'usercustomize.py' \) 2>/dev/nullInspect
grep -n '' <.pth_or_customize_file> | Python interpreter start | Most relevant on dev, automation, and build hosts. Hunt for executable import lines in .pth files, unexpected site-package directories, and hooks that silently reach out to the network or launch other interpreters. Treated as mixed scope because this row covers both user-specific and system-level Python locations. |
|
LaunchServices / URL Handlers
[S31]
| User | Both |
Artifact / Path
LSHandlersCustom scheme / file-handler associations~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plistResolves to Handler Bundle ID and actual registered appCustom scheme or file association that causes the execution pathEnumerate
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump | grep -E 'bindings:|claim *id:|claim *rank:|bundle: *.+\.app'Inspect
plutil -p ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist | File open / URL scheme invocation | All versions; preference and registered-state storage varies. Lower-priority and indirect, but useful when persistence appears to hinge on a document opener or custom URL scheme workflow. |
|
Input Methods / Input Sources
[S29]
| Mixed | Both |
Artifact / Path
/Library/Input Methods/~/Library/Input Methods/~/Library/Preferences/com.apple.HIToolbox.plist~/Library/Preferences/ByHost/com.apple.HIToolbox.*.plistResolves to Input method .app bundle that runs in the user's GUI session when its input source is activatedBundle's executable, signer, and Team IDHIToolbox AppleEnabledInputSources entries identifying which input methods are actually selectedEnumerate
ls -la /Library/Input\ Methods ~/Library/Input\ Methods 2>/dev/nulldefaults read com.apple.HIToolbox AppleEnabledInputSources 2>/dev/nulldefaults -currentHost read com.apple.HIToolbox 2>/dev/nullInspect
plutil -p <input_method.app>/Contents/Info.plistcodesign -dv --verbose=4 <input_method.app> | Input source selection / typing with input method active | Input Method bundles are full .app bundles (typically with LSBackgroundOnly=true) loaded by the system in user GUI context when the user activates the corresponding input source. Persistence requires both a bundle on disk and the input source being enabled in HIToolbox: a bundle without an active HIToolbox entry is dormant. Correlate disk presence with AppleEnabledInputSources on each user. Conversely, a HIToolbox entry without a matching expected bundle, or a bundle in ~/Library/Input Methods that mimics a system-shipped input method name, are strong indicators. Baseline against Apple's shipped input methods under /System/Library/Input Methods/ on the target OS and treat any third-party or unsigned entry in /Library/Input Methods/ or ~/Library/Input Methods/ as worth investigating. Bundles run with the user's TCC permissions. Treated as mixed scope because bundles can live in either user or system Input Methods directories. |
|
Screen Saver bundles
[S41]
| Mixed | Both |
Artifact / Path
/Library/Screen Savers/~/Library/Screen Savers//System/Library/Screen Savers/ (Apple-shipped baseline)~/Library/Preferences/ByHost/com.apple.screensaver.*.plist (selection state)App Extension screensavers (.appex) registered via pluginkit on macOS 14+Resolves to .saver bundle loaded by the legacyScreenSaver host on activationSelected screen saver module identified by moduleDict in the ByHost plistApp Extension at the com.apple.screensaver extension point on Sonoma+Enumerate
ls -la /Library/Screen\ Savers ~/Library/Screen\ Savers 2>/dev/nullls -la ~/Library/Preferences/ByHost/com.apple.screensaver*.plist 2>/dev/nullpluginkit -mAv | grep -i screensaver 2>/dev/nullInspect
plutil -p ~/Library/Preferences/ByHost/com.apple.screensaver*.plistplutil -p <saver_bundle>/Contents/Info.plistcodesign -dv --verbose=4 <saver_bundle_or_appex> | Screen saver activation (idle timeout, hot corner, manual) | Two surfaces. Legacy .saver bundles in /Library/Screen Savers and ~/Library/Screen Savers execute compiled code via the legacyScreenSaver host process when activated; this remains supported on current macOS but is being moved toward an App Extension model. macOS 14 Sonoma introduced App Extension-based screensavers at the com.apple.screensaver extension point, registered through pluginkit and packaged as .appex. Selection state lives in ~/Library/Preferences/ByHost/com.apple.screensaver.<UUID>.plist (moduleDict, idleTime). Persistence requires both the bundle on disk and the moduleDict pointing at it; an unselected .saver is dormant. Baseline against /System/Library/Screen Savers, focus on third-party or unsigned .saver bundles in user/admin library locations, and on appex extensions whose owner app does not justify a screensaver capability. Code runs in the logged-in user context with that user's TCC permissions. |
| Mechanism | Required privilege | Source of Truth | Collection / Triage | Trigger | What to review |
|---|---|---|---|---|---|
|
Installer Packages
[S32]
| Admin | Disk |
Artifact / Path
preinstallpostinstallResolves to preinstall / postinstall script targetDropped payload or persistence artifact created by the packageEnumerate
pkgutil --pkgsInspect
pkgutil --expand <pkg> <dir>spctl --assess --type install <pkg> | Package installation | Usually a deployment or dropper surface rather than final persistence. Review because it often lays down LaunchDaemons, helpers, login items, or extensions. Scope is treated as mixed because packages are a deployment surface that can lay down either user- or system-scoped persistence. |
|
Folder Actions / Automator / Quick Actions
[S28][S28a][S28b][S28c][S28d]
| Mixed | Both |
Artifact / Path
~/Library/Scripts/Folder Action Scripts/Library/Scripts/Folder Action Scripts~/Library/Preferences/com.apple.FolderActionsDispatcher.plist~/Library/Workflows/Applications/Folder Actions~/Library/Services/Library/Services/System/Library/Services (Apple baseline only)*.workflow/Contents/document.wflow*.workflow/Contents/Resources/Scripts/**.scpt / *.scptd / AppleScript / JXA payloads referenced by folder actionsResolves to Watched folder path, attached script path, enabled/disabled Folder Action stateAutomator workflow bundle, document.wflow actions, embedded shell / AppleScript / JXAQuick Action / Service workflow exposed in Finder, Services, or Quick Actions menuEnumerate
find ~/Library/Scripts/Folder\ Action\ Scripts /Library/Scripts/Folder\ Action\ Scripts -maxdepth 2 -type f 2>/dev/nullfind ~/Library/Workflows/Applications/Folder\ Actions ~/Library/Services /Library/Services -name '*.workflow' -maxdepth 4 2>/dev/nullplutil -p ~/Library/Preferences/com.apple.FolderActionsDispatcher.plist 2>/dev/nullosascript -e 'tell application "System Events" to get the name of every folder action' 2>/dev/nullosascript -e 'tell application "System Events" to get the properties of every folder action' 2>/dev/nullInspect
plutil -p <workflow>/Contents/document.wflowfind <workflow>/Contents -maxdepth 5 -type f -printosadecompile <script.scpt> 2>/dev/nullgrep -RInE 'Run Shell Script|do shell script|osascript|curl|python|perl|ruby|zsh|bash|nc |ncat|base64|launchctl' ~/Library/Services ~/Library/Workflows/Applications/Folder\ Actions ~/Library/Scripts/Folder\ Action\ Scripts /Library/Scripts/Folder\ Action\ Scripts 2>/dev/nulllog show --predicate 'process == "FolderActionsDispatcher" OR process == "WorkflowService" OR process == "Automator" OR process == "System Events"' --last 7d 2>/dev/null | Folder event / Finder Services / user-invoked Quick Action | Distinguish Folder Actions from Quick Actions. Folder Actions can be event-triggered persistence through add, remove, open, close, move, or resize activity on a watched folder. Quick Actions / Services are generally user-invoked and should not be treated as automatic persistence unless chained to Folder Actions, Calendar Alarms, LaunchAgents, app login items, or another trigger. Treat privilege as mixed: user-library workflows and scripts are user-scoped, while /Library placements require admin/root. Review sensitive watched folders such as ~/Downloads, ~/Desktop, cloud sync folders, shared folders, email or attachment staging folders, and business drop folders. Review script and workflow ownership, timestamps, quarantine xattrs, parent process or install provenance, and script paths outside standard locations. com.apple.FolderActionsDispatcher.plist may contain nested or base64-encoded plist data, so offline parsing may require recursive decoding. For .workflow bundles, inspect Contents/document.wflow and embedded scripts/resources, not only the bundle name. Prioritize workflows that run shell commands, network tooling, encoded payloads, LaunchAgent creation, Python/Ruby/Perl/JXA/osascript, or binaries in user-writable paths. |
|
Mail.app Rules (AppleScript actions)
[S40][S40a]
| User | Disk |
Artifact / Path
~/Library/Mail/V*/MailData/SyncedRules.plist~/Library/Mail/V*/MailData/RulesActiveState.plist~/Library/Application Scripts/com.apple.mail/ (default location for referenced .scpt / .applescript files)User-specified script paths referenced by individual rulesResolves to Mail rule definition (criteria + action) that triggers on incoming mail matching the criteriaAppleScript file referenced by an "Apply AppleScript" actionPer-rule active/enabled state from RulesActiveState.plistEnumerate
find ~/Library/Mail -path '*/MailData/SyncedRules.plist' 2>/dev/nullfind ~/Library/Mail -path '*/MailData/RulesActiveState.plist' 2>/dev/nullls -la ~/Library/Application\ Scripts/com.apple.mail/ 2>/dev/nullInspect
plutil -p ~/Library/Mail/V*/MailData/SyncedRules.plistplutil -p ~/Library/Mail/V*/MailData/RulesActiveState.plistgrep -RIn '' ~/Library/Application\ Scripts/com.apple.mail/ 2>/dev/nullosadecompile <referenced_script.scpt> 2>/dev/null | Mail.app receives a message matching the rule criteria | Mail rules can chain an "Apply AppleScript" action to arbitrary AppleScript on incoming mail, providing email-triggered code execution in user context. The "Apply AppleScript" action is supported on current macOS (verified on Sequoia 15.x and Tahoe 26 per Apple's user guide). Execution is gated by AppleEvents / Automation TCC: depending on what the script does to other apps, prompts may fire on first use, but a script that confines itself to shellouts via do shell script or to Mail itself can avoid extra prompts. Parse SyncedRules.plist directly because the Mail UI may not render rules whose referenced scripts are missing or malformed. Look for rules with broad criteria (e.g. "every message"), rules referencing scripts in user-writable or hidden paths, scripts that shell out, scripts that call curl / osascript / python, and recently-modified rule plists on accounts where the user does not normally configure Mail rules. Rules sync via iCloud when Mail is iCloud-enabled, so the same rule may appear on multiple devices. Use the V* directory version (V8, V9, V10 depending on Mail version) actually present on disk. |
|
TCC / Accessibility Grants
[S33]
| MixedApproval | Both |
Artifact / Path
/Library/Application Support/com.apple.TCC/TCC.db~/Library/Application Support/com.apple.TCC/TCC.dbResolves to Granted client bundle ID / path and the TCC service it can accessAccessibility, Screen Recording, Full Disk Access, AppleEvents, Input Monitoring, and related grants that make persistence more dangerousEnumerate
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db '.schema access'sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db '.schema access'Query, Big Sur+ / modern schema
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db "SELECT service, client, client_type, auth_value, auth_reason, auth_version, policy_id, last_modified FROM access ORDER BY last_modified DESC;"Query, Mojave / Catalina legacy schema
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db "SELECT service, client, client_type, allowed, prompt_count, last_modified FROM access ORDER BY last_modified DESC;"Inspect
codesign -dv --verbose=4 <granted_binary> | Permission grant / later payload execution | TCC is not persistence by itself, but it materially changes what a persisted binary can do once it runs. Check the database schema before running version-specific queries: Mojave and Catalina commonly use allowed / prompt_count, while Big Sur and later commonly use auth_value, auth_reason, and auth_version. Do not treat auth_value=2 as universal for all 10.14+ hosts. Correlate grants with code signing identity, Team ID, MDM PPPC payloads, user consent history, and whether the client path still exists. Do not treat a missing column as absence of evidence; it may indicate a different macOS TCC schema. Full Disk Access is required to query some protected TCC state reliably. Rows with a non-null policy_id were granted by a configuration profile (PPPC) rather than by user consent; this is the cleanest way to separate MDM-managed grants from interactive ones during triage. |
|
DYLD_* injection / LSEnvironment
[S34]
| Mixed | Disk |
Artifact / Path
LSEnvironment in app Info.plistEnvironmentVariables in launchd plistsDYLD_INSERT_LIBRARIESDYLD_FRAMEWORK_PATHResolves to Injected dylib path and targeted host processPlist or app bundle whose launch path causes the dylib to loadEnumerate
grep -R -n 'DYLD_\|LSEnvironment\|EnvironmentVariables' /Applications ~/Applications ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons 2>/dev/nullInspect
plutil -p <plist_or_Info.plist>codesign -d --entitlements :- <target_app_or_binary> 2>/dev/null | Target process launch | Separate from Mach-O load-command tampering. Modern macOS heavily constrains DYLD_* injection through SIP, Hardened Runtime, code signing, and library validation. com.apple.security.cs.allow-dyld-environment-variables and com.apple.security.cs.disable-library-validation are related but distinct runtime exceptions: the first affects whether DYLD_* environment variables are honored, while the second affects whether non-Apple or different-Team-ID libraries may load. Treat this as viable only when the target binary's signing, entitlements, and runtime constraints make injection possible. Fast triage: look for LSEnvironment or launchd EnvironmentVariables entries, inspect the target with codesign -d --entitlements :- <app>, and verify Hardened Runtime, library validation, Team ID compatibility, and any relevant launch or library constraints before treating the path as realistically injectable. |
|
Application / binary modification
[S35]
| Mixed | Disk |
Artifact / Path
Modified app bundle executable or resourcesTrojanized or infected Mach-O host binaryResolves to Host app or binary that now re-executes malicious codeTampered code signature, loader chain, or embedded persistence logicEnumerate
codesign --verify --deep --strict <app_or_binary>spctl --assess --type execute <app_or_binary>Inspect
codesign -dv --verbose=4 <app_or_binary>otool -l <app_or_binary>shasum -a 256 <app_or_binary> | Host app / binary execution | Broader than added load commands alone. Use this row for trojanized apps, patched bundle contents, and infected binaries that silently restore or re-establish other persistence when the host is launched. Treated as mixed scope because the tampered host may live in user-writable or system-wide locations. |
|
LC_LOAD_DYLIB Addition
[S36]
| Mixed | Disk |
Artifact / Path
Mach-O load commandsResolves to Injected / added dylib pathImpacted Mach-O binary that loads the dylib at runtimeEnumerate
otool -L <binary>Inspect
otool -l <binary>codesign -dv <binary> | Trusted binary execution | Persistence-adjacent binary tampering. Focus on unexpected dylibs from user-writable paths, hidden directories, or signers that do not fit the host baseline. Treated as mixed scope because the modified host binary may live in user-writable or system-wide locations. |
|
Dylib Hijacking / Proxying
[S36a]
| Mixed | Disk |
Artifact / Path
App-local Frameworks / PlugIns / LoginItems dylib search pathsWeak / optional dylib dependencies declared by a host Mach-OProxy dylib carrying an LC_REEXPORT_DYLIB to a renamed copy of the originalResolves to Malicious dylib loaded under an existing trusted host processOriginal library functionality preserved through the proxy re-exportEnumerate
otool -l <host_binary> | grep -E 'LC_LOAD_DYLIB|LC_LOAD_WEAK_DYLIB|LC_REEXPORT_DYLIB' -A 3find <App>.app/Contents \( -name '*.dylib' -o -name '*.framework' \)Inspect
otool -l <suspect_dylib> | grep -A 3 LC_REEXPORT_DYLIBcodesign -dv --verbose=4 <suspect_dylib>codesign -d --entitlements :- <host_app> 2>/dev/null | Host app launch / dynamic load | Distinct from a simple LC_LOAD_DYLIB addition. Two related techniques: (1) dylib hijacking exploits a host that searches multiple directories for a dylib (or has a weak / optional dependency that does not exist), letting the attacker drop a malicious dylib of the same name in the first searched / writable location; (2) dylib proxying replaces a real dependency with a malicious dylib that carries an LC_REEXPORT_DYLIB command pointing to a renamed copy of the original, so functionality is preserved while the proxy executes its constructor inside the trusted host. Both inherit the host's TCC permissions (camera, mic, etc.). Detection pivots: presence of LC_REEXPORT_DYLIB in an app's bundled dylib, dylibs in writable per-app directories whose signer differs from the host, and hardened-runtime hosts carrying com.apple.security.cs.disable-library-validation. |
|
Login Hooks
[S37]
| Mixed | Disk |
Artifact / Path
/Library/Preferences/com.apple.loginwindow.plist~/Library/Preferences/com.apple.loginwindow.plist/private/var/root/Library/Preferences/com.apple.loginwindow.plistLoginHookLogoutHookResolves to LoginHook / LogoutHook script pathCommand or script executed by loginwindow policyEnumerate
sudo defaults read /Library/Preferences/com.apple.loginwindow 2>/dev/nullfind /Users -path '*/Library/Preferences/com.apple.loginwindow.plist' 2>/dev/nulldefaults read ~/Library/Preferences/com.apple.loginwindow 2>/dev/nullplutil -p /private/var/root/Library/Preferences/com.apple.loginwindow.plist 2>/dev/nullInspect
plutil -p /Library/Preferences/com.apple.loginwindow.plistplutil -p ~/Library/Preferences/com.apple.loginwindow.plistplutil -p /private/var/root/Library/Preferences/com.apple.loginwindow.plist | Login / logout | LoginHook/LogoutHook were deprecated in favor of launchd as early as OS X 10.4 (Tiger) per TN2228 and continued to work with progressive tightening through later releases. Hooks can be installed either user-scoped (~/Library/Preferences/com.apple.loginwindow.plist, current user only) or system-wide (/Library/Preferences/com.apple.loginwindow.plist, requires root, fires for every user at login). Some tooling also writes the hook into root's loginwindow preference domain and stages the payload separately under shared locations such as /Users/Shared/.security/. The system-wide form is the more dangerous DFIR variant and is often missed when only the user plist is checked. Execution behavior and practical support are historical / target-OS dependent, so verify on the endpoint rather than assuming a hard upper bound from the badge alone. |
|
Startup Items
[S38]
| Root | Disk |
Artifact / Path
/Library/StartupItemsStartupParameters.plistResolves to StartupItem script or executableStartupParameters.plist-defined item actually launched during bootEnumerate
find /Library/StartupItems -maxdepth 2 -type fInspect
plutil -p /Library/StartupItems/*/StartupParameters.plist | Boot | Legacy mechanism mainly relevant for OS X 10.3 and earlier compatibility. From 10.4 onward, low-level service startup largely moved to launchd, but the artifact still matters in older images, migrations, and niche DFIR cases. |
|
RC Scripts
[S39]
| Root | Disk |
Artifact / Path
rc.localrc.common/private/etc/rc*Resolves to Script body or chained command pathActual command invoked from rc.local / rc.commonEnumerate
find /private/etc -maxdepth 2 -name 'rc*'Inspect
grep -n '' /private/etc/rc* | Boot / init path | Mostly historical on current macOS: like Login Hooks and Startup Items, these mechanisms are deprecated boot paths whose presence on a modern host is itself anomalous and therefore high-priority. Keep for completeness and older-host triage; on modern fleets, treat any non-empty rc.local / rc.common as immediate priority rather than as a first-pass enumeration target. |
.mobileconfig) and MDM-pushed payloads are a deployment surface, not a launchd-style autostart, but they materially shape what runs and what is allowed to run on a Mac. Treat them as a first-class persistence and policy surface: a profile can install login items, allowlist a system extension or kext, grant TCC / PPPC exceptions, install root certificates, push managed preferences for any app, or enroll the device in MDM with remote-management privileges. On a compromised or social-engineered host, a single rogue profile can re-establish other persistence and silently relax defenses.| Mechanism | Required privilege | Source of Truth | Collection / Triage | Trigger | What to review |
|---|---|---|---|---|---|
|
Configuration Profiles (.mobileconfig)
[S42]
| Mixed | Both |
Artifact / Path
/var/db/ConfigurationProfiles/Store//var/db/ConfigurationProfiles/Settings//Library/Managed Preferences/ (resulting managed prefs)/Library/Managed Preferences/<user>/ (per-user managed prefs)Stray .mobileconfig files in Downloads / Desktop / tempResolves to Installed configuration profile, its identifier, and the issuing organizationEffective managed-preference values that override per-app or per-system defaultsEnumerate
sudo profiles listsudo profiles show -type configurationsudo profiles show -type enrollmentsudo profiles show -type configuration -output stdout-xmlfind /Library/Managed\ Preferences -type f 2>/dev/nullfind ~/Downloads ~/Desktop /tmp /var/tmp -name '*.mobileconfig' 2>/dev/nullInspect
sudo profiles show -type configuration -output stdout-xmlsudo ls -la /var/db/ConfigurationProfiles/Store/ /var/db/ConfigurationProfiles/Settings/security cms -D -i <profile.mobileconfig>plutil -p /Library/Managed\ Preferences/<domain>.plist | Profile install / MDM push / managed pref load | Two install paths exist: a user double-clicks a .mobileconfig and approves it in System Settings, or an MDM server pushes it silently to an enrolled device. In both cases the on-disk truth lives under /var/db/ConfigurationProfiles/ while the enforced effects surface in /Library/Managed Preferences/. Use sudo profiles list as the first pivot and profiles show -type configuration -output stdout-xml when you need payload UUIDs, payload identifiers, PPPC service dictionaries, certificate payloads, managed preferences, and policy details in a parseable form. Correlate profile install time, MDM enrollment state, signing chain, payload scope, and removal permissions. Profiles installed outside an MDM enrollment are particularly suspicious. Decode raw .mobileconfig files with security cms -D -i to read their CMS-signed XML payloads. Treated as mixed scope because profiles can target the device, the current user, or both. |
|
High-impact MDM payloads
[S43][S44][S45][S46][S47]
| Mixed | Both |
Artifact / Path
PayloadType com.apple.TCC.configuration-profile-policy (PPPC / privacy)PayloadType com.apple.system-extension-policyPayloadType com.apple.syspolicy.kernel-extension-policyPayloadType com.apple.servicemanagement (managed login items)PayloadType com.apple.security.pkcs1 / .pem / .root (installed certs)PayloadType com.apple.ManagedClient.preferences (custom managed prefs for any app)Resolves to TCC / PPPC policy entry that allows or denies selected privacy services for a client binaryHigh-value allow cases include Full Disk Access / SystemPolicyAllFiles, Accessibility, AppleEvents, PostEvent, and file-scope servicesCamera, Microphone, Screen Recording, and Input Monitoring / ListenEvent require careful interpretation: do not assume PPPC silently grants them unless verified for the target OS and service.Allowlisted Team ID / Bundle ID for system extensions or kextsTrusted root, intermediate, identity, or certificate payload added to system/user trust stores or keychainsManaged or auto-approved login/background item rule matching an app, helper, LaunchAgent, LaunchDaemon, or SMAppService itemEnumerate
sudo profiles listsudo profiles show -type configurationsudo profiles show -type enrollmentsudo profiles show -type configuration -output stdout-xmlsudo profiles show -type configuration | grep -E 'PayloadType|PayloadIdentifier|PayloadOrganization'sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db '.schema access' 2>/dev/nullsecurity find-certificate -a -p /Library/Keychains/System.keychainsystemextensionsctl listInspect
sudo profiles show -type configuration -output stdout-xmlsecurity cms -D -i <profile.mobileconfig> | plutil -p -security cms -V -i <profile.mobileconfig>codesign -dv --verbose=4 <binary_granted_TCC_via_profile> | Profile install / payload activation | These payloads matter because they don't create launchd-style persistence by themselves, but they can materially strengthen another mechanism already on the host. PPPC payloads can grant persistent privacy exceptions for selected services such as Full Disk Access / SystemPolicyAllFiles, Accessibility, AppleEvents, PostEvent, and protected file locations, reducing or removing user-prompt friction for those services. Do not treat every TCC class as silently grantable: Camera, Microphone, Screen Recording, and Input Monitoring / ListenEvent require service-specific interpretation and may be deny-only or user-approval constrained depending on the OS and payload semantics. System-extension and kext policy payloads can pre-allowlist a third-party Team ID, removing approval friction that normally protects extension installs. com.apple.servicemanagement can manage and auto-approve matching login/background items; the actual item still maps back to an app, helper, LaunchAgent, LaunchDaemon, or SMAppService registration. Certificate payloads can install trusted roots or identities, enabling trust-store manipulation or enterprise TLS interception for software that relies on system trust; correctly implemented certificate or public-key pinning may still resist this. com.apple.ManagedClient.preferences is a broad managed-preferences surface: it can push key/value policy into application preference domains and should be interpreted per target domain. Operational caveat: querying the system TCC.db requires the terminal (or your DFIR tool) to hold Full Disk Access, even with sudo, because the file is itself protected by TCC; entries with non-null policy_id are the ones installed by configuration profile rather than by user click. For each profile, decode and review the full payload list, map every PPPC entry to the concrete client binary it empowers, and tie any managed login/background item back to the app, helper, or launchd label it matches. |
|
Declarative Device Management / Managed background tasks
[S48][S48a][S45]
| Mixed | Both |
Artifact / Path
Declarative Device Management background task configurationManaged background task executables / scripts / launchd configurationsMDM-delivered declarations and profile stateBTM / backgroundtaskmanagementagent statelaunchd state for managed service labelsResolves to MDM-deployed executable, script, or launchd configuration used to run a managed background taskDeclaration / profile / MDM server provenance that explains why the task existsEffective launchd / BTM state for managed servicesEnumerate
sudo profiles show -type configurationsudo profiles show -type enrollmentsudo profiles show -type configuration -output stdout-xmlsudo sfltool dumpbtmlaunchctl print systemlog show --predicate 'process == "backgroundtaskmanagementagent" OR process == "profiles" OR process == "mdmclient"' --last 24hInspect
sudo profiles show -type configuration -output stdout-xml | plutil -p -codesign -dv --verbose=4 <managed_task_binary_or_script_owner> 2>&1ls -laO@ <managed_task_path>plutil -p <managed_launchd_plist>launchctl print system/<managed_label> | MDM declaration / managed launchd background task | macOS 15+ can use device management to deploy and control managed background tasks using Declarative Device Management. Local admin rights alone are not sufficient for this management path; this normally requires MDM / declarative management authority on a supervised or otherwise managed device. Treat this as a policy-plane persistence surface: it may be legitimate enterprise management, but it can also explain durable scripts, executables, and launchd configurations that do not look like normal user-installed software. Correlate each managed task with enrollment status, supervision, MDM server identity, declaration/profile provenance, launchd label, BTM/backgroundtaskmanagementagent state, signer, Team ID, ownership, and expected enterprise baseline. Do not classify as malicious solely because the task is MDM-delivered; require mismatch, suspicious payload path, unsigned or unexpected code, unexplained profile/declaration changes, or inconsistency with the managed fleet baseline. |
launchctl print and print-disabled.emond.ZDOTDIR before concluding nothing changed.crontab, atq, and /etc/periodic.sudo profiles list / sudo profiles show -type configuration; flag any profile not pushed by your MDM and review high-impact payloads (PPPC, system-extension and kext allowlists, certificate installs, managed login items).~/Library/LaunchAgents or /Library/LaunchDaemons that points into /tmp, /private/tmp, /Users/Shared, a hidden directory, or an Apple-looking filename in a user-writable path.RunAtLoad + KeepAlive on an unsigned or newly written target, especially when paired with WatchPaths or QueueDirectories on ~/Downloads, Desktop, temp, or shared folders.ZDOTDIR set to an unusual path or shell rc files that silently source another script from a hidden or transient location.systemextensionsctl list or a kext that appears on a host where one is not operationally expected..pth file containing executable import lines, or a new sitecustomize.py / usercustomize.py under an unexpected site-packages directory.xattr -l for com.apple.quarantine and review how the artifact first arrived on the host.