Einfach erklärt
- Ein Dienst darf von Haus aus fast alles, was sein Benutzer darf. Läuft er als root, darf er alles.
- systemd kann jeden Dienst in einen Käfig setzen: nur lesen statt schreiben, kein Zugriff auf /home, keine Geräte, nur bestimmte Systemaufrufe.
- systemd-analyze security bewertet, wie offen ein Dienst ist: 10 = alles offen, 0 = gut abgeschottet.
- Weil systemd Programme automatisch und oft als root startet, ist es auch ein beliebter Ort für Angreifer, um sich festzusetzen. Wer systemd kennt, findet sie.
Animation
Teil A: Dienste härten
Wie offen sind meine Dienste?
UNIT EXPOSURE PREDICATE HAPPY
NetworkManager.service 7.8 EXPOSED 🙁
cron.service 9.6 UNSAFE 😨
ssh.service 9.6 UNSAFE 😨
systemd-journald.service 4.9 OK 🙂
systemd-logind.service 2.8 OK 🙂
systemd-timesyncd.service 2.1 OK 🙂
...
- Die eigenen Dienste von systemd sind gut abgeschottet, viele Dienste aus Paketen gar nicht.
- UNSAFE heißt nicht „kaputt“, sondern: Wird der Dienst übernommen, hat der Angreifer viel Spielraum.
- Einzelner Dienst mit allen Punkten: systemd-analyze security ssh.service
- Eine Unit-Datei bewerten, bevor sie installiert ist: systemd-analyze security --offline=true ./datei.service
Ausgangslage
- /etc/systemd/system/simplehttp.service
[Unit]
Description=Simple Http Server
[Service]
Type=exec
ExecStart=/usr/bin/python3 -m http.server
[Install]
WantedBy=multi-user.target
- Wert: 9,4 UNSAFE
- Das größere Problem sieht man im Browser: Der Dienst läuft als root und ohne WorkingDirectory. http.server zeigt deshalb das komplette Dateisystem ab / an.
- Erste Regel beim Härten: Was soll der Dienst eigentlich können? Hier: als normaler Benutzer ein einziges Verzeichnis ausliefern.
Schritt für Schritt
- Gemessen mit systemd 257. Jede Zeile kommt zu den vorherigen dazu.
- Nach jedem Schritt:
systemctl daemon-reload
systemctl restart simplehttp
systemctl status simplehttp
curl -s http://localhost:8000/ | head -3
systemd-analyze security simplehttp | tail -1
- Ein besserer Wert nützt nichts, wenn der Dienst danach nicht mehr läuft.
Die Optionen
| Bereich |
Option |
Wirkung
|
| Dateisystem |
ProtectSystem=strict |
das ganze Dateisystem nur lesbar, außer /dev, /proc, /sys
|
| ProtectHome=yes |
/home, /root und /run/user unsichtbar
|
| PrivateTmp=yes |
eigenes /tmp, nicht das gemeinsame
|
| PrivateDevices=yes |
eigenes /dev nur mit null, zero, random und Co.
|
| ReadWritePaths=/pfad |
Ausnahme: hier darf geschrieben werden
|
| Benutzer und Rechte |
DynamicUser=yes |
systemd legt beim Start einen Benutzer an und löscht ihn danach
|
| NoNewPrivileges=yes |
kein Aufstieg zu mehr Rechten, z.B. über sudo oder SUID-Programme
|
| CapabilityBoundingSet= |
leer: keine Root-Sonderrechte, auch nicht für root
|
| RestrictSUIDSGID=yes |
darf keine SUID/SGID-Dateien anlegen
|
| Kernel |
ProtectKernelTunables=yes |
/proc/sys und /sys nur lesbar
|
| ProtectKernelModules=yes |
darf keine Kernelmodule laden
|
| ProtectKernelLogs=yes, ProtectClock=yes, ProtectHostname=yes, ProtectControlGroups=yes |
weitere Teile des Kernels gesperrt
|
| Netzwerk |
RestrictAddressFamilies=AF_INET AF_INET6 |
nur IPv4 und IPv6, keine anderen Socket-Arten
|
| IPAddressDeny=any und IPAddressAllow=10.0.10.0/24 localhost |
Firewall direkt am Dienst
|
| Systemaufrufe |
SystemCallFilter=@system-service |
nur die Aufrufe, die normale Dienste brauchen
|
| SystemCallArchitectures=native |
keine 32-Bit-Aufrufe auf 64-Bit-Systemen
|
| RestrictNamespaces=yes |
darf keine eigenen Namespaces anlegen
|
- Mehr Optionen: man systemd.exec
Das Ergebnis
[Unit]
Description=Einfacher Webserver, gehärtet
After=network.target
[Service]
Type=exec
ExecStart=/usr/bin/python3 -m http.server 8000
WorkingDirectory=/srv/www
DynamicUser=yes
# Dateisystem
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
# Rechte
NoNewPrivileges=yes
CapabilityBoundingSet=
RestrictSUIDSGID=yes
# Kernel
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
# Netzwerk
RestrictAddressFamilies=AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=10.0.10.0/24 localhost
# Systemaufrufe und Prozesse
SystemCallFilter=@system-service
SystemCallArchitectures=native
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
ProtectProc=invisible
ProcSubset=pid
UMask=0077
[Install]
WantedBy=multi-user.target
- Wert: 1,4 OK
- Port 8000 liegt über 1024, deshalb braucht der Dienst auch CAP_NET_BIND_SERVICE nicht.
Dienste aus Paketen härten
- Nie die Unit unter /usr/lib ändern, sondern einen Drop-in anlegen:
- systemctl edit dienst
[Service]
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
- So bleibt die Härtung bei Paket-Updates erhalten.
- Mit systemctl revert dienst ist alles wieder wie vorher.
Stolperfallen beim Härten
- Schreibt der Dienst Dateien, scheitert er an ProtectSystem=strict. Lösung: ReadWritePaths= oder besser StateDirectory=name (legt /var/lib/name passend an).
- DynamicUser=yes und eigene Dateien: Der Benutzer ist jedes Mal ein anderer. Dauerhafte Daten nur über StateDirectory=.
- MemoryDenyWriteExecute=yes verträgt sich nicht mit Programmen, die Code zur Laufzeit erzeugen, z.B. Java oder Node.js.
- SystemCallFilter=: Ruft der Dienst einen verbotenen Aufruf, wird er beendet. Im Status steht dann code=killed, signal=SYS. Zum Testen hilft SystemCallErrorNumber=EPERM: Dann bekommt der Dienst nur einen Fehler und läuft weiter.
- SystemCallErrorNumber=EPERM allein bewirkt nichts, es gehört immer zu einem SystemCallFilter=.
- Mit sed in Units schreiben: sed -i -e schreiben, nicht sed -ie. Sonst entsteht eine Sicherungskopie mit „e“ am Ende im Unit-Verzeichnis.
Optionen live ausprobieren mit systemd-run
- Bevor man Optionen in eine Unit schreibt, kann man sie mit einer Shell im Käfig ausprobieren:
- systemd-run -t -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /bin/bash
- In dieser Shell:
touch /etc/test
touch: cannot touch '/etc/test': Read-only file system
ls /home
(leer)
- Mit exit ist der Käfig wieder weg.
- Einen einzelnen Befehl mit Ausgabe: systemd-run -P -p DynamicUser=yes id
- So sieht man in Sekunden, was ein Dienst mit einer Option noch darf.
Teil B: Persistenz erkennen
Warum systemd?
- Angreifer wollen nach einem Neustart wieder da sein. systemd startet Programme automatisch, oft als root, und eine Unit sieht harmlos aus.
- In MITRE ATT&CK:
- T1543.002: Systemd Service anlegen oder ändern
- T1053.006: Systemd Timer
- Für das Blue Team heißt das: Man muss wissen, wo Units liegen können, und erkennen, welche nicht dorthin gehören.
Wo man suchen muss
| Ort |
Was dort normal ist |
Worauf achten
|
| /usr/lib/systemd/system/ |
nur Dateien aus Paketen |
Dateien, die zu keinem Paket gehören
|
| /etc/systemd/system/ |
eigene Units des Admins, Links in *.wants/ |
unbekannte Units, neue Links, Drop-ins in *.d/
|
| /run/systemd/system/ |
zur Laufzeit erzeugt |
Units, die kein bekanntes Programm erzeugt hat
|
| /etc/systemd/user/, ~/.config/systemd/user/ |
selten benutzt |
User-Units laufen mit den Rechten des Benutzers
|
| /var/lib/systemd/linger/ |
meist leer |
Linger: User-Units laufen auch, wenn der Benutzer nicht angemeldet ist
|
| */systemd/system-generators/ |
wenige Generatoren aus Paketen |
Generatoren laufen sehr früh beim Start, als root
|
Werkzeuge
- Unit-Dateien, die zu keinem Paket gehören (Debian)
for f in $(find /etc/systemd/system /usr/lib/systemd/system -type f); do
dpkg -S "$f" >/dev/null 2>&1 || echo "FREMD $f"
done
- Rocky: rpm -qf "$f" statt dpkg -S
- Treffer in /etc sind oft gewollt, z.B. eigene Dienste. Jeder Treffer muss sich aber erklären lassen.
- Was wurde in letzter Zeit geändert?
- find /etc/systemd /usr/lib/systemd /run/systemd -type f -mtime -7 -ls
- Was überschreibt oder ergänzt Units der Pakete?
- systemd-delta --type=overridden,extended
[EXTENDED] /usr/lib/systemd/system/systemd-logind.service → /usr/lib/systemd/system/systemd-logind.service.d/dbus.conf
...
- Timer und User-Units
- systemctl list-timers --all
- ls /var/lib/systemd/linger/
- find /home /root -path '*/.config/systemd/user/*' -type f 2>/dev/null
- Verdächtige Befehle in Units
grep -r -n -E 'Exec.*(/tmp/|/dev/shm|/var/tmp|curl|wget|base64|bash -c|sh -c|nc )' \
/etc/systemd /usr/lib/systemd /run/systemd /home/*/.config/systemd 2>/dev/null
- Nicht jeder Treffer ist böse, aber jeder gehört angesehen.
- Paketdateien verändert?
- dpkg --verify
- Rocky: rpm -Va
- Was läuft gerade, und zu welcher Unit gehört es?
- systemd-cgls
- systemctl status <PID>
Überwachen statt suchen
- Mit auditd jede Änderung an Unit-Verzeichnissen melden:
-w /etc/systemd/system -p wa -k systemd_units
-w /usr/lib/systemd/system -p wa -k systemd_units
-w /etc/systemd/user -p wa -k systemd_units
- ausearch -k systemd_units
- Das Journal an ein zentrales Logsystem (SIEM) schicken: Journald Remote Logging
- Ein Angreifer mit root kann das lokale Journal löschen. Was schon beim SIEM ist, bleibt.
Stolperfallen
- Score ist nicht Sicherheit. Der Wert zeigt, wie eingesperrt ein Dienst ist, nicht, ob er Lücken hat.
- Härten ohne Test legt Dienste lahm. Immer schrittweise und mit Funktionstest.
- Nur /etc durchsuchen reicht nicht. User-Units, /run und Generatoren gehören dazu.
- Drop-ins übersehen. Eine harmlose Paket-Unit kann über einen Drop-in ein anderes ExecStart= bekommen. systemctl cat und systemd-delta zeigen es.
Aufgaben
Grundaufgaben
- Lassen Sie sich mit systemd-analyze security alle Dienste anzeigen. Welche drei sind am offensten?
- Legen Sie die ungehärtete simplehttp.service an und rufen Sie die Seite auf. Was sehen Sie?
- Härten Sie den Dienst Schritt für Schritt, bis er unter 4,0 liegt und die Seite weiter funktioniert.
- Härten Sie einen Dienst aus einem Paket per systemctl edit mit vier Optionen. Prüfen Sie den Wert vorher und nachher.
- Suchen Sie auf Ihrem System alle Unit-Dateien, die zu keinem Paket gehören. Können Sie jede erklären?
Für Schnelle
- Schalten Sie SystemCallFilter=@system-service ein und entfernen Sie dann @system-service schrittweise. Wann bricht der Dienst ab, und was steht im Journal?
- Richten Sie die auditd-Regel ein, legen Sie eine Test-Unit an und finden Sie das Ereignis mit ausearch.
- Prüfen Sie mit systemd-delta, welche Units auf Ihrem System verändert oder ergänzt wurden.
Vertiefung
Weiter