Un fuzzer guiado por cobertura para intérpretes de lenguajes dinámicos basado en un lenguaje intermedio personalizado ("FuzzIL") que puede ser mutado y traducido a JavaScript.
Uso
Los pasos básicos para usar este fuzzer son:
Descarga el código fuente de uno de los motores JavaScript compatibles. Consulta el directorio Targets/ para la lista de motores JavaScript compatibles.
Aplica los parches correspondientes del directorio del objetivo. Consulta también el README.md en ese directorio.
Compila el motor con instrumentación de cobertura (requiere clang >= 4.0) como se describe en el README.
Compila el fuzzer: swift build [-c release].
Ejecuta el fuzzer: swift run [-c release] FuzzilliCli --profile=<profile> [other cli options] /path/to/jsshell. Consulta también swift run FuzzilliCli --help.
La compilación y ejecución de Fuzzilli y los motores JavaScript compatibles dentro de Docker y en Google Compute Engine también es compatible.
Hacking
Revisa main.swift para ver un ejemplo de uso de la librería Fuzzilli y juega con las diversas opciones de configuración. Luego, echa un vistazo a Fuzzer.swift para la lógica de fuzzing de alto nivel. A partir de ahí, sumérgete en cualquier parte que parezca interesante.
Se agradecería mucho si pudieras enviar una nota breve (posiblemente incluyendo un número CVE) a [email protected] o abrir un pull request por cualquier vulnerabilidad encontrada con la ayuda de este proyecto para que pueda ser incluida en la sección bug showcase. Aparte de eso, por supuesto puedes reclamar cualquier recompensa por errores, créditos CVE, etc. por las vulnerabilidades :)
Concepto
Al fuzzear para encontrar errores en el núcleo del intérprete, por ejemplo en compiladores JIT, la corrección semántica de los programas generados se convierte en una preocupación. Esto contrasta con la mayoría de otros escenarios, por ejemplo, fuzzear APIs de tiempo de ejecución, donde la corrección semántica se puede sortear fácilmente envolviendo el código generado en construcciones try-catch. Hay diferentes posibilidades para lograr una tasa aceptable de muestras semánticamente correctas, una de ellas es un enfoque mutacional en el que todas las muestras en el corpus también son semánticamente válidas. En ese caso, cada mutación solo tiene una pequeña probabilidad de convertir una muestra válida en una inválida.
Para implementar un fuzzer JavaScript basado en mutaciones, es necesario definir mutaciones al código JavaScript. En lugar de mutar el AST u otros elementos sintácticos de un programa, se define un lenguaje intermedio personalizado (IL) sobre el cual se pueden realizar mutaciones al flujo de control y datos de un programa de manera más directa. Este IL luego se traduce a JavaScript para su ejecución. El lenguaje intermedio tiene aproximadamente el siguiente aspecto:
Un programa FuzzIL es simplemente una lista de instrucciones.
Una instrucción FuzzIL es una operación junto con variables de entrada y salida y potencialmente uno o más parámetros (encerrados entre comillas simples en la notación anterior).
Las entradas a las instrucciones son siempre variables, no hay valores inmediatos.
Cada salida de una instrucción es una nueva variable, y las variables existentes solo pueden reasignarse mediante operaciones dedicadas como la instrucción Reassign.
Cada variable se define antes de ser utilizada.
Se pueden realizar varias mutaciones en estos programas:
InputMutator: reemplaza las variables de entrada de las instrucciones por otras diferentes para mutar el flujo de datos del programa.
CodeGenMutator: genera código y lo inserta en algún lugar del programa mutado. El código se genera ejecutando un generador de código o copiando algunas instrucciones de otro programa en el corpus (splicing).
CombineMutator: inserta un programa del corpus en una posición aleatoria en el programa mutado.
OperationMutator: muta los parámetros de las operaciones, por ejemplo reemplazando una constante entera por otra diferente.
y más...
Se puede encontrar una discusión mucho más detallada sobre cómo funciona Fuzzilli aquí.
Implementación
El fuzzer está implementado en Swift, con algunas partes (por ejemplo, mediciones de cobertura, interacciones de socket, etc.) implementadas en C.
Arquitectura
Una instancia del fuzzer (implementada en Fuzzer.swift) está compuesta por los siguientes componentes centrales:
MutationFuzzer: produce nuevos programas a partir de los existentes aplicando mutaciones. Luego ejecuta las muestras producidas y las evalúa.
ScriptRunner: ejecuta programas del lenguaje objetivo.
Corpus: almacena muestras interesantes y las suministra al núcleo del fuzzer.
Environment: tiene conocimiento del entorno de ejecución, por ejemplo, los builtins disponibles, nombres de propiedades y métodos.
Minimizer: minimiza programas que causan fallos y programas interesantes.
Evaluator: evalúa si una muestra es interesante según alguna métrica, por ejemplo, cobertura de código.
Lifter: traduce un programa FuzzIL al lenguaje objetivo (JavaScript).
Además, hay varios módulos disponibles opcionalmente:
Statistics: recopila diversas piezas de información estadística.
NetworkSync: sincroniza múltiples instancias a través de la red.
ThreadSync: sincroniza múltiples instancias dentro del mismo proceso.
Storage: almacena programas que causan fallos en disco.
El fuzzer está basado en eventos, con la mayoría de las interacciones entre diferentes clases ocurriendo a través de eventos. Los eventos se despachan, por ejemplo, como resultado de un fallo o de que se encuentre un programa interesante, se ejecute un nuevo programa, se genere un mensaje de registro, etc. Consulta Events.swift para la lista completa de eventos. El mecanismo de eventos desacopla efectivamente los diversos componentes del fuzzer y facilita la implementación de módulos adicionales.
Un programa FuzzIL se puede construir usando una instancia de ProgramBuilder. ProgramBuilder proporciona métodos para crear y agregar nuevas instrucciones, agregar instrucciones de otro programa, recuperar variables existentes, consultar el contexto de ejecución en la posición actual (por ejemplo, si está dentro de un bucle) y más.
Ejecución
Fuzzilli utiliza un modo de ejecución personalizado llamado REPRL (read-eval-print-reset-loop). Para ello, el motor objetivo se modifica para aceptar una entrada de script a través de pipes y/o memoria compartida, ejecutarlo, luego restablecer su estado interno y esperar el siguiente script. Esto elimina la sobrecarga de la creación de procesos y en gran parte de la inicialización del motor.
Escalabilidad
Hay una instancia de Fuzzer por proceso objetivo. Esto permite la ejecución síncrona de programas y, por lo tanto, simplifica la implementación de varios algoritmos, como mutaciones consecutivas y minimización. Además, evita la necesidad de implementar acceso seguro para subprocesos al estado interno, por ejemplo, el corpus. Cada instancia del fuzzer tiene su propia DispatchQueue, que corresponde conceptualmente a un solo subproceso. Como regla general, cada interacción con una instancia de Fuzzer debe ocurrir en la cola de distribución de esa instancia. Esto garantiza la seguridad de subprocesos ya que la cola es serial. Para más detalles, consulta la documentación.
Para escalar, las instancias del fuzzer pueden formar una jerarquía de árbol, en cuyo caso informan las muestras interesantes y los fallos recién encontrados a su nodo padre. A su vez, un nodo padre sincroniza su corpus con sus nodos hijos. La comunicación entre nodos en el árbol puede ocurrir de diferentes maneras, cada una implementada como un módulo:
Comunicación entre subprocesos: sincroniza instancias en el mismo proceso encolando tareas en la DispatchQueue del otro fuzzer.
Este diseño permite que el fuzzer escale a muchos núcleos en una sola máquina, así como a muchas máquinas diferentes. Como un nodo padre puede sobrecargarse rápidamente si demasiadas instancias le envían programas, es posible configurar múltiples niveles de instancias, por ejemplo, una instancia raíz, 16 nodos intermedios conectados a la raíz y 256 "hojas" conectadas a los nodos intermedios. Consulta el directorio Cloud/ para más información sobre fuzzing distribuido.
Recursos
Recursos adicionales sobre este fuzzer:
Una presentación sobre Fuzzilli presentada en Offensive Con 2019.
La tesis de maestría para la cual se realizó la implementación inicial.
Un artículo de blog de Sensepost sobre el uso de Fuzzilli para encontrar un error en v8.
Un artículo de blog de Doyensec sobre fuzzing del motor JerryScript con Fuzzilli.
Un artículo del Simposio NDSS 2023 sobre Fuzzilli y cómo se compara con otros fuzzers.
Escaparate de Bug
La siguiente es una lista de algunos de los bugs encontrados con la ayuda de Fuzzilli. Solo deben incluirse en esta lista los bugs con impacto en la seguridad que estuvieran presentes en al menos una versión Beta del software afectado. Dado que Fuzzilli se utiliza a menudo para pruebas de fuzzing continuas durante el desarrollo, muchos problemas encontrados por él no se incluyen en esta lista, ya que generalmente se descubren antes de que el código vulnerable alcance una versión Beta. Sin embargo, se puede encontrar una lista de todos los problemas encontrados recientemente por Fuzzilli en V8 aquí.
¡Un agradecimiento especial a todos los usuarios de Fuzzilli que han informado bugs encontrados por él!
WebKit/JavaScriptCore
Issue 185328: El compilador DFG utiliza un registro de salida incorrecto para la operación NumberIsInteger
CVE-2018-4299: performProxyCall filtra un objeto interno al script
CVE-2018-4359: compileMathIC produce código máquina incorrecto
CVE-2019-8518: Acceso fuera de límites en FTL JIT debido a que LICM mueve el acceso al array antes de la comprobación de límites
CVE-2019-8558: Use-after-free de CodeBlock debido a Watchpoints colgantes
CVE-2019-8611: La optimización AIR elimina incorrectamente la asignación a un registro
CVE-2019-8623: El movimiento de código invariante de bucle (LICM) en DFG JIT deja una variable de pila sin inicializar
CVE-2019-8622: doesGC() de DFG es incorrecto acerca del comportamiento de la operación HasIndexedProperty en StringObjects
CVE-2019-8671: DFG: El movimiento de código invariante de bucle (LICM) deja el acceso a propiedades de objeto sin protección
CVE-2019-8672: Use-after-free de JSValue en ValueProfiles
CVE-2019-8678: JSC no ejecuta haveABadTime() cuando algunos prototipos son modificados, lo que lleva a confusiones de tipo
CVE-2019-8685: JSPropertyNameEnumerator usa identificadores de estructura incorrectos
CVE-2019-8765: Confusión de tipo GetterSetter durante la compilación DFG
CVE-2019-8820: Confusión de tipo durante el bailout al reconstruir objetos de argumentos
CVE-2019-8844: ObjectAllocationSinkingPhase no debería insertar pistas para asignaciones que ya no son válidas
CVE-2020-3901: Confusión de tipo GetterSetter en código FTL JIT (debido a LICM no siempre seguro)
CVE-2021-30851: Falta de bloqueo durante la búsqueda concurrente en HashTable
CVE-2021-30818: Confusión de tipo al reconstruir argumentos en la salida OSR de DFG
CVE-2022-46696: Fallo de aserción debido a falta de comprobación de excepción en código compilado por JIT
CVE-2022-46699: Fallo de aserción debido a almacenamiento en caché incorrecto de propiedades especiales en ICs
CVE-2022-46700: Intl.Locale.prototype.hourCycles filtra un JSValue vacío al script
CVE-2025-43214: Corrupción de memoria durante JSToWasmEntry al iterar sobre la pila
CVE-2025-43213: Tipificación inválida de la operación NewRegExpUntyped
Gecko/Spidermonkey
CVE-2018-12386: Error de asignación de registros en IonMonkey conduce a confusiones de tipo
CVE-2019-9791: La inferencia de tipos de IonMonkey es incorrecta para constructores ingresados a través de OSR
CVE-2019-9792: IonMonkey filtra el valor mágico JS_OPTIMIZED_OUT al script
CVE-2019-9816: ObjectGroup inesperado en la operación ObjectGroupDispatch
CVE-2019-9813: El código compilado por IonMonkey no actualiza los tipos de propiedad inferidos, lo que lleva a confusiones de tipo
CVE-2019-11707: IonMonkey predice incorrectamente el tipo de retorno de Array.prototype.pop, lo que lleva a confusiones de tipo
CVE-2020-15656: Confusión de tipo para argumentos especiales en IonMonkey