Evaluando firmware IoT a través de fuzzing y emulación

Blog

24
- Ago
2022
Evaluando firmware IoT a través de fuzzing y emulación

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:

  • Llevar a cabo un proceso de ingeniería inversa en el que se investiguen los periféricos y la configuración del sistema que requieren los distintos componentes del firmware para funcionar correctamente. Una vez hecho esto, se configuraría el entorno de emulación adecuadamente. Esta opción resulta bastante costosa en tiempo y esfuerzo ya que se busca replicar con un alto nivel de exactitud el dispositivo original mediante emulación.
  • Complementar una emulación básica con el uso de instrumentación dinámica para modificar aspectos de la ejecución del binario, solucionando así en tiempo de ejecución los problemas identificados. Por ejemplo, si se desea realizar fuzzing sobre un demonio que falla al llevar a cabo comprobaciones previas sobre el entorno (listado de interfaces de red, NVRAM, variables de entorno, etc.), podríamos parchear o incluso saltar estas comprobaciones y ejecutar exclusivamente la funcionalidad que nos interesa. Este enfoque de parchear dinámicamente la ejecución del binario hace mucho más ameno el proceso de conseguir una "correcta" emulación.
  • Hay una gran cantidad de herramientas que nos permiten instrumentar código dinámicamente, pero en este artículo nos centraremos en Qiling y Unicorn. Estos frameworks no solo son capaces de emular binarios compilados para otras arquitecturas, sino que también proporcionan una API de bajo nivel con funcionalidad similar a la de FRIDA con la que poder alterar la ejecución del proceso.

    Unicorn es un emulador multi-arquitectura que se limita a emular exclusivamente instrucciones de CPU. Aunque esto es un buen punto de partida, no se tienen en cuenta aspectos de más alto nivel como llamadas al sistema o I/O por lo que será necesario parchear la ejecución dinámicamente para evitar tareas comunes como forks de procesos, lectura y escritura de ficheros, etc. En cambio, Qiling es un framework de más alto nivel capaz de solucionar este problema. Qiling hace uso de Unicorn para emular instrucciones de CPU implementando también funcionalidad a nivel de sistema operativo como manejadores de llamadas al sistema e I/O o enlazado dinámico de librerías. A continuación veremos cómo podemos usar este framework para identificar mediante fuzzing una vulnerabilidad conocida en un router Netgear.

    En resumen, para aplicar fuzzing sobre binarios extraídos del firmware de un dispositivo empotrado/IoT querremos usar herramientas que nos posibiliten tanto emulación como instrumentación dinámica de código para obtener los mejores resultados.

    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 R7000

    Ahora 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í.

    Sergio García/Evaluador Junior

    Ingeniero informático por la Universidad de Granada trabajando como Evaluador de ciberseguridad junior en jtsec desde Enero de 2022, participando en proyectos relacionados con la certificación LINCE de productos. Sergio busca seguir expandiendo sus conocimientos sobre ciberseguridad y su aplicación en diferentes metodologías.

    .


    Contacto

    ¡Envíanos tus dudas o sugerencias!

    Al enviar tus datos nos permites que los usemos para resolver tus dudas enviándote información comercial de tu interés. Los suprimiremos cuando dejen de ser necesarios para esto. Infórmate de tus derechos en nuestra Política de Privacidad.