Error de Heartbleed explicado

Como habrás escuchado, hay un nuevo error de OpenSSL, y es malo. Este no es uno de esos errores o hacks que escuchas en las noticias y con seguridad...

Como habrás escuchado, hay un nuevo error de OpenSSL, y es malo. Este no es uno de esos errores o hacks que escuchas en las noticias e ignoras con seguridad como siempre lo haces. Afecta a alrededor del 66% de todos los servidores de Internet, lo que probablemente incluye un sitio web que frecuentas o en el que tienes información confidencial.

Cómo funciona

Entonces, ¿cuál es el error exactamente? Para describir el error, necesitamos comprender solo algunas partes de la implementación de OpenSSL. La siguiente estructura contiene el registro SSL:

typedef struct ssl3_record_st
    {
/*r */  int type;               /* type of record */
/*rw*/  unsigned int length;    /* How many bytes available */
/*r */  unsigned int off;       /* read/write offset into 'buf' */
/*rw*/  unsigned char *data;    /* pointer to the record data */
/*rw*/  unsigned char *input;   /* where the decode bytes are */
/*r */  unsigned char *comp;    /* only used with decompression - malloc()ed */
/*r */  unsigned long epoch;    /* epoch number, needed by DTLS1 */
/*r */  PQ_64BIT seq_num;       /* sequence number, needed by DTLS1 */
/*rw*/  unsigned int orig_len;  /* How many bytes were available before padding
                   was removed? This is used to implement the
                   MAC check in constant time for CBC records.
                 */
    } SSL3_RECORD;

Esta estructura se usa en el método dtls1_process_heartbeat(SSL *s) en el archivo d1_both.c para manejar los latidos que mantienen activa la conexión SSL. Este método accede a los datos del registro en la estructura, como se muestra a continuación:

int
dtls1_process_heartbeat(SSL *s)
    {
    unsigned char *p = &s->s3->rrec.data[0], *pl;
    unsigned short hbtype;
    unsigned int payload;
    unsigned int padding = 16; /* Use minimum padding */
    
    /* Read type and payload length first */
    hbtype = *p++;
    n2s(p, payload);
    pl = p;

El primer byte del registro SSL es el tipo de latido recibido, y la macro n2s(c, l) solo toma dos bytes de p y los coloca en carga útil. Estos bytes son la longitud de la carga útil en el latido del corazón. Lo importante a tener en cuenta aquí es que la longitud en el registro SSL no se comprueba/verifica. La variable pl ahora apunta a los datos del latido, que fue enviado por el cliente.

El hecho de que no se haya verificado la longitud del registro SSL es un gran problema, ya que más adelante en la misma función, la memoria se asigna utilizando la misma longitud de carga útil, que se muestra en el siguiente código:

unsigned char *buffer, *bp;
int r;

/* Allocate memory for the response, size is 1 byte
 * message type, plus 2 bytes payload length, plus
 * payload, plus padding
 */
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;

Dado que a payload se le asignaron los primeros dos bytes de los datos de latidos, que los proporciona el cliente, eso significa que puede tener un valor máximo de 65535 (o 64k, cuando se refiere a bytes). Básicamente, estamos asignando la cantidad de memoria que el cliente quiere (hasta 64k), pero no es gran cosa, ¿verdad? La asignación de cantidades de datos definidas por el usuario no es muy dañina en sí misma (aunque definitivamente no es algo bueno), pero aquí es dañina ya que más adelante en la misma función copiamos esa cantidad de memoria en nuestro búfer:

s2n(payload, bp);
memcpy(bp, pl, payload);

Aquí, s2n(c, l) mueve el tamaño de la carga útil al búfer bp (al contrario de lo que hizo n2s), y memcpy copia payload el número de bytes de pl a bp. Esto tampoco parece gran cosa hasta que consideras el tamaño real de pl. ¿Qué sucede si pl tiene solo unos pocos bytes de tamaño? ¿O incluso solo un byte? Un atacante podría habernos dicho que la carga útil era de 64 kb, cuando solo era de 1 byte. Entonces, ¿de dónde se copiaría el resto de la memoria si pl es mucho más pequeño de lo esperado? Memoria circundante, que puede haberse liberado recientemente del mismo proceso.

Hay potencialmente una gran cantidad de datos confidenciales en la memoria circundante, como nombres de usuario, contraseñas e incluso claves privadas. Esto tiene a mucha gente preocupada, ya que un atacante podría enviar continuamente latidos maliciosos a un servidor y seguir recuperando 64 kb de su memoria cada vez. Luego, una simple búsqueda de palabras clave como 'contraseña' o 'tarjeta de crédito' y listo, el atacante robó sus datos sin que nadie lo supiera. Mark Loman, investigador de seguridad, ya ha mostrado este ataque es posible con Yahoo Mail.

Sin embargo, por suerte para nosotros, lo más probable es que la clave privada sea segura, como señala Neel Mehta (uno de los investigadores que descubrió el error):

Los patrones de asignación de montones hacen que la exposición de la clave privada sea poco probable para #heartbleed [#no entrar en pánico](https://twitter.com/search? q=%23dontpanic&src=hash).

— Neel Mehta (@neelmehta) 8 de abril de 2014

Sean Cassidy proporciona información muy útil sobre estos métodos de asignación de montones:

Hay dos formas en que la memoria se asigna dinámicamente con malloc (at
menos en Linux): usando sbrk(2) y usando mmap(2). Si la memoria es
asignado con sbrk, luego usa las viejas reglas de crecimiento del montón y
limita lo que se puede encontrar con esto, aunque múltiples solicitudes
(especialmente simultáneamente) aún podría encontrar algunas cosas divertidas1.

Las asignaciones para bp no importan en absoluto, en realidad. La asignación
para pl, sin embargo, importa mucho. Es casi seguro que está asignado con sbrk debido al umbral de mmap en malloc. Sin embargo,
cosas interesantes (como documentos o información de usuario), es muy probable que
asignado con mmap y puede ser accesible desde pl. Múltiple
las solicitudes simultáneas también pondrán a disposición algunos datos interesantes.

Cómo se solucionó

Dado que este error es tan simple de ejecutar, eso significa que también es fácil de solucionar. OpenSSL ya ha publicado una actualización, la versión 1.0.1g. Contiene la siguiente corrección:

/* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
    return 0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;

Como puede ver, las diferencias son las comprobaciones de longitud, que en primer lugar garantiza que el latido no esté vacío (longitud 0) y, en segundo lugar, comprueba que la carga útil del latido coincida con la longitud proporcionada por el usuario. Es así de simple. Cuatro líneas de código ahora protegen más del 66% de los servidores del catastrófico error Heartbleed.

Cómo está parcheado

¿Ejecutas un servidor público que usa SSL? Entonces definitivamente deberías parchearlo. Solo puedo suponer que alguien ya ha configurado un marcador de guerra que verifica todos los servidores que puede para detectar este error y luego explota aquellos que lo tienen. Entonces, si está ejecutando Ubuntu, ejecute lo siguiente:

sudo apt-get update
sudo apt-get install -y libssl1.0.0 openssl
 
# Verify that the Build Date is April 7th 2014 or later
openssl version -a

Luego, asegúrese de reiniciar todos y cada uno de los servicios que usan OpenSSL. Si no está seguro de que el parche funcionó, simplemente use esta herramienta para averiguarlo.