So patchen Sie den K̶D̶E̶ TCP-Stack unter FreeBSD

    Bei der Wahl zwischen proprietärer und Open-Source-Software wird häufig das folgende Argument für letztere vorgebracht: Bei Bedarf können Sie die Quelle an die Anforderungen Ihres Projekts anpassen oder den Fehler sofort beheben, anstatt monatelang auf die Reaktion des Anbieters zu warten. Oft wird diese Überlegung von der Praxis getrennt - es ist viel einfacher, die SQL-Abfrage zu beheben, als den SQL-Scheduler zu optimieren oder die Problemausrüstung zu ändern, anstatt den Treiberfehler zu suchen und zu beheben. Manchmal ist es jedoch nur die Offenheit des Codes, die potenzielle Verluste und Überlastung der Rechenressourcen vermeidet. Ich möchte über einen dieser Fälle sprechen, die während meiner Arbeit bei Advanced Hosting aufgetreten sind

    Ein Anstieg des Verkehrsaufkommens und der erste Verdacht auf DDoS


    Am späten Samstagabend kam einer der Server herunter. Der gesamte Cluster dieses Projekts bestand aus 3 + 3 Teilen, und jeder zieht die gesamte Last seiner drei Teile, so dass der Verlust von einem den Dienst nicht bedrohte. Trotzdem war es äußerst unangenehm, dass die Server bisher stillschweigend einen eingehenden Gesamtverkehr von 10 + K http-Anfragen pro Sekunde akzeptierten und (wie es schien) ein paar mehr Leistungsspanne hatten, sich plötzlich als nicht so stabil herausstellten. Während RAID1 neu erstellt wurde und PostgreSQL die Replikation einholte, blieb Zeit, sich den Rest des Servers anzusehen.

    Es lohnt sich, im Voraus zu erklären, wie dieser Cluster funktioniert. Server befinden sich an verschiedenen Orten, zwei in Europa und vier in den USA. Sie sind in Triple-Server unterteilt und dienen ihrer IP-Gruppe (d. H. Für jeden Triple-Server in Europa und die beiden anderen in den USA). Der Datenverkehr wird über Anycast-Mittel verteilt. Auf allen drei Servern werden dieselben IP-Adressen registriert, und eine BGP-Sitzung mit dem direkten Router wird ausgelöst. Wenn ein Server ausfällt, kündigt der entsprechende Router sein Netzwerk nicht mehr im Internet an und der Datenverkehr wird automatisch zu den verbleibenden Servern geleitet.

    Es gab nichts zu sehen. Den Überwachungsdaten zufolge gab es kurz vor dem Fall einen starken Anstieg des eingehenden und ausgehenden Datenverkehrs zu beiden europäischen Servern (einer davon war leichtgewichtig). Wenn sich die Bandbreite verdoppelte, erhöhte sich die Anzahl der Pakete pro Sekunde bereits um das Zehnfache und in beide Richtungen. Das heißt, Die Pakete waren klein und es gab viele (unter 200K pro Sekunde).

    Bei HighLoad-Diensten ändert sich der Datenverkehr einfach nicht, insbesondere bei solchen Größen. Sehr ähnlich zu DDoS, oder? Um nicht zu sagen, dass ich sehr überrascht war, dass ich viele verschiedene Arten von DDoSs sehen musste, und wenn die Netzwerkausrüstung der Anbieter es uns bisher ermöglichte, Datenverkehr ohne Verlust an die Server zu liefern, konnten sie diese immer blockieren. Es war alarmierend, dass der Anstieg des Datenverkehrs nur auf europäischen Servern zu verzeichnen war: Wenn das Botnetz verteilt ist, sollte der Datenverkehr schließlich über den gesamten Cluster verteilt werden.

    Paketverluste und erhöhte aktive TCP-Sitzungen


    Nachdem ich den Server in Betrieb genommen hatte, startete ich `top`,` nload` und begann die Last zu überwachen. Bald verdoppelte sich der Verkehr wieder und die SSH-Sitzung begann erheblich zu verzögern. Es gibt einen Paketverlust, `mtr -ni 0.1 8.8.8.8` bestätigte diese Hypothese sofort und` top -SH` zeigte an, dass der Kern des Betriebssystems darin besteht, dass der Prozessor eingehender Netzwerkpakete nicht über genügend CPU verfügt. Nun ist klar, warum der Server eingefroren ist - Paketverluste ähneln dem Tod.

    Zum Zeitpunkt des Schreibens dieses Beitrags hatte FreeBSD eine sehr unangenehme Funktion im Netzwerkstapel - es lässt sich in Bezug auf die Anzahl der TCP-Sitzungen nicht gut skalieren. Eine mehrfache Erhöhung der Anzahl der TCP-Sitzungen führt zu einem überproportional hohen CPU-Verbrauch. Obwohl es nur wenige Sitzungen gibt, gibt es keine Probleme. Beginnend mit mehreren Zehntausenden von aktiven TCP-Sitzungen tritt beim eingehenden Pakethandler jedoch ein CPU-Mangel auf und er muss Pakete verwerfen. Und dies führt zu einer Kettenreaktion - aufgrund des Paketverlusts werden aktive TCP-Sitzungen langsam bedient, ihre Anzahl beginnt dort zu wachsen, und mit ihr wächst der CPU-Mangel und der Paketverlust steigt weiter an.

    Während der Server nicht vollständig hängt, lösche ich dringend die BGP-Sitzung und beginne parallel damit, den Paketverlust auf dem Server zu überprüfen, der den europäischen Verkehr übernommen hat. Es hat stärkeres Eisen - d.h. Es besteht die Möglichkeit, dass in den USA nichts Schlimmes passiert. Mit dem Problemserver muss etwas getan werden, und das erste, was ich deaktiviere, ist, dass TCP-Sitzungen früher beginnen und insgesamt weniger. Das Einstellen der Netzwerkkarteneinstellungen dauerte mehr als ein Dutzend Minuten. Jedes Mal, wenn die BGP-Sitzung kurz ausgelöst wurde, wurde der Paketverlust überprüft. Ich musste den Abfragemodus verlassen, aber den Leerlauf aktivieren. Jetzt war ein Kern des Prozessors ausschließlich von der Netzwerkkarte belegt, aber der Paketverlust wurde gestoppt.

    Es gab immer noch unverständliche Momente - zum Beispiel unterschied sich die Anzahl der TCP-Sitzungen während des Angriffs und im normalen Betriebsmodus nicht wesentlich, dh das Problem war kein signifikanter Anstieg ihrer Anzahl. Völlig unverständlich war jedoch, warum dieser Angriff auf amerikanischen Servern überhaupt nicht sichtbar war! Während der Trennung der europäischen Server kam nur der tatsächliche Arbeitsverkehr zu den Staatsservern, aber es gab keinen zusätzlichen Verkehr! Nachdem der Verkehr nach Europa zurückgekehrt war, blieb er einige Zeit auf einem funktionierenden Niveau, und dann begann ein weiterer Anstieg.

    Es war ein Uhr morgens, es scheint, als könnten wir den Verlust von Paketen stoppen, und Sie können mit diesen Netzwerk-Kuriositäten mit einem frischen Verstand umgehen. Mit solchen Gedanken ging ich wieder ins Bett, aber nach ein paar Stunden weckten sie mich wieder - diesmal logen beide europäischen Server bereits. Dies brachte eine weitere Kuriosität in die Staatskasse dieses Falles - schließlich war die Zeit bereits spät und der Höhepunkt des Verkehrs war lange zurück. Obwohl dies bei einem DDoS-Angriff normal ist, da die meisten Experten schlafen und sich auf einen Angriff einlassen, gibt es in der Regel niemanden. Beide Server wurden bald gestartet, aber die anschließende Überwachung der Situation brachte nichts Neues - der Angriff an diesem Tag wurde nicht wiederholt.

    Kurzfristige Lösung


    Am Sonntag musste ich ein wenig arbeiten. Ein separates Skript überwachte bereits die Anzahl der TCP-Sitzungen und entfernte vorübergehend den Datenverkehr (d. H. Übertrug ihn in die Staaten), falls die Last erhöht wurde, wodurch der resultierende Schaden verringert wurde. Bisher haben die US-Server ohne Probleme funktioniert, aber es war dennoch notwendig, mit diesem Datenverkehr umzugehen und zu lernen, wie man ihn blockiert. Es gab keine Anomalien in den http-Protokollen, netstat und ähnlichen Dienstprogrammen zeigten auch nichts Verdächtiges. Wenn wir jedoch einen erhöhten Datenverkehr auf der Netzwerkkarte feststellen, können wir ihn mit tcpdump untersuchen.

    Das Scrollen durch Tonnen von Netzwerkpaket-Dumps kann schwierig sein, aber diesmal mussten Sie nicht lange suchen - unter den üblichen HTTP / HTTPS-Austauschen waren ungewöhnlich viele leere TCP-Pakete sichtbar, d. H. legale Pakete mit korrekten IP- und TCP-Headern, jedoch ohne Daten. Wenn HTTP deaktiviert ist, gibt es bereits zahlreiche leere Pakete - drei leere, um eine Verbindung herzustellen, dann zwei Datenaustauschpakete (Anforderungs-Antwort) und dann wieder leere Pakete, um die Verbindungen zu schließen. Darüber hinaus verfügen wir bei Verwendung von HTTPS auch über Datenpakete zum Einrichten einer TLS-Sitzung.

    Selektive Tests einzelner TCP-Sitzungen haben gezeigt, dass einige Sitzungen tatsächlich einen sehr intensiven Austausch leerer TCP-Pakete aufweisen. Fast alle diese Sitzungen kamen aus Indien! Es gab ein bisschen Saudi-Arabien mit Kuwait. Es ist schwer zu sagen, was für ein gerissenes Botnetz es ist, aber es ist noch nicht so. Ich schreibe ein zweites unkompliziertes Skript, das alle 30.000 Pakete pro Sekunde tcpdump ausführt und nach Sitzungen sucht, in denen die Anzahl aufeinanderfolgender Austausche leerer Pakete den angegebenen Grenzwert überschreitet. Die gefundenen IPs werden sofort blockiert. Das Ergebnis ließ nicht lange auf sich warten - beim Blockieren von nur fünf IP-Daten wird der Datenverkehr sofort zweimal unterbrochen. Jede Minute wurden ein oder zwei neue IPs blockiert. Sieg!

    Symptomanalyse und Problemidentifikation


    Nachdem wir diesen Fall mit Kollegen von Advanced Hosting besprochen hatten, stellte sich heraus, dass nicht alles so rosig war. Erstens nahm die Blockierungsintensität neuer IPs zu - bereits auf dem Höhepunkt des Verkehrs erreichte die Blockierungsgeschwindigkeit mehrere zehn Stück pro Minute. Zweitens waren nicht nur diese Server betroffen, sondern auch viele andere und andere Clients. Normalerweise alles in Europa und alles auf FreeBSD. Es wurde klar, dass dies kein DDOS-Angriff ist.

    Die blockierten IPs mussten freigegeben werden und anstatt zu blockieren, wurden jetzt die TCP-Sitzungen selbst gelöscht (in FreeBSD gibt es dafür ein tcpdrop-Dienstprogramm). Außerdem wurde die Last effektiv unter Kontrolle gehalten und sogar die HTTP-Keep-Alive-Funktion aktiviert.

    Auch hier müssen Sie tcpdump abholen und den Verkehr weiter betrachten. Ich werde die Stunden, die für die Suche nach Anomalien und Mustern in den Daten aufgewendet wurden, nicht im Detail beschreiben. TCP-Sitzungen waren unterschiedlich. Es gab völlig leere, aber es gab auch Datenaustausch, der dann in den Zyklus des Austauschs leerer Pakete überging.

    Aber es gab einen Hinweis. Vor dem Verlassen des leeren Paketaustauschzyklus kam ein FIN-Paket von der Remote-Seite (ein Paket mit dem FIN-Flag signalisiert, dass keine Daten mehr vorhanden sind und die Sitzung geschlossen werden muss), manchmal nicht eines, aber es ist auch ein RST-Paket aufgetreten (ein Paket mit dem RST-Flag zeigt an, dass die Sitzung bereits geschlossen und nicht mehr gültig).

    Was interessant ist, trotz des Vorhandenseins von FIN- und RST-Paketen, kam es vor, dass Datenpakete auf den Server kamen. Entweder ist der TCP-Stack irgendwo so schief implementiert, dass es unwahrscheinlich ist, oder irgendwo gibt es einen groben Eingriff in TCP-Sitzungen, aber dies ist sehr wahrscheinlich (Mobilfunkbetreiber möchten dies besonders gerne tun, ich werde nicht mit dem Finger zeigen). Die zweite Version wurde auch durch die Tatsache bestätigt, dass eine http-Protokollprüfung der erkannten schädlichen TCP-Sitzungen ergab, dass fast alle einen mobilen Browser hatten, sowohl Android als auch iOS.

    Es war logisch anzunehmen, dass das FIN- oder RST-Paket die TCP-Sitzung in einen geschlossenen Zustand versetzte, in dem der TCP-Stapel einfach den Empfang von Paketen bestätigte. Es war interessant, welcher der TCP-Staaten
    tcp_fsm.h
    #define TCP_NSTATES     11
    #define TCPS_CLOSED             0       /* closed */
    #define TCPS_LISTEN             1       /* listening for connection */
    #define TCPS_SYN_SENT           2       /* active, have sent syn */
    #define TCPS_SYN_RECEIVED       3       /* have sent and received syn */
    /* states < TCPS_ESTABLISHED are those where connections not established */
    #define TCPS_ESTABLISHED        4       /* established */
    #define TCPS_CLOSE_WAIT         5       /* rcvd fin, waiting for close */
    /* states > TCPS_CLOSE_WAIT are those where user has closed */
    #define TCPS_FIN_WAIT_1         6       /* have closed, sent fin */
    #define TCPS_CLOSING            7       /* closed xchd FIN; await FIN ACK */
    #define TCPS_LAST_ACK           8       /* had fin and close; await FIN ACK */
    /* states > TCPS_CLOSE_WAIT && < TCPS_FIN_WAIT_2 await ACK of FIN */
    #define TCPS_FIN_WAIT_2         9       /* have closed, fin is acked */
    #define TCPS_TIME_WAIT          10      /* in 2*msl quiet wait after close */
    

    So verhält es sich, und bevor ich tcpdrop aufrufe, habe ich in der Ausgabe von netstat -an eine Suche nach der gelöschten TCP-Sitzung hinzugefügt. Das Ergebnis war ein wenig entmutigend - sie wurden alle gegründet! Dies war bereits einem Fehler sehr ähnlich - eine geschlossene TCP-Sitzung kann nicht in den Status ESTABLISHED zurückkehren, diese Option wird nicht bereitgestellt. Ich begann sofort, die Quellen und Kernel zu überprüfen und wurde ein zweites Mal entmutigt:

    tp->t_state = TCPS_ESTABLISHED

    Es wird im Code genau zweimal aufgerufen, und beide Male unmittelbar davor wird der aktuelle t_state-Wert überprüft. In einem Fall entspricht er TCPS_SYN_SENT (der Server hat ein SYN-Paket gesendet und eine Bestätigung erhalten). Im zweiten Fall ist es TCPS_SYN_RECEIVED (der Server hat eine SYN empfangen, eine SYN / ACK empfangen und empfangen Bestätigung von ACK). Die Schlussfolgerung daraus ist ziemlich konkret: FIN- und RST-Pakete wurden vom Server ignoriert, und es gibt keinen Fehler im TCP-Stapel (zumindest gibt es einen Fehler mit dem falschen Übergang von einem Zustand in einen anderen).

    Es war jedoch nicht klar, warum der Server auf jedes empfangene TCP-Paket antworten sollte. Normalerweise ist dies nicht erforderlich, und der TCP-Stack funktioniert anders - er empfängt mehrere Pakete und sendet dann eine Paketbestätigung für alle gleichzeitig - dies ist wirtschaftlicher. Eine sorgfältige Untersuchung des Inhalts von Paketen, insbesondere von 32-Bit-TCP-Zählern - Sequenz (SEQ) und Bestätigung (ACK) - trug dazu bei, die Situation zu beleuchten. Das Standardverhalten von tcpdump - um den seq / ack-Unterschied zwischen Paketen anstelle von absoluten Werten anzuzeigen - hat in diesem Fall einen schlechten Dienst geleistet. Wir schauen uns die absoluten Werte genau an. Das erste Paket enthält die Sequenz 3834615051, als Antwort auf den Server wurde die Paketsequenz 1594895211 gesendet, ack 3834615052 (out-ack ging in-seq + 1). Dann kamen ein paar RST-Pakete, die für uns nicht interessant sind.

    16:03:21.931367 IP (tos 0x28, ttl 47, id 44771, offset 0, flags [DF], proto TCP (6), length 60)
    46.153.19.182.54645 > 88.208.9.111.80: Flags [S], cksum 0x181c (correct), seq 3834615051, win 65535, options [mss 1460,sackOK,TS val 932840 ecr 0,nop,wscale 6], length 0
    16:03:21.931387 IP (tos 0x0, ttl 64, id 1432, offset 0, flags [DF], proto TCP (6), length 60)
    88.208.9.111.80 > 46.153.19.182.54645: Flags [S.], cksum 0xa4bc (incorrect -> 0xf9a4), seq 1594895211, ack 3834615052, win 8192, options [mss 1460,nop,wscale 6,sackOK,TS val 2509954639 ecr 932840], length 0
    16:03:22.049434 IP (tos 0x28, ttl 47, id 44772, offset 0, flags [DF], proto TCP (6), length 52)
    46.153.19.182.54645 > 88.208.9.111.80: Flags [.], cksum 0x430b (correct), seq 3834615052, ack 1594895212, win 1369, options [nop,nop,TS val 932852 ecr 2509954639], length 0
    16:03:22.053697 IP (tos 0x28, ttl 47, id 44773, offset 0, flags [DF], proto TCP (6), length 40)
    46.153.19.182.54645 > 88.208.9.111.80: Flags [R], cksum 0x93ba (correct), seq 211128292, win 1369, length 0
    16:03:22.059913 IP (tos 0x28, ttl 48, id 0, offset 0, flags [DF], proto TCP (6), length 40)
    46.153.19.182.54645 > 88.208.9.111.80: Flags [R.], cksum 0xa03f (correct), seq 0, ack 1594897965, win 0, length 0
    16:03:22.060700 IP (tos 0x28, ttl 47, id 44774, offset 0, flags [DF], proto TCP (6), length 52)
    46.153.19.182.54645 > 88.208.9.111.80: Flags [.], cksum 0x3a48 (correct), seq 3834615953, ack 1594896512, win 1410, options [nop,nop,TS val 932853 ecr 2509954639], length 0
    16:03:22.060706 IP (tos 0x0, ttl 64, id 3974, offset 0, flags [DF], proto TCP (6), length 52)
    88.208.9.111.80 > 46.153.19.182.54645: Flags [.], cksum 0xa4b4 (incorrect -> 0x475c), seq 1594895212, ack 3834615052, win 135, options [nop,nop,TS val 2509954768 ecr 932852], length 0





    Das nächste Paket ist für uns jedoch interessant - es enthält die Nummern seq 3834615953, ack 1594896512. Beide Nummern sind erheblich größer als die anfängliche seq / ack, was bedeutet, dass die Remote-Seite bereits 3834615953-3834615052 = 901 Bytes gesendet und sogar 1594896512-1594895212 abgerufen hat = 1300 Bytes.

    Natürlich sehen und werden wir diese Datenpakete nicht sehen - dieser Austausch war mit dem MiTM-System. Aber der Server weiß das nicht. Er sieht ein Paket mit der Sequenz 3834615953 und kommt daher zu dem Schluss, dass er keine 901 Datenbytes empfangen hat, und sendet daher ein Paket mit den letzten gültigen Sequenz- / Bestätigungsnummern zurück, von denen er weiß, dass sie die Sequenz 1594895212, die Nummer 3834615052 sind. Die Gegenseite empfängt dieses Paket und wiederum berichtet, dass alles in Ordnung mit ihr ist, 1300 Bytes Daten wurden erfolgreich empfangen. Hier haben wir eine Schleife.

    Es wird auch klar, warum die US-Server diesen Verkehr nicht gesehen haben - es war tatsächlich, aber um ein Vielfaches weniger -, als der Ping von Indien nach Amerika größer war als der Ping von Indien nach Europa.

    Letzter Patch


    Es bleibt in der Tat zu finden, wie dieser Fehler behoben werden kann. Wieder nehmen wir die Quellen, der Code, an dem wir interessiert sind, befindet sich in der Datei tcp_input.c. Dies war nicht schwierig, da die Funktion tcp_input () an der primären Verarbeitung des TCP-Pakets beteiligt ist. Der Funktionsalgorithmus ist so angeordnet, dass das Paket ganz am Ende an die Funktion tcp_do_segment () übergeben wird, wenn alle Prüfungen bestanden wurden und sich die TCP-Verbindung im Status ESTABLISHED befindet.

    Es muss eine weitere Prüfung hinzugefügt werden. Wenn der Bestätigungszähler von der Remote-Seite anzeigt, dass Daten empfangen wurden, die der Server nicht gesendet hat, muss das Paket ignoriert werden. Sie können eine Verbindung nicht sofort trennen. Andernfalls bieten wir Angreifern eine einfache Möglichkeit, die TCP-Verbindungen anderer Personen zu beenden.

    Das Testen des Patches hat gezeigt, dass Pakete mit einem Bestätigungswert von Null auch im TCP-Verkehr vorhanden sind - Sie müssen sie nicht mehr ignorieren. Der letzte Patch bestand aus drei Zeilen (ohne Kommentare):
    +	if(SEQ_GT(th->th_ack, tp->snd_max) && th->th_ack != 0) {
    +		goto dropunlock;
    +	}
    

    Am selben Tag wurde ein PR (Problembericht) an FreeBSD-Entwickler gesendet .

    PS Wie ist die Situation mit Linux und Windows? Dort ist alles in Ordnung, solche Pakete werden ignoriert (getestet unter Windows 10 und Linux 3.10).

    Jetzt auch beliebt: