Rossibaer schrieb:
Mal so ein paar dumme Fragen zu den bisherigen Codes hier
Deine Fragen sind absolut berechtigt und richtig. Im Allgemeinen gilt:
- Die Länge für den Aufruf der Funktion muss der Länge der kleineren Bytefolge entsprechen sonst gibt es eine Zugriffsverletzung.
- Wenn zwei unterschiedlich lange Bytefolgen bis zur Länge der kleineren Bytefolge gleich sind, gibt die Funktion, bei Übergabe der kleineren Länge als Grenze, die Gleichheit der Bytefolge aus.
Die richtige Lösung hast du schon vorgeschlagen: Die kleinere Bytefolge muss aufgefüllt werden.
Wie füllt man aber auf, ohne nicht zufällig das "richtige" aufzufüllen?
Man muss zwischen null-terminierten Zeichenketten (also Texteingaben) und Binärdaten unterscheiden.
Da Binärdaten nicht null-terminiert sind - Null kann ja durchaus ein gültiger Wert sein - muss die Länge immer bekannt sein.
Wenn man Binärdaten auf Gleichheit testen will, sind diese für gewöhnlich auch gleich lang.
Eine Hash-Funktion kann zum Beispiel aus einer beliebig langen Bytefolge einen genau 16 Bytes langen Hash-Wert berechnen.
Möchtest du zwei Hash-Werte vergleichen, so liest du exakt 16 Bytes pro Wert ein. Mehr Bytes werden ignoriert, bei weniger Bytes gibt es einen Fehler oder man liest so lange, bis man 16 Bytes bekommt.
Den Vergleich der Hash-Werte erreichst du dann über den Aufruf von memcmp().
Binärdaten muss man also gar nicht auffüllen.
Zeichenketten sind dagegen null-terminiert. Das bedeutet, dass das erste Null-Byte das Ende der Zeichenkette markiert.
Alles was dahinter liegt, wird ignoriert. Alle C-Funktionen, die auf null-terminierten Zeichenketten arbeiten,
beenden die Abarbeitung der Zeichenkette bei Vorkommen eines Null-Bytes.
Da man im Regelfall die Länge von Texteingaben nicht kennt, nutzt man zum Vergleich zweier Zeichenketten die Funktion strcmp(), welche beim ersten Null-Byte stoppt.
Zum Auffüllen lässt sich daher das Null-Byte verweden.
In der C-Standardbibliothek sieht man zum Beispiel, dass alle Funktionen, die mit "mem" beginnen einen Längenparameter haben und alle Funktionen, die mit "str" beginnen das Null-Byte als Endemarke verwenden.
Die Frage des Thread-Erstellers war ja, wie man Eingaben vergleichen kann, sodass die Vergleiche unabhängig von der Eingabe gleich lang dauern, um Timing-Attacken zu verhindern.
Eine Lösung für memcmp() scheint deutlich leichter als eine für strcmp().
Schafft man es, zwei Zeichenketten auf quasi gleiche und bekannte Längen zu bekommen, kann man die Lösung für memcmp() für den Vergleich verwenden.
Ein Beispiel wie man das macht, zeige ich weiter unten.
asdfman schrieb:
Man muss vor dem Aufruf der Funktion die Längen vergleichen. Wenn die sich unterscheiden, muss man gar nichts mehr weiter prüfen, um zu wissen, dass die Puffer nicht identisch sind.
Wenn die Eingabe eine Bytefolge ist, deren Länge bekannt ist, hat man kein Problem.
Wenn es sich um eine Zeichenfolge handelt, kann der Angreifer erfahren, aus wie vielen Zeichen die gesuchte Zeichenkette besteht.
Als Beispiel soll ein Webserver dienen, bei dem man auf den Adminbereich zugreifen kann, indem man ein Passwort eingibt.
Dieses Passwort ist 20 Zeichen lang und soll im Klartext in der Variable "geheim" gespeichert sein (Passwörter sollten natürlich nie im Klartext gespeichert sein, sondern zum Beispiel als Hash-Wert).
Code:
char * geheim = "...";
int sizeOfGeheim = sizeof geheim; // 20
char * input = inputOfClient();
int sizeOfInput = strlen(input); // Dauer in Abhängigkeit der Eingabe
if(sizeOfInput == sizeOfGeheim) {
return cmp(geheim, input, sizeofGeheim); // Vergleichsfunktion mit konstanter Dauer
} else {
return 1;
}
Angenommen das Verarbeiten eines Bytes benötigt eine Zeiteinheit. Dann ergibt sich folgende Tabelle:
Eingabe (Zeichen) | Dauer (Zeiteinheiten) |
18 | 18 |
19 | 19 |
20 | 60 |
21 | 21 |
Bei einer Eingabe von 20 Zeichen dauert die Verarbeitung viel länger, als bei den anderen Eingaben, da in diesem Fall cmp() aufgerufen wird.
Ein Angreifer erfährt also, dass das Passwort 20 Zeichen lang sein muss und kann zum Beispiel eine Wörterbuchattacke mit passenden Wörtern starten.
Durch die Verwendung von cmp() statt strcmp() wird die Timing-Attacke sogar verstärkt, da über die vollen 20 Bytes verglichen wird. strcmp() würde nach dem ersten falschen Zeichen beenden.
Der gesamte Code würde mit strcmp() zum Beispiel nur 22 Zeiteinheiten benötigen.
Eine Lösung ist folgende:
Code:
// Vergleichsfunktion aus meinem vorherigen Beitrag
int cmp(const void * a, const void * b, size_t size) {
const char * aa = a; const char * bb = b;
int result = 0;
size_t i;
for(i = 0; i < size; ++i) {
result |= aa[i] ^ bb[i];
}
return result;
}
#define MAX_SIZE 100
static char password[MAX_SIZE];
void setPassword(const char * text) {
strncpy(password, text, MAX_SIZE);
}
int testPassword(const char * text) {
char buffer[MAX_SIZE];
strncpy(buffer, text, MAX_SIZE);
return cmp(password, buffer, MAX_SIZE);
}
In der Variable "password" wird das Passwort über setPassword() einmalig gesetzt.
Die maximale Länge von Passwörtern wird auf 100 Zeichen beschränkt.
Es ist sehr sinnvoll Eingaben zu beschränken. Auch Passwörter die man nur als Hash-Wert speichert, sollten in der Eingabe begrenzt sein. Vor ein paar Wochen war ein CMS in den News, welches Passwörter zwar hashte aber keine Begrenzung in der Länge vornahm. Dadurch konnte das CMS per DOS-Angriff lahmgelegt werden, indem man Passwörter mit einer Länge von 100 bis 1000 MB übergab und der Server somit mit Hashen beschäftigt war.
Über testPassword() kann dann ein beliebiges Passwort gegen das geheime Passwort getestet werden.
Warum der Code funktioniert, liegt an der Funktionsweise von str
ncpy().
Die Funktion schreibt immer MAX_SIZE viele Zeichen. Ist die Eingabe kürzer, füllt die Funktion den Rest mit Null-Bytes auf.
Dadurch kann cmp() über die Länge der Arrays (also MAX_SIZE) vergleichen.
ACHTUNG: Ist die Eingabe 100 Zeichen oder länger, wird der Rest zwar abgeschnitten, strncpy() setzt aber kein Null-Byte.
Dadurch sind password[] und buffer[] keine null-terminierten Zeichenketten und sollten außerhalb der zwei Funktionen nicht verwendet werden.
Der Vergleich ist aber kein Problem, da wir in cmp() über die bekannte Länge der Arrays vergleichen.
Da kein Null-Byte geschrieben wird, ist auch meine Aussage richtig, dass ein Passwort maximal 100 Zeichen lang sein kann.