Heartbleed

Onlangs brak een kleine rel uit toen een vulnerability genaamd "Heartbleed" [CVE-2014-0160] werd aangekondigd . Dit bleek een kwetsbaarheid te zijn in de befaamde cryptografische OpenSSL library, die vanwege z'n populariteit en bruikbaarheid in veel andere systemen gebruikt werd, waaronder Apache en Nginx, welke samen het leeuwedeel van de op internet gebruikte webservers vormen.

Introductie

In februari 2012 doen een aantal mensen van de universiteit van Munchen een voorstel voor een uitbreiding van het bestaande TLS en DTLS protocol, de zogenaamde "Heartbeat" extensie RFC 6520, waarmee gecontroleerd kan worden of een met OpenSSL beveiligde service nog beschikbaar is op een server, .

Details zijn hier te vinden, samengevat gaat het om kleine pakketjes data:

The format of the Heartbeat Hello Extension is defined by:

   enum {
      peer_allowed_to_send(1),
      peer_not_allowed_to_send(2),
      (255)
   } HeartbeatMode;

   struct {
      HeartbeatMode mode;
   } HeartbeatExtension;

 enum {
      heartbeat_request(1),
      heartbeat_response(2),
      (255)
   } HeartbeatMessageType;


   struct {
      HeartbeatMessageType type;
      uint16 payload_length;
      opaque payload[HeartbeatMessage.payload_length];
      opaque padding[padding_length];
   } HeartbeatMessage;

   The total length of a HeartbeatMessage MUST NOT exceed 2^14 or
   max_fragment_length when negotiated as defined in [RFC6066].

Interessant is om te zien, dat hoewel er nadrukkelijk wordt gesproken dat de grootte van HeartbeatMessage moet voldoen aan bepaalde voorwaarden, er is gekozen voor een variabele grootte. Ook zijn variabele groottes in een dergelijke message 'lastig'. De vraag is wat het toevoegt. Een fixed length message had volstaan! Hier geldt zeker het KISS principe: houd het simpel!

Implementatie

Eerder had een van de bijbehorende auteurs al een commit gedaan op git.openssl.org, in o.a. de file d1_both.c zien we de beruchte code staan, waar het fout gaat bij het coderen van een HeartbeatMessage.

+int
+dtls1_process_heartbeat(SSL *s)
+       {
+       unsigned char *p = &s->s3->rrec.data[0], *pl;

Bovenstaande declaratie maakt een pointer p aan naar de ontvangen data

+       unsigned short hbtype;
+       unsigned int payload;
+       unsigned int padding = 16; /* Use minimum padding */
+
+       /* Read type and payload length first */
+       hbtype = *p++;

De datadrager .data[0] wordt uitgelezen dmv een pointer die over de data wordt geschoven. Na het type te hebben gelezen wordt p naar de volgende byte verwezen.

+       n2s(p, payload);

n2s zal een functie/macro zijn die de volgende 2 bytes uitleest en converteert naar een integer genaamd payload, rekening houdend met big/little endian, en tevens p 2 bytes ophoogt. payload bevat hier al de foutieve, te grote waarde.

+       pl = p;

Vervolgens wijst nu pl naar het begin van de terug te sturen tekenreeks, die normaal lengte payload zou moeten hebben...

+       if (s->msg_callback)
+               s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
+                       &s->s3->rrec.data[0], s->s3->rrec.length,
+                       s, s->msg_callback_arg);
+

+if (hbtype == TLS1_HB_REQUEST)
+               {
+               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;

 

De buffer voor de data die teruggestuurd gaat worden wordt hier gealloceerd. Let op de alternatieve versie van malloc die hier wordt gebruikt, hiervoor wordt het openSSL team bekritiseerd. Door een eigen memory manager te willen gebruiken wil het team een betere performance bewerkstelligen. Maar dit is zeker niet de oorzaak van het probleem!

+               /* Enter response type, length and copy payload */
+               *bp++ = TLS1_HB_RESPONSE;
+               s2n(payload, bp);
+               memcpy(bp, pl, payload);

Hier geschiedt het eigenlijke kwaad: buffer bp (die groot genoeg is) wordt gevuld met een grootte van payload, beginnend bij het adres van pl (oorspronkelijk terug te sturen tekenreeks). Maar omdat de lengte van pl kleiner is dan payload, wordt het geheugen 'achter' pl gelezen, waar dus klaarblijkelijk gevoelige informatie in kan zitten.

+               /* Random padding */
+               RAND_pseudo_bytes(p, padding);
+
+               r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
+
+               if (r >= 0 && s->msg_callback)
+                       s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
+                               buffer, 3 + payload + padding,
+                               s, s->msg_callback_arg);
+
+               OPENSSL_free(buffer);
+
+               if (r < 0)
+                       return r;
+               }

Commentaar

Het is een vreemde zaak dat een library die inmiddels zo vaak gebruikt wordt en klaarblijkelijk zo'n grote impact heeft op Internet, wordt onderhouden door een betrekkelijk klein team. Het betreft honderdduizenden regels code. Code reviewing kan leuk zijn, maar de betreffende commit werd gedaan enkele uren voor Nieuwjaarsavond (!) 2012. Het toont de betrokkenheid aan dat men zelfs dan werkt aan OpenSSL, maar de human factor is dan ook de weakest link.

Oplossingen

Probleem is dat er geen input/protocol-validatie gepleegd wordt. De data ontvangen door de server [OF CLIENT!!] wordt niet gevalideerd, er wordt uitgegaan van correctheid. Mogelijkerwijs hebben andere programmeertalen hier geen of minder last van  [e.g.: in Delphi kun je dynamische bounds checking aanzetten, maar dat had ook in dit specifieke geval niet hoeven te helpen.], maar dat is feitelijk bijzaak.

De meegegeven lengte van de payload zou nooit langer mogen zijn dan de tcp fragment size, zoals de auteur in z'n eigen aanbevelingen schrijft, maar hier controleert hij niet op...

Wat ook 'lelijk' en 'vies' programmeren is, is dat een protocol/extensie tussen neus en lippen door, losjes geïmplementeerd wordt in een aspecifieke functie. Bij aspect georiënteerd programmeren zou je dergelijke items scheiden. Er ligt dan meer nadruk op de items zelf: controleren of het protocol gehandhaafd wordt en de afhandeling hiervan.

Implicaties

Iedere server, maar ook client (!) die de kwetsbare variant van OpenSSL gebruikt loopt risico. Dus afgezien van een kwetsbare Apache server ben je ook als Android gebruiker (4.1.1) mogelijk kwetsbaar, als je gebruikt maakt van die specifieke OpenSSL libraries.

Specifieke in geheugen opgeslagen items zoals de private key van de webserver kan worden verkregen. In dat geval zijn nieuwe certificaten nodig. Met een private key kan zelfs voorheen verkregen data gedecodeerd worden, tenzij Perfect Forward Secrecy gebruikt is. Ook is gebleken dat ongecodeerde wachtwoorden in het geheugen uitgelezen konden worden (Yahoo). Bij secure coding dien je ongecodeerde wachtwoorden te overschrijven, dat gebeurde hier duidelijk niet!

Maar andersom zou het ook kunnen. Als een server wordt geprogrammeerd gebruik te maken van de Heartbleed vulnerability, dan kunnen ook clients uitgelezen worden. Nu is dit minder eenvoudig omdat iedereen zich als client voor kan doen maar niet iedereen als server, maar de implicaties hiervan kunnen ook enorm zijn!