Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
blackbox-fuzzing — Fuzzing de dispositivos IoT usando el router TL-WR902AC como ejemplo | Kitploit
Herramientas/GitHubGitHub/otsmr/blackbox-fuzzing
Seguridad IoTAnálisis de VulnerabilidadesExplotaciónIngeniería InversaFuzzingAnálisis de BinariosPapers e InvestigaciónAprendizaje y EducaciónAnálisis de Firmware
GitHubotsmr/blackbox-fuzzing

blackbox-fuzzing

Fuzzing de dispositivos IoT usando el router TL-WR902AC como ejemplo

13217hace 9 mesesRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir
Ver RepositorioSitio web

Fuzzing de caja negra de dispositivos IoT usando el router TL-WR902AC como ejemplo

Esta es la versión HTML de mi trabajo académico, que se puede descargar como PDF aquí.

Introducción

El fuzzing se ha convertido en "una de las formas más eficaces" de encontrar errores en el software. Con esta o afirmaciones similares, muchos artículos actuales relacionados con el fuzzing comienzan [google-scholar]. El objetivo principal de nuestro último trabajo académico sobre el tema "Internet of Vulnerable Things" era encontrar un error relacionado con la memoria y luego escribir un exploit para esa vulnerabilidad. Pudimos encontrar una vulnerabilidad mediante la ingeniería inversa del firmware, pero no se encontraron errores relacionados con la memoria. Encontrar un desbordamiento de búfer mediante la ingeniería inversa manual de un binario no solo consume tiempo, sino que también requiere mucha experiencia. Al mismo tiempo, el fuzzing pretende ser la "forma más eficaz" de encontrar este tipo de vulnerabilidades relacionadas con la memoria. Google, por ejemplo, introdujo OSS-Fuzz, que fuzzea continuamente software de código abierto y ya ha encontrado más de 10 000 vulnerabilidades en 1 000 proyectos [oss-fuzz].

El objetivo de este trabajo académico es, de nuevo, encontrar una vulnerabilidad relacionada con la memoria, pero esta vez mediante fuzzing. La vulnerabilidad objetivo debe ser explotable a través de la red sin conocimiento de las credenciales de administrador. Este documento describe el camino para lograr este objetivo. Para ello, el documento se divide en dos partes. La primera parte se centra en cómo encontrar un objetivo potente, qué herramientas se pueden utilizar y en qué debe consistir un buen objetivo de fuzzing. La segunda parte describe cómo desarrollar y depurar un harness capaz de fuzzear una función específica de un binario. Luego, el harness desarrollado se usa con AFL++ para fuzzear la función objetivo. A continuación se ofrece un breve contexto y se describe cuál es el estado del arte actual en lo que respecta al fuzzing de dispositivos IoT.

Todos los archivos creados en el contexto de este trabajo académico se publican íntegramente en GitHub y se pueden acceder mediante la siguiente URL: otsmr/blackbox-fuzzing.

Estado del arte

Fuzzear dispositivos IoT no es tan fácil como fuzzear un proyecto de código abierto. A menudo, el código fuente es propietario, lo que hace imposible el fuzzing de caja gris, que instrumenta el código fuente para obtener el mejor rendimiento de fuzzing, [afl-persistent]. Además, la arquitectura de CPU a menudo no es compatible de forma nativa con los fuzzers, lo que requiere un emulador como QEMU [qemu], que además ralentiza la velocidad de fuzzing [afl-persistent]. Otro problema son los periféricos de hardware, lo que complica el desarrollo de un enfoque general. El artículo "Embedded Fuzzing: A Review of Challenges, Tools, and Solutions" [embedded-fuzzing] ofrece una visión general de diferentes estrategias de fuzzing, como el fuzzing embebido basado en hardware. La mayoría de estas estrategias necesitan el código fuente del programa objetivo, como cuando se porta el código fuente del fuzzer, como AFL, a dispositivos IoT basados en ARM para ejecutar el fuzzer en el hardware IoT. Ejecutar el fuzzer en el hardware del dispositivo también tiene problemas de rendimiento, porque a menudo tienen CPU de bajo nivel, que son más lentas que las CPU de escritorio normales. Otro enfoque presentado en este artículo es el fuzzing embebido basado en emulación, donde se ejecuta en un emulador un único programa objetivo para realizar fuzzing guiado por cobertura, o bien el sistema completo.

Los enfoques mencionados anteriormente apuntan directamente a un binario mediante un emulador o instrumentando el código fuente. Estos enfoques requieren una configuración de fuzzing que a menudo debe estar diseñada específicamente para un único dispositivo IoT y son difíciles de generalizar. Para ello, los investigadores crearon un programa, IoTFuzzer que pretende ser un framework de fuzzing automatizado orientado a "encontrar vulnerabilidades de corrupción de memoria sin acceso a sus imágenes de firmware [iotfuzzer]." IoTFuzzers se basa en la observación de que la mayoría de los dispositivos IoT tienen una aplicación móvil para controlarlos, y dichas aplicaciones contienen información sobre el protocolo utilizado para comunicarse con el dispositivo. El programa luego identifica y reutiliza la lógica específica del programa para mutar los casos de prueba y así probar eficazmente los objetivos IoT [iotfuzzer].

Antecedentes

Harness

Un harness describe una secuencia de llamadas API que procesan las entradas proporcionadas por el fuzzer. A diferencia de una aplicación normal, que a menudo no necesita un harness, una biblioteca que implementa funciones reutilizables debe ser llamada con los parámetros correctos y también en la secuencia correcta, para que el estado entre múltiples llamadas a funciones compartidas pueda ser invocado. Fuzzear aleatoriamente la biblioteca sin construir la máquina de estados es poco probable que tenga éxito y, por el contrario, creará muchos falsos positivos de fallos cuando no se respetan las dependencias de la biblioteca. Esto puede ocurrir cuando, por ejemplo, el fuzzer omite una comprobación del tamaño del búfer, lo que produce un desbordamiento de búfer espurio.

En este artículo se fuzzearán aplicaciones normales, pero debido a las dependencias de hardware del uso de sockets y del multiproceso, también necesitamos crear un harness para ellas. El harness se carga en el contexto del binario y puede llamar a funciones internas del programa objetivo, como se muestra en Código 10.

Corpus

El término "corpus" describe muestras de entrada válidas o casos de prueba y sirve como referencia fundamental para generar nuevos datos de entrada durante el proceso de fuzzing. En Código 10 esto sería, por ejemplo, una petición HTTP. Los fuzzers aprovechan este corpus para crear casos de prueba mutados o diversificados, lo que ayuda a la detección de vulnerabilidades de software mediante la exploración de varios escenarios de entrada.

Encontrar un objetivo potente

La parte que más tiempo consume del fuzzing de caja negra es encontrar una posible función vulnerable en el firmware. El primer paso es encontrar binarios interesantes que, por ejemplo, sean accesibles a través de la red, utilicen funciones inseguras o no tengan habilitadas características de seguridad como el stack canary, que es la protección contra desbordamiento de búfer. Nuestro artículo anterior ([iovt]) ya describía cómo extraer el firmware del router objetivo y cómo encontrar un binario potencialmente peligroso. Para ello se utilizó la herramienta EMBA [emba]. EMBA clasifica todos los binarios encontrados en el firmware según el número de funciones inseguras como strcpy, el acceso a la red y las protecciones de seguridad como el stack canary o el NX-Bit, que resultan interesantes a la hora de explotar un desbordamiento de búfer, lo cual se puede encontrar en Código 1.

```txt [+] STRCPY - top 10 results: 235 : libcmm.so : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | 77 : wscd : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | Networking | [snip] 28 : httpd : common linux file: yes | RELRO | No Canary | NX enabled | No Symbols | Networking | 27 : cli : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | ```

Código 1: resultado de EMBAs de usos no seguros de la función strcpy.

Debido a que el objetivo de este artículo es encontrar una vulnerabilidad de memoria que pueda explotarse a través de la red sin conocer las credenciales del administrador, la función vulnerable debe poder invocarse a través de la red e interactuar directamente con la entrada proporcionada por el usuario. Sin embargo, tener interacción de red no significa que el binario también sea directamente accesible a través de la red. Para averiguar qué binarios están a la escucha, podemos usar el shell root UART, que ya se estableció en [iovt].

```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```

Código 2: Uso de la shell root UART para ejecutar netstat

Ingeniería inversa del binario

El primer binario que parece prometedor es wscd. El binario tiene las llamadas strcpy más inseguras (excepto la librería libcmm.so) y la interacción de red, que en el caso de wscd significa que se conecta a un dispositivo UPnP y no escucha en un puerto específico. Tiene, como se mostrará más adelante, una función fácil de fuzzear, por lo que este binario fue seleccionado como ejemplo en este artículo para explicar el procedimiento general. Antes de hacer ingeniería inversa, podemos usar la shell root UART para averiguar si el binario está en ejecución y cómo se inició.

```txt $ ps PID USER VSZ STAT COMMAND 962 admin 1096 S wscd -i ra0 -m 1 -w /var/tmp/wsc_upnp/ 1018 admin 1080 S wscd_5G -i rai0 -m 1 -w /var/tmp/wsc_upnp_5G/ ```

Código 3: Uso del comando ps para mostrar todos los programas en ejecución.

Con ps no solo vemos que el binario se está ejecutando, sino también cuáles son los argumentos, que son importantes para verificar si una posible función se llama siquiera. El significado de estos argumentos se puede obtener de la ayuda de la CLI, que se muestra al invocar el binario sin ningún argumento.

```txt $ chroot root /qemu-mipsel-static /usr/bin/wscd Usage: wscd [-i infName] [-a ipaddress] [-p port] [-f descDoc] [-w webRootDir] -m UPnPOpMode -D [-d debugLevel] -h -i: Interface name this daemon will run wsc protocol(if not set, will use the default interface name - ra0) e.g.: ra0 -w: Filesystem path where descDoc and web files related to the device are stored e.g.: /etc/xml/ -m: UPnP system operation mode 1: Enable UPnP Device service(Support Enrolle or Proxy functions) 2: Enable UPnP Control Point service(Support Registratr function) 3: Enable both UPnP device service and Control Point services. [...] ```

Código 4: Opciones del binario wscd.

Como se muestra en el Código 4, wscd se inicia con el "Servicio de dispositivo UPnP habilitado", lo cual resulta prometedor. Después de verificar que el binario realmente se está ejecutando en el router, el binario puede analizarse a continuación usando Ghidra para buscar funciones sospechosas. Para el fuzzing, las funciones de parseo son especialmente interesantes porque suelen ser complejas y, con frecuencia, la entrada que se analiza tiene campos de longitud para los datos contenidos, como el paquete TCP contiene la longitud de la carga útil.

Figura 1: Uso de Ghidra para buscar funciones de parseo.

Otra ventaja de las funciones de parseo es que a menudo no interactúan con otras partes del código ni tienen interacción con el usuario a través de la red. Por lo tanto, la función de parseo puede llamarse directamente con la entrada sin modificar el binario ni sobrescribir otras funciones, de modo que la función puede someterse a fuzzing.

Antes de comenzar a fuzzear la función, se debe comprobar si la función se activa siquiera, porque la función solo es interesante cuando se la llama con una entrada controlada por el usuario. Para ello, se puede usar Ghidra para buscar referencias a la función objetivo. En el caso de la función parser_parse hay múltiples maneras. Como sabemos cómo se inicia el programa, las llamadas pueden reducirse a un solo árbol de llamadas de función, que se muestra en el Código 5.

```c main() if ((WscUPnPOpMode & 1) != 0) // Argument -m 1 WscUPnPDevStart() UpnpDownloadXmlDoc() -> my_http_Download() -> http_Download() if (http_MakeMessage()) http_RequestAndResponse() http_RecvMessage() ```

Código 5: Árbol de llamadas de la función parser_parse

Una vez que se encuentra una función objetivo, podemos crear una configuración de fuzzing para fuzzear la función, que se describe en la siguiente parte. Pero primero se presentan otras funciones potentes.

Otras funciones potencialmente vulnerables

Para este trabajo, se analizaron manualmente múltiples binarios potenciales en busca de funciones sospechosas. A continuación se presenta un breve resumen de otros posibles objetivos que se encontraron.

El binario httpd es el backend de la interfaz web de administración. El binario es accesible a través de la red en el puerto 80. Una función interesante en httpd es la función httpd_parser_main. Mientras se examinaba la implementación del parser con Ghidra, se pudieron identificar varias partes de código sospechosas. Una de las partes sospechosas es el análisis del Content-Type. A continuación, se puede encontrar una petición HTTP básica.```txt POST / HTTP/1.1\r\n Content-Type: multipart/form-data; boundary=X;\r\n Host: example.com\r\n \r\n \r\n DATA\r\n

root@kitploit:~
A continuación se muestra un fragmento de la función `httpd_parser_main` que analiza el `Content-Type` de la
petición http proporcionada por el usuario.


<div id="c6"></div>```c
// user_input_ptr points to
//  "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");

if (user_input_ptr == cursor) {
 cursor = strstr(user_input_ptr,"boundary=");
 user_input_ptr = cursor + 9;

 // user_input_ptr points now to "X;\r\nHost: example.com\r\n..."

 if (cursor != (char *)0x0) {

  do {
    while (cursor = user_input_ptr, *cursor == " ") {
      user_input_ptr = cursor + 1;
    }
    user_input_ptr = cursor + 1;
  } while (*cursor == "\t");

  // cursor points now to "X;\r\nHost: example.com\r\n..."

  // strchr returns a pointer to the first occurrence of ";" in the user request.
  // If ";" is not found, the function returns a null pointer.
  user_input_ptr = strchr(cursor, ";");
  if (user_input_ptr != (char *)0x0) {
    // The character ";" is replaced by an null byte to terminate the string
    *user_input_ptr = "\0";
    // cursor points now to "X\0\r\nHost: example.com\r\n..."
  }

  // DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
  strcpy(&DAT_00444050, cursor);
  // DAT_00444050 contains now "X"
 }
}

Código 6: Árbol de llamadas de la función parser_parse

La vulnerabilidad en este código es la llamada a la función strcpy y la suposición de que el Content-Type termina con un punto y coma. Debido a que strcpy copia el búfer hasta el siguiente byte nulo, y como se muestra en Código 6 el byte nulo solo se añade cuando se encuentra un punto y coma. Al eliminar el punto y coma, el siguiente byte nulo está al final del búfer de entrada, por ejemplo, al final de la solicitud HTTP. Así que la variable global DAT_00444050 puede desbordarse, sobrescribiendo datos más allá de la dirección 0x0044414f. La parte difícil no es solo encontrar una variable global interesante más allá de esta dirección que pueda sobrescribirse, sino también que no se pueden usar bytes nulos debido a strcpy. Pero cuando hay un error de este tipo, probablemente haya más por encontrar.

El binario tdpd es utilizado por la aplicación móvil y es accesible a través de UDP en la red local. tdpd tiene casi las mismas funciones que tmpd, las cuales en su mayoría simplemente nunca se llaman. La función principal solo escucha mensajes a través del puerto UDP y siempre responde con información básica sobre el router, como el nombre o el modelo. Apenas hay interacción con la entrada proporcionada por el usuario, por lo que no es interesante para fuzzing.

Otro par de binarios interesantes son upnpd y ushare. Ambos binarios manejan mensajes UPnP por lo que necesitan analizar XML. Debido a que se puede encontrar una cadena de copyright en el binario, se puede suponer que estos programas no fueron desarrollados por TP-Link.```sh $ strings usr/bin/ushare | grep "(C)" Benjamin Zores (C) 2005-2007, for GeeXboX Team.

root@kitploit:~
Ambos binarios cargan las bibliotecas compartidas `libupnp.so` y `libixml.so`, que tienen las mismas funciones que el proyecto de código abierto `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/). Dado que el enfoque de este documento es el fuzzing de caja negra, estos binarios se ignoran. Pero el fuzzing de caja gris de esta biblioteca podría tener potencial porque en 2021 se encontró una fuga de memoria en `libixml.so` [\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249).

El binario **tmpd** es el backend de la aplicación móvil. La parte interesante es que el router y la aplicación móvil se comunican mediante un protocolo binario personalizado. A continuación, se muestra un mensaje del cliente al servidor.

<div id="c7"></div>```txt
00000000  01 00 05 00 00 08 00 00  00 00 00 17 50 7b 6e fe  |............P{n.|
00000010  01 01 02 00 00 00 00 00                           |........        |

Code 7: Mensaje de la aplicación móvil al router.

Para entender el protocolo binario, se realizó ingeniería inversa del binario tmpd con Ghidra. Con esta información, el mensaje en Code 7 puede desglosarse de la siguiente manera:```txt 01 00 05 00 : Version 00 08 00 00 : Size (8 Bytes) 00 00 00 17 : Datatype 50 7b 6e fe : Checksum (CRC32) 01 01 : Options 02 00 : Function id 00 00 00 00 : Function parameters

root@kitploit:~
<p class="text-align: center">Código 8: Protocolo binario personalizado desglosado.</p>

Esto parece prometedor porque dichos protocolos binarios deben ser analizados. Pero la parte más sospechosa del
protocolo binario no es el campo de longitud, sino el uso del ID de función y los parámetros de función.


<figure id="f2">
  <p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
  <figcaption>
    <p style="text-align: center">Figura 2: Función invertida de tmpd que analiza el ID de la función y sus parámetros.</p>
  </figcaption>
</figure>

La [Figura 2](#f2) muestra una parte de la función de análisis descompilada del protocolo personalizado. En la línea 16,
se extrae el ID de la función, y la función correspondiente se llama entonces en la línea 29. El comportamiento
sospechoso es que la función se llama con parámetros extraídos sin ninguna comprobación del
búfer de entrada controlado por el usuario. Ahora podríamos intentar encontrar una función en la tabla de saltos mostrada en
la [Figura 3](#f3) donde esto pudiera ser peligroso, como cuando el parámetro se usa para indexar un búfer
o se interpreta como una cadena. En lugar de invertir y buscar manualmente entre las más de 100 funciones,
lo cual llevaría mucho tiempo, podemos usar un fuzzer que haga esto automáticamente.

<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">Figura 3: Función invertida de tmpd que analiza el ID de la función
y sus parámetros.</p></figcaption>
</figure>

Desafortunadamente, el binario `tmpd` solo es accesible localmente a través de la red, como se muestra en el [Código
2](#c2). Para conectarse a este binario, la aplicación primero se conecta al router mediante SSH en el modo
`direct-tcpip`, que simplemente reenvía los paquetes al proceso local. Y la conexión SSH está
protegida por las credenciales de administrador. Pero como se describe en
[\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf)
la conexión SSH puede verse comprometida fácilmente porque la aplicación nunca verifica la clave de host del servidor.
Al descartar todos los paquetes enrutados a internet, se puede engañar al administrador para que inicie sesión en
el router mientras se realiza un ataque man-in-the-middle para robar las credenciales.

## Fuzzing con AFL++ y QEMU

En esta sección, se desarrolla un harness dirigido a una de las funciones encontradas anteriormente. Después de
desarrollar el harness, se utiliza el fuzzer de última generación AFL++
[\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) para hacer fuzzing sobre la función objetivo. Debido a que
los binarios están compilados para la arquitectura `mipsel`, se utiliza el emulador QEMU para ejecutar el
binario. La configuración básica de fuzzing utilizada en este artículo está inspirada principalmente en la entrada de blog "Firmware
Fuzzing 101" de Adam Van Prooyen [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101).

### Entorno de fuzzing 

Para crear fácilmente un entorno de fuzzing reproducible, Docker es la mejor opción. Creamos un
Dockerfile que instala todas las herramientas necesarias, como un compilador cruzado para la arquitectura de CPU `mipsel`
o `gdb-multiarch`, que se puede utilizar para depurar el harness.

Además, AFLplusplus se descarga y se compila junto con QEMU, que se construye en una versión
con pequeños ajustes para permitir que binarios no instrumentados se ejecuten bajo afl-fuzz.```docker
FROM debian:latest

RUN apt update && apt install -y \
      curl \
      vim \
      gcc-mipsel-linux-gnu \
      openssh-server \
      qemu-user-static \
      gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static

# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh

RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh

WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]

Dockerfile que instala las herramientas necesarias.

La imagen se puede construir utilizando docker build.```sh docker build -t fuzz .

root@kitploit:~
Cuando la imagen está construida, se puede usar fácilmente con `docker run` que
luego inicia el contenedor.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz

El uso de la opción -d iniciará el contenedor en segundo plano. Con docker exec se pueden iniciar múltiples shells dentro del contenedor, lo que resulta útil para iniciar el ejecutable en una sesión usando QEMU y en la otra sesión gdb-multiarch.```sh docker exec -it fuzz /bin/bash

root@kitploit:~
### Sobrescribir la función main

En la sección anterior, se identificó un objetivo de fuzzing potente. El problema es que al ejecutar el binario, nunca se llegará a la llamada a la función porque la función `parser_parse` solo se invoca si se recibe un paquete TCP a través de un socket. Esto no solo sería malo para el rendimiento, sino también difícil de configurar. Por eso el punto de entrada del fuzzer debería estar en una ubicación distinta a la de la función main normal. Para ello, se puede utilizar la variable de entorno `LD_PRELOAD`, que permite inyectar un harness que tiene acceso a las funciones internas. Como describe la página de manual de `ld.so`, que es la responsable de enlazar las bibliotecas compartidas que necesita un ejecutable en tiempo de ejecución, `LD_PRELOAD` se puede usar "para sobrescribir selectivamente funciones en otros objetos compartidos
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)."

La función `__uClibc_main` es la más adecuada para este propósito. Para sobrescribir esta función, se debe crear un archivo C que contenga una función con el mismo nombre.```c
void __uClibc_main(void *main, int argc, char** argv) {
    // Harness code, e.g. call the function parser_append
    printf("My custom __uClibc_main was called!");
}

El archivo C puede entonces compilarse de forma cruzada a un objeto compartido en la arquitectura mipsel usando mipsel-linux-gnu-gcc. La opción -fPIC habilita "Código Independiente de la Posición", lo que significa que el código máquina no depende de estar ubicado en una dirección específica al usar direccionamiento relativo en lugar de absoluto.```txt $ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC

root@kitploit:~
La biblioteca compartida recién creada se puede cargar añadiendo la variable de entorno `LD_PRELOAD` al comando QEMU.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!

Con el comando chroot se pueden cambiar los directorios actual y raíz para el comando proporcionado. Esto es útil porque el ejecutable wscd abre otros archivos, como bibliotecas compartidas del firmware. Podemos ver este comportamiento añadiendo el argumento -strace a QEMU.```txt chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt 38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000 38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory) 38180 open("/parser_parse_hook.o",O_RDONLY) = 3 38180 fstat(3,0x7ffff920) = 0 38180 close(3) = 0 38180 munmap(0x7f7e6000,4096) = 0 38180 open("/lib/libpthread.so.0",O_RDONLY) = 3 38180 open("/lib/libc.so.0",O_RDONLY) = 3 [...]

root@kitploit:~
Como podemos ver, el ejecutable abre múltiples bibliotecas en la carpeta `/lib/` del firmware y no
en el host.

### Desarrollo y depuración del harness

Una vez creada la configuración, ahora podemos empezar a desarrollar un harness. Como se describe en la sección de antecedentes,
el harness es el conductor entre el fuzzer y la función objetivo. El harness carga la
entrada de fuzzing, que AFL++ almacena en un archivo. Con la ruta del archivo como parámetro, el harness luego
llama al objetivo de fuzzing; en este caso, sería `parser_append`. Las funciones pueden ser llamadas
usando la dirección.

<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
  // Verify that a filename is provided
  if (argc != 2) exit(1);

  // Create function pointer to the fuzz target
  int (*parser_request_init)(void *, int) = (void *) 0x00412564;
  int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;

  // Open the fuzz input file
  int fd = open(argv[1], O_RDONLY);
  char fuzz_buf[2048 + 1];
  int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
  if (fuzz_buf_len < 0) exit(1);
  fuzz_buf[fuzz_buf_len] = 0;

  // Call the target functions
  uint8_t parsed_data[220]; 
  parser_request_init(parsed_data, 8);
  int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
  printf("Response is %d\n", status);
  exit(0);
}

Código 10: Código de arnés con el objetivo de fuzzing `parser_append` en el binario wscd.

Como se muestra en Código 10, la función parser_parse no se llama directamente, sino mediante la función parser_append. Antes de llamar a esta función, se debe llamar a la función de inicialización parser_request_init, que inicializa la estructura de salida de la función parser_parse.

Mientras que en el caso de parser_parse el arnés es bastante fácil de configurar, otros objetivos requieren arneses más sofisticados como la función httpd_parser_main. Por ejemplo, antes de llamar al objetivo, se debe llamar a la función http_init_main, que termina en un SIGSEGV. Para averiguar dónde se causa esta falla de segmentación, es útil depurar el código con un depurador como gdb. Para hacerlo, QEMU se puede iniciar con la opción -g, que abre un gdb-server en el puerto proporcionado.```sh chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd corpus/httpd/simple.txt

root@kitploit:~
Debido a que el binario está en la arquitectura `mipsel`, se debe usar `gdb-multiarch`. Después de iniciar gdb, el siguiente script de inicio se puede cargar con gdb usando `sources <path to script>`.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc

Debido al chroot, el script primero cambió la ruta de prefijo absoluta para que cuando el binario cargue un objeto compartido, gdb encuentre el archivo. Luego se establece el archivo objetivo, porque el servidor gdb de QEMU no admite transferencia de archivos, por lo que gdb intenta cargar los archivos desde el disco en su lugar. Una vez que gdb está configurado, el script se conecta al servidor gdb con target remote y crea un punto de interrupción al inicio de la función objetivo. Con display, la salida simplemente mejora, de modo que al avanzar paso a paso, se mostrarán las siguientes cuatro líneas de ensamblador. Usando si podemos avanzar una instrucción, lo cual es útil cuando el harness tiene un fallo de segmentación usando el corpus predeterminado, que siempre debería funcionar. Como se muestra en Code 11, el binario tiene un fallo de segmentación en la función fprintf.

```sh (gdb) si 0x004059b0 in http_parser_makeHeader () 1: x/4i \$pc => 0x4059b0 : jalr t9 0x4059b4 : addiu a1,a1,16248 0x4059b8 : li v0,200 0x4059bc : lw gp,16(sp) (gdb) ni 0x7f56a8ac in fprintf () from /share/root/lib/libc.so.0 1: x/4i \$pc => 0x7f56a8ac \: bal 0x7f56db80 0x7f56a8b0 \: nop 0x7f56a8b4 \: lw ra,36(sp) 0x7f56a8b8 \: jr ra 0x7f56a8bc \: addiu sp,sp,40 (gdb) n Single stepping until exit from function fprintf, which has no line number information.

Program received signal SIGSEGV, Segmentation fault.

root@kitploit:~
<p style="text-align: center">Código 11: Fallo de segmentación en printf.</p>

Para investigar el error, se puede utilizar Ghidra para averiguar con qué parámetros se llama a la
función.```c
fprintf(
 *(FILE **)(iVar1 + 0x101c),
 "HTTP/1.1 %d %s\r\n",
 *(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
 (&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);

El SIGSEGV probablemente se debe al hecho de que el primer parámetro no es un descriptor de archivo sino un puntero nulo. Donde iVar1 es solo una referencia a la entrada de la función httpd_parser_main. Esto significa que la entrada de fuzzing debe tener un descriptor de archivo en la posición 0x101c. Por lo tanto, la entrada debe ajustarse al siguiente struct.```c typedef struct { int _a; // 4 Bytes int _b; // 4 Bytes int socket; // 4 Bytes int ip; // 4 Bytes int mac; // 4 Bytes unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes FILE * fd_out; // expected to be a valid file descriptor } HttpMainT;

root@kitploit:~
Because `fd_out` must just be a valid file descriptor pointer, it can easily be set to `stdout`.
Executing the `httpd_parser_main` again will now produce a valid HTTP output.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
    /usr/bin/httpd /httpd_corpus.txt

bind: No such file or directory
[ dm_shmInit ] 086:  shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close

<!DOCTYPE html>
[...]

El harness ahora funciona y puede usarse para fuzzear la función con AFL++, lo cual se explicará en la siguiente sección.

Generar datos del corpus

Como se mencionó en los antecedentes, un corpus semilla describe muestras de entrada válidas, que sirve como referencia fundamental para generar nuevos datos de entrada durante el proceso de fuzzing.

Estas entradas normalmente se eligen para representar diferentes aspectos de los programas objetivo. El corpus semilla es utilizado por un fuzzer para generar casos de prueba mutados o evolucionados que luego se ejecutan contra el software objetivo para descubrir errores, fallos u otros problemas. Este corpus desempeña un papel importante al dirigir el fuzzer hacia áreas relevantes del programa y aumentar la probabilidad de detectar vulnerabilidades o comportamientos inesperados. Al proporcionar un conjunto diverso y representativo de entradas iniciales, el corpus semilla ayuda al fuzzer a explorar diferentes rutas en el objetivo más rápidamente y, por lo tanto, aumenta la cobertura.

Cuando se trata de funciones que parsean datos de red, estas entradas se pueden crear usando Wireshark para registrar diferentes paquetes.

Para la función, httpd_parse_main se crearon cuatro corpus diferentes. Cada uno apuntaba a diferentes rutas en el binario. Un ejemplo es la solicitud de inicio de sesión, que contiene el nombre de usuario y la contraseña. Para este corpus, el harness tuvo que modificarse porque TP-Link utiliza criptografía (débil) para "proteger" la contraseña. Para ello, la contraseña se cifra en el navegador mediante AES y luego se descifra en el backend. De este modo, la contraseña se genera en el navegador y luego se cifra con RSA. Luego, los datos cifrados se firman. Debido a que un fuzzer no puede crear una firma ni cifrar datos, algunas funciones fueron sobrescritas y ahora solo decodifican los datos desde base64. Para ello, los datos se extrajeron primero en texto plano del navegador usando el depurador que se muestra en la Figura 4.

Figura 4: Extrayendo los datos antes del cifrado.

En el objetivo, la función rsa_tmp_decrypt_bypart se sobrescribió para reemplazar la lógica de descifrado de los datos por la simple decodificación desde base64.```c // Replacing the logic with b64_decode int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00; b64_decode(output, 0x1000, input, input_len); int * seqnumber = (int *) 0x00444db0; *seqnumber = 0x3ac28e29-input_len+12; return 0; // says it was okay }

root@kitploit:~
<p style="text-align: center">Código 12: La función rsa_tmp_decrypt_bypart ahora solo decodifica base64
en lugar de descifrar los datos.</p>

Mientras se ejecuta el corpus, la función objetivo siempre devuelve un documento HTML con el error "408
Request Timeout". Usando Ghidra y GDB se pudo identificar el problema. El error siempre ocurre
después de la llamada a la función `http_stream_fgets`. La línea problemática era la comprobación del
carácter de salto de línea `\n`.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {

Esta condición garantiza que después de cada salto de línea deba seguir un retorno de carro. Tras añadir el retorno de carro, todos los corpora creados funcionaron.

Fuzzear el objetivo

En la última sección, desarrollamos múltiples harnesses y los ejecutamos usando QEMU. En esta sección, QEMU es reemplazado por AFL++, que recibe los corpora generados como entrada semilla para fuzzear la función objetivo. En la sección "Entorno de fuzzing" se creó una imagen de docker que ya descarga AFL++ desde GitHub y luego usa un script proporcionado por AFL++ para construir una versión parcheada de QEMU. Así, AFL++ puede ahora iniciarse con el siguiente comando que recibe diferentes parámetros, como -Q, que le indica a AFL++ que use la versión parcheada de QEMU.```sh QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@

root@kitploit:~
<p style="text-align: center">Código 13: Fuzzing del binario <code>httpd</code> usando el harness
y <code>afl-fuzz</code>.</p>

A diferencia de antes, el comando `chroot` ya no es necesario y es reemplazado por la variable
`QEMU_LD_PREFIX`. Que le indica a QEMU dónde buscar los objetos compartidos. Además, la variable `LD_PRELOAD`
es reemplazada por la versión específica de AFL `AFL_PRELOAD`. El último argumento en el comando son
los dos caracteres `@`. Serán reemplazados por AFL++ con una ruta de archivo que contiene la entrada de
fuzzing. Al iniciarse, AFL++ muestra el progreso usando la interfaz de terminal mostrada en la [Figura 5](#f5).

<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">Figura 5: La pantalla de estado de AFL++.</p></figcaption>
</figure>

La pantalla de estado de `AFL++` proporciona información esencial sobre el proceso de fuzzing actual. La documentación de
`AFL++` ofrece una buena visión general de los términos utilizados en la pantalla de estado
[\[afl-screen\]](https://aflplus.plus/docs/status_screen/).  Al depurar el corpus con las
siguientes variables de entorno, la interfaz puede desactivarse y con `AFL_DEBUG` se habilita un registro detallado
que muestra la entrada actual del fuzzer y el `stdout` del programa objetivo.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI

Como se muestra en Figure 5, el fuzzing de un binario puede llevar bastante tiempo. Según la documentación, "debe esperarse que se ejecute durante días o semanas" y "a algunos trabajos se les permitirá ejecutarse durante meses." Para mejorar el tiempo necesario, la velocidad de ejecución debería ser superior a 100 ejecuciones/seg. Cuando, por ejemplo, se aplicó fuzzing al objetivo httpd_main_parser, la velocidad de ejecución era al principio de alrededor de 30/seg. Para mejorar la velocidad, se buscaron en el binario objetivo funciones sospechosas, que probablemente sean la causa de la ralentización. Una de las funciones sospechosas era rsa_gdpr_generate_key porque se sabe que generar una clave RSA es lento. Tras sobrescribir la función, la velocidad mejoró a 600 ejecuciones por segundo.

Uno de los indicadores que ayuda a determinar cuándo detener el fuzzing es el contador de ciclos. AFL++ resaltará el número en verde cuando "el fuzzer no ha estado viendo ninguna acción durante un tiempo," lo que ayuda a tomar la decisión de detener el fuzzer.

Pero el número más interesante es probablemente "total crashes". Este muestra cuándo el programa falla debido a la entrada de fuzzing actual y probablemente sea un bug relacionado con la memoria. Para verificar que es un bug real, se puede usar gdb nuevamente para encontrar la posición del bug.

Conclusión

El fuzzing puede ser la forma más efectiva de encontrar vulnerabilidades de seguridad. En este trabajo de curso, se aplicó fuzzing a tres funciones diferentes, pero no se encontró ninguna. Mientras que la configuración de fuzzing de caja negra en sí no es tan compleja ni requiere mucho tiempo, encontrar un objetivo potente y desarrollar un harness funcional sí lo es. La mayor parte del tiempo, el harness tiene que ser depurado, y luego la lógica subyacente en el binario debe ser invertida, lo que nuevamente consume mucho tiempo.

Descargar herramienta