Warum ist printf mit einem einzelnen Argument (ohne Konvertierungsspezifizierer) veraltet?


102

In einem Buch, das ich lese, steht, dass printfmit einem einzigen Argument (ohne Konvertierungsspezifizierer) veraltet ist. Es wird empfohlen, zu ersetzen

printf("Hello World!");

mit

puts("Hello World!");

oder

printf("%s", "Hello World!");

Kann mir jemand sagen warum printf("Hello World!");das falsch ist? In dem Buch steht, dass es Schwachstellen enthält. Was sind diese Schwachstellen?


34
Hinweis: printf("Hello World!")ist nicht dasselbe wie puts("Hello World!"). puts()fügt a hinzu '\n'. Vergleichen Sie stattdessen printf("abc")mitfputs("abc", stdout)
chux

5
Was ist das für ein Buch? Ich denke nicht, dass printfes auf die gleiche Weise getsveraltet ist , wie es beispielsweise in C99 veraltet ist. Sie können daher in Betracht ziehen, Ihre Frage genauer zu bearbeiten.
el.pescado

14
Es hört sich so an, als ob das Buch, das Sie lesen, nicht sehr gut ist - ein gutes Buch sollte nicht einfach sagen, dass so etwas "veraltet" ist (das ist sachlich falsch, es sei denn, der Autor verwendet das Wort, um seine eigene Meinung zu beschreiben) und sollte erklären, welche Verwendung ist tatsächlich ungültig und gefährlich, anstatt sicheren / gültigen Code als Beispiel für etwas anzuzeigen, das Sie "nicht tun sollten".
R .. GitHub STOP HELPING ICE

8
Können Sie das Buch identifizieren?
Keith Thompson

7
Bitte geben Sie den Titel des Buches, den Autor und die Seitenreferenz an. Vielen Dank.
Greenonline

Antworten:


122

printf("Hello World!"); ist meiner Meinung nach nicht verwundbar, aber bedenken Sie Folgendes:

const char *str;
...
printf(str);

Wenn Sie strzufällig auf eine Zeichenfolge mit Formatspezifizierern verweisen %s, zeigt Ihr Programm ein undefiniertes Verhalten (meistens ein Absturz), während puts(str)die Zeichenfolge nur so angezeigt wird, wie sie ist.

Beispiel:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"

21
Neben dem Absturz des Programms sind mit Formatzeichenfolgen viele andere Exploits möglich. Weitere Informationen finden Sie hier: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan

9
Ein weiterer Grund ist, dass putsdies vermutlich schneller sein wird.
Edmz

38
@black: putsist "vermutlich" schneller, und dies ist wahrscheinlich ein weiterer Grund, warum die Leute es empfehlen, aber es ist nicht wirklich schneller. Ich habe gerade "Hello, world!"1.000.000 Mal gedruckt , in beide Richtungen. Damit printfdauerte es 0,92 Sekunden. Damit putsdauerte es 0,93 Sekunden. Es gibt Dinge, über die man sich Sorgen machen muss, wenn es um Effizienz geht, aber printfvs. putsgehört nicht dazu.
Steve Summit

10
@KonstantinWeitz: Aber (a) ich habe gcc nicht verwendet und (b) es spielt keine Rolle, warum die Behauptung " putsist schneller" falsch ist, sie ist immer noch falsch.
Steve Summit

6
@KonstantinWeitz: Die Behauptung, für die ich Beweise vorgelegt habe, war (das Gegenteil von) die Behauptung, die der Benutzer Schwarz gemacht hat. Ich versuche nur zu klären, dass Programmierer sich keine Sorgen machen sollten, putsaus diesem Grund anzurufen. (Aber wenn Sie darüber streiten wollten: Ich wäre überrascht, wenn Sie einen modernen Compiler für eine moderne Maschine finden könnten, der putsdeutlich schneller ist als printfunter allen Umständen.)
Steve Summit

75

printf("Hello world");

ist in Ordnung und hat keine Sicherheitslücke.

Das Problem liegt bei:

printf(p);

Dabei phandelt es sich um einen Zeiger auf eine Eingabe, die vom Benutzer gesteuert wird. Es ist anfällig für Formatierungsangriffe : Benutzer können Konvertierungsspezifikationen einfügen, um die Kontrolle über das Programm zu übernehmen, z. B. %xum Speicher zu sichern oder %num Speicher zu überschreiben.

Beachten Sie, dass das puts("Hello world")Verhalten nicht gleichbedeutend ist mit, printf("Hello world")sondern mit printf("Hello world\n"). Compiler sind normalerweise intelligent genug, um den letzteren Aufruf zu optimieren und durch ihn zu ersetzen puts.


10
printf(p,x)Genauso problematisch wäre es natürlich , wenn der Benutzer die Kontrolle darüber hätte p. Das Problem ist also nicht die Verwendung printfmit nur einem Argument, sondern mit einer benutzergesteuerten Formatzeichenfolge.
Hagen von Eitzen

2
@HagenvonEitzen Das ist technisch gesehen richtig, aber nur wenige würden absichtlich eine vom Benutzer bereitgestellte Formatzeichenfolge verwenden. Wenn Leute schreiben printf(p), weil sie nicht erkennen, dass es sich um eine Formatzeichenfolge handelt, denken sie nur, dass sie ein Literal drucken.
Barmar

33

Neben den anderen Antworten printf("Hello world! I am 50% happy today")ist ein Fehler leicht zu machen, der möglicherweise alle möglichen unangenehmen Speicherprobleme verursacht (es ist UB!).

Es ist einfach, einfacher und robuster, von Programmierern zu verlangen, dass sie absolut klar sind, wenn sie eine wörtliche Zeichenfolge und nichts anderes wollen .

Und das printf("%s", "Hello world! I am 50% happy today")bringt dich. Es ist völlig kinderleicht.

(Steve ist natürlich printf("He has %d cherries\n", ncherries)absolut nicht dasselbe. In diesem Fall ist die Programmiererin nicht in der Einstellung "wörtliche Zeichenfolge"; sie ist in der Einstellung "Formatzeichenfolge".)


2
Dies ist kein Argument wert, und ich verstehe, was Sie über die wörtliche und die Format-String-Denkweise sagen, aber nicht jeder denkt so, was ein Grund dafür ist, dass One-Size-Fits-All-Regeln rangeln können. Das Sprichwort "Niemals konstante Zeichenfolgen drucken mit printf" ist genau das Gleiche wie "Immer schreiben" if(NULL == p). Diese Regeln können für einige Programmierer nützlich sein, aber nicht für alle. In beiden Fällen (nicht übereinstimmende printfFormate und Yoda-Bedingungen) warnen moderne Compiler ohnehin vor Fehlern. Daher sind die künstlichen Regeln noch weniger wichtig.
Steve Summit

1
@Steve Wenn die Verwendung von etwas genau null Vorteile hat, aber einige Nachteile, dann gibt es wirklich keinen Grund, es zu verwenden. Yoda Bedingungen auf der anderen Seite do den Nachteil haben , dass sie den Code schwerer zu lesen ist (man intuitiv würde sagen , „wenn p Null“ nicht „ wenn Null ist p“).
Voo

2
@Voo printf("%s", "hello")wird langsamer sein als printf("hello"), also gibt es einen Nachteil. Ein kleines, weil IO fast immer viel langsamer ist als solch eine einfache Formatierung, aber ein Nachteil.
Yakk - Adam Nevraumont

1
@ Yakk Ich bezweifle, dass das langsamer sein würde
MM

gcc -Wall -W -Werrorverhindert schlimme Konsequenzen aus solchen Fehlern.
Chqrlie

17

Ich werde hier nur ein paar Informationen zum Schwachstellenteil hinzufügen .

Es soll wegen der Sicherheitslücke im Format printf string format anfällig sein. In Ihrem Beispiel ist die Zeichenfolge, in der die Zeichenfolge fest codiert ist, harmlos (selbst wenn eine solche Festcodierung von Zeichenfolgen niemals vollständig empfohlen wird). Die Angabe der Parametertypen ist jedoch eine gute Angewohnheit. Nehmen Sie dieses Beispiel:

Wenn jemand anstelle einer regulären Zeichenfolge ein Formatzeichenfolgenzeichen in Ihre printf einfügt (z. B. wenn Sie das Programm stdin drucken möchten), nimmt printf alles, was er kann, auf den Stapel.

Es wurde (und wird) sehr häufig verwendet, um Programme zum Erkunden von Stapeln auszunutzen, um beispielsweise auf verborgene Informationen zuzugreifen oder die Authentifizierung zu umgehen.

Beispiel (C):

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

wenn ich als Eingabe dieses Programms setze "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

Dies weist die printf-Funktion an, fünf Parameter vom Stapel abzurufen und als 8-stellige aufgefüllte Hexadezimalzahlen anzuzeigen. Eine mögliche Ausgabe könnte also so aussehen:

40012980 080628c4 bffff7a4 00000005 08059c04

Sehen Sie dies für eine vollständigere Erklärung und andere Beispiele.


13

Das Aufrufen printfmit Zeichenfolgen im Literalformat ist sicher und effizient, und es gibt Tools, die Sie automatisch warnen, wenn Ihr Aufruf printfmit vom Benutzer angegebenen Formatzeichenfolgen nicht sicher ist.

Die schwersten Angriffe printfnutzen den %nFormatbezeichner. Im Gegensatz zu allen anderen Formatbezeich zB %d, %nschreibt eigentlich einen Wert in einer Speicheradresse in eines der Format Argumente zur Verfügung gestellt. Dies bedeutet, dass ein Angreifer den Speicher überschreiben und somit möglicherweise die Kontrolle über Ihr Programm übernehmen kann. Wikipedia bietet mehr Details.

Wenn Sie printfmit einer Literalformatzeichenfolge aufrufen , kann sich ein Angreifer nicht %nin Ihre Formatzeichenfolge einschleichen , und Sie sind somit sicher. Tatsächlich ändert gcc Ihren Anruf printfin einen Anruf in puts, sodass es keinen Unterschied gibt (testen Sie dies durch Ausführen gcc -O3 -S).

Wenn Sie printfmit einer vom Benutzer angegebenen Formatzeichenfolge aufrufen , kann sich ein Angreifer möglicherweise %nin Ihre Formatzeichenfolge einschleichen und die Kontrolle über Ihr Programm übernehmen. Ihr Compiler wird Sie normalerweise warnen, dass sein Computer unsicher ist -Wformat-security. Es gibt auch erweiterte Tools, die sicherstellen, dass ein Aufruf von printfauch mit vom Benutzer bereitgestellten Formatzeichenfolgen sicher ist, und sie können sogar überprüfen, ob Sie die richtige Anzahl und Art von Argumenten an übergeben printf. Für Java gibt es beispielsweise Google's Error Prone und das Checker Framework .


12

Dies ist ein fehlgeleiteter Rat. Ja, wenn Sie eine Laufzeitzeichenfolge zum Drucken haben,

printf(str);

ist ziemlich gefährlich, und Sie sollten immer verwenden

printf("%s", str);

stattdessen, weil man im Allgemeinen nie wissen kann, ob strein %Zeichen enthalten sein könnte . Wenn Sie jedoch eine konstante Zeichenfolge zur Kompilierungszeit haben , ist daran überhaupt nichts auszusetzen

printf("Hello, world!\n");

(Unter anderem ist dies das klassischste C-Programm aller Zeiten, buchstäblich aus dem C-Programmierbuch von Genesis. Jeder, der diese Verwendung ablehnt, ist also eher ketzerisch, und ich jedenfalls wäre etwas beleidigt!)


because printf's first argument is always a constant stringIch bin mir nicht ganz sicher, was du damit meinst.
Sebastian Mach

Wie gesagt, "He has %d cherries\n"ist eine konstante Zeichenfolge, was bedeutet, dass es sich um eine Konstante zur Kompilierungszeit handelt. Um fair zu sein, lautete der Rat des Autors nicht "Übergeben Sie keine konstanten Zeichenfolgen als printferstes Argument", sondern "Übergeben Sie keine Zeichenfolgen ohne %als printferstes Argument".
Steve Summit

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- Sie haben K & R in den letzten Jahren nicht wirklich gelesen. Es gibt eine Menge Ratschläge und Codierungsstile, die heutzutage nicht nur veraltet, sondern einfach nur schlecht sind.
Voo

@Voo: Nun, sagen wir einfach, dass nicht alles, was als schlechte Praxis angesehen wird, tatsächlich schlechte Praxis ist. (Der Ratschlag, "niemals einfache int" zu verwenden, kommt mir in den Sinn.)
Steve Summit

1
@Steve Ich habe keine Ahnung, wo du das gehört hast, aber das ist sicherlich nicht die Art von schlechter (schlechter?) Praxis, über die wir dort sprechen. Verstehen Sie mich nicht falsch, für die Zeit war der Code vollkommen in Ordnung, aber Sie möchten k & r wirklich nicht viel, sondern heutzutage als historische Notiz betrachten. "It's in k & r" ist heutzutage einfach kein Indikator für gute Qualität, das ist alles
Voo

9

Ein ziemlich unangenehmer Aspekt printfist, dass selbst auf Plattformen, auf denen das Lesen des Streuspeichers nur begrenzten (und akzeptablen) Schaden verursachen kann, eines der Formatierungszeichen %nbewirkt, dass das nächste Argument als Zeiger auf eine beschreibbare Ganzzahl interpretiert wird, und das Anzahl der bisher ausgegebenen Zeichen, die in der dadurch identifizierten Variablen gespeichert werden sollen. Ich habe diese Funktion selbst nie verwendet, und manchmal verwende ich einfache Methoden im Printf-Stil, die ich so geschrieben habe, dass sie nur die Funktionen enthalten, die ich tatsächlich verwende (und die eine oder ähnliche nicht enthalten), aber die empfangenen Standard-Printf-Funktionszeichenfolgen einspeisen aus nicht vertrauenswürdigen Quellen können Sicherheitslücken aufdecken, die über das Lesen von beliebigem Speicher hinausgehen.


8

Da niemand erwähnt hat, möchte ich einen Hinweis zu ihrer Leistung hinzufügen.

Unter normalen Umständen würde ich unter der Annahme, dass keine Compiler-Optimierungen verwendet werden (dh printf()tatsächlich Aufrufe printf()und nicht fputs()), printf()eine weniger effiziente Leistung erwarten , insbesondere bei langen Zeichenfolgen. Dies liegt daran printf(), dass die Zeichenfolge analysiert werden muss, um zu überprüfen, ob Konvertierungsspezifizierer vorhanden sind.

Um dies zu bestätigen, habe ich einige Tests durchgeführt. Der Test wird unter Ubuntu 14.04 mit gcc 4.8.4 durchgeführt. Mein Computer verwendet eine Intel i5-CPU. Das getestete Programm lautet wie folgt:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

Beide sind kompiliert mit gcc -Wall -O0. Die Zeit wird mit gemessen time ./a.out > /dev/null. Das Folgende ist das Ergebnis eines typischen Laufs (ich habe sie fünf Mal ausgeführt, alle Ergebnisse liegen innerhalb von 0,002 Sekunden).

Für die printf()Variante:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

Für die fputs()Variante:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

Dieser Effekt wird verstärkt, wenn Sie eine sehr lange Saite haben.

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

Für die printf()Variante (dreimal gelaufen, echtes Plus / Minus 1,5s):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

Für die fputs()Variante (dreimal ausgeführt, real plus / minus 0,2 s):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

Hinweis: Nachdem ich die von gcc generierte Assembly überprüft hatte, stellte ich fest, dass gcc den fputs()Aufruf eines fwrite()Aufrufs auch mit optimiert -O0. (Der printf()Aufruf bleibt unverändert.) Ich bin nicht sicher, ob dies meinen Test ungültig macht, da der Compiler die Zeichenfolgenlänge zur fwrite()Kompilierungszeit berechnet .


2
Ihr Test wird dadurch nicht ungültig, wie er fputs()häufig bei Zeichenfolgenkonstanten verwendet wird, und diese Optimierungsmöglichkeit ist Teil des Punktes, den Sie ansprechen möchten. Das heißt, das Hinzufügen eines Testlaufs mit einer dynamisch generierten Zeichenfolge mit fputs()und fprintf()wäre ein netter zusätzlicher Datenpunkt .
Patrick Schlüter

@ PatrickSchlüter Das Testen mit dynamisch generierten Strings scheint den Zweck dieser Frage zu vereiteln ... OP scheint nur an String-Literalen interessiert zu sein, die gedruckt werden sollen.
user12205

1
Er gibt es nicht explizit an, selbst wenn sein Beispiel String-Literale verwendet. Tatsächlich denke ich, dass seine Verwirrung über den Rat des Buches auf die Verwendung von String-Literalen im Beispiel zurückzuführen ist. Bei String-Literalen ist der Buchratschlag irgendwie zweifelhaft, bei dynamischen Strings ist er ein guter Rat.
Patrick Schlüter

1
/dev/nullDies macht dies zu einem Spielzeug. Normalerweise besteht Ihr Ziel beim Generieren einer formatierten Ausgabe darin, dass die Ausgabe irgendwohin geht und nicht verworfen wird. Wie vergleichen sie die Zeit, wenn Sie die Zeit hinzufügen, in der die Daten tatsächlich nicht verworfen werden?
Yakk - Adam Nevraumont

7
printf("Hello World\n")

Kompiliert automatisch auf das Äquivalent

puts("Hello World")

Sie können dies überprüfen, indem Sie Ihre ausführbare Datei zerlegen:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

mit

char *variable;
... 
printf(variable)

wird zu Sicherheitsproblemen führen, verwenden Sie printf niemals so!

Ihr Buch ist also tatsächlich korrekt. Die Verwendung von printf mit einer Variablen ist veraltet. Sie können jedoch weiterhin printf ("my string \ n") verwenden, da es automatisch zu Puts wird


12
Dieses Verhalten hängt tatsächlich vollständig vom Compiler ab.
Jabberwocky

6
Das ist irreführend. Sie sagen A compiles to B, in Wirklichkeit aber Sie bedeuten A and B compile to C.
Sebastian Mach

6

Für gcc ist es möglich, bestimmte Warnungen zur Überprüfung printf()und zu aktivieren scanf().

In der gcc-Dokumentation heißt es:

-Wformatist enthalten in -Wall. Für mehr Kontrolle über einige Aspekte der Formatprüfung, die Optionen -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, und -Wformat=2stehen zur Verfügung, sind aber nicht in enthalten -Wall.

Das, -Wformatwas in der -WallOption aktiviert ist, aktiviert nicht mehrere spezielle Warnungen, die beim Auffinden dieser Fälle helfen:

  • -Wformat-nonliteral wird warnen, wenn Sie keinen String litteral als Formatbezeichner übergeben.
  • -Wformat-securitywird warnen, wenn Sie eine Zeichenfolge übergeben, die möglicherweise ein gefährliches Konstrukt enthält. Es ist eine Teilmenge von -Wformat-nonliteral.

Ich muss zugeben, dass das Aktivieren -Wformat-securitymehrere Fehler in unserer Codebasis aufgedeckt hat (Protokollierungsmodul, Fehlerbehandlungsmodul, XML-Ausgabemodul). Alle hatten einige Funktionen, die undefinierte Dinge tun konnten, wenn sie mit% Zeichen in ihrem Parameter aufgerufen wurden. Unsere Codebasis ist jetzt ungefähr 20 Jahre alt und selbst wenn wir uns dieser Art von Problemen bewusst waren, waren wir äußerst überrascht, als wir diese Warnungen aktivierten, wie viele dieser Fehler sich noch in der Codebasis befanden.


1

Neben den anderen gut erläuterten Antworten mit allen Nebenanliegen möchte ich eine präzise und präzise Antwort auf die gestellte Frage geben.


Warum ist printfein einzelnes Argument (ohne Konvertierungsspezifizierer) veraltet?

Ein printfFunktionsaufruf mit einem einzigen Argumente wird im Allgemeinen nicht als veraltet und hat auch keine Schwachstellen , wenn sie ordnungsgemäß verwendet wird, wie Sie immer kodieren sollen.

C Benutzer auf der ganzen Welt, vom Statusanfänger bis zum Statusexperten printf, geben auf diese Weise eine einfache Textphrase als Ausgabe an die Konsole.

Außerdem muss jemand unterscheiden, ob dieses einzige Argument ein Zeichenfolgenliteral oder ein Zeiger auf eine Zeichenfolge ist, die gültig ist, aber häufig nicht verwendet wird. Für letztere kann es natürlich zu unbequemen Ausgaben oder zu undefiniertem Verhalten kommen , wenn der Zeiger nicht richtig auf eine gültige Zeichenfolge verweist, aber diese Dinge können auch auftreten, wenn die Formatspezifizierer nicht mit den jeweiligen Argumenten übereinstimmen mehrere Argumente.

Natürlich ist es auch nicht richtig und richtig, dass die Zeichenfolge, die als einziges Argument bereitgestellt wird, Format- oder Konvertierungsspezifizierer hat, da keine Konvertierung stattfinden wird.

Das heißt, ein einfaches String-Literal wie "Hello World!"als einziges Argument ohne Formatbezeichner in diesem String anzugeben, wie Sie es in der Frage angegeben haben:

printf("Hello World!");

ist weder veraltet oder " schlechte Praxis " noch hat es keine Schwachstellen.

Tatsächlich haben viele C-Programmierer begonnen, C oder sogar Programmiersprachen im Allgemeinen mit diesem HelloWorld-Programm und dieser printfAussage als erste ihrer Art zu lernen und zu verwenden .

Das wären sie nicht, wenn sie veraltet wären.

In einem Buch, das ich lese, steht, dass printfmit einem einzigen Argument (ohne Konvertierungsspezifizierer) veraltet ist.

Dann würde ich mich auf das Buch oder den Autor selbst konzentrieren. Wenn ein Autor meiner Meinung nach wirklich solche falschen Behauptungen aufstellt und dies sogar lehrt, ohne explizit zu erklären, warum er / sie dies tut (wenn diese Behauptungen in diesem Buch wirklich buchstäblich gleichwertig sind), würde ich es als schlechtes Buch betrachten. Ein gutes Buch soll im Gegensatz dazu erklären, warum bestimmte Arten von Programmiermethoden oder -funktionen vermieden werden sollten.

Nach dem, was ich oben gesagt habe, wird die Verwendung printfmit nur einem Argument (einem String-Literal) und ohne Formatbezeichner auf keinen Fall als "schlechte Praxis" missbilligt oder angesehen .

Sie sollten den Autor fragen, was er damit gemeint hat oder noch besser, und ihn bitten, den entsprechenden Abschnitt für die nächste Ausgabe oder die Abdrücke im Allgemeinen zu klären oder zu korrigieren.


Man könnte hinzufügen , dass printf("Hello World!");ist nicht äquivalent puts("Hello World!");sowieso, was etwas über den Autor der Empfehlung erzählt.
Chqrlie
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.