Newsy|Artykuły|Projekty|Dodaj newsa|Dodaj na bugtraq|Rejestracja|Archiwum|Forum
Programowanie Raw Socket pod Windows

RAW SOCKET under Windows by StOcKy

Spis treści:

1. Wstęp
2. Co to Raw Socket?
3. Budowanie struktury nagłówków
4. Budowanie pakietu
5. Wysyłanie pakietu
6. Zastosowania praktyczne
7. Przykładowa aplikacja



1. Wstęp

Artykuł opisuje podstawy programowania RAW SOCKET pod systemem Windows przy użyciu języka programowania C/C++.

Co powinieneś wiedzieć…
- znać podstawy programowania w języku C/C++
- znać podstawy działania sieci.
- posiadać chociaż małe doświadczenie w pisaniu aplikacji sieciowych.

Protokół IP, którym się zajmiemy będzie na przykładzie jego czwartej wersji, IPv4. Przy pisaniu tego artykułu korzystałem z kompilatora Visual 7.0 firmy Microsoft.

2. Co to RAW SOCKET?

Systemy operacyjne zajmujące się obsługą TCP/IP, potrafią samoczynnie wypełniać za programistę wszystkie potrzebne dane w nagłówkach pakietu (np. adres nadawcy, pole TTL itd.),  który ma powędrować w sieć. Jeśli więc nasza aplikacja chciałaby np. zmienić adres nadawcy musimy samodzielnie od podstaw skonstruować cały pakiet, łącznie z nagłówkiem. I tu z pomocą przychodzą nam Raw Socket (dosłownie „surowe gniazda”), dzięki którym jesteśmy w stanie uzyskać nasz cel. System oprócz zwykłych gniazd PF_INET jak UDP czy TCP – oferuje również gniazda surowe. Przy tworzeniu owych gniazd to na użytkowniku spoczywa obowiązek stworzenia oraz analizy przesłanych pakietów. Niestety gniazda surowe pod systemem Windows są bardzo ograniczone (w przeciwieństwie do systemu GNU/Linux), więc stworzenie pakietu nie będzie tu takie proste.


3. Budowanie struktury nagłówków.

Więc nasze przygotowania do wysyłania naszych pakietów zaczniemy od zbudowania struktury wszystkich pól nagłówków protokołu TCP/IP. Protokół – TCP/IP –  możemy opisać za pomocą siedmiowarstwowego modelu ISO/OSI. Lepiej jednak oddaje funkcje i właściwości protokołu TCP/IP uproszczony model czterowarstwowy:

- warstwa aplikacji
- warstwa transportowa
- warstwa Internetu
- warstwa dostępu do sieci

Jak widzimy w modelu TCP/IP warstwa aplikacji zastąpiła warstwę aplikacji, prezentacji oraz sesji z siedmiowarstwowego modelu OSI. Funkcje tych warstw pokrywają się z zadaniami odpowiadających im warstw w modelu ISO/OSI. Podobnie jak w modelu OSI, kolejne warstwy dołączają bądź usuwają, w zależności od tego, w którą stronę przesuwają dane własne nagłówki – proces ten nazywany jest enkapsulacją. Tak wygląda jej przebieg:

- warstwa aplikacji                | DANE |
- warstwa transportowa         | NAGŁÓWEK | DANE |
- warstwa Internetu               | NAGŁÓWEK |  NAGŁÓWEK | DANE |
- warstwa dostępu do sieci    | NAGŁÓWEK | NAGŁÓWEK |  NAGŁÓWEK | DANE |

Tak więc z warstwy aplikacji gdzie znajdują się jedynie dane, dodawany jest nagłówek TCP w warstwie transportowej, następnie nagłówek IP w warstwie Internetu, oraz ostatecznie nagłówek ethernet w warstwie dostępu do sieci. Również i nas jako programistów czeka własnoręczne zbudowanie tych nagłówków. Przyjrzyjmy się budowie poszczególnych pakietów

Budowa pakietu TCP (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                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


krótki opis pól:

Source Port - (Port źródłowy) Port TCP, którego używa nadawca pakietu.

Destination Port – (Port docelowy) Port TCP, którego używa odbiorca pakietu

Sequence Numer – (Numer sekwencji) Zawiera numer pierwszego bajtu danych transportowanych za pomocą pakietu.

Acknowledgment Number – (Numer kolejnej sekwencji) Zawiera numer pierwszego bajtu danych, których oczekuje nadawca.

Data Offset – (Przesunięcie danych) Liczba 32-bitowych słów nagłówka.

Reserved – (Zarezerwowane) Do wykorzystania w przyszłości.

Flags – (Flagi) Informacje sterujące – m.un. bity SYN, ACK oraz FIN – używane do rozpoczęcia, kontynuacji i zakończenia połączenia.

Window – (Rozmiar okna) Wielkość okna transmisji lub rozmiar bufora danych odbieranych.

Checksum – (Suma kontrolna) Pozwala wykryć uszkodzenia nagłówka pakietu, które nastąpiły w wyniku błędów transmisji.

Urgens Pointer – (Wskaźnik pilnych danych) Opcjalny, pierwszy bajt pilnych danych w pakiecie, który pokazuje gdzie jest ich koniec.

Options – (Opcje) Opcje protokołu TCP, takie jak na przykład rozmiar największego segmentu TCP

Pudding – (uzupełnienie) jeśli pole opcji nie zajmuje pełnego słowa, to zostaje uzupełnione do 32 bitów [długość pola zmienna].

Data – (Dane) Dane protokołu wyższej warstwy.  




Budowa pakietu IP (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    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                  

Krótki opis pól pakietu IP:

Version – (Wersja) Wersja protokołu IP.

IHL (Internet Header Length) – (Długość) Rozmiar nagłówka datagramu mierzony w 32-bitowych słowach.

Type of Sernice – (Typ usługi) Określa sposób obsługi datagramu przez protokół wyższej warstwy (np. TCP lub UDP).

Total Length – (Rozmiar pakietu) Rozmiar całego pakietu IP mierzony w bajtach.

Identification – (Identyfikator) Liczba pozwalająca połączyć w całość fragmenty datagramu.

Flags – (Flagi) 3-bitowe pole, w którym pierwszy bit określa, czy pakiet może zostać podzielony na fragmenty. Drugi informuje, czy pakiet jest ostatnim fragmentem. Ostatni nie jest używany.

Fragment Offset – (Przesunięcie fragmentu) Położenie danych fragmentu liczone w bajtach od początku danych pakietu przed fragmentacją. Informacja ta ułatwia prawidłową rekonstrukcję pakietu.

Time to Live – (Czas życia) Specjalny licznik, którego wartość jest zmiejszana przez każde urządzenie sieciowe warstwy trzeciej. Gdy jego zawartość osiągnie zero, pakiet nie będzie dalej transmitowany, a nadawca zostanie poinformowany, że pakietu nie można dostarczyć. Taki sposób postępowania zabezpiecza przez wędrowaniem pakietów w nieskończoność.

Protocol – (Protokół) Kod protokołu wyższej warstwy.

Header Checksum – (Suma kontrolna) Suma wszystkich 16-bitowych słów nagłówka IP  policzona modulo 2^16. Przed jej wyliczeniem wartość tego pola wynosi 0. Po odebraniu pakietu pole to jest liczone ponownie i porównywane z oryginalną wartością. Suma kontrolna nagłówka pozwala wykryć ewentualne uszkodzenia nagłówka pakietu.

Source Adres – (Adres źródłowy) Adres IP nadawcy.

Destination Address – (Adres docelowy) Adres IP odbiorcy.

Options – (Opcje) Dodatkowe informacje dotyczące na przykład bezpieczeństwa.

Pudding – (uzupełnienie) jeśli pole opcji nie zajmuje pełnego słowa, to zostaje uzupełnione do 32 bitów [długość pola zmienna].



Dosyć już suchej teorii. Poniżej model struktury pakietu IP:

typedef struct iphdr {

        unsigned char  verlen;                        // IP version & length
        unsigned char  tos;                        // Type of service
        unsigned short total_len;                // Total length of the packet
        unsigned short ident;                        // Unique identifier
        unsigned short frag_and_flags;        // Flags
        unsigned char  ttl;                        // Time to live
        unsigned char  proto;                        // Protocol (TCP, UDP etc)
        unsigned short checksum;                // IP checksum
        unsigned int   sourceIP;                // Source IP
        unsigned int   destIP;                        // Destination IP

} IPHEADER;

Wyjaśnienia wymaga z pewnością zmienna verlen. Ponieważ pole “version” oraz „lenght” mają po cztery bity, nie można zaimplementować ich oddzielnie, ponieważ w języku C/C++ niema typu 4-bitowego, użyty tu został więc typ 8-bitowy, char. To samo tyczy się zmiennej frag_and_flags, z tym że tu pola są kolejno 4 i 12-bitowe. Tak, więc została stworzona struktura pakietu IP. Teraz musimy również zaimplementować pakiet TCP, który może przedstawiać się następująco:

typedef struct tcphdr {

        unsigned short sport;                        // Source port
        unsigned short dport;                        // Destination port
        unsigned int   seq;                        // Sequence number
        unsigned int   ack_seq;                        // Acknowledgement number
        unsigned char  lenres;                        // Length return size
        unsigned char  flags;                        // Flags and header length
        unsigned short window;                        // Window size
        unsigned short checksum;                // Packet Checksum
        unsigned short urg_ptr;                        // Urgent Pointer

} TCPHEADER;


Jak widać pod systemem Windows wszystko musimy zrobić własnoręcznie i nie mamy do dyspozycji gotowych prostych komponentów. Artykuł opisuje protokół TCP/IP jednak nic nie stoi na przeszkodzie aby utworzyć inny protokół np. UDP lub ICMP.


3. Budowanie pakietu.

A więc po stworzeniu struktur TCP oraz IP, możemy przystąpić do wypełniania ich w celu wysłania pakietu w świat. Od strony programistycznej może to wyglądać w następujący sposób:

Budowanie pakietu IP:


ipHeader.verlen=(4<<4 | sizeof(ipHeader)/sizeof(unsigned long));
ipHeader.total_len=htons(sizeof(ipHeader)+sizeof(tcpHeader));
ipHeader.ident=1;
ipHeader.frag_and_flags=0;
ipHeader.ttl=128;
ipHeader.proto=IPPROTO_TCP;
ipHeader.checksum=0;
ipHeader.destIP=ResolveAddress("192.168.0.2");
ipHeader.sourceIP=ResolveAddress("192.168.0.1");

Zmiennej verlen pierwszym 4-bitom została przypisana liczba 4, która reprezentuje wersję protokołu, a mianowicie Ipv4. Total_len przyjęła wartość łącznej długości bajtów pakietu IP ora TCP. Czas życia (TTL) został ustawiony na 128 „przeskoków” co spokojnie powinno  wystarczyć na przebycie każdej drogi w Internecie. Proto wyznaczało protokół wyższej warstwy, tutaj TCP. Checksum na razie ustawiamy na 0. W destIP wpisujemy docelowy adres IP, oraz analogicznie w polu sourceIP nasz IP. Do zamienienia ciągu znaków adresu IP na jego postać liczbową 32-bitową użyłem funkcji ResolveAddress, która wygląda następująco:


unsigned long ResolveAddress(char *szHost)
{
        unsigned long IP = inet_addr(szHost);
        if (IP==INADDR_NONE) {
                hostent *pHE = gethostbyname(szHost);
                if (pHE == 0)
                        return INADDR_NONE;
                IP = *((unsigned long *)pHE->h_addr_list[0]);        
        }

        return IP;
}


Nadszedł czas na pakiet TCP:

tcpHeader.dport=htons(4321);
tcpHeader.ack_seq=0;
tcpHeader.lenres=(sizeof(tcpHeader)/4<<4|0);
tcpHeader.flags=2;
tcpHeader.window=htons(16384);
tcpHeader.urg_ptr=0;
tcpHeader.checksum=0;
tcpHeader.sport=htons(1234);
tcpHeader.seq=htons((unsigned short)((rand() << 16) | rand()));


Port źródłowy ustawiliśmy na liczbę 1234, natomiast port docelowy na 4321. Oprócz tego numer sekwencji (seq) po prostu sobie wylosowaliśmy. Tak zbudowany pakiet nie możemy jeszcze wysłać, została nam jeszcze do wypełnienia suma kontrolna zarówno pakietu TCP jak i IP. Jako pierwszą wypełnimy sumę pakietu TCP, i tutaj posłużymy się tzw. Pakietem „pseudo”, który będzie miał na celu udanie pakietu IP, aby poprawnie obliczyć sumę kontrolną.

typedef struct pshdr {

        unsigned int   saddr;                        // Source address
        unsigned int   daddr;                        // Destination address
        unsigned char  zero;                        // Placeholder
        unsigned char  proto;                        // Protocol
        unsigned short length;                        // TCP length
        struct tcphdr tcp;                        // TCP Header struct

} PSDHEADER;

Teraz wypełniamy część z jego pól:

psdHeader.daddr=ipHeader.destIP;
psdHeader.zero=0;
psdHeader.proto=IPPROTO_TCP;
psdHeader.length=htons(sizeof(tcpHeader));
psdHeader.saddr=ipHeader.sourceIP;

Oraz obliczamy sumę kontrolną:

memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), &tcpHeader, sizeof(tcpHeader));
                
tcpHeader.checksum=checksum((USHORT*)szSendBuf,sizeof(psdHeader)+sizeof(tcpHeader));

Gdzie funkcja obliczająca sumę kontrolną wygląda tak:

USHORT checksum(USHORT *buffer, int size)
{
    unsigned long cksum=0;

    while (size > 1) {
        cksum += *buffer++;
        size  -= sizeof(USHORT);  
    }

    if (size)
        cksum += *(UCHAR*)buffer;  

    cksum = (cksum >> 16) + (cksum & 0xffff);
    cksum += (cksum >>16);

    return (USHORT)(~cksum);
}

Zanim suma została obliczona skopiowaliśmy do bufora szSendBuf nasz pseudo nagłówek IP oraz od razu po nim pakiet TCP, bowiem właśnie na podstawie tego bufora była wyliczona suma.

memcpy(szSendBuf, &ipHeader, sizeof(ipHeader));
memcpy(szSendBuf+sizeof(ipHeader), &tcpHeader, sizeof(tcpHeader));
memset(szSendBuf+sizeof(ipHeader)+sizeof(tcpHeader), 0, 4);

ipHeader.checksum=checksum((USHORT *)szSendBuf, sizeof(ipHeader)+sizeof(tcpHeader));

memcpy(szSendBuf, &ipHeader, sizeof(ipHeader));

Trzy pierwsze operacje kopiują do bufora szSendBuf kolejno pakiet IP oraz TCP, na końcu dodawane są cztery puste bajty. Następnie liczona jest suma kontrolna IP, oraz spowotem wpisanie do bufora nowego IP (już z poprawną sumą kontrolną)

Po wszystkich uciążliwych przeszkodach, nasz pakiet został utworzony.



5. Wysyłanie pakietu


Wszystkie potrzebne rzeczy do wysłania pakietu zostały zrobione. Teraz wystarczy wywołać prostą funkcję sendto() aby nasz pakiet powędrował dokąd chcemy, jednak wcześniej należy wspomnieć że inicjacja gniazd sieciowych odbywa się nieco inaczej, poniżej znajduje się przykładowy kod.

Sock = WSASocket(AF_INET,SOCK_RAW,IPPROTO_RAW,NULL,0, 0x01);

bool fag=true;
setsockopt(sock,IPPROTO_IP,2,(char *)&flag,sizeof(flag));

A następnie możemy już wysłać nasz pakiet:

sendto(sock, szSendBuf, sizeof(ipHeader)+sizeof(tcpHeader),0,(LPSOCKADDR)&ssin, sizeof(ssin));

Gdzie oczywiście sock jest deskryptorem gniazda sieciowego, a ssin jest strukturą SOCKADDR_IN.


Połączenie testowane jest na dwóch komputerach, jeden działa pod kontrolną systemu Windows XP sp1 o adresie IP 192.168.0.1. Oraz drugi komputer działający pod systemem GNU/Linux Slackware 9.1 (2.4.22), z adresem IP 192.168.0.2.

Tak wygląda log z programu TCPDUMP po wysłaniu pakietu:

11:20:11.352949 192.168.0.1.4321 > 192.168.0.2.1234: S [tcp sum ok] 1210253312:1210253312(0) win 16384 (ttl 128, id 256, len 40)

11:20:11.607799 192.168.0.2.1234 > 192.168.0.1.4321: R [tcp sum ok] 0:0(0) ack 1210253313 win 0 (DF) (ttl 64, id 0, len 40)

Jak widzimy wysłanie pakietu powiodło się. Na wysłanie naszego pakietu z flagą SYN, drugi komputer odpowiedział z flagą Rst, ponieważ próbowaliśmy połączyć się z portem, który jest zamknięty.


6. Zastosowania praktyczne

Pewnie wielu zadaje pytanie „No fajnie, ale właściwie do czego mi się to może przydać?”. Oczywiście „normalnych” aplikacji nie będziemy programować przy użyciu Raw Socket, ponieważ byłoby to zbyt trudne i pracochłonne – ogółem zbyteczne. Ale są pewne aplikacje, które potrzebują rozwiązania surowych gniazd. Programy tj. różnego rodzaju aplikacje do wysyłania „niestandardowych” pakietów, które potrzebują zmieniać różne pola w pakiecie, programy do ataków DoS (Denial of Sernice), które wysyłają pakiety z ustawioną flagą SYN, oraz podmienionym adresem źródłowym w celu sfałszowania swojego prawdziwego IP, używają tego również programy do tzw. Fingerprintingu czyli programy, które są w stanie wykryć OS’a na podstawie otrzymanych od niego pakietów. Raw Socket przydają się wszędzie, gdzie potrzebna jest stuprocentowa kontrola nad wysyłanymi pakietami.


7. Przykładowa aplikacja

Program wysyła pakiet po zebraniu od nas części informacji o jego polach.


TUTAJ ZNAJDUJE SIĘ KOD ŹRÓDŁOWY

Program kompilowałem w Microsoft Visual 7.0

Oczywiście jest to bardzo mały i ograniczony program, spróbujcie sami poeksperymentować i dodać do niego obsługę większej kontroli nad wysyłanym pakietem. Możliwości są niemal nieograniczone…




Autor:StOcKy
Czytań: 24054
Data publikacji: 2005-03-09 15:55:20


Copyrights 2002 - 2009 © CC-Team.org design by fre3ke. Hosted by Net1.pl