Skip to main content

Node.js-Event-Loop für Timing-Angriffe nutzen

Artikel von

16. Februar 2016

0 Min. Lesezeit

Vor 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:

function isAuthenticated(user, token) {
  var correctToken = FetchUserTokenFromDB(user);
  return token === correctToken;
}

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:

var mismatch = 0;
for (var i = 0; i < a.length; ++i) {
  mismatch |= (a.charCodeAt(i) ^ b.charCodeAt(i));
}
return mismatch;

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?

  1. Es gibt einen unbekannten Algorithmus, der fünf Zahlen zwischen 0 und 100 verarbeitet.

  2. Um die Flag zu erhalten, müssen wir die richtigen Zahlen angeben.

  3. Die Ausführungszeit des Algorithmus hängt wahrscheinlich von der Eingabe ab und könnte so einen Timing-Angriff ermöglichen.

  4. 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 …

$ curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140
At least one value is wrong!

$ time curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140
At least one value is wrong!
0.00s user
0.01s system
0% cpu
3.174 total

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.

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 1

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 2

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 3

$ curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140<br>At least one value is wrong!

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 5

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.

Sequenzdiagramm, das zeigt, wie ein Angreifer Prüfanforderungen und Antworten zeitlich überwacht, während die Ereignisschleife des Servers blockiert ist und setTimeout die Antwort verzögert.

Um die Suche zu automatisieren und Netzwerk-Jitter auszugleichen, bestand der nächste Schritt darin, ein Python-Skript zu schreiben, das bei gegebenen Eingabewerten:

  • n Threads öffnet

  • Eine check-Anfrage sendet und misst, wie lange eine parallele /info-Anfrage bis zur Antwort braucht

  • Die 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.

[0, 0, 0, 0, 0]: 0.4933500289916992
[1, 1, 1, 1, 1]: 0.3603970050811768
[2, 2, 2, 2, 2]: 0.4297104358673095
[3, 3, 3, 3, 3]: 0.4705570697784423
[4, 4, 4, 4, 4]: 0.9154952526092529 <<<<<<<<
[5, 5, 5, 5, 5]: 0.4637355804443359
[6, 6, 6, 6, 6]: 0.3557830333709716
[7, 7, 7, 7, 7]: 0.4418128013610840
[8, 8, 8, 8, 8]: 0.4297045707702637

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.

[4, 10, 10, 10, 10]: 0.840841388702392
[4, 11, 11, 11, 11]: 0.859339499473571
[4, 12, 12, 12, 12]: 1.228700304031372 <<<<<<<<
[4, 13, 13, 13, 13]: 0.907831811904907

Und so weiter, bis wir alle richtigen Parameter ermittelt hatten: [4, 12, 77, 98, 35]

$ curl 'https://sequencehunt.com/check?val0=4&val1=12&val2=77&val3=98&val4=35'
you are 37.120.106.140
Congratulations, here is your prize:
32C3_round_and_round_and_round_IT_goes

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.

  • Testen Sie auf Timing-Schwachstellen – mit Tools.

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.

Weiterlesen

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

Blog

Ihr Schwachstellen-Backlog ist keine technische Schuld mehr, sondern eine Angriffsfläche

Ein wachsender Schwachstellen-Backlog ist mehr als eine technische Altlast: Er ist eine Angriffsfläche. Erfahren Sie, warum veraltete Risikoeinschätzungen, automatisierte Angreifer und verkettete Findings einen neuen Ansatz erfordern.