shelltrap.com
en de

So funktioniert es

Wie Dateierkennung in Echtzeit funktioniert

Ein Scanner, der nachts läuft, findet die Shell von gestern. Ein Scanner, der bei jedem Schreibvorgang läuft, muss vier Probleme lösen, denen der nächtliche nie begegnet. Hier sind sie, mit sichtbaren Abwägungen.

Das Problem mit geplanten Scans

Ein Cronjob, der um drei Uhr morgens /home durchläuft, hat eine Tugend: Er ist einfach. Alles andere an ihm ist ein Kompromiss.

Eine um 09:15 Uhr hochgeladene Webshell hat achtzehn Stunden Zeit, benutzt zu werden, bevor irgendetwas sie bemerkt. In diesen achtzehn Stunden kann sie Mails versenden, eine Phishing-Seite ausliefern, in ein anderes Konto wechseln oder eine zweite Hintertür an einer Stelle installieren, an der der nächste Scan nicht nachsieht. Wenn der Scan meldet, ist die gefundene Datei oft längst nicht mehr die interessante — sie ist die, die der Angreifer absichtlich zurückgelassen hat.

Ein Terabyte Kundendaten zu durchlaufen kostet außerdem echte I/O. Auf einem Shared Host spürt das jeder Kunde auf der Maschine, weshalb so viele geplante Scans am Ende bis zur Bedeutungslosigkeit gedrosselt oder stillschweigend abgeschaltet werden.

Echtzeit-Erkennung ändert die Frage von „was liegt auf dieser Platte“ zu „was hat sich gerade geändert“ — eine sehr viel kleinere und sehr viel nützlichere Frage.

Zwei Kernel-Schnittstellen, und warum die Wahl zählt

Linux bietet zwei Wege, über Dateiänderungen informiert zu werden.

inotify überwacht Verzeichnisse. Es ist fast überall verfügbar und benötigt keine besonderen Rechte, und damit endet die gute Nachricht. Jedes Verzeichnis braucht seinen eigenen Watch, Watches sind eine begrenzte Ressource je Benutzer, und die Rekursion ist Ihr Problem: Ein neues Unterverzeichnis braucht einen neuen Watch, und zwischen seiner Erstellung und Ihrem Watch klafft eine Lücke. Auf einem Host mit hunderten Konten und tiefen WordPress-Bäumen erschöpfen Sie entweder das Watch-Limit oder Sie erhöhen ein globales sysctl, das alles andere auf der Maschine betrifft.

fanotify kann einen ganzen Mount markieren. Eine Markierung deckt jede Datei darunter ab, einschließlich Verzeichnissen, die noch gar nicht existieren. Das beseitigt das Rekursionsproblem und das Problem der erschöpften Watches in einem Zug. Der Preis ist CAP_SYS_ADMIN und — für die Modi, die Elternverzeichnis und Namen melden — ein hinreichend aktueller Kernel mit exportierbaren File-Handles.

Shelltrap bevorzugt fanotify mit FAN_REPORT_DFID_NAME und markiert je Mount, und fällt ehrlich zurück, wenn das nicht möglich ist:

Vier Beobachtungsstufen, nach Laufzeitfähigkeit statt Kernelversion gewähltDer Installer probiert die Fähigkeiten des Hosts. Stufe A nutzt fanotify mit Filehandles und liefert vollständige Echtzeit. Stufe B verzichtet auf Filehandles, Umbenennungen kommen verzögert. Stufe C arbeitet ohne CAP_SYS_ADMIN mit budgetiertem inotify und einem Crawler. Stufe D installiert nicht und liefert nur Diagnose.LAUFZEITPROBE, KEINE VERSIONSANNAHMEAvollständigfanotify mit FAN_REPORT_DFID_NAME, Marks je MountUbuntu 22.04/24.04 · Alma/Rocky/RHEL 9 und 10Echtzeit, alle FunktionenBfd-modefanotify ohne Filehandles, Renames über ctime-AbgleichAlma/Rocky/RHEL/CloudLinux 8 · Ubuntu 20.04 GA-KernelEchtzeit beim Schreiben, Umbenennen verzögertCcontainerbudgetiertes inotify plus checkpointbarer Crawlerunprivilegiertes LXC/OpenVZ ohne CAP_SYS_ADMINErkennung mit Verzögerung, im Produkt ausgewiesenDnicht unterstütztkein verwendbares Backend oder unbekanntes DateisystemRestkeine Installation, nur Diagnose
Die Fähigkeiten werden auf dem laufenden Host geprüft. Eine Kernel-Versionsnummer ist ein Hinweis; die Prüfung ist die Antwort.

Die wichtige Entwurfsentscheidung dabei ist nicht, welche Stufe verwendet wird. Sie besteht darin, dass die Stufe gemeldet wird. Ein Container, der nur verzögerte Erkennung leisten kann, sagt das im Produkt, statt sich als Echtzeit auszugeben.

Was als Ereignis zählt

Das naheliegende Ereignis ist „eine Datei wurde geschrieben“. Wer sich darauf beschränkt, verpasst zwei der häufigsten Persistenztechniken auf Shared Hosting, deshalb ist die Maske breiter:

  • CLOSE_WRITE — ein Schreibvorgang ist abgeschlossen. Der übliche Moment zum Scannen, weil die Datei vollständig ist.
  • MOVED_TO und MOVED_FROM — ein Umbenennen hinein oder heraus. Angreifer legen eine Datei anderswo ab und verschieben sie an ihren Platz, gerade weil ein naiver Watcher nur auf Schreibvorgänge schaut.
  • CREATE und DELETE — Auftauchen und Verschwinden.
  • ATTRIB — Attributänderungen. Eine Datei, die als harmlose Daten hochgeladen und dann ausführbar gemacht oder mit einem setuid-Bit versehen wird, ist ein Angriff, an dem überhaupt kein Schreibvorgang beteiligt ist.

Es gibt auch eine Subtraktion. Eine Markierung auf Dateisystemebene auf dem Root-Dateisystem liefert jedes Öffnen auf dem Host, einschließlich des Datenverzeichnisses des Datenbankservers und jedes Log-Schreibvorgangs. Diese Pfade werden bewusst als ignoriert markiert. Alles zu überwachen ist nicht sicherer; es ist langsamer, lauter und wird eher von einem entnervten Administrator abgeschaltet.

Die Privilegientrennung

Hier ist die unbequeme Wahrheit über jeden Dateiscanner: Er ist ein Programm, das vom Angreifer gewählte Dateien öffnet und sie an Parser für Archive, Bilder, Dokumente und ausführbare Dateien weiterreicht. Das ist der gefährlichste Code im System. Ein Parser-Fehler in einer Archivbibliothek ist ein vom Angreifer kontrollierter Speichersicherheitsfehler, und wenn dieser Parser als root läuft, dann auch der Angreifer.

Deshalb teilt Shelltrap die Aufgabe auf:

Shelltrap-Architektur: Ereignisquellen, Root-Broker, isolierter Worker, Verdikt und ReaktionVier Ereignisquellen speisen den parserfreien Root-Broker. Der Broker übergibt einen nur lesbaren Dateideskriptor an einen unprivilegierten Worker mit ClamAV, YARA, Hash-Sets und Heuristik. Der Worker liefert eines von fünf Verdikten zurück, aus dem die Reaktion folgt. Rechts hängen Steuerebene, signierte Feeds und Telemetrie am Broker.01 · EREIGNISQUELLENWeb-Uploadlsphp · auto_prepend_filesynchron, 2 sfanotifyTier A–D · CLOSE_WRITEEchtzeitFTPpure-uploadscriptnach dem UploadZeitplanBaseline · inkrementellI/O-Budget02 · ROOT-BROKERshelltrapd — läuft als root, enthält keinen Parseropenat2 · RESOLVE_NO_SYMLINKSJob-Journal in SQLite (WAL)nur opake Objekt-IDskein YARA-, kein ClamAV-CodeSCM_RIGHTS · nur-lesbarer FD03 · SCANNER-WORKEReigener Benutzer · Namespaces · seccomp · Landlock · kein NetzClamAVclamd-Socket · Full-ProfilYARA 4.5je Generation vorkompiliertHash-Setsplus Known-Good-AllowlistHeuristikmehrere Signale nötigGrenzen je Mandant: Dateigröße, Scanzeit, Archivtiefe, expandierte Bytes, Temp-Speicher04 · VERDIKTcleansuspiciousmaliciousunscanneddegraded„nicht gescannt" und „gestört" sind eigene Zustände und werden nie als clean angezeigtSTEUEREBENECyberPanel Plugin · CLI/run/shelltrap/api.sockSO_PEERCRED · Rollenkeine freien PfadeSignatur-FeedsEd25519 · GenerationenProvenienz je RegelKorpus-Gate · CanaryCircuit Breaker · RollbackLizenzEd25519-Token, offline geprüftFingerabdruck je Serverohne Lizenz: keine FeedsReaktionQuarantäne als TransaktionRestore mit MetadatenMail · Webhook · DigestPolicy je DomainHealth · Metriken · AuditPrometheus-Textformatgehashte Audit-Kette
Privilegien links von der SCM_RIGHTS-Linie, Parser rechts davon. Keine der beiden Seiten hat beides.

Der Broker hat die Privilegien und keine Parser. Er hält die fanotify-Gruppen, öffnet Dateien mit openat2 und RESOLVE_NO_SYMLINKS (oder mit einem komponentenweisen openat-Durchlauf mit O_NOFOLLOW, wo dieser Syscall nicht verfügbar ist) und reicht einen nur lesbaren Deskriptor weiter. Er enthält keinen YARA-Code, keinen ClamAV-Code und kein Parsing von Archiven, Office-Dateien oder ELF. Er nimmt vom Panel und von der Oberfläche undurchsichtige Objekt-IDs entgegen, niemals einen Pfad.

Der Worker hat die Parser und keine Privilegien. Er läuft als eigener Benutzer in einem eigenen Mount- und PID-Namespace, mit seccomp, Landlock, rlimits und cgroup-Limits — und ganz ohne Netzwerk. Er erhält Deskriptoren; er löst niemals selbst einen Pfad auf, sodass ein Symlink-Race in einem Kundenverzeichnis nichts hat, wogegen es antreten könnte.

Diese Trennung ist die folgenreichste Entwurfsentscheidung im Produkt, und es lohnt sich, bei jedem Scanner, den Sie prüfen, danach zu fragen: Was läuft als root, und parst es?

Wenn die Warteschlange überläuft

Echtzeitsysteme haben einen Fehlerfall, den Batch-Systeme nicht kennen: Die Ereignisse treffen schneller ein, als sie verarbeitet werden können. Ein Kunde spielt ein Backup zurück, ein Deploy schreibt zehntausend Dateien, ein Angreifer entpackt absichtlich ein Archiv. Die Warteschlange des Kernels füllt sich, und der Kernel meldet Ihnen, dass er Ereignisse verworfen hat.

Es gibt drei mögliche Antworten, und nur eine davon ist ehrlich.

Sie können es ignorieren und stillschweigend eine Abdeckungslücke haben, von der niemand weiß. Sie können abstürzen und gar keine Abdeckung haben. Oder Sie können es zählen, darauf alarmieren und abgleichen: einen begrenzten Scan über den betroffenen Teilbaum laufen lassen, um zu finden, was übersehen wurde.

Shelltrap tut das Dritte. Derselbe Abgleich läuft nach einem Neustart, einem Absturz, einem Upgrade und immer dann, wenn ein neuer Mount unter einem Kundenverzeichnis auftaucht — alles Situationen, in denen Ereignisse passiert sind, denen niemand zugehört hat. Reader und Scan-Warteschlange sind getrennte Goroutinen mit einem persistenten Job-Journal in SQLite, sodass ein Neustart fortsetzt, statt neu zu beginnen.

Das hat einen Preis, und den sollten Sie vor der Installation kennen: Ein erster Start ohne Marker für ein sauberes Herunterfahren plant einen vollständigen Durchlauf. Auf einem ausgelasteten Host ist dieser Rückstand echte Arbeit, das Alter der Warteschlange steigt, während er abgearbeitet wird, und der Gesundheitszustand bleibt degraded, bis das erledigt ist. Das ist das richtige Verhalten — der Scanner ist tatsächlich im Rückstand —, aber es ist deutlich angenehmer, wenn Sie cgroup-Limits vor dem ersten Start setzen statt danach.

Warum „nicht gescannt“ eine eigene Antwort ist

Jeder Scanner hat Grenzen: eine maximale Dateigröße, eine maximale Scandauer, eine maximale Rekursionstiefe für Archive, ein maximales Expansionsverhältnis für komprimierte Daten. Ohne sie legt ein einziges präpariertes Archiv die Maschine lahm.

Die Frage ist, was gemeldet wird, wenn eine Grenze erreicht ist. Eine beunruhigende Zahl von Werkzeugen zählt das Objekt stillschweigend als sauber, weil die Alternative eine hässliche Zahl auf einem Dashboard wäre. Damit wird aus einer Ressourcengrenze eine Erkennungslücke, die genau dort unsichtbar ist, wo man nach ihr suchen würde.

Shelltrap hat aus diesem Grund fünf Urteile statt zwei:

UrteilBedeutung
cleanvon jeder Engine im aktiven Profil gescannt, nichts gefunden
suspiciousmehrere unabhängige Signale, unterhalb der Handlungsschwelle
maliciousein Signatur- oder Hash-Treffer oder ein heuristischer Wert über der Schwelle
unscannedeine Grenze wurde erreicht; der Grund wird festgehalten und das Objekt für einen begrenzten Tiefenscan eingereiht
degradedder Scanner selbst war beeinträchtigt — clamd weg, ein Worker tot, ein Regelsatz fehlt

unscanned und degraded werden niemals als clean angezeigt. Das macht manche Dashboards weniger grün. Es macht sie auch wahr.

Der Cache, und warum ein Regel-Update ihn ungültig macht

Dieselbe unveränderte Datei wiederholt zu scannen ist Verschwendung, deshalb werden Urteile zwischengespeichert. Was in den Cache-Schlüssel eingeht, entscheidet darüber, ob der Cache ein Leistungsgewinn oder ein Sicherheitsloch ist.

Der Schlüssel enthält die Mount-ID, die Inode, mtime und ctime in hoher Auflösung, die Größe, den Inhalts-Hash — und die Generationen von Regelsatz, Engine und Policy. Genau dieser letzte Teil sorgt dafür, dass ein Signatur-Update alte clean-Urteile automatisch ungültig macht: Eine Datei, die unter den Regeln von gestern sauber war, ist unter den heutigen Regeln nicht gescannt worden.

Pfadabhängige Policy-Urteile liegen in einem eigenen Cache, weil sich der Kontext ändern kann, ohne dass sich der Inhalt überhaupt ändert. Dieselbe Inode kann durch einen Hardlink oder eine Umbenennung an einer neuen Stelle auftauchen, und „PHP in einem Uploads-Verzeichnis“ ist eine Aussage über den Ort, nicht über die Bytes.

Was Echtzeit-Erkennung nicht ist

Sie ist keine Prävention. Eine Datei, die geschrieben wird, ist geschrieben; die Erkennung geschieht nach Abschluss des Schreibvorgangs, in Millisekunden statt in Stunden, aber danach. Den Schreibvorgang selbst auf Kernel-Ebene zu blockieren ist mit fanotify-Berechtigungsereignissen möglich und liegt für eine spätere Version als Prototyp vor, wird heute aber bewusst nicht versprochen — denn ein Scanner, der bei jedem Öffnen einer Datei eine Berechtigungsentscheidung zurückhält, ist ein Scanner, der einen Server zum Stillstand bringen kann, wenn er hängt.

Es gibt einen Weg, auf dem Prävention heute existiert, und es ist derjenige, der für einen Webhoster am meisten zählt: das synchrone Upload-Gate , das einen Web-Upload beurteilt, bevor Ihre Anwendung ihn je zu sehen bekommt.

Weiterführende Lektüre