Node.js-Event-Loop für Timing-Angriffe nutzen
16. Februar 2016
0 Min. LesezeitVor etwas mehr als drei Jahren gründeten ein paar Freunde und ich eine Gruppe namens pasten, um am Chaos Computer Club Capture-the-Flag-Wettbewerb (CTF) teilzunehmen. Dabei handelt es sich um ein Jeopardy-CTF, bei dem die teilnehmenden Teams sicherheitsbezogene Aufgaben aus verschiedenen Kategorien wie Exploitation, Reverse Engineering, Web, Forensik und Kryptografie lösen müssen. CTFs machen Spaß und sind lehrreich, und ich kann die Teilnahme daran nur empfehlen. Noch mehr Spaß macht es, wenn man gewinnt – so wie wir dieses Jahr!
Der diesjährige Wettbewerb umfasste neben anderen Aufgaben eine Node.js-Anwendung, die für einen interessanten Timing-Angriff anfällig war, bei dem der Node.js-spezifische Event-Loop ausgenutzt wurde. In diesem Beitrag gehen wir die Aufgabe durch, erklären das Risiko und zeigen, wie Angreifer eine solche Sicherheitslücke aufdecken und ausnutzen können. Hoffentlich hilft Ihnen das auch dabei, ähnliche Schwachstellen in Ihrem eigenen Code zu vermeiden.
Timing-Angriffe
Bevor wir uns die Aufgabe genauer ansehen, erklären wir zunächst, was ein Timing-Angriff ist.
Stellen Sie sich einen Dienst vor, der bei einem falschen Passwort zu einer existierenden E-Mail-Adresse antwortet: „Der fünfte Buchstabe Ihres Passworts ist falsch. Bitte versuchen Sie es erneut.“ Klingt absurd, oder? Solche Informationen ermöglichen es Angreifern, das Passwort Zeichen für Zeichen durch Ausprobieren zu knacken – ein Einbruch wird damit zum Kinderspiel. Genau das passiert jedoch, wenn wir Passwörter oder Authentifizierungstoken mit einem naiven Zeichenkettenvergleich überprüfen.
Ein nativer Zeichenkettenvergleich – auch mit dem JavaScript-Operator == – durchläuft in der Regel zwei Zeichenketten gleicher Länge, vergleicht die Zeichen nacheinander und bricht beim ersten abweichenden Zeichen ab. Vergleiche ich also foo mit bar, wird die Schleife einmal durchlaufen. Beim Vergleich von foo mit fox werden dagegen drei Zeichen verglichen, was länger dauert.
Beispiel für eine anfällige Authentifizierungsfunktion:
Der Vergleich eines einzelnen Zeichens geht ziemlich schnell, aber langsam genug. Untersuchungen zeigen, dass Angreifer Ereignisse über das Internet mit einer Genauigkeit von 15–100 µs und in einem lokalen Netzwerk mit 100 ns Genauigkeit messen können. Angreifer können diese Techniken nutzen: Schon geringe Verzögerungen verraten dem Benutzer beinahe, welches Zeichen falsch war.
Diese Art von Angriff wird Timing-Angriff genannt und ist immer dann möglich, wenn eine Eingabe die Verarbeitungszeit beeinflusst. Um das zu verhindern, muss der Zeichenkettenvergleich unabhängig vom eingegebenen Passwort immer gleich lange dauern – zum Beispiel, indem wir die beiden Passwörter mit xoring verknüpfen und prüfen, ob das Ergebnis null ist.
Nicht anfällig für Timing-Angriffe:
Die Aufgabe: Sequence Hunt
Mit diesem Wissen können wir uns nun der Aufgabe widmen. Sie begann mit einem Link zu einer Seite mit folgendem Inhalt:
Sequence HuntWillkommen bei meiner Suche nach ganzzahligen Folgen. Sie müssen fünf Zahlen zwischen 0 und 100 berechnen und dabei die folgenden Algorithmen implementieren. Wenn Sie glauben, eine richtige Lösung gefunden zu haben, senden Sie sie hier als GET-Parameter.Sie können hier auch Informationen über sich abrufen.Ich rechne mit einer hohen Auslastung und habe das Ganze daher auf asynchronen IOLoops aufgebaut.Viel Spaß!
[EDIT]Jemand hat mir das hier geschickt. Da jede Überprüfung eine gewisse Zeit in Anspruch nimmt, wartet der Server nun nach Eingang der Anfrage drei Sekunden, bevor er antwortet, um diese Angriffe zu verhindern.[/EDIT]
AlgorithmenNOCH ZU ERLEDIGEN: Algorithmen veröffentlichen
Der Text hier führt zu /check, an das wir die richtigen Parameter senden müssen, um die Flag zu erhalten. Über IOLoops gelangen wir zu /info. Dort wird Folgendes angezeigt:
Ihre IP-Adresse lautet 37.120.106.140Anzahl der Anfragen in Ihrem IOLoop: 1
Was wissen wir bisher?
Es gibt einen unbekannten Algorithmus, der fünf Zahlen zwischen 0 und 100 verarbeitet.
Um die Flag zu erhalten, müssen wir die richtigen Zahlen angeben.
Die Ausführungszeit des Algorithmus hängt wahrscheinlich von der Eingabe ab und könnte so einen Timing-Angriff ermöglichen.
Der Server wartet immer mindestens drei Sekunden, bevor er antwortet, um Timing-Angriffe zu erschweren.
Jetzt müssen wir herausfinden, wie wir die Flag erbeuten können. Da die Aufgabe Sequence Hunt hieß, verwende ich in meinen Beispielen sequencehunt.com als Platzhalter-Domain. Führen Sie jedoch keine Angriffe auf diese (nicht damit verbundene) Domain aus! Sie können die anfällige Beispielanwendung ausführen, die ich geschrieben habe. Sie sollte sich ähnlich wie die CTF-Anwendung verhalten.
Untersuchen
Ein kurzer Test bestätigt, dass eine check-Anfrage unabhängig von den übermittelten Werten etwa drei Sekunden bis zur Antwort benötigt. Damit scheidet ein Durchprobieren aller Kombinationen aus. Dafür müssten 100^5 (10 Milliarden) Kombinationen ausprobiert werden – das würde Tausende von Jahren dauern …
Wenn wir mit der Seite /info experimentieren, stellen wir fest, dass jede /check-Anfrage den IOLoop-Zähler erhöht. Dasselbe gilt für die /info-Anfrage selbst.
Anfragen an /check mit verschiedenen Zufallszahlen ergeben keine statistisch signifikanten Zeitunterschiede. Der Algorithmus braucht offenbar weniger als drei Sekunden; daher werden alle Anfragen erst nach der festgelegten Mindestwartezeit von drei Sekunden beantwortet.
Wir haben jedoch festgestellt, dass eine Anfrage an /info deutlich länger braucht, wenn gleichzeitig noch eine /check-Anfrage läuft. Damit haben wir einen Ansatzpunkt für weitere Untersuchungen. Um den nächsten Schritt zu verstehen, sehen wir uns zunächst den Event-Loop von Node.js genauer an.
Node.js-Event-Loop
Node.js wurde mit Blick auf Skalierbarkeit entwickelt und ist – wie JavaScript im Allgemeinen – ein asynchrones, ereignisgesteuertes Framework. Wenn ein Codeabschnitt eine potenziell blockierende Aktion ausführen soll, etwa eine Datei zu öffnen oder Daten über das Netzwerk zu senden, registriert er eine Callback-Funktion, löst die entsprechende Aktion aus und wird beendet. Sobald die Aktion abgeschlossen ist (zum Beispiel die Datei geöffnet wurde), ruft der Event-Loop das Callback-Ereignis auf.
Das ereignisgesteuerte Modell ist äußerst skalierbar, da Threads nie untätig auf etwas warten. Es bringt jedoch ein Problem mit sich, wenn eine Funktion lange läuft. Da der Node.js-Server standardmäßig nur einen Thread pro Kern ausführt, beansprucht eine solche Funktion den gesamten Kern, während andere auf ihren Einsatz warten. Im Browser ist das der Grund für die Fehlermeldung „Skript benötigt zu lange für die Ausführung“, die Ihnen vielleicht schon einmal begegnet ist. Auf dem Server bleiben Anfragen dadurch einfach hängen.
Um den Node.js-Event-Loop besser zu verstehen, sehen Sie sich den hervorragenden Vortrag an, den Philip Roberts auf der JSConf gehalten hat, oder lesen Sie einen dieser Artikel.
Das Rätsel lösen
In unserem Fall lieferte uns der Event-Loop die benötigten Zeitinformationen. Solange /check ausgeführt wurde, erhielt der Event-Loop die Kontrolle nicht zurück, wodurch die Anfrage an /info hängen blieb. Im Gegensatz dazu registriert die Funktion setTimeout lediglich ein geplantes Ereignis und gibt die Kontrolle ab. So können Anfragen an /info schnell verarbeitet werden. Den Code für einen einfachen Server, der diesen Effekt veranschaulicht, habe ich auf GitHub bereitgestellt, falls Sie ihn lokal ausprobieren möchten.
Nachdem wir nun eine Möglichkeit hatten, Zeitinformationen zu erhalten (ein sogenannter Side-Channel), mussten wir nur noch eine Anfrage an /check senden und – ohne auf die Antwort zu warten – eine Anfrage an /info schicken und messen, wie lange es bis zur Antwort dauert.

Um die Suche zu automatisieren und Netzwerk-Jitter auszugleichen, bestand der nächste Schritt darin, ein Python-Skript zu schreiben, das bei gegebenen Eingabewerten:
nThreads öffnetEine
check-Anfrage sendet und misst, wie lange eine parallele/info-Anfrage bis zur Antwort brauchtDie Zeiten mittelt und das Ergebnis zurückgibt
Ich verwendete n=5, was sich als ausreichend erwies. Je nach Qualität der Internetverbindung und den zeitlichen Eigenschaften des Algorithmus kann ein höherer Wert nötig sein. Hier sehen Sie die Ergebnisse des ersten Durchlaufs – beachten Sie den deutlichen Unterschied beim Wert 4.
Da der Zeitunterschied nur auftrat, wenn 4 an erster Stelle stand, bestand der nächste Schritt darin, die übrigen Parameter durchzuprobieren. Als zweite Ziffer wurde 12 ermittelt.
Und so weiter, bis wir alle richtigen Parameter ermittelt hatten: [4, 12, 77, 98, 35]
Flag erbeutet.
Zusammenfassung
Timing-Angriffe sind eine echte Bedrohung und betreffen kleine wie große Akteure (wie Google). Selbst geringe Unterschiede bei der Ausführungszeit lassen sich mit einer relativ kleinen Stichprobe (im Maßstab des Internets) erkennen. Angreifer können diese nutzen, um Informationen zu gewinnen und Brute-Force-Angriffe gezielter durchzuführen. Bei Node.js kann der Event-Loop als weiteres Signal für Timing-Angriffe dienen.
Ein paar Tipps, wie Sie Ihre Anwendung vor solchen Schwachstellen schützen:
Achten Sie bei der Implementierung von Authentifizierungsprüfungen darauf, dass der Authentifizierungsprozess immer gleich lange dauert. Für solche konstanten Vergleiche können Sie secure-compare oder andere Pakete verwenden.
Prüfen Sie unbedingt, ob diese bekannte Schwachstellen aufweisen.
Wenn Sie schließlich Ihre eigenen Fähigkeiten unter Beweis stellen und sich diesen Herausforderungen stellen möchten, nehmen Sie nächstes Jahr selbst am Capture-the-Flag-Wettbewerb des CCC teil!
Starten Sie mit Capture-the-Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Challenges lösen.


