Internet-Technologie

Prof. Jürgen Plate

3 RAW-Sockets und spezielle Anwendungen

Im Folgenden wird öfters Wissen über den Aufbau von Datenpaketen (IP, TCP, UDP, ARP usw.) vorausgesetzt. Diese Informationen zu Netzwerkprotokollen, insbesondere die IP-, TCP- und UDP-Header finden Sie unter "TCP/IP" im Netzwerk-Skript. Ale hier gezeigten Programme können Sie auch wieder als C-Quelldatei aus der Beispielsammlung herunterladen. Die aufgeführten Beispiel dienen primär der Illustration der Socket-Funktionen und sind ausdrücklich keine Programme die in einer Produktivumgebung eingesetzt werden könnten. Dazu müsste eine weitergehende Fehlerbehandlung, die Prüfung von Kommandozeilenparametern und vieles andere hinzugefügt werden. In diesem Skript geht es aber auch garnicht um fertige Tools, sondern um die Erläuterung der wesentlichen Funktionsweise. Deshalb wurde manchmal auch ein Parameter einfach als Konstante im Programm eingetragen, der eigentlich "von aussen" einstellbar sein sollte.

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.

3.1 Wake-on-LAN

Dieses erste Beispiel geht die ganze Sache noch lockerer an, hier dürfen noch UDP-Pakete verschickt werden. So ist der Sprung von den TCP- und UDP-Sockets in die Tiefen der Protokolle nicht ganz so groß. Es zeigt aber schon als ersten Schritt, wie man den Inhalt eines Pakets zusammenbaut, denn eigentlich kommt es nur auf das Ethernet-Paket an.

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: yes
Wichtig sind die beiden Zeilen
  Supports Wake-on: umbg
  Wake-on: g
Die 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 Magic Packet

Wie nun den Rechner aufwecken? Das Magische am "Magic Packet" ist gar nicht so magisch: Es wird ein Datenpaket an den Rechner gesendet, das mit sechs Bytes mit dem hexadezimalen Wert 0xFF beginnt. Dann folgt 16-mal die Hardwareadresse (MAC-Adresse) der Netzwerkkarte. Die Daten können als Ethernet-Paket gesendet werden, oder per UDP bzw. TCP. In diesem Fall werden UDP-Pakete auf Port 9 (default) versendet, wobei das UDP-Protokoll und die Portnummer eigentlich keine Rolle spielen, aber die gleiche Programmierung wie im ersten Abschnitt erlauben.

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;
  }

3.2 RAW IP-, ICMP-, UDP- und TCP-Pakete

Sie müssen immer daran denken, dass Sie bei RAW-Sockets alles selbst machen müssen, also auch das Ethernet-Paket mit den richtigen Nutzdaten füllen. Wie im Netzwerk-Skript besprochen, besteht ein TCP-Paket aus:

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:

Struktur des IP-Headers (RFC 791)

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    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Das Feld Protocol legt fest, welches Protokoll in der Schicht oberhalb von IP verwendet wird. Bei Linux ist das folgendermaßen definiert:
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) */

Struktur des ICMP-Headers (RFC 792)

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.

Struktur des TCP-Headers (RFC 793)

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.

Struktur des UDP-Headers (RFC 768)

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) */

3.3 Programmierung mit RAW-Sockets

Für die Programmierung werden in der Regel die folgenden Header-Dateien benötigt. Am Einfachsten setzen Sie die folgenden Zeilen an den Programmanfang:
#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.

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:

  1. Direktes Anwenden von IPPROTO_RAW:
    	
    int s = socket (AF_INET, SOCK_RAW, IPPROTO_RAW);
    
  2. Benutzen von IPPROTO_TCP und Setzen der Socket-Option IP_HDRINCL auf 1:
    	
    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);
      }
    
Ist IP_HDRINCL nicht gesetzt, gibt die Startadresse der Daten das erste Byte nach dem IP-Header an, d.h. der Kernel baut den IP-Header auf und setzt das Protokoll-Feld auf den Wert, der beim Socket-Aufruf angegeben wurde. Ist IP_HDRINCL dagegen gesetzt, gibt die Startadresse der Daten das erste Byte des IP-Headers an, d.h. die Daten müssen auch den IP-Header enthalten. Ist dabei das IP-Identifizierungsfeld 0, wird es vom Kernel ergänzt. Des Weiteren berechnet und speichert der Kernel die Prüfsumme des IP-Headers. Weiterhin kann das Feld "Source Address" auf 0 gesetzt werden, dann wird hier ebenfalls der Kernel bemüht. Alle weiteren Prüfsummen (ICMP-, UDP- oder TCP-Header) muss das Anwenderprogramm selbst berechnen.

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:

Das empfangene Paket enthält immer den IP-Header.

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.

Ein erster Schritt - Lesen von ICMP-Paketen

Das folgende Programm illustriert erste Schritt mit RAW-Sockets. Es liest das nächste ICMP Paket, das den Host erreicht, und gibt dessen Inhalt binär und als ASCII-Zeichen auf dem Bildschirm aus. Nicht darstellbare ASII-Zeichen werden durch einen Punkt ersetzt.
/* 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/null
Die 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 7
Wenn 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 ...

Variable Headerlänge und Byteorder

Bei der Programmierung muss einerseits auf die Länge der jeweiligen Protokollheader geachtet werden und andererseits sollte man immer daran denken, dass im Netz eine bestimmte Byte-Order (Endianess) vorgeschrieben ist, die nicht unbedingt mit der Byte-Order des Rechners übereinstimmt.

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

Die Prüfsumme

In Netzwerkprotokollen werden zur Erkennung von Fehlern bei der Übertragung Prüfsummen über bestimmte Teile eine Protokollheaders gebildet. Im RFC 1071 wird die Berechnung der Prüfsumme beschrieben. Alle Internet-Protokolle (z. B. IP, ICMP, UDP und TCP) bilden ihre Prüfsumme nach dem im RFC beschreibenen Prinzip: Es werden die Daten in 16-Bit-Worte unterteilt und aufsummiert. Anschließend wird das Einerkomplement aus dieser Summe gebildet. Obwohl sich die Prüfsummenberechnung für alle gängigen Internet-Protokolle gleich gestaltet, variiert der Umfang der zu schützenden Daten von Protokoll zu Protokoll. Die Prüfsumme im IP-Header berechnet sich beispielsweise ausschließlich aus den Daten des IP-Headers, wobei die Nutzdaten nicht beachtet werden. Erst die Prüfsummen der Transport-Layer-Protokolle integrieren auch die Nutzdaten eines Pakets. So werden für die Berechnung der ICMP-Prüfsumme der ICMP-Header und die Nutzdaten herangezogen. Für die TCP- und UDP-Prüfsumme bildet man zusätzlich noch einen sogenannten Pseudo-Header, der weiter unten behandelt wird. Das beschriebene Verfahren ist relativ einfach, die Internet-Prüfsumme bietet daher keine optimale Sicherheit. Die Einfachheit hat aber auch Vorteile. Es müsste beispielsweise die Prüfsumme des IP-Headers bei jedem Router-Hop neu berechnet werden, da der Wert des Feldes "Time-To-Live" bei jedem Hop dekrementiert wird. Jeoch reicht in diesem Fall das Inkrementieren der Prüfsumme als Korrekturoperation aus. Die genauen Modalitäten der Prüfsummenbildung für jedes Protokoll lassen sich den zugehörigen Protokollspezifikation (RFCs) entnehmen. Die Berechnung erfolgt RFC folgendermaßen:
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.

Daten senden und empfangen mit RAW-Sockets

Jedes Datenpaket im Netz braucht Absender- und Empfängeradresse. In den vorhergehenden Kapiteln war das ganz klar, weil dort ja auf UDP- oder TCP-Ebene kommuniziert wurde. Auch das Wake-on-LAN-Beispiel und die anderen Beispiele verwenden zumindest IP-Adressen. Obwohl das Programm lies_paket.c sich dahingehend von den "üblichen" Programmen unterscheidet, dass es recht unspezifiziert die ICMP-Pakete annimmt. Bei der Arbeit mit RAW-Sockets muss aber doch recht viel selbst gemacht werden. Wir haben es ja mit MAC-Adressen auf Layer 2, IP-Adressen auf Layer 3 und Portnummern aus Layer 4 zu tun. Dabei muss eine Absenderadresse nicht zwangsläufig vom Benutzerprogramm definiert werden. Das Betriebssystem setzt automatisch in jedes ausgehende Paket als Absenderadresse die Adresse des Interfaces ein, über das die Pakete gesendet werden. Mit der Funktion bind() kann, wie schon erwähnt, auch explizit eine Absenderadresse definiert werden.

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.

Ankuftszeit eines Pakets aus dem Netz

Die genaue Ankunftszeit des zuletzt empfangenen Pakets kann mit einem ioctl()-Aufruf vom Kernel erfragt werden. Als Parameter für ioctl() werden der Socket-Deskriptor, der Funktionscode SIOCGSTAMP und ein Zeiger auf eine Variable vom Typ struct timeval. übergeben. Dieser Datentyp repräsentiert die genaue Zeit. Er besteht aus einem Sekundenwert, (long int) der die Sekunden seit der UNIX-Epoche enthält und dem Nachkommaanteil der Zeit in Mikrosekunden (long int).

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

RAW-Pakete versenden

Das folgende Beispiel zeigt den üblichen Weg der Paketgenerierung. Weiter unten werden dann auch weitergehende Möglichkeiten der manuellen Paketgenerierung auf Basis von RAW-Sockets gezeigt. Das Programm arbeitet zum Test auf dem Loopback-Interface, was aber keine Einschränkung darstellt. Im Prinzip sind beliebige IP-Adressen möglich. Zum Senden von Informationen werden ausschließlich IP-Pakete verwendet - ohne jedes höhere Protokoll. Es gibt auch keine Sicherungsschicht oder dergleichen, die Pakete werden einfach auf den Weg gebracht - in der Hoffnung, dass sie schon irgend jemand empfangen wird. Die eigentlichen Nutzdaten sind Teil des IP-Pakets und folgen direkt auf den IP-Header. Der Einfachheit halber ist die Paketgröße willkürlich auf 50 Byte begrenzt - zieht man die 20 Bytes für den IP-header ab, bleiben noch 30 Bytes für eine Nachricht. Der IP-Header wird im Programm zusammengebaut - der Programmierer hat somit alle Freiheiten. Da unter Linux eine passende Datenstruktur für den Header bereitgestellt wird, muss man auch nicht mit einzelnen Bits herumwursteln, sondern kann alles halbwegs übersichtlich machen. Der einzige Trick bei der Sache ist, dass ein Zeiger auf die IP-Struktur auf den Anfang des (unspezifischen) Paket-Puffers gesetzt wird. Damit landen die Headereinträge an der richtigen Stelle. Alles andere wirde schon weiter oben besprochen. Das Generieren der Checksumme überlasse ich dem Kernel, indem ich das Feld ip->check mit 0 vorbesetze.
#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.

RAW-Pakete empfangen

Das Empfangsprogramm hat einen ähnliche Aufbau. Mit der Funktion recvfrom() werden die gesendeten Pakete eingelesen und im Puffer gespeichert. Auch hier bildet der IP-Header den Anfang des Puffers. Eine Ähnlichkeit mit dem weiter oben gezeigten Testprogramm lies_paket.c ist unverkennbar.
#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 kernel
Die 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!..........
^C
Damit 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:

Die Tatsache, dass Sie mit einem Protokoll arbeiten, für das die Antwortpakete an Ihren RAW-Socket gerichtet werden, bedeutet nicht unbedingt, dass Sie die Antwortpaket auch bekommen. Dazu müssen Sie weitere Dinge berücksichtigen:

Senden von ICMP-Paketen

In den bisherigen Beispielen wurden reine IP-Pakete verschickt, ohne jegliches darüber liegendes Protokoll. Nun soll versucht werden, ein Protokoll auf Schicht 4 zu implementieren. Dazu muss der Puffer für das Senden zuerst mit der IP-Info gefüllt werden (siehe oben) und danach kommen der ICMP-Header und die ICMP-Nutzdaten. Die Berechnung von IP-Checksumme und IP-Länge macht netterweise der Kernel. Für das ICMP-Paket muss die Checksumme aber vom Programm mit dem weiter oben beschriebenen Algorithmus berechnet werden. Das Zusammenhängen von IP- und ICMP-Header wird wieder mit etwas Pointer-Arithmetik erledigt.

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.1
Mit 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]

Senden von TCP-Paketen

Das folgende Beispielprogramm zeigt das Versenden von TCP-SYN-Paketen mittels RAW-Sockets. Beim Verbindungsaufbau über TCP sendet der Client ja zuerst ein Paket mit gesetzttem SYN-Flag, was vom Server mit einem Plaket beantwortet wird, bei dem SYN-ACK gesetzt ist. Daraufhin bestätigt der Client mit einem ACK. Das Ganze ist als "Drei-Wege-Handshake" bekannt. Hier wird nur ein SYN-Paket gesendet, worauf sich das Programm beendet. Aus dem Beispiel wird aber auch klar, wie kompliziert es wäre, die komplette TCP-Kommunikation mittels RAW-Sockets nachzuvollziehen.

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

3.4 Programmierung mit PACKET-Sockets

Unter Linux enthält das Socket-API neben den bisher besprochenen RAW-Sockets noch einen weiteren Socket-Typ, den PACKET-Socket. Er ermöglicht den Zugriff auf die bisher noch nicht erreichbare zweite Schicht des TCP/IP-Referenzmodells, den Data-Link-Layer - also die Ethernet-Schnittstelle.

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:

Der Platzhalterwert ETH_P_ALL kann, wie schon erwähnt, für jeden Ethermet-Typ verwendet werden. Wenn man einen Ethernet-Typ für experimentelle Zwecke braucht, kann man die Werte 0x88b5 und 0x88b6 verwenden, die für diesen Zweck reserviert sind.

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.

Eigene MAC-Adresse und IP-Adresse ermitteln

Im Kapitel 1 haben sie gesehen, dass es einen ioctl()-Aufruf zum Ermitteln der eigenen IP-Adresse gibt. Einen entsprechenden Aufruf gibt es auch für die eigene MAC-Adresse. Interessant ist in diesem Zusammenhang auch die Datenstruktur, in der die Information zurückgegeben wird:
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.160
Netzwerk-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.     */
   ...

Ethernet-Pakete senden und empfangen

Beim Senden mittels PACKET-Sockets müssen die Netzwerkpakete grundsätzlich inklusive Ethernet-Header generiert werden. Es entfällt lediglich die vier Byte lange CRC-Prüfinfo am Ende des Ethernet-Frames. Header oder Headerteile können nicht mehr automatisch vom Kernel generiert werden. Einzige Ausnahme bilden die gegebenenfalls notwendigen Bytes zum Auffüllen des Nutzdatenblocks auf die minimale Größe von 64 Bytes (sogenannte Padding-Bytes). Obwohl Ethernet für einen Frame die genannte Mindestlänge vorschreibt, lassen sich auch kürzere Pakete über PACKET-Sockets senden. Die Treibersoftware füllt dann die fehlenden Bytes automatisch mit Nullen auf. Die CRC-Prüfinfo wird, wie schon erwähnt, ebenfalls vom Gerätetreiber erzeugt.

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.

3.5 Netzwerk-Sniffer

Zum Schluss des Kapitels soll dann doch noch eine einfache Komplettanwendung gezeigt werden. Mit den bisher gesammelten Erfahrungen kann man einen einfachen Netzwerk-Sniffer bauen, der zumindest die eingehenden Pakete anzeigen kann (für abgehende Pakete ist mehr Aufwand notwendig). Packet-Sniffer werden für verschiedene Zwecke eingesetzt, z. B. die Analyse von Protokollen, die Überwachung des Netzes und die Sicherheit eines Netzs. Das oben mehrfach verwendete Programm Tcpdump oder Wireshark sind beliebte Paket-Sniffer und letzterer ist für alle Plattformen verfügbar. Hier soll ein eigener Paket-Sniffer in C und auf der Linux-Plattform programmiert werden.

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...

Zum Inhaltsverzeichnis Zum vorhergehenden Abschnitt


Copyright © Hochschule München, FK 04, Prof. Jürgen Plate
Letzte Aktualisierung: