WinPCAP | Część druga - Łapanie pakietów
23.02.2013 17:07
Witam w kolejnej części kursu WinPcap, jak zakładam zapoznałeś się z częścią pierwszą, która jest absolutnie niezbędna, aby zrozumieć lekcję dzisiejszą. Jak sugeruje tytuł zajmiemy się łapaniem pakietów z wybranego interfejsu oraz wyświetleniem ich „surowej” zawartości, ale najpierw opiszę struktury, z których będziemy korzystać:
typedef struct pcap_if_t // To samo co pcap_if { struct pcap_if* next; /* Jeśli nie jest zerem * wskazuje kolejne urządzenie */ char* name; /* Nazwa urządzenia * Nie zrozumiałe dla normalnego człowieka */ char* description; /* Opis urządzenia * Zrozumiałe dla normalnego człowieka */ struct pcap_addr* addresses; /* Struktura struktur * dotycząca adresowania interfejsu */ unsigned int flags; /* Flaga interfejsu, * na razie jest jedynie PCAP_IF_LOOPBACK * co oznacza pętle zwrotną */ } pcap_if_t; struct pcap_addr { struct pcap_addr* addresses; /* Jeśli nie jest zerem * wskazuje kolejny element listy */ struct sockaddr* addr; /* Adres urządzenia */ struct sockaddr* netmask /* Maska sieci, może być zerem * jeśli urządzenie nie obsługuje */ struct sockaddr* broadaddr /* Adres rozgłoszeniowy, może być zerem * jeśli urządzenie nie obsługuje */ struct sockaddr* dstaddr /* Adres docelowy, może być zerem * jeśli urządzenie nie obsługuje */ /* Jak widać wszędzie jest sockaddr, znane z WinSOCK * struktura zawiera rodzinę adresów i dodatkowe informacje */ }; struct pcap_pkthdr { struct timeval ts; /* Struktura czasu */ bpf_u_int32 caplen; /* Ilość bitów pobrana z pakietu */ bpf_u_int32 len; /* Rzeczywisty rozmiar pakietu */ /* bpf_u_int32 to 32 bitowy, nieujemny typ int */ }; typedef struct timeval { long tv_sec; // Sekundy long tv_usec; // Mikrosekundy } timeval; // Opcjonalna struct pcap_rmtauth { char* username; // Nazwa użytkownika char* password; // Hasło użytkownika int type; /* Typ uwierzytenienia * RPCAP_RMTAUTH_NULL – Nie uwierzytelniamy się * RPCAP_RMTAUTH_PWD – Uwierzytelniamy się */ };
Kod programu
#define HAVE_REMOTE typedef unsigned int u_int; typedef unsigned short u_short; typedef unsigned char u_char; #define FAILURE -1 #define SUCCESS 0 #include <pcap.h> #include <stdio.h> #include <time.h> void PacketHandler(u_char* Param, const struct pcap_pkthdr* Header, const u_char* PacketData); int main() { pcap_if_t* Devices = NULL; pcap_if_t* FirstDevice = NULL; pcap_t* CaptureInstance = NULL; int DevicesCounter = -1; int DeviceSelected = -1; char ErrorBuffer[PCAP_ERRBUF_SIZE] = ""; if (FAILURE == pcap_findalldevs_ex("rpcap://", NULL, &Devices, ErrorBuffer)) { fprintf(stderr, "pcap_findalldevs_ex error: %s", ErrorBuffer); return FAILURE; } FirstDevice = Devices; do { DevicesCounter++; printf(" [%i]:\n", DevicesCounter); printf(" Name: %s\n", Devices->name); printf(" Description: %s\n", Devices->description); } while (NULL != (Devices = Devices->next)); Devices = FirstDevice; printf("Choice: "); scanf("%d", &DeviceSelected); if (0 > DeviceSelected || DevicesCounter < DeviceSelected) { printf("There are no devices in this range!"); pcap_freealldevs(Devices); return FAILURE; } for (int i = 0; i < DeviceSelected && NULL != (Devices = Devices->next); ++i); if (NULL == (CaptureInstance = pcap_open(Devices->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, ErrorBuffer))) { fprintf(stderr, "pcap_open error: %s", ErrorBuffer); pcap_freealldevs(Devices); return FAILURE; } pcap_freealldevs(Devices); printf("Listening...\n\n\n"); if (FAILURE == pcap_loop(CaptureInstance, 0, PacketHandler, NULL)) { fprintf(stderr, "Some error occurred while listening"); return FAILURE; } return SUCCESS; } void PacketHandler(u_char* Param, const struct pcap_pkthdr* Header, const u_char* PacketData) { struct tm* LocalTime; time_t LocalSeconds = 0; char Time[16] = ""; LocalSeconds = Header->ts.tv_sec; LocalTime = localtime(&LocalSeconds); strftime(Time, 16, "%H:%M:%S: ", LocalTime); printf("%s | %s |\n", Time, PacketData); }
Kompilacja i efekty pracy programu
Powyższy kod programu kompilujemy poleceniem: gcc -std=c99 ścieżka\wejście.c -o ścieżka\wyjście.exe -l wpcap (i ew. -l ws2_32)
Po uruchomieniu programu ukazały nam się wszystkie interfejsy jakie mamy lokalnie. Po wyborze jednego z nich zaczniemy „łapać” pakiety.
Uwaga! Jeśli mamy kilka interfejsów i po wyborze jakiegoś z nich nic się nie dzieje, oznacza to, że akurat jest on nieaktywny lub przeznaczony do innych celów np. wirtualizacji. W tym wypadku wybieramy inny.
W konsoli po lewej widać datę o jakiej przechwyciliśmy pakiet oraz szum informacyjny pośrodku. Ten szum będziemy interpretować w kolejnej części.
Przechwytywałeś pakiety całą noc, teraz chcesz program wyłączyć. Robimy to kombinacją klawiszy: CTRL+C
Omówienie i analiza kodu
typedef unsigned int u_int; typedef unsigned short u_short; typedef unsigned char u_char;
Musimy zrobić aliasy na potrzeby biblioteki, która nagminnie korzysta z tych typów, inaczej będzie się do nas kompilator rzucał. Jest też druga metoda: Możemy zlinkować ws2_32 i dorzucić nagłówek winsock2.h przed pcap.h, ale to by zwiększyło niepotrzebnie rozmiar programu.
#define FAILURE -1 #define SUCCESS 0
Zdefiniowałem to bo większość funkcji w razie błędu zwraca -1 oraz wygląda to lepiej i czytelniej. Podczas warunków widać od razu czego oczekujemy od funkcji.
#include <pcap.h> #include <stdio.h> #include <time.h>
Tutaj dodatkowo dorzucony nagłówek time.h, bo podczas przechwytywania chcemy ujrzeć godzinę.
void PacketHandler(u_char* Param, const struct pcap_pkthdr* Header, const u_char* PacketData);
Prototyp funkcji PacketHandler, do której pcap_loop wysyła odebrane pakiety do obróbki tzw. Wywołanie zwrotne (ang. callback). Funkcja nic nie zwraca, co widać. u_char Param, przyjmuje także postać u_char u_User. Jeszcze się nie spotkałem z użyciem tej zmiennej. const struct pcap_pkthdr* Header Opisuje czas przechwytu pakietu w sekundach, wielkość pobraną i wielkość całkowitą pakietu. const u_char* PacketData zawiera pakiet.
pcap_if_t* Devices = NULL; pcap_if_t* FirstDevice = NULL; pcap_t* CaptureInstance = NULL; int DevicesCounter = 0; int DeviceSelected = -1; char ErrorBuffer[PCAP_ERRBUF_SIZE] = "";
FirstDevice, dalej przypisujemy wskaźnikowi adres na pierwszy element Devices. Jak „przelecimy” wszystkie elementy zbioru Devices, to będzie on wynosił NULL i nie będziemy mogli przeskoczyć do pierwszego elementu, ani zwolnić pamięci, dlatego używamy wskaźnika pomocniczego.
CaptureInstance, instancja, na której odbywa się łapanie pakietów. Nie mamy do niej dostępu bezpośredniego, ani nie wolno jej kopiować, bo nie jest gwarantowane, że będzie działać jak należy.
DevicesCounter Będziemy liczyć ilość urządzeń i wstawimy w warunek przy wyborze interfejsu łapania pakietów.
DeviceSelected Wybór urządzenia przez użytkownika.
FirstDevice = Devices; do { DevicesCounter++; printf(" [%i]:\n", DevicesCounter); printf(" Name: %s\n", Devices->name); printf(" Description: %s\n", Devices->description); } while (NULL != (Devices = Devices->next));
Tak jak pisałem, przypisujemy adres pierwszego elementu Devices do FirstDevice. Widzimy pętle do .. while, w której wypisujemy kolejne elementy Devices oraz post-inkrementujemy ilość urządzeń jak i przesuwamy wskaźnik Devices, następnie sprawdzamy cały warunek. (Pierwszeństwo operatorów ())
Devices = FirstDevice; printf("Choice: "); scanf("%d", &DeviceSelected); if (0 > DeviceSelected || DevicesCounter < DeviceSelected) { printf("There are no devices in this range!"); pcap_freealldevs(Devices); return FAILURE; }
Tym razem przypisujemy do Devices adres FirstDevice i od tej pory możemy znów robić iteracje na Devices. Następnie mamy proste pobranie wartości od użytkownika, kontynuowane przez sprawdzenie czy podany interfejs faktycznie jest wybranym z listy dostępnych.
for (int i = 0; i < DeviceSelected && NULL != (Devices = Devices->next); ++i);
Tutaj wykonujemy iteracje, aż dotrzemy do pożądanego interfejsu. Dodatkowym zabezpieczeniem jest tu sprawdzanie czy wskaźnik Devices nie jest pusty, jednak nie o zabezpieczenie nam tu chodzi, bo i tak scanf się wysypie jeśli wpiszemy inny znak niż cyfry.
if (NULL == (CaptureInstance = pcap_open(Devices->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, ErrorBuffer))) { fprintf(stderr, "pcap_open error: %s", ErrorBuffer); pcap_freealldevs(Devices); return FAILURE; }
Mamy tutaj przypisanie wyniku funkcji pcap_open do CaptureInstance. pcap_open zwraca pusty wskaźnik jeśli utworzenia deskryptora się nie udało. Jako pierwszy argument podajemy nazwę interfejsu, jako drugi maksymalną wielkość pakietu jaka ma być przechwycona. Kolejny argument to specjalna flaga, która zmienia działanie funkcji. Oznacza ona w jaki sposób pakiety mają być łapane.
#define PCAP_OPENFLAG_PROMISCUOUS 1
Wszystko jest wyłapywane.
#define PCAP_OPENFLAG_DATATX_UDP 2
Jeśli wyłapujemy pakiety na zdalnej maszynie to przez jaki protokół mają przychodzić do lokalnego klienta. Jeśli ustawimy tę flagę to będą one ściągane ze zdalnej maszyny przez protokół UDP, czyli ten najszybszy i najlżejszy, ale wiąże się to z konsekwencją utraty poprawności danych po drodze. Jeśli zależy nam na poprawności transmisji to odradzam stosowania tego protokołu. Jeśli tej flagi nie wybierzemy to wszystkie pakiety będą ściągane wolniej, ale nie utracimy żadnego. Flaga jest bez znaczenia jeśli ciągniemy od siebie.
#define PCAP_OPENFLAG_NOCAPTURE_RPCAP 4
Flaga określa czy zdalne urządzenie ma wysyłać do lokalnego klienta, ruch generowany przez siebie samego.
#define PCAP_OPENFLAG_NOCAPTURE_LOCAL 8
Flaga określa czy lokalne urządzenie ma wyłapywać ruch generowany przez siebie samego.
#define PCAP_OPENFLAG_MAX_RESPONSIVENESS 16
Standardowo WinPCAP czeka na kilka pakietów przed kopiowaniem ich do użytkownika, co gwarantuje zmniejszone zużycie procesora, zmniejszoną ilość wywołań systemowych oraz ogólnie poprawioną wydajność. Natomiast ustawiona flaga powoduje, że każdy złapany pakiet natychmiast jest kopiowany do aplikacji, ale dodatkowo obciąża procesor i zwiększa ilość użyć wywołań systemowych, co powoduje zmniejszenie wydajności.
W czwartym argumencie podajemy liczbę milisekund jaką będziemy czekać na pakiety. Jest to ustawiane po to, aby zmniejszyć obciążenie procesora. Pakiety nie są kopiowane natychmiast po ich odebraniu, ale pod upływie czasu. Argument jest ignorowany jeśli używamy flagi PCAP_OPENFLAG_MAX_RESPONSIVENESS
Kolejnym argumentem jest adres struktury pcap_rmtauth, która jest przeznaczona do uwierzytelniania. Łatwo można nauczyć się zdalnego łapania pakietów wypisując w pcap_findalldevs_ex „na pałę” „rpcap://xxx.xxx.xxx.xxx” gdzie x to dowolna cyfra, ale liczba powstała nie może być większa niż 255, a w strukturę pcap_rmtauth jako user i password „admin”, albo jakieś inne kombinacje tego typu. Możecie wierzyć lub nie, ale dużo ludzi nie konfiguruje swoich routerów pod kątem bezpieczeństwa i zostawia wszystko domyślnie. Jednakże takowa przypadkowa osoba musi mieć zainstalowanego WinPCAPA ;]
Szósty argument to standardowa tablica char-ów, która wyłapuje wszystkie komunikaty o błędach.
pcap_freealldevs(Devices); printf("Listening...\n\n\n"); pcap_loop(CaptureInstance, 0, PacketHandler, NULL);
Czyścimy pamięci, zerujemy wskaźniki i informujemy użytkownika o kontynuowaniu operacji.
pcap_loop zwraca -1 podczas błędu lub -2 gdy przerwaliśmy wywołanie funkcją pcap_breakloop(), a nawet 0 gdy pakiety zostały poprawnie skopiowane. Pierwszy argument to nasza instancja, drugi oznacza limit pakietów. Jest to warunek, który sprawdza czy przypadkiem ilość skopiowanych pakietów nie przekroczyła limitu i w zależności od wyniku tej operacji pętla jest przerywana lub nie. Wpisujemy 0 jeśli chcemy pętle nieskończoną. Trzeci argument to nazwa funkcji, która jest przeznaczona do obróbki pakietów. Czwarty argument to nazwa użytkownika. Przyznaje się, że nie wiem co to ma robić. Niestety dokumentacja jest tak napisana, że większości trzeba się domyślać. Tak czy siak wpisujemy tam NULL.
Teraz zajmijmy się opisem funkcji PacketHandler.
void PacketHandler(u_char* Param, const struct pcap_pkthdr* Header, const u_char* PacketData) { struct tm* LocalTime; time_t LocalSeconds = 0; char Time[16] = ""; LocalSeconds = Header->ts.tv_sec; LocalTime = localtime(&LocalSeconds); strftime(Time, 16, "%H:%M:%S: ", LocalTime); printf("%s | %s |\n", Time, PacketData); }
LocalTime to wskaźnik na strukture tm, która jak każdy wie posiada wiele pól na zachowanie np. aktualnej daty. Potem mamy definicje zmiennej LocalSeconds typem time_t, która będzie zachowywać ilość sekund pobranej z Header->ts.tv_sec. Następnie konwertujemy zmienną LocalSeconds na strukturę tm, która jest niebywale czytelniejsza od zwykłego typu time_t. Funkcja strftime() przekształca zawartość struktury LocalTime na tekst. Pierwszy argument to tablica char-ów. Drugi, wielkość tej tablicy. Trzeci, formatowanie. Czwarty, struktura LocalTime jako wskaźnik. I w ostatniej linijce wypisujemy datę pobrania pakietu oraz jego zawartość.
Kolejna część będzie dotyczyła zdalnego pobierania pakietów, bez funkcji callback, poprzez filtrowany ruch. Także zajmiemy się interpretacją nagłówka IPv4
Serwus!