The FireAI Security Blog

By FireAI Security & Research Team · Published

Hunting Persistence on macOS: LaunchAgents, Login Items and Background Tasks

Hunting Persistence on macOS: LaunchAgents, Login Items and Background Tasks

"Persistence" is the security term for one specific problem: how does code that ran once arrange to run again, automatically, after a restart or a login, without anyone re-launching it by hand? macOS gives legitimate software a lot of sanctioned ways to do exactly that — an update checker, a menu-bar sync client, a printer driver’s helper — and every one of those mechanisms is equally available to something you would rather were not running at all. This is a tour of the actual places to look, with the real commands, and an honest account of where a network-focused tool like FireAI does and does not belong in that picture.

LaunchAgents and LaunchDaemons: the two big ones

macOS starts almost everything through launchd, driven by property list (.plist) files in a small number of known directories. MITRE ATT&CK’s technique T1543.001 describes the mechanism plainly: at login, a per-user launchd process loads plists from user and system LaunchAgents directories, and a plist with RunAtLoad set to true executes automatically the moment it is loaded — no further action needed from whoever put it there. The technique lists the three locations that matter: /System/Library/LaunchAgents, /Library/LaunchAgents, and ~/Library/LaunchAgents. MITRE notes something worth remembering while you are scrolling through a list of these files: agents installed for persistence are often "disguise[d]... using names resembling legitimate OS or software components" — the file that looks like com.apple.something.plist is worth a second look precisely because it is trying not to get one.

LaunchDaemons, covered by the related technique T1543.004, are the system-wide, no-login-required version: they run as root, starting at boot, from /System/Library/LaunchDaemons/ or /Library/LaunchDaemons/. Because installing one there requires administrative privileges to begin with, MITRE frames the daemon route as a way to convert an initial privileged foothold into something that survives a restart, running with root-level access from then on — which is also why a new, unfamiliar file appearing in /Library/LaunchDaemons is a heavier signal than one appearing in a user’s own LaunchAgents folder.

Terminal — listing what is actually registered
ls -la ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons
# compare this list against what you remember installing; anything you don't
# recognise is worth reading with: plutil -p /path/to/the.plist

A file existing on disk and a job actually being loaded are two different questions, and launchctl print answers the second one. Its man page describes it as printing "information about the specified service or domain" — pointed at a domain like system/ or gui/501/ (501 being a user’s UID), it lists every service and endpoint currently loaded in that context, plus each one’s state:

Terminal — what is actually loaded right now
launchctl print gui/$(id -u)
# example output, trimmed — a real run lists every loaded agent for your session
	"com.apple.someAgent" => {
		active count = 1
		path = /Library/LaunchAgents/com.apple.someAgent.plist
		state = running
	}

Login items and Background Task Management

The user-facing surface for a lot of this is System Settings’ Login Items pane, and it is worth checking with your own eyes, not just the command line. Apple’s support guide describes it directly: you can "choose login items that open automatically when you log in," add or remove them there, and separately allow or deny apps that "perform tasks when the app isn’t open, such as checking for software updates or syncing data" — that second category covers background helpers that are not full login items but still run unattended.

Since macOS Ventura, the system underneath that settings pane is commonly called Background Task Management (BTM) in the security community: a service that tracks every launch agent, launch daemon and login item as it registers itself, which is what lets System Settings show you a live, centralized list instead of you having to go hunting through three plist directories by hand. There is an undocumented command-line tool, sfltool, that some researchers use to query that database more directly with sfltool dumpbtm — Apple ships no man page for it and its output format is not guaranteed to stay stable, so treat it as a research curiosity to try on your own machine rather than something to build a workflow around. The supported, stable way to see the same information is still the Login Items pane in System Settings, or launchctl print for the live state of a specific job.

cron: older, quieter, still there

launchd has been Apple’s preferred scheduler for a long time, but the older Unix cron daemon still ships and still runs anything scheduled in it. The crontab man page describes the file format directly: each line has five time/date fields — minute, hour, day of month, month, day of week — followed by the command to run, with @reboot and similar shorthand strings available in place of the five fields on some systems. crontab -l, per the crontab(1) man page, will "Display the current crontab on standard output" for the current user:

Terminal — checking your own and root’s crontab
crontab -l
sudo crontab -l -u root

An empty result for both is normal on most Macs today — that is exactly why anything in there deserves attention. cron is unglamorous and rarely checked, which is precisely why it still shows up as a fallback persistence location in incident write-ups.

Configuration profiles: persistence with a paper trail

A configuration profile can install a LaunchDaemon, grant privacy permissions, or push settings across a fleet of Macs — legitimately, this is how MDM (mobile device management) works. Illegitimately, a profile is a documented way to make changes stick without touching a plist file directly. The profiles command-line tool lists what is installed: profiles list shows installed profiles, and, as its man page notes, running it as root with -all will "list all configuration profiles on the system" rather than only the current user’s.

Terminal — every configuration profile on the Mac
sudo profiles list -all
sudo profiles show -all

A personal Mac with no MDM enrollment should generally have none, or only ones you installed deliberately (a VPN configuration, a work profile). A profile you do not remember installing is worth investigating before removing, since profiles also supports removal with password protection for exactly that step.

Authorization plugins and the long tail

Beyond the big four above, there is a long tail of smaller, older mechanisms: Directory Service and authorization plugins, Spotlight importers, QuickLook generators, Dock tile plugins, and shell startup files that run every time a new terminal session opens. This is where a purpose-built tool earns its keep over manual checking. Objective-See’s KnockKnock, for instance, enumerates more than twenty categories of persistence locations in one pass — including launch agents and daemons, login items, browser extensions, cron jobs, kernel and system extensions, and authorization and directory service plugins — and shows the code-signing status of what it finds in each one. Its companion, BlockBlock, takes the same list of locations and watches them continuously, alerting the moment something new registers itself; per its own description, it "monitors common persistence locations and alerts whenever a new persistent component is added," showing the responsible process, its signing status, and letting you allow or block on the spot.

Shell startup files: the quiet catch-all

One more location worth a direct look, since it needs no plist and no privileged install at all: shell configuration files. ~/.zshrc, ~/.zprofile and ~/.bash_profile run every time a matching new terminal session opens, and a single appended line — piping to a script, exporting a hijacked PATH, launching a background process — is enough to re-establish a foothold every time you open Terminal, with nothing to load into launchd and nothing for launchctl print to show. Objective-See’s KnockKnock includes exactly this category in its scan, listed alongside launch agents and login items as shell configuration files, for the same reason it belongs in this article: it is common enough, and unglamorous enough, to be worth checking rather than assuming.

Terminal — a quick read, not a substitute for actually reading it
cat -A ~/.zshrc ~/.zprofile ~/.bash_profile 2>/dev/null | less
# -A shows non-printing characters, which surfaces anything hidden with
# trailing whitespace or a carriage return trying to push it off-screen

Where FireAI fits, and where it deliberately does not

To be direct about it: FireAI does not scan /Library/LaunchDaemons, does not read plist files, and makes no attempt to detect a new login item or configuration profile. That is a distinct discipline from what FireAI does, and tools built specifically for it — KnockKnock and BlockBlock among them — already do that job well. What FireAI watches is the step that comes after persistence, and that every one of these mechanisms eventually needs if it is going to be useful to whoever installed it: a network connection. A LaunchAgent that runs quietly at every login but never talks to the network is, from a network firewall’s point of view, invisible — and also, in practice, far less useful to an attacker. The moment it does open a socket, FireAI’s per-app rules apply to it like any other process: an unfamiliar signed or unsigned binary making its first connection triggers a prompt, with the on-device model’s reasoning shown in plain language, and every decision is visible, undoable and exportable as a text rule afterward.

The honest way to put the two disciplines together: check the locations in this article on a schedule that matches your risk tolerance — monthly is reasonable for most people, weekly if you install a lot of third-party software — and let a network tool carry the load in between, on the assumption that anything that persisted quietly will eventually have to speak to reach whoever it answers to.

How FireAI and HisnLabs fit in

FireAI does not scan for persistence — that is a different job, and tools like KnockKnock and BlockBlock already do it well; what FireAI watches is what that persisted code does the moment it opens a socket, which is the step every one of these mechanisms eventually has to take to be useful to whoever installed it.

FireAI is HisnLabs’ own product: an on-device AI firewall for Mac. It shows every connection your apps make, in plain language, and lets you decide what leaves your Mac — its AI runs locally, so your traffic is never sent to us or anyone else. HisnLabs’ security research team is the group that keeps that decision-making accurate: cataloguing which domains are ordinary telemetry versus a real product, tracking the country and network behind a connection, and training the on-device model (its Autopilot feature) on real traffic patterns, all without any of it leaving your Mac.

You can read the technical decisions behind it, or try FireAI for 17 days, at FireAI, by HisnLabs.

Sources