
Jeder Programmierer ist im Wesentlichen ein Hacker. Schließlich wurde Hacking zunächst als Suche nach einer geschickten und nicht offensichtlichen Lösung bezeichnet. Das Verständnis der Programmierung hilft Ihnen beim Auffinden von Schwachstellen, und das Erkennen von Schwachstellen hilft Ihnen beim Erstellen von Programmen, sodass viele Hacker beides gleichzeitig tun. Es gibt interessante nicht standardmäßige Bewegungen sowohl bei Techniken zum Schreiben eleganter Programme als auch bei Techniken zum Auffinden von Schwachstellen.
Wo soll ich anfangen? Um Speicher mithilfe von Pufferüberläufen zu überschreiben, auf einen Remote-Server zuzugreifen und Verbindungen abzufangen, müssen Sie in C und Assembler programmieren, Shell-Code- und Prozessorregister verwenden, sich mit Netzwerkinteraktionen und -verschlüsselung vertraut machen und vieles mehr.
Egal wie sehr wir an Wunder glauben wollen, die Software und Computernetzwerke, von denen unser tägliches Leben abhängt, weisen Schwachstellen auf.
Eine Welt ohne Hacker ist eine Welt ohne Neugier und innovative Lösungen. (John Erickson)
Gegenmaßnahmen
Es gibt so einen Frosch - einen schrecklichen Listolaz (Phyllobates terribilis). Ihre Hautdrüsen enthalten das stärkste Gift. Berühren Sie es einfach, um eine tödliche Vergiftung zu bekommen. Ein solch wirksames Mittel erklärt sich aus der Tatsache, dass diese Frösche sich von bestimmten Arten von Schlangen ernähren, die eine Immunität gegen Gift entwickelt haben. Allmählich wurde das Gift der Frösche immer stärker. Infolge einer solchen gemeinsamen Entwicklung haben die schrecklichen Blattkletterer keine anderen natürlichen Feinde mehr. Ähnliches passiert mit Hackern. Die Techniken, die sie erfunden haben, sind seit langem bekannt, so dass die Entstehung von Gegenmaßnahmen ganz natürlich ist. Als Reaktion darauf suchen Hacker nach Wegen, um Abwehrmechanismen zu umgehen und zu zerstören, was zur Schaffung neuer Verteidigungstechniken führt.
Dieser Zyklus der Suche nach Maßnahmen und Gegenmaßnahmen ist sehr nützlich. Viren und Würmer verursachen zahlreiche Probleme und bringen große Verluste für das Geschäft mit sich. Gleichzeitig zwingen sie Entwickler, Vergeltungsmaßnahmen zu ergreifen, um die aufgetretenen Probleme zu lösen. Würmer replizieren sich selbst mit Software-Schwachstellen von geringer Qualität. Fehler bleiben im Laufe der Jahre oft unbemerkt, und relativ harmlose Würmer wie CodeRed oder Sasser zwingen Entwickler, sie zu beheben. Dieser Prozess kann mit Windpocken verglichen werden, die besser für die Krankheit in der Kindheit und nicht im Erwachsenenalter geeignet sind, wenn sie katastrophale Folgen haben können. Wenn Internet-Würmer nicht die allgemeine Aufmerksamkeit auf Sicherheitslücken gelenkt hätten, würden diese Lücken für Angriffe mit weitaus böswilligeren Zwecken als der einfachen Selbstreproduktion offen bleiben. Auf diese Weise tragen Würmer und Viren zur langfristigen Sicherheit bei. Es gibt jedoch aktivere Methoden: Techniken, die versuchen, die Ergebnisse eines Angriffs aufzuheben oder völlig unmöglich zu machen. Das Konzept der „Gegenmaßnahmen“ ist eher vage. Diese Wörter können technische Sicherheitsmittel, ein Regelwerk, ein Programm oder nur einen aufmerksamen Systemadministrator bedeuten. Gegenmaßnahmen werden bedingt in zwei Gruppen unterteilt: Versuch, einen Angriff zu erkennen und die Sicherheitsanfälligkeit zu schützen.
0x610 Angriffserkennungs-Tool
Gegenmaßnahmen der ersten Gruppe laufen darauf hinaus, das Eindringen rechtzeitig zu bemerken und irgendwie darauf zu reagieren. Der Erkennungsprozess kann auf beliebige Weise implementiert werden - vom Administrator, der die Systemprotokolle liest, bis zu Programmen, die den Netzwerkverkehr analysieren. Manchmal besteht die Reaktion auf eine Invasion darin, die Verbindung automatisch zu trennen oder den Prozess zu schließen, und manchmal muss der Administrator den gesamten Inhalt der Konsole sorgfältig untersuchen.
Bekannte Methoden zum Ausnutzen von Sicherheitslücken sind für den Systemadministrator nicht so gefährlich wie diejenigen, die er noch nicht kennt. Je früher ein Eindringen erkannt werden kann, desto eher beginnt die Arbeit damit und desto wahrscheinlicher ist es, dass er lokalisiert wird. Einbruch, der seit Monaten ignoriert wird, gibt Anlass zur Sorge.
Um das Eindringen zu erkennen, müssen Sie vorhersehen, was der Angreifer tun wird. Hier finden Sie Informationen darüber, was genau überwacht werden muss. Detektoren suchen in Protokollen, Netzwerkpaketen und sogar im Programmspeicher nach bekannten Angriffsmustern. Wenn ein Eindringen festgestellt wird, können Sie dem Hacker den Zugriff auf den Computer entziehen, das beschädigte Dateisystem mithilfe einer Sicherung wiederherstellen und die Sicherheitslücke identifizieren und beheben. Mit den heute verfügbaren Sicherungs- und Wiederherstellungsfunktionen sind Gegenmaßnahmen, die sich auf die Angriffserkennung reduzieren, sehr effektiv.
Erkennung bedeutet für einen Angreifer, alles zu kontern, was er tut. Natürlich ist es nicht immer möglich, einen Angriff sofort zu bemerken, daher gibt es eine Reihe von "Grab and Run" -Szenarien, in denen die Tatsache der Erkennung nicht wichtig ist, aber selbst in diesen Fällen ist es besser, keine Spuren zu hinterlassen. Geheimhaltung ist eine der wertvollsten Eigenschaften von Hackern. Wenn Sie durch Ausnutzen von Sicherheitslücken Zugriff auf die Befehlsshell mit Administratorrechten erhalten, können Sie alles auf dem System tun. Wenn Sie es schaffen, eine Erkennung zu vermeiden, weiß niemand von Ihrer Anwesenheit. Es ist die Kombination von Zulässigkeit mit Unsichtbarkeit, die Hacker gefährlich macht. Sie können problemlos Kennwörter und Daten im Netzwerk abfangen, Lesezeichen zu Programmen für den nachfolgenden nicht autorisierten Zugriff hinzufügen und andere Netzwerkknoten angreifen. Um im Schatten zu bleiben, sollte der Hacker verstehen, welche Erkennungsmethoden in einem bestimmten Fall verwendet werden. Wenn Sie genau wissen, wonach sie suchen, können Sie bestimmte Muster der Ausnutzung der Sicherheitsanfälligkeit vermeiden oder Ihre Aktionen als gültig tarnen. Der treibende Faktor für den Zyklus der gemeinsamen Entwicklung von Erkennungswerkzeugen und -techniken, die es ermöglichen, unbemerkt zu bleiben, sind Ideen, die auf der anderen Seite noch nicht vorgekommen sind.
0x620 Systemdämonen
Gegenmaßnahmen gegen Hacker und ihre Problemumgehungen lassen sich jedoch am besten anhand eines praktischen Beispiels diskutieren. Jetzt betrachten wir einen Angriff auf ein Serverprogramm, das eingehende Verbindungen akzeptiert. Unter UNIX-ähnlichen Betriebssystemen sind Systemdämonen für diese Kriterien geeignet. Ein Daemon ist ein Programm, das im Hintergrund ausgeführt wird und in gewisser Weise vom Steuerterminal getrennt ist. Der Begriff wurde in den 1960er Jahren von Hackern des Massachusetts Institute of Technology geprägt. Der Prototyp war ein Fabelwesen, das Moleküle nach einem mentalen Experiment des Physikers James Maxwell sortierte. Der Maxwell-Dämon besaß eine übernatürliche Fähigkeit, komplexe Aufgaben leicht auszuführen, was gegen den zweiten Hauptsatz der Thermodynamik verstieß. In ähnlicher Weise führen Linux-Systemdämonen unermüdlich Aufgaben wie das Gewähren des Zugriffs auf SSH und das Verwalten von Systemprotokollen aus. Dämonennamen enden normalerweise mit d, was ihre Natur betont: zum Beispiel sshd oder syslogd.
Eine kleine Bearbeitung verwandelt tinyweb.c aus Abschnitt 0x427 in eine realistische Ähnlichkeit mit einem Systemdämon. Die neue Version des Codes enthält die Funktion daemon (), die einen neuen Hintergrundprozess auslöst. Es wird von vielen Linux-Systemdämonprozessen verwendet. Hier ist die ihr gewidmete Seite aus dem Verzeichnis:
DAEMON (3) Linux-Programmierreferenz DAEMON (3)
NAME
Daemon - im Hintergrund ausführen
Syntaxis
#include <unistd.h> int daemon(int nochdir, int noclose);
BESCHREIBUNG
Die Funktion daemon () trennt das Programm vom Steuerterminal und startet
sie im Hintergrund als Systemdämon.
Mit einem Argument ungleich Null, nochdir, ändert die Funktion daemon () das aktuelle Arbeitsverzeichnis.
auf der Wurzel ("/").
Mit nullose leitet die Funktion daemon () Threads um
Standardeingabe, Standardausgabe und Fehler in / dev / null.
RÜCKGABEWERT
(Diese Funktion erzeugt eine Kopie des Prozesses, und wenn fork () erfolgreich ist
Das übergeordnete Element führt _exit (0) aus, sodass nur weitere Fehler sichtbar sind
untergeordneter Prozess.) Wenn erfolgreich, wird Null zurückgegeben. Bei Fehler die Funktion
daemon () gibt -1 zurück und weist der globalen Variablen errno die Fehlernummer zu
aus der Funktionsbibliothek fork (2) und setsid (2).
System-Daemons haben kein Steuerterminal, daher wird der Code für den neuen tinywebd-Daemon in das Logbuch ausgegeben. Dämonen werden normalerweise durch Signale gesteuert. Die neue Version von tinyweb sollte in der Lage sein, ein Abschlusssignal zu empfangen, um das Programm korrekt zu beenden.
0x621 Signalübersicht
Signale ermöglichen die prozessübergreifende Kommunikation unter UNIX. Nachdem ein Signal von einem Prozess empfangen wurde, unterbricht das Betriebssystem seine Ausführung, um den Signalhandler aufzurufen. Jedes Signal hat eine eigene Nummer und einen eigenen Handler. Wenn Sie beispielsweise die Tastenkombination Strg + C drücken, wird ein Interrupt-Signal gesendet, dessen Handler das auf dem Steuerterminal geöffnete Programm beendet, auch wenn es in eine Endlosschleife eingetreten ist.
Sie können Ihre eigenen Signalhandler erstellen und diese mit der Funktion signal () registrieren. Schauen wir uns einen Code an, in dem mehrere Handler für einige Signale registriert sind und im Hauptteil eine Endlosschleife vorhanden ist.
signal_example.c #include <stdio.h> #include <stdlib.h> #include <signal.h> /* signal.h * #define SIGHUP 1 * #define SIGINT 2 (Ctrl+C) * #define SIGQUIT 3 (Ctrl+\) * #define SIGILL 4 * #define SIGTRAP 5 * #define SIGABRT 6 * #define SIGBUS 7 * #define SIGFPE 8 * #define SIGKILL 9 * #define SIGUSR1 10 1 * #define SIGSEGV 11 * #define SIGUSR2 12 2 * #define SIGPIPE 13 , * #define SIGALRM 14 , alarm() * #define SIGTERM 15 ( kill) * #define SIGCHLD 17 * #define SIGCONT 18 , * #define SIGSTOP 19 ( ) * #define SIGTSTP 20 [] (Ctrl+Z) * #define SIGTTIN 21 * #define SIGTTOU 22 */ /* */ void signal_handler(int signal) { printf(, signal); if (signal == SIGTSTP) printf(); else if (signal == SIGQUIT) printf(); else if (signal == SIGUSR1) printf(); else if (signal == SIGUSR2) printf(); printf(); } void sigint_handler(int x) { printf(); exit(0); } int main() { /* Registering signal handlers */ signal(SIGQUIT, signal_handler); // signal_handler() signal(SIGTSTP, signal_handler); // signal(SIGUSR1, signal_handler); signal(SIGUSR2, signal_handler); signal(SIGINT, sigint_handler); // sigint_handler() SIGINT while(1) {} // }
Wenn ein kompiliertes Programm ausgeführt wird, werden Signalhandler registriert, und dann tritt das Programm in eine Endlosschleife ein. Trotzdem unterbrechen eingehende Signale die Ausführung und wenden sich an registrierte Handler. Das Folgende sind Beispiele für die Anwendung von Signalen, die vom Steuerterminal aktiviert werden können. Nach Abschluss der Funktion signal_handler () kehrt die Steuerung zur unterbrochenen Schleife zurück, während die Funktion sigint_handler () das Programm beendet.
reader@hacking:~/booksrc $ gcc -o signal_example signal_example.c reader@hacking:~/booksrc $ ./signal_example 20 SIGTSTP (Ctrl-Z) 3 SIGQUIT (Ctrl-\) Ctrl-C (SIGINT) Exiting. reader@hacking:~/booksrc $
Mit dem Befehl kill können Sie eine ganze Reihe von Signalen an einen Prozess senden. Standardmäßig sendet es ein Abschlusssignal (SIGTERM). Durch Hinzufügen der Option -l wird eine Liste aller möglichen Signale angezeigt. Mal sehen, wie das auf einem anderen Terminal laufende Programm signal_example die Signale SIGUSR1 und SIGUSR2 sendet.
reader@hacking:~/booksrc $ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX reader@hacking:~/booksrc $ ps a | grep signal_example 24491 pts/3 R+ 0:17 ./signal_example 24512 pts/1 S+ 0:00 grep signal_example reader@hacking:~/booksrc $ kill -10 24491 reader@hacking:~/booksrc $ kill -12 24491 reader@hacking:~/booksrc $ kill -9 24491 reader@hacking:~/booksrc $
Am Ende sendet der Befehl kill -9 ein SIGKILL-Signal. Der Signalhandler kann nicht geändert werden, daher wird der Befehl kill -9 immer zum Beenden von Prozessen verwendet. Das auf einem anderen Terminal gestartete Programm signal_example zeigt an, dass die Signale abgefangen und der Prozess zerstört wurde.
reader@hacking:~/booksrc $ ./signal_example 10 SIGUSR1 12 SIGUSR2 Killed reader@hacking:~/booksrc $
Die Signale selbst sind sehr einfach, aber die Interaktion zwischen Prozessen kann schnell zu einem komplexen Netz von Abhängigkeiten werden. Glücklicherweise werden in unserem tinyweb-Daemon Signale nur zum korrekten Herunterfahren verwendet, und alles wird sehr einfach implementiert.
0x622 Tinyweb Daemon
Die neue Version von tinywebd ist ein Systemdämon, der im Hintergrund ohne Steuerterminal gestartet wird. Die Ausgabedaten werden mit Zeitstempeln in das Logbuch geschrieben, und das Programm selbst wartet darauf, dass das SIGTERM-Signal die Arbeit korrekt abschließt.
Die am Original vorgenommenen Änderungen sind nicht sehr bedeutsam, ermöglichen jedoch eine realistischere Untersuchung des Ausnutzungsprozesses der Sicherheitsanfälligkeit. Neue Codefragmente sind fett gedruckt.
tinywebd.c #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <time.h> #include <signal.h> #include "hacking.h" #include "hacking-network.h" #define PORT 80 // , #define WEBROOT "./webroot" // - #define LOGFILE "/var/log/tinywebd.log" // int logfd, sockfd; // void handle_connection(int, struct sockaddr_in *, int); int get_file_size(int); // // void timestamp(int); // // void handle_shutdown(int signal) { timestamp(logfd); write(logfd, " .\n", 16); close(logfd); close(sockfd); exit(0); } int main(void) { int new_sockfd, yes=1; struct sockaddr_in host_addr, client_addr; // socklen_t sin_size; logfd = open(LOGFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(logfd == -1) fatal(" "); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal(" "); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal(" SO_REUSEADDR"); printf(" tiny web.\n"); if(daemon(1, 0) == -1) // fatal(" "); signal(SIGTERM, handle_shutdown); // handle_shutdown signal(SIGINT, handle_shutdown); // handle_shutdown timestamp(logfd); write(logfd, ".\n", 15); host_addr.sin_family = AF_INET; // host_addr.sin_port = htons(PORT); // , host_addr.sin_addr.s_addr = INADDR_ANY; // IP memset(&(host_addr.sin_zero), '\0', 8); // if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal(" "); if (listen(sockfd, 20) == -1) fatal(" "); while(1) { // accept sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1) fatal(" "); handle_connection(new_sockfd, &client_addr, logfd); } return 0; } void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) { unsigned char *ptr, request[500], resource[500], log_buffer[500]; int fd, length; length = recv_line(sockfd, request); sprintf(log_buffer, " %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr->sin_addr), ntohs(client_addr_ptr->sin_port), request); ptr = strstr(request, " HTTP/"); // if(ptr == NULL) { // HTTP strcat(log_buffer, " HTTP!\n"); } else { *ptr = 0; // URL ptr = NULL; // ptr NULL ( // ) if(strncmp(request, "GET ", 4) == 0) // GET ptr = request+4; // ptr - URL if(strncmp(request, "HEAD ", 5) == 0) // HEAD ptr = request+5; // ptr - URL if(ptr == NULL) { // strcat(log_buffer, " !\n"); } else { // ptr, if (ptr[strlen(ptr) - 1] == '/') // , // '/', strcat(ptr, "index.html"); // 'index.html' strcpy(resource, WEBROOT); // resource // strcat(resource, ptr); // fd = open(resource, O_RDONLY, 0); // if(fd == -1) { // strcat(log_buffer, " 404 Not Found\n"); send_string(sockfd, "HTTP/1.0 404 NOT FOUND\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); send_string(sockfd, "<html><head><title>404 Not Found</title> </head>"); send_string(sockfd, "<body><h1>URL not found</h1></body></html> \r\n"); } else { // strcat(log_buffer, " 200 OK\n"); send_string(sockfd, "HTTP/1.0 200 OK\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); if(ptr == request + 4) { // GET if( (length = get_file_size(fd)) == -1) fatal(" "); if( (ptr = (unsigned char *) malloc(length)) == NULL) fatal(" "); read(fd, ptr, length); // send(sockfd, ptr, length, 0); // free(ptr); // } close(fd); // } // if / } // if } // if HTTP timestamp(logfd); length = strlen(log_buffer); write(logfd, log_buffer, length); // shutdown(sockfd, SHUT_RDWR); // } int get_file_size(int fd) { struct stat stat_struct; if(fstat(fd, &stat_struct) == -1) return -1; return (int) stat_struct.st_size; } void timestamp(fd) { time_t now; struct tm *time_struct; int length; char time_buffer[40]; time(&now); // time_struct = localtime((const time_t *)&now); // tm length = strftime(time_buffer, 40, "%m/%d/%Y %H:%M:%S> ", time_struct); write(fd, time_buffer, length); // }
Dieses Programm erstellt einen doppelten Prozess, der im Hintergrund ausgeführt wird, zusammen mit Zeitstempeln in die Protokolldatei schreibt und nach dem Empfang des entsprechenden Signals korrekt beendet wird. Der Protokolldateideskriptor und der Socket, die die Verbindung akzeptieren, werden als globale Variablen deklariert, damit die Funktion handle_shutdown () ihre Arbeit korrekt ausführen kann. Es ist als Rückruf-Handler für Vervollständigungs- und Interrupt-Signale definiert, der sicherstellt, dass das Programm durch den Befehl kill beendet wird.
Hier ist das Ergebnis des Kompilierens, Ausführens und Vervollständigens des Programms. Beachten Sie die Zeitstempel im Protokoll und die Abschlussmeldung, die angezeigt wurde, nachdem das Programm das entsprechende Signal empfangen und die Funktion handle_shutdown () aufgerufen hat.
reader@hacking:~/booksrc $ gcc -o tinywebd tinywebd.c reader@hacking:~/booksrc $ sudo chown root ./tinywebd reader@hacking:~/booksrc $ sudo chmod u+s ./tinywebd reader@hacking:~/booksrc $ ./tinywebd tiny web. reader@hacking:~/booksrc $ ./webserver_id 127.0.0.1 The web server for 127.0.0.1 is Tiny webserver reader@hacking:~/booksrc $ ps ax | grep tinywebd 25058 ? Ss 0:00 ./tinywebd 25075 pts/3 R+ 0:00 grep tinywebd reader@hacking:~/booksrc $ kill 25058 reader@hacking:~/booksrc $ ps ax | grep tinywebd 25121 pts/3 R+ 0:00 grep tinywebd reader@hacking:~/booksrc $ cat /var/log/tinywebd.log cat: /var/log/tinywebd.log: Permission denied reader@hacking:~/booksrc $ sudo cat /var/log/tinywebd.log 07/22/2007 17:55:45> . 07/22/2007 17:57:00> 127.0.0.1:38127 "HEAD / HTTP/1.0" 200 OK 07/22/2007 17:57:21> . reader@hacking:~/booksrc $
Das neue Programm tinywebd verarbeitet HTTP-Inhalte wie das ursprüngliche Programm tinyweb, verhält sich jedoch wie ein Systemdämon, da es kein Steuerterminal hat und die Ausgabe in eine Protokolldatei erfolgt. Beide Programme sind für denselben Pufferüberlauf anfällig - dies ist jedoch nur der Anfang dieser Sicherheitsanfälligkeit. Nachdem wir tinywebd als Ziel des Angriffs ausgewählt haben, werde ich Ihnen zeigen, wie Sie eine Erkennung vermeiden können, nachdem Sie eine andere Maschine betreten haben.
»Weitere Informationen zum Buch finden Sie auf
der Website des Herausgebers»
Inhalt»
Auszug20% Rabatt auf Gutschein für Habrozhitel -
Hacking