Introducción
No cabe duda de que el fuzzing se ha convertido en una de las principales técnicas a aplicar a la hora de identificar vulnerabilidades y fallos en productos software. Esta técnica consiste en ejecutar un software un gran número de veces por segundo, aportando en cada ejecución unos datos de entrada que van siendo mutados progresivamente por herramientas conocidas como fuzzers. La idea principal es encontrar unos datos de entrada para los cuales el software no sea capaz de gestionarlos correctamente, dando lugar así a problemas como corrupción de memoria o cuelgues.
El fuzzing se ha popularizado en la última década gracias a su gran efectividad y facilidad de uso. Vulnerabilidades archiconocidas como Shellshock o Heartbleed las cuales afectaron a millones de dispositivos por todo el mundo, fueron identificadas mediante fuzzing.
Aunque esta técnica es ampliamente utilizada para poner a prueba software en plataformas más orientadas al propósito general, presenta una serie de retos cuando se trata de implementar en el desarrollo o investigación de software para sistemas empotrados e IoT. Pero, ¿por qué querríamos aplicar fuzzing al internet de las cosas? La respuesta es simple, estos dispositivos trabajan comunicándose entre sí y reciben una gran cantidad de información del exterior. Si dicha información no es validada adecuadamente pueden originarse vulnerabilidades en el software. El fuzzing nos permitirá automatizar ese proceso de generar datos de entrada posiblemente mal formados para poner a prueba un software.
En este artículo hablaremos sobre cómo aplicar fuzzing a software desarrollado para dispositivos empotrados e IoT a través de técnicas como la emulación y la instrumentación dinámica, con el objetivo de aprender una forma innovadora de evaluar aspectos de la seguridad de aparatos como routers, bombillas inteligentes, IoT de aplicación industrial, etc.
Fuzzing con emulación e instrumentación dinámica
Dado que este tipo de dispositivos suele caracterizarse por tener unos recursos muy limitados, querremos recurrir a métodos alternativos para evitar realizar el fuzzing sobre el hardware original. Una posible solución es emular en un ordenador de propósito general el firmware o los componentes software del sistema. Desgraciadamente, todo el que haya intentado alguna vez emular software diseñado para plataformas específicas sabe de los problemas de inestabilidad y compatibilidad comúnmente asociados a esto. Este caso no es una excepción, una gran mayoría del software IoT emulado con herramientas como QEMU no se ejecutará correctamente debido a la existencia de tareas con fuertes dependencias sobre otros procesos o sobre el hardware original como antenas o microprocesadores auxiliares. Para solucionar esto podríamos:
Caso práctico: Netgear R7000
Para demostrar el funcionamiento de Qiling y su uso junto a fuzzers, reproduciremos una vulnerabilidad descubierta por la firma de ciberseguridad GRIMM en abril de 2022. La vulnerabilidad se trata de un desbordamiento de pila producido en el proceso de actualización de firmware del router Netgear R7000. Durante este proceso se extraen una serie de parámetros de la cabecera del firmware como su tamaño o el modelo del dispositivo para el que está destinado el firmware. Dado que el parámetro que indica el tamaño de la cabecera no es validado antes de realizar operaciones de memoria (memcpy), un usuario podría proporcionar un paquete de actualización modificado con el que se escriba más allá de los límites del buffer de destino, alterando así el valor de los registros del procesador y consiguiendo ejecución remota de código.
Pongámonos en la situación de que deseamos evaluar el proceso de actualización de dicho router mediante fuzzing. Utilizar el hardware real del dispositivo para ir aplicando paquetes de actualización mutados puede ser lento y tedioso, por lo que recurrimos a la emulación. Para ello empezaremos por obtener el firmware del dispositivo en su versión 1.0.11.128 desde su portal oficial de soporte. En caso de no tener acceso al firmware se podría recurrir a crear un volcado desde el propio dispositivo a través de JTAG o con un programador EEPROM. Una vez tenemos el firmware, extraemos su sistema de archivos SquashFS utilizando Binwalk sobre la imagen del firmware (fichero con extensión .chk) para obtener acceso a los distintos binarios que contiene. *Figura 1: Extracción de firmware con Binwalk *Figura 2: Sistema de archivos del Netgear R7000Ahora es necesario identificar qué binario y qué funciones de código gestionan la actualización de firmware mediante ingeniería inversa. Tras esto, descubrimos que nuestro binario de interés se trata del demonio UPNP del router (usr/bin/upnpd), el cual posee una función que recibe el paquete de actualización y se encarga de extraer los parámetros de la cabecera de este para realizar comprobaciones en base a ellos. Analizamos el binario con Ghidra para ver el código decompilado de la función y la llamada a memcpy insegura en el paso 3.
*Figura 3: Decompilado en Ghidra de la comprobación de cabeceras de firmware. Se comprueban número mágico del fichero, checksum y modelo de dispositivo Ahora que hemos decidido tomar esta función como punto de partida, es necesario asegurarnos de que el código puede ser emulado correctamente antes de plantearnos aplicar fuzzing. Para ello, hacemos un pequeño script en Python que use la API de Qiling para escribir en memoria el firmware de entrada, cambiar la función principal del binario a nuestra función de interés y continuar con la ejecución. Ejecutamos el script pasándole un paquete firmware y observamos que, aunque las comprobaciones de número mágico y checksum se realizan correctamente, la del modelo falla debido a que no estamos emulando una NVRAM que contenga dicho parámetro. *Figura 4: Emulación de la función de comprobación de cabeceras sin NVRAM Para solucionarlo, podemos saltar dicha comprobación o si deseamos ejecutar la función en su totalidad podemos utilizar la herramienta nvram-faker para interceptar las lecturas a la NVRAM y hacer que devuelvan el valor deseado. ¡Emulamos la función al completo con nvram-faker! *Figura 5: Emulación de la función de comprobación de cabeceras con nvram-faker Una vez que sabemos que la emulación funciona correctamente, podemos usar el script creado como base para integrar el proceso de emulación con un fuzzer. De esta forma, el fuzzer se encarga de generar las mutaciones que posteriormente Qiling proporcionará al binario emulado para su ejecución. Qiling puede integrarse fácilmente con AFL++ o incluso con fuzzers de caja negra como Radamsa. Para integrar el script con AFL++ simplemente hacemos que la escritura en memoria de los bytes del firmware se realice en un callback llamado por AFL++ en lugar de hacerlo nosotros manualmente. Así, la función será llamada en cada iteración de fuzzing con el firmware mutado como parámetro. Por último, preparamos un conjunto de cabeceras firmware válidas que servirán al fuzzer como punto de partida. Este conjunto estará formado por los tres últimos paquetes de actualización disponibles en el portal de soporte. Arrancamos AFL++ en modo Unicorn con el nuevo script y esperamos. *Figura 6: Inicio de sesión de fuzzing en AFL++. *Figura 7: Fuzzing en AFL++ con salida de debug activada. En menos de un minuto AFL++ es capaz de detectar el crash provocado por el desbordamiento de pila en la función. La cabecera firmware mutada que origina el crash especifica un tamaño (representado por los bytes 5, 6 y 7) superior al disponible en el buffer de destino. Si probamos a emular la función con el firmware mutado comprobamos que efectivamente se produce una escritura inválida en memoria. ¡Hemos dado con la vulnerabilidad que andábamos buscando! *Figura 8: Crash producido por firmware mutado.Conclusión
Aunque el fuzzing es una técnica efectiva ampliamente utilizada a día de hoy, no suele ser aplicada al mundo del IoT y los sistemas empotrados debido a los retos y dificultades que esto supone. Combinar los conocimientos sobre emulación, instrumentación dinámica y fuzzing que hemos tratado en este artículo nos permite ir un paso más allá a la hora de poner a prueba la seguridad de todo tipo de dispositivos inteligentes para identificar vulnerabilidades que de otra manera podrían pasar desapercibidas.
Todo el código utilizado puede ser consultado desde aquí.