Squid: Denial-of-Service-Attacke durch DNS-Daten

Eine Schwachstelle im Caching-Proxy Squid hat zur Folge, dass ein entfernter Angreifer mit speziellen DNS-Einträgen Denial-of-Service-Attacken durchführen kann, die zum Absturz von Squid führen.

Die erste Meldung zu dieser Sicherheitslücke gab es schon am 6.6.2011, als ein Benutzer der Squid Version 3.1.12.1-20110523 einen Absturz mit folgendem Traceback auf seinem Squid-Cache bekannt gab:

0x0000003b87230265 in raise () from /lib64/libc.so.6
0x0000003b87231d10 in abort () from /lib64/libc.so.6
0x00000000005445f5 in death (sig=<value optimized out>) at tools.cc:398 <signal handler called>
0x0000003b8727288e in free () from /lib64/libc.so.6
0x00000000005a4517 in rfc1035RRDestroy (rr=0x2732f988, n=1) at rfc1035.c:488
0x00000000005a4571 in rfc1035MessageDestroy (msg=0x7fff26d4a440) at rfc1035.c:534
0x00000000004be7d2 in idnsGrokReply (buf=<value optimized out>, sz=<value optimized out>) at dns_internal.cc:1144
0x00000000004bfece in idnsRead (fd=11, data=<value optimized out>) at dns_internal.cc:1229
0x00000000004ae1a5 in comm_select (msec=<value optimized out>) at comm_epoll.cc:307
0x000000000055c24d in CommSelectEngine::checkEvents (this=<value optimized out>, timeout=321) at comm.cc:2687
0x00000000004c440e in EventLoop::checkEngine (this=0x7fff26d4a650, engine=0x7fff26d4a6b0, primary=160) at EventLoop.cc:50
0x00000000004c4563 in EventLoop::runOnce (this=0x7fff26d4a650) at EventLoop.cc:124
0x00000000004c4658 in EventLoop::run (this=0x7fff26d4a650) at EventLoop.cc:94
0x000000000050adef in SquidMain (argc=<value optimized out>, argv=0x7fff26d4a7d8) at main.cc:1416
0x000000000050b3f6 in SquidMainSafe (argc=841888563, argv=0x2) at main.cc:1176
main (argc=841888563, argv=0x2) at main.cc:1168

Einen solchen Traceback liest man von unten nach oben: Er beginnt in der “main()”-Funktion und endet schließlich in der GNU C-Bibliothek (“glibc”). Kurz zuvor tauchen allerdings Funktionen wie “idnsRead”, “idnsGrokReply”, und “rfc1035″… auf. RFC 1035 ist eines der Dokumente, die das Domain Name System (DNS) definieren, und “idns”… sind Funktionen aus der Datei “dns_internal.cc” von Squid, die für das Verarbeiten von DNS-Anfragen und -Antworten zuständig sind. Dies deutet darauf hin, dass es ein Problem beim Verarbeiten von DNS-Daten gab, das schließlich zu dem Absturz führte.

Weiter erscheint der Fehler im Zusammenhang mit einem “free()”-Aufruf, was darauf hindeutet, dass versehentlich versucht wurde, einen ungültigen Speicherbereich mit “free()” freizugeben. Offenbar kommt dieser Speicherbereich beim Verarbeiten von DNS-Nachrichten oder DNS Resource Records (DNS-RR) vor, denn vor dem “free()” werden “rfc1035MessageDestroy()” und “rfc1035RRDestroy()” aufgerufen, die beide DNS-Nachrichten-Speicher oder aber allozierten Speicher für DNS-RRs freigeben.

Resource Records

Resource Records sind die grundlegende Informationseinheit im DNS. Sie werden in ASCII-Darstellung in Zonendateien gespeichert und bei DNS-Abfragen via
Netzwerk verschickt. Das allgemeine Format dieser Einträge ist immer folgende:

name [ttl] [class] type rdata

Dabei sind Time-to-live “ttl” (in Sekunden) und “class” optional. Zentral für RR-“type” sind A und AAAA zum Abspeichern von IPv4- und IPv6-Adressen. Ein DNS-Lookup sucht nach diesen Einträgen, um einen Domainnamen, der in “name” abgelegt ist, in eine IP-Adresse, die in “rdata” abgelegt ist, umzuwandeln.

Neben der direkten Namens-zu-IP-Übersetzung erlaubt es das DNS auch, Aliase zu definieren, wobei hier der “type” CNAME zum Einsatz kommt. In diesem Fall sind sowohl “name” als auch “rdata” Domainnamen. Am Ende einer solchen Alias-Kette sollte natürlich ein A- bzw. AAAA-RR stehen, um den Namen letztendlich in eine IP-Adresse aufzulösen.

Zurück zu dem Fehler: Trotz des oben wiedergegebenen Tracebacks war die eigentlich Ursache des Squid-Crashes nicht so einfach zu identifizieren, da der betroffene Cache in ein Produktionssystem mit sehr vielen Anfragen integriert war. Trotzdem konnte den Log-Dateien folgende Information entlockt werden:

2011/06/20 04:55:30| ipcacheParse: No Address records in response to 'l15.sphotos.l3.fbcdn.net'
2011/06/20 04:55:30| ipcacheParse: No Address records in response to 'l15.sphotos.l3.fbcdn.net'
FATAL: Received Segment Violation...dying.

Der Segmentation Fault-Fehler wird mit “FATAL” eingeleitet, aber die zwei Zeilen zuvor zeigen, dass es beim Verarbeiten des DNS-Eintrags für “l15.sphotos.l3.fbcdn.net” Probleme gab. Anscheinend konnte diese Adresse nicht in eine IP-Adresse aufgelöst werden, da sich Squid darüber beschwert, dass kein A-Record für “l15.sphotos.l3.fbcdn.net” gefunden werden konnte. Dieser Fehler wird in der Datei “ipcache.cc” generiert, falls etwas mit dem A- oder AAAA-Lookup schief lief:

static int ipcacheParse(ipcache_entry *i, rfc1035_rr * answers, int nr, const char *error_message)
{ int na = 0; ... if (Ip::EnableIpv6 && answers[k].type == RFC1035_TYPE_AAAA) { if (answers[k].rdlength != sizeof(struct in6_addr)) { debugs(14, 1, "ipcacheParse: Invalid IPv6 address in response to '" << name << "'"); continue; } na++; IpcacheStats.rr_aaaa++; continue; } if (answers[k].type == RFC1035_TYPE_A) { if (answers[k].rdlength != sizeof(struct in_addr)) { debugs(14, 1, "ipcacheParse: Invalid IPv4 address in response to '" << name << "'"); continue; } na++; IpcacheStats.rr_a++; continue; }
... if (na == 0) { debugs(14, 1, "ipcacheParse: No Address records in response to '" << name << "'"); i->error_message = xstrdup("No Address records"); if (cname_found) IpcacheStats.cname_only++; return 0; }

Der “na”-Integer dient hier als Flag und ist nur dann 0, wenn sowohl A- als auch AAAA-Records nicht korrekt verarbeitet werden konnten. In diesem Fall beklagt Squid, dass es keinen Address Record gefunden hat, es gab also keinen A- oder AAAA-Eintrag.

An dieser Situation ist zunächst einmal nichts problematisch, und eigentlich sollte Squid sie meistern können. Doch wie sich herausstellte, gibt es eine bestimmte Art von DNS-Einträgen, die Squid hier in die Knie zwingen. Und zwar tritt dies genau dann auf, wenn vor dem Nichtvorhandensein von A- oder AAAA-Eintrag DNS mehrmals via CNAME Alias-Verweise verwendet hat. Der Crash ließ sich durch solche DNS-Einträge reproduzieren, bei denen eine erste Abfrage via CNAME auf einen Alias verweist, und der zweite Eintrag dann wieder ein CNAME-Alias ist, der diesmal auf einen Namen verweist, dem kein A- oder AAAA-RR zugewiesen ist. Genau dann kommt das Speichermanagement von Squid durcheinander und stürzt ab.

Folgendes DNS-Beispiel zeigt eine solche fatale Situation:

1. Anfrage:
Hier wird ein DNS-Lookup nach “l15.sphotos.l3.fbcdn.net” durchgeführt, und die Antwort verweist via CNAME auf den Alias “l15.sphotos.l3.fbcdn.net.c.footprint.net”.

;; QUESTION SECTION:
;l15.sphotos.l3.fbcdn.net. IN A
;; ANSWER SECTION:
l15.sphotos.l3.fbcdn.net. 7032 IN CNAME l15.sphotos.l3.fbcdn.net.c.footprint.net.
l15.sphotos.l3.fbcdn.net.c.footprint.net. 62 IN CNAME s1048.dnssecure.nsatc.net.

2. Anfrage:
Die darauffolgende Anfrage versucht, “l15.sphotos.l3.fbcdn.net.c.footprint.net” aufzulösen, wobei DNS wieder einen CNAME zurückliefert, aber diesmal auf “s1048.dnssecure.nsatc.net”.

;; QUESTION SECTION:
;l15.sphotos.l3.fbcdn.net.c.footprint.net. IN A
;; ANSWER SECTION:
l15.sphotos.l3.fbcdn.net.c.footprint.net. 57 IN CNAME s1048.dnssecure.nsatc.net.

3. Anfrage:
Daraufhin wird nun versucht, “s1048.dnssecure.nsatc.net” endlich in eine IP-Adresse aufzulösen, aber hierfür findet sich kein A- oder AAAA-RR.

;; QUESTION SECTION:
;s1048.dnssecure.nsatc.net. IN A

Würde ein Benutzer in diesem DNS-Setup nun “l15.sphotos.l3.fbcdn.net” in seinem Browser via Squid-Proxy abfragen, so würde das Squid zum Absturz bringen. Und das geschah in obigem Beispiel.

Um den Programmierfehler zu finden, muss man sich die Squid-Routinen zum Verarbeiten von DNS-Antworten genauer ansehen. Diese sind in der Datei “dns_internal.cc” untergebracht, wobei hier die Funktion “idnsGrokReply()” eine zentrale Rolle spielt. Da es sich hier um RFC-1035-konforme Nachrichten handelt, wird dort zunächst die Funktion “rfc1035MessageUnpack()” aufgerufen, die eingehende DNS-Daten in eine “rfc1035_message”-Struktur packt:

typedef struct _rfc1035_message rfc1035_message;
struct _rfc1035_message { unsigned short id; unsigned int qr:1; unsigned int opcode:4; unsigned int aa:1; unsigned int tc:1; unsigned int rd:1; unsigned int ra:1; unsigned int rcode:4; unsigned short qdcount; unsigned short ancount; unsigned short nscount; unsigned short arcount; rfc1035_query *query; rfc1035_rr *answer;
};

Das Feld “ancount” speichert dabei die Zahl der in der Nachricht enthaltenen Resource Records. Die RRs selbst werden innerhalb der Struktur “rfc1035_message” in einer “rfc1035_rr”-Struktur untergebracht:

typedef struct _rfc1035_rr rfc1035_rr;
struct _rfc1035_rr { char name[RFC1035_MAXHOSTNAMESZ]; unsigned short type; unsigned short _class; unsigned int ttl; unsigned short rdlength; char *rdata;
};

Diese Struktur speichert allgemeine RRs, wobei “type” für A-Einträge beispielsweise 1 und für CNAME-Einträge 5 ist. Die eigentlichen Daten dieses Records werden in “rdata” abgelegt. Der “_class”-Eintrag der Struktur trägt meist den Wert 1 und repräsentiert damit die üblich IN- (Internet) Einträge. Nachdem die eingehende DNS-Nachricht so aufbereitet wurde, muss Squid noch die für diese Antwort passende DNS-Anfrage (Query) finden. Hierzu ruft der Code “idnsFindQuery()” auf, um diese in einer verketteten Liste von Anfragen zu finden. Dabei kommt eine
“query”-Struktur zurück, die eine Sub-Struktur namens “initial_AAAA” enthält, die vorherige DNS-Antworten auf diese Anfrage speichert.

struct _idns_query { hash_link hash; rfc1035_query query; char buf[RESOLV_BUFSZ]; char name[NS_MAXDNAME + 1]; char orig[NS_MAXDNAME + 1]; ssize_t sz; unsigned short msg_id; /// random query ID sent to server; changes with every query sent InstanceId<idns_query> xact_id; /// identifies our "transaction", stays constant when query is retried int nsends; int need_vc; struct timeval start_t; struct timeval sent_t; struct timeval queue_t; dlink_node lru; IDNSCB *callback; void *callback_data; int attempt; int rcode; idns_query *queue; unsigned short domain; unsigned short do_searchpath; bool need_A; struct { int count; rfc1035_rr *answers; } initial_AAAA;
};

Für das weitere Behandeln der so abgelegten DNS-Antworten ist es natürlich wichtig zu wissen, wie viele Records korrekt entpackt werden konnten. Darum liefert
“rfc1035MessageUnpack()” die Zahl dieser Records als Integer zurück. Da alle diese Strukturen selbst und die darin enthalten Zeiger je nach DNS-Antwort dynamisch alloziert werden, ist das Freigeben des Speichers nicht mit einem simplen “free()”-Aufruf auf die gesamte Struktur möglich. Stattdessen müssen die verschiedenen Zeiger der Strukturen selbst korrekt freigegeben werden. Deshalb gibt es in “rfc1035.c” auch eigens dafür vorgesehene “Destroy()”-Funktionen. Zur Freigabe der “rfc1035_message”-Struktur dient beispielsweise die Funktion “rfc1035MessageDestroy()”. Aufgrund all dieser verschiedenen Strukturen ist es natürlich sehr wichtig, dass Squid genau darüber Buch führt, wie viele RRs schon alloziert wurden, damit diese auch am Ende wieder korrekt freigegeben werden können.

Der Fehler

Wo tritt nun genau das Problem mit obigem DNS-Beispiel auf? Dazu muss man sich einen Teil der Funktion “idnsGrokReply()” ansehen, der genau dann aufgerufen wird, wenn für eine DNS-Abfrage schon zuvor DNS-Antworten (in diesem Fall CNAME-RRs) gegeben wurden. Dieser Code-Abschnitt sieht folgendermaßen aus:

/** If there are two result sets from preceeding AAAA and A lookups merge them with a preference for AAAA */
if (q->initial_AAAA.count > 0 && n > 0) { ...
} else if (q->initial_AAAA.count > 0 && n <= 0) { /* initial of dual queries was the only result set. */ ...
}
/* else initial results were empty. just use the final set as authoritative */
...

Wie die Kommentare andeuten, werden die If-Fälle aufgerufen, wenn schon DNS-Antworten vorhanden sind, was durch “q->initial_AAAA.count>0” angezeigt wird, wobei “q” ein Zeiger auf die von “idnsFindQuery(“) zurückgelieferte “_idns_query-Struktur” ist. Innerhalb der If-Anweisungen versucht Squid dann, diese verschiedenen DNS-Antworten zu kombinieren und aktualisiert auch die “initial_AAAA.count”- und “n”-Werte, die über die Zahl verarbeiteter RRs Buch führen. Genau hier verzettelt sich der Code nun beim Updaten dieser Werte für obiges DNS-Beispiel. Das führt zu einer Inkonsistenz, die fatale Folgen hat, wenn der für diese DNS-Nachrichten und -RRs allozierte Speicher wieder freigegeben werden soll. Dann nämlich versucht Squid aufgrund des Verzählens Speicher freizugeben, der gar nicht alloziert wurde. Dies führt schließlich zu einem Segmentation Fault beim Aufruf der Glibc-Funktion “free()”.

Das Patch für diese recht versteckte Sicherheitslücke sieht so aus:

--- src/dns_internal.cc 2011-07-13 09:25:23 +0000
+++ src/dns_internal.cc 2011-10-11 02:08:18 +0000
@@ -1126,16 +1126,17 @@ /* free the RR object without freeing its child strings (they are now taken by the copy above) */ safe_free(message->answer);
+ n += q->initial_AAAA.count;
+ q->initial_AAAA.count = 0; message->answer = result;
- message->ancount += q->initial_AAAA.count;
- n += q->initial_AAAA.count;
- q->initial_AAAA.count=0;
+ message->ancount = n; } else if (q->initial_AAAA.count > 0 && n <= 0) { /* initial of dual queries was the only result set. */ debugs(78, 6, HERE << "Merging DNS results " << q->name << " AAAA has " << q->initial_AAAA.count << " RR, A has " << n << " RR"); rfc1035RRDestroy(&(message->answer), n); message->answer = q->initial_AAAA.answers; n = q->initial_AAAA.count;
+ message->ancount = n; } /* else initial results were empty. just use the final set as authoritative */

Es korrigiert unter anderem “message->ancount” und “n”, und stellt sicher, dass in diesen Variablen immer die korrekte Zahl an Resource Records abgelegt ist. Damit kommt der Code nun nicht mehr durcheinander, und die Denial-of-Service-Attacke ist nicht mehr möglich.

Betroffen sind die Squid-Versionen vor 3.1.16.

 

Nach oben