En programación y desarrollo de software , la fuzzing o prueba de 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 , aserciones de código integradas fallidas o posibles pérdidas de memoria . Normalmente, los fuzzers se utilizan para probar programas que reciben entradas estructuradas. Esta estructura se especifica, por ejemplo, en un formato de archivo o protocolo y distingue las entradas válidas de las no válidas. Un fuzzer efectivo genera entradas semi-válidas que son "lo suficientemente válidas" en el sentido de que no son rechazadas directamente por el analizador, pero crean comportamientos inesperados más profundamente en el programa y son "lo suficientemente inválidas" como para exponer casos extremos que no se han tratado adecuadamente. con.
A 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 difuminar el código que maneja la carga de un archivo por parte de cualquier usuario que difuminar el código que analiza un archivo de configuración al que solo puede acceder un usuario privilegiado.
El término "fuzz" se origina en un proyecto de clase de 1988 [2] en la clase de posgrado de Sistemas Operativos Avanzados (CS736), impartida por el Prof. Barton Miller en la Universidad de Wisconsin , cuyos resultados se publicaron posteriormente en 1990. [3] [4 ] Para probar de forma difusa una utilidad UNIX destinada a generar automáticamente parámetros de entrada aleatorios y de línea de comandos 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 fallaron. 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 fallos para determinar la causa y categorizaron cada fallo detectado. 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 sin procesar de los resultados se pusieron a disposición del público. [5] Esta temprana confusión se llamaría ahora confusión de caja negra, generacional, no estructurada (tonta o "clásica").
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 pelusa." [4]
Una contribución clave de estos primeros trabajos fue un oráculo simple (casi simplista). Un programa falló su prueba si fallaba o se bloqueaba bajo la entrada aleatoria y se consideraba que había pasado en caso contrario. Si bien construir oráculos de prueba puede ser un desafío, el oráculo para estas primeras pruebas difusas fue 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 de seguridad del navegador web Chromium . [6] Los investigadores de seguridad pueden cargar sus propios fuzzers y recolectar recompensas por errores si ClusterFuzz encuentra un fallo con el fuzzer cargado.
En septiembre de 2014, Shellshock [7] fue revelado como una familia de errores de seguridad en el shell UNIX Bash ampliamente utilizado ; la mayoría de las vulnerabilidades de Shellshock se encontraron utilizando el fuzzer AFL . [8] (Muchos servicios conectados 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 una computadora. sistema [9] )
En abril de 2015, Hanno Böck mostró cómo el fuzzer AFL pudo 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 comunicaciones cifradas . La vulnerabilidad se introdujo accidentalmente en OpenSSL , que implementa TLS y es utilizada por la mayoría de los servidores en Internet Shodan informó que 238.000 máquinas aún eran 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 totalmente automatizada de capturar la bandera que duró 11 horas. [14] El objetivo era desarrollar sistemas de defensa automáticos que puedan descubrir, explotar y corregir fallas de software en tiempo real . El fuzzing se utilizó como una estrategia ofensiva eficaz para descubrir fallas en el software de los oponentes. Mostró un enorme potencial en la automatización de la detección de vulnerabilidades. El ganador fue un sistema llamado "Mayhem" [15] desarrollado por el equipo ForAllSecure liderado 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 la fuzzización continua 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] Ha sido archivado tres años después, el 1 de noviembre de 2023. [21]
Los programas de prueba con entradas aleatorias se remontan a la década de 1950, cuando los datos todavía se almacenaban en tarjetas perforadas . [22] Los programadores usarían tarjetas perforadas extraídas de la basura o barajas de números aleatorios como entrada para los programas de computadora. 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, Durán y Ntafos investigaron formalmente la eficacia 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 son 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 generaba 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 presiona teclas al azar en el teclado de una máquina de escribir durante un período de tiempo infinito 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 Unix y sistemas operativos similares mediante la ejecución aleatoria de llamadas al sistema con parámetros elegidos al azar. [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 aplicar fuzzer a la biblioteca de imágenes libpng , el usuario proporcionaría un conjunto de archivos de imágenes 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 semilla puede contener miles de entradas potencialmente similares. La selección de semillas automatizada (o reducción del conjunto de pruebas) permite a los usuarios elegir las mejores semillas para maximizar la cantidad total de errores encontrados durante una campaña fuzz. [30]
Un fuzzer basado en generación genera entradas desde cero. Por ejemplo, un fuzzer basado en generación inteligente [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 generaciones no depende de la existencia o calidad de un corpus de insumos de semillas.
Algunos fuzzers tienen la capacidad de hacer ambas cosas: generar insumos desde cero y generar insumos mediante mutación de semillas existentes. [32]
Normalmente, 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 el programa acepta y procesa de la entrada no válida que el programa rechaza rápidamente. 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 GUI y protocolos de red . Incluso los elementos que normalmente no se consideran entradas pueden verse afectados, como el contenido de las bases de datos , la memoria compartida , las 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 por el analizador y "lo suficientemente inválidas" para que puedan enfatizar casos extremos y ejercer comportamientos interesantes en el programa.
Un fuzzer inteligente (basado en modelos, [32] basado en gramática, [31] [33] o basado en protocolos [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 mutaciones [33] emplearía transformaciones aleatorias para mover subárboles completos de un nodo a otro. Si la entrada puede modelarse mediante una gramática formal , un fuzzer inteligente basado en generación [31] crearía instancias de 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 cual 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 tanto, puede emplearse para fuzzer una variedad más amplia de programas. Por ejemplo, AFL es un fuzzer tonto basado en mutaciones que modifica un archivo semilla volteando bits aleatorios , sustituyendo bytes aleatorios con valores "interesantes" y moviendo o eliminando bloques de datos. Sin embargo, un fuzzer tonto podría generar una menor proporción 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 mediante la construcción de una suma de verificación válida para una verificación de redundancia cíclica (CRC). Un 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 verificación registrada no coincide con la suma de verificación recalculada, el archivo se rechaza como no válido. Ahora bien, es poco probable que un fuzzer que desconoce el CRC genere la suma de comprobación correcta. Sin embargo, hay intentos de identificar y recalcular una posible suma de verificación en la entrada mutada, una vez que un tonto fuzzer basado en mutaciones ha modificado los datos protegidos. [39]
Normalmente, un fuzzer se considera más eficaz si logra un mayor grado de cobertura de código . La razón es que, si un fuzzer no ejercita ciertos elementos estructurales en el programa, tampoco podrá 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 provocar un error de división por cero o una llamada al sistema puede bloquear el programa.
Un fuzzer de caja negra [37] [33] trata el programa como una caja negra y desconoce 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, puede paralelizarse fácilmente y puede escalarse a programas de tamaño arbitrario. Sin embargo, los fuzzers de caja negra sólo pueden rayar la superficie y exponer errores "superficiales". Por lo tanto, hay intentos de desarrollar fuzzers de caja negra que puedan aprender incrementalmente sobre la estructura interna (y el comportamiento) de un programa durante la fuzzing observando 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 de programas para aumentar sistemáticamente la cobertura del código o para llegar a ciertas ubicaciones críticas del programa. Por ejemplo, SAGE [40] aprovecha la ejecución simbólica para explorar sistemáticamente diferentes rutas 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 comparar las salidas del programa con la especificación del programa. Un fuzzer de caja blanca puede ser muy eficaz para exponer errores que se esconden en lo más profundo del programa. Sin embargo, el tiempo empleado para el análisis (del programa o de su especificación) puede resultar prohibitivo. Si el fuzzer de caja blanca tarda demasiado en generar una entrada, un fuzzer de caja negra será más eficiente. [41] Por lo tanto, hay 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 recopilar 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 conduce a una sobrecarga de rendimiento razonable, pero informa al fuzzer sobre el aumento en la cobertura del código durante la 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 técnica automatizada para exponer vulnerabilidades en programas críticos para la seguridad que podrían explotarse con intenciones maliciosas. [6] [16] [17] De manera más general, la 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 demuestra que el programa sea correcto. [44] Después de todo, el programa aún puede fallar por una entrada que aún no se ha ejecutado; ejecutar un programa para todos los insumos es prohibitivamente costoso. Si el objetivo es demostrar que un programa es correcto para todas las entradas, debe existir una especificación formal y se deben utilizar técnicas de métodos formales .
Para exponer errores, un fuzzer debe poder distinguir 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 problema del oráculo de prueba . [45] [46]
Normalmente, un fuzzer distingue entre entradas que fallan y que no fallan en ausencia de especificaciones y para utilizar una medida simple y objetiva. Los fallos 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 un bloqueo no indica la ausencia de una vulnerabilidad. Por ejemplo, un programa escrito en C puede fallar o no cuando una entrada provoca un desbordamiento del búfer . Más bien, el comportamiento del programa no está definido .
Para hacer que un fuzzer sea más sensible a fallas distintas a las fallas, se pueden usar desinfectantes para inyectar aserciones que bloqueen el programa cuando se detecta una falla. [47] [48] Existen diferentes desinfectantes para diferentes tipos de errores:
La 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 pruebas diferenciales automatizadas , [50] las entradas generadas se ejecutan en dos implementaciones del mismo programa (por ejemplo, lighttpd y httpd son implementaciones de un servidor web). Si las dos variantes producen resultados diferentes para la misma entrada, entonces una puede tener errores y debería examinarse más de cerca.
El análisis estático de programas analiza un programa sin ejecutarlo realmente. Esto podría dar lugar a falsos positivos en los que la herramienta informa problemas con el programa que en realidad no existen. Se puede utilizar la fuzzing en combinación con el análisis dinámico del programa para intentar generar una entrada que realmente sea testigo del problema informado. [51]
Los navegadores web modernos se someten a una extensa confusión. El equipo de seguridad de Chrome con 15.000 núcleos modifica continuamente el código Chromium de Google Chrome . [52] Para Microsoft Edge e Internet Explorer , Microsoft realizó pruebas difusas 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 OSS-fuzz de Google 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, que siguen a la generación automatizada de entradas que inducen fallas.
La clasificación automatizada de errores se utiliza para agrupar una gran cantidad de entradas que provocan fallas 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 provocan fallas pueden exponer efectivamente el mismo error de software . Sólo algunos de estos errores son críticos para la seguridad y deben corregirse con mayor prioridad. Por ejemplo, el Centro de Coordinación CERT proporciona herramientas de clasificación de Linux que agrupan las entradas que fallan según el seguimiento de la pila producido y enumeran cada grupo según su probabilidad de ser explotables . [54] El Centro de Investigación de Seguridad de Microsoft (MSEC) desarrolló la herramienta explotable que primero crea un hash para una entrada que falla para determinar su singularidad y luego asigna una calificación de explotabilidad: [55]
Los errores clasificados y no informados anteriormente pueden informarse automáticamente a un sistema de seguimiento de errores . Por ejemplo, OSS-Fuzz ejecuta campañas de fuzzing de larga duración y gran escala para varios proyectos de software críticos para la seguridad donde cada error distinto y no reportado previamente se informa directamente a un rastreador de errores. [17] El rastreador de errores OSS-Fuzz informa automáticamente al mantenedor del software vulnerable y verifica en intervalos regulares si el error se ha solucionado en la revisión más reciente utilizando la entrada minimizada que induce fallas cargada.
La minimización de entradas automatizada (o reducción de casos de prueba) es una técnica de depuración automatizada para aislar esa parte de la entrada que induce a la falla y que en realidad está provocando la falla. [56] [57] Si la entrada que induce el error es grande y en su mayoría está mal formada, puede resultar difícil para un desarrollador comprender qué está causando exactamente el error. Dada la entrada que provoca fallas, una herramienta de minimización automatizada eliminaría tantos bytes de entrada como sea posible sin dejar de reproducir el error original. Por ejemplo, Delta Debugging es una técnica de minimización de entrada automatizada que emplea un algoritmo de búsqueda binaria extendido para encontrar una 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]