![]() |
Internet-TechnologieProf. Jürgen Plate |
Für den Zugriff auf den Transport- und den Network-Layer im OSI-Schichtenmodell, bietet die Socket-API einen speziellen Socket-Typ, den RAW-Socket (SOCK_RAW), an. Mit ihm lassen sich Netzwerk-Pakete inklusive aller Headerdaten bis zum Header des Network-Layers auslesen und auch generieren. Typischerweise handelt es sich dabei um IP-Pakete. Die Besonderheit des RAW-Sockets ist die Möglichkeit, direkten Einfluss auf die Headerdaten eines jeden Paketes zu nehmen. Sowohl beim Senden, als auch beim Empfangen wird dem Payload (den eigentlichen Nutzdaten) der Header vorangestellt. Im Buffer, der bei den Lese- bzw. Schreibzugriffen angegeben wird, werden beim RAW-Socket sämtliche Daten abgelegt, die ab Layer 3 (Vermittlungsschicht) im Paket enthalten sind. So ist beispielsweise die Überwachung und Analyse des Netzwerkverkehrs auf einer sehr systemnahen Ebene möglich. Weitere Anwendungsmöglichkeiten sind auch die Implementierung von Proxies oder Gateways. RAW-Sockets brechen das stickte OSI-Schichtenmodell auf, indem man die Header der darunterliegenden Schichten manipulieren kann. Im Endeffekt obliegt es dem Programmierer, mit seiner Implementation die Aufgaben der Schichten 3 (Internetschicht) und 4 (Transportschicht) sicherzustellen.
Unter Linux gibt es für den Zugriff auf den Data-Link-Layer die sogenannten PACKET-Sockets. PACKET-Sockets gehören nicht zu den Standard Berkley Sockets, sondern sind Teil der Linux-eigenen Socket-API. Dadurch bleibt unter Linux das Prinzip der Socket-Programmierung auch beim Zugriff auf den Data-Link-Layer erhalten. PACKET-Sockets werden unter Linux als spezielle Formvon RAW-Sockets behandelt. Insofern gelten viele der Aussagen, die im Folgenden über RAW-Sockets gemacht werden, auch für PACKET-Sockets. Bei der Implementierung gibt es jedoch Unterschiede. Um die große Zahl an Möglichkeiten zu kapseln und die Programme unabhängig vom Betriebssystem zu erstellen, sollte stets auf universelle Packet Capturing Bibliotheken wie z. B. pcap (http://www.tcpdump.org/) und libnet (http://www.packetfactory.net/projects/libnet/) zurückgegriffen werden.
Im Vergleich zu dem Einsatz von gewöhnlichen Sockets gibt es bei der Verwendung von RAW-Sockets ein paar Besonderheiten, die die Sie beachten müssen. Programme, die RAW-Sockets verwenden, müssen immer mit Root-Rechten laufen. Da Ports erst auf höheren Schichten vorhanden sind, haben hier natürlich noch keine Bedeutung. RAW-Sockets können daher nicht auf einem bestimmten Port lauschen. In der Regel nimmt ein RAW-Socket einfach allen Datenverkehr eines bestimmten IP-basierten Protokolls entgegen. Eine eventuell vorhandene Portnummer muss ggf. von Hand aus dem TCP- oder UDP-Header extrahiert werden. Ein RAW-Socket wird wie üblich über die Funktion socket() angelegt. Jedoch wird für den zweiten Parameter statt SOCK_STREAM oder SOCK_DGRAMM die Konstante SOCK_RAW verwendet. Die Protokollnummer (dritter Parameter) kann als Zahl angegeben werden oder es wird eine bereits vordefinierte Konstante wie z. B. IPPROTO_ICMP oder IPPROTO_IGMP verwendet. Eine Liste der offiziellen Protokollnummern ist unter www.iana.org zu finden. Die Nummern 253 und 254 sind speziell für das Experimentieren reserviert ("use for experimentation and testing").
Die Funktionen bind() und connect() sind bei RAW-Sockets zwar noch nutzbar, jedoch nicht mehr unbedingt nötig. bind() erlaubt es beispielsweise beim Senden, eine Absenderadresse zu definieren, die automatisch im IP-Header der zu sendenden Pakete eintragen wird. Beim Empfang werden nur Pakete angenommen, die sich an die mit bind() festgelegte Adresse wenden. Bei PACKET-Sockets kann mit der Funktion bind() der Socket an ein bestimmtes Netzwerkinterface gebunden werden. Mit connect() wird eine Ziel-IP-Adresse für den RAW Socket festgelegt. Eine so definierte Adresse wird als Empfängeradresse im IP-Header eines jeden ausgehenden Pakets eingetragen und beim Empfang werden nur die Pakete angenommen, die von dieser Adresse stammen. Im Zusammenhang mit PACKET-Sockets hat die Funktion connect() keine Bedeutung mehr. Die Funktionen listen() und accept() haben bei RAW Sockets keine Bedeutung mehr, connect() ist nur noch optional und entfällt für PACKET-Sockets vollständig.
Auch bei den RAW-Sockets übernimmt per default das Betriebssystem die Generierung des IP-Headers. Erst wenn mit der Funktion setsockopt() die Socket-Option IP_HDRINCL gesetzt wird, ist die manuelle Konstruktion des IP-Headers möglich. Aber auch, wenn diese Option gesetzt ist, generiert das Betriebssystem die IP-Prüfsumme von sich aus. Die IP-Identifikationsnummer oder die Absenderadresse werden automatisch vom Kernel in den IP-Header eingesetzt, wenn diese zuvor mit dem Wert Null übergeben wurden. Dadurch wird die Programmierung etwas erleichtert. Bei PACKET-Sockets müssen die Pakete immer vollständig von Hand generiert werden.
Diese Adressierung mit IP-Adresse und Portnummer entfällt aus den o. g. Gründen bei RAW-Sockets. Liest der RAW-Socket nur den ankommenden Datenverkehr, ist keine Adressierung nötig. Ein RAW-Socket, der Pakete versendet, deren IP-Header automatisch generiert werden, ist die Definition einer Empfänger-Socketadresse notwendig, um das Netzwerkinterface festzulegen, über das gesendet werden soll. Die Angabe einer Portnummer kann entfallen, wenn ein Protokoll verwendet wird, das keine Ports unterstützt (z. B. ARP, ICMP). Socketadressen werden jedoch von den Funktionen bind(), connect(), sendto() oder recvfrom() in struct sockaddr verlangt. RAW-Sockets verwenden dabei die schon bekannte struct sockaddr_in, PACKET-Sockets hingegen struct sockaddr_ll.
Daten werden bei den RAW-Sockets, wie schon bei UDP, mit den Funktionen sendto() und recvfrom() geschrieben oder gelesen. Die Daten werden in die Payload des IP-Paketes verpackt und versendet. Alle Felder für das Paket müssen dabei in network-byte-order vorliegen. Generell gilt, dass mit RAW-Sockets nur noch einzelne, meist unsortierte und unzusammenhängende Pakete versendet und empfangen werden. Die Auswertung und Interpretation der Daten oder ein eventuell gewünschtes Verhalten als Client oder Server obliegt allein der Anwendung. Das Versenden von Netzwerk-Paketen per RAW- oder PACKET-Socket ist komplexer als man denkt. Fehlerhaft konstruierte Pakete werden vom Betriebssystem des Sende-Rechners in der Regel gar nicht erst verschickt und oft reagiert die Gegenstelle im Netz, je nach Betriebssystem oder Firewalleinstellung, auch auf an sich korrekte Pakete sehr unterschiedlich, manchmal überhaupt nicht.
In den Deklarationen der Funktionen werden manchmal Datentypen wie socklen_t, size_t, ssize_t etc. aufgeführt, die der Portabilität der Funktionen dienen. Als Programmierer will man aber manchmal wissen, was sich hinter den Typen verbirgt. Dazu muss man dann leider diverse Headerdateien durchsuchen, die sich meist in /usr/include/ und /usr/include/sys/ befinden. Oft sind diese Typen kompatibel zu (unsigned) (short) int, aber letzendlich ist man gut beraten, wenn man die vorgesehenen Typen verwendet.
Bei Wake-On-LAN, kurz WOL, handelt es sich um einen 1995 von AMD in Zusammenarbeit mit HP veröffentlichten Standard, der das Einschalten von Rechnern über das Netz regelt. Um WOL zu nutzen, müssen das Mainboard und die Netzwerkkarte des jeweiligen PCs ACPI unterstützen. Um einen PC aufzuwecken, sendet man ein sogenanntes Magic Packet an diesen. Dieses Paket veranlasst dann den Rechner, zu starten. Natürlich muss im BIOS des jeweiligen Rechners WOL aktiviert werden. Unter Umständen muss bei einigen Netzwerkkarten auch erst das passende WOL-Kabel zwischen Netzwerkkarte und Mainboard gesteckt werden (damit die Karte auch bei ausgeschaltetem Rechner mit Strom versorgt wird).
Bei Linux deaktivieren viele Netzwerkkarten-Treiber die WOL-Funktion nach dem Starten des Interfaces. Um diese nun wieder zu aktivieren, benutzen wir das Programm ethtool, das in vielen Fällen nützlich ist. Durch den Aufruf unter Angabe des Netzwerk-Interfaces erhalten Sie alle Informationen über die Netzwerkkarte:
# ethtool eth0 Settings for eth0: Supported ports: [ TP ] Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Supports auto-negotiation: Yes Advertised link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Advertised auto-negotiation: Yes Speed: 1000Mb/s Duplex: Full Port: Twisted Pair PHYAD: 0 Transceiver: internal Auto-negotiation: on Supports Wake-on: umbg Wake-on: d Current message level: 0x00000007 (7) Link detected: yesWichtig sind die beiden Zeilen
Supports Wake-on: umbg Wake-on: gDie erste Zeile listet die Möglichkeiten des Weckrufs auf. Bei meiner Karte werden u, m, b und g unterstützt. Die Bedeutung der Buchstaben zeigt folgende Tabelle:
Code | Bedeutung |
---|---|
p | Wake on phy activity |
u | Wake on unicast messages |
m | Wake on multicast messages |
b | Wake on broadcast messages |
a | Wake on ARP |
g | Wake on MagicPacket |
s | Enable SecureOn password for MagicPacket |
d | Disable (wake on nothing) |
Wie man an der zweiten Zeile sieht, ist das WOL derzeit deaktiviert. Um WOL per Magic Packet zu aktivieren, genügt der Kommandoaufruf ethtool -s eth0 wol g. Damit man das nicht nach jedem Reboot des Rechners das Kommando von Hand eingeben muss, schreibt man es am besten in die Datei /etc/rc.local. Debian-User vermerken NETDOWN=no in der Datei /etc/default/halt, damit die Netzwerkkarte eingeschaltet bleibt.
Das folgende Programm versendet ein solches "Magic Packet". Als Parameter reicht die MAC-Adresse aus, jedoch sind weitere Angaben auf der Kommandozeile möglich:
/* wol.c - Simple Wake-On-LAN utility to wake a networked PC. */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <errno.h> #define u_int8_t unsigned char int send_wol(u_int8_t *mac_addr, unsigned port, unsigned long bcast); /* Sends Wake-On-LAN packet to given address with the given options * Returns 0 on success, 1 on error. */ int get_ether(char *mac_address_string, u_int8_t *mac_addr); /* Extract inet address from hardware address. */ void print_usage(char *arg); /* print help information */ int get_hex(char buf); /* Attempts to extract hexadecimal from ASCII string. * Returns byte value read on success, or -1 on error. */ int verbose = 0; /* verbosity */ int debug = 0; /* debuging output */ unsigned port = 9; /* portnumber, 7 or 9 */ unsigned long bcast = 0xFFFFFFFF; /* broadcast address (IP) */ int main(int argc, char * argv[]) { int c; u_int8_t mac_addr[8]; /* parse command line */ while ((c = getopt(argc, argv, "hvdb:p:")) != -1) { switch (c) { case 'd': /* debug */ debug = 1; verbose = 1; break; case 'v': /* verbose */ verbose = 1; break; case 'b': /* broadcast */ bcast = inet_addr(optarg); if (bcast == INADDR_NONE) { perror("-b: Broadcast Address required"); return 1; } break; case 'p': /* port */ port = strtol(optarg, NULL, 0); if (port == 0 && errno != 0) { perror("-p: Portnumber (int) requirend!"); return 1; } case 'h': /* help */ case '?': /* unrecognized option */ default: print_usage(argv[0]); return 1; break; } } /* parse any remaining arguments (not options) */ if (optind != (argc - 1)) { print_usage(argv[0]); return 1; } /* fetch the hardware address */ if (get_ether(argv[optind], mac_addr) < 0) { fprintf(stderr,"\"%s\" is not a valid ether address!\n", mac_addr); return 1; } /* send magic packet */ if (send_wol(mac_addr, port, bcast) < 0) { fprintf(stderr,"Error sending packet %s.\n", strerror(errno)); return 1; } if (verbose) { printf("Packet sent to %08X - %s on port %d\n", htonl(bcast), argv[optind], port); } return 0; } void print_usage(char *arg) /* print help information */ { fprintf(stderr, "Usage: %s [-v] [-b <bcast>] [-p <port>] <dest>\n\n", arg); fprintf(stderr, "The single required parameter is the Ethernet MAC address\n"); fprintf(stderr, "of the machine to wake.\n\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, " -b Send wake-up packet to the broadcast address.\n"); fprintf(stderr, " -v Increase the verbosity level.\n"); fprintf(stderr, " -d Increase the debug level.\n"); fprintf(stderr, "-p <port> Set the TCP-Port (default 9)\n\n"); } int send_wol(u_int8_t *mac_addr, unsigned port, unsigned long bcast) /* Sends Wake-On-LAN packet to given address with the given options * Returns 0 on success, -1 on error. */ { u_int8_t message[102]; u_int8_t *message_ptr = message; int client, i; struct sockaddr_in addr; int optval = 1; /* setsockopt needs a variable, not a constant */ /* Build the message: 6 * 0xFF followed by 16 * destination address */ memset(message_ptr, 0xFF, 6); message_ptr = message_ptr + 6; for (i = 0; i < 16; ++i) { memcpy(message_ptr, mac_addr, 6); message_ptr = message_ptr + 6; } if (debug) { fprintf(stderr,"The final packet is: "); for (i = 0; i < sizeof(message); i++) fprintf(stderr," %02x", message[i]); fprintf(stderr,"\n"); } if ((client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) { fprintf(stderr,"Cannot create socket%s\n", strerror(errno)); return -1; } if (debug) { fprintf(stderr,"Socket created\n"); } /* Set socket options */ if (setsockopt(client, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)) < 0) { fprintf(stderr,"Cannot set socket options %s\n", strerror(errno)); close(client); return -1; } if (debug) { fprintf(stderr,"Options changed\n"); } /* Set up broadcast address */ addr.sin_family = AF_INET; addr.sin_addr.s_addr = bcast; addr.sin_port = htons(port); /* Send the packet out */ if (sendto(client, (char *)message, sizeof message, 0, (struct sockaddr *)&addr, sizeof(addr)) < 0) { fprintf(stderr,"sendto failed %s\n", strerror(errno)); close(client); return -1; } if (debug) { fprintf(stderr,"Sendto successful\n"); } close(client); return 0; } int get_hex(char buf) /* Attempts to extract hexadecimal from ASCII character. * Returns value read on success, or -1 on error. */ { int hex; hex = 0; if (buf >= '0' && buf <= '9') { hex |= buf - '0'; } else if (buf >= 'a' && buf <= 'f') { hex |= buf - 'a' + 10; } else if (buf >= 'A' && buf <= 'F') { hex |= buf - 'A' + 10; } else { return -1; /* Error */ } return hex; } int get_ether(char *mac_address_string, u_int8_t *mac_addr) /* Extract MAC address from ASCII string. * Returns 0 on success, -1 on error. */ { char *orig = mac_address_string; int i; int hex; for (i = 0; *mac_address_string != '\0' && i < 6; ++i) { /* Parse two characters at a time. */ hex = get_hex(*mac_address_string); if (hex == -1) { return -1; } mac_address_string++; hex = (hex << 4) | get_hex(*mac_address_string); if (hex == -1) { return -1; } mac_address_string++; mac_addr[i] = (char) (hex & 0xFF); /* We might get a ' ', ':', '.' or '-' here */ if ((*mac_address_string == ':') || (*mac_address_string == ' ') || (*mac_address_string == '.') || (*mac_address_string == '-')) ++mac_address_string; } if (debug) { fprintf(stderr,"The extracted MAC address is: "); for (i = 0; i < 6; i++) fprintf(stderr,"%02x", mac_addr[i]); fprintf(stderr,"\n"); } return (mac_address_string - orig == 17) ? 0 : -1; }
Ethernet-Header + IP Header + TCP Header + Data
Damit Sie nicht immer im o. g. Skript nachsehen müssen, soll hier nochmals die Struktur der verschiedenen Header gezeigt werden - diesmal in der legendären ASCII-Grafik der RFCs. Eine Erklärung zu den einzelnen Feldern liefert das o. g. Skript:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
enum { IPPROTO_IP = 0, /* Dummy protocol for TCP */ IPPROTO_ICMP = 1, /* Internet Control Message Protocol */ IPPROTO_IGMP = 2, /* Internet Group Management Protocol */ IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */ IPPROTO_TCP = 6, /* Transmission Control Protocol */ IPPROTO_EGP = 8, /* Exterior Gateway Protocol */ IPPROTO_PUP = 12, /* PUP protocol */ IPPROTO_UDP = 17, /* User Datagram Protocol */ IPPROTO_IDP = 22, /* XNS IDP protocol */ IPPROTO_DCCP = 33, /* Datagram Congestion Control Protocol */ IPPROTO_RSVP = 46, /* RSVP protocol */ IPPROTO_GRE = 47, /* Cisco GRE tunnels (rfc 1701,1702) */ IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */ IPPROTO_ESP = 50, /* Encapsulation Security Payload protocol */ IPPROTO_AH = 51, /* Authentication Header protocol */ IPPROTO_BEETPH = 94, /* IP option pseudo header for BEET */ IPPROTO_PIM = 103, /* Protocol Independent Multicast */ IPPROTO_COMP = 108, /* Compression Header protocol */ IPPROTO_SCTP = 132, /* Stream Control Transport Protocol */ IPPROTO_UDPLITE = 136, /* UDP-Lite (RFC 3828) */ IPPROTO_RAW = 255, /* Raw IP packets */ IPPROTO_MAX };
Etwas Spezielles ist der doch recht häufig verwendete Wert 0 (IPPROTO_IP). Haben Sie sich je gefragt, wie die Socket-Funktionen auf magische Weise erraten, welches Protokoll zu verwenden ist, wenn doch 0 angegeben ist - etwa bei dem Aufruf:
fd = socket(AF_INET, SOCK_STREAM, 0);Tatsache ist, dass der Kernel gar nicht versucht, zu raten. Er sucht vielmehr nach dem erstbesten Protokoll, das mit der Domain (erster Parameter der socket()-Funktion, im Beispiel AF_INET) assoziiert ist. Dazu wird die Funktion pffindproto(domain, typ) aufgerufen (typ ist der zweite Parameter der socket()-Funktion, im Beispiel SOCK_STREAM). In der C-Bibliothek der socket()-Funktion kann man sich das so vorstellen:
... if (proto == 0) prp = pffindtype(dom, type); else prp = pffindproto(dom, proto, type); ...Linux liefert bei der "0" einen Fehler, alss Default-Protokoll kann hier IPPROTO_RAW verwendet werden. Somit haben wir eigentlich (aus Sicht des Überfliegers) drei feste Zuordnungen
fd_tcp = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); fd_udp = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); fd_raw = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);wobei man beim letzten eigentlich jedes beliebige Protokoll ausser 0 angeben kann.
Um das Befüllen des IP-Headers zu vereinfachen, kann er im C-Programm als Structure definiert werden, wobei die Datentypen u_int8_t, u_int16_t und u_int32_t in der Headerdatei types.h definiert sind:
struct ipheader { u_int8_t ip_hl:4, ip_v:4; u_int8_t ip_tos; u_int16_t ip_len; u_int16_t ip_id; u_int16_t ip_off; u_int8_t ip_ttl; u_int8_t ip_p; u_int16_t ip_sum; u_int32_t ip_src; u_int32_t ip_dst; }; /* Gesamtlaenge: 20 Bytes (= 160 Bits) */
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type | Code | Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Vom Nachrichtentyp abhängiger Teil | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Um das Befüllen des ICMP-Headers zu vereinfachen, kann er im C-Programm als Structure definiert werden:
struct icmpheader { u_int8_t icmp_type; u_int8_t icmp_code; u_int16_t icmp_cksum; /* die folgenden Definitionen haengen von Nachrichtentyp ab, z. B. */ u_int16_t icmp_id; u_int16_t icmp_seq; }; /* auch die Gesamtlänge haengt von Typ ab */Die aktuelle Version der Definition ist bei Linux in netinet/ip.h abgelegt.
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|P|R|S|F| | | Offset| Reserved |R|C|S|S|Y|I| Window | | | |G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Auch der TCP-Header kann einfacher befüllt werden, wenn er im C-Programm als Structure definiert ist:
struct tcpheader { u_int16_t th_sport; u_int16_t th_dport; u_int32_t th_seq; u_int32_t th_ack; u_int8_t th_x2:4, th_off:4; u_int8_t th_flags; u_int16_t th_win; u_int16_t th_sum; u_int16_t th_urp; }; /* Gesamtlaenge: 20 Bytes (= 160 Bits) */Die aktuelle Version der Definition ist bei Linux in netinet/tcp.h abgelegt.
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Length | Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Die Struktur für den UDP-Header ist wesentlich einfacher als beim TCP-Header:
struct udpheader { u_int16_t uh_sport; u_int16_t uh_dport; u_int16_t uh_len; u_int16_t uh_check; }; /* Gesamtlaenge: 8 Bytes (= 64 Bits) */
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netinet/ip.h>Zum Aufbau eines Sockets wird wie schon im ersten Abschnitt die Funktion socket() aus <netinet/in.h> verwendet, jedoch mit anderen Parametern:
int socket(int family, int type, int protocol);Die Funktion gibt im Erfolgsfall einen nicht negativen Socket-Deskriptor zurück (Handle). Im Fehlerfall liefert sie den Wert -1.
PF_INET / AF_INET Internet-Protokolle (TCP, UDP etc.) PF_LOCAL, PF_UNIX / AF_LOCAL, AF_UNIX Lokales Unix-IPC-Protokoll PF_ROUTE / AF_ROUTE Routing-TabellenLinux definiert diese Konstanten in socket.h:
/* Supported address families. */ #define AF_UNSPEC 0 #define AF_UNIX 1 /* Unix domain sockets */ #define AF_LOCAL 1 /* POSIX name for AF_UNIX */ #define AF_INET 2 /* Internet IP Protocol */ ...Dabei werden gleich die Protokoll- und Adressfamilien über einen Kamm geschoren:
/* Protocol families, same as address families. */ #define PF_UNSPEC AF_UNSPEC #define PF_UNIX AF_UNIX #define PF_LOCAL AF_LOCAL #define PF_INET AF_INET ...Das muss aber nicht bei allen Unix-Varianten oder anderen Betriebssystemen so sein.
enum sock_type { SOCK_STREAM = 1, SOCK_DGRAM = 2, SOCK_RAW = 3, SOCK_RDM = 4, SOCK_SEQPACKET = 5, SOCK_DCCP = 6, SOCK_PACKET = 10, };Bei FreeBSD werde sie dagegen als Konstante definiert:
#define SOCK_STREAM 1 /* stream socket */ #define SOCK_DGRAM 2 /* datagram socket */ #define SOCK_RAW 3 /* raw-protocol interface */ #if __BSD_VISIBLE #define SOCK_RDM 4 /* reliably-delivered message */ #endif #define SOCK_SEQPACKET 5 /* sequenced packet stream */
Ein typischer Aufruf zum Erstellen eines RAW-Sockets sieht beispielsweise folgendermaßen aus:
fd = socket (AF_INET, SOCK_RAW, IPPROTO_TCP);Wird als Protokoll anstelle von IPPROTO_TCP die Konstante IPPROTO_RAW angegeben, so kann der Socket jedes im Header angegebene Protokoll versenden. Der Empfang funktioniert jedoch nicht immer und für alle Protokolle. In solchen Fällen gibt man ein Protokoll an, dessen Pakete empfangen werden sollen (TCP oder UDP). Die gebräuchlichsten Konstanten sind: IPPROTO_TCP, IPPROTO_UDP und IPPROTO_ICMP. Die IPPROTO_xxx-Konstanten finden Sie in der Headerdatei netinet/in.h. Weiterhin impliziert die Konstante IPPROTO_RAW die Socketoption IP_HDRINCL, die nun erläutert wird.
Wenn also auch der IP-Header manipuliert werden soll, muss man IPPROTO_RAW verwenden, in allen anderen Fällen reicht IPPROTO_TCP aus. Damit stehen zwei Möglichkeiten zur Verfügung:
int s = socket (AF_INET, SOCK_RAW, IPPROTO_RAW);
int s = socket (AF_INET, SOCK_RAW, IPPROTO_TCP); int one = 1; if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, &one, sizeof (one)) < 0) { printf ("Error %d setting IP_HDRINCL: %s \n", errno, strerror(errno)); exit(0); }
Die normale Ausgabe erfolgt bei RAW-Sockets per sendto() oder sendmsg() unter Angabe der Zieladresse. Prinzipiell können auch write() oder send() aufgerufen werden, wenn der Socket verbunden ist.
Anders als bei den TCP- oder UDP-Sockets befinden sich im Lesepuffer nach Eintreffen eines Pakets nicht mehr nur die Nutzdaten, sondern auch die Headerdaten. Also zuerst die Daten des IP-Headers (nur bei IP_HDRINCL), dann die Headerdaten des Transportprotokolls (ICMP, TCP, UDP etc.) und danach erst die Daten, die auch ein gewöhnlicher Socket Übermittelt bekäme. Beim Empfang mit RAW-Sockets wird es daher komplizierter:
Wurde mit bind() gearbeitet, muss die Zieladresse des zu empfangenden Pakets der gebundenen Adresse entsprechen. Wurde mittels connect() die fremde Zieladresse fesgelegt, muss das zu empfangende Paket diese Adresse als Absender tragen.
Ist das Protokollfeld (dritter Parameter von socket) nicht 0, muss das empfangene Paket den gleichen Wert haben. Wurde weder bind() noch connect() verwendet und das Protokollfeld auf 0 gesetzt, empfängt der Socket eine Kopie eines jeden RAW-Datagramms, das der Kernel an RAW-Sockets weiterleitet.
/* lies_paket.c - liest ein ICMP-Paket per RAW Socket (ping) */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <netinet/in.h> #include <netinet/ether.h> /* Gibt ein Byte binaer aus */ void print_bin(unsigned char x) { int i; char erg[10]; erg[9] = '\0'; erg[8] = ' '; for (i = 7; i >= 0 ; i--) { erg[i] = (x%2 == 0)?'0':'1'; x = x >> 1; } printf("%s", erg); } int main(int argc, char *argv[]) { int j, k, sock, bytes; /* Puffer fuer ein Ethernet-Paket */ unsigned char packet[ETHERMTU]; // ETHERMTU = 1500 /* root-Rechte pruefen */ if(getuid() != 0) { printf("\nDu musst Root sein, um das zu machen!\n"); exit(1); } sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* Paket lesen */ bytes = read(sock, packet, sizeof(packet)); /* Ausgabe binaer/ASCII, immer 4 Bytes pro Zeile */ k = 0; /* Bytezaehler */ for (j = 0; j < bytes; j++) { k++; print_bin(packet[j]); if (packet[j] >= ' ' && packet[j] < 127) printf("%c ", packet[j]); else printf(". "); /* alle 4 Bytes Newline */ if (!(k % 4)) { printf ("\n"); } } printf("\n"); return 0; }Zum Testen startet man das Programm im Hintergrund (mit Root-Berechtigung) und ruft dann das ping-Kommando auf:
./lies_paket & ping -c 1 localhost >/dev/nullDie Ausgabe zeigt dann den Anfang des ICMP-Pakets, beginnend mit dem IP-Header:
01000101 E 00000000 . 00000000 . 01010100 T 00000000 . 00000000 . 01000000 @ 00000000 . 01000000 @ 00000001 . 00111100 < 10100111 . 01111111 . 00000000 . 00000000 . 00000001 . 01111111 . 00000000 . 00000000 . 00000001 . 00001000 . 00000000 . 10101110 . 01000111 G 01010111 W 11010100 . 00000000 . 00000001 . 00111111 ? 11110100 . 00110111 7 01010101 U 10001110 . 10010110 . 00000001 . 00000000 . 00001000 . 00001001 . 00001010 . 00001011 . 00001100 . 00001101 . 00001110 . 00001111 . 00010000 . 00010001 . 00010010 . 00010011 . 00010100 . 00010101 . 00010110 . 00010111 . 00011000 . 00011001 . 00011010 . 00011011 . 00011100 . 00011101 . 00011110 . 00011111 . 00100000 00100001 ! 00100010 " 00100011 # 00100100 $ 00100101 % 00100110 & 00100111 ' 00101000 ( 00101001 ) 00101010 * 00101011 + 00101100 , 00101101 - 00101110 . 00101111 / 00110000 0 00110001 1 00110010 2 00110011 3 00110100 4 00110101 5 00110110 6 00110111 7Wenn man die Ausgabe-Werte dann in das IP-Schema einträgt, ergibt sich unter anderem Typ = 4, IHL = 5, Type of Service = 1 (also ICMP), Source Address = 127.0.0.1, Destination Address 127.0.0.1. Bei ping gibt es keine weiteren IP-Optionen und daher beginnt in der sechsten Zeile bereits der Header des ICMP-Protokolls. Dort sieht man dann Typ = 8 (Echo Request), Code = 0 und die Checksumme. Ab Zeile sieben folgen die ICMP-Daten. wenn man nun noch ein ICMP-Sendeprogrmm schreibt, kann man damit Daten zwischen zwei Rechnern übermitteln, auch wenn alle TCP-Ports geschlossen sind, es muss nur ICMP zugelassen sein ...
Oben hat der IP-Header seine Minimallänge. Durch das Anfügen beliebiger Optionen kann er jedoch eine variable Größe haben (was für die Längen von fast allen Protokollheadern gilt). Daher ist die korrekte Bestimmung von Headerlängen im Programm unabdingbar. Sieht ein Protokoll eine variable Headerlänge vor, wird die Headerlänge in einem Pflichtfeld des Headers eingetragen. Bei IP befindet sich die Headerlänge im Feld IHL, das oben den Wert 5 hat. Brei der Auswertung der Headerlänge muss beachtet werden, dass es unterschiedliche Masseinheiten gibt. Im IHL des IPHeaders wird beispielsweise in 32-Bit-Einheiten gerechnet, weshalb ich auch im Programm nach jeweils 32 Bit eine neue Zeile begonnen habe. Somit ist der IHL-Wert (5) auch gleich der Zeilenzahl im Output. Auch bei TCP ist eine variable Headerlänge möglich. Hier wird wie bei IP die Headerlänge in 32-Bit-Einheiten angegeben und sie ist im Feld "Data Offset" gespeichert.
Die Network-Byte-Order ist eine Definition, bei festgelegt wird in welcher Reihenfolge die Bytes zu interpretieren sind. Allgemein wird die Byteorder zwischen Little-Endian und Big-Endian unterschieden. Big-Endian ist die Network-Byte-Order. Man sollte stets die entsprechenden Funktionen zum Konvertieren zwischen der Host- und der Network-Byte-Order verwenden - schon alleine wegen der besseren Portierbarkeit.
uint32_t htonl(uint32_t hostlong) host-to-network
uint16_t htons(uint16_t hostshort) host-to-network
uint32_t ntohl(uint32_t netlong) network-to-host
uint16_t ntohs(uint16_t netshort) network-to-host
u_int16_t csum(unsigned short *ptr, int nbytes) { u_int32_t sum; /* Summenvariable */ u_int16_t oddbyte; /* fuer Addition bei ungerader Blocklaenge */ sum = 0; while(nbytes > 1) /* ueber den Datenblock summieren */ { sum += *ptr++; /* 16-Bit-Wert summieren, daher nbytes um 2 vermindern */ nbytes -= 2; } if(nbytes == 1) /* ungerde Blocklaenge (eher selten) */ { oddbyte = 0; /* letztes Byte addieren */ *((u_char*)&oddbyte) = *(u_char*)ptr; sum += oddbyte; } /* die oberen und unteren 16 Bit der 32-Bit-Summe addieren */ sum = (sum >> 16) + (sum & 0xffff); sum = sum + (sum >> 16); /* Uebertrag beruecksichtigen */ /* Einerkomplement bilden */ return (u_int16_t)~sum; }Die manuell zusammengeschraubten IP- UDP- oder TCP-Paketebrauchen natürlich eine korrekte Prüfsumme. Zu deren Berechnung wird oft ein sogenannter Pseudo-Header gebaut, der Informationen aus dem IP- und dem TCP-Header enthält. Die Nutzung von Informationen aus dem IP-Header ermöglicht die Erkennung von fehlerhaft gerouteten Paketen. Die UDP-Prüfsumme berechnet sich genauso wie die TCP-Prüfsumme. Da bei UDP die Prüfsumme optional ist, kann alternativ auch das Einer-Komplement von Null angegeben werden (lauter 1-Bits). Prinzipiell sollte man aber immer eine korrekte Prüfsumme bilden. Für die Generierung des Pseudo-Headers definiert man eine eigene Datenstruktur:
struct pseudo_header { u_int32_t source_address; u_int32_t dest_address; u_int8_t placeholder; u_int8_t protocol; u_int16_t tcp_length; };Die Felder source_address und dest_address enthalten die Absender- und Empfänger-IP-Adresse aus dem IP-Header. Bei IPv4 muss das Feld placeholder mit Nullen gefüllt werden, damit die Länge des Pseudo-Headers ein Vielfaches von 16 Bit beträgt. Im Feld protocol befindet sich die Protokollnummer (IPPROTO_xxx) Layer-4-Protokolls (z. B. TCP: IPPROTO_TCP = 6, UDP: IPPROTO_UDP = 17), die auch dem IP-Header entnommen wird. tcp_length enthält die Länge des UDP- oder TCP-Pakets in Bytes, also die Länge vom Beginn des UDP- oder TCP-Headers bis zum Ende der Nutzdaten. Der Pseudo-Header dient allein der Prüfsummenberechnung und wird nicht zum Senden verwendet.
... struct pseudo_header psh; ... psh.source_address = inet_addr( source_ip ); psh.dest_address = sin.sin_addr.s_addr; psh.placeholder = 0; psh.protocol = IPPROTO_TCP; psh.tcp_length = htons(sizeof(struct tcphdr) + strlen(data)); ...Zur Prüfsummenberechnung werden dann Pseudoheader, TCP-Header und Datenpuffer hintereinander in einen "Pseudo-Buffer" gepackt und über diesen dann die Prüfsumme berechnet.
... /* Pseudo-Puffer allokieren */ int ps_size = sizeof(struct pseudo_header) + sizeof(struct tcphdr) + strlen(data); ps_buffer = malloc(ps_size); /* Pseudo-Buffer fuellen (tcph und data sind Pointer auf die entsprechenden Bereiche */ memcpy(ps_buffer, (char*) &psh, sizeof (struct pseudo_header)); memcpy(ps_buffer + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr)); memcpy(ps_buffer + sizeof(struct pseudo_header) + sizeof(struct tcphdr), data, strlen(data)); /* Pruefsumme berechnen */ tcph->check = csum((unsigned short*) ps_buffer, ps_size); ...Beachten Sie auch, dass die Gesamtlänge der Daten einem Vielfachen von 16 Bit entsprechen muss. Gegebenenfalls sind Nullen aufzufüllen.
Im Gegensatz zur Absenderadresse ist die Angabe einer Empfängeradresse immer notwendig. Um einen Empfänger für jedes einzelne, ausgehende Paket zu definieren, bietet sich die Funktion sendto() an:
ssize_t sendto(int s, /* Socket */ const void *buf, /* Datenpuffer */ size_t len, /* Laenge Datenpuffer */ int flags, /* routing flags, in der Regel 0 */ const struct sockaddr *to, /* Ziel-IP-Adresse */ socklen_t tolen); /* Laenge Ziel-IP-Adresse */Neben den anderen Parametern benötigt sendto() auch eine Ziel-IP-Adresse in der Struktur sockaddr. Je nach Anwendungsfall wird diese allgemeine Struktur durch eine spezielle, fallbezogene Socket-Adress-Struktur ersetzt. So wird für IP-Pakete die struct sockaddr_in verwendet (siehe Kapitel 1). Deshalb ist auch die Angabe deren Länge notwendig.
Nach dem ioctl()-Aufruf befindet sich die Ankunftszeit des letzten Paketes als Zeitstempel in der Zeitvariablen und kann beispielsweise mittels strftime() in einen lesbaren String umgewandelt werden. Das folgende Listig zeigt, wie die Zeit abgefragt wird.
/* lastpacket.c - Ankunftszeit des letzten Pakets */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <netinet/ip.h> #include <sys/ioctl.h> #include <time.h> int main(int argc, char *argv[]) { int s; /* Socket-Handle */ int bytes; /* Bytes gelesen */ struct timeval t_last; /* Zeit/Datum inkl. Mikrosekunden */ time_t ctime; /* Zeit/Datum (Sekunden) */ int mikrosek; /* Anteil Mikrosekunden */ char time_buffer[30]; /* Puffer fuer Datum + Uhrzeit */ char packet[1600]; /* Datenpaket */ /* Socket kreieren */ s = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP); if (s == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } while(1) { /* Datenpaket lesen */ bytes = read(s, packet, sizeof(packet)); /* Ankuftszeit des letzten Paketes lesen und aufspalten */ ioctl(s, SIOCGSTAMP, &t_last); ctime = t_last.tv_sec; mikrosek = (int)t_last.tv_usec; /* Datum/Zeit in String umwandeln und ausgeben */ strftime(time_buffer, 30, "%Y-%m-%d, %T", localtime(&ctime)); printf("%s.%06d\n", time_buffer, mikrosek); } return(0); }Nun wird das Programm im Hintergrund gestartet und anschließend ein "stiller" ping ausgelöst. Man sieht dann sehr gut, dass für jeden Ping zwei dicht aufeinanderfolgende Zeitstempel ausgegeben werden - für den Echo Request und den darauf folgenden Echo Reply. Anschliessend wird das Programm wieder in den Vordergrund geholt und beendet.
# ./lastpacket & [1] 14847 # ping -c 10 localhost >/dev/null 2015-04-26, 13:34:31.917398 2015-04-26, 13:34:31.917433 2015-04-26, 13:34:32.916399 2015-04-26, 13:34:32.916421 2015-04-26, 13:34:33.916692 2015-04-26, 13:34:33.916714 2015-04-26, 13:34:34.915694 2015-04-26, 13:34:34.915715 2015-04-26, 13:34:35.916692 2015-04-26, 13:34:35.916721 2015-04-26, 13:34:36.915693 2015-04-26, 13:34:36.915722 2015-04-26, 13:34:37.915192 2015-04-26, 13:34:37.915220 2015-04-26, 13:34:38.915198 2015-04-26, 13:34:38.915219 2015-04-26, 13:34:39.916692 2015-04-26, 13:34:39.916715 2015-04-26, 13:34:40.915689 2015-04-26, 13:34:40.915711 # fg ./lastpacket ^C
#include <sys/socket.h> #include <sys/types.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> /* Ziel (und Quelle) ist localhost */ #define DEST "127.0.0.1" int main(void) { int s; /* Sockethandle */ struct sockaddr_in daddr; /* IP-Adresse */ char packet[50]; /* Datenpaket-Puffer */ /* Der IP-Header zeigt auf den Pufferanfang (ueberlagert den Puffer) */ struct iphdr *ip = (struct iphdr *)packet; /* RAW-Socket kreieren */ s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (s == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* IP-Adresse etc. eintragen, Port wird bei SOCK_RAW nicht gebraucht */ daddr.sin_family = AF_INET; daddr.sin_port = 0; /* IP-Adresse (String) in 32-Bit-Binaerwert umwandeln */ inet_pton(AF_INET, DEST, (struct in_addr *)&daddr.sin_addr.s_addr); /* Struktur mit Nullen fuellen */ memset(daddr.sin_zero, 0, sizeof(daddr.sin_zero)); /* Puffer loeschen */ memset(packet, 0, sizeof(packet)); /* Nachricht als Nutzdaten in den Puffer schreiben, aber hinter(!) den IP-Header */ strcpy(packet + sizeof(struct iphdr),"Here comes the Evil!"); /* IP-Header zusammenbauen */ ip->ihl = 5; /* Headerlaenge, 5 x 32 Bit */ ip->version = 4; /* IP-Version 4 */ ip->tos = 0; /* Type of Service */ ip->tot_len = htons(40); /* 16-Byte-Wert */ ip->frag_off = 0; /* keine Fragmentierung */ ip->ttl = 64; /* default */ ip->protocol = IPPROTO_RAW; /* Protocol auf Layer 4 */ ip->check = 0; /* wird autom. berechnet */ ip->saddr = daddr.sin_addr.s_addr; /* Quell-Adresse */ ip->daddr = daddr.sin_addr.s_addr; /* Ziel-Adresse */ /* Nachricht senden */ while(1) { sleep(1); if (sendto(s, /* Socket */ (char *)packet, /* Datenpuffer */ sizeof(packet), /* Laenge Datenpuffer */ 0, /* routing flags, in der Regel 0 */ (struct sockaddr *)&daddr, /* Ziel-IP-Adresse */ (socklen_t)sizeof(daddr) /* Laenge Ziel-IP-Adresse */ ) < 0) perror("packet send error:"); } exit(0); }Das Listing zeigt auch ganz deutlich, dass hier wirklich nur das IP-Protokoll zum Einsatz kommt.
Denken Sie immer daran, den gesamten Schreibpuffer mit Nullen aufzufüllen, da eine sorgfältige Initialisierung von Speicherbereichen bei der Generierung von Netzwerkpaketen besonders wichtig ist. Häufig kommt es vor, dass bestimmte Felder in Protokollheadern für den konkreten Anwendungsfall nicht relevant sind und somit leer bleiben müssen. Ein nicht initialisierter Schreibpuffer führt jedoch oft zu einer unvorhersehbaren Belegung der nicht genutzen Felder und dann funktioniert das Programm nicht. Beachten Sie auch, Werte, die mehr als ein Byte Speicherplatz belegen, in die Network Byte-Order zu transformieren.
#include <sys/socket.h> #include <sys/types.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> /* Ziel (und Quelle) ist localhost */ #define DEST "127.0.0.1" int main(void) { int s; /* Sockethandle */ struct sockaddr_in saddr; /* IP-Adresse */ char packet[50]; /* Datenpaket-Puffer */ int i, fromlen; /* RAW-Socket kreieren */ s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (s == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* Puffer loeschen */ memset(packet, 0, sizeof(packet)); fromlen = sizeof(saddr); /* Nachricht empfangen */ while(1) { if (recvfrom(s, (char *)&packet, sizeof(packet), 0, (struct sockaddr *)&saddr, (socklen_t *)&fromlen) < 0) perror("packet receive error"); /* Nur die Daten nach dem IP-Header ausgeben */ i = sizeof(struct iphdr); while (i < sizeof(packet)) { if (packet[i] >= ' ' && packet[i] < 127) printf("%c", packet[i]); else printf("."); i++; } printf("\n"); } exit(0); }Senden und Empfangen kann man gut mit Hilfe von tcpdump studieren:
# tcpdump -i lo -X -vv tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes 18:28:38.593513 IP (tos 0x0, ttl 64, id 15899, offset 0, flags [none], proto unknown (255), length 50) localhost > localhost: ip-proto-255 30 0x0000: 4500 0032 3e1b 0000 40ff 3db0 7f00 0001 E..2>...@.=..... 0x0010: 7f00 0001 2048 6572 6520 636f 6d65 7320 .....Here.comes. 0x0020: 7468 6520 4576 696c 2100 0000 0000 0000 the.Evil!....... 0x0030: 0000 .. 18:28:39.593608 IP (tos 0x0, ttl 64, id 15900, offset 0, flags [none], proto unknown (255), length 50) localhost > localhost: ip-proto-255 30 0x0000: 4500 0032 3e1c 0000 40ff 3daf 7f00 0001 E..2>...@.=..... 0x0010: 7f00 0001 2048 6572 6520 636f 6d65 7320 .....Here.comes. 0x0020: 7468 6520 4576 696c 2100 0000 0000 0000 the.Evil!....... 0x0030: 0000 .. 18:28:40.593703 IP (tos 0x0, ttl 64, id 15901, offset 0, flags [none], proto unknown (255), length 50) localhost > localhost: ip-proto-255 30 0x0000: 4500 0032 3e1d 0000 40ff 3dae 7f00 0001 E..2>...@.=..... 0x0010: 7f00 0001 2048 6572 6520 636f 6d65 7320 .....Here.comes. 0x0020: 7468 6520 4576 696c 2100 0000 0000 0000 the.Evil!....... 0x0030: 0000 .. 18:28:41.593800 IP (tos 0x0, ttl 64, id 15902, offset 0, flags [none], proto unknown (255), length 50) localhost > localhost: ip-proto-255 30 0x0000: 4500 0032 3e1e 0000 40ff 3dad 7f00 0001 E..2>...@.=..... 0x0010: 7f00 0001 2048 6572 6520 636f 6d65 7320 .....Here.comes. 0x0020: 7468 6520 4576 696c 2100 0000 0000 0000 the.Evil!....... 0x0030: 0000 .. ^C 4 packets captured 8 packets received by filter 0 packets dropped by kernelDie Ausgabe des Empfangsprogramms zeig schließlich das gewünschte Ergebnis:
# ./rawrcv Here comes the Evil!.......... Here comes the Evil!.......... Here comes the Evil!.......... Here comes the Evil!.......... Here comes the Evil!.......... Here comes the Evil!.......... Here comes the Evil!.......... ^CDamit haben Sie nun zwei Programme, die auf IP-Ebene kommunizieren können. Aber denken Sie daran, dass keinerlei Bindung an eine bestimmte IP-Adresse erfolgte. Bei mehreren Sendern oder Empfängern bekommt immer jeder alles mit.
Wenn Sie nicht mit RAW-Sockets auf der Basis "Senden und Vergessen" arbeiten, wollen Sie irgendwann auch die Antwortpakete Ihrer RAW-Pakete lesen. Die Entscheidungslogik dafür, ob ein Paket an einen RAW-Socket geliefert wird, hängt vom Inhalt ab:
Im folgenden Listing wird ein ICMP-Echo-Request generiert. Absender- und Empfängeradresse werden über die Kommandozeile eingegeben (Dotted Quad). Bitte beachten Sie, dass für die angegebenen Adressen keinerlei Prüfung implementiert ist. Wenn Sie da einen Fehler machen, passiert im Programm meist gar nichts. Der RAW-Socket wird angelegt und die IP_HDRINCL-Option gesetzt, was bedeutet, dass dier IP-Header im Programm generiert wird. Des weiteren wird die Option SO_BROADCAST gesetzt, die es erlauben würde, als Zieladresse auch eine Broadcast-Adresse anzugeben. Da ist aber Vorsicht geboten, denn damit kann man ein Netz recht schnell mit Paketen fluten (also liebe Kinder: Nicht im der Öffentlichkeit machen). Der IP-Header siht fast genauso aus wie oben. Neu ist dagegen der darauf folgende ICMP-Header.
Der Pointer auf die Datenstruktur des ICMP-Headers zeigt hinter den IP-Header:
struct icmphdr *icmp = (struct icmphdr *) (packet + sizeof (struct iphdr));Dann wird der Header gefüllt, wobei ID und Sequence-Number auf Zufallswerte gesetzt werden. Nun wird, wie schon oben, für die sendto()-Funktion noch die Zeiladresse passend umgeformt und es kann ans Senden gehen. Bei der Generierung des Headers oberhalb des IP-Layers erhält man prinzipiell keine Unterstützung durch die Socket-API oder den Kernel, weshalb die ICMP-Prüfsumme selbst berechnet und eingetragen werden muss. Im Beispiel hat das Paket noch 64 Byte Nutzdaten, die aus Zufallswerten bestehen. Dabei wird bei jedem Schleifendurchlauf die Nutzlast neu generiert - und damit natürlich auch die Prüfinformation. Der anschließende Aufruf von socket()ist identisch mit den Aufrufen der Listings weiter oben.
/* ICMP ping flood example in c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <time.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <arpa/inet.h> #define PAYLOAD_SIZE 64 /* 64 Byte Nutzdaten */ //typedef unsigned char u8; //typedef unsigned short int u16; unsigned short csum(unsigned short *ptr, int nbytes); //unsigned short in_cksum(unsigned short *ptr, int nbytes); int main(int argc, char **argv) { unsigned long daddr; /* Ziel-IP-Adresse */ unsigned long saddr; /* Quell-IP-Adresse */ int packet_size; /* Paketgroesse (wird berechnet) */ char *packet; /* Pointer auf das Paket */ int sent; /* Anzahl gesendete Pakete */ int sent_size; /* Resultatwert von sendto() */ int on = 1; /* fuer setsockopt() */ if (argc < 3) { printf("usage: %s <source IP> <destination IP>\n", argv[0]); exit(0); } saddr = inet_addr(argv[1]); daddr = inet_addr(argv[2]); /* RAW-Socket anlegen - Wenn IPPROTO_ICMP angegeben wird, berechnet der Kernel die ICMP-Checksumme, bei IPPROTO_RAW macht er nichts */ int sockfd = socket (AF_INET, SOCK_RAW, IPPROTO_RAW); if (sockfd < 0) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* IP-Header mache ich von Hand, deshalb: */ if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, (const char*)&on, sizeof (on)) == -1) { perror("setsockopt() - IP-HDRINCL ging schief"); return 1; } /* Broadcast-Addresse als Zieladresse erlauben (Hackeralarm!) */ if (setsockopt (sockfd, SOL_SOCKET, SO_BROADCAST, (const char*)&on, sizeof (on)) == -1) { perror("setsockopt() - SO_BROADCAST ging schief"); return 1; } /* Paketgroesse berechnen und Speicher fuer den Puffer allokieren */ packet_size = sizeof (struct iphdr) + sizeof (struct icmphdr) + PAYLOAD_SIZE; packet = (char *) malloc (packet_size); if (!packet) { perror("out of memory"); close(sockfd); return 1; } /* Puffer mit Nullen fuellen */ memset (packet, 0, packet_size); /* IP-Header zusammenbasteln, der Pointer ip zeigt auf den Pufferanfang */ struct iphdr *ip = (struct iphdr *) packet; ip->version = 4; ip->ihl = 5; ip->tos = 0; ip->tot_len = htons (packet_size); ip->id = rand (); ip->frag_off = 0; ip->ttl = 255; ip->protocol = IPPROTO_ICMP; ip->check = 0; ip->saddr = saddr; ip->daddr = daddr; /* Macht der Kernel: ip->check = in_cksum ((u16 *) ip, sizeof (struct iphdr)); */ /* ICMP-Header zusammenbasteln, der Pointer icmp zeigt auf den Puffer hinter dem IP-Header */ struct icmphdr *icmp = (struct icmphdr *) (packet + sizeof (struct iphdr)); icmp->type = ICMP_ECHO; icmp->code = 0; icmp->un.echo.sequence = rand(); icmp->un.echo.id = rand(); icmp->checksum = 0; /* Adress-Struktur belegen */ struct sockaddr_in servaddr; servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = daddr; memset(&servaddr.sin_zero, 0, sizeof (servaddr.sin_zero)); puts("flooding..."); sent = 0; while (sent < 100) { /* Nutzlast mit Zufallswerten fuellen */ memset(packet + sizeof(struct iphdr) + sizeof(struct icmphdr), rand() % 255, PAYLOAD_SIZE); /* ICMP-Checksumme berechnen */ icmp->checksum = 0; icmp->checksum = csum((unsigned short *)icmp, sizeof(struct icmphdr) + PAYLOAD_SIZE); /* Paket senden */ sent_size = sendto(sockfd, packet, packet_size, 0, (struct sockaddr*) &servaddr, sizeof (servaddr)); if (sent_size < 1) { perror("sendto ging schief\n"); break; } sent++; printf("%d packets sent\r", sent); fflush(stdout); /* 10 ms warten */ usleep(10000); } free(packet); close(sockfd); return 0; } unsigned short csum(unsigned short *ptr, int nbytes) { u_int32_t sum; /* Summenvariable */ u_int16_t oddbyte; /* fuer Addition bei ungerader Blocklaenge */ sum = 0; while(nbytes > 1) /* ueber den Datenblock summieren */ { sum += *ptr++; /* 16-Bit-Wert summieren, daher nbytes um 2 vermindern */ nbytes -= 2; } if(nbytes == 1) /* ungerde Blocklaenge (eher selten) */ { oddbyte = 0; /* letztes Byte addieren */ *((u_char*)&oddbyte) = *(u_char*)ptr; sum += oddbyte; } /* die oberen und unteren 16 Bit der 32-Bit-Summe addieren */ sum = (sum >> 16) + (sum & 0xffff); sum = sum + (sum >> 16); /* Uebertrag beruecksichtigen */ /* Einerkomplement bilden */ return (u_int16_t)~sum; }Das Programm generiert nur 100 Pakete und wartet auch fairerweise nach jedem Paket 10 ms, so dass keine merkbar erhöhte Netzlast entsteht. Als Absender-IP können Sie natürlich nach Lust und Laune angeben, was Sie möchten (Spoofing), z. B.
# ./ping 10.10.10.10 127.0.0.1Mit tcpdump können Sie das Programm beobachten. Man sieht dabei auch, dass der Kernel einen Echo-Reply von sich aus generiert:
~# tcpdump -i lo icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes 11:18:36.926887 IP 10.10.10.10 > localhost: ICMP echo request, id 27032, seq 50723, length 72 11:18:36.926918 IP localhost > localhost: ICMP echo reply, id 27032, seq 50723, length 72 11:18:36.937069 IP 10.10.10.10 > localhost: ICMP echo request, id 27032, seq 50723, length 72 11:18:36.937090 IP localhost > localhost: ICMP echo reply, id 27032, seq 50723, length 72 11:18:36.947187 IP 10.10.10.10 > localhost: ICMP echo request, id 27032, seq 50723, length 72 11:18:36.947208 IP localhost > localhost: ICMP echo reply, id 27032, seq 50723, length 72 11:18:36.957299 IP 10.10.10.10 > localhost: ICMP echo request, id 27032, seq 50723, length 72 11:18:36.957318 IP localhost > localhost: ICMP echo reply, id 27032, seq 50723, length 72 11:18:36.967414 IP 10.10.10.10 > localhost: ICMP echo request, id 27032, seq 50723, length 72 11:18:36.967436 IP localhost > localhost: ICMP echo reply, id 27032, seq 50723, length 72 ...Das Programm kann um eine Empfangsfunktion erweitert werden, indem man nach dem Senden auf Empfang umschaltet. Deshalb wird hier nur der neue Code für den Empfang aufgelistet. Im restlichen Programm gibt es nur marginale Änderungen (einige Variablen sind neu und einige Codezeilen wurden verschoben). Das komplette Programm ping2.c finden Sie, wie auch alle anderen Programme, bei den Beispielen.
... printf("Packet sent\n"); /* Nun wird auf die Antwort gewartet */ len = sizeof (servaddr); servaddr.sin_addr.s_addr = saddr; rcv_size = recvfrom(sockfd, packet, sizeof(struct iphdr) + sizeof(struct icmphdr), 0, (struct sockaddr*) &servaddr, &len); if (rcv_size == -1) { perror("recvfrom ging schief\n"); return 1; } printf("Received %d bytes\n", rcv_size); /* IP-Header auswerten */ struct iphdr* ip_reply = (struct iphdr*) packet; printf("ID: %d\n", ntohs(ip_reply->id)); printf("TTL: %d\n", ip_reply->ttl); printf("IHL: %d\n", ip_reply->ihl); printf("SRC: %s\n", inet_ntoa(*(struct in_addr*)&ip_reply->saddr)); printf("DST: %s\n", inet_ntoa(*(struct in_addr*)&ip_reply->daddr)); /* ICMP-Header auswerten */ struct icmphdr *icmp_r = (struct icmphdr*) (packet + ip_reply->ihl*4); if (icmp_r->un.echo.id == pid) { printf("ICMP: type[%d/%d] checksum[%d] id[%d] seq[%d]\n\n", icmp_r->type, icmp_r->code, ntohs(icmp_r->checksum), icmp_r->un.echo.id, icmp_r->un.echo.sequence); } ...Die Ausgabe des Programms sieht dann folgendermaßen aus:
# ./ping2 127.0.0.2 127.0.0.1 Sending with pid=17767 ... Packet sent Received 28 bytes ID: 50723 TTL: 255 IHL: 5 SRC: 127.0.0.2 DST: 127.0.0.1 ICMP: type[8/0] checksum[31863] id[17767] seq[39017]
Im Paketpuffer steht nun am Anfang der IP-Header, auf den der TCP-Header folgt. Da bei einem SYN-Paket keine Nutzdaten gesendet werden, endet das Paket nach dem TCP-Header. Im Programm ist in einem Kommentar aber gezeigt, wie die Nutzdaten einzubinden wären. In diesem Beispiel kommt erstmals auch der oben besprochene Pseudoheader zum Berechnen der Checksumme zu Einsatz. Zudem muss der Programmierer darauf achten, dass im IP-Header die richtige Paketlänge eingetragen wird. Die größte Schwierigkeit dürfte bei Lesen des Codes die Pointer-Verschachtelung und die Berechnung der Positionen bzw. Blocklängen darstellen.
Beim IP-Header muss man bei Verwendung von RAW-Sockets und der Option IP_HDRINCL einige Felder automatisch vom Kernel ausfüllen lassen. Die IP-Prüfsumme wird grundsätzlich vom Kernel berechnet. Setzt man das ID-Feld im IP-Header auf 0, setzt der Kernel automatisch einen gültigen Wert ein. Der TCP-Header wird genauso generiert wie der IP-Header oder der ICMP-Header im vorangegangenen Beispiel. Dank der Datenstruktur tcphdr ist der Zugriff auf die einzelnen Felder des TCP-Headers, insbesondere auch auf die Flag-Bits, recht einfach und übersichtlich. In diesem Beispiel werden auch einfach die IP-Adressen und Portnummern aus der Kommandozeile ohne jegliche Fehlerprüfung übernommen, was hier der Übersichtlichkeit wegen geschieht. In einem produktiv verwendeten Programm ist an der Stelle natürlich eine eingehende Prüfung der Parameter unabdingbar.
Die TCP-/UDP-Prüfsumme wird über den Pseudo-Header, den TCP/UDP-Header sowie über die Nutzdaten gebildet. Vor der Prüfsummenberechnung müssen daher alle benötigten Daten hintereinander im Speicher abgelegt werden. Dazu wird dynamisch ein Puffer passender Größe bereitgestellt, der dann mittels etwas Pointerarithmetik mit den drei Headern (und ggf. mit Daten) gefüllt wird. Über diesen Puffer berechnet das Programm anschließend die Prüfsumme. Danach wird der Puffer wieder freigegeben. Den Ablauf zeigt der folgende Programmausschnitt:
/* Pseudoheader pshdr erstellen */ pshdr.src_addr = ip->saddr; /* Source-IP-Adresse */ pshdr.dst_addr = ip->daddr; /* Destination-IP-Adresse */ pshdr.padding = 0; /* Fuellbits */ pshdr.proto = ip->protocol; /* Protokoll der Schicht oberhalb von IP IPPROTO_UDP oder IPPROTO_TCP */ pshdr.length = htons(sizeof(struct tcphdr) + PAYLOAD_SIZE); /* Laenge Pseudoheader */ /* Pseudoheader, TCP-Header und Daten kombinieren */ /* Zuerst Speicher fuer den Puffer allokieren */ pseudogram = malloc(sizeof(struct pseudoheader) + sizeof(struct tcphdr) + PAYLOAD_SIZE); /* Pseudoheader an den Pufferanfang kopieren */ memcpy(pseudogram, (char*) &pshdr, sizeof(struct pseudoheader)); /* TCP-Header hinter denPseudoheader kopieren */ memcpy(pseudogram + sizeof(struct pseudoheader), tcp, sizeof(struct tcphdr) + PAYLOAD_SIZE); /* hier wuerden dann fallweise die Daten hinter den TCP-Header kopiert */ /* Checksumme berechnen */ tcp->check = csum((unsigned short*)pseudogram, sizeof(struct pseudoheader) + sizeof(struct tcphdr) + PAYLOAD_SIZE); /* Speicher wieder freigeben */ free(pseudogram);Damit ist dann auch die schwierigste Aufgabe erledigt. Der Programmanfang enthält die üblichen Include-Anweisungen und die Variablendefinitionen. Nach dem Berechnen der Checksumme folgt nur noch die Adressdefinition für dieZieladresse und der Aufruf der sendto()-Funktion. Bitte denken Sie daran, dass dieses Programm nur den Start des Verbindungsaufbaus realisiert, also noch nicht einmal einen komplette TCP-Verbindung aufbaut.
/* Dieses Programm generiert ein TCP-SYN-Paket */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <time.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/tcp.h> #define PAYLOAD_SIZE 0 /* SYN-Paket -> keine Nutzdaten */ /* Pseudo-Datenstrukturen zur Bildung der Pruefsumme */ struct pseudoheader { u_int32_t src_addr; u_int32_t dst_addr; u_int8_t padding; u_int8_t proto; u_int16_t length; }; unsigned short csum(unsigned short *ptr, int nbytes); int main(int argc, char *argv[]) { int s; /* Socket-Deskriptor */ int bytes; /* Byte-Zaehler */ int buffer_size; /* Paketgroesse (wird berechnet) */ char *buffer; /* Pointer auf das Paket */ struct iphdr *ip; /* Pointer auf IP-Struktur */ struct tcphdr *tcp; /* Pointer auf TCP-Struktur */ char *data; /* Pointer auf Nutzdatenbereich */ char *pseudogram; /* Pointer auf den Checksum-Puffer */ struct sockaddr_in destination; /* fuer Ziel-Adressierung */ struct pseudoheader pshdr; /* der Pseudoheader */ int on = 1; /* fuer setsockopt() */ // Anzahl der Programm-Parameter pruefen if (argc != 5) { fprintf(stderr, "Usage: %s <src-addr> ", argv[0]); fprintf(stderr, "<src-port> <dest-addr> <dest-port>\n"); return 1; } /* RAW-Socket aufbauen */ s = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if (s < 0) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* IP-Header mache ich von Hand, deshalb: */ if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, (const char*)&on, sizeof (on)) == -1) { perror("setsockopt() - IP-HDRINCL ging schief"); return 1; } buffer_size = sizeof (struct iphdr) + sizeof (struct tcphdr) + PAYLOAD_SIZE; buffer = (char *) malloc (buffer_size); /* Puffer mit Nullen fuellen */ memset (buffer, 0, sizeof(buffer)); /* IP-Header zusammenbasteln, der Pointer ip zeigt auf den Pufferanfang */ ip = (struct iphdr*) buffer; ip->version = 4; ip->ihl = 5; ip->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + PAYLOAD_SIZE); /* ID automatisch vom Kernel beziehen */ ip->id = 0; ip->ttl = 255; ip->protocol = IPPROTO_TCP; ip->saddr = inet_addr(argv[1]); ip->daddr = inet_addr(argv[3]); ip->check = 0; /* Checksum macht der Kernel */ /* TCP Header mit Werten fuellen, der Pointer zeigt hinter den IP-Header */ tcp = (struct tcphdr*) (buffer + sizeof(struct iphdr)); tcp->source = htons(atol(argv[2])); tcp->dest = htons(atol(argv[4])); tcp->ack_seq = 0; tcp->seq = random(); tcp->doff = 5; tcp->window = htons(65535); /* TCP-Flags setzen */ tcp->fin = 0; tcp->syn = 1; tcp->rst = 0; tcp->psh = 0; tcp->ack = 0; tcp->urg = 0; /* Datenbereich, der Pointer zeigt hinter den TCP-Header */ /* Hier auskommentiert, da ein SYN-Paket normalerweise * keine Daten enthaelt - es wird nur gezeigt, wie man * Daten eintragen wuerde * data = (char*) (buffer + sizeof(struct iphdr) + sizeof(struct tcphdr)); * strcpy(data, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); */ /* Pseudoheader erstellen */ pshdr.src_addr = ip->saddr; pshdr.dst_addr = ip->daddr; pshdr.padding = 0; pshdr.proto = ip->protocol; pshdr.length = htons(sizeof(struct tcphdr) + PAYLOAD_SIZE); /* Pseudoheader, TCP-Header und Daten kombinieren */ pseudogram = malloc(sizeof(struct pseudoheader) + sizeof(struct tcphdr) + PAYLOAD_SIZE); memcpy(pseudogram, (char*) &pshdr, sizeof(struct pseudoheader)); memcpy(pseudogram + sizeof(struct pseudoheader), tcp, sizeof(struct tcphdr) + PAYLOAD_SIZE); tcp->check = csum((unsigned short*)pseudogram, sizeof(struct pseudoheader) + sizeof(struct tcphdr) + PAYLOAD_SIZE); free(pseudogram); /* Speicher wieder freigeben */ /* Statistik ausgeben */ printf ("TCP Pruefsumme: %i\n", tcp->check); printf ("Ziel : %s:%i\n", argv[3], ntohs(tcp->dest)); printf ("Quelle: %s:%i\n", argv[1], ntohs(tcp->source)); /* Socket-Adress-Struktur fuer sendto() */ destination.sin_addr.s_addr = ip->daddr; destination.sin_family = AF_INET; destination.sin_port = tcp->dest; /* Paket versenden */ bytes = sendto(s, buffer, ntohs(ip->tot_len), 0, (struct sockaddr*)&destination, sizeof(destination)); if (bytes == -1) { perror("sendto() ging schief"); return 1; } return 0; } unsigned short csum(unsigned short *ptr, int nbytes) { u_int32_t sum; /* Summenvariable */ u_int16_t oddbyte; /* fuer Addition bei ungerader Blocklaenge */ sum = 0; while(nbytes > 1) /* ueber den Datenblock summieren */ { sum += *ptr++; /* 16-Bit-Wert summieren, daher nbytes um 2 vermindern */ nbytes -= 2; } if(nbytes == 1) /* ungerde Blocklaenge (eher selten) */ { oddbyte = 0; /* letztes Byte addieren */ *((u_char*)&oddbyte) = *(u_char*)ptr; sum += oddbyte; } /* die oberen und unteren 16 Bit der 32-Bit-Summe addieren */ sum = (sum >> 16) + (sum & 0xffff); sum = sum + (sum >> 16); /* Uebertrag beruecksichtigen */ /* Einerkomplement bilden */ return (u_int16_t)~sum; }Nachdem keine komplette Verbindung aufgebaut wird, kann man auf der Gegenseite auch keinen TCP-Client wie z. .b telnet oder einer der Programme aus Kapitel 1 verwenden. Aber mit der Hilfe von tcpdump läßt sich die Tätigkeit des Programms verfolgen. Zunächst die unspektakuläre Ausgabe des Programms selbst:
# ./raw_tcp_syn 127.0.0.2 65000 127.0.0.1 22 TCP Pruefsumme: 39489 Ziel : 127.0.0.1:22 Quelle: 127.0.0.2:65000
In einem zweiten Fenster kann man dann wieder den tcpdump laufen lassen. Man sieht zuerst das vom Programm gesendete Paket mit den SYN-flag und die Antwort von Kernel (Flag "S."). Weil das Programm nach dem Senden des Pakets beendet wird, generiert der Kernel noch ein Abschlusspaket mit den RST-Flag. Stellen Sie sich einmal vor, das Programm würde nicht enden, sondern in einer Schleife immer neue Pakete produzieren. Dann würden für jede dieser "halboffene" Verbindung Netzwerk-Ressourcen belegt - solange, bis nichts mehr geht. Natürlich ist der Kernel nicht total bekloppt und schliesst die Verbindungen nach einer gewissen Zeit. Wenn aber mehr Verbindungen generiert als geschlossen werden, kann das System keine "echten" Anfragen mehr beantworten. Man spricht dann von einem "Denial of Service"-Angriff.
# tcpdump -i lo -vv tcp tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes 18:35:57.871027 IP (tos 0x0, ttl 255, id 15894, offset 0, flags [none], proto TCP (6), length 1064) 127.0.0.2.65000 > localhost.ssh: Flags [S], cksum 0x419a (correct), seq 1732610923:1732611947, win 65535, length 1024 18:35:57.871088 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 44) localhost.ssh > 127.0.0.2.65000: Flags [S.], cksum 0xc23f (correct), seq 1019801577, ack 1732610924, win 32792, options [mss 16396], length 0 18:35:57.871107 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40) 127.0.0.2.65000 > localhost.ssh: Flags [R], cksum 0xc12c (correct), seq 1732610924, win 0, length 0
Ein PACKET-Socket wird dabei durch die Angabe der Adressfamilie AF_PACKET als Spezialfall eines RAW-Sockets definiert:
s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));Statt htons(ETH_P_ALL) wird beim dritten Parameter auch oft IPPROTO_RAW angegeben. Dieser Parameter gibt an, mit welchen Protokollen der PACKET-Socket arbeiten soll. Gültige Konstanten der Form finden Sie in der Headerdatei linux/if_ether.h. Bei der Verwendung von ETH_P_ALL akzeptiert der PACKET-Socket alle ein- und ausgehenden Ethernet-Frames. Mit ETH_P_ARP oder ETH_P_IP würden dagegen nur Ethernet-Frames akzeptiert, die ein ARP- oder IP-Paket enthalten. Da das Protokoll-Feld (bzw. "Ethertype"-Feld) im Ethernet-Header 16 Bit lang ist, muss die Konstante bei der Übergabe in Network Byte-Order konvertiert werden.
Durch die PACKET-Sockets sind beim Lese- und Schreibzugriff alle Paketdaten bis zum Header des Data-Link-Layer (Schicht 2) erreichbar. Das folgende Schema zeigt einen Ethernet-Frame, der im lokalen Netz in der Regel für Schicht 2 eingesetzt wird.
Bitfolge 1010101010... |
Bitfolge 10101011 |
Ethernet-Frame, min. 64 Bytes, max. 1518 Bytes | Inter Frame Gap 9,6µs |
|||||
Preamble | SFD | 6 Byte Dest.-Addr. |
6 Byte Source-Addr. |
2 Byte Type |
min. 46 Bytes, max. 1500 Bytes Daten |
4 Byte FCS |
Der Ethernet-Typ eines Ethernet-Frames gibt die Art seiner Nutzdaten an. Es gibt mehrere Quellen, aus denen man den Ethernet-Typ ermitteln kann:
Bezogen auf Ethernet-Frames hat der Programmierer Zugriff auf alle Felder mit Ausnahme der Präambel mit SFD (Starting Frame Delimiter) und der vier Bytes langen CRC-Prüfinfo am Ende. Diese Felder werden in der Regel vom Gerätetreiber automatisch behandelt. Im Datenfeld würden dann die Header und Daten der höheren Schichten untergebracht, beispielsweise der IP- und TCP-Schicht. Vereinfacht gesagt erweitert der PACKET-Socket die Header-Kette nach "vorne" und erlaubt nun zusätzlich die Angabe von MAC-Adressen usw. Programme wie tcpdump oder wireshark nutzen das für das "Sniffen" der Pakete im Netz.
struct ifreq { char ifr_name[IFNAMSIZ]; /* Interface name */ union { struct sockaddr ifr_addr; struct sockaddr ifr_dstaddr; struct sockaddr ifr_broadaddr; struct sockaddr ifr_netmask; struct sockaddr ifr_hwaddr; short ifr_flags; int ifr_ifindex; int ifr_metric; int ifr_mtu; struct ifmap ifr_map; char ifr_slave[IFNAMSIZ]; char ifr_newname[IFNAMSIZ]; char * ifr_data; }; };Da jeder ioctl()-Aufruf immer nur einen der in der union zusammengefassten Wert liefern kann, sind alle Datenkomponenten im gleichen Speicher übereinander gelegt, wodurch eine Variable vom Typ struct ifreq nur den Speicherplatz der größten Komponente beansprucht. Mit diesem Wissen kann das in Kapitel 1 gezeigte Programmstück erweitert werden. das folgende Programm ermittelt den Index des angegebenen Interfaces, seine MAC-Adresse und die darauf gebundene IP-Adresse:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <net/if.h> #include <netinet/ether.h> #include <arpa/inet.h> #include <linux/if_packet.h> /* Default-Netzwerk-Interface */ #define DEFAULT_IF "eth0" // #define DEFAULT_IF "lo" int main(int argc, char *argv[]) { char ifName[IFNAMSIZ] = DEFAULT_IF; /* Interfacename */ int sockfd; /* Socket-Descriptor */ struct ifreq if_idx; /* Dateastruktur fuern Interface-Index */ struct ifreq if_mac; /* Datenastruktur fuer MAC-Adresse */ struct ifreq if_ip; /* Datenastruktur fuer IP-Adresse */ unsigned char hwaddr[6]; /* speichert MAC-Adresse */ int index; /* speichert Interface-Index */ char *addr; /* Zeiger auf IP-Adresse (dotted quad) */ /* Packet-Socket oeffnen */ if ((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* Den Index des Interface ermitteln */ memset(&if_idx, 0, sizeof(struct ifreq)); strncpy(if_idx.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFINDEX, &if_idx) < 0) { perror("SIOCGIFINDEX"); return 1; } index = if_idx.ifr_ifindex; /* Ausgabe */ printf("The interface index of %s is %d\n", ifName, index); /* Die MAC-Adresse des Interface ermitteln */ memset(&if_mac, 0, sizeof(struct ifreq)); strncpy(if_mac.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFHWADDR, &if_mac) < 0) { perror("SIOCGIFHWADDR"); return 1; } /* 6 Bytes MAC-Adresse in das Array hwaddr kopieren */ memcpy(hwaddr, if_mac.ifr_hwaddr.sa_data, 6); /* Ausgabe */ printf("The hardware address of %s, type %d is " "%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x.\n", ifName, if_mac.ifr_hwaddr.sa_family, hwaddr[0], hwaddr[1], hwaddr[2], hwaddr[3], hwaddr[4], hwaddr[5]); /* Die IP-Adresse des Interface ermitteln */ memset(&if_ip, 0, sizeof(struct ifreq)); strncpy(if_ip.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFADDR, &if_ip) < 0) { perror("SIOCGIFADDR"); return 1; } /* in "dotted quad" konvertieren */ addr = inet_ntoa(((struct sockaddr_in *)&if_ip.ifr_addr)->sin_addr); /* Ausgabe */ printf("The IP address of %s is %s\n", ifName, addr); return 0; }Die Ausgabe ist hier sowohl für lo als auch eth0 aufgelistet:
# ./raw_info The interface index of lo is 1 The hardware address of lo, type 772 is 00:00:00:00:00:00. The IP address of lo is 127.0.0.1 # ./raw_info The interface index of eth0 is 2 The hardware address of eth0, type 1 is 00:07:e9:23:71:62. The IP address of eth0 is 129.187.206.160Netzwerk-Interfaces werden in der Regel über Namen identifiziert ("lo", "eth0" etc.). Jedoch brauchen einige Low-Level-Funktionen stattdessen eine laufende Nummer. Deshalb wird im Progrmm oben auch der Index mittels SIOCGIFINDEX abgefragt. Die wichtigsten Hardware-Typen sind in der Headerdatei net/if_arp.h definiert. Für die Ausgaben oben findet man die beiden "ARP protocol HARDWARE identifiers":
... #define ARPHRD_ETHER 1 /* Ethernet 10/100Mbps. */ ... #define ARPHRD_LOOPBACK 772 /* Loopback device. */ ...
Obwohl alle Protokoll- oder Hardware-Adressen von Sender und Empfänger schon in den Paketheadern vorliegen, verlangt das Socket-API in den letzten beiden Parametern der Funktion sendto() eine Socket-Adresse und deren Länge. Die Socket-Adresse wird immer in der allgemeinen Form struct sockaddr verlangt. Wie schon erwähnt, ist dies eine allgemeine Struktur, die für die konkrete Anwendung durch eine spezialisierte Datenstruktur ersetzt wird. Wenn es um Ethernet-frames geht, wird die Datenstruktur struct sockaddr_ll ("ll" für Link Layer) verwendet:
struct sockaddr_ll { unsigned short sll_family; /* immer AF_PACKET */ unsigned short sll_protocol; /* Protokoll des Physical Layer */ int sll_ifindex; /* Interface-Index */ unsigned short sll_hatype; /* Header-Typ */ unsigned char sll_pkttype; /* Packet-Typ */ unsigned char sll_halen; /* Länge der adresse */ unsigned char sll_addr[8]; /* Physische Adresse */ };Da unsere MAC-Adresse nur sechs Bytes lang ist, muss das letzte Feld mit Nullbytes aufgefüllt werden. Das geschieht im folgenden Programm dadurch, dass der gesamte Speicher der Strukturvariablen vorher auf 0 gesetzt wird.
Im folgenden Programm gibt es daher nur wenig Neues. Die Abfrage von Quell-MAC-Adresse und Interface-Index wurden vom vorhergehenden Programm übernommen (nur diesmal ohne Ausgabe). Da der Ethernet-Header reich einfach aufgebaut ist, müssen hier nur Quell- und Zieladresse sowie Typ eingesetzt werden. Wie oben erwähnt brauch sendto() noch einmal die gleichen Daten, was sich in acht Programmzeilen erledigen läßt. Schließlich wird das Paket auf den Weg gebracht.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <net/if.h> #include <netinet/ether.h> #include <arpa/inet.h> #include <linux/if_packet.h> /* Default-Netzwerk-Interface */ #define DEFAULT_IF "eth0" /* Puffergroesse */ #define BUF_SIZ 1024 /* destination host MAC address */ unsigned char dest_mac[6] = {0x00, 0x07, 0xe9, 0x23, 0x71, 0x62}; /* Packet data */ char packet_data[] = "0123456789"; int main(int argc, char *argv[]) { char ifName[IFNAMSIZ] = DEFAULT_IF; /* Interfacename */ int sockfd; /* Socket-Descriptor */ struct ifreq if_idx; /* Dateastruktur fuer Interface-Index */ struct ifreq if_mac; /* Datenastruktur fuer MAC-Adresse */ unsigned char hwaddr[6]; /* speichert MAC-Adresse */ char sendbuf[BUF_SIZ]; /* Sendepuffer */ struct sockaddr_ll socket_address; /* fuer sendto() */ int index; /* speichert Interface-Index */ /* Zeiger auf Sendepuffer-Anfang und -Datenbereich */ struct ether_header *eh = (struct ether_header *) sendbuf; unsigned char *data = (unsigned char *) (sendbuf + sizeof(struct ether_header)); /* Get interface name */ if (argc > 1) strcpy(ifName, argv[1]); else strcpy(ifName, DEFAULT_IF); /* Packet-Socket oeffnen */ if ((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* den Index des Interface ermitteln */ memset(&if_idx, 0, sizeof(struct ifreq)); strncpy(if_idx.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFINDEX, &if_idx) < 0) { perror("SIOCGIFINDEX"); return 1; } index = if_idx.ifr_ifindex; /* Die MAC-Adresse des Interface ermitteln */ memset(&if_mac, 0, sizeof(struct ifreq)); strncpy(if_mac.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFHWADDR, &if_mac) < 0) { perror("SIOCGIFHWADDR"); return 1; } /* 6 Bytes MAC-Adresse in das Array hwaddr kopieren */ memcpy(hwaddr, if_mac.ifr_hwaddr.sa_data, 6); /* Construct the Ethernet header */ memset(sendbuf, 0, BUF_SIZ); /* Ethernet header */ memcpy(eh->ether_shost, hwaddr, 6); /* eigene Adresse */ memcpy(eh->ether_dhost, dest_mac, 6); /* Zielandresse */ eh->ether_type = htons(ETH_P_IP); /* Ethertype field */ /* Packet data dahinter */ memcpy(data, packet_data, sizeof(packet_data)); /* prepare sockaddr_ll */ memset(&socket_address, 0, sizeof(struct sockaddr_ll)); socket_address.sll_family = PF_PACKET; /* RAW communication */ socket_address.sll_protocol = htons(ETH_P_IP); /* Protokoll oberhalb (egal) */ socket_address.sll_hatype = ARPHRD_ETHER; /* Ethernet */ socket_address.sll_pkttype = PACKET_OTHERHOST; /* Ziel ist ein anderer Rechner */ socket_address.sll_ifindex = index; /* Interface-Index */ socket_address.sll_halen = ETH_ALEN; /* Address length*/ memcpy(socket_address.sll_addr, dest_mac, ETH_ALEN); /* Zielandresse */ /* Send packet */ if (sendto(sockfd, sendbuf, ETH_HLEN + strlen(packet_data), 0, (struct sockaddr*)&socket_address, sizeof(struct sockaddr_ll)) < 0) printf("Send failed\n"); return 0; }Wenn man das Senden wieder mit tcpdump anschaut, kann man auch erkennen, dass die Nutzdaten, also der ASCII-String "0123456789" im Paket enthalten ist (0x0000: 3031 3233 3435 3637 3839=. Nachdem ich leichtfertig als Protokoll auf Schicht 3 das IP-Protokoll angegeben habe (socket_address.sll_protocol = htons(ETH_P_IP);) kann es bei anderen Nutztdaten zu Fehlermeldungen im Dump kommen, da tcpdump natürlich brav versucht, den Datenbereich des ethernet-Frames als IP-Header zu interpretieren.
# tcpdump -nexttt -i eth0 '(ether src host 00:07:e9:23:71:62 and ether dst host 00:07:e9:23:71:62)' tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 00:00:00.000000 00:07:e9:23:71:62 > 00:07:e9:23:71:62, ethertype IPv4 (0x0800), length 24: IP3 [|ip] 0x0000: 3031 3233 3435 3637 3839
Nun kann das Programm wieder genauso "aufgeblasen" werden wie die Beispiele oben: Hinzufügen eines ICMP- oder IP-Headers, dann ein UDP- oder TCP-Header usw. Immer mit dem Vorteil, dass man jedes Bit des Datenpakets in der Hand hat - vom Ethernet bis hinauf zu Schicht 4. Sie könnten auf der Basis des Programms auch z. B. ARP-Requests generieren oder eine Broadcast-Sturm auslösen (Quell- und Zieladresse auf die Broadcastadresse FF:FF:FF:FF:FF:FF setzen).
Ein Empfangsprogramm auf Ethernet-Basis ist schon beinahe ein Netzwerk-Sniffer. Die Änderungen gegenüber dem Sendeprogrmm sind nicht gross: Der Sendeteil fällt weg, aus dem ersten Programm dieses Kapitels hole ich mir die Dezimal-Binärumwandlung und an die Stelle von sendto() tritt rcvfrom():
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <net/if.h> #include <netinet/ether.h> #include <arpa/inet.h> #include <linux/if_packet.h> /* Default-Netzwerk-Interface */ #define DEFAULT_IF "eth0" /* Gibt ein Byte binaer aus */ void print_bin(unsigned char x) { int i; char erg[10]; erg[9] = '\0'; erg[8] = ' '; for (i = 7; i >= 0 ; i--) { erg[i] = (x%2 == 0)?'0':'1'; x = x >> 1; } printf("%s", erg); } int main(int argc, char *argv[]) { char ifName[IFNAMSIZ] = DEFAULT_IF; /* Interfacename */ int sockfd; /* Socket-Descriptor */ struct ifreq if_idx; /* Dateastruktur fuer Interface-Index */ struct ifreq if_mac; /* Datenastruktur fuer MAC-Adresse */ unsigned char hwaddr[6]; /* speichert MAC-Adresse */ struct sockaddr_ll socket_address; /* fuer sendto() */ int index; /* speichert Interface-Index */ unsigned char rcvbuf[ETH_FRAME_LEN];/* Empfangs-Buffer */ int length = 0; /* Laenge des empfangenen Frames */ int j, k; /* Get interface name */ if (argc > 1) strcpy(ifName, argv[1]); else strcpy(ifName, DEFAULT_IF); /* Packet-Socket oeffnen */ if ((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } /* den Index des Interface ermitteln */ memset(&if_idx, 0, sizeof(struct ifreq)); strncpy(if_idx.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFINDEX, &if_idx) < 0) { perror("SIOCGIFINDEX"); return 1; } index = if_idx.ifr_ifindex; /* Die MAC-Adresse des Interface ermitteln */ memset(&if_mac, 0, sizeof(struct ifreq)); strncpy(if_mac.ifr_name, ifName, IFNAMSIZ-1); if (ioctl(sockfd, SIOCGIFHWADDR, &if_mac) < 0) { perror("SIOCGIFHWADDR"); return 1; } /* 6 Bytes MAC-Adresse in das Array hwaddr kopieren */ memcpy(hwaddr, if_mac.ifr_hwaddr.sa_data, 6); /* Ausgabe */ printf("The hardware address of %s, type %d is " "%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x.\n", ifName, if_mac.ifr_hwaddr.sa_family, hwaddr[0], hwaddr[1], hwaddr[2], hwaddr[3], hwaddr[4], hwaddr[5]); /* prepare sockaddr_ll */ memset(&socket_address, 0, sizeof(struct sockaddr_ll)); socket_address.sll_family = PF_PACKET; /* RAW communication */ socket_address.sll_protocol = htons(ETH_P_IP); /* Protokoll oberhalb (egal) */ socket_address.sll_hatype = ARPHRD_ETHER; /* Ethernet */ socket_address.sll_pkttype = PACKET_OTHERHOST; /* Ziel ist ein anderer Rechner */ socket_address.sll_ifindex = index; /* Interface-Index */ socket_address.sll_halen = ETH_ALEN; /* Address length*/ length = recvfrom(sockfd, rcvbuf, ETH_FRAME_LEN, 0, NULL, NULL); if (length == -1) printf("Empfang ging schief"); printf("The Source address is " "%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x.\n", rcvbuf[0], rcvbuf[1], rcvbuf[2], rcvbuf[3], rcvbuf[4], rcvbuf[5]); printf("The Destination address is " "%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x.\n", rcvbuf[6], rcvbuf[7], rcvbuf[8], rcvbuf[9], rcvbuf[10], rcvbuf[11]); /* Ausgabe binaer/ASCII, immer 4 Bytes pro Zeile */ k = 0; /* Bytezaehler */ for (j = 14; j < length; j++) { k++; print_bin(rcvbuf[j]); if (rcvbuf[j] >= ' ' && rcvbuf[j] < 127) printf("%c ", rcvbuf[j]); else printf(". "); /* alle 4 Bytes Newline */ if (!(k % 4)) { printf ("\n"); } } printf("\n"); return 0; }Um den gesamten Empfangsteil (von length = recvfrom ... bis printf("\n");) könnte man noch eine Endlosschleife legen, um zu beobachten, was sich auf dem Netz tut. Der Nachteil von Sender und Empfänger ist natürlich, dass der Empfänger alle Ethernet-Frames annimmt, die an seine MAC-Adresse gerichtet sind und nicht nur die des Senders. Wenn man - aus welchen Gründen auch immer - nur auf der Basis von Ethernet-Frames kommunizieren will, muss die Info im Frame den Empfänger genauer spezifizieren. Damit ist man dann schnell wieder bei Protokollen wie ICMP, ARP oder IP.
Im folgenden Sniffrer-Programm ist eigentlich nichts Neues gegenüber den Beispielen oben enthalten. Es wird ein PACKET-Socket geöffnet und dann werden die ankommenden Pakete mittels recvfrom() gelesen. Neu ist nur die "Kosmetik" der Anzeige. Es werden der Ethernet-Frameheader und die Paketheader der Protokolle IP, ICMP, UDP und TCP strukturiert angezeigt. Wer will, kann auch weitere Protokolle implementieren. Im Anschluß an die Header wirde der Nutzdatenteil des Pakets Hexadezimal und als ASCII-Zeichen dargestellt. Für jeden Header und auch den Hexdump gibt es eine eigene Funktion, sodass auch hier die Übersicht (und Erweiterbarkeit) erhalten bleibt. Anhand der Funktionen kann man auch gut studieren, wie eine Erweiterung aussehen könnte.
Damit das Programm auch "ordentlich" beendet werden kann, wird nicht die Tastatur-Eingabe abgefragt, sondern ein Signalhandler für die Unterbrechung des Programms mit der Tastenkombination [Strg][C] eingesetzt - was auch viel einfacher ist.
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<errno.h> #include<netdb.h> #include<signal.h> #include<sys/socket.h> #include<sys/ioctl.h> #include<sys/time.h> #include<sys/types.h> #include<arpa/inet.h> #include<netinet/in.h> #include<netinet/ip_icmp.h> #include<netinet/udp.h> #include<netinet/tcp.h> #include<netinet/ip.h> #include<net/ethernet.h> #include<netinet/if_ether.h> /* Default-Netzwerk-Interface */ #define DEFAULT_IF "eth0" void do_packet(unsigned char*, int); void print_ip_header(unsigned char*, int); void print_tcp_packet(unsigned char *, int); void print_udp_packet(unsigned char *, int); void print_icmp_packet(unsigned char*, int); void dump_data(unsigned char*, int); void print_data(unsigned char*, int); void beenden(int); struct sockaddr_in source, dest; /* fuer Adressierung */ int tcp = 0, udp = 0, icmp = 0, others = 0, igmp = 0, total = 0; /* Zaehler */ //int i, j; int go = 1; int main() { struct sockaddr saddr; /* fuer recvfrom() */ int saddr_size; int data_size; /* Paketgroesse */ int sockfd; /* sockethandle */ unsigned char *buffer = (unsigned char *) malloc(65536); //Its Big! /* Signalhandler aktivieren */ signal(SIGINT,beenden); printf("Starte...\n"); /* Packet-Socket oeffnen */ if ((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) { perror("socket() ging schief"); if (errno == EACCES) printf(" (EACCES)\n"); if (errno == EAFNOSUPPORT) printf(" (EAFNOSUPPORT)\n"); return 1; } while(go) { saddr_size = sizeof saddr; /* Paket empfangen */ data_size = recvfrom(sockfd, buffer, 65536, 0, &saddr, (socklen_t*)&saddr_size); if(data_size < 0) { perror("Recvfrom-Error, Abbruch\n"); return 1; } /* Paket verarbeiten */ do_packet(buffer, data_size); } close(sockfd); /* Zaehler ausgeben */ printf("\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"); printf("TCP: %d UDP: %d ICMP: %d IGMP: %d Others: %d Total: %d\n", tcp, udp, icmp, igmp, others, total); printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"); printf("Fertig...\n"); return 0; } void do_packet(unsigned char* buffer, int size) { /* IP-Header untersuchen; er beginnt nach dem Ethernet-Header */ struct iphdr *iph = (struct iphdr*)(buffer + sizeof(struct ethhdr)); ++total; switch (iph->protocol) /* Check Protocol */ { case 1: /* ICMP Protocol */ ++icmp; print_icmp_packet(buffer , size); break; case 2: /* IGMP Protocol */ ++igmp; /* keine Ausgabe */ break; case 6: /* TCP Protocol */ ++tcp; print_tcp_packet(buffer , size); break; case 17: /* UDP Protocol */ ++udp; print_udp_packet(buffer , size); break; default: /* andere Protokolle z. B. ARP etc. */ ++others; break; } } void print_ethernet_header(unsigned char* Buffer, int Size) { struct ethhdr *eth = (struct ethhdr *)Buffer; printf("\n"); printf("Ethernet Header\n"); printf(" Destination Address : %.2X-%.2X-%.2X-%.2X-%.2X-%.2X \n", eth->h_dest[0], eth->h_dest[1], eth->h_dest[2], eth->h_dest[3], eth->h_dest[4], eth->h_dest[5] ); printf(" Source Address : %.2X-%.2X-%.2X-%.2X-%.2X-%.2X \n", eth->h_source[0], eth->h_source[1], eth->h_source[2], eth->h_source[3], eth->h_source[4], eth->h_source[5]); printf(" Protocol : %u \n",(unsigned short)eth->h_proto); } void print_ip_header(unsigned char* Buffer, int Size) { unsigned short iphdrlen; struct iphdr *iph = (struct iphdr *)(Buffer + sizeof(struct ethhdr) ); print_ethernet_header(Buffer, Size); iphdrlen =iph->ihl*4; memset(&source, 0, sizeof(source)); source.sin_addr.s_addr = iph->saddr; memset(&dest, 0, sizeof(dest)); dest.sin_addr.s_addr = iph->daddr; printf("\n"); printf("IP Header\n"); printf(" IP Version : %d\n",(unsigned int)iph->version); printf(" IP Header Length : %d DWORDS or %d Bytes\n", (unsigned int)iph->ihl,((unsigned int)(iph->ihl))*4); printf(" Type Of Service : %d\n",(unsigned int)iph->tos); printf(" IP Total Length : %d Bytes(Size of Packet)\n",ntohs(iph->tot_len)); printf(" Identification : %d\n",ntohs(iph->id)); printf(" TTL : %d\n",(unsigned int)iph->ttl); printf(" Protocol : %d\n",(unsigned int)iph->protocol); printf(" Checksum : %d\n",ntohs(iph->check)); printf(" Source IP : %s\n",inet_ntoa(source.sin_addr)); printf(" Destination IP : %s\n",inet_ntoa(dest.sin_addr)); } void print_tcp_packet(unsigned char* Buffer, int Size) { unsigned short iphdrlen; int header_size; struct iphdr *iph = (struct iphdr *)(Buffer + sizeof(struct ethhdr) ); iphdrlen = iph->ihl*4; struct tcphdr *tcph = (struct tcphdr*)(Buffer + iphdrlen + sizeof(struct ethhdr)); header_size = sizeof(struct ethhdr) + iphdrlen + tcph->doff*4; printf("\n\n======================= TCP-Paket ==========================\n"); print_ip_header(Buffer,Size); printf("\n"); printf("TCP Header\n"); printf(" Source Port : %u\n",ntohs(tcph->source)); printf(" Destination Port : %u\n",ntohs(tcph->dest)); printf(" Sequence Number : %u\n",ntohl(tcph->seq)); printf(" Acknowledge Number : %u\n",ntohl(tcph->ack_seq)); printf(" Header Length : %d DWORDS or %d BYTES\n" , (unsigned int)tcph->doff, (unsigned int)tcph->doff*4); printf(" Urgent Flag : %d\n",(unsigned int)tcph->urg); printf(" Acknowledgement Flag : %d\n",(unsigned int)tcph->ack); printf(" Push Flag : %d\n",(unsigned int)tcph->psh); printf(" Reset Flag : %d\n",(unsigned int)tcph->rst); printf(" Synchronise Flag : %d\n",(unsigned int)tcph->syn); printf(" Finish Flag : %d\n",(unsigned int)tcph->fin); printf(" Window : %d\n",ntohs(tcph->window)); printf(" Checksum : %d\n",ntohs(tcph->check)); printf(" Urgent Pointer : %d\n",tcph->urg_ptr); dump_data (Buffer + header_size, Size - header_size); } void print_udp_packet(unsigned char *Buffer , int Size) { unsigned short iphdrlen; int header_size; struct iphdr *iph = (struct iphdr *)(Buffer + sizeof(struct ethhdr)); iphdrlen = iph->ihl*4; struct udphdr *udph = (struct udphdr*)(Buffer + iphdrlen + sizeof(struct ethhdr)); header_size = sizeof(struct ethhdr) + iphdrlen + sizeof udph; printf("\n\n======================= UDP-Paket ==========================\n"); print_ip_header(Buffer,Size); printf("\nUDP Header\n"); printf(" Source Port : %d\n" , ntohs(udph->source)); printf(" Destination Port : %d\n" , ntohs(udph->dest)); printf(" UDP Length : %d\n" , ntohs(udph->len)); printf(" UDP Checksum : %d\n" , ntohs(udph->check)); dump_data (Buffer + header_size, Size - header_size); } void print_icmp_packet(unsigned char* Buffer, int Size) { unsigned short iphdrlen; int header_size; struct iphdr *iph = (struct iphdr *)(Buffer + sizeof(struct ethhdr)); iphdrlen = iph->ihl * 4; struct icmphdr *icmph = (struct icmphdr *)(Buffer + iphdrlen + sizeof(struct ethhdr)); header_size = sizeof(struct ethhdr) + iphdrlen + sizeof icmph; printf("\n\n======================= ICMP-Paket =========================\n"); print_ip_header(Buffer , Size); printf("\nICMP Header\n"); printf(" Type : %d",(unsigned int)(icmph->type)); if((unsigned int)(icmph->type) == 11) { printf(" (TTL Expired)\n"); } else if((unsigned int)(icmph->type) == ICMP_ECHOREPLY) { printf(" (ICMP Echo Reply)\n"); } printf(" Code : %d\n",(unsigned int)(icmph->code)); printf(" Checksum : %d\n",ntohs(icmph->checksum)); dump_data (Buffer + header_size, Size - header_size); } void dump_data (unsigned char* addr, int len) { int i; unsigned char buff[17]; unsigned char *pc = (unsigned char*)addr; printf("\n- - - - - - - - - - - - Daten-Dump - - - - - - - - - - - - -\n"); for (i = 0; i < len; i++) { /* alle 16 Werte eine neue Zeile */ if ((i % 16) == 0) { if (i != 0) /* nicht bei der ersten Zeile */ printf (" %s\n", buff); printf (" %04x ", i); /* Offset ausgeben */ } printf(" %02x", pc[i]); /* Hexausgabe, ASCCI fuer spaeter speichern */ if ((pc[i] < 32) || (pc[i] > 127)) buff[i % 16] = '.'; else buff[i % 16] = pc[i]; buff[(i % 16) + 1] = '\0'; } /* Rest des Puffers in der letzten Zeile ausgeben */ while ((i % 16) != 0) { printf (" "); i++; } printf (" %s\n", buff); } void beenden(int dummy) /* STRG-C behandeln */ { go = 0; }
Die Ausgabe des Programms kann man natürlich zur besseren Auswertung in eine Datei umleiten. Im folgenden Ausschnitt kann man sogar einen Angriff auf den Server sehen: Es werden UDP-Pakete an die von Windows genutzten Ports 137 und 138 gesehndet, was den Linux-Server aber kalt lässt.
Starte... ======================= TCP-Paket ========================== Ethernet Header Destination Address : 00-07-E9-23-71-62 Source Address : D8-67-D9-0F-40-C2 Protocol : 8 IP Header IP Version : 4 IP Header Length : 5 DWORDS or 20 Bytes Type Of Service : 0 IP Total Length : 40 Bytes(Size of Packet) Identification : 14296 TTL : 118 Protocol : 6 Checksum : 64121 Source IP : 93.133.36.157 Destination IP : 129.187.206.160 TCP Header Source Port : 50618 Destination Port : 80 Sequence Number : 337566547 Acknowledge Number : 1575459156 Header Length : 5 DWORDS or 20 BYTES Urgent Flag : 0 Acknowledgement Flag : 1 Push Flag : 0 Reset Flag : 0 Synchronise Flag : 0 Finish Flag : 0 Window : 260 Checksum : 12186 Urgent Pointer : 0 - - - - - - - - - - - - Daten-Dump - - - - - - - - - - - - - 0000 00 1f 9e d2 63 80 ....c. ======================= UDP-Paket ========================== Ethernet Header Destination Address : FF-FF-FF-FF-FF-FF Source Address : 00-19-99-92-A3-56 Protocol : 8 IP Header IP Version : 4 IP Header Length : 5 DWORDS or 20 Bytes Type Of Service : 0 IP Total Length : 78 Bytes(Size of Packet) Identification : 18267 TTL : 128 Protocol : 17 Checksum : 7925 Source IP : 10.27.192.25 Destination IP : 10.27.255.255 UDP Header Source Port : 137 Destination Port : 137 UDP Length : 58 UDP Checksum : 61695 - - - - - - - - - - - - Daten-Dump - - - - - - - - - - - - - 0000 00 3a f0 ff b5 a2 01 10 00 01 00 00 00 00 00 00 .:.............. 0010 20 46 44 45 4a 45 4e 44 42 44 44 43 4e 45 4d 46 FDEJENDBDDCNEMF 0020 44 46 4a 43 41 43 41 43 41 43 41 43 41 43 41 42 DFJCACACACACACAB 0030 4d 00 00 20 00 01 M.. .. ======================= UDP-Paket ========================== Ethernet Header Destination Address : FF-FF-FF-FF-FF-FF Source Address : 00-19-99-92-A3-9C Protocol : 8 IP Header IP Version : 4 IP Header Length : 5 DWORDS or 20 Bytes Type Of Service : 0 IP Total Length : 222 Bytes(Size of Packet) Identification : 1549 TTL : 128 Protocol : 17 Checksum : 21835 Source IP : 10.27.202.129 Destination IP : 10.27.255.255 UDP Header Source Port : 138 Destination Port : 138 UDP Length : 202 UDP Checksum : 63249 - - - - - - - - - - - - Daten-Dump - - - - - - - - - - - - - 0000 00 ca f7 11 11 02 9c 56 0a 1b ca 81 00 8a 00 b4 .......V........ 0010 00 00 20 45 48 46 43 45 4a 45 46 46 44 45 48 46 .. EHFCEJEFFDEHF 0020 43 45 42 45 4e 45 4a 46 49 43 41 43 41 43 41 43 CEBENEJFICACACAC 0030 41 41 41 00 20 46 48 45 50 46 43 45 4c 45 48 46 AAA. FHEPFCELEHF 0040 43 45 50 46 46 46 41 43 41 43 41 43 41 43 41 43 CEPFFFACACACACAC 0050 41 43 41 42 4f 00 ff 53 4d 42 25 00 00 00 00 00 ACABO..SMB%..... 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0070 00 00 00 00 00 00 11 00 00 1a 00 00 00 00 00 00 ................ 0080 00 00 00 e8 03 00 00 00 00 00 00 00 00 1a 00 56 ...............V 0090 00 03 00 01 00 01 00 02 00 2b 00 5c 4d 41 49 4c .........+.\MAIL 00a0 53 4c 4f 54 5c 42 52 4f 57 53 45 00 08 01 21 0f SLOT\BROWSE...!. 00b0 01 10 f2 1e a8 5f 00 00 00 00 47 52 49 45 53 47 ....._....GRIESG 00c0 52 41 4d 49 58 00 RAMIX. ======================= TCP-Paket ========================== Ethernet Header Destination Address : 00-00-0C-9F-F0-01 Source Address : 00-07-E9-23-71-62 Protocol : 8 IP Header IP Version : 4 IP Header Length : 5 DWORDS or 20 Bytes Type Of Service : 16 IP Total Length : 92 Bytes(Size of Packet) Identification : 64909 TTL : 64 Protocol : 6 Checksum : 50039 Source IP : 129.187.206.160 Destination IP : 84.148.212.150 TCP Header Source Port : 22 Destination Port : 52144 Sequence Number : 3968830379 Acknowledge Number : 3452165137 Header Length : 5 DWORDS or 20 BYTES Urgent Flag : 0 Acknowledgement Flag : 1 Push Flag : 1 Reset Flag : 0 Synchronise Flag : 0 Finish Flag : 0 Window : 176 Checksum : 31189 Urgent Pointer : 0 - - - - - - - - - - - - Daten-Dump - - - - - - - - - - - - - 0000 f2 15 db 4b cc ba b6 1a de 05 3b b9 8e 80 3c 98 ...K......;...<,. 0010 f8 c8 07 9b b6 7a 49 db a0 c2 89 56 db 60 ba a8 .....zI....V.`.. 0020 19 3d 6d 96 00 cd 6c bb 29 b4 f1 30 30 a8 11 0c .=m...l.)..00... 0030 47 c4 de 21 G..! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TCP: 19 UDP: 29 ICMP: 0 IGMP: 0 Others: 59 Total: 107 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Fertig...
![]() |
![]() |