DNS Dampening

Wie schon in DNSSEC Amplification Attack erklärt, mißbraucht man meine Technik für DDoS Angriffe. Unglücklicherweise bieten weder das Protokoll noch die Software Schutz. Im Gegenteil: Beide versuchen die bestmögliche (Angriffs-)Performance hinzulegen. Dies ist der Versuch einer intelligenteren Abwehr.

Die Ausgangslage ist wohlbekannt. Man sendet einem DNS-Server Anfragen mit gefälschten Absendeadressen, welcher dieser beantwortet und deutlich größere Pakete an das Opfer sendet. Man kann so auf triviale Weise mehrere hundert Mbps zusammenbekommen.

Bekannte Lösungen

Als Serverbetreiber steht man hilflos da. Genaugenommen hat man folgende Möglichkeiten:

  • Man kann den DNS Betrieb ganz einstellen. Das bedeutet, daß man keine eigenen DNS Server für eigene Zonen mehr betreiben kann.
  • Man kann die Bandbreite des DNS Servers beschränken. Dann wird der Hauptteil der DNS Pakete immer noch der Angriff sein und man verwirft hauptsächlich die notwendigen Antworten für den eigenen Wirkbetrieb. Die Paketverluste der Drosselung treffen alle Anfragen mit gleicher Wahrscheinlichkeit, den Angreifer stört es aber nicht.
  • Man kann pro Client ein Ratelimit einführen. Diese Lösung behagt mir nicht, weil sie weiterhin einen konstanten Angriffstrom liefert. DDoS funktioniert auch mit niedrigen Bandbreiten, wenn man nur genügend Server findet (was leicht ist). Darüber hinaus ist die Lösung ressourcenintensiv.
  • Man kann auf die häufigsten Anfragen (ANY via UDP) mit REFUSED oder TRUNCATED antworten. Die Antwortpakete sind dann kleiner als die Anfragen, so daß sich der Angriff für den Angreifer nicht lohnt. Unglücklicherweise interessiert es den Angreifer nicht und die Restbandbreiten der Angriffe sind, wie im Punkt zuvor, immer noch ausreichend.

Alle diese Möglichkeiten haben mich nicht praktisch überzeugt.

Tägliche Praxis

Um ein Gefühl für die Größenordnung des Problem zu bekommen, hier einige reale Beispielbilder. Zuerst die Bandbreitenbeschränkung auf 100Mbps. Die Leitung geht zum Switch hin in die Sättigung, obwohl nur acht Mbps an Angriffsdaten vorliegen. Der Verstärkungseffekt ist unverkennbar. So geht das also nicht.

dnssec-attack-bandwith

Nun die Verkleinerung der Antwortdaten (in meinem Fall durch REFUSED). Der Effekt tritt unmittelbar nach Wechsel der Software ein und ist sehr beruhigend. Allerdings beschweren sich die Opfer weiterhin völlig zu Recht, weil allein dieser Server immer noch eine typische Geschäftskundenleitung mit 2Mbps in die Sättigung treibt. Darüber hinaus fallen auch einige Datenpakete auf, die den Angriff nicht mit der klassischen ANY Anfrage durchführen. Ein Wettrennen, das man nur verlieren kann, hat schon längst begonnen.

dnssec-attack-refused

Meine Überlegung war nun, einen selbstlernenden Prozeß zu haben, der auf "böse" Aktionen mit vollständiger Ignoranz reagiert. Allerdings soll er auch selbstheilend sein. Das Ganze System muß auf Autopilot laufen können, ohne Schaden anzurichten.

Dampening in der Theorie

Als Netzwerker kenne ich BGP Route Dampening, das konzeptionell auf die Problemstellung paßt. Falsche Defaults hatten diese Lösung in Verruf gebracht. Ich hatte vorsichtig zu sein und der Praxis das letzte Wort zu überlassen.

Die Idee bestand also darin, pro anfragender IP (oder Netz) Strafpunkte je nach Art der Anfrage und Größe der Antwort zu vergeben. Diese Strafpunkte sollten nach einer gewissen Halbwertzeit verfallen. Im Sinne einer Hysterese sollte das Dampening bei hohen Werten aktiviert werden und erst bei einer deutlich niedrigeren Schwelle wieder aus gehen. Während des Dampenings sollten keine Anfragen beliebigen Typs beantwortet werden, insbesondere auch keine Fehlermeldungen verschickt werden.

An der Cisco Implementation gefiel mir instantan nicht, daß in regelmäßigen Abständen ein Wartungsdurchlauf notwendig war, um die Werte in der Tabelle altern zu lassen. Ich wollte in konstanter, oder wenigstens logarithmischer Zeit fertig werden. Das Altern Tabelleneinträge mußte irgendwie nebenbei mit abfallen.

Darüber hinaus wollte ich eine möglichst schnelle Bearbeitung im durchschnittlichen Fall erreichen. Dieser wird aber von den Angriffen dominiert. Eine Reihenfolge, in der die Angriffs-IPs vorn stehen, würde sich hier anbieten.

Als Datenmodell wählte ich einen Heap. In Arraydarstellung benötigt der Heap keinerlei Verwaltungsinformation, denn die Baumstruktur ergibt sich über die Indexarithmetik. In dieser Darstellung stehen die "höchstwertigen" Einträge vorn, so daß die lineare Suche die Adresse in ungefährer Reihenfolge der Häufigkeit durchsuchen wird.

Anstatt den Heap regelmäßig zu altern, wollte ich nur das aktuell bearbeitete Element und das erste Element anfassen. Dazu war es notwendig, nicht nur den aktuellen Punktestand, sondern auch den Zeitpunkt der letzen Änderung abzuspeichern. So läßt sich nachträglich der jetztige Punktstand berechnen. Damit sich die Berechnung auch lohnt, kann man sie einige Sekunden bedenkenlos aussetzen und mit veralteten Werten rechnen.

Ein wesentliches Designkriterium stabiler Software ist der deterministische Umgang mit Ressourcen. Der Heap sollte also nur eine feste Größe haben. Einträge, die unter einen bestimmten Grenzwert fallen, können einfach gelöscht werden. Ist der Heap trotzdem voll, so überschreiben neue Werte den allerletzten Eintrag im Heap.

Theoretisch sollte so der Heap alle Elemente regelmäßig altern lassen. Da das erste Element jedesmal gealtert wird, fällt es so unter die Punktzahl anderer Einträge. Da es dabei seinen Spitzenplatz verliert, kommen alle Einträge mal zum Zuge.

Aus der Sicherheitstechnik kannte ich auch die Gefahr von Integer-Overflows. Die Strafpunkte werden also auf einen Maximalwert begrenzt und nicht endlos aufaddiert.

Tückische Praxis

Mangels implementierter Konfigurationsmöglichkeiten habe mit einigen konservativen Default begonnen:

  • 1 Strafpunkt pro Anfrage
  • Dampening aktiv bei 40000 Strafpunkten
  • Dampening inaktiv bei 1000 Strafpunkten
  • Rauswurf aus der Tabelle bei 100 Strafpunkten

Der allererste Versuch war ernüchternd: Kommentarloser Absturz nach wenigen Sekunden. Grund war schlicht, daß der Heap nicht alloziert war. Anfängerfehler. Also habe ich gleich noch die Funktionsaufrufe mit ausführlichen asserts versehen.

Aber schon die nächsten Tests waren vielversprechend.

dnssec-attack-dampening1

Bis 02:00 lief die alte Software, die REFUSED Pakete ausliefert (mit nahezu konstanter Datenrate). Beim Wechsel der Software fällt diese aus und es gibt einen Einbruch, dem ein drastisches Ansteigen folgt, weil das Dampening lernt. Bereits nach 19 Sekunden hat das Dampening die ersten Angreifer gefangen. Das waren 40000 Queries!

02:10:30.212 general: notice: running
02:10:49.136 client: warning: 99.71.68.170 enters dampening.
02:11:07.477 client: warning: 108.248.25.44 enters dampening.
02:11:27.321 client: warning: 173.218.224.224 enters dampening.
02:11:58.131 client: warning: 217.23.3.214 enters dampening.

Aber wenige Minuten später war der nächste Absturz da:

02:15:51.732 critical: dampening.c:282: fatal error:
02:15:51.732 critical: RUNTIME_CHECK(b > 0 && b <= dampening_last) failed
02:15:51.732 critical: exiting (due to fatal error in library)

Dieser Fehler steht direkt in den Heap-Manipulationsroutinen. Also wurde die Funktion "heap_down" logisch aufgeräumt (mit Dank an Knuth, TAoCP) und verlor dabei 80% des Codes.

Erst 13:30 ging es wieder los und prompt in die Hose: Der Traffic stieg unaufhaltsam. Grund war, daß sich die IPs um den letzten Eintrag in der Tabelle prügelten. Die Einträge warfen sich gegenseitig raus, so daß keiner die notwendige Punktzahl aufsammeln konnte.

13:22:31.985 client: info: 61.247.0.2 added to dampening with 1 (replacing 212.49.98.14 with 31)

Die Lösung war schlicht, nicht zu überschreiben, wenn der vorhandene Eintrag bereits eine höhere Punktzahl aufweist. Statt dessen wird eine Fehlermeldung geworfen, wenn der letzte Eintrag zu hohe Strafpunkte hat. Dann ist die Tabelle offenbar nur mit Angreifern gefüllt.

Mit diesen Änderungen hielt sich das System richtig gut. Nach einer Anlernphase um 14:30 drückte es den Ausgangstraffic herunter auf ca. 2 Mbps. Dann begann es wieder Angriffe durchzulassen.

15:29:59.321 client: error: Dampening table to small! Entry 213.186.33.20 with 12175 at the end.

WTF? So viele Angreifer hatte ich im mitlaufenden Sniffing nicht beobachtet. Stattdessen gab es ab und zu mal einen Spike mit einer neuen Adresse. Der Fehler muß woanders liegen. Meine Annahme, die Tabelle würde irgendwie automatisch durchaltern schien sich nicht erfüllt zu haben. Wie auch? Die vorderen Einträge werden durch den weiter bestehenden Angriff immer weiter angeheizt und liegen permanent am oberen Anschlag. Die Strafpunkte verfallen also langsamer als neue Punkte dazukommen.

Es war keine schlaue Idee, jeden Tabellenüberlauf zu loggen, ohne etwas zu tun. Das GB Logfiles hätte ich mir sparen können. Es ist aber auch keine Lösung, bei Tabellenüberlauf das Programm zu beenden. Denn alles was man dann tun kann, ist das Programm neu zu starten. Es genügt also, die Tabelle zu leeren und neu zu lernen.

Um die komplette Tabelle altern zu lassen werden jetzt drei Werte angefaßt: Der erste, der letzte und ein Wert an einer zufälligen Stelle. Der Zufall muß nicht perfekt sein, nur ab und zu mal hier oder dort reinschlagen. Kurz nach 17:00 ging auch diese Änderung live. Kurz nach 18:00 ist allerdings schon wieder unterwartet Schluß:

17:55:51.045 general: critical: dampening.c:283: fatal error:
17:55:51.045 general: critical: RUNTIME_CHECK(b > 0 && b <= dampening_last) failed
17:55:51.045 general: critical: exiting (due to fatal error in library

An dieser Stelle hätte dieser Fehler aber nie auftreten können. Was ist nur geschehen?

Die einzige Erklärung ist, daß jemand an dem Heap gedreht hat, während er bearbeitet wurde. Bind läuft aber in einem Prozeß, also Singletask. Oder? Eben nicht. Es ist multithreaded. Ein Quickcheck mit einer statischen Variable, die beim API Aufruf hoch zählt und dann wieder abfällt ergibt instantan:

general: critical: RUNTIME_CHECK(duplicatecheck == 0) failed

Na Prima! Der Code wurde nun heftig aufgeräumt, um ein zentrales LOCK um die Datenstruktur zu erlauben. Und siehe da! Es geht.

dnssec-attack-dampening3

Weiterhin wurde die Anlernphase verkürzt, indem folgende Strafpunkttabelle aktiv ist:

  • 10 Strafpunkte pro erste Query
  • 100 Strafpunkte für eine ANY Anfrage, sonst 1 Strafpunkt
  • Wird eine Anfrage mit gleicher ID wiederholt, so gibt es 100 Strafpunkte mal die schon vorhandenen Wiederholungen. D.h. für die erste Wiederholung einer Query gibt es 100 Punkte, für die zweite Wiederholung 200. Das summiert sich schnell auf.
  • Je nach Antwortgröße gibt es zwischen 1 und 5 Punkten bis 500 Byte, 10 und 50 Punkten bis 2500 Byte und dann 100 oder 200 Punkte für größere Antworten.

Mit diesen Einstellungen werden Angriffe im Schnitt nach 40 Paketen erkannt und ignoriert. Im Klartext die Zahlen noch mal nach zwei Stunden Betrieb:

  5 minute  input rate  381000 bits/sec,   23 packets/sec
  5 minute output rate 7275000 bits/sec, 9479 packets/sec 

Von 10000 Paketen werden 30 als sinnvoll anerkannt und bearbeitet.

Der Patch

Dieser Patch steht hier nur noch aus historischen Gründen. Eine benutzbare Version findet sich hier.

Achtung Update
diff -rbTuN bind-9.9.1-P3/bin/named/client.c bind-9.9.1-P3-dampening/bin/named/client.c
--- bind-9.9.1-P3/bin/named/client.c    2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/client.c  2012-09-23 22:05:38.000000000 +0200
@@ -55,6 +55,8 @@
        #include <named/server.h>
        #include <named/update.h>

+       #include "dampening.h"
+
        /***
         *** Client
         ***/
@@ -877,6 +879,8 @@
                        goto done;
                }

+               dampening_by_size(client, mr->length);
+
                result = client_allocsendbuf(client, &buffer, NULL, mr->length,
                                             sendbuf, &data);
                if (result != ISC_R_SUCCESS)
diff -rbTuN bind-9.9.1-P3/bin/named/dampening.c bind-9.9.1-P3-dampening/bin/named/dampening.c
--- bind-9.9.1-P3/bin/named/dampening.c 1970-01-01 01:00:00.000000000 +0100
+++ bind-9.9.1-P3-dampening/bin/named/dampening.c       2012-09-23 22:34:37.000000000 +0200
@@ -0,0 +1,415 @@
+       /*
+        * Dampening
+        * =========
+        *
+        * Purpose of dampening is to sort out misbehaving clients in order to
+        * serve the legitemate clients and protect the innocent vitims of a
+        * DNS(SEC) amplification attack caused by spoofed UDP queries.
+        *
+        * Overview
+        * ~~~~~~~~
+        *
+        * Each querier IP gets a additive penalty depening on the type of the
+        * query, the size of the answer, and other parameters.
+        *
+        * If the penalty reaches a limit, the state of the IP switches to a
+        * "dampening". In this state the server drops every query from this IP
+        * without further processing (besides adding penalty points).
+        *
+        * The penalty is decreased expotentially over time. So if no further points
+        * are added fast enough, the total penalty will drop below a secondary limit.
+        * Then the state of the IP is changed back to "normal".
+        *
+        * Implementation
+        * ~~~~~~~~~~~~~~
+        *
+        * DNS servers to not track their clients. So adding state to querier IPs
+        * requires a new storage model. Because access to this storage is needed
+        * by processing each query, the storage must be in memory only. In order
+        * to prevent ressouce exhaustion, the used memory is fixed.
+        *
+        * The assumed access pattern is that high penalty clients will be cause
+        * most of the queries. The used data structure is therefore a heap.
+        *
+        * The following operations are implemented:
+        *
+        *  a) Searching an IP by travering the heap array from front (high values)
+        *     to the end (beware of a partially filled heap).
+        *
+        *  b) Adding a new IP (if not found) by overwriting the very last entry
+        *     of the heap, if the heap is full. This will cause one of the low
+        *     penality entries to be lost.
+        *
+        *  c) If a searched IP was found, the last updated timestamp is used to
+        *     recalculate the penality value (expotential decrease plus new
+        *     points), update the state, and rebalanced the heap for this modified
+        *     entry only. If the penality value falls below a certain limit, the
+        *     entry is removed from the heap.
+        *
+        *  d) The top, last, and a random node are recalculated and rebalanced
+        *     after each update or insertion, if the nodes were not updated
+        *     recently.
+        *
+        * Using this approach, the operations on the heap will be O(log n) on
+        * each access. There is no need for a regular maintainence activity.
+        *
+        * TODO
+        * ~~~~
+        *
+        *  - All hardcoded parameters need to be (re)configurable.
+        *  - Allow ACL dependant penalty parameters.
+        *  - Check long time stability.
+        *  - Check, if the assumptions are really true.
+        *
+        */
+
+       #include "dampening.h"
+       #include <isc/util.h>
+       #include <stdlib.h>
+       #include <math.h>
+       #include <named/globals.h>
+       #include <named/log.h>
+
+       #include <isc/netaddr.h>
+       #include <isc/stdtime.h>
+
+       typedef unsigned int dampening_penalty_t;
+       typedef size_t dampening_position_t;
+       #define DAMPENING_NOTHING 0
+
+       struct {
+          struct {
+             dampening_penalty_t top, damp, norm, drop;
+          } limit;
+          size_t heapsize;
+          int updatedelay;
+          int halflife;
+       } dampening_configuration = {
+            { 60000, 40000, 1000, 100 },      /* limits */
+            5000,                             /* entries in the heap */
+            5,                                /* delay expire by seconds */
+            600                               /* half-live of penalty in seconds */
+       };
+
+       typedef struct dampening_entry {
+          isc_netaddr_t netaddr;
+          isc_stdtime_t last_updated;
+          dns_messageid_t last_id;
+          size_t last_id_count;
+          unsigned int penalty;
+          dampening_state_t state;
+       } dampening_entry_t;
+
+       static dampening_entry_t * dampening_heap = NULL;
+       static dampening_position_t dampening_last = 0;
+       static isc_mutex_t dampening_lock;
+
+       /*
+        * Helper functions
+        */
+       void dampening_init(void);
+       dampening_position_t dampening_search(const isc_netaddr_t * netaddr);
+       void dampening_add(const isc_netaddr_t * netaddr, dampening_penalty_t points, isc_stdtime_t now);
+       void dampening_update(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now);
+       void dampening_update1(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now);
+       dampening_state_t dampening_get_state(dampening_position_t entry);
+       void dampening_expire(isc_stdtime_t now);
+       void dampening_heap_up(dampening_position_t entry);
+       void dampening_heap_down(dampening_position_t entry);
+       void dampening_heap_swap(dampening_position_t a, dampening_position_t b);
+       int dampening_below_top(dampening_position_t entry);
+
+       /*
+        * Main API function
+        */
+       dampening_state_t dampening_query(ns_client_t * client) {
+          isc_stdtime_t now;
+          isc_netaddr_t addr;
+          dampening_state_t state;
+          dampening_position_t entry;
+
+          RUNTIME_CHECK(client != NULL);
+          LOCK(&dampening_lock);
+
+          if(dampening_heap == NULL)
+            dampening_init();                 /* Autoinit */
+
+          isc_stdtime_get(&now);
+          isc_netaddr_fromsockaddr(&addr, &client->peeraddr);
+
+          entry = dampening_search(&addr);
+          if(entry == DAMPENING_NOTHING) {
+             dampening_add(&addr, 10, now);
+             state = DAMPENING_NORMAL;
+          } else {
+             state = dampening_get_state(entry);
+          }
+
+          UNLOCK(&dampening_lock);
+          return state;
+       }
+
+       void dampening_by_qtype(ns_client_t * client, dns_rdatatype_t qtype) {
+          isc_stdtime_t now;
+          isc_netaddr_t addr;
+          dampening_position_t entry;
+          dampening_penalty_t points;
+
+          RUNTIME_CHECK(client != NULL);
+          INSIST(dampening_heap != NULL);
+
+          LOCK(&dampening_lock);
+          isc_stdtime_get(&now);
+          isc_netaddr_fromsockaddr(&addr, &client->peeraddr);
+
+          entry = dampening_search(&addr);
+          if(entry != DAMPENING_NOTHING) {
+             switch(qtype) {
+              case dns_rdatatype_any: points = 100; break;
+              default               : points =   1; break;
+             }
+
+             if(dampening_heap[entry].last_id == client->message->id) {
+                points += (dampening_heap[entry].last_id_count++)*100;
+             } else {
+                dampening_heap[entry].last_id = client->message->id;
+                dampening_heap[entry].last_id_count = 1;
+             }
+
+             dampening_update(entry, points, now);
+          }
+
+          UNLOCK(&dampening_lock);
+       }
+
+       void dampening_by_size(ns_client_t * client, size_t length) {
+          isc_stdtime_t now;
+          isc_netaddr_t addr;
+          dampening_position_t entry;
+          dampening_penalty_t points;
+
+          RUNTIME_CHECK(client != NULL);
+          INSIST(dampening_heap != NULL);
+
+          LOCK(&dampening_lock);
+          isc_stdtime_get(&now);
+          isc_netaddr_fromsockaddr(&addr, &client->peeraddr);
+
+          entry = dampening_search(&addr);
+          if(entry != DAMPENING_NOTHING) {
+             if(length <= 100)  points =   1; else
+             if(length <= 200)  points =   2; else
+             if(length <= 300)  points =   3; else
+             if(length <= 400)  points =   4; else
+             if(length <= 500)  points =   5; else
+             if(length <= 700)  points =  10; else
+             if(length <= 1000) points =  20; else
+             if(length <= 1500) points =  30; else
+             if(length <= 2000) points =  40; else
+             if(length <= 2500) points =  50; else
+             if(length <= 5000) points = 100; else
+                                points = 200;
+             dampening_update(entry, points, now);
+          }
+
+          UNLOCK(&dampening_lock);
+       }
+
+       /*
+        * Set up the heap: allocate the memory range. Change of size is possibe,
+        * so let dampening_last untouched.
+        */
+       void dampening_init(void) {
+          dampening_heap = realloc(dampening_heap, (1+dampening_configuration.heapsize) * sizeof(dampening_entry_t));
+          if(dampening_heap == NULL)
+            UNEXPECTED_ERROR(__FILE__, __LINE__, "realloc() failed");
+       }
+
+       /*
+        * Return the current state.
+        */
+       dampening_state_t dampening_get_state(dampening_position_t entry) {
+          REQUIRE(dampening_heap != NULL);
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          return dampening_heap[entry].state;
+       }
+
+       /*
+        * Simple search though the heap, exploiting the feature, that the heap
+        * is mapped to a continiously filled, linear array. High penalty values
+        * occur first, so they are searched first.
+        * Returns DAMPENING_NOTHING if no match.
+        */
+       dampening_position_t dampening_search(const isc_netaddr_t * netaddr) {
+          dampening_position_t i;
+
+          REQUIRE(dampening_heap != NULL);
+          for(i = 1; i <= dampening_last; i++) {
+             if(ISC_TRUE == isc_netaddr_equal(&dampening_heap[i].netaddr, netaddr))
+               return i;
+          }
+          return DAMPENING_NOTHING;
+       }
+
+
+       /*
+        * Add an element to the end (eventually overwrite the last one)
+        */
+       void dampening_add(const isc_netaddr_t * netaddr, dampening_penalty_t points, isc_stdtime_t now) {
+          REQUIRE(dampening_heap != NULL);
+
+          if(dampening_last < dampening_configuration.heapsize) {
+             dampening_last++;
+          } else {
+             if(dampening_heap[dampening_last].penalty > points) {
+                if(dampening_heap[dampening_last].penalty > dampening_configuration.limit.drop)
+                  isc_log_write(ns_g_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, ISC_LOG_ERROR,
+                                "Dampening table overflow! Restart dampening.");
+                dampening_last = 1;
+                return;
+             }
+          }
+
+          memcpy(&dampening_heap[dampening_last].netaddr, netaddr, sizeof(isc_netaddr_t));
+          dampening_heap[dampening_last].last_updated = now;
+          dampening_heap[dampening_last].penalty = 0;
+          dampening_heap[dampening_last].last_id = 0;
+          dampening_heap[dampening_last].last_id_count = 0;
+
+          dampening_update(dampening_last, points, now);
+       }
+
+       /*
+        * Update an existing entry with new points
+        */
+       void dampening_update(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now) {
+          dampening_update1(entry, points, now);
+          dampening_expire(now);
+       }
+
+       void dampening_update1(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now) {
+          int timediff;
+          char onbuf[ISC_NETADDR_FORMATSIZE];
+
+          REQUIRE(dampening_heap != NULL);
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          timediff = now - dampening_heap[entry].last_updated;
+          if(timediff > dampening_configuration.updatedelay) {
+             float penalty = dampening_heap[entry].penalty;
+             penalty *= exp(-(0.693*timediff)/dampening_configuration.halflife);
+             dampening_heap[entry].penalty = penalty;
+             dampening_heap[entry].last_updated = now;
+          }
+          if(dampening_heap[entry].penalty >= dampening_configuration.limit.top - points)
+            dampening_heap[entry].penalty = dampening_configuration.limit.top;
+          else
+            dampening_heap[entry].penalty += points;
+
+          if(dampening_heap[entry].state == DAMPENING_SUPPRESS &&
+             dampening_heap[entry].penalty < dampening_configuration.limit.norm) {
+
+             isc_netaddr_format(&dampening_heap[entry].netaddr, onbuf, sizeof(onbuf));
+             isc_log_write(ns_g_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
+                           "%s dampening removed.", onbuf);
+             dampening_heap[entry].state = DAMPENING_NORMAL;
+          }
+
+          if(dampening_heap[entry].state == DAMPENING_NORMAL &&
+             dampening_heap[entry].penalty > dampening_configuration.limit.damp) {
+
+             isc_netaddr_format(&dampening_heap[entry].netaddr, onbuf, sizeof(onbuf));
+             isc_log_write(ns_g_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
+                           "%s dampening activated.", onbuf);
+             dampening_heap[entry].state = DAMPENING_SUPPRESS;
+          }
+
+          if(dampening_heap[entry].penalty < dampening_configuration.limit.drop &&
+             timediff > dampening_configuration.updatedelay) {
+             if(entry < dampening_last) {
+                dampening_heap_swap(entry, dampening_last);
+             } else {
+                entry = 0;
+             }
+             dampening_last--;
+          }
+
+          if(entry > 0) {
+             if(dampening_below_top(entry))
+               dampening_heap_down(entry);
+             else
+               dampening_heap_up(entry);
+          }
+       }
+
+       /*
+        * Expire penalties
+        */
+       void dampening_expire(isc_stdtime_t now) {
+          dampening_penalty_t poi[3] = { 1, dampening_last, 1+(random()%dampening_last) };
+          unsigned int i;
+
+          for(i=0; i<sizeof(poi)/sizeof(*poi); i++) {
+
+             if(0 < poi[i] && poi[i] <= dampening_last &&
+                now > dampening_heap[poi[i]].last_updated + dampening_configuration.updatedelay)
+               dampening_update1(poi[i], 0, now);
+          }
+       }
+
+       /*
+        * Heap operations
+        */
+       void dampening_heap_up(dampening_position_t entry) {
+          dampening_position_t e;
+
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          for(e = entry/2;
+              e > 0;
+              entry = e, e = entry/2) {
+             if(dampening_heap[entry].penalty <= dampening_heap[e].penalty)
+               break;
+             dampening_heap_swap(entry, e);
+          }
+       }
+
+       void dampening_heap_down(dampening_position_t entry) {
+          dampening_position_t e;
+
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          for(e = 2*entry;
+              e <= dampening_last;
+              entry = e, e = 2*entry) {
+
+             if(e+1 <= dampening_last &&
+                dampening_heap[e].penalty <= dampening_heap[e+1].penalty)
+               e = e+1;
+
+             if(dampening_heap[e].penalty <= dampening_heap[entry].penalty)
+               break;
+
+             dampening_heap_swap(entry, e);
+          }
+       }
+
+       void dampening_heap_swap(dampening_position_t a, dampening_position_t b) {
+          dampening_entry_t temp;
+
+          RUNTIME_CHECK(a > 0 && a <= dampening_last);
+          RUNTIME_CHECK(b > 0 && b <= dampening_last);
+          RUNTIME_CHECK(a != b);
+
+          memcpy(&temp,              &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[a], &dampening_heap[b], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[b], &temp,              sizeof(dampening_entry_t));
+       }
+
+       int dampening_below_top(dampening_position_t entry) {
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+          return entry <= 1 ||
+            (dampening_heap[entry].penalty <= dampening_heap[entry/2].penalty);
+       }
+
diff -rbTuN bind-9.9.1-P3/bin/named/dampening.h bind-9.9.1-P3-dampening/bin/named/dampening.h
--- bind-9.9.1-P3/bin/named/dampening.h 1970-01-01 01:00:00.000000000 +0100
+++ bind-9.9.1-P3-dampening/bin/named/dampening.h       2012-09-23 21:37:44.000000000 +0200
@@ -0,0 +1,14 @@
+       #ifndef _DAMPENING_H
+       #define _DAMPENING_H
+
+       #include <named/client.h>
+
+       typedef enum dampenting_state {
+          DAMPENING_NORMAL, DAMPENING_SUPPRESS
+       } dampening_state_t;
+
+       dampening_state_t dampening_query(ns_client_t * client);
+       void dampening_by_qtype(ns_client_t * client, dns_rdatatype_t qtype);
+       void dampening_by_size(ns_client_t * client, size_t length);
+
+       #endif /* _DAMPENING_H */
diff -rbTuN bind-9.9.1-P3/bin/named/Makefile.in bind-9.9.1-P3-dampening/bin/named/Makefile.in
--- bind-9.9.1-P3/bin/named/Makefile.in 2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/Makefile.in       2012-09-19 15:55:39.000000000 +0200
@@ -87,6 +87,7 @@
                        zoneconf.@O@ \
                        lwaddr.@O@ lwresd.@O@ lwdclient.@O@ lwderror.@O@ lwdgabn.@O@ \
                        lwdgnba.@O@ lwdgrbn.@O@ lwdnoop.@O@ lwsearch.@O@ \
+                       dampening.@O@ \
                        ${DLZDRIVER_OBJS} ${DBDRIVER_OBJS}

        UOBJS =         unix/os.@O@ unix/dlz_dlopen_driver.@O@
@@ -101,6 +102,7 @@
                        zoneconf.c \
                        lwaddr.c lwresd.c lwdclient.c lwderror.c lwdgabn.c \
                        lwdgnba.c lwdgrbn.c lwdnoop.c lwsearch.c \
+                       dampening.c \
                        ${DLZDRIVER_SRCS} ${DBDRIVER_SRCS}

        MANPAGES =      named.8 lwresd.8 named.conf.5
diff -rbTuN bind-9.9.1-P3/bin/named/query.c bind-9.9.1-P3-dampening/bin/named/query.c
--- bind-9.9.1-P3/bin/named/query.c     2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/query.c   2012-09-23 21:50:22.000000000 +0200
@@ -61,6 +61,8 @@
        #include <named/sortlist.h>
        #include <named/xfrout.h>

+       #include "dampening.h"
+
        #if 0
        /*
         * It has been recommended that DNS64 be changed to return excluded
@@ -7282,6 +7284,14 @@
                }

                /*
+                * Update the penalty and report the current state
+                */
+               if (dampening_query(client) == DAMPENING_SUPPRESS) {
+                       query_next(client, DNS_R_DROP);
+                       return;
+               }
+
+               /*
                 * Get the question name.
                 */
                result = dns_message_firstname(message, DNS_SECTION_QUESTION);
@@ -7323,6 +7333,7 @@
                INSIST(rdataset != NULL);
                qtype = rdataset->type;
                dns_rdatatypestats_increment(ns_g_server->rcvquerystats, qtype);
+               dampening_by_qtype(client, qtype);
                if (dns_rdatatype_ismeta(qtype)) {
                        switch (qtype) {
                        case dns_rdatatype_any:

Einen richtigen Patch zum Download gibt es, wenn die Konfigurierbarkeit gegeben ist.

Peinlich, peinlich

Da muß man erst den Quellcode veröffentlichen, um einen besonders abstrusen Fehler gezeigt zu bekommen. Ja merkt denn keiner die Copy und Waste Orgien? Hier werden effektiv Daten vernichtet und der Heap wird völlig zerstört!

+          memcpy(&temp,              &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[a], &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[b], &temp,              sizeof(dampening_entry_t));

Nochmal zum Mitschreiben: t = a; a = a; b = t; Wie soll das funktionieren?

+          memcpy(&temp,              &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[a], &dampening_heap[b], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[b], &temp,              sizeof(dampening_entry_t));  

So ist es natürlich richtig! Und der Bug ist oben im Patch schon gefixt. Danke an den Suse Mitarbeiter, der das bemerkt hat.

Und noch so ein Hammerfehler: Die Berechnung des Punkteverfalls enthielt nicht das notwendige Minuszeichen. Die Strafpunkte wurden mit der Zeit immer mehr. Ist im Patch behoben, wie ich das gefunden habe, kommt in einem extra Beitrag.

Avatar
Lutz Donnerhacke 19.11.2015 08:08
Please use the language selector in the top navigation to switch to English
Avatar
Erid 15.11.2015 22:42
Could you please have your whole blog in English.We also would like to read
Avatar
Karl Ranseier 27.09.2012 22:58
Das eigentliche Problem des Lösungsansatzes ist, daß er nicht allgemein genutzt werden kann:

Blockiere ich die gespooften IPs (allgemein), so kann ich dafür sorgen, daß ganze Access Netze einen Resolver nicht mehr erreichen. Ein einfaches Beispiel hierfür ist:

Man nehme an, die DNSes von google z.B. würden ein Dampening anwenden wollen. Ich suggeriere eine Amplification attack seitens der recursor mehrerer großer Provider. Der DNS des Anbieters (im Beispiel google) würde also die Recursor der Zugangsprovider 'blacklisten' zumindest solange ich regelmässig (aber nur gelegentlich) einige Anfragen aussende, um die Penalty gezielt oben zu halten. Die Amplification-Attacke ist abgewendet, dafür habe ich sie gegen eine Service-Denial beim Dienstanbieter getauscht. Daher wäre ein Dampening für keinen Diensteanbieter umsetzbar, sofern er gelegentlich erreichbar sein möchte. Das schlimmste aber ist, ich kann spezifische Targets für spezifische Kunden (Clients) gezielt ins Dampening treiben und somit 2 Parteien direkt schaden, wenn ich es geschickt anstelle. (Sorge dafür das Firma X nicht mehr bei Clouddiensteanbieter Y resolven kann ... Nicht so klug ;-) )
Avatar
Lutz Donnerhacke 27.09.2012 22:49
Namecoin skaliert nicht, weil es zu viele "zentrale" Stellen hat. Namecoin ist nicht massentauglich, weil es nicht in die aktuelle Protokollversion hineinwächst, wie es DNSSEC derzeit tut.
Avatar
jym 27.09.2012 17:28
Hallo Lutz,

ist vielleicht ein bisschen off-topic, aber hast du dich mal mit Namecoin beschäftigt?

http://de.wikipedia.org/wiki/Namecoin

Was hältst du von so einem Ansatz?

Gruß
jym
Avatar
Lutz Donnerhacke 25.09.2012 21:51
Es gibt keine offensichtliche Lösung des Problems. Allerdings hat mir Dein Kommentar geholfen, meine aktuellen Experimente an diesem Problem auszurichten: Wie kann ich wenigstens ansatzweise legitime Anfragen beantworten, während der Angriff selbst versandet.

Abwarten!
Avatar
Thomas 25.09.2012 18:03
Wie sinnvoll ist eine Lösung, die "DoS Tür und Tor öffnet"?
Sie macht es sogar noch viel einfacher, Domains für manche Gruppen, zum Beispiel DTAG-Kunden oder x-beliebige andere aus dem Netz zu schießen. Ich muß Deine Nameserver gar nicht mehr unter Dauerfeuer nehmen, sondern ich kann das bequem und bandbreitenschonend über die Strafpunkte steuern. Also DoS statt dDoS.
Avatar
Lutz Donnerhacke 25.09.2012 15:49
IP Spoofing ist genau der Grund, weswegen der Zinnober gemacht wird. Meine Antwortpakete stellen ja genau den Angriff dar.

Ich schweig dann also eine Weile (bis die Punkte verfallen) und mache dann erst weiter. Ja, das öffnet einem DoS Tür und Tor. Ja, es kann sein, daß dann der DTAG Resolver meine Domains gar nicht mehr auflösen kann, weil ich seinen legitimen Paketen auch nicht antworte.

Post a comment

Verwandter Inhalt