En programación y desarrollo de software , el fuzzing o prueba fuzz es una técnica de prueba de software automatizada que implica proporcionar datos no válidos, inesperados o aleatorios como entradas a un programa de computadora . Luego, el programa se monitorea para detectar excepciones como fallas , fallas en las aserciones de código integrado o posibles fugas de memoria . Por lo general, los fuzzers se utilizan para probar programas que toman entradas estructuradas. Esta estructura se especifica, por ejemplo, en un formato de archivo o protocolo y distingue la entrada válida de la no válida. Un fuzzer efectivo genera entradas semiválidas que son "lo suficientemente válidas" en el sentido de que no son rechazadas directamente por el analizador, pero sí crean comportamientos inesperados más profundos en el programa y son "lo suficientemente inválidas" para exponer casos especiales que no se han tratado adecuadamente.
A los efectos de seguridad, la entrada que cruza un límite de confianza suele ser la más útil. [1] Por ejemplo, es más importante analizar el código que maneja la carga de un archivo por parte de cualquier usuario que analizar un archivo de configuración al que solo puede acceder un usuario privilegiado.
El término "fuzz" se origina de un proyecto de clase de 1988 [2] en la clase de posgrado de Sistemas operativos avanzados (CS736), impartido por el profesor Barton Miller en la Universidad de Wisconsin , cuyos resultados se publicaron posteriormente en 1990. [3] [4] Para probar una utilidad de UNIX destinada a generar automáticamente parámetros de entrada y línea de comandos aleatorios para la utilidad. El proyecto fue diseñado para probar la confiabilidad de los programas de línea de comandos de UNIX ejecutando una gran cantidad de entradas aleatorias en rápida sucesión hasta que se bloquearan. El equipo de Miller pudo bloquear entre el 25 y el 33 por ciento de las utilidades que probaron. Luego depuraron cada uno de los bloqueos para determinar la causa y categorizaron cada falla detectada. Para permitir que otros investigadores realicen experimentos similares con otro software, el código fuente de las herramientas, los procedimientos de prueba y los datos de resultados sin procesar se pusieron a disposición del público. [5] Este fuzzing temprano ahora se llamaría fuzzing de caja negra, generacional, no estructurado (tonto o "clásico").
Según el profesor Barton Miller, "en el proceso de redacción de la descripción del proyecto, necesitaba darle un nombre a este tipo de prueba. Quería un nombre que evocara la sensación de datos aleatorios y no estructurados. Después de probar varias ideas, me decidí por el término fuzz". [4]
Una contribución clave de este trabajo inicial fue la creación de un oráculo simple (casi simplista). Un programa no superaba la prueba si se bloqueaba o se bloqueaba con una entrada aleatoria y se consideraba que la había superado en caso contrario. Si bien la construcción de oráculos de prueba puede resultar complicada, el oráculo para esta prueba fuzz temprana era simple y de aplicación universal.
En abril de 2012, Google anunció ClusterFuzz, una infraestructura de fuzzing basada en la nube para componentes críticos para la seguridad del navegador web Chromium . [6] Los investigadores de seguridad pueden cargar sus propios fuzzers y recolectar recompensas por errores si ClusterFuzz encuentra una falla con el fuzzer cargado.
En septiembre de 2014, Shellshock [7] fue revelado como una familia de errores de seguridad en el shell Bash de UNIX ampliamente utilizado ; la mayoría de las vulnerabilidades de Shellshock se encontraron utilizando el fuzzer AFL . [8] (Muchos servicios que dan a Internet, como algunas implementaciones de servidores web, utilizan Bash para procesar ciertas solicitudes, lo que permite a un atacante hacer que versiones vulnerables de Bash ejecuten comandos arbitrarios . Esto puede permitir que un atacante obtenga acceso no autorizado a un sistema informático. [9] )
En abril de 2015, Hanno Böck mostró cómo el fuzzer AFL podría haber encontrado la vulnerabilidad Heartbleed de 2014. [10] [11] (La vulnerabilidad Heartbleed se reveló en abril de 2014. Es una vulnerabilidad grave que permite a los adversarios descifrar una comunicación que de otro modo estaría cifrada . La vulnerabilidad se introdujo accidentalmente en OpenSSL , que implementa TLS y es utilizado por la mayoría de los servidores de Internet. Shodan informó que 238.000 máquinas seguían siendo vulnerables en abril de 2016; [12] 200.000 en enero de 2017. [13] )
En agosto de 2016, la Agencia de Proyectos de Investigación Avanzada de Defensa (DARPA) celebró la final del primer Cyber Grand Challenge , una competición de captura de la bandera totalmente automatizada que duró 11 horas. [14] El objetivo era desarrollar sistemas de defensa automáticos que puedan descubrir, explotar y corregir fallos de software en tiempo real . Se utilizó el fuzzing como una estrategia de ofensiva eficaz para descubrir fallos en el software de los oponentes. Mostró un tremendo potencial en la automatización de la detección de vulnerabilidades. El ganador fue un sistema llamado "Mayhem" [15] desarrollado por el equipo ForAllSecure dirigido por David Brumley .
En septiembre de 2016, Microsoft anunció el Proyecto Springfield, un servicio de pruebas fuzz basado en la nube para encontrar errores críticos de seguridad en el software. [16]
En diciembre de 2016, Google anunció OSS-Fuzz, que permite el fuzzing continuo de varios proyectos de código abierto críticos para la seguridad. [17]
En Black Hat 2018, Christopher Domas demostró el uso de fuzzing para exponer la existencia de un núcleo RISC oculto en un procesador. [18] Este núcleo pudo eludir los controles de seguridad existentes para ejecutar comandos del Anillo 0 desde el Anillo 3.
En septiembre de 2020, Microsoft lanzó OneFuzz , una plataforma de fuzzing como servicio autohospedada que automatiza la detección de errores de software . [19] Es compatible con Windows y Linux. [20] Se archivó tres años después, el 1 de noviembre de 2023. [21]
La prueba de programas con entradas aleatorias se remonta a la década de 1950, cuando los datos todavía se almacenaban en tarjetas perforadas . [22] Los programadores utilizaban tarjetas perforadas extraídas de la basura o barajas de números aleatorios como entrada a los programas informáticos. Si una ejecución revelaba un comportamiento no deseado, se había detectado un error .
La ejecución de entradas aleatorias también se denomina prueba aleatoria o prueba de mono .
En 1981, Duran y Ntafos investigaron formalmente la efectividad de probar un programa con entradas aleatorias. [23] [24] Si bien las pruebas aleatorias habían sido ampliamente percibidas como el peor medio para probar un programa, los autores pudieron demostrar que es una alternativa rentable a técnicas de prueba más sistemáticas.
En 1983, Steve Capps de Apple desarrolló "The Monkey", [25] una herramienta que generaría entradas aleatorias para aplicaciones clásicas de Mac OS , como MacPaint . [26] El "mono" figurativo se refiere al teorema del mono infinito que establece que un mono que presione teclas al azar en un teclado de máquina de escribir durante una cantidad infinita de tiempo eventualmente escribirá las obras completas de Shakespeare. En el caso de las pruebas, el mono escribiría la secuencia particular de entradas que desencadenarían un bloqueo.
En 1991, se lanzó la herramienta crashme, cuyo objetivo era probar la solidez de los sistemas operativos Unix y similares mediante la ejecución aleatoria de llamadas al sistema con parámetros elegidos aleatoriamente. [27]
Un fuzzer se puede clasificar de varias maneras: [28] [1]
Un fuzzer basado en mutaciones aprovecha un corpus existente de entradas de semillas durante el fuzzing. Genera entradas modificando (o más bien mutando ) las semillas proporcionadas. [29] Por ejemplo, al fuzzear la biblioteca de imágenes libpng , el usuario proporcionaría un conjunto de archivos de imagen PNG válidos como semillas, mientras que un fuzzer basado en mutaciones modificaría estas semillas para producir variantes semiválidas de cada semilla. El corpus de archivos de semillas puede contener miles de entradas potencialmente similares. La selección automática de semillas (o reducción de la suite de pruebas) permite a los usuarios elegir las mejores semillas para maximizar la cantidad total de errores encontrados durante una campaña de fuzzing. [30]
Un fuzzer basado en generación genera entradas desde cero. Por ejemplo, un fuzzer inteligente basado en generación [31] toma el modelo de entrada proporcionado por el usuario para generar nuevas entradas. A diferencia de los fuzzers basados en mutaciones, un fuzzer basado en generación no depende de la existencia o calidad de un corpus de entradas iniciales.
Algunos fuzzers tienen la capacidad de hacer ambas cosas, generar entradas desde cero y generar entradas mediante mutación de semillas existentes. [32]
Por lo general, los fuzzers se utilizan para generar entradas para programas que toman entradas estructuradas, como un archivo , una secuencia de eventos de teclado o mouse , o una secuencia de mensajes . Esta estructura distingue la entrada válida que es aceptada y procesada por el programa de la entrada no válida que es rechazada rápidamente por el programa. Lo que constituye una entrada válida puede especificarse explícitamente en un modelo de entrada. Ejemplos de modelos de entrada son gramáticas formales , formatos de archivo , modelos de GUI y protocolos de red . Incluso elementos que normalmente no se consideran como entrada pueden ser fuzzeados, como el contenido de bases de datos , memoria compartida , variables de entorno o el entrelazado preciso de subprocesos . Un fuzzer efectivo genera entradas semiválidas que son "lo suficientemente válidas" para que no sean rechazadas directamente del analizador y "lo suficientemente inválidas" para que puedan enfatizar casos extremos y ejercitar comportamientos interesantes del programa.
Un fuzzer inteligente (basado en modelos [32] , basado en gramática [31] [33] o basado en protocolo [34] ) aprovecha el modelo de entrada para generar una mayor proporción de entradas válidas. Por ejemplo, si la entrada se puede modelar como un árbol de sintaxis abstracta , entonces un fuzzer inteligente basado en mutación [33] emplearía transformaciones aleatorias para mover subárboles completos de un nodo a otro. Si la entrada se puede modelar mediante una gramática formal , un fuzzer inteligente basado en generación [31] instanciaría las reglas de producción para generar entradas que sean válidas con respecto a la gramática. Sin embargo, generalmente el modelo de entrada debe proporcionarse explícitamente, lo que es difícil de hacer cuando el modelo es propietario, desconocido o muy complejo. Si se dispone de un gran corpus de entradas válidas e inválidas, una técnica de inducción gramatical , como el algoritmo L* de Angluin , podría generar un modelo de entrada. [35] [36]
Un fuzzer tonto [37] [38] no requiere el modelo de entrada y, por lo tanto, se puede emplear para fuzzear una variedad más amplia de programas. Por ejemplo, AFL es un fuzzer tonto basado en mutaciones que modifica un archivo de semillas invirtiendo bits aleatorios , sustituyendo bytes aleatorios con valores "interesantes" y moviendo o eliminando bloques de datos. Sin embargo, un fuzzer tonto puede generar una proporción menor de entradas válidas y estresar el código del analizador en lugar de los componentes principales de un programa. La desventaja de los fuzzers tontos se puede ilustrar por medio de la construcción de una suma de comprobación válida para una verificación de redundancia cíclica (CRC). Una CRC es un código de detección de errores que garantiza que la integridad de los datos contenidos en el archivo de entrada se preserve durante la transmisión . Se calcula una suma de comprobación sobre los datos de entrada y se registra en el archivo. Cuando el programa procesa el archivo recibido y la suma de comprobación registrada no coincide con la suma de comprobación recalculada, entonces el archivo se rechaza como inválido. Ahora bien, es poco probable que un fuzzer que no conozca el CRC genere la suma de comprobación correcta. Sin embargo, hay intentos de identificar y volver a calcular una posible suma de comprobación en la entrada mutada, una vez que un fuzzer basado en mutaciones ha modificado los datos protegidos. [39]
Por lo general, se considera que un fuzzer es más efectivo si logra un mayor grado de cobertura del código . La razón es que, si un fuzzer no ejercita ciertos elementos estructurales del programa, tampoco puede revelar errores que se esconden en estos elementos. Algunos elementos del programa se consideran más críticos que otros. Por ejemplo, un operador de división puede causar un error de división por cero , o una llamada al sistema puede hacer que el programa se bloquee.
Un fuzzer de caja negra [37] [33] trata al programa como una caja negra y no es consciente de la estructura interna del programa. Por ejemplo, una herramienta de prueba aleatoria que genera entradas al azar se considera un fuzzer de caja negra. Por lo tanto, un fuzzer de caja negra puede ejecutar varios cientos de entradas por segundo, se puede paralelizar fácilmente y se puede escalar a programas de tamaño arbitrario. Sin embargo, los fuzzers de caja negra solo pueden arañar la superficie y exponer errores "superficiales". Por lo tanto, existen intentos de desarrollar fuzzers de caja negra que puedan aprender de manera incremental sobre la estructura interna (y el comportamiento) de un programa durante el fuzzing al observar la salida del programa dada una entrada. Por ejemplo, LearnLib emplea aprendizaje activo para generar un autómata que representa el comportamiento de una aplicación web.
Un fuzzer de caja blanca [38] [32] aprovecha el análisis del programa para aumentar sistemáticamente la cobertura del código o para alcanzar ciertas ubicaciones críticas del programa. Por ejemplo, SAGE [40] aprovecha la ejecución simbólica para explorar sistemáticamente diferentes caminos en el programa (una técnica conocida como ejecución concólica ). Si la especificación del programa está disponible, un fuzzer de caja blanca podría aprovechar técnicas de pruebas basadas en modelos para generar entradas y verificar las salidas del programa contra la especificación del programa. Un fuzzer de caja blanca puede ser muy eficaz para exponer errores que se esconden en lo profundo del programa. Sin embargo, el tiempo utilizado para el análisis (del programa o su especificación) puede llegar a ser prohibitivo. Si el fuzzer de caja blanca tarda relativamente demasiado en generar una entrada, un fuzzer de caja negra será más eficiente. [41] Por lo tanto, existen intentos de combinar la eficiencia de los fuzzers de caja negra y la efectividad de los fuzzers de caja blanca. [42]
Un fuzzer de caja gris aprovecha la instrumentación en lugar del análisis del programa para obtener información sobre el programa. Por ejemplo, AFL y libFuzzer utilizan instrumentación liviana para rastrear transiciones de bloques básicas ejercidas por una entrada. Esto genera una sobrecarga de rendimiento razonable pero informa al fuzzer sobre el aumento en la cobertura de código durante el fuzzing, lo que hace que los fuzzers de caja gris sean herramientas de detección de vulnerabilidades extremadamente eficientes. [43]
El fuzzing se utiliza principalmente como una técnica automatizada para exponer vulnerabilidades en programas críticos para la seguridad que podrían ser explotadas con intenciones maliciosas. [6] [16] [17] De manera más general, el fuzzing se utiliza para demostrar la presencia de errores en lugar de su ausencia. Ejecutar una campaña de fuzzing durante varias semanas sin encontrar un error no prueba que el programa sea correcto. [44] Después de todo, el programa aún puede fallar para una entrada que aún no se ha ejecutado; ejecutar un programa para todas las entradas es prohibitivamente costoso. Si el objetivo es probar que un programa es correcto para todas las entradas, debe existir una especificación formal y deben usarse técnicas de métodos formales .
Para descubrir errores, un fuzzer debe poder distinguir entre el comportamiento esperado (normal) del inesperado (con errores) del programa. Sin embargo, una máquina no siempre puede distinguir un error de una característica. En las pruebas de software automatizadas , esto también se denomina el problema del oráculo de prueba . [45] [46]
Por lo general, un fuzzer distingue entre entradas que fallan y entradas que no fallan en ausencia de especificaciones y para usar una medida simple y objetiva. Las fallas se pueden identificar fácilmente y pueden indicar vulnerabilidades potenciales (por ejemplo, denegación de servicio o ejecución de código arbitrario ). Sin embargo, la ausencia de una falla no indica la ausencia de una vulnerabilidad. Por ejemplo, un programa escrito en C puede o no fallar cuando una entrada causa un desbordamiento de búfer . Más bien, el comportamiento del programa es indefinido .
Para hacer que un fuzzer sea más sensible a fallas distintas de los bloqueos, se pueden usar sanitizadores para inyectar afirmaciones que hagan que el programa se bloquee cuando se detecta una falla. [47] [48] Existen diferentes sanitizadores para diferentes tipos de errores:
El fuzzing también se puede utilizar para detectar errores "diferenciales" si hay una implementación de referencia disponible. Para las pruebas de regresión automatizadas , [49] las entradas generadas se ejecutan en dos versiones del mismo programa. Para las pruebas diferenciales automatizadas , [50] las entradas generadas se ejecutan en dos implementaciones del mismo programa (por ejemplo, lighttpd y httpd son ambas implementaciones de un servidor web). Si las dos variantes producen una salida diferente para la misma entrada, entonces una puede tener errores y debe examinarse más de cerca.
El análisis estático de programas analiza un programa sin ejecutarlo realmente. Esto puede dar lugar a falsos positivos en los que la herramienta informa de problemas con el programa que en realidad no existen. Se puede utilizar el fuzzing en combinación con el análisis dinámico de programas para intentar generar una entrada que realmente detecte el problema informado. [51]
Los navegadores web modernos se someten a pruebas exhaustivas de fuzzing. El código Chromium de Google Chrome es sometido a pruebas de fuzzing continuas por parte del equipo de seguridad de Chrome con 15 000 núcleos. [52] Para Microsoft Edge e Internet Explorer , Microsoft realizó pruebas de fuzzing con 670 años-máquina durante el desarrollo del producto, generando más de 400 mil millones de manipulaciones DOM a partir de mil millones de archivos HTML. [53] [52]
Un fuzzer produce una gran cantidad de entradas en un tiempo relativamente corto. Por ejemplo, en 2016, el proyecto Google OSS-fuzz produjo alrededor de 4 billones de entradas por semana. [17] Por lo tanto, muchos fuzzers proporcionan una cadena de herramientas que automatiza tareas que de otro modo serían manuales y tediosas y que siguen a la generación automatizada de entradas que inducen fallas.
El triage automatizado de errores se utiliza para agrupar una gran cantidad de entradas que inducen fallos por causa raíz y para priorizar cada error individual por gravedad. Un fuzzer produce una gran cantidad de entradas, y muchas de las que inducen fallos pueden exponer efectivamente el mismo error de software . Solo algunos de estos errores son críticos para la seguridad y deberían ser parcheados con mayor prioridad. Por ejemplo, el Centro de Coordinación CERT proporciona las herramientas de triage de Linux que agrupan las entradas que causan fallos por el seguimiento de la pila producido y enumera cada grupo según su probabilidad de ser explotable . [54] El Centro de Investigación de Seguridad de Microsoft (MSEC) desarrolló la herramienta "!exploitable" que primero crea un hash para una entrada que causa fallos para determinar su unicidad y luego asigna una calificación de explotabilidad: [55]
Los errores clasificados y no informados previamente pueden ser reportados automáticamente a un sistema de seguimiento de errores . Por ejemplo, OSS-Fuzz ejecuta campañas de fuzzing a gran escala y de larga duración para varios proyectos de software críticos para la seguridad, donde cada error distinto y no informado previamente se informa directamente a un rastreador de errores. [17] El rastreador de errores de OSS-Fuzz informa automáticamente al mantenedor sobre el software vulnerable y verifica en intervalos regulares si el error ha sido corregido en la revisión más reciente utilizando la entrada minimizada de inducción de errores cargada.
La minimización automática de entradas (o reducción de casos de prueba) es una técnica de depuración automática para aislar la parte de la entrada que induce el error y que en realidad lo está provocando. [56] [57] Si la entrada que induce el error es grande y está mayoritariamente malformada, puede resultar difícil para un desarrollador comprender qué es exactamente lo que está causando el error. Dada la entrada que induce el error, una herramienta de minimización automática eliminaría tantos bytes de entrada como fuera posible mientras seguía reproduciendo el error original. Por ejemplo, la depuración delta es una técnica de minimización automática de entradas que emplea un algoritmo de búsqueda binaria extendida para encontrar dicha entrada mínima. [58]
La siguiente es una lista de fuzzers descritos como "populares", "ampliamente utilizados" o similares en la literatura académica. [59] [60]