fuzzing parte 1

Fuzzing Parte 1: La teoría

Fuzzing

En primer lugar, ¿qué es exactamente el fuzzing? Cuando probamos fuzz un programa o función que recibe entrada (cualquier tipo de entrada), probamos diferentes combinaciones de entrada hasta que obtenemos un aplastamiento u otro resultado deseado (frecuentemente pérdidas de memoria). Cuando un programa no desinfecta su entrada correctamente, la entrada mal formada o incorrecta puede provenir del usuario, lo que puede causar fallas y un comportamiento extraño o indefinido. Un buen ejemplo de fuzzing del que probablemente haya oído hablar antes es desbaratar, cuando probamos diferentes URLs, comenzando primero por las comunes hasta encontrar directorios legítimos en un sitio web. Herramientas como puf hacer un excelente trabajo en ello.

Fuzzing es especialmente útil en programas escritos en lenguajes de bajo nivel o más antiguos, como Basic, Assembly, C/C++, etc., que no evitan que el programador escriba errores como encasillamiento incorrecto, use after free [of a pointer], o desbordamiento de memoria (búfer, pila, número entero), que normalmente no provocan necesariamente fallas en tiempo de ejecución o errores en tiempo de compilación y podrían ser explotados por un atacante astuto. Por lo general, cuando se menciona el término 'fuzzing', se refiere a este tipo de fuzzing.

Tipos de fuzzing

Hay tres enfoques de fuzzing que debemos enumerar.

Fuzzing de caja blanca

Por 'fuzzing de caja blanca' nos referimos a un tipo de fuzzing en el que el fuzzer intenta analizar la estructura interna del programa para rastrear y maximizar la cobertura del código. La cobertura del código se refiere a la proporción de sucursales que alcanzamos en nuestro código; cada vez que una declaración condicional como if or mientras se ejecuta, el código se divide en una rama donde la declaración es verdadera y otra donde es falsa. La razón es que si hay un error oculto en alguna rama, primero debemos asegurarnos de llegar a esa rama en primer lugar. Para realizar este tipo de análisis, el programa debe ser instrumentado durante la compilación.

La instrumentación funciona llamando a una función especial cada vez que se ejecuta una rama, que registra que se ha ejecutado, a veces incluso con la resolución de qué línea, para que se pueda rastrear la cobertura. La instrumentación también podría ayudar a encontrar errores. Por ejemplo, Address Sanitation (ASan) ayuda a encontrar fugas de memoria que normalmente no provocan bloqueos. Dado que el análisis estructural completo requiere una gran cantidad de recursos, el fuzzing de caja blanca es lento, pero mucho más efectivo para encontrar errores y vulnerabilidades, especialmente las que están ocultas en lo profundo del programa. Por supuesto Para realizar una instrumentación extensa, se debe tener acceso al código fuente, que no siempre está disponible, especialmente como atacante.

Fuzzing de caja negra

En este enfoque, por otro lado, no nos importa la estructura del programa y lo tratamos como una caja negra. Aún así, podemos usar la salida del programa para tratar de descubrir qué sucede dentro y, por lo tanto, crear una entrada más inteligente y eficiente. Dado que evitamos la sobrecarga asociada con el fuzzing de caja blanca, el fuzzing de caja negra es mucho más rápido pero menos eficiente para encontrar errores y vulnerabilidades.

Fuzzing de caja gris

Este enfoque intenta lograr un equilibrio entre los respectivos pros y contras de los enfoques antes mencionados; utiliza instrumentación ligera durante el tiempo de compilación en lugar de un análisis completo para calcular la cobertura del código y rastrear el estado del programa. Los fuzzers más populares hoy en día, como AFL, HongFuzz y LibFuzzer, son fuzzers de caja gris. Para ejecutar dicho fuzzing, generalmente se necesita el código fuente, pero existen soluciones alternativas si solo hay un ejecutable disponible, pero perjudican gravemente el rendimiento. En este artículo no tocaré estas técnicas.

Técnicas de Fuzzing

Al igual que en el dirbusting, no es necesario realizar el fuzzing utilizando la fuerza bruta, es decir, intentando una entrada aleatoria. De hecho, la fuerza bruta no es una buena técnica para fuzzing y esto rara vez se hace. Afortunadamente, hay una serie de técnicas más inteligentes que conducen a mejores resultados en cantidades razonables de tiempo y recursos. Aquí hablaré de dos de ellos:

Fuzzing basado en mutaciones

Esta técnica se basa en el fuzzing de caja gris. Comenzamos con una entrada legítima bien formada y le aplicamos un par de tipos de mutaciones. Usando instrumentación, intentamos tomar las mutaciones más exitosas, que revelan nuevas ramas, y usarlas como semilla para la próxima generación. Este proceso recuerda un poco a la selección natural darwiniana que vemos en la naturaleza. También tenemos el útil efecto secundario de que las mutaciones filtradas por el programa quedan fuera de uso y nos quedamos solo con la entrada de trabajo.

La herramienta más conocida (y posiblemente el fuzzer más conocido en general) es AFL, o American Fuzzy Lop.

AFL utiliza tres tipos de mutaciones básicas:

  • Eliminación de un bit aleatorio.
  • Inserción de un bit aleatorio.
  • Volteo de un bit aleatorio (de 0 a 1 o al revés)

AFL no trabaja con una sola mutación en un momento dado, sino que distribuye sus recursos entre un par de ellas y prefiere las que tienen más energía. La energía de cada mutación se basa en las ramas del código que ejecuta: a las ramas más raras, que pasan la mayoría de las entradas, se les asigna más energía. Cada rama tiene un hash único, y esto permite rastrear el número de ejecuciones. Es importante mencionar que la entrada inicial en esta técnica puede provenir de un diccionario que contenga más de una variación.

Fuzzing gramatical

Probablemente sepa que muchos programas no pueden recibir entradas no estructuradas, pero tienen reglas específicas sobre cómo se pueden formar las entradas. Estas reglas se denominan sintaxis de entrada, ya que son similares a las reglas sintácticas de los idiomas hablados. Para realizar pruebas de fuzzing de dichos programas de una manera que cubra la mayoría de las opciones sintácticas, el fuzzer debe conocer la sintaxis, de lo contrario, la entrada producida mediante mutaciones simples se filtraría y descartaría. Estos fuzzers ya existen y pueden fuzzear JavaScript, WASM, URL y otros. pero al momento de escribir este artículo, la mayoría de ellos son experimentales y lentos, y hasta donde yo sé, todos están escritos en Python, lo cual es excelente para la creación de prototipos y demostraciones, pero no tanto para fuzzers de grado de producción optimizados. Por lo tanto, rara vez se utilizan.

Siguiente – Parte 2

En el próximo capítulo “Fuzzing con AFL“, demuestro cómo fuzzear un programa simple usando AFL para encontrar fallas que podrían ser peligrosas en el mundo real.

Ir al contenido