Ten dokument w formacie .doc
Źródła i dokumentacja biblioteki libnids

 

 

 

 

Uniwersytet Warszawski

 

 

Wydział Matematyki, Informatyki i Mechaniki

 

 

 

 

 

Libnids – biblioteka wspomagająca

konstruowanie oprogramowania typu

Network Intrusion Detection Systems

 

 

 

Rafał Wojtczuk

numer albumu: 158044

Promotor: dr Janina Mincer-Daszkiewicz

 

 

 

 

 

 

 

Warszawa, czerwiec 1999

 

 

Praca magisterska napisana w Instytucie Informatyki

 

 

 

 

 

 

Spis treści

 

 

 

1. Wstęp *

2. Bezpieczeństwo w sieci Internet *

2.1. Waga zagadnienia *

2.2. Przyczyny istnienia luk w bezpieczeństwie systemów *

3. Definicja i cele działania IDS *

3.1. Wprowadzenie *

3.2. IDS sieciowe, czyli NIDS *

3.3. Rodzaje ataków sieciowych *

4. Metody działania NIDS *

4.1. Logiczne komponenty IDS *

4.2. Algorytmy IDS *

4.3. NIDS a inne systemy bezpieczeństwa *

5. Problemy ze zdobywaniem danych *

5.1. Znaczenie poprawnego emulowania stosu TCP/IP *

5.2. Złożoność systemów komputerowych a złożoność NIDS *

5.3. Rozbieżności między różnymi systemami w implementacji stosu TCP/IP *

5.4. Problem wykorzystania zasobów przez NIDS *

5.5. Problem wykorzystania zasobów przez monitorowane węzły *

6. Opis libnids *

6.1. Dlaczego libnids jest potrzebna *

6.2. Oferowana funkcjonalność *

6.3. Niezawodność i konfigurowalność *

7. Przeprowadzone testy *

8. Napotkane problemy z jądrem Linuksa *

9. Możliwe drogi rozwoju libnids *

10. Podsumowanie *

Bibliografia *

Dodatek A. Tożsamość klienta usługi TCP a jądro Linuksa 2.0.36 *

Dodatek B. Wykryte błędy w jądrze Linuksa 2.0.36 *

Dodatek C. Słownik niektórych użytych terminów *

Dodatek D. Libnids – interfejs programisty *

 

 

 

1. Wstęp

 

 

Niniejsza praca jest poświęcona zagadnieniu niezawodnego dostarczania informacji o ruchu w sieci modułom oprogramowania typu NIDS, czyli Network Intrusion Detection Systems. Takie oprogramowanie ma na celu wykrycie prób nieautoryzowanego dostępu lub nadużycia systemów komputerowych.

Przyczyną powstania i szybkiego rozwoju programów NIDS jest rosnące zagrożenie systemów komputerowych ze strony intruzów. W powszechnie używanych systemach operacyjnych i programach użytkowych znajduje się wiele błędów. Są one stopniowo wykrywane, a wiedza o nich rozpowszechniana za pośrednictwem Internetu [9]. Intruz może wykorzystać luki w systemie w celu uzyskania dodatkowych uprawnień lub dostępu do tego systemu. NIDS ma za zadanie wspomagać administratora w przeciwdziałaniu intruzom.

Oprogramowanie typu NIDS zazwyczaj działa na poziomie sieci lokalnej. Takie programy analizują każdy pakiet, który przepływa przez sieć i starają się symulować jego wpływ na węzeł, który go odbierze. Jeśli obserwowany ruch nosi znamiona wrogiego działania, to NIDS informuje o tym administratora lub samodzielnie podejmuje kroki zaradcze.

Ideą leżącą u podstaw NIDS jest możliwość przewidzenia, w jaki sposób węzły w sieci interpretują docierające do nich pakiety, szczególnie w kontekście protokołu TCP/IP. NIDS musi być w stanie określić, jakie dane otrzymają sieciowe aplikacje działające na chronionych węzłach. Nie jest to łatwe, gdyż intruz może starać się ominąć NIDS generując ciąg nietypowych pakietów, które w skrajny sposób wykorzystują liberalność ograniczeń nakładanych przez protokoły sieciowe.

W pracy [1] przeanalizowano działanie czterech znanych komercyjnych programów NIDS. Żaden z nich nie umiał przewidzieć, w jaki sposób testowe, nietypowe pakiety TCP zostaną odczytane przez węzły w sieci.

Częścią niniejszej pracy jest projekt i implementacja biblioteki libnids, która ma za zadanie w niezawodny sposób dostarczać dane o ruchu w sieci wyższym warstwom NIDS. Dzięki niej projektant NIDS może skupić się na analizie danych, a nie na ich pozyskiwaniu. Biblioteka została zaimplementowana w języku C. Została przetestowana pod Linuksem, a jej przeniesienie na inny system operacyjny powinno wymagać minimalnych zmian.

Moduły pozyskujące dane dla NIDS (w szczególności libnids) mają swoje ograniczenia. Wiele systemów operacyjnych implementuje protokoły sieciowe w sposób nieścisły, co musi być uwzględnione przez NIDS. W szczególności, dzięki testom biblioteki zostały wykryte trzy błędy w kodzie sieciowym jądra Linuksa 2.0.36, które utrudniają niezawodne działanie libnids.

Na początku niniejszej pracy w sposób ogólny omówiono zagadnienia związane z bezpieczeństwem w sieci (rozdział 2). W dwóch kolejnych rozdziałach (3 i 4) zamieszczono idee leżące u podstaw NIDS. W rozdziale 5 szerzej opisano problem pozyskiwania danych dla NIDS. Rozdział 6 i 7 to opis libnids. Pracę zamykają rozważania nad nieuchronnymi ograniczeniami NIDS, działających w środowisku systemów Linux (rozdział 8). Szczególną uwagę poświęcono problemowi przewidzenia gospodarki zasobami chronionych węzłów, który do tej pory nie był należycie doceniony.

 

 

 

 

 

 

2. Bezpieczeństwo w sieci Internet

 

 

2.1. Waga zagadnienia

 

Jednym z najważniejszych wydarzeń w historii systemów operacyjnych było pojawienie się systemów wielodostępnych. Takie systemy wykorzystują możliwości komputera wydajnie i w sposób wygodny dla użytkowników i administratora. Praca wielu użytkowników na jednej maszynie w oczywisty sposób umożliwia współdzielenie zasobów. Pojawia się jednak nowe wyzwanie dla systemu operacyjnego – różnicowanie praw dostępu do obiektów w systemie, egzekwowanie tych praw oraz ochrona użytkowników przed destrukcyjną działalnością jednego z nich. Powstała nowa klasa problemów – bezpieczeństwo systemów komputerowych.

Złożoność zagadnienia wzrosła znacząco po powstaniu wielkich sieci, a szczególnie Internetu. Obecnie większość maszyn połączonych z siecią udostępnia pewne usługi. Są one narażone na ataki przez sieć, które mają na celu obejście zabezpieczeń systemu. Administrator musi brać pod uwagę możliwość podjęcia wrogich działań nie tylko przez użytkowników maszyn będących pod jego opieką (których jest zwykle niezbyt wielu, są dobrze znani i często darzeni zaufaniem), ale również przez jednego z dziesiątków milionów użytkowników Internetu.

Dla administratora nie ma gorszej wiadomości niż ta, że jego system jest pod kontrolą intruza. Konsekwencje mogą być fatalne. Po pierwsze, wszystkie informacje zgromadzone na serwerze, często poufne, są dostępne dla osoby o nieznanych intencjach. Przykładowo, intruz ma możliwość czytania i usuwania poczty użytkowników. Może on podmienić główną stronę WWW serwera na stronę zawierającą dowolne treści (ulubiona czynność niektórych intruzów). Groźniejsze mogą być zmiany, które nie zostaną szybko zauważone, na przykład modyfikacja bazy danych znajdującej się na serwerze.

Po drugie, intruz może sparaliżować pracę systemu, często nie ujawniając swojej obecności. Najprostszym sposobem jest skasowanie wszystkich plików na serwerze. Jeśli intruz nie uzyskał praw nadzorcy, to może on sabotować system przez zużywanie dużej ilości zasobów – pamięci wirtualnej, dysku, czasu procesora.

Co gorsza, zniszczenia nie ograniczają się tylko do jednego serwera. Jeśli użytkownicy sieci lokalnej korzystają z nieszyfrowanych protokołów sieciowych (takich jak telnet, ftp czy imap) lub słabo szyfrowanych (na przykład PPTP), to intruz podsłuchując ruch w sieci lokalnej (ang. sniffing) może poznać hasła wymieniane podczas autoryzacji. Zazwyczaj wystarcza to do uzyskania dostępu do innych systemów. Protokół rsh (który realizuje zdalny dostęp) korzysta z pojęcia zaufanych węzłów sieci (ang. trusted host). Jeśli zaufany węzeł A jest pod kontrolą intruza, to ten ostatni ma on automatycznie dostęp do węzłów, które ufają węzłowi A.

Nawet jeśli użytkownicy serwera kontrolowanego przez intruza korzystają wyłącznie z aplikacji stosujących mocne kodowanie (np. ssh), to istnieje możliwość podmienienia programów wykonywanych na serwerze na konie trojańskie (patrz dodatek C), które na przykład zapisują hasła dostępu do innych systemów. Aby przeciwdziałać takim praktykom, niektóre systemy operacyjne (między innymi OpenBSD, Linux) umożliwiają zdefiniowanie poziomu bezpieczeństwa systemu (ang. securelevel). Jeśli jest on różny od zera, to nawet nadzorca nie ma praw do wykonywania pewnych czynności, między innymi modyfikacji plików oznaczonych jako “niezmienne” (ang. immutable). Przy niezerowym poziomie bezpieczeństwa intruz nie ma możliwości podmiany programów systemowych.

Dobrze wyszkoleni intruzi nie atakują systemów z komputerów, do których mają legalny dostęp (czyli założone przez administratora konto). W przypadku nieudanego ataku umożliwiałoby to ich łatwą identyfikację i pociągnięcie do odpowiedzialności. Zazwyczaj intruz podporządkowuje sobie pewien słabo zabezpieczony system, zaciera w nim swoje ślady, a następnie może go używać wielokrotnie do atakowania innych celów. Dlatego nawet administrator serwera, który nie zawiera bardzo istotnych danych, również powinien dbać o bezpieczeństwo powierzonego mu systemu, aby utrudnić intruzom poruszanie się w sieci.

 

2.2. Przyczyny istnienia luk w bezpieczeństwie systemów

 

Bezpieczeństwo wielu dzisiejszych systemów operacyjnych (w tym Uniksa) oraz protokołów sieciowych (w tym TCP/IP) nie było priorytetem przy ich projektowaniu. Dlatego twórcy ich implementacji mieli duże szanse popełnienia błędów. Niestety są one obecne w wielu miejscach. Ich wykorzystanie daje intruzowi dostęp do systemu lub dodatkowe uprawnienia. Przykładowo, Unix jest napisany w języku C, w którym jest możliwe popełnienie pomyłki programistycznej zwanej przepełnieniem bufora [2] (patrz dodatek C). Wykorzystanie tego błędu jest łatwe i efektywne, stąd jest ulubioną techniką intruzów.

Często bezpieczeństwo jest osłabiane w imię wygody administratora i użytkowników. Przykładowo, nie ma żadnej przyczyny, aby używać nieszyfrowanych protokołów takich jak telnet czy ftp, skoro są dostępne ich bezpieczniejsze odpowiedniki (ssh). Często domyślne konfiguracje systemu operacyjnego mają na celu maksymalną funkcjonalność. Udostępniają wiele usług, które intruz może wykorzystać do swoich celów. Administrator, który nie jest ekspertem, może nie wiedzieć, które usługi może wyłączyć, a które są niezbędne dla działania systemu. Dlatego domyślna konfiguracja często pozostaje niezmieniona przez cały czas istnienia systemu.

Nawet najbezpieczniejszy system jest niewiele wart w rękach niekompetentnego nadzorcy. Wiele systemów zostało spenetrowanych dzięki temu, że domorosły administrator napisał pełen błędów program i nadał mu najwyższe przywileje, co otworzyło furtkę dla intruza. Również wiele komercyjnych aplikacji ma błędy. Czas pracy programisty, który powinien być poświęcony na zabezpieczenie oprogramowania lub systemu przed zakusami intruzów, często jest ograniczany w imię redukcji kosztów.

Praca niniejsza jest poświęcona bezpieczeństwu sieciowemu, stąd pominięto w niej kwestie nie związane bezpośrednio z siecią. Jednakże trzeba pamiętać, że sieć jest tylko jednym ze źródeł zagrożenia. O bezpieczeństwo należy dbać całościowo. Najlepsze zabezpieczenia systemu i aplikacji nic nie dadzą, jeśli nie jest zapewnione fizyczne bezpieczeństwo systemów komputerowych. Jeśli ktoś chce wykraść firmie X jej dane, to jedną z metod jest przecież włamanie do budynku, w którym mieści się serwer i kradzież twardego dysku. Trzeba być przygotowanym również na taką możliwość. Istotne jest sformułowanie i przestrzeganie ogólnej polityki bezpieczeństwa, określonej dla wszystkich członków danej organizacji i jej zasobów komputerowych. Ponadto, według wielu niezależnych źródeł w 80 % przypadków naruszenie bezpieczeństwa systemów należących do organizacji jest dziełem jej pracowników. Fakt, że zazwyczaj mogą być oni łatwo zidentyfikowani, często nie odstrasza wystarczająco. Pamiętając o bezpieczeństwie aplikacji sieciowych, nie wolno zaniedbywać innych systemowych programów, do których mają dostęp użytkownicy lokalni. Jest to tym bardziej istotne, że często intruzi zdobywają najpierw nieuprzywilejowany dostęp do atakowanego systemu.

Podsumowując, w systemach obecnie najczęściej używanych są błędy, które intruzi mogą próbować wykorzystać. Ta sytuacja nie zmieni się szybko. Nadzorca systemu musi poświęcać część swojego czasu na wypatrywanie oznak wrogiej działalności. Nie jest to łatwe, szczególnie kiedy należy chronić większą liczbę maszyn. Potrzebne są więc programy komputerowe, które bez pomocy administratora będą mogły rozpoznać atak i być może podjąć kroki zaradcze.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3. Definicja i cele działania IDS

 

 

3.1. Wprowadzenie

 

W pracy [1] Intrusion Detection System, czyli IDS, jest zdefiniowany jako technologia mająca na celu wykrycie i identyfikację działań mających na celu nieautoryzowany dostęp lub nadużycie systemu komputerowego. Według tej definicji, do IDS można zaliczyć zapory przeciwogniowe (ang. firewall) i aplikacje “owijające” (ang. wrappers). Jednakże zazwyczaj mianem IDS określa się systemy bardziej złożone, dysponujące obszerną wiedzą o wzorcach zachowań uznawanych za niebezpieczne.

 

3.2. IDS sieciowe, czyli NIDS

 

Im więcej informacji o monitorowanym systemie jest dostępnych dla IDS, tym lepszą ochronę może on zapewnić. Pewne dane, na przykład liczba i rodzaj procesów w systemie czy też identyfikatory zarejestrowanych w nim użytkowników, są najłatwiej osiągalne wtedy, gdy IDS jest uruchomiony na tym samym węźle, który kontroluje. To rozumowanie prowadzi do koncepcji IDS opartego na węźle (ang. host based). Takie systemy często ograniczają się do analizy systemowych dzienników zdarzeń (ang. logs), szczególnie jeśli system operacyjny zapisuje do nich informacje o powstaniu i zakończeniu każdego procesu (ang. accounting).

IDS oparte na węźle zapewniają najlepszą ochronę kontrolowanej maszynie. W szczególności, jeśli administratorowi zależy najbardziej na wykrywaniu ataków lokalnych, czyli podejmowanych przez użytkowników jego maszyny, to taki typ IDS jest najlepszym rozwiązaniem.

Warto w tym miejscu wspomnieć o programach zwanych monitorami integralności systemu. Raczej nie można zaliczyć ich do IDS, ale mogą one ściśle z IDS współpracować. Takie programy liczą co pewien czas sumy kontrolne tych plików w systemie, które powinny być niezmienne (w szczególności programów wykonywalnych). Jeśli monitor wykryje zmianę sumy kontrolnej, to oznacza to, że najprawdopodobniej pewien intruz zdołał spenetrować system i zamienić jeden z jego ważnych plików, zwykle w celu późniejszego łatwego uzyskania dostępu. Jest to bezcenne ostrzeżenie dla administratora, który powinien podjąć kroki mające na celu identyfikację i pozbycie się intruza.

Pewne formy ataku nie są wykrywane przez IDS oparte na węźle. Powiedzmy, że atakujący zna lukę w bezpieczeństwie usługi słuchającej na porcie X. Załóżmy, że spróbuje on połączyć się z portem X na każdej z maszyn w naszej sieci. Nawet jeśli na każdym komputerze pracuje jakiś IDS, to żaden z tych systemów nie zauważy nic szczególnego – jednokrotną próbę połączenia się z usługą. Atak jest widoczny w większej skali, na poziomie sieci. Aby go wykryć, IDS musi słuchać całego ruchu w sieci lokalnej. Jest to łatwo osiągalne, jeśli medium sieci jest ethernet. W takim przypadku IDS może być uruchomiony na jednym węźle, którego karta sieciowa jest ustawiona w tryb “rozwiązły” (ang. promiscuous), co powoduje, że odbiera ona wszystkie pakiety przepływające przez sieć. IDS kontrolujący ruch w sieci (zazwyczaj lokalnej) nazywamy NIDS, czyli sieciowy IDS.

Niewątpliwie NIDS otrzymuje dane tylko o ruchu w sieci. Może to być w wielu przypadkach niewystarczające. Jak już wspomniano, do wykrywania ataków lokalnych najlepiej nadają się IDS oparte na węźle. Jednak koncepcja NIDS ma wiele zalet. Po pierwsze, NIDS jest w stanie wykryć atak pochodzący z zewnątrz sieci (na przykład z jednego z milionów węzłów Internetu). Jest to źródło szczególnego zagrożenia. Po drugie, jeden NIDS jest bardziej ekonomiczny i łatwiejszy w zarządzaniu od grupy IDS, rozproszonych po węzłach sieci lokalnej. NIDS analizuje ruch w sieci pasywnie – węzły sieci nie muszą wiedzieć o istnieniu NIDS, co ułatwia jego wdrożenie. Również wydajność sieci ani jej węzłów nie jest obniżona przez działanie NIDS. Te względy powodują, że zainteresowanie NIDS rośnie. Również niniejsza praca jest poświęcona niektórym aspektom funkcjonowania takich systemów.

 

3.3. Rodzaje ataków sieciowych

 

Rodzaje zdalnych ataków (takie wykrywa NIDS) można podzielić na dwie grupy.

3.3.1. Ataki typu DOS. Pierwsza z nich to uniemożliwienie dostępu do zasobów (ang. DOS, denial of service). Mają one na celu utrudnienie uprawnionym użytkownikom korzystania z zaatakowanego systemu. Mogą to być ataki wykorzystujące błędy w systemie operacyjnym. Na przykład słynny program “teardrop.c” wysyła serie fragmentowanych pakietów IP (patrz dodatek C), zachodzących na siebie w specyficzny sposób. Wiele systemów nie było przygotowanych na takie nietypowe pakiety – ich nadejście powodowało zawieszenie lub restart systemu operacyjnego.

Najprostszy atak DOS to zatapianie (ang. flood). Polega on na wygenerowaniu dużego ruchu skierowanego do atakowanego systemu. Jeśli jest on podłączony do sieci niezbyt szybkim łączem, to atakujący może całkowicie zająć jego przepustowość, znacząco utrudniając komunikację z zaatakowanym serwerem. Źle skonfigurowane rutery mogą spowodować, że cała sieć zostanie użyta jako “wzmacniacz ICMP” [6]. W takim przypadku atakujący generując przepływ X KB/s może obciążyć atakowany system przepływem nawet 250*X KB/s.

Specyficzną formą powyższego ataku jest zatapianie pakietami TCP z flagą SYN (ang. SYN flood). Takie pakiety inicjują połączenie TCP. Jeśli system otrzyma więcej niż 5 (to standard, ale Linux dopuszcza 128) pakietów SYN skierowanych do jednego portu, to nie jest w stanie zaakceptować nowych połączeń do tego portu do momentu, kiedy jedno z poprzednich żądań połączenia zostanie odrzucone lub zaakceptowane. Atakujący nie dopuszcza do zakończenia zestawienia połączenia TCP. Dopóki pakiety wysłane przez atakującego nie ulegną przedawnieniu (ang. timeout), a jest to czas rzędu minuty, blokują one nowe połączenia.

Niektóre aplikacje sieciowe zużywają dużo zasobów węzła. Otwarcie niezbyt dużej liczby połączeń przez atakującego może zaangażować wszystkie dostępne zasoby, uniemożliwiając pracę innym aplikacjom. Z kolei pewne ważne programy limitują liczbę klientów, którzy mogą się z nimi połączyć w jednostce czasu. Atakujący kosztem otwarcia kilkudziesięciu połączeń może odciąć użytkowników od wielu istotnych usług.

Ataki typu DOS mogą być bardzo uciążliwe. Nie dają one atakującemu kontroli nad serwerem, ale uniemożliwiają korzystanie z niego innym. W wielu przypadkach (na przykład dostawca Internetu, popularny serwer WWW) jest to nie do zaakceptowania. Pewne konstruktywne ataki (na przykład podszywanie się, ang. spoofing, patrz dodatek A) wymagają, aby pewien węzeł sieci był przez krótki czas nieaktywny. Można to osiągnąć właśnie atakiem typu DOS.

Wykrywanie źródła takich ataków jest często bardzo trudne. Protokół IP daje możliwość fałszowania adresu nadawcy, toteż na informacji z nagłówka IP nie można polegać. Jeśli atak DOS jest długotrwały, to można próbować prześledzić jego drogę po ruterach. Wymaga to oczywiście współpracy ich administratorów.

3.3.2 Ataki konstruktywne. Tego typu ataki mają na celu uzyskanie dostępu do systemu, często z przywilejami administratora. Wykorzystują one luki w aplikacjach sieciowych lub słabość protokołów. Najczęściej wykorzystywanym błędem jest przepełnienie bufora (patrz dodatek C). Jego pomyślne wykorzystanie daje atakującemu możliwość wykonania dowolnego kodu na serwerze. Można długo wyliczać aplikacje, które były podatne na ten atak. Wystarczy wspomnieć o demonie pocztowym sendmail (praktycznie wszystkie wersje wcześniejsze niż 8.8.0) czy też demonie obsługującym zapytania DNS, named (wersje 4.x). Te programy były używane na wielu odmianach Uniksa. Przepełnienie bufora w aplikacji pracującej pod kontrolą Windows NT jest równie fatalne – opublikowano już niemało konkretnych przykładów wykorzystania tego błędu [7].

Wiele programów (w szczególności często aplikacje CGI) to skrypty interpretatora poleceń (ang. shell), takiego jak bash czy tcsh, oraz skrypty w języku perl. Częstym błędem jest nieuwzględnianie możliwości pojawienia się w danych wejściowych programu znaków specjalnych interpretatora lub perla, takich jak znak “uruchom proces” (czyli ` ) albo znak końca linii (czyli \n). Atakujący może wewnątrz danych posyłanych do serwera zamieścić polecenia interpretatora poleceń lub perla, poprzedzając je jednym ze znaków specjalnych. Niektóre skrypty podczas parsowania danych wywołują interpretator poleceń, podając jako jeden z argumentów dane posłane przez atakującego. Może to doprowadzić do wykonania na serwerze poleceń posłanych przez atakującego.

Niekiedy programy sieciowe próbują ograniczyć zakres usługi, który oferują. Na przykład serwer WWW udostępnia tylko pliki leżące w pewnym wydzielonym podkatalogu. Niestety, pewne serwery nie uwzględniają możliwości pojawienia się w ścieżce żądanego pliku znaku katalogu nadrzędnego (czyli ..). Daje to atakującemu możliwość wyjścia ponad wydzielony katalog, a w konsekwencji dostęp do każdego pliku w systemie, do którego proces serwera WWW ma prawo odczytu.

Ataki konstruktywne wykorzystują błędy nie w systemie operacyjnym, lecz w aplikacjach. Większość programów sieciowych używa TCP, który jest protokołem połączeniowym, dzięki czemu trudno sfałszować źródło ataku (z pewnymi wyjątkami – patrz dodatek A). Dlatego identyfikacja atakującego jest zazwyczaj wiarygodna.

 

 

 

 

 

 

 

 

 

 

4. Metody działania NIDS

 

 

 

4.1. Logiczne komponenty IDS

 

Ciekawym projektem mającym na celu ustanowienie standardu opisu (i projektowania) IDS jest CIDF, Common Intrusion Detection Framework [5]. Jego autorzy definiują cztery elementy, które można wyodrębnić w każdym IDS. Są to generatory zdarzeń (E-komponenty), moduły analizujące (A-komponenty), moduły magazynujące informacje (D-komponenty) i moduły przeciwdziałające (C-komponenty). Komponent w sensie CIDF może być samodzielną aplikacją lub jej częścią.

Zadaniem E-komponentów jest dostarczanie danych o zdarzeniach w chronionym systemie lub sieci pozostałym komponentom IDS. Interesującymi zdarzeniami mogą być na przykład nadejście pakietu TCP czy też pomyślne zarejestrowanie się użytkownika do systemu. Zdarzenia te same w sobie nie muszą oznaczać próby ataku.

A-komponenty analizują dane od generatorów zdarzeń. Zapewne najciekawszą częścią badań nad IDS jest konstruowanie algorytmów, będących w stanie wyłowić z szumu zdarzeń tworzonego przez E-komponenty ślady działania intruza. Charakterystyka takich algorytmów jest zawarta w następnym punkcie tej pracy.

Komponenty A i E produkują bardzo duże ilości danych, które mogą być potrzebne przez dłuższy czas. Zadaniem D-komponentu jest ich magazynowanie i udostępnianie w wygodny sposób administratorowi. W szczególności D-komponenty muszą uwzględniać skończoną pojemność nośników danych.

W przypadku wykrycia ataku IDS zazwyczaj ogranicza się do poinformowania o nim administratora, używając systemowego dziennika zdarzeń czy też pewnych rozproszonych metod, na przykład pułapek SNMP (ang. SNMP traps, patrz dodatek C). W niektórych przypadkach możliwe jest przeciwdziałanie atakowi bez pomocy człowieka. Przykładowo, NIDS może zmodyfikować reguły filtrujące na ruterze tak, aby atakujący nie miał dostępu do chronionej sieci, co może zapobiec dalszym atakom. NIDS może też zerwać podejrzane połączenie TCP. Jeśli IDS jest oparty na węźle, to ma bogatsze możliwości, na przykład wymuszenie zakończenia działania interpretatora poleceń niewłaściwie zachowującego się użytkownika. Wykonaniem takich metod obronnych zajmują się C-komponenty.

 

4.2. Algorytmy IDS

 

4.2.1. Metoda sygnatur. Ta metoda sprawdza się najlepiej przy wykrywaniu znanych ataków. Twórca IDS przypisuje pewnemu atakowi towarzyszący mu ciąg zdarzeń (sygnaturę). Wystąpienie tego ciągu zdarzeń powinno być łatwo wykrywalne. Jeśli IDS odszuka w monitorowanych zdarzeniach sygnaturę, to przyjmuje, że nastąpił atak.

Najczęściej stosowanym typem sygnatur jest wystąpienie pewnego ciągu bajtów w strumieniu TCP. Przykładowo, jeśli atakowana aplikacja jest podatna na przepełnienie bufora (patrz dodatek C), to w 95 procentach przypadków strumień danych skierowany do tej aplikacji przez atakującego będzie zawierał napis (w notacji języka C) ”\xff\xff\xff/bin/sh” . Jest to fragment typowego kodu maszynowego wywołującego interpretator poleceń. Szukanie wystąpienia danego napisu w strumieniu danych jest łatwe – jest to zadanie dopasowywania wzorca (ang. pattern matching). Niestety, sygnatury oparte na wzorcu wykrywają często tylko konkretną implementację ataku. Jest to szczególnie widoczne przy atakach wykorzystujących przepełnienie bufora – atakujący może wykorzystać ten błąd przy użyciu różnych danych.

Pewne sygnatury oparte na wzorcu są wspólne dla wielu ataków. Atakujący zazwyczaj dążą do otrzymania interaktywnej sesji interpretatora poleceń posiadającego uprawnienia nadzorcy. Dlatego za sygnaturę ataku przeciwko systemowi Unix można uznać pojawienie się w strumieniu TCP napisu zachęty (ang. prompt) ”bash#” . Ta sygnatura jest ciekawa, gdyż może wykryć ataki, o których nie wiadomo podczas tworzenia IDS. Ponadto, typowy sieciowy IDS, którego źródłem danych jest ruch w sieci, może wykryć atak typowo lokalny, przeprowadzony przez użytkownika zarejestrowanego w systemie za pomocą sesji programu telnet. Wady tej sygnatury są jednak doskonale widoczne. Atakujący może osiągnąć wszystkie swoje cele bez pomocy interaktywnej sesji interpretatora poleceń. Wzorzec ”bash#” będzie widoczny tylko w niekodowanym strumieniu danych – atakujący, który użyje ssh w celu zarejestrowania się w systemie, uniknie wykrycia. Jest też duża szansa wystąpienia fałszywego alarmu (ang. false positive).

Mimo swoich ograniczeń, sygnatury oparte na wzorcu są podstawową metodą używaną przez dzisiejsze komercyjne IDS. Ta metoda efektywnie wykrywa ataki przeprowadzane przez intruzów nie spodziewających się obecności IDS lub nie mających wystarczającej wiedzy, aby samodzielnie zmodyfikować konkretną implementację ataku.

Znacznie mocniejszą sygnaturą jest ogólny opis metody wykorzystania błędu obecnego w aplikacji. W przypadku przepełnienia bufora będzie to “jedna z danych wejściowych aplikacji ma rozmiar większy niż MAX”, gdzie MAX jest rozmiarem bufora alokowanego przez aplikację. Wszystkie ataki wykorzystujące przepełnienie tego bufora mogą być wykryte przy pomocy tej sygnatury. Dlatego nazwijmy ją sygnaturą uniwersalną. Jednakże IDS musi umieć interpretować dane napływające do aplikacji – czyli parsować je według protokołu czy też gramatyki używanej przez aplikację. Czyni to IDS znacznie bardziej złożonym i trudniejszym do rozbudowy – dodanie sygnatury nowego ataku przeciwko pewnej aplikacji wymaga dodania do IDS umiejętności parsowania jej danych.

4.2.2. Wykrywanie anomalii. Jak już wspomniano, metoda sygnatur zazwyczaj nie sprawdza się w przypadku nieznanych ataków, gdyż dla nich nie istnieją sygnatury. Aby się przed nimi bronić, można zbudować pewien profil działalności obserwowanych obiektów (użytkownika, sieci lub jej węzła) a następnie wykrywać odchylenia od tego profilu. Im silniejsze odchylenie, tym większe prawdopodobieństwo, że w obserwowanym systemie dzieje się coś niepokojącego.

Ta metoda najlepiej działa na poziomie użytkownika. Powiedzmy, że w systemie identyfikator X należy do dyrektora. Używa on programu pocztowego, przeglądarek WWW i innych podobnych aplikacji. Jeśli użytkownik po zarejestrowaniu się w systemie jako X zaczyna kompilować programy albo przeglądać pliki systemowe, to jest duża szansa, że ktoś niepowołany korzysta z konta X.

Dość łatwo zauważalną anomalią jest skanowanie portów (patrz dodatek C). Duża liczba połączeń inicjowanych przez jeden węzeł sieci wyraźnie odróżnia się od profilu. Zauważmy jednak, że atakujący może wykonać skanowanie powoli, w tempie powiedzmy jednego portu na godzinę. Wtedy odchylenie od profilu może być zbyt małe. Ten przykład ilustruje ogólną słabość metod opartych na statystyce – atakujący może prowadzić nietypowe działania powoli lub w przemieszaniu z niegroźną aktywnością, co ujdzie uwagi IDS.

Wykrywanie anomalii jest trudne do zaimplementowania. Dobór kryteriów używanych przy konstruowaniu profilu, progowe wartości odchyleń powodujących wszczęcie alarmu i inne parametry tej metody nie są oczywiste ani jednoznaczne. Dla potrzeb tej metody używano algorytmów ewolucyjnych i genetycznych oraz pojęć zapożyczonych z badań nad sztuczną inteligencją, jak sieci neuronowe i logika rozmyta. Niestety, rezultaty nie są do tej pory na tyle obiecujące, aby uznać tę metodę za wiodącą przy projektowaniu IDS. Jednakże obecnie badania nad wykrywaniem anomalii są prowadzone intensywnie, są więc szanse na szersze uwzględnienie tej metody przez przyszłych twórców IDS.

 

4.3. NIDS a inne systemy bezpieczeństwa

 

Wymienione w tytule podrozdziału systemy to głównie zapory przeciwogniowe i skanery luk (ang. vulnerability scanner). Pierwsze z nich rozdzielają sieci o różnym poziomie bezpieczeństwa. Drugie z nich aktywnie badają węzły sieci, starając się znaleźć w nich luki i poinformować o nich administratora, zanim węzłem zainteresuje się intruz.

NIDS uzupełniają oba wymienione typy systemów. Zapory nie chronią przed atakami, których źródło znajduje się w sieci lokalnej. Poza tym zapory nie zatrzymują pewnych form ataków (na przykład skanowania portów, patrz dodatek C). Ponadto, niekiedy nie można pozwolić sobie na zablokowanie na zaporze pewnych ważnych, lecz potencjalnie niebezpiecznych usług.

Z kolei skanery luk badają stan zabezpieczenia węzłów sieci w ustalonej chwili. Dzięki NIDS można się dowiedzieć w czasie rzeczywistym, czy ktoś próbuje dostać się do chronionych systemów. Daje to możliwość reakcji na konkretny atak, w szczególności zaostrzenia polityki bezpieczeństwa oraz podstawę do pociągnięcia intruzów do odpowiedzialności.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

5. Problemy ze zdobywaniem danych

 

 

5.1. Znaczenie poprawnego emulowania stosu TCP/IP

 

Dla typowego NIDS jedynym źródłem danych są przepływające przez sieć pakiety. Aby NIDS był użyteczny, musi on przewidywać reakcję węzła na kierowany do niego ruch. W szczególności, konieczna jest możliwość odtworzenia danych, jakie system operacyjny przekaże aplikacji, kiedy ta wykona operację czytania z gniazda sieciowego. Oznacza to potrzebę emulacji stosu TCP/IP chronionych maszyn.

Po obejrzeniu pakietu NIDS musi najpierw zdecydować, czy pakiet IP zostanie zaakceptowany przez węzeł, do którego jest skierowany. Jak przekonamy się dalej, nie jest to trywialny problem. Powinno być jasne, że jeśli węzeł przyjmie pakiet, a NIDS go odrzuci, to pojawią się problemy. W odrzuconym pakiecie mogły znajdować się dane, które wykorzystywały luki w bezpieczeństwie pewnej aplikacji. Intruz mógł pomyślnie dokonać ataku, a NIDS tego nie zauważył. Taka akcja intruza jest określona w pracy [1] jako atak typu uniknięcie (ang. evasion).

Mniej oczywiste jest to, że równie fatalna jest sytuacja odwrotna, gdy NIDS akceptuje pakiet i analizuje jego zawartość, a węzeł go odrzuca. Konsekwencje są najlepiej widoczne na przykładzie protokołu TCP. Ponieważ pakiety TCP (ogólnie, IP) mogą przybywać do celu w kolejności różnej od tej, w jakiej były wysłane, kolejność danych zawartych w pakietach TCP jest ustalana przez liczby naturalne zwane numerami sekwencji (ang. sequence numbers). Bajtom danych w strumieniu TCP są przypisane kolejne numery sekwencji. W nagłówku każdego pakietu TCP jest zawarta informacja (numer sekwencji pakietu), jaki jest numer sekwencji pierwszego bajtu danych zawartych w tym pakiecie. Zazwyczaj pakiety TCP przybywające do węzła mają rosnące numery sekwencji. Jeśli węzeł otrzyma pakiet TCP o numerze sekwencji X, a następnie otrzyma pakiet TCP o tym samym numerze sekwencji X, to dane z drugiego pakietu nie zostaną przekazane do aplikacji (zostanie on potraktowany jako retransmisja). Intruz może stworzyć następujący scenariusz. Najpierw wysłany jest pakiet TCP o numerze sekwencji X, który nie zostanie przetworzony przez docelowy węzeł (na przykład nie będzie zgadzać się suma kontrolna nagłówka IP), ale NIDS go zaakceptuje. Następnie atakujący wysyła pakiet TCP o numerze sekwencji X, zaakceptowany zarówno przez węzeł, jak i przez NIDS. Jak widać, aplikacja na węźle otrzyma dane z drugiego pakietu (potencjalnie groźne), a NIDS będzie sądzić, że z pierwszego (w którym atakujący zawrze nieszkodliwe dane). Taka akcja intruza jest określona w pracy [1] jako atak typu wstawienie (ang. insertion).

Konsekwencja powyższego wywodu jest bardzo znacząca. NIDS musi interpretować pakiety w dokładnie ten sam sposób, co chroniony węzeł. Nazwijmy to twierdzenie “podstawowym”. Intruz może wykorzystać wszelkie rozbieżności, dokonując ataku typu uniknięcie lub wstawienie, aby ukryć przed NIDS swoją działalność. To stwierdzenie jest ważne na każdym poziomie protokołu sieciowego.

Powyższe twierdzenie wymaga pewnego uzupełnienia. Aby NIDS mógł działać poprawnie, musi on otrzymywać od fizycznego nośnika datagramów sieciowych takie same pakiety co chronione węzły, w tej samej kolejności i względnych odstępach czasowych. Abstrahując od poprawności algorytmów używanych przez NIDS, jeśli dostaje on pakiety w inny sposób niż chronione węzły (w sensie powyższych warunków), to może on działać niepoprawnie. Co więcej, jeśli atakujący może wytworzyć w powtarzalny sposób sytuację, w której jeden warunków wymienionych powyżej nie jest spełniony, to NIDS może być powtarzalnie ominięty. Warunki będą spełnione, jeśli NIDS będzie działać na jednym z węzłów sieci lokalnej, będzie monitorował inne węzły tej sieci, a medium sieci jest ethernet. Dlatego jest to najczęstsza topologia, w jakiej położony jest NIDS.

 

5.2. Złożoność systemów komputerowych a złożoność NIDS

 

Z podanego w poprzednim podrozdziale twierdzenia wynika, że budowanie NIDS należy zacząć od skonstruowania niezawodnego stosu TCP/IP. Nie jest to łatwe. W każdym systemie operacyjnym kod realizujący obsługę sieci jest obszerny. W źródłach jądra Linuksa 2.0.36 katalog net/ipv4 zajmuje 789 KB. Jest to efektem złożoności obecnie używanych protokołów sieciowych.

Skupmy się najpierw na poziomie protokołu IP. Inne protokoły sieciowe są oparte na IP, więc błąd popełniony podczas interpretacji pakietu IP może spowodować niewłaściwą interpretację danych przesyłanych w nadrzędnym protokole. W pracy [1] autorzy wyliczają możliwe powody odrzucenia pakietu IP przez węzeł pracujący pod kontrolą systemu operacyjnego FreeBSD 2.2. Są one zamieszczone w poniższej tabeli. W pierwszej kolumnie podano numer linii pliku netinet/ip_input.c, w której funkcja ip_input() wychodzi z błędem.

 

 

Linia

Opis powodu odrzucenia pakietu

229

Nie określono adresu IP

232

Otrzymany pakiet jest za krótki, by być legalnym datagramem IP

240

Otrzymany pakiet jest za krótki, by być legalnym datagramem IP

247

Wersja protokołu IP jest różna od 4

253

Zbyt mała wartość pola “długość nagłówka”

257

Wartość pola “długość nagłówka” jest większa od rozmiaru pakietu

269

Nieprawidłowa suma kontrolna nagłówka

278

Wartość pola “długość pakietu” jest mniejsza niż pola “długość nagłówka”

348

Pakiet ma opcje IP, funkcja ip_dooptions() zwraca błąd

437

Pakiet nie jest skierowany do węzła, który go otrzymał

450

Pakiet jest za krótki, aby być fragmentem datagramu IP

 

Ponadto funkcja ip_dooptions() może zwrócić błąd w 12 przypadkach (co spowoduje odrzucenie pakietu). Tak więc widzimy, że problem określenia, czy węzeł zaakceptuje pakiet IP, jest złożony.

Integralną cechą protokołu IP jest fragmentacja (patrz dodatek C). NIDS musi umieć składać fragmentowane pakiety IP. Jeśli nie jest w stanie tego dokonać, atakujący wykona prosty atak typu uniknięcie używając fragmentacji. Warto zauważyć, że realizacja defragmentacji IP była piętą Achillesową wielu systemów operacyjnych. Twórca NIDS powinien starać się uniknąć powtórzenia błędów zawartych w innych implementacjach. Szczególną uwagę należy poświęcić obsłudze nakładających się fragmentów IP.

Rozważmy teraz poziom protokołu TCP, którego używa ogromna większość aplikacji sieciowych. Również dla systemu FreeBSD 2.2 autorzy pracy [1] podają powody, dla których pakiet TCP może nie zostać zaakceptowany. Jest ich 31. Jednakże w większości przypadków generowany jest pakiet RST (co NIDS łatwo zauważy) lub też mogą się one zdarzyć już po zakończeniu przesyłania danych (po wysłaniu pakietu z flagą FIN), co nie daje intruzowi możliwości przeprowadzenia ataku typu wstawienie. Pozostają jednak cztery przypadki związane z niepoprawnymi wartościami pól nagłówka TCP, które NIDS musi koniecznie uwzględniać.

Protokół TCP jest znacznie bogatszy od IP. Definiuje on wiele stanów, w jakich może znaleźć się gniazdo. Interpretacja pakietu może zależeć od stanu gniazda, do którego pakiet skierowano. NIDS musi więc utrzymywać informację o stanie gniazd, tworzących śledzone połączenie. Sterowanie przepływem, stanowiące integralną część protokołu TCP, musi być uwzględnione przy projektowaniu każdego E-komponentu. Niewłaściwe zaimplementowanie dowolnej właściwości protokołu TCP daje intruzowi możliwość ataku. Dlatego podsystem NIDS rozpoznający TCP musi być z konieczności złożony, a przez to podatny na błędy. Stąd jego projekt wymaga szczególnej uwagi.

 

5.3. Rozbieżności między różnymi systemami w implementacji stosu TCP/IP

 

W poprzednim podrozdziale przedstawiono część zagadnień związanych z emulowaniem stosu TCP/IP przez NIDS. Jest ich pokaźna, ale skończona liczba. Może to wywrzeć wrażenie, że skonstruowanie niezawodnego E-komponentu NIDS to tylko kwestia uważnego przestudiowania odpowiednich dokumentów RFC definiujących protokoły i ich dokładnego zaimplementowania. Niestety, okazuje się, że różne systemy operacyjne w różny sposób implementują protokoły sieciowe, co prowadzi do odmiennej interpretacji takiego samego strumienia TCP przez różne systemy. Można podać przynajmniej dwie przyczyny takiego stanu rzeczy.

5.3.1. Odstępstwa od standardu. Jednym z problemów jest fakt, że istniejące implementacje stosu TCP/IP pozwalają sobie na drobne odstępstwa od obowiązujących standardów. Takie praktyki mają na celu skrócenie i uproszczenie kodu sieciowego, a także poprawienie jego wydajności. Niezgodności z definicją protokołów widoczne są tylko w szczególnych sytuacjach, na ogół nie spotykanych. Nic jednak nie przeszkodzi atakującemu w stworzeniu takich sytuacji.

Typowym przykładem jest problem związany z danymi pilnymi (ang. urgent data). Protokół TCP daje możliwość posłania w ramach połączenia TCP jednego bajtu, który nie należy do zwykłego strumienia danych. Zazwyczaj jego wystąpienie ma sygnalizować pojawienie się nietypowych warunków, na które należy pilnie zareagować (stąd nazwa). Lokalizację bajtu pilnych danych określa pole w nagłówku pakietu TCP, zwane wskaźnikiem danych pilnych (ang. urgent pointer). Jeśli oznaczymy przez PTR sumę wskaźnika pilnych danych i numeru sekwencji pakietu TCP, to według specyfikacji protokołu TCP numer sekwencji bajtu danych pilnych jest równy PTR. Niestety, większość systemów przyjmuje PTR minus jeden jako położenie danych pilnych. W przypadku, kiedy NIDS bazuje na sygnaturach opartych na wzorcu, niewłaściwa interpretacja nawet jednego bajtu może spowodować, że wzorzec nie zostanie znaleziony w strumieniu TCP.

W dodatku B zamieszczono opis błędu w jądrze Linuksa 2.0.36, związanego z interpretacją pilnych danych. Jak zwykle, może on być wykorzystany przez intruza.

Kolejną kwestią jest obecność w pakiecie TCP flagi ACK. Jeśli jest ona ustawiona, to pole w nagłówku pakietu TCP zwane potwierdzanym numerem sekwencji (ang. acknowledgment number) określa, ile danych w tym połączeniu TCP otrzymał system wysyłający pakiet. Specyfikacja protokołu TCP stwierdza, że w normalnym połączeniu TCP tylko pierwszy pakiet nie ma ustawionej flagi ACK. Jest to naturalne – wszystkie następne pakiety powinny potwierdzać nadejście otrzymanych uprzednio danych. Większość systemów nie przekaże do aplikacji danych otrzymanych w pakiecie TCP bez ustawionej flagi ACK. Jednak niektóre (między innymi Linux) czynią przeciwnie.

Ciekawy przykład świadomego naruszenia protokołu TCP, którego implikacje dla NIDS nie były dotąd zauważone, można znaleźć w kodzie jądra Linuksa. Na wielu systemach pracujących pod tym systemem używana jest wersja instalacyjna jądra. Zawiera ona podsystem filtrowania pakietów, udostępniający funkcjonalność zapory. Domyślnie reguły filtrujące zapory są puste. Należałoby zatem się spodziewać, że obecność podsystemu filtrowania nie wpływa na to, jakie pakiety zostaną zaakceptowane przez jądro. W rzeczywistości jest inaczej. W kodzie zapory znajduje się pewna reguła, która nie jest definiowana przez administratora, ale wpisana na stałe – nie można w żaden sposób jej usunąć. Jej efekt jest taki, że fragmentowany pakiet IP o długości 28, niosący część pakietu TCP, jest zawsze odrzucany. Komentarz w kodzie zapory stwierdza, że jedynym powodem pojawienia się tak krótkiego pakietu jest akcja intruza, tak więc dla poprawy bezpieczeństwa pakiet należy odrzucić. W istocie, taki pakiet zawiera tylko część nagłówka TCP, dlatego zapora może mieć za mało informacji, aby zadecydować o odrzuceniu takiego pakietu. Posyłanie tak krótkich pakietów jest znaną techniką intruzów, stosowaną w nadziei przemycenia pakietów przez niektóre zapory.

Niestety, w celu ominięcia NIDS atakujący może wykorzystać każde odstępstwo implementacji od standardu, nawet jeśli zostało uczynione w intencji poprawienia bezpieczeństwa. Jeśli NIDS nie uwzględnia opisanej wyżej własności jądra Linuksa, to intruz może poprzedzić atak wysłaniem fragmentu pakietu TCP o długości 28. NIDS zaakceptuje taki pakiet, a węzeł go odrzuci. Jest to scenariusz ataku typu wstawienie.

5.3.2. Niezdefiniowanie i opcjonalność niektórych fragmentów protokołów. Pewne istotne parametry protokołów sieciowych nie są podane w ich specyfikacji. Z kolei niektóre cechy protokołów zdefiniowano, ale nie wymaga się ich implementacji.

Typowym przykładem jest problem obecności danych w pakietach TCP niosących flagę SYN. Ta flaga jest obecna w dwóch pierwszych pakietach połączenia TCP. Specyfikacja protokołu TCP [11] dopuszcza załączenie danych w pakietach z flagą SYN. Jednakże korzystając z API gniazd, nie ma możliwości zamieszczenia danych w pierwszych trzech pakietach, nawiązujących połączenie TCP. Dlatego w praktyce nie obserwuje się takiej sytuacji. Niektóre systemy operacyjne (Linux, Solaris) nie uwzględniają danych zawartych w pakietach z flagą SYN, co daje atakującemu możliwość przeprowadzenia ataku typu wstawienie. Większość systemów (między innymi wszystkie odmiany BSD) przekazuje do aplikacji dane otrzymane w pakiecie TCP niosącym flagę SYN.

Największym źródłem rozbieżności między różnymi systemami jest niezdefiniowanie ilości zasobów, które system powinien poświęcić na obsługę sieci. Bliżej temu zagadnieniu przyjrzymy się w rozdziale 8.

5.3.3. Wnioski. Konsekwencje faktów przedstawionych w tym podrozdziale są znaczące. Aby stworzyć NIDS (lub jego część), który implementuje stos TCP/IP w sposób identyczny, jak system operacyjny A, nie wystarczy zrealizować wszystkie zalecenia specyfikacji protokołów sieciowych. Niezbędne jest też uwzględnienie odstępstw systemu A (zwykle nieudokumentowanych) od standardu. Zwykle oznacza to przeprowadzenie szerokiego zestawu testów. Ponadto, jeśli sieć monitorowana przez NIDS jest heterogeniczna, to NIDS musi wiedzieć, jaki system operacyjny działa na każdym węźle sieci oraz umieć symulować każdy system obecny w sieci. Oznacza to wielokrotny wzrost ilości kodu NIDS w porównaniu z NIDS realizującym tylko zalecenia specyfikacji protokołów.

 

5.4. Problem wykorzystania zasobów przez NIDS

 

Zasoby każdego komputera są ograniczone. Kiedy program nie jest w stanie uzyskać od systemu operacyjnego dostatecznej ilości zasobów, zazwyczaj jest zmuszony awaryjnie zakończyć pracę lub jego poprawne działanie jest niemożliwe.

W typowym przypadku NIDS jest uruchomiony na jednym węźle sieci i ma za zadanie monitorować więcej niż jeden węzeł. Oznacza to, że potencjalnie może on potrzebować tyle zasobów, ile zużywają wszystkie monitorowane węzły w celu obsługi sieci. Ponadto, zazwyczaj karta sieciowa węzła, na którym działa NIDS, działa w trybie rozwiązłym. Konsekwencją tego faktu jest to, że NIDS będzie musiał przetworzyć również te pakiety, których żaden inny węzeł sieci nie odbierze. Dlatego NIDS musi ściśle kontrolować sposób, w jaki alokuje zasoby, aby nie przekroczyć możliwości węzła, na którym działa. Można wyróżnić cztery rodzaje zasobów, których wymaga NIDS.

5.4.1. Czas procesora. Jeżeli NIDS zostanie zmuszony do wykonywania wielu operacji obciążających procesor, to może nie być w stanie odczytywać pakietów z sieci dostatecznie szybko. Bufory, jakie rezerwuje system operacyjny na przechowywanie odebranych pakietów sieciowych, mają z konieczności ograniczoną pojemność. Jeśli nowe pakiety napływają z sieci szybciej, niż aplikacja (czyli NIDS) czyta je z systemowych buforów, to przynajmniej część danych zostanie utracona. Aby stworzyć taką sytuację, atakujący może próbować zmusić NIDS do czasochłonnego przetwarzania danych. W tym celu atakujący powinien określić, która część kodu NIDS zużywa najwięcej mocy obliczeniowej procesora. Zazwyczaj algorytmy, których używa NIDS do przechowywania i przetwarzania danych o ruchu w sieci, optymalizowano pod kątem typowego ruchu w sieci. Atakujący może stworzyć sytuacje niestatystyczne, powodując, że algorytm będzie działał w czasie pesymistycznym, a nie oczekiwanym.

Jako przykład może posłużyć algorytm defragmentacji pakietów IP. Musi on przechowywać wszystkie fragmenty pakietu IP aż do chwili, kiedy będzie możliwa defragmentacja (czyli do momentu przybycia ostatniego potrzebnego fragmentu). Aby ułatwić defragmentację, większość systemów magazynuje pakiety w pewnej strukturze danych w kolejności, w jakiej wystąpią one w składanym pakiecie. Ponieważ wszystkie systemy operacyjne wysyłają fragmenty właśnie w tej kolejności, narzuca się przechowywanie fragmentów na liście, w kolejności odwrotnej do tej, w jakiej zostały odebrane. Wstawianie kolejnego pakietu na listę będzie najczęściej polegało na włożeniu go na początek listy, co zajmuje czas stały.

Niestety, atakujący może posyłać fragmenty w kolejności od ostatniego do pierwszego. W takim przypadku wstawienie kolejnego fragmentu na listę będzie wymagało przejścia jej całej. Zajmie to czas proporcjonalny do liczby pakietów na liście. Jeśli atakujący pośle wiele tysięcy krótkich fragmentów, to wstawianie ich na listę zabierze znaczącą część czasu procesora. Zauważmy, że jeśli algorytm nie bierze pod uwagę możliwości nakładania się i duplikacji fragmentów IP, to atakujący może posłać dowolnie wiele fragmentów tego samego pakietu. Oznacza to powstanie dowolnie długiej listy i dowolnie długi czas wstawiania fragmentu.

Zupełnie analogiczny problem jest związany z kolejkowaniem segmentów TCP, które atakujący również może wysyłać w kolejności odwrotnej do tej, w jakiej zostaną złożone. Rozdział 8 przedstawia algorytm, jaki zastosowano w jądrze Linuksa 2.0.36 oraz problemy z nim związane.

Jak już wspomniano, do niezawodnego wykrywania ataków na aplikacje niezbędne jest parsowanie protokołu danej usługi. Wymaga to zazwyczaj pewnej ilości operacji na tekstach, które mogą być kosztowne. Również daje to intruzowi możliwość ataku.

5.4.2. Pamięć. Większość algorytmów, w szczególności również te używane przez NIDS, alokuje pewną ilość pamięci. Jeśli atakujący spowoduje, że NIDS zużyje całą pamięć fizyczną, to NIDS zakończy działanie, albo zacznie działać bardzo powoli wskutek intensywnego wykorzystania przez system operacyjny pliku wymiany (co da efekt podobny do zbyt dużego obciążenia procesora). Wrażliwe na ten atak są algorytmy, które na skutek niektórych zdarzeń (na przykład nadejścia pewnego typu pakietów) muszą zaalokować struktury danych, które nie będą zwolnione przez dłuższy czas. Jeśli atakujący zacznie zatapiać chronioną sieć pakietami o opisanym wyżej wpływie na NIDS, to ten ostatni może szybko zużyć całą dostępną pamięć.

Zatapianie pakietami TCP niosącymi flagę SYN to typowy atak mający na celu zmuszenie NIDS do alokacji dużej ilości pamięci. Zazwyczaj nadejście takiego pakietu oznacza początek nowego strumienia TCP. Jeśli NIDS jest zainteresowany śledzeniem tego strumienia (ponieważ na przykład jest on skierowany do portu, na którym słucha wrażliwa na atak aplikacja), to musi zaalokować strukturę danych (zwaną TCB, od ang. TCP Control Block) zawierającą informacje o tym strumieniu. Na pewno będą w niej obecne dane takie jak adresy klienta i serwera, numery portów, bieżący numer sekwencji, ponieważ są one niezbędne do poprawnego składania strumienia TCP. TCB musi istnieć przynajmniej przez czas, przez jaki węzeł sieci czeka na potwierdzenie żądania nawiązania połączenia. Jest to czas rzędu minuty. Jeśli atakujący przez minutę może posłać dostatecznie wiele pakietów z flagą SYN, a NIDS nie limituje liczby śledzonych połączeń, to cała dostępna pamięć zostanie zużyta.

Narzucającą się metodą obrony przed takimi atakami jest ograniczanie liczby tworzonych struktur danych, takich jak TCB. Należy jednak czynić to rozważnie. Jeżeli po prostu nie będziemy tworzyć nowych TCB po przekroczeniu pewnego progu ich ilości, to da to intruzowi sposobność wykonania ataku typu uniknięcie. Atakujący może najpierw posłać pewną liczbę sfałszowanych pakietów, które wyczerpią limity pamięciowe nałożone na NIDS, a potem otworzyć kolejne połączenie, które nie będzie już śledzone przez NIDS.

Najlepsza metoda radzenia sobie z powyższym typem ataków polega na ustawieniu limitów alokowania struktur danych na dokładnie tym samym poziomie, na jakim są one ustalone w monitorowanych węzłach. Wtedy jeśli nawet NIDS nie będzie miał wystarczającej ilości pamięci, aby śledzić pewne połączenie, to nie będzie to problemem, gdyż chroniony węzeł nie zaakceptuje takiego połączenia. Niestety, takie rozwiązanie narzuca dość duże (ale skończone) wymagania na ilość pamięci dostępną dla NIDS. Wrócimy do tej kwestii w rozdziale 7.

5.4.3. Przepustowość interfejsu sieciowego. Nadejście każdego pakietu powoduje wykonanie prze interfejs sieciowy szeregu czynności. Musi on skopiować dane z sieci do wewnętrznego bufora, wywołać przerwanie i pozwolić systemowi operacyjnemu skopiować pakiet do wewnętrznych struktur. Interfejs jest w stanie obsłużyć tylko skończoną ilość pakietów w jednostce czasu. W przypadku starszych urządzeń, atakujący może wygenerować ruch na tyle duży, że interfejs nie zdoła go przyjąć w całości, a jednocześnie sieć nie będzie całkowicie zablokowana.

Jeśli węzeł posiada szybki interfejs sieciowy i wolny procesor, to w wyniku dużego ruchu procesor może nie nadążać z kopiowaniem pakietów z karty sieciowej do przestrzeni jądra. Również spowoduje to, że NIDS nie zobaczy istotnych pakietów przepływających przez chronioną sieć.

Konieczność wygenerowania znacznego ruchu oznacza, że intruz może wykonać opisywany atak prawie wyłącznie wtedy, kiedy źródłem ataku jest węzeł w sieci lokalnej.

5.4.4. Pojemność nośników danych. Po wykryciu ataku NIDS powinien poinformować o tym zdarzeniu administratora. Najczęściej jest to realizowane przez zapis odpowiedniej informacji do systemowego dziennika zdarzeń lub plików przeznaczonych wyłącznie na komunikaty od NIDS. Często wysłanie tylko jednego pakietu (na przykład pakietu TCP niosącego nieważne flagi) może spowodować wygenerowanie wiadomości przez NIDS. Atakujący może wysłać wiele pakietów, których jedynym celem jest zmuszenie NIDS do wytworzenia dużej liczby komunikatów. Kiedy pliki tworzone przez NIDS zapełnią całe dostępne miejsce na dysku, NIDS staje się bezradny – nawet jeśli wykryje atak, to nie będzie miał możliwości zapisu informacji o jego wystąpieniu.

Zazwyczaj tylko protokół TCP daje wiarygodną informację o adresie nadawcy pakietów. Oznacza to, że atakujący może wygenerować dużą liczbę pakietów IP ze sfałszowanym adresem nadawcy, licząc na to, że komunikaty tworzone przez NIDS nie zdradzą źródła ataku. Mogą one natomiast zużyć całe wolne miejsce na dysku, co efektywnie zablokuje NIDS.

Nawet jeśli objętość miejsca na dysku dostępnego dla NIDS liczy się w gigabajtach (co spowoduje, że atakujący musiałby poświęcić czas rzędu dni na zapełnienie dysku), to wygenerowanie przez NIDS nawet o rząd wielkości mniejszej ilości komunikatów może być utrapieniem dla administratora. Analiza plików z danymi o rozmiarze megabajtów stanowi nietrywialne zadanie.

Duża liczba komunikatów tworzonych przez NIDS jest problemem nie tylko w przypadku, gdy przechowuje się je na dysku. Jeśli NIDS wysyła ostrzeżenia dla administratora pocztą elektroniczną albo przy użyciu wiadomości SMS, to konsekwencje mogą być jeszcze gorsze.

 

5.5. Problem wykorzystania zasobów przez monitorowane węzły

 

W przełomowej pracy [1] autorzy zauważają, że węzeł może nie zaakceptować pakietu, jeśli przekroczy limity zasobów przeznaczonych na obsługę sieci. Jeśli NIDS zaakceptuje ten pakiet, to może paść ofiarą ataku typu wstawienie. Jednakże nie uwzględniono faktu, że powyższa sytuacja może być efektem nie tylko losowych zdarzeń, ale również akcji intruza. Poniżej rozpatrzymy dwa scenariusze, jakie intruz może próbować realizować.

5.5.1. Kolejkowanie segmentów TCP. Specyfikacja protokołu TCP [11] stwierdza, że segmenty TCP mogą przebywać do miejsca przeznaczenia w kolejności różnej od tej, w jakiej zostały wysłane. Węzeł powinien magazynować pewną ilość pakietów TCP o numerach sekwencji większych od oczekiwanej. Jeśli węzeł oczekiwał na nadejście pakietu TCP o numerze sekwencji X, a otrzymał pakiet o numerze sekwencji Y>X, to powinien zachować ten pakiet w nadziei, że otrzyma za chwilę opóźniony pakiet o numerze sekwencji X niosący Y-X danych, co pozwoli potwierdzić nadejście obu pakietów.

Protokół TCP zawiera pojęcie okna (ang. window). Jest to liczba naturalna. Jeśli węzeł oczekuje na nadejście pakietu o numerze sekwencji X, to według zaleceń specyfikacji powinien kolejkować pakiety o numerach sekwencji z przedziału <X+1,X+okno>. Niektóre systemy, na przekład FreeBSD 2.x spełniają to zalecenie. Jednakże w przypadku pewnych systemów (między innymi Linuksa 2.0.x) ścisłe przestrzeganie specyfikacji byłoby zgubne. Okno ma typowo wartość około 32768. Oznacza to, że w skrajnym przypadku konieczne byłoby zmagazynowanie ponad 32 tysięcy jednobajtowych pakietów na jedno połączenie TCP. Dałoby to możliwość wykonania ataków typu DOS, mających na celu wyczerpanie całej pamięci atakowanej maszyny. Dlatego często systemy operacyjne kolejkują pakiety, które mieszczą się w oknie połączenia, z dodatkowym zastrzeżeniem, mówiącym, że magazynowanie pakietów nie może obciążyć systemu na więcej, niż pewną progową ilość pamięci (na przykład 64 KB). Zauważmy, że w typowym przypadku pakiety TCP przychodzą w dobrej kolejności, dzięki czemu nie trzeba ich magazynować.

Niestety, oznacza to, że NIDS musi uwzględniać kolejny parametr – ilość pamięci przeznaczaną przez chroniony system na jedno połączenie. Co gorsza, algorytm gospodarki zasobami przy kolejkowaniu segmentów TCP istotnie różni się nie tylko między systemami operacyjnymi, ale nawet między wersjami tego samego systemu. W konsekwencji NIDS musi posiadać bardzo dokładną wiedzę o wszystkich systemach, jakie chroni. W rozdziale 8 opisano bliżej algorytm, którego używa jądro Linuksa 2.0.36.

Przyjrzyjmy się efektom niedokładnego przewidzenia gospodarki zasobami chronionego węzła. W pierwszym przypadku NIDS magazynuje mniej pakietów, niż monitorowany węzeł. Atak przebiega wtedy w dwóch krokach. Niech X to numer sekwencji oczekiwany przez węzeł, W – liczba jednobajtowych pakietów TCP, jakie jest w stanie zmagazynować węzeł, N – liczba pakietów, jaką przechowuje NIDS, N<W. W kroku numer 1 intruz posyła W pakietów o numerach sekwencji X+1,X+2,...,X+W. Węzeł magazynuje je wszystkie, a NIDS będzie musiał część z nich odrzucić. W kroku numer 2 intruz posyła pakiet o numerze sekwencji X. Spowoduje on, że węzeł przekaże do aplikacji bajty danych o numerach sekwencji od X do X+W. Ponieważ NIDS odrzucił część pakietów, nie będzie w stanie przekazać W+1 pakietów swoim modułom analizującym. Jak widać, zaszedł atak typu ominięcie – A-komponenty NIDS nie dostaną pewnych danych, które otrzymał chroniony węzeł.

Przyjmijmy teraz oznaczenia jak w powyższym przykładzie, z tą różnicą, że N>W – czyli NIDS magazynuje więcej pakietów, niż węzeł. Atak przebiega w tym przypadku bardzo podobnie, ale w trzech krokach. W pierwszym z nich intruz wysyła N pakietów o numerach sekwencji X+1,X+2,...,X+N. NIDS zmagazynuje je wszystkie, a węzeł część z nich odrzuci. Dla ustalenia uwagi przyjmijmy, że węzeł zachował pakiety o numerach sekwencji X+1,X+2,...,X+W, a odrzucił pozostałe. W kroku drugim intruz posyła pakiet o numerze sekwencji X. Spowoduje to, że węzeł przekaże aplikacji dane o numerach sekwencji od X do X+W, a A-komponenty NIDS dostaną dane o numerach sekwencji od X do X+N. W kroku trzecim intruz posyła pakiet zawierający dane o numerach sekwencji od X+W+1 do X+N, zawierający dane, które wykorzystują lukę w bezpieczeństwie aplikacji. NIDS ten pakiet zignoruje (gdyż zawiera on dane o numerach sekwencji, które już zostały przyjęte – ten pakiet wygląda dla NIDS jak retransmisja). Jak widać, węzeł oczekuje na dane o numerze sekwencji X+W+1, więc zaakceptuje ten pakiet. Łatwo rozpoznać w tym scenariuszu schemat ataku typu wstawienie.

W powyższym opisie przyjęliśmy, że po kroku pierwszym węzeł zachował pakiety o numerach sekwencji od X+1 do X+W. Łatwo się przekonać, że nie jest to konieczne założenie. Nazwijmy przez A zbiór numerów sekwencji, jakie po kroku pierwszym odrzucił węzeł, |A| >N-W-1. Wtedy w kroku trzecim zamiast posłania jednego pakietu intruz wygeneruje |A| pakietów według algorytmu

while A niepusty do begin

send_seq:=min{x:x należy do A}

A:=A\{send_seq}

if A niepusty then

exp_seq:=min{x:x należy do A}

else

exp_seq:=X+N+1

poślij jednobajtowy pakiet o numerze sekwencji send_seq

end

 

NIDS zignoruje wszystkie pakiety wysyłane podczas działania tego algorytmu. Węzeł przeciwnie, zaakceptuje je wszystkie. Wysłanie pakietu o numerze sekwencji send_seq spowoduje, że aplikacja na węźle dostanie dane o numerach sekwencji send_seq,send_seq+1,...,exp_seq-1. Po zakończeniu algorytmu aplikacja na węźle otrzyma N bajtów danych. Bajty o numerach sekwencji ze zbioru A będą inne, niż te, które otrzymał NIDS. Daje to atakującemu możliwość ominięcia NIDS.

5.5.2. Kolejkowanie fragmentów IP. Kolejnym algorytmem, który w istotny sposób zależy od ograniczeń nałożonych na ilość zużywanych przezeń zasobów, jest algorytm defragmentacji pakietów IP. Można łatwo zauważyć konieczność określenia limitu pamięci, przeznaczonej na przechowywanie fragmentów IP. Gdyby nie było takiego progu, atakujący mógłby spowodować zużycie przez węzeł całej dostępnej pamięci na potrzeby defragmentacji.

W Linuksie 2.0.36 algorytm zarządzania zasobami podczas defragmentacji wygląda następująco. Zadeklarowano strukturę struct ipq, której zadaniem jest przechowywanie listy fragmentów tego samego pakietu IP. Zmienna ip_queue przechowuje wskaźnik na listę obiektów typu struct ipq. Jeżeli jedna ze struktur struct ipq nie była modyfikowana przez 30 sekund (to znaczy nie wstawiono do niej nowego fragmentu), to jest ona usuwana z listy ip_queue. Jeżeli pamięć zużyta do celów defragmentacji przekroczy rozmiar IPFRAG_HIGH_THRESH (zdefiniowany jako 256 KB), to wywoływana jest funkcja ip_evictor.

function ip_evictor

begin

while (zużyta pamięć>IPFRAG_LOW_THRESH) do

weź z listy ip_queue pierwszą strukturę ipq, usuń ją i jej zawartość

end

 

Stałą IPFRAG_LOW_THRESH zdefiniowano jako 192 KB.

Jądro alokuje strukturę ipq po nadejściu pakietu, który nie należy do żadnej innej struktury ipq. Po utworzeniu, struktura ipq jest wstawiana na początek listy ip_queue. Oznacza to, że funkcja ip_evictor usuwa te struktury ipq, które były ostatnio utworzone, a nie te, które najdawniej zmodyfikowano.

Te techniczne detale są bardzo istotne dla twórcy NIDS. Projektując podsystem defragmentacji, musi on przyjąć dokładnie ten sam algorytm, z tymi samymi parametrami. Co więcej, musi on dbać o zachowanie rozmiaru pomocniczych struktur typu ipq (gdyż ich rozmiar jest wliczany do zużytej na defragmentację pamięci). Najdrobniejsze odstępstwo od Linuksowego algorytmu może stworzyć możliwość ataku typu ominięcie lub wstawienie.

Oczywiście, podany algorytm jest używany tylko w Linuksie 2.0.36. Inne systemy operacyjne implementują defragmentację inaczej, toteż jeśli NIDS ma chronić wiele różnych systemów, musi on uwzględniać różnice między nimi również w zakresie defragmentacji.

Przykładowo, w innej wersji Linuksa, mianowicie w dowolnej 2.2.x, ip_queue nie jest listą, lecz tablicą mieszającą. Zmienia to algorytm zwalniania pamięci. Co gorsza, parametry IPFRAG_HIGH_THRESH i IPFRAG_LOW_THRESH nie są już stałymi definiowanymi dyrektywą preprocesora #define, ale zmiennymi, które administrator może łatwo modyfikować podczas pracy systemu (przy pomocy systemu plików proc). W zasadzie czyni to algorytm defragmentacji nieprzewidywalnym. Projektant NIDS może mieć tylko nadzieję, że administrator chronionego systemu nie będzie zmieniał domyślnych wartości tych parametrów.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

6. Opis libnids

 

 

6.1. Dlaczego libnids jest potrzebna

 

W poprzednim rozdziale sformułowano szereg poważnych problemów, przed którymi staje projektant NIDS. Niestety, większość (jeśli nie wszystkie) istniejące implementacje NIDS mają wady wspomniane w poprzednim rozdziale. W pracy [1] autorzy przetestowali cztery komercyjne systemy NIDS. Wszystkie z nich można było łatwo oszukać. Przykładowo, żaden z nich nie przetwarzał poprawnie fragmentowanych pakietów IP.

Częścią niniejszej pracy jest projekt i implementacja biblioteki libnids. Kod w niej zawarty można traktować jako E-komponent NIDS. Ma ona na celu rozwiązanie większości problemów, opisanych w poprzednim rozdziale. Jeśli twórca NIDS zdecyduje się na korzystanie z libnids, to pozwoli mu to na skupienie się na tworzeniu innych komponentów NIDS. Poza tym, libnids definiuje interfejs programisty pomiędzy E-komponentem a resztą systemu. Pozwala to na wymienne stosowanie bibliotek, zgodnych ze interfejsem libnids.

Ważnym wnioskiem z poprzedniego rozdziału jest to, że NIDS musi uwzględniać różnice między systemami operacyjnymi w implementacji protokołów sieciowych. Realizacja E-komponentu, który poprawnie przetwarzałby pakiety skierowane do różnych systemów operacyjnych, wykracza poza ramy tej pracy. Libnids niezawodnie interpretuje pakiety skierowane do systemów opartych na Linuksie 2.0.36 (z pewnymi, opisanymi dalej ograniczeniami). Jednakże aby wykorzystać rozbieżności między libnids a innymi systemami operacyjnymi, atakujący musi włożyć dużo wysiłku. Tak więc zastosowanie libnids podnosi znacząco poprzeczkę dla intruza, który chce obejść NIDS. Natomiast jeśli chroniona sieć składa się wyłącznie z systemów opartych na Linuksie 2.0.36, to libnids daje bardzo dobry obraz tego, co dzieje się w sieci.

Libnids zaimplementowano jako bibliotekę dzieloną (ang. shared library) dla systemu Linux. Libnids korzysta z dwóch bibliotek: libpcap (umożliwiającą pozyskiwania pakietów z sieci) i libnet (umożliwiającą konstruowanie i wysyłanie dowolnych pakietów). Obie te biblioteki są dostępne dla praktycznie każdej architektury. Z kolei sam kod libnids nie wykorzystuje żadnych specyficznych dla Linuksa możliwości. Dlatego przeniesienie libnids na inny system operacyjny powinno być łatwe.

Źródła biblioteki libnids wraz z dokumentacją i programami przykładowymi można pobrać stąd.

 

6.2. Oferowana funkcjonalność

 

6.2.1. Defragmentacja IP. Jak już wspomniano, niezawodny algorytm defragmentacji jest podstawą działania E-komponentu. Atakujący może łatwo podzielić generowany przez siebie strumień danych na fragmenty IP, licząc na to, że NIDS niewłaściwie je poskłada. Dlatego ważną część libnids stanowi algorytm defragmentacji. Został on skopiowany z jądra Linuksa 2.0.36. Dokładniej, plik net/ipv4/ip_fragment.c stanowiący część kodu Linuksa zmodyfikowano tak, aby umożliwić jego działanie w przestrzeni użytkownika. Ponadto, algorytm wzbogacono tak, aby mógł defragmentować pakiety skierowane do wielu węzłów. Na poziomie jednego węzła sam algorytm pozostał jednak niezmieniony. Dzięki temu podejściu defragmentacja oferowana przez NIDS jest w takim samym stopniu niezawodna, co stosowana przez jądro Linuksa. Ponadto, libnids defragmentuje pakiety w dokładnie ten sam sposób, co Linux, uwzględniając nawet tak subtelne kwestie jak gospodarka zasobami węzła.

6.2.2. Śledzenie strumienia TCP. Najważniejszą część E-komponentu stanowi algorytm składania segmentów TCP w strumień danych. Algorytm ten musi uwzględniać kolejkowanie segmentów oraz możliwość wystąpienia duplikowanych lub zachodzących na siebie pakietów TCP. Oczywiście konieczne jest zaimplementowanie całego protokołu TCP, czyli schematu nawiązywania i kończenia połączenia, śledzenia numerów sekwencji, uwzględniania danych pilnych i wielu innych zagadnień.

Kuszącą możliwością było postąpienie podobnie jak w przypadku defragmentacji, czyli przystosowanie fragmentów kodu jądra Linuksa do pracy w trybie użytkownika. Jednakże kod implementujący protokół TCP jest znacznie bardziej obszerny i ściślej zintegrowany z resztą jądra, niż ma to miejsce w przypadku kodu implementującego defragmentację. Ponadto realizuje on wiele funkcji nieprzydatnych dla NIDS, takich jak utrzymywanie liczników czasu w celu właściwego retransmitowania pakietów. Poza tym, specyfika NIDS pozwala na zastosowanie licznych optymalizacji, nieobecnych w jądrze. Dlatego kod realizujący śledzenie strumienia TCP napisano na potrzeby libnids od początku. Jednakże dużo pracy poświęcono na takie skonstruowanie kodu, aby był on w pełni zgodny z algorytmami używanymi przez jądro Linuksa. W szczególności gospodarka zasobami podczas kolejkowania segmentów TCP jest zaimplementowana dokładnie tak, jak w jądrze Linuksa.

Aby skorzystać z oferowanej przez libnids możliwości śledzenia każdego strumienia TCP, programista musi przekazać tej bibliotece adres pewnej funkcji F, która będzie w przyszłości wywoływana przez libnids. Deklaracja tej funkcji musi mieć postać

void (struct tcp_stream * ns, void ** param)

Jednym z jej argumentów jest wskaźnik na strukturę struct tcp_stream. Struktura ta zawiera wszelkie potrzebne informacje o każdym strumieniu TCP. Jednym z pól tej struktury jest pole o nazwie nids_state. W zależności od jego wartości funkcja powinna zachowywać się następująco:

Przypadek 1: ns->nids_state==NIDS_JUST_EST. Oznacza to, że argument ns wskazuje na strukturę opisującą połączenie, które właśnie zostało nawiązane. Funkcja musi zdecydować, czy chce w przyszłości być wywoływana na skutek pojawienia się nowych danych w połączeniu TCP, opisywanym przez strukturę ns. Aby to określić, ma dostępne przez parametr ns wszystkie dane o połączeniu, takie jak adresy klienta i serwera, numery portów, numery sekwencji i tak dalej. Jeśli połączenie jest interesujące, to funkcja określa, jakie dane chce otrzymywać (dane skierowane do klienta, dane do serwera, dane pilne do klienta, dane pilne do serwera lub dowolną ich kombinację).

Przypadek 2. ns->nids_state==NIDS_DATA. Oznacza to, że w strumieniu opisywanym przez argument ns pojawiły się nowe dane. Struktura tcp_stream zawiera bufory na dane wymieniane w połączeniu. Bufory te będą zawierać dokładnie takie dane, jakie otrzymają aplikacje na serwerze i kliencie. Funkcja F powinna je przetworzyć, poszukując oznak ataku.

Przypadek 3. Pozostałe wartości pola ns->nids_state (NIDS_CLOSE, NIDS_RESET, NIDS_TIMEOUT) informują o tym, że połączenie zostało zamknięte i w jaki sposób. Funkcja powinna podjąć odpowiednie kroki, na przykład zwolnić zaalokowane zasoby.

Programista może przekazać do libnids adresy dowolnie wielu funkcji o podanym działaniu. Ten sam strumień TCP może być oglądany przez więcej niż jedną funkcję.

6.2.3. Wykrywanie skanowania portów TCP. Libnids po obejrzeniu każdego pakietu TCP nie niosącego flagi ACK (również takiego, który powstał w wyniku defragmentacji przeprowadzonej przez libnids) bada, czy pakiet ten jest częścią próby skanowania portów. Jeśli takich pakietów pojawi się w jednostce czasu więcej, niż pewna progowa wielkość (definiowalna przez użytkownika), to libnids wywoła funkcję nids_syslog (która może być przedefiniowana przez użytkownika) w celu zaraportowania ataku.

6.2.4. Inne własności. Libnids udostępnia wiele innych funkcji. Dokładny opis ich wszystkich znajduje się w plikach nagłówkowych stanowiących część kodu libnids; część z nich opisano w dodatku D. Warto wspomnieć o niektórych z nich.

a) dostęp do każdej warstwy protokołu TCP/IP. Użytkownik może zdefiniować funkcję, która będzie wywoływana po nadejściu pewnego typu pakietu (IP, defragmentowanego IP, TCP, UDP), co ułatwia rozszerzenie funkcjonalności libnids.

b) możliwość konfiguracji dużej liczby parametrów systemu. Zmienna globalna nids_params zawiera liczne pola, których przedefiniowanie przez użytkownika zmienia parametry libnids. Można zdefiniować między innymi maksymalną liczbę śledzonych jednocześnie połączeń TCP, parametry podsystemu wykrywania skanowania portów, funkcję nids_syslog (o niej poniżej) i wiele innych.

c) funkcjonalność D-komponentu. Libnids jest E-komponentem. Użytkownik powinien zdefiniować funkcję, która będzie wywoływana przez libnids z argumentami przekazującymi informacją o wystąpieniu istotnego zdarzenia (wykryciu skanowania portów, nadejścia pakietu TCP z nieważnymi flagami, nakładających się pakietów IP i innych). Jej adres należy przepisać polu syslog struktury nids_params. Domyślnie pole to zawiera adres definiowanej przez libnids prowizorycznej funkcji nids_syslog, używającej systemowego demona syslogd do przekazywania informacji administratorowi.

d) funkcjonalność C-komponentu. Do celów testowych libnids definiuje funkcję nids_kill_tcp, której argumentem jest wskaźnik na strukturę typu struct tcp_stream. Użytkownik może wywołać tę funkcję w celu wymuszenia zerwania dowolnego połączenia TCP. Funkcja ta wysyła odpowiednio spreparowane pakiety TCP niosące flagę RST, które zakończą połączenie.

 

6.3. Niezawodność i konfigurowalność

 

Najważniejszą własnością libnids miała być wysoka niezawodność, z jaką ta biblioteka miała rekonstruować ruch kierowany do wybranego systemu – Linuksa 2.0.36. Cel ten miały przybliżyć dwie techniki: użycie części kodu jądra Linuksa do budowy libnids oraz dogłębne testowanie.

Zamierzony cel został prawie osiągnięty. Szczególnie defragmentacja pakietów IP, dzięki zapożyczeniu dużej części kodu ze źródeł Linuksa, powinna działać wysoce niezawodnie. Również emulacja kolejkowania segmentów TCP zachowuje się zgodnie z oczekiwaniami.

Podczas testów biblioteki wykryto trzy niezgodności między jej działaniem a funkcjonowaniem jądra Linuksa. Okazało się, że wina leżała po stronie jądra – powodem były trzy niezamierzone odstępstwa od specyfikacji protokołu TCP, które należy zaklasyfikować jako błędy w jądrze 2.0.36. Dwa z nich opisano w dodatku B. Trzeci z nich ma ciekawą naturę z teoretycznego punktu widzenia, stąd został dokładniej opisany w rozdziale 8.

Możliwe były dwie metody usunięcia powyższego problemu. Pierwszą z nich było zaimplementowanie w libnids wadliwego sposobu obsługi protokołu TCP przez jądro. Jednakże po usunięciu tych usterek przez programistów rozwijających jądro Linuksa, libnids nie funkcjonowałaby poprawnie.

Wybrano inną drogę. Informacja o zauważonych błędach została przekazana osobie odpowiedzialnej za przygotowanie nowej wersji jądra Linuksa, mianowicie 2.0.37. Ta wersja powinna ukazać się na przełomie czerwca i lipca 1999 roku. Zauważone błędy mają być w niej usunięte. Tak więc libnids będzie najlepiej chronić systemy oparte na tej wersji jądra.

Warto podkreślić, że libnids dokona poprawnej interpretacji większości pakietów IP skierowanych do dowolnego systemu, nawet jeśli będą one wysoce nietypowe (na przykład wytworzone przez intruza). Aby ominąć libnids, atakujący musi wykorzystać dość subtelne kwestie związane z gospodarka zasobami, bądź też odstępstwa od protokołów, obecne w atakowanym systemie. Jest to nader nietrywialne.

W celu zapewnienia jak największej funkcjonalności, libnids jest w wysokim stopniu konfigurowalna. Szereg istotnych parametrów można zmieniać bez rekompilacji biblioteki. Poza tym, dostępność jej kodu źródłowego pozwala na dokonywanie wszystkich potrzebnych modyfikacji tego kodu przez użytkownika biblioteki.

 

 

 

 

 

 

 

7. Przeprowadzone testy

 

Rozdział ten zawiera opis testów biblioteki libnids.

8. Napotkane problemy z jądrem Linuksa

 

Rozdział ten zawiera szczegółową analizę algorytmów używanych przez jądro Linuksa 2.0.36 do kontroli zasobów potrzebnych do utrzymywania informacji o stanie gniazd sieciowych.

 

 

 

 

9. Możliwe drogi rozwoju libnids

 

 

Najistotniejszym rozszerzeniem libnids byłoby dodanie możliwości niezawodnej rekonstrukcji ruchu w sieci skierowanego do systemów innych niż Linux. Spowodowałoby to konieczność dodatkowej konfiguracji libnids – dla każdego chronionego adresu IP musiałby być znany system operacyjny zainstalowany na węźle o tym IP.

Przy implementacji libnids intensywnie wykorzystano dostępność źródeł systemu, który libnids ma emulować. W szczególności pliki ip_fragment.c i ip_options.c, stanowiące część kodu libnids, są mocno przerobionymi odpowiednimi plikami z kodu jądra Linuksa. Również przy implementacji symulowania protokołu TCP stosowano liczne zapożyczenia z jądra Linuksa.

Ciekawą ideą jest jeszcze silniejsze wykorzystanie kodu jądra. Istotny jest fakt, że kod obsługujący urządzenia sieciowe dość ściśle oddzielono od kodu implementującego protokoły. Interesujące byłoby napisanie kodu (nazwijmy go libdev) pracującego w trybie użytkownika, który pobierałby pakiety z sieci (oczywiście korzystając z funkcji systemowych) i udostępniałby je innym częściom programu za pomocą interfejsu identycznego z tym, którego używają sterowniki urządzeń sieciowych w Linuksie. Następnym krokiem byłoby przystosowanie całego kodu Linuksa, odpowiedzialnego za implementacje protokołów (katalog net/ipv4), do pracy w trybie użytkownika. Nie powinno to nastręczyć dużych trudności – kod ten nie odwołuje się do sprzętu bezpośrednio, jego działanie to wyłącznie operacje na własnych strukturach danych. Pozostają do zaimplementowania funkcje-“łączniki” (ang. stub) zamieniające specyficzne dla trybu jądra procedury (takie jak get_free_page) na ich funkcjonalne odpowiedniki z trybu użytkownika (na przykład alloca). Po połączeniu tak przystosowanego kodu jądra z kodem libdev otrzymalibyśmy proces, interpretujący wszystkie pakiety dokładnie tak, jak jądro Linuksa. Procesów takich należałoby uruchomić tyle, ile ma być chronionych węzłów. Procesy te komunikowałyby się z resztą NIDS za pomocą mechanizmów IPC.

Aby projekt opisany w powyższym akapicie powiódł się, konieczne będą pewne modyfikacje kodu wyjętego z jądra Linuksa. Przykładowo, po zobaczeniu pakietu TCP z flagą SYN kod ten powinien wytworzyć w swoich strukturach danych realizację gniazda akceptującego połączenia (znajdującego się w stanie LISTEN) i zrealizować połączenie.

Libnids jest E-komponentem. Niektóre fragmenty funkcjonalności innych typów komponentów mogłyby zostać łatwo zaimplementowane. Przykładowo, każdy A-komponent potrzebuje modułu realizującego parsowanie strumienia danych według jednej z zadanych nieskomplikowanych gramatyk. Prosty, wydajny parser warto włączyć do biblioteki, która ma ułatwiać konstruowanie NIDS.

 

 

 

 

 

 

 

 

 

10. Podsumowanie

 

 

W niniejszej pracy opisano zagadnienia związane z interpretacją przez NIDS danych, zawartych w przepływających w sieci pakietach. Moduły analizujące, które stanowią część każdego NIDS, powinny dostawać do przetworzenia dokładnie takie same dane, jak aplikacje działające na chronionych węzłach. Nie jest to łatwe do osiągnięcia. Dlatego problem konstrukcji podsystemów NIDS dostarczających dane dla modułów analizujących powinien zająć ważne miejsce podczas projektowania NIDS.

Niestety, wydaje się, że twórcy współczesnych NIDS nie przywiązują należytej wagi do problemów poruszonych w niniejszej pracy. Większość NIDS można ominąć przy pomocy nieskomplikowanych ataków.

Biblioteka libnids miała na celu niezawodną emulację stosu TCP/IP maszyn pracujących pod kontrolą systemu operacyjnego Linux. Zadanie to zrealizowano tak dokładnie, jak było to możliwe. Niestety, Linux ma kilka właściwości, których obecność przeszkadza w osiągnięciu pełnej zgodności między działaniem libnids a funkcjonowaniem jądra systemu. Praca zawiera dokładny opis napotkanych problemów.

Ciekawa byłaby analiza innych systemów operacyjnych pod kątem łatwości symulowania ich działania przez NIDS. Obecnie używane protokoły sieciowe są tak skomplikowane, że nader prawdopodobne jest znalezienie w każdym systemie fragmentów kodu o działaniu trudnym do przewidzenia.

Nawet proste ataki przeciwko NIDS wymagają sporej wiedzy. Wielu administratorów, nawet zaznajomionych z zagadnieniami bezpieczeństwa, skłania się ku lekceważeniu intruzów. Funkcjonuje wizerunek intruza, który jest niedouczonym nastolatkiem, włamującym się do słabo zabezpieczonych serwerów w celu zdobycia poważania w swoim środowisku. Tworzenie oprogramowania z założeniami takimi, jakie przyjęto przy projektowaniu libnids, może wydać się niepotrzebne.

Z pewnością duża część intruzów to “miłośnicy skryptów” (ang. script kiddies), którzy umieją co najwyżej zastosować ściągnięte z rozmaitych serwerów [9] gotowe programy, wykorzystujące luki w bezpieczeństwie systemów. Jednak jakość dostępnego publicznie oprogramowania, pisanego z myślą o łamaniu systemów, jest czasami niebezpiecznie wysoka. Przykładowo, w artykule [4] autor zamieszcza źródła bibliotek i modyfikacje jądra systemu OpenBSD, po których zastosowaniu atakujący może w dużym stopniu kontrolować kształt pakietów, opuszczającego jego maszynę. Zaimplementowano między innymi wymuszanie fragmentacji wychodzących pakietów i wstawianie w strumień TCP pakietów z niewłaściwą sumą kontrolną. Korzystając z tego oprogramowania, intruz może ominąć większość obecnie dostępnych systemów NIDS.

Wreszcie oczywistym być powinno, że o dobrze wyszkolonych intruzach słychać niewiele dlatego, że oni sobie tego nie życzą. Zidentyfikowanie ich podczas próby ataku jest praktycznie niemożliwe – zbyt wiele istnieje sposobów na utrudnienie dotarcia do rzeczywistego źródła agresji. Podsumowując – zagrożenie jest realne. Dlatego każdy projekt, który ma na celu utrudnienie intruzom ataku, zasługuje na uwagę. Czas spędzony na implementacji libnids nie został stracony.

 

 

 

 

 

 

 

 

Bibliografia

 

 

  1. Thomas H.Ptacek, Timothy N. Newsham, ”Eluding Network Intrusion Detection”, http://www.nai.com/products/security/advisory/papers/ids-html/doc000.asp.
  2. Aleph One, ”Smashing the stack for fun and profit”, http://www.phrack.com/search.phtml?view&article=p49-14.
  3. Solar Designer, ”Designing and Attacking Port Scan Detection Tools”, http://www.phrack.com/search.phtml?view&article=p53-13.
  4. Horizon, ”Defeating Sniffers and Intrusion Detection Systems”, http://www.phrack.com/search.phtml?view&article=p54-10.
  5. S. Staniford-Chen, ”Common Intrusion Detection Framework”, http://seclab.cs.ucdavis.edu/cidf.
  6. Analiza ataku “smurf”, http://netscan.org.
  7. David Litchfield, ”Windows NT Buffer Overruns”, http://www.infowar.co.uk/mnemonix/ntbufferoverruns.htm.
  8. Źródła jądra Linuksa 2.0.36.
  9. Archiwum luk w systemach i aplikacjach, http://www.rootshell.com.
  10. David Ferbrache, ”Patologia wirusów komputerowych”, Wydawnictwa Naukowo-Techniczne, Warszawa 1993.
  11. ”RFC 793. Transmission Control Protocol”.

 

 

 

 

 

Dodatek A. Tożsamość klienta usługi TCP a jądro Linuksa 2.0.36

 

 

Dodatek ten zawiera opis pewnych cech jądra Linuksa 2.0.36, które umożliwiają wykonanie ataku podszywania się (ang. blind spoofing) przeciwko maszynie pracującej pod tym systemem.

 

 

Dodatek B. Wykryte błędy w jądrze Linuksa 2.0.36

 

 

Dodatek ten zawiera opis dwóch błędów w kodzie sieciowym jądra Linuksa, które zostały wykryte dzięki libnids.

 

Dodatek C. Słownik niektórych użytych terminów

 

 

fragmentowane pakiety IP, implikacje – maksymalny rozmiar pakietu IP to 65535 bajtów. Jednak fizyczny nośnik pakietów (warstwa transportowa) nakłada ograniczenia na rozmiar ramki, jaką może przenieść. Protokół IP dopuszcza fragmentację, czyli posłanie jednego pakietu w wielu mniejszych częściach, tak aby każda z nich mieściła się w ramce fizycznego medium.

Możliwość wystąpienia fragmentowanych pakietów IP znacząco utrudnia konstruowanie oprogramowania zajmującego się bezpieczeństwem sieci. Przykładowo, możliwe jest posłanie fragmentowanych pakietów IP, z których każdy niesie 8 bajtów danych. Nie wystarcza to na przeniesienie nagłówka TCP w pierwszym fragmencie IP. Jeśli ruter filtruje pakiety TCP na podstawie docelowego portu TCP, to nie ma on wystarczających danych, aby zaakceptować lub odrzucić taki pakiet.

Co gorsza, protokół IP zezwala, aby dane z poszczególnych fragmentów zachodziły na siebie (to znaczy przykładowo: pierwszy pakiet przenosi bajty danych o numerach od 0 do 15, a drugi o numerach od 8 do 23). Starsze systemy operacyjne nie przewidywały wszystkich implikacji takiej sytuacji. Odpowiednio spreparowane fragmenty IP mogły spowodować zawieszenie się lub restart systemu operacyjnego.

 

koń trojański – dowolny program, który zawiera kod realizujący funkcje inne niż te, których spodziewa się użytkownik lub deklarowanych w dokumentacji systemu [10]. Koń trojański jest umieszczany w systemie przez intruza w nadziei, że zostanie wykonany przez użytkownika o wysokich uprawnieniach. Kod konia trojańskiego ma za zadanie udostępnić jego autorowi wszystkie przywileje, z jakimi został uruchomiony.

 

przepełnienie bufora (ang. buffer overflow, buffer overrun) – błąd programisty, polegający na skopiowaniu do bufora w pamięci większej ilości danych, niż wynosi rozmiar bufora. W jego wyniku zostają zmodyfikowane zmienne położone za przepełnionym buforem, co może zmienić tok wykonywania się programu.

W najbardziej typowym przypadku bufor jest zmienną lokalną pewnej procedury. W architekturach, w których stos rośnie w dół przestrzeni adresowej (nie jest to niezbędne założenie), za zmiennymi lokalnymi procedury jest odkładany adres jej powrotu. Jeśli atakujący kontroluje dane, które przepełnią bufor (mogą to być np. parametry wywołania programu), może on zmienić adres powrotu z procedury tak, aby wskazywał na dowolne miejsce w przestrzeni adresowej procesu. Po zakończeniu procedury, sterowanie trafi w wybrane przez atakującego miejsce w pamięci. Powinien znajdować się tam kod maszynowy, który wykona czynności pożądane przez atakującego.

Celami ataku wykorzystującego przepełnienie bufora najczęściej są aplikacje sieciowe i programy wykonywane z podwyższonymi prawami (na przykład programy setuid w Uniksie). W pierwszym przypadku pomyślny atak daje dostęp do serwera. W drugim atakujący uzyskuje wszystkie przywileje, jakie posiadał zaatakowany program.

Możliwość przepełnienia bufora jest szczególnie łatwo przeoczyć programując w języku C. W jego standardowej bibliotece libc zawartych jest wiele funkcji operujących na napisach, które nie sprawdzają, czy nie zachodzi sytuacja przepełnienia bufora. Korzystanie z nich wymaga uwagi programisty.

Typową metodą wykorzystania przepełnienia bufora jest podawanie aplikacji danych o dużym rozmiarze – mogą to być na przykład parametry programu, zmienne środowiskowe, dane wejściowe programu.

 

pułapka SNMP (ang. SNMP trap) – SNMP, czyli Simple Network Management Protocol, to protokół ułatwiający scentralizowane zarządzanie siecią. Na przynajmniej jednym węźle uruchomiony jest zarządca (ang. network manager), który komunikuje się z demonami SNMP uruchomionymi na innych węzłach sieci (stacjami roboczymi, ruterami, drukarkami i innymi). SNMP definiuje pięć rodzajów komunikatów, wymienianych między zarządcą a innymi elementami sieci. Pierwsze cztery typy komunikatów umożliwiają zarządcy dostęp do zmiennych zawierających informacje o stanie elementu sieci. Piąty typ to pułapka SNMP. Jest to komunikat wysyłany przez element sieci do zarządcy w celu poinformowania go o wystąpieniu nietypowej sytuacji. Zdefiniowanych jest sześć standardowych pułapek (między innymi zanik komunikacji z ruterem, odebranie nieważnego komunikatu SNMP) oraz siódma, definiowalna przez aplikacje. Tej ostatniej może użyć IDS do komunikacji z zarządcą w celu przekazania informacji o wystąpieniu próby naruszenia bezpieczeństwa węzła lub sieci.

 

skanowanie portów (ang. port scan) – działalność intruza mająca na celu ustalenie, jakie porty TCP (znacznie rzadziej UDP) są otwarte na atakowanej maszynie. Dzięki tej wiedzy atakujący może się zorientować, jakie usługi są udostępniane przez daną sieć lub jej węzeł. Zwykle skanowanie portów poprzedza inne formy ataku. Można wyróżnić skanowanie węzła (badana jest jedna maszyna) i skanowanie sieci (badane są wszystkie węzły w sieci, zazwyczaj w intencji znalezienia na jednym z nich konkretnej usługi, którą atakujący umie wykorzystać).

Najprostszy program skanujący wywołuje wielokrotnie funkcję connect(), starając się połączyć z kolejnymi portami albo maszynami. Takie próby są widoczne w dzienniku systemowym (ang. log) atakowanych maszyn. Skanowanie niewidzialne (ang. stealth port scan) unika przeprowadzenia kompletnego zestawienia połączenia TCP, przez co próby połączenia nie są widoczne dla aplikacji.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Dodatek D. Libnids – interfejs programisty

 

 

Niniejszy dodatek zawiera opis podstawowych struktur danych i funkcji używanych przez libnids. Zamieszczono także prosty przykład zastosowania biblioteki.

Deklaracje struktur danych i funkcji definiowane przez libnids znajdują się w pliku nagłówkowym nids.h. Należy go włączyć dyrektywą #include do każdego pliku zawierającego kod źródłowym korzystający z libnids.

Podstawowym zadaniem libnids jest dostarczenie aplikacji informacji o danych wymienianych w każdym połączeniu TCP. Aby skorzystać z tej funkcjonalności, użytkownik libnids musi zadeklarować funkcję F, która będzie potem wywoływana przez libnids po pojawieniu się nowych danych. Przykładowa deklaracja funkcji F ma postać

void F (struct tcp_stream * ns, void ** param)

Znaczenie parametru ns (w szczególności jego pola nids_state) opisano już w punkcie 6.6.2. Struktura tcp_stream zawiera między innymi dwa pola typu struct half_stream o nazwach client i server, które opisują obie strony połączenia. W strukturach client i server znajdują się między innymi bufory z danymi skierowanymi do klienta i serwera.

Poniższy prosty program wypisuje dane wymieniane w każdym połączeniu TCP nawiązanym w sieci lokalnej.

 

#include "nids.h"

#include <stdio.h>

#include <netinet/in.h> // zawiera deklarację funkcji inet_ntoa

// struktura struct tuple4 zawiera adresy i porty klienta i serwera

// poniższa funkcja zwraca napis typu 10.0.0.1,1024,10.0.0.2,23

char *

address (struct tuple4 addr)

{

static char buf[256];

strcpy (buf, inet_ntoa (addr.saddr));

sprintf (buf + strlen (buf), ",%i,", addr.source);

strcat (buf, inet_ntoa (addr.daddr));

sprintf (buf + strlen (buf), ",%i", addr.dest);

return buf;

}

 

// adres poniższej funkcji przekażemy libnids

void

libnids_callback (struct tcp_stream *a_tcp, void *usually_unneeded_param)

{

char buf[1024]; // wpisujemy do niego dane o połączeniu

 

strcpy (buf, address (a_tcp->addr)); // najpierw adresy i porty, potem inne dane

if (a_tcp->nids_state == NIDS_JUST_EST)

{

// połączenie opisywane przez a_tcp zostało nawiązane

// w tym miejscu określamy, czy chcemy w przyszłości otrzymywać dane

// należące do tego strumienia

// przykładowy warunek: if (a_tcp->addr.dest!=23) return; (nie chcemy)

// w tym przykładowym programie śledzimy każdy strumień TCP, zatem...

a_tcp->client.collect++; // chcemy otrzymywać dane skierowane do klienta

a_tcp->server.collect++; // do serwera także

a_tcp->server.collect_urg++; // dane pilne do serwera

// a_tcp->client.collect_urg++; a pilnych danych do klienta nie chcemy

fprintf (stderr, "%s established\n", buf);

return ;

}

if (a_tcp->nids_state == NIDS_CLOSE) // połączenie jest zamykane

{

fprintf (stderr, "%s closing\n", buf);

return ;

}

if (a_tcp->nids_state == NIDS_RESET) // połączenie zamknięte przez segment

// niosący flagę RST

{

fprintf (stderr, "%s reset\n", buf);

return ;

}

 

if (a_tcp->nids_state == NIDS_DATA) // nowe dane w połączeniu

{

struct half_stream *hlf;

if (a_tcp->server.count_new_urg) // jeśli nowe pilne dane do serwera

{

strcat(buf,"(urgent->)");

buf[strlen(buf)+1]=0;

buf[strlen(buf)]=a_tcp->server.urgdata; // pobieramy bajt pilnych danych

write(2,buf,strlen(buf)); //wypisujemy

return;

}

if (a_tcp->client.count_new) // jeśli nowe dane do klienta

{

hlf = &a_tcp->client; // hlf wskazuje na dane o kliencie

strcat (buf, "(<-)");

}

else

{

hlf = &a_tcp->server; // dane do serwera

strcat (buf, "(->)");

}

fprintf(stderr,”%s”,buf); // wypisujemy parametry połączenia

write(2,hlf->data,hlf->count_new); // wypisujemy dane ze strumienia TCP

}

return ;

}

 

int

main ()

{

// w tym miejscu możemy zmienić parametry libnids

// na przykład wykonując nids_params.n_tcp_streams=2048

nids_init (); // inicjalizacja libnids

nids_register_tcp (libnids_callback); // funkcja libnids_callback ma być wywoływana

// kiedy zostanie wykryty nowy strumień TCP

nids_run (); // z tej instrukcji nie powinniśmy wyjść

return 0;

}

 

 

Poniżej zmieszczono opis struktur zadeklarowanych w pliku nids.h.

 

struct tuple4 // czwórka definiująca połączenie TCP

{

unsigned short source,dest; // numery portów klienta, serwera

unsigned long saddr,daddr; // adresy IP klienta, serwer

};

 

struct half_stream // struktura zawierająca dane o jednej ze stron połączenia TCP

// (”połówce”)

{

char state; // stan gniazda (np. TCP_ESTABLISHED )

char collect; // jeśli >0, to w buforze ”data” należy zapisywać dane TCP skierowane

// do połówki połączenia, którą opisuje ta struktura

char collect_urg; // jak wyżej, ale określa, czy magazynować dane pilne

char * data; // bufor ze zwykłymi danymi;

unsigned char urgdata; // bufor (jednoelementowy) na dane pilne

int count; // ile danych (zwykłych) zostało do tej pory zapisanych do bufora ”data”

int offset; // patrz wyjaśnienia po opisie struktury tcp_stream

int count_new; // ile danych zostało zapisanych do bufora ”data”

// podczas ostatniego zapisu do niego

char count_new_urg; // czy są nowe dane pilne

... // inne pola mają znaczenie pomocnicze dla libnids

};

 

struct tcp_stream

{

struct tuple4 addr; // parametry połączenia

char nids_state; // logiczny stan połączenia, opisany w punkcie 6.2.2

struct half_stream client,server; // struktury opisujące klienta i serwer

... // inne pola mają znaczenie pomocnicze dla libnids

};

 

W powyższym przykładowym programie funkcja libnids_callback pobierała dane z bufora hlf->data, wypisywała je na standardowe wyjście diagnostyczne, po czym dane te nie były już potrzebne. Po powrocie funkcji libnids_callback, libnids domyślnie usuwa wszystkie dane z bufora. Jeśli tak nie powinno być (na przykład do obróbki danych potrzeba, aby nadeszło ich co najmniej 1024 bajty, a po pierwszym wywołaniu funkcja libnids_callback ma ich do dyspozycji mniej) należy wykonać funkcję

void nids_discard(struct tcp_stream * a_tcp, int num)

jej wykonanie spowoduje odrzucenie (po powrocie funkcji libnids_callback) co najwyżej num pierwszych bajtów z bufora z danymi należącego do połączenia wskazywanego przez a_tcp. Jeśli powyższa funkcja nigdy nie będzie wykonana, to w buforze hlf->data zawsze będzie dostępnych hlf->count_new bajtów danych. W ogólnym przypadku ilość danych w buforze równa się hlf->count – hlf->offset ; hlf->offset to numer pierwszego bajtu z bufora hlf->data w strumieniu TCP, a hlf->count to liczba odebranych bajtów. Aby nie obciążać pamięci danymi zgromadzonymi w buforze hlf->data, zaleca się wywołać w funkcji takiej jak libnids_callback polecenie nids_discard z jak największym parametrem num (lub nie wywoływać jej wcale, jeśli możliwe).

Często występuje potrzeba skojarzenia ze śledzonym strumieniem TCP pomocniczych struktur danych. Przykładowo, program bufovtest opisany w punkcie 7.3 musi dla każdego połączenia TCP skierowanego do demona ftpd utrzymywać napis zawierający katalog bieżący demona (będzie on zmieniany przez polecenia, które aplikacja odczyta ze strumienia TCP). Funkcja taka jak libnids_callback po pierwszym wywołaniu (podczas którego ma określić, czy jest zainteresowana śledzeniem strumienia TCP) może zaalokować pewne zasoby, a wskaźnik na nie przypisać na dereferencję jej drugiego parametru. Przy następnym jej wywołaniu drugi parametr funkcji libnids_callback będzie wskazywał na zaalokowane zasoby. Przykład:

void

libnids_callback_2 (struct tcp_stream * a_tcp, struct conn_param **ptr)

{

struct conn_param * a_conn, *current_conn_param;

if (a_tcp->nids_state==NIDS_JUST_EST)

{

jeśli połączenie nas nie interesuje, wracamy;

a_conn=alokujemy strukturę conn_param;

inicjalizujemy a_conn;

określamy, jakie dane nas interesują;

*ptr=a_conn

return;

}

if (a_tcp->nids_state==NIDS_DATA)

{

current_conn_param=*ptr;

na podstawie current_conn_param i nowych danych wypatrujemy objawów ataku, być może modyfikując current_conn_param

return ;

}

...

}

Funkcję nids_register_tcp można wywołać dowolną liczbę razy. Dwie różne funkcje takie jak libnids_callback mogą śledzić ten sam strumień TCP. Powyższe funkcje i struktury danych wystarczają do efektywnego obrabiania danych otrzymanych w strumieniu TCP.

Ponadto, libnids definiuje funkcję

void nids_register_ip(void (*ip_func));

po jej wywołaniu, funkcja ip_func będzie wywoływana po nadejściu każdego niefragmentowanego pakietu IP (również takiego, który libnids złożyła z fragmentów). Deklaracja funkcji ip_func powinna mieć postać

void ip_func(struct iphdr * pakiet_ip)

będzie ona wywoływana z parametrem równym wskaźnikowi na otrzymany pakiet IP.

Podobnie, wywołanie funcji

void nids_register_ip_frag(void (*ip_frag_func));

spowoduje, że funkcja ip_frag_func będzie wywoływana z parametrem równym wskaźnikowi na otrzymany pakiet IP (być może fragmentowany lub mający niewłaściwą sumę kontrolną nagłówka).

Parametry libnids zmienia się modyfikując pola zmiennej globalnej nids_params (przed wywołaniem nids_init()).

 

struct nids_prm

{

int n_tcp_streams; // ile strumieni TCP libnids może śledzić jednocześnie, domyślnie

// 1024

int n_hosts; // dla ilu węzłów ma być przeprowadzana defragmentacja IP, domyślnie

// 256

char * device; // urządzenie, z którego mają być pobierane pakiety, domyślnie eth0

int sk_buff_size; // rozmiar struktury sk_buff (patrz rozdział 8), domyślnie 168

int dev_addon; // ile bajtów w strukturze sk_buff jest rezerwowanych dla danych o

// urządzeniu, domyślnie 32 dla interfejsów eth, 0 dla interfejsów ppp

void (*syslog)(); // patrz opis poniżej

int syslog_level; // jeśli pole ”syslog” ma wartość adresu funkcji nids_syslog, wtedy

// to pole określa poziom istotności komunikatów przekazywanych

// za pośrednictwem systemowego demona syslogd

int scan_num_hosts; // liczba prób skanowania portów, które jednocześnie można

// wykryć, domyślnie 64

int scan_num_ports; // ile portów TCP musi być skanowanych z tego samego

// źródła ...

int scan_delay; // .. z odstępem co najwyżej scan_delay milisekund

// miedzy kolejnymi portami, aby libnids zgłosiła próbę ataku

};

struct nids_prm nids_params;

 

 

Pole ”syslog” powyższej struktury domyślnie zawiera adres funkcji nids_syslog. Zdefiniowano ją następująco:

void nids_syslog (int type, int errnum,struct iphdr * iph,void * data);

Parametr type określa rodzaj komunikatu. Może mieć on wartość

NIDS_WARN_IP – dla komunikatów dotyczących poziomu IP

NIDS_WARN_TCP – dla komunikatów dotyczących poziomu TCP

NIDS_WARN_SCAN – komunikat ostrzega o próbie skanowania portów.

W dwóch pierwszych przypadkach parametr errnum określa numer błędu (można go użyć jako indeks tablicy napisów nids_warnings, zawierającej tekstowe reprezentacje błędów). Parametr iph to wskaźnik na pakiet IP, który spowodował wszczęcie alarmu. Funkcja nids_syslog, używając systemowego demona syslogd, zapisuje do dziennika zdarzeń odpowiednie komunikaty (na przykład: próba skanowania z adresu x.y.z.t, skanowane porty: IP1:port1, IP2:port2,..., typ skanowania: SYN scan).

Funkcję nids_syslog zdefiniowano wyłącznie w celu zilustrowania sposobu obsługi komunikatów generowanych przez libnids. Nie jest ona sprawnym D-komponentem (na przykład, nie limituje liczby komunikatów na sekundę, co może doprowadzić do zapełnienia dysku przez plik dziennika zdarzeń).

Ponadto, zaimplementowano funkcję

void nids_killtcp(struct tcp_stream * a_tcp);

Ma ona na celu wysłanie segmentów TCP niosących flagę RST, które zakończą połączenie TCP opisywane przez a_tcp.