Efektywne wyszukiwanie wzorców w systemach automatycznej generacji sygnatur ataków sieciowych



Podobne dokumenty
Efektywne wyszukiwanie wzorców w systemach automatycznej generacji sygnatur ataków sieciowych

0-0000, , , itd

Modelowanie motywów łańcuchami Markowa wyższego rzędu

Nierówność Krafta-McMillana, Kodowanie Huffmana

TEORETYCZNE PODSTAWY INFORMATYKI

Algorytmiczna teoria grafów

Podstawy programowania 2. Temat: Drzewa binarne. Przygotował: mgr inż. Tomasz Michno

Grafy (3): drzewa. Wykłady z matematyki dyskretnej dla informatyków i teleinformatyków. UTP Bydgoszcz

Definicje. Algorytm to:

Algorytmy przeszukiwania wzorca

Sortowanie - wybrane algorytmy

Każdy węzeł w drzewie posiada 3 pola: klucz, adres prawego potomka i adres lewego potomka. Pola zawierające adresy mogą być puste.

Algorytmy i struktury danych. Drzewa: BST, kopce. Letnie Warsztaty Matematyczno-Informatyczne

Kodowanie Huffmana. Platforma programistyczna.net; materiały do laboratorium 2014/15 Marcin Wilczewski

Algorytmy i struktury danych. wykład 8

Liczba zadań a rzetelność testu na przykładzie testów biegłości językowej z języka angielskiego

Temat: Algorytm kompresji plików metodą Huffmana

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Algorytmy i struktury danych Laboratorium 7. 2 Drzewa poszukiwań binarnych

Def. Kod jednoznacznie definiowalny Def. Kod przedrostkowy Def. Kod optymalny. Przykłady kodów. Kody optymalne

9.9 Algorytmy przeglądu

KODY SYMBOLI. Kod Shannona-Fano. Algorytm S-F. Przykład S-F

Drzewa poszukiwań binarnych

SCHEMAT ROZWIĄZANIA ZADANIA OPTYMALIZACJI PRZY POMOCY ALGORYTMU GENETYCZNEGO

Matematyka dyskretna - 7.Drzewa

lekcja 8a Gry komputerowe MasterMind

Abstrakcyjne struktury danych - stos, lista, drzewo

Przykładowe sprawozdanie. Jan Pustelnik

Algorytmy zachłanne. dr inż. Urszula Gałązka

Algorytm. Krótka historia algorytmów

Laboratorium Sieci Komputerowe

Rozwiązaniem jest zbiór (, ] (5, )

1. Synteza automatów Moore a i Mealy realizujących zadane przekształcenie 2. Transformacja automatu Moore a w automat Mealy i odwrotnie

Metody Kompilacji Wykład 3

Podstawy programowania. Wykład 7 Tablice wielowymiarowe, SOA, AOS, itp. Krzysztof Banaś Podstawy programowania 1

Instytut Politechniczny Państwowa Wyższa Szkoła Zawodowa. Diagnostyka i niezawodność robotów

Algorytmy sztucznej inteligencji

Algorytm obejścia drzewa poszukiwań i zadanie o hetmanach szachowych

5c. Sieci i przepływy

Sprawozdanie do zadania numer 2

Wyszukiwanie binarne

Spacery losowe generowanie realizacji procesu losowego

1 Automaty niedeterministyczne

SYSTEMY UCZĄCE SIĘ WYKŁAD 4. DRZEWA REGRESYJNE, INDUKCJA REGUŁ. Dr hab. inż. Grzegorz Dudek Wydział Elektryczny Politechnika Częstochowska

XQTav - reprezentacja diagramów przepływu prac w formacie SCUFL przy pomocy XQuery

Podstawy Informatyki. Metody dostępu do danych

Kompresja bezstratna. Entropia. Kod Huffmana

5.5. Wybieranie informacji z bazy

Konkurs z przedmiotu eksploracja i analiza danych: problem regresji i klasyfikacji

Dynamiczny przydział pamięci w języku C. Dynamiczne struktury danych. dr inż. Jarosław Forenc. Metoda 1 (wektor N M-elementowy)

Algebrą nazywamy strukturę A = (A, {F i : i I }), gdzie A jest zbiorem zwanym uniwersum algebry, zaś F i : A F i

INŻYNIERIA BEZPIECZEŃSTWA LABORATORIUM NR 2 ALGORYTM XOR ŁAMANIE ALGORYTMU XOR

OSTASZEWSKI Paweł (55566) PAWLICKI Piotr (55567) Algorytmy i Struktury Danych PIŁA

ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2014/2015. Drzewa BST c.d., równoważenie drzew, kopce.

Algorytmy i. Wykład 5: Drzewa. Dr inż. Paweł Kasprowski

Algorytm SAT. Marek Zając Zabrania się rozpowszechniania całości lub fragmentów niniejszego tekstu bez podania nazwiska jego autora.

prowadzący dr ADRIAN HORZYK /~horzyk tel.: Konsultacje paw. D-13/325

Wykład 3. Złożoność i realizowalność algorytmów Elementarne struktury danych: stosy, kolejki, listy

Za pierwszy niebanalny algorytm uważa się algorytm Euklidesa wyszukiwanie NWD dwóch liczb (400 a 300 rok przed narodzeniem Chrystusa).

Funkcja kwadratowa. f(x) = ax 2 + bx + c,

Wykład 2. Drzewa zbalansowane AVL i 2-3-4

Sortowanie topologiczne skierowanych grafów acyklicznych

Metoda tabel semantycznych. Dedukcja drogi Watsonie, dedukcja... Definicja logicznej konsekwencji. Logika obliczeniowa.

Drzewa BST i AVL. Drzewa poszukiwań binarnych (BST)

Szukanie rozwiązań funkcji uwikłanych (równań nieliniowych)

Egzamin, AISDI, I termin, 18 czerwca 2015 r.

3. MINIMAX. Rysunek 1: Drzewo obrazujące przebieg gry.

Wykład 6. Wyszukiwanie wzorca w tekście

ZARZĄDZANIE PROJEKTAMI I PROCESAMI. Mapowanie procesów AUTOR: ADAM KOLIŃSKI ZARZĄDZANIE PROJEKTAMI I PROCESAMI. Mapowanie procesów

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Zadanie 1. Suma silni (11 pkt)

PLAN WYKŁADU BAZY DANYCH INDEKSY - DEFINICJE. Indeksy jednopoziomowe Indeksy wielopoziomowe Indeksy z użyciem B-drzew i B + -drzew

Lista 5 Typy dynamiczne kolejka

Funkcje wyszukiwania i adresu PODAJ.POZYCJĘ

Algorytmy i struktury danych

Matematyczne Podstawy Informatyki

Michał Kazimierz Kowalczyk rok 1, semestr 2 nr albumu indeksu: Algorytmy i struktury danych. Problem połączeń

Reprezentacje grafów nieskierowanych Reprezentacje grafów skierowanych. Wykład 2. Reprezentacja komputerowa grafów

Rys Wykres kosztów skrócenia pojedynczej czynności. k 2. Δk 2. k 1 pp. Δk 1 T M T B T A

Wprowadzenie do maszyny Turinga

a) 7 b) 19 c) 21 d) 34

Algorytmy i złożoności. Wykład 3. Listy jednokierunkowe

Ekonometria - ćwiczenia 10

Drzewa czerwono-czarne.

< K (2) = ( Adams, John ), P (2) = adres bloku 2 > < K (1) = ( Aaron, Ed ), P (1) = adres bloku 1 >

L E X. Generator analizatorów leksykalnych

wagi cyfry pozycje

Uniwersytet Zielonogórski Wydział Elektrotechniki, Informatyki i Telekomunikacji Instytut Sterowania i Systemów Informatycznych

Wykład 2: Arkusz danych w programie STATISTICA

5.4. Tworzymy formularze

Algorytmy i struktury danych

FUNKCJE. Kurs ZDAJ MATURĘ Z MATEMATYKI MODUŁ 5 Teoria funkcje cz.1. Definicja funkcji i wiadomości podstawowe

Indukowane Reguły Decyzyjne I. Wykład 3

Układy VLSI Bramki 1.0

Tworzenie prezentacji, PowerPoint

Zajęcia nr. 3 notatki

XML i nowoczesne technologie zarządzania treścią 2007/08

Języki formalne i automaty Ćwiczenia 2

Równoległy algorytm wyznaczania bloków dla cyklicznego problemu przepływowego z przezbrojeniami

Działanie algorytmu oparte jest na minimalizacji funkcji celu jako suma funkcji kosztu ( ) oraz funkcji heurystycznej ( ).

Algorytmy i str ruktury danych. Metody algorytmiczne. Bartman Jacek

Transkrypt:

Efektywne wyszukiwanie wzorców w systemach automatycznej generacji sygnatur ataków sieciowych Tomasz Jordan Kruk NASK Cezary Rzewuski Politechnika Warszawska Wstęp Zagrożenia związane z siecią Internet stały się w ostatnich latach bardzo ważnym aspektem oceny nowych rozwiązań i usług. Pomimo tego, liczba dziur wykrywanych w nowym oprogramowaniu nie pozwala na optymizm w kwestii jakości udostępnianych produktów. Liczba robaków sieciowych wykorzystujących podatności systemów działających w Internecie sprawia, że poza wysiłkiem wkładanym w udoskonalanie metod inżynierii oprogramowania, równocześnie opracowywane są coraz nowsze metody walki z oprogramowaniem złośliwym. W artykule przedstawiona zostanie metoda automatycznej generacji sygnatur robaków internetowych. W szczególności przedstawiony zostanie algorytm pozwalający na szybkie wyszukiwanie wzorców, dzięki któremu automatyczna generacja sygnatur może odbywać się w czasie rzeczywistym. Działanie systemu wykrywania włamań polega na porównywaniu ruchu przechwyconego z sieci z rekordami bazy sygnatur robaków. Jeżeli przechwycony ruch pasuje do sygnatury, zgłaszany jest alarm z informacją o wykrytym zagrożeniu. Sygnatura składa się najczęściej z ciągu bajtów, charakterystycznego dla danego eksploita. Obecność tego ciągu w strumieniu danych połączenia pozwala zidentyfikować zagrożenie. Przykładowa sygnatura mogłaby zawierać ciąg znaków cmd.exe. Po wprowadzeniu takiego rekordu do bazy IDS, każde połączenie, przekazujące ciąg cmd.exe, powodowałoby zgłoszenie alarmu. Polecenie cmd.exe wywołuje powłokę w systemach z rodziny Windows, co jest często jednym z kroków ataku na to oprogramowanie. Jednak nie każde połączenie przekazujące ciąg cmd.exe jest atakiem. Przedstawiony przykład nie jest więc dobrą sygnaturą, ponieważ spowoduje generację ogromnej liczby alarmów, wśród których wiele będzie fałszywych. Sygnatura powinna identyfikować robaka w sposób na tyle precyzyjny,

żeby liczba fałszywych alarmów wywoływanych przez nią była jak najmniejsza. Z drugiej strony pożądana jest pewna elastyczność, tak aby minimalna modyfikacja eksploita nie powodowała oszukania systemu wykrywania włamań. Tabela 1 przedstawia fragment sygnatury identyfikującej robaka Slammer 1. EB 0E 01 01 01 01 01 01 01 p AE B 01 p AE B 90 90 90 90 90 90 90 90 h DC C9 B0 B B8 01 01 01 01 1 C9 B1 18 P E2 FD 5 01 01 01 05 P 89 E5 Qh.dllhel32hkernQhounthickChGetTf B9 llqh32.dhws2_f B9 etqhsockf B9 toqhsend BE 18 10 AE B 8D E D4 P FF 16 P 8D E E0 P 8D E F0 P FF 16 Tabela 1: Fragment sygnatury robaka Slammer Tradycyjne metody tworzenia sygnatur, polegające na dogłębnej analizie kodu źródłowego i działania robaka, przestały zdawać egzamin w obliczu skali do jakiej urósł problem złośliwego oprogramowania. Obecnie wykorzystywane są rozwiązania umożliwiające automatyczną generację sygnatur. Takie narzędzia uruchamiane są zazwyczaj w systemach narażonych na atak i jednocześnie umożliwiających łatwą klasyfikację ruchu potencjalnie szkodliwego. Dobrym przykładem środowisk, w których może pracować narzędzie generujące sygnatury, są systemy honeypot. Technologia honeypot wykorzystuje nieużywane adresy IP, emulując na nich popularne serwisy (np. www, telnet). Emulacja serwisów zapewnia interakcję z potencjalnym intruzem, co pozwala na przeprowadzenie ataku tak, jakby odbył się on na realnym systemie produkcyjnym. Wykorzystanie wolnych adresów IP sprawia, że połączenia do systemu honeypot można przypisać do jednej z dwóch kategorii: zostały zainicjowane pomyłkowo i są niegroźne (małe prawdopodobieństwo), pochodzą od robaków skanujących sieć w poszukiwaniu nowych ofiar (duże prawdopodobieństwo). Duże prawdopodobieństwo interakcji z robakiem powoduje, że cały ruch sieciowy przetwarzany przez honeypot klasyfikuje się jako potencjalnie niebezpieczny. Wyniki zwracane przez systemy automatycznej generacji sygnatur konstruowane są na podstawie wspólnych cech ruchu obserwowanego przez te systemy. Wspólnymi cechami są identyczne ciągi bajtów występujące w kilku połączeniach, ponieważ eksploity identyfikowane są właśnie na podstawie charakteryzujących je ciągów bajtów. Działanie tego mechanizmu bardzo łatwo zobrazować przykładem. Załóżmy, że system automatycznej generacji sygnatur zarejestrował trzykrotny atak robaka CodeRed. Dane każdego połączenia składane są w jeden ciąg bajtów. Po 1 Źródło: [6]

złożeniu kilku strumieni danych przeprowadzana jest operacja wyznaczenia ich wspólnego podciągu. Porównanie ze sobą w ten sposób wielu połączeń jednego ataku zwróci powtarzający się ciąg bajtów, charakterystyczny dla danego eksploita (w tym przypadku CodeRed). Operacja została zilustrowana na rysunku 1. Wspólny podciąg kilku strumieni nazywany jest wzorcem. Przedstawiona wcześniej sytuacja jest wyidealizowana, ponieważ w warunkach rzeczywistych system ma do czynienia z wieloma różnymi atakami i nie jest w stanie stwierdzić które połączenia należą do eksploita jednego typu. Rysunek 1: Wyznaczenie wspólnego podciągu w złożonych strumieniach połączeń Wspólny podciąg znaleziony w strumieniach robaków dwóch różnych typów nie jest odpowiednim wzorcem do wprowadzenia do sygnatury, ponieważ nie identyfikuje żadnego zagrożenia w sposób precyzyjny. Ten problem rozwiązywany jest poprzez ustawienie kryterium minimalnej długości wzorca, które musi być spełnione do stworzenia sygnatury. Wspólne podciągi robaków różnych typów są zazwyczaj krótkie, a ich wystąpienie jest w pewnym stopniu wynikiem zbiegu okoliczności. Jak wynika z przedstawionych do tego momentu informacji, zasadniczą operacją wykonywaną przez systemy automatycznej generacji sygnatur jest wyznaczenie wspólnego podciągu dwóch lub więcej łańcuchów bajtów. Problem ten znany jest w algorytmice jako LCS (ang. longest common substring) i poza dziedziną wykrywania włamań występuje również w kontekście wielu innych zagadnień 2. Sygnatury generowane są w czasie rzeczywistego działania łącza, dlatego wszystkiego operacje wykonywane na danych wejściowych muszą się odbywać w czasie wprost proporcjonalnym do ich wielkości. Wyznaczenie wzorca dwóch ciągów bajtów w czasie O(n) nie jest zadaniem trywialnym. Najczęściej stosowanym podejściem jest wykorzystanie drzew sufiksowych. Wskazanie wspólnego podciągu, jeżeli jeden z łańcuchów wejściowych jest dany w postaci drzewa sufiksowego, jest operacją nieskomplikowaną i można ją przeprowadzić w czasie wprost proporcjonalnym do długości drugiego łańcucha (tzn. tego, który nie został przekazany jako drzewo 2 w genetyce algorytmy LCS wykorzystywane są do wyszukiwania wspólnych fragmentów łańcuchów DNA

sufiksowe). Nadal pozostaje jednak problem przekształcenia ciągu bajtów do postaci drzewa sufiksowego. Chociaż również nie jest to operacja trywialna, to algorytmy wykonujące tę operację w czasie O(n) (gdzie n jest długością ciągu) zostały już opracowane. W dalszej części omówiony zostanie jeden z algorytmów konstrukcji drzewa sufiksowego. Przedstawiony zostanie również efektywny mechanizm wyznaczanie wzorca w sytuacji, gdy jeden z łańcuchów jest już w postaci drzewa. Omówione algorytmy zostały zaimplementowane przez jednego z autorów artykułu w postaci biblioteki liblcs. Na końcu pracy zamieszczono porównanie tej biblioteki z najbardziej popularną z dotychczas dostępnych bibliotek do operowania na drzewach sufiksowych. 2. Drzewa sufiksowe Przed przejściem do dalszych części związanych z samym algorytmem wprowadzone zostaną podstawowe pojęcia, które dalej będą używane. ciąg znaków, ciąg bajtów Jeśli nie podano inaczej, określenia ciągu znaków i ciągu bajtów są równoważne. Znakami, zgodnie z powszechnie uznaną definicją, są bajty o wartościach reprezentujących w kodzie ASCII symbole używane do druku. W dziedzinie ruchu sieciowego bajty przyjmują wartości należące do całego zakresu (od 0 do 255), stąd posługiwanie się pojęciem znaku w tym kontekście wymaga precyzyjnej definicji. W tej pracy przyjmuje się, że każdy ciąg bajtów jest również ciągiem znaków. Dowolny ciąg bajtów oznaczany będzie literą S (ang. string). S[i..j] oznacza podciąg ciągu S rozpoczynający się znakiem na pozycji i i kończący znakiem na pozycji j. S[i] oznacza i-ty znak ciągu. Calif jest S[1..5] podciągiem słowa California; S[7] w tym słowie to r. Podciąg S[i..j], gdzie i > j, jest podciągiem pustym. S oznacza długość ciągu S. prefiks Prefiksem ciągu S jest jego dowolny podciąg rozpoczynający się znakiem S[1] składający z n pierwszych znaków. Cal jest prefiksem ciągu California. N-tym prefiksem ciągu S, jest podciąg będący prefiksem i kończący się na pozycji n. Cal jest trzecim, a Calif piątym prefiksem ciągu California, sufiks Sufiksem ciągu S jest podciąg składający się z ostatnich znaków ciągu. N-tym sufiksem jest sufiks, rozpoczynający się na pozycji n ciągu S. Trzecim sufiksem ciągu California jest lifornia, a piątym fornia.

2.1 Struktura drzewa sufiksowego Drzewo sufiksowe jest strukturą reprezentującą ciąg znaków. Według ścisłej definicji algorytmicznej, drzewo sufiksowe jest grafem, będącym drzewem wolnym i ukorzenionym (zgodnie z [5]) w którym: liczba liści jest równa długości reprezentowanego ciągu S, każdy węzeł nie będący korzeniem, posiada co najmniej dwoje potomków, każda krawędź reprezentuje niepusty podciąg ciągu S, nie ma dwóch krawędzi wychodzących z jednego węzła i prowadzących do jego potomków, które zaczynają się tym samym znakiem, konkatenacja podciągów na ścieżce od korzenia do i-tego liścia jest i-tym sufiksem ciągu S. Ostatnia z wymienionych własności jest kluczową i zostanie szerzej omówiona. Każda ścieżka od korzenia do liścia drzewa sufiksowego reprezentuje jeden sufiks ciągu S. Liczba liści drzewa jest równa długości ciągu, w związku z czym każdy liść odpowiada sufiksowi rozpoczynającemu się na innej pozycji ciągu S. Można zatem nadać liściom numery od 1 do S, które będą oznaczały numer sufiksów ciągu S. W konsekwencji można powiedzieć, że konkatenacja podciągów na ścieżce do i-tego liścia daje i-ty sufiks ciągu S. Własność ta zastała zilustrowana na rysunku 2, na którym przedstawione jest drzewo sufiksowe ciągu abcabd z zaznaczanymi numerami liści. Konkatenacja podciągów na krawędziach prowadzących do liścia o numerze 4 daje ciąg abd, który jest czwartym sufiksem ciągu abcabd. Istnieje grupa ciągów znaków, których nie da się przedstawić za pomocą drzewa sufiksowego. Do tej grupy należą łańcuchy mające jeden ze swoich sufiksów identyczny z jakimkolwiek prefiksem. Przedstawicielem może być ciąg z poprzedniego przykładu (rys. 2) po skróceniu go o ostatni znak d. Wynikiem takiej operacji jest abcab, w którym drugi prefiks jest identyczny z prefiksem sufiksu ab. Utworzenie drzewa sufiksowego w tym przypadku jest niemożliwe, ponieważ ścieżka do sufiksu ab nie może zakończyć się na liściu. Gdyby w drzewie istniała ścieżka o etykiecie ab kończąca się na liściu, drzewo nie reprezentowałoby fragmentu znajdującego się za podciągiem ab. Podobnie ścieżka do każdego następnego sufiksu (w podanym przykładzie jedynym następnym sufiksem po ab jest jednoelementowy ciąg b) nie będzie mogła zakończyć się na liściu.

Rysunek 2: Drzewo sufiksowe reprezentujące ciąg abcabd Próbę konstrukcji drzewa dla ciągu abcab prezentuje rysunek 3. Otrzymane drzewo nie spełnia pierwszej z wymienionych uprzednio własności drzew sufiksowych - liczba liści nie odpowiada długości ciągu, w wyniku czego nie dla wszystkich sufiksów istnieje ścieżka od korzenia do liścia. Rysunek 3: Próba konstrukcji drzewa sufiksowego ciągu abcab Problem można wyeliminować dodając na koniec ciągu znak terminujący, czyli specjalny bajt, który nie występuje na żadnej innej pozycji przetwarzanego łańcucha. Ta prosta operacja zapewnia, że żaden sufiks nie jest jednocześnie prefiksem ciągu. W praktyce jednak, najczęściej niemożliwe jest wybranie jednego specjalnego znaku, który będzie zawsze końcowym elementem ciągu. W takich przypadkach problem można rozwiązać na poziomie implementacji. Jednym ze sposobów, jest rozszerzenie alfabetu znaków tak, aby obejmował on większy zakres niż dziedzina rozwiązywanego problemu. Dziedziną danych przesyłanych w pakietach IP jest bajt, stąd alfabet składa się ze znaków przyjmujących wartości od 0 do 255. Rozszerzenie alfabetu polega na zwiększeniu zakresu możliwych wartości poprzez zapisywanie znaków alfabetu w nadmiarowej strukturze. W przypadku alfabetu jednobajtowego, nadmiarową strukturą będzie np. struktura dwóch bajtów. Dzięki takiemu zapisowi alfabet rozszerza się do 65536 znaków, z których tylko pierwsze 256 jest zajęte przez dziedzinę problemu. Wada tego rozwiązania jest oczywista - dwukrotny wzrost zapotrzebowania na pamięć. Z tego powodu rozszerzanie alfabetu nie powinno być brane pod uwagę, gdy pamięć jest zasobem krytycznym.

W bibliotece liblcs zastosowany został pewien zabieg" implementacyjny, który pozwala w pewnych sytuacjach traktować węzeł jako liść, a w innych kierować przebiegiem programu tak, jakby tego węzła w ogóle nie było. 2.2 Wyznaczanie wzorca Przed przejściem do bardziej zaawansowanych zagadnień związanych z drzewami sufiksowymi, przedstawione zostanie zastosowanie tych struktur do wyszukiwania wzorca w ciągu znaków. Zrozumienie tego mechanizmu jest konieczne przed analizą problemu wyznaczenia najdłuższych wspólnych podciągów z wykorzystaniem drzewa sufiksowego. Mając dwa ciągi - krótszy P, który dalej będzie nazywany wzorcem, oraz dłuższy S, należy wskazać pozycje ciągu S, na których występuje wzorzec P. Przykład: wzorzec ania występuje w ciągu brytania jeden raz - na piątej pozycji. Problem ten jest w literaturze często określany mianem dokładnego dopasowania (ang.. exact string matching), ponieważ interesującymi rozwiązaniami są tylko podciągi pasujące dokładnie do całego wzorca. Szukanie wzorca P rozpoczyna się od budowy drzewa sufiksowego T ciągu S. W kolejnym kroku następuje przejście ścieżki rozpoczynającej się w korzeniu drzewa T, wyznaczonej przez dopasowanie kolejnych znaków wzorca P do znaków na krawędziach drzewa. Przejście kończy się w sytuacji gdy: wykorzystano wszystkie znaki wzorca P, niemożliwe jest dalsze dopasowanie znaków wzorca do znaków w drzewie. Wystąpienie drugiego warunku oznacza, że dany wzorzec nie znajduje się w ciągu. Zakończenie przy spełnieniu pierwszego warunku oznacza natomiast, że wzorzec P występuje w ciągu S co najmniej raz. Numery liści będących poniżej miejsca, w którym wystąpiło ostatnie dopasowanie oznaczają pozycje ciągu S, na których występuje wzorzec. Przykład na rysunku 4 ilustruje przypadek, gdy tekstem jest abcabdabcz, a poszukiwanym wzorcem ab. Strzałka wskazuje miejsce końca ścieżki od korzenia wzdłuż wzorca. Od tego miejsca możliwe jest przejście do trzech liści, reprezentujących trzy sufiksy: abcabdabcz abdabcz

abcz Numery sufiksów oznaczają jednocześnie pozycję wystąpienia wzorca ab. Wyznaczenie numerów sufiksów wymaga odwiedzenia każdego liścia drzewa, otrzymanego poprzez potraktowanie jako korzeń miejsca wskazanego strzałką na rysunku 4. Rysunek 4: Poszukiwanie wzorca ab w drzewie polega na przejściu ścieżką od korzenia wzdłuż wzorca Czas wymagany do znalezienia wszystkich wystąpień wzorca P w ciągu S przedstawionym na drzewie T wynosi O( S + P ). Składa się na niego czas potrzebny na przejście ścieżką drzewa wzdłuż ciągu P oraz czas potrzebny na odwiedzenie wszystkich liści pod ścieżką. 2.3 Budowa drzewa sufiksowego Drzewo sufiksowe jest strukturą danych, która eksponuje wewnętrzną strukturę ciągu znaków.[2] Eksponowanie wewnętrznej struktury ciągu w postaci drzewa sufiksowego umożliwia rozwiązywanie wielu złożonych problemów w czasie liniowym. Czynnikiem krytycznym jest czas potrzebny na przedstawienie ciągu wejściowego w postaci drzewa. Istnieje kilka algorytmów umożliwiających przedstawienie ciągu w postaci drzewa sufiksowego w czasie liniowym. Pierwszym opracowanym algorytmem osiągającym te ograniczenia czasowe jest algorytm Weiner'a z 1973 roku. Inne znane algorytmy zostały opracowane przez McCreight'a oraz Ukkonen'a. Spośród trzech wymienionych rozwiązań, najpopularniejszym jest algorytm Ukkonen'a. Jest on szczególnym przypadkiem algorytmu McCreight'a, jednak jest oparty na znacznie prostszej koncepcji. Biblioteka liblcs jest implementacją właśnie tego rozwiązania. Algorytm Weiner'a jest mało popularny - prawdopodobnie przez fakt, że praca Weiner'a z 1973 roku zyskała opinie wyjątkowo trudnej w zrozumieniu. Przed przejściem do opisu algorytmu Ukkonen'a, przedstawiony zastanie prosty i intuicyjny algorytm tworzenia drzew sufiksowych, który można określić mianem algorytmu naiwnego.

Metoda naiwna polega na przyrostowym budowaniu drzewa, poprzez dodawanie kolejnych sufiksów. Najpierw tworzone jest drzewo składające się z korzenia oraz jednej krawędzi, reprezentującej pierwszy sufiks S[1.. S ]. W kolejnych, krokach dodawane są pozostałe sufiksy S[i.. S ], gdzie i należy do przedziału [1; S ]. Dodanie i- tego sufiksu, polega na przejściu od korzenia drzewa wzdłuż ścieżki przystającej do jak najdłuższego prefiksu ciągu S[i.. S ]. Ścieżka jest unikalna, ponieważ z żadnego węzła nie wychodzą dwie krawędzie rozpoczynające się tym samym znakiem. Ten krok jest analogiczny do opisanego wcześniej kroku przejścia w problemie dokładnego dopasowania wzorca. W tym przypadku przejście zawsze zakończy się na znaku powodującym niedopasowanie, ponieważ żaden sufiks nie jest jednocześnie prefiksem ciągu. Niedopasowanie może nastąpić na węźle lub w środku krawędzi. W pierwszym przypadku do węzła dodawany jest nowy potomek, a krawędź do niego prowadząca otrzymuje etykietę S[j.. S ], gdzie j jest pozycją niedopasowania (j > i). W drugiej sytuacji w środku krawędzi wstawiany jest nowy węzeł wraz z nowym potomkiem o etykiecie S[j.. S ]. Rysunek 5 przedstawia kolejne fazy konstrukcji drzewa sufiksowego dla ciągu abcabd. W czwartym kroku dodawany jest sufiks abd. Niedopasowanie ma miejsce na trzecim znaku (c na krawędzi jest różne od d w sufiksie), w wyniku czego tworzony jest nowy węzeł (na rysunku wskazany przez strzałkę). Nowy sufiks dodawany jest n razy, przy czym dodanie jednego sufiksu wymaga najwyżej n porównań - daje to czas działania algorytmu rzędu O(n 2 ). Rysunek 5: Budowa drzewa sufiksowego ciągu ababc algorytmem naiwnym W algorytmie Ukkonen' a, w przeciwieństwie do algorytmu naiwnego, gałęzie wszystkich sufiksów budowane są równocześnie. Jednostką przyrostu" każdego kroku jest jeden znak, podczas gdy w algorytmie naiwnym jednostką przyrostu był jeden sufiks. Algorytm naiwny buduje drzewo przesuwając się w kierunku poziomym,

ponieważ każdy krok rozbudowuje drzewo o jedną całą ścieżkę od korzenia do liścia - dodając tym samym jeden sufiks. Algorytm Ukkonen'a tworzy wszystkie ścieżki równocześnie - wydłużając je o jeden znak w każdym kroku. łącznik sufiksowy (ang. suffix link) Niech xα będzie pewnym ciągiem, gdzie x oznacza jeden znak, a α podciąg o nieokreślonej długości(w tym również podciąg pusty). Jeżeli xα jest ścieżką od korzenia do węzła wewnętrznego v i istnieje inny węzeł s(v) o ścieżce α, to s(v) jest dla v łącznikiem sufiksowym. Łącznik sufiksowy jest kluczowym pojęciem algorytmu Ukkonen'a. Rysunek 6 przedstawia to samo drzewo, co rysunek 2. Zaznaczone na nim zostały węzły v i w. W tym drzewie węzeł w jest łącznikiem sufiksowym węzła v. Długość α wynosi jeden. Rysunek 6: Łącznikiem sufiksowym węzła v jest węzeł w W szczególnym przypadku, gdy α ma długość zero, to łącznikiem sufiksowym węzła wewnętrznego o ścieżce xα jest korzeń drzewa. W trakcie budowy drzewa, ciąg wejściowy czytany jest od początku do końca jeden raz. Odczyt odbywa się zawsze w kierunku końca ciągu. Konstrukcja drzewa odbywa się równocześnie z wczytywaniem kolejnych znaków. Faza i rozszerzenie są zmiennymi, które wskazują lewe (dolne) oraz prawe (górne) ograniczenie analizowanego Rozszerzenie Faza 1 2 Analizowany fragment ciągu a b c a b d 2 2 a b c a b d 2 3 a b c a b d 3 3 a b c a b d 3 4 a b c a b d 4 4 a b c a b d 4 5 a b c a b d 4 6 a b c a b d 5 6 a b c a b d 6 6 a b c a b d Tabela 2: Wartości fazy i rozszerzenia podczas przetwarzania ciągu abcabd w danej chwili fragmentu łańcucha. Faza jest ograniczeniem górnym, przechowującym wartości od 2 do S. Wartość fazy określa numer ostatnio wczytanego znaku. Rozszerzenie jest ograniczeniem dolnym i przyjmuje wartości od 1 do S. Rozszerzenie nie przyjmuje wartości większych od fazy; wielkość obydwu zmiennych nigdy nie

zmniejsza się. Tabela 2 przedstawia wartości fazy i rozszerzenia przyjmowane w trakcie przetwarzania ciągu abcabd algorytmem Ukkonen'a. Rozszerzenie, oprócz podanego wcześniej znaczenia, jest w kontekście algorytmu Ukkonen'a również elementarną operacją konstrukcji drzewa sufiksowego. Każdy wiersz tabeli 1 symbolizuje jedną operację rozszerzenia. Poniżej przedstawiony został poglądowy zapis przebiegu algorytmu Ukkonen'a w pseudokodzie: 1 stwórz drzewo złożone z korzenia 2 dodaj potomka i gałąź o etykiecie S[1] 3 LastExtension 1 4 Phase 2 to length[s] 5 do 6 for Extension LastExtension to Phase 7 do 8 znajdź koniec ścieżki o etykiecie S[Extension.. Phase-1] 9 rozszerz ścieżke 10 jeżeli reguła rozszerzenia == 3, wyskocz z pętli 11 done 12 LastExtension Extension 13 done Przykładowe wykonanie algorytmu zostanie przeanalizowane dla wejściowego ciągu znaków abcabd. Pierwszą, wstępną operacją algorytmu(linie 1, 2) jest stworzenie drzewa o jednym liściu i etykiecie z pierwszym znakiem (rys. 7). Każde wejście do pierwszej (zewnętrznej) pętli dodaje do przetwarzanego fragmentu kolejny znak. Pętla wewnętrzna dokonuje rozszerzenia drzewa i może zwiększyć o jeden zmienną rozszerzenia. Rysunek 7: Pierwsza postać drzewa ciągu abcabd Elementarna operacja rozbudowy drzewa, określana rozszerzeniem, zachodzi wewnątrz drugiej pętli (linie 8-10). Dokładny opis operacji zachodzących w tym fragmencie algorytmu znajduje się poniżej. We wcześniejszym zapisie pseudokodu, szczegóły operacji rozszerzenia przedstawione zostały jedynie w postaci komentarza. 1 znajdź koniec ścieżki o etykiecie S[Extension.. Phase-1] Pierwszym krokiem rozszerzenia, jest znalezienie końca ścieżki aktualnie analizowanego podciągu, którym jest S [Extension.. Phase-1]. Niech symbol β oznacza podciąg S[Extension.. Phase-1]. 9 rozszerz ścieżkę

W zależności od miejsca drzewa, w jakim kończy się ścieżka β, rozszerzenie następuje według jednej z trzech reguł: Reguła 1: Ścieżka β kończy się na liściu. Krawędź prowadzącą do liścia należy rozszerzyć o podciąg S[Phase.. S ] Reguła 2: Za ostatnim znakiem ścieżki β rozpoczyna się co najmniej jedna ścieżka, ale żadna ze ścieżek nie rozpoczyna się znakiem S[Phase]. W miejscu zakończenia ścieżki β należy dodać nową krawędź z etykietą S[Phase]. Jeżeli koniec ścieżki β wypadł na środek krawędzi, to najpierw należy dodać nowy węzeł, który rozwidli dalsze ścieżki. Reguła 3: Za ostatnim znakiem ścieżki β jest ścieżka rozpoczynająca się znakiem S[Phase]. W tym przypadku należy przejść do kolejnej fazy. 10 jeżeli reguła rozszerzenia == 3, wyskocz z pętli Pierwszy przebieg pętli wewnętrznej dla ciągu abcabd i pierwsze rozszerzenie drzewa na rysunku 7 zachodzi zgodnie z regułą pierwszą, ponieważ ścieżka z etykietą a, której koniec jest poszukiwany kończy się na jedynym liściu drzewa. Rysunek 8 przedstawia drzewo po pierwszym rozszerzeniu. Rysunek 8: Faza 2, rozszerzenie 1 Rysunek 9: Faza 2, rozszerzenie 2 W drugim przebiegu wewnętrznej pętli wartości Phase oraz Extension są równe 2. W linii 8 poszukiwana ścieżka S[2..1] jest ciągiem pustym. Ścieżka o etykiecie ciągu pustego rozpoczyna się i kończy na korzeniu. W analizowanym przypadku od korzenia odchodzą ścieżki, ale żadna nie rozpoczyna się znakiem S[Phase] (tutaj S[2], czyli b), więc zgodnie z regułą drugą do korzenia dodawany jest potomek o krawędzi S[Phase]. Ta operacja kończy drugą fazę. Drzewo na tym etapie konstrukcji zostało przedstawione na rysunku 9. Przebieg fazy trzeciej jest analogiczny z fazą drugą. Najpierw rozszerzany jest liść o ścieżce b, następnie dodawany jest nowy liść o ścieżce c (rys. 10). Faza czwarta rozpoczyna się rozszerzeniem ścieżki c (rys. 11). Przejście ścieżką S[4..3] kończy się na korzeniu drzewa. Jedna ze ścieżek odchodzących od

korzenia zaczyna się znakiem S[Phase] (czyli S[4], a); faza kończy się na skutek wystąpienia reguły trzeciej. Rysunek 10: Faza 3, rozszerzenie 3 Rysunek 11: Faza 6, rozszerzenie 4 Analogiczna sytuacja ma miejsce w fazie piątej, w której za ścieżką o etykiecie a jest ścieżka rozpoczynająca się znakiem b. Rozszerzenie czwarte kończy się dopiero w fazie szóstej, w której za ścieżką ab nie istnieje ścieżka rozpoczynająca się znakiem d (reguła druga). Rozszerzenia czwarte i piąte zostały przedstawione na rysunkach 11 oraz 12. Rozszerzenie szóste daje w wyniku kompletne drzewo z rysunku 1. Rysunek 12: Faza 6, rozszerzenie 5 Złożoność czasowa przedstawionego do tego momentu mechanizmu to O(n 2 ), Każdy wiersz tabeli 2 symbolizuje jeden przebieg wewnętrznej pętli algorytmu, czyli jedno rozszerzenie. W każdym rozszerzeniu zmienia się wartość zmiennej Phase lub Extension. Ponieważ żadna z tych zmiennych nie przyjmuje więcej niż S wartości, wykonanie algorytmu wymaga najwyżej 2 S rozszerzeń. Spośród opisanych wcześniej operacji składających się na jedno rozszerzenie, tylko przechodzenie ścieżki ma złożoność O(n), pozostałe operacje zajmują stały czas. Oczekiwana złożoność całego algorytmu to O(n), dlatego czas jednego rozszerzenia musi zostać zredukowany do wartości stałej. Osiągnięcie tego celu jest możliwe dzięki zdefiniowanym wcześniej łącznikom sufiksowym. Każdy wewnętrzny węzeł v o ścieżce xα posiada w drzewie sufiksowym łącznik sufiksowy s(v). Węzeł będący łącznikiem będzie dodany do drzewa najpóźniej w tej samej fazie co węzeł v.

Dowód podanego twierdzenia znajduje się w [2]. Użyte określenie najpóźniej oznacza, że węzeł s(v) może być w drzewie już w momencie dodawania węzła v. Należy zauważyć, że etykiety ścieżek, którymi należy przejść w kolejnych rozszerzeniach, zmieniają się w określonym porządku. Mianowicie, każde rozszerzenie usuwa pierwszy znak z przetwarzanego fragmentu ciągu. W analogiczny porządek układają się ścieżki wskazywane przez kolejne łączniki. Łącznik sufiksowy łączy ścieżkę o etykiecie xαα ze ścieżką α, co odpowiada zmianie etykiety ścieżki przy zwiększeniu wartości rozszerzenia o jeden. Znalezienie końca ścieżki z następnego rozszerzenia, może zatem zostać wykonane poprzez przejście przez łącznik sufiksowy, zamiast przechodzić za każdym razem od korzenia. Na przejście do kolejnej ścieżki składają się dwa kroki: 1 Znajdź pierwszy węzeł v powyżej lub na końcu przetwarzanej w danym rozszerzeniu ścieżki. 2 Jeżeli v nie jest korzeniem, przejdź na węzeł s(v), używając łącznika sufiksowego. Niech γ będzie ciągiem znaków pomiędzy końcem ścieżki w aktualnym rozszerzeniu a najbliższym węzłem powyżej tego miejsca (rys. 13). Przejście do węzła v oraz przejście łącznikiem sufiksowym do węzła s(v) zajmuje stały czas(rys. 13). Czas potrzebny na znalezienie końca ścieżki w kolejnym rozszerzeniu, jest zatem wprost proporcjonalny do γ. Poddrzewo z korzeniem w s(v) musi zawierać ścieżkę o etykiecie γ, czego dowód zostanie pominięty. Własność ta w połączeniu z faktem, że pierwsze znaki krawędzi wychodzących z węzłów są unikalne, pozwala zredukować liczbę porównań na ścieżce γ z γ do liczby węzłów występujących na tej ścieżce. Kolejna krawędź, którą należy przejść do następnego węzła wybierana jest tylko na podstawie pierwszego znaku etykiety tej krawędzi. Po przejściu krawędzi, należy przesunąć wskaźnik aktualnej pozycji ciągu γ o długość etykiety krawędzi. Czas jednego rozszerzenia zastał zredukowany do O(m), gdzie m to liczba węzłów na ścieżce γ. Złożoność O(n) w skali wszystkich rozszerzeń zapewnia następująca własność[2]: Maksymalna liczba odwiedzonych węzłów na ścieżce γ po przejściu łącznika sufiksowego w trakcie przebiegu całego algorytmu jest wprost proporcjonalna do S. Przedstawiony do tego momentu schemat budowy drzewa sufiksowego, osiąga zatem ograniczenie czasowe O(n).

Powyższy opis zostanie uzupełniony o informacje związane z mechanizmem dodawania łączników sufiksowych. Operacja jest trywialna i nie wprowadza do algorytmu żadnej dodatkowej złożoności. Rysunek 13: Wykorzystanie łącznika sufiksowego do przejścia pomiędzy kolejnymi rozszerzeniami Łącznik s(v) nowego wewnętrznego węzła będzie dodany w następnym rozszerzeniu lub już istnieje. W związku z tym, aby uzupełnić wartość łącznika nowego węzła, należy przechować między rozszerzeniami wskaźnik dodanego węzła. W kolejnym rozszerzeniu wartość łącznika zostanie ustawiona przy dodaniu (jeśli w drzewie jeszcze nie było s(v)) lub odwiedzeniu (w przypadku, gdy s(v) został dodany wcześniej) węzła-łącznika. W jednym rozszerzeniu możliwe jest stworzenie najwyżej jednego węzła wewnętrznego, dzięki czemu jeden wskaźnik jednoznacznie wskazuje właściwą strukturę węzła. 2.4 Wyznaczanie wspólnego podciągu dwóch łańcuchów Znanych jest kilka mechanizmów wyznaczania najdłuższego wspólnego podciągu używających drzew sufiksowych. Biblioteka liblcs korzysta z metody zapewniającej efektywne użycie pamięci (ang. space-efficient longest common substring algorithm). Oprócz drzewa sufiksowego, kluczowym elementem opisywanej metody jest struktura tablicy dopasowań (ang. matching statistics). Niech P, T będą ciągami znaków, przy czym P < T. M jest tablicą dopasowań długości T, zawierającą liczby całkowite. Na i-tej pozycji tablicy M zapisana jest długość najdłuższego wspólnego podciągu ciągów P i T, rozpoczynającego się na i-tej pozycji w T. Przykład: T = abcxabcdexuwxyz, P = wyabcwzgabcdw, to M[1] = 3, M[5] = 4.

Po wyznaczeniu wartości wszystkich pozycji tablicy M, znalezienie najdłuższego podciągu jest trywialne - sprowadza się do przeszukania tablicy w celu znalezienia pozycji o największej wartości. W dalszej części zostanie opisany algorytm wyznaczania wartości tablicy dopasowań. Wprowadzone prędzej definicje P, T oraz M zachowują znaczenie. Pierwszym krokiem jest przedstawienie ciągu P na drzewie sufiksowym T. Następnie, korzystając z drzewa T, wyznaczane są najdłuższe wspólne fragmenty ciągu P z kolejnymi podciągami T. Najprostszy mechanizm jest analogiczny do przedstawionej wcześniej naiwnej metody budowy drzewa sufiksowego. Polega on na przechodzeniu kolejnymi krawędziami od korzenia tak długo, jak długo kolejne znaki ciągów są sobie równe. Wypełnienie wszystkich pozycji tablicy M wymaga T przejść, a jedno przejście zajmuje czas proporcjonalny do długości wspólnego fragmentu. Wyznaczenie wspólnych podciągów na wszystkich pozycjach zajmuje zatem czas O(n 2 ). Redukcja tego czasu do O(n) nie jest skomplikowana, zakładając, że struktura drzewa posiada łączniki sufiksowe. Zamiast dokonywać kosztownego przejścia od korzenia drzewa T dla każdego podciągu T, można przejść do kolejnej gałęzi za pomocą łącznika sufiksowego. Poprawność tego mechanizmu wynika z faktu, że łącznik sufiksowy prowadzi zawsze od gałęzi xα do α. Podobnie, kolejny podciąg T różni się od poprzedniego brakiem pierwszego znaku. Schemat przejścia łącznikiem sufiksowym jest identyczny z przejściem dokonywanym przez algorytm Ukkonen'a pomiędzy kolejnymi rozszerzeniami. Aktualne pozostają tutaj własności związane z obecnością ciągu γ w następnej gałęzi oraz ograniczenie na liczbę węzłów, które trzeba przejść po pokonaniu łącznika sufiksowego - jest ona wprost proporcjonalna do T w skali całego algorytmu. Przedstawione algorytmy, pozwalają na wyznaczenie najdłuższego wspólnego podciągu dwóch ciągów znaków w czasie równym 0(n+m). m to czas budowy drzewa sufiksowego dłuższego ciągu T, n jest czasem potrzebnym do stworzenia tablicy dopasowań i przeszukania jej. 3. Porównanie bibliotek liblcs, libstree Biblioteka libstree jest implementacją algorytmu Ukkonen a. Jest ona dostępna już od dłuższego czasu i jest szeroko wykorzystywana w oprogramowaniu związanym z

bioinformatyką. Używa jej również honeycomb system automatycznej generacji sygnatur[6]. Biblioteka libstree może być wykorzystana do rozwiązania szerszej gamy problemów niż liblcs, ponieważ zaimplementowanych w niej zostało więcej algorytmów. Porównanie przedstawione w tej sekcji dotyczy jedynie aspektu znajdowania największych wspólnych podciągów. Poza tym algorytmem libstree udostępnia również algorytmy przeszukiwania drzewa oraz wyznaczania najdłuższego powtarzającego się ciągu. W pierwszej kolejności przedstawione zostaną różnice w zastosowanych algorytmach obu bibliotek, następnie testy porównujące szybkość ich działania. 3.2 Różnice algorytmiczne W obu bibliotekach drzewo sufiksowe budowane jest według algorytmu Ukkonen'a. Ogromną zaletą libstree jest fakt, że elementami ciągu mogą być dowolne struktury danych, podczas gdy liblcs ogranicza się do operowania jedynie jednobajtowymi znakami. Chociaż operowanie jednobajtowymi znakami jest rozwiązaniem wystarczającym w zastosowaniach związanych z ruchem sieciowym, jest to ograniczenie, które sprawia, że libstree jest mechanizmem dużo bardziej uniwersalnym. Przykładem może być zastosowanie tej biblioteki do poszukiwania wspólnych fragmentów obrazów, używając wartości RGB jako elementów łańcucha. Ceną uniwersalności libstree są ograniczone możliwości optymalizacji. Elementy ciągu przetwarzanego przez liblcs mogą przyjmować 256 różnych wartości. Wiedza ta pozwala na użycie tablic w implementacji dostępu do potomków węzła. W libstree węzły przechowują wskaźniki do potomków za pomocą list, w wyniku czego wybranie kolejnego potomka wymaga kosztownego przeszukania listy. Wyznaczenie wspólnego podciągu przez liblcs wiąże się z budową drzewa sufiksowego krótszego ciągu i stworzeniem tablicy dopasowań ciągu dłuższego.w libstree zastosowano inny algorytm, w którym ze wszystkich przetwarzanych ciągów budowane jest jedno uogólnione drzewo sufiksowe. Uogólnione drzewo sufiksowe jest drzewem sufiksowym reprezentującym więcej niż jeden ciąg znaków. Proces konstrukcji zachodzi osobno dla każdego łańcucha. Drzewo pierwszego łańcucha budowane jest jak drzewo nieuogólnione. Każdy kolejny łańcuch traktowany jest jakby był sufiksem ciągu wprowadzonego już do drzewa i dodawany jest według algorytmu Ukkonen'a. Rysunek 14 przedstawia uogólnione

drzewo sufiksowe ciągów S1 = xabxa i S2 = babxba. Wyznaczenie najdłuższego wspólnego podciągu na drzewie uogólnionym polega na znalezieniu najdłuższej ścieżki rozpoczynającej się w korzeniu i reprezentującej wszystkie przedstawione na drzewie łańcuchy. Rysunek 14: Uogólnione drzewo sufiksowe ciągów xabxa oraz babxba. Pierwsza liczba przy liściu oznacza numer ciągu, druga numer sufiksu w tym ciągu Zaletą użycia uogólnionego drzewa sufiksowego jest możliwość jednoczesnego porównania więcej niż dwóch łańcuchów. Budowa drzewa zajmuje czas proporcjonalny do sumy długości wszystkich porównywanych ciągów. Możliwość jednoczesnego porównywania wielu łańcuchów oferowana przez libstree nie ma większego zastosowania do analizy ruchu sieciowego (przynajmniej w formie oferowanej przez tą bibliotekę). Wynika to z faktu, iż zbudowanie jednego drzewa dla wszystkich pamiętanych strumieni danych wymagałoby ogromnych zasobów pamięci. Dodatkowo, takie drzewo musiałoby być budowane od nowa po każdej operacji zwolnienia jednego strumienia, ponieważ nie opracowano efektywnego mechanizmu usuwania łańcuchów z uogólnionego drzewa sufiksowego. Korzystające z libstree rozwiązanie honeycomb przeprowadza wszystkie operacje porównania strumieni parami. 3.3 Porównanie wydajności Wykresy załączone w tej sekcji przedstawiają porównanie czasów zmierzonych wykonując znalezienie wspólnych podciągów na łańcuchach wygenerowanych losowo. Każdy pojedynczy test polegał na wykonaniu obu algorytmów na dwóch, tych samych ciągach znaków. Działanie każdego algorytmu zaimplementowane zostało osobno, także przeprowadzenie pojedynczego porównania wymagało uruchomienia dwóch

procesów. Wszystkie testy zostały przeprowadzone na komputerze z procesorem Pentium4 2GHz i 256 MB pamięci operacyjnej. Wykres 1 przedstawia poszukiwanie wspólnego podciągu o minimalnej długości równej 5 w dwóch ciągach, każdy o długości 10000 znaków. W ramach testu przeprowadzono 1000 uruchomień, wykres pokazuje czas dla każdej próby. Wybrana wartość 10000 jest maksymalną liczbą bajtów, do której honeycomb przeprowadza składanie strumienia TCP. W konsekwencji porównywane strumienie najczęściej mają wielkości zbliżone do 10000. Wykres 1: Porównanie wydajności bibliotek na ciągach długości 10000 znaków Wykres 2: Ilość cykli używanych przy rosnącej długości zadawanych ciągów Liczba cykli zużywana przez libstree oscylowała wokół wartości 390000. liblcs rozwiązywał problem w czasie 20 do 40 razy krótszym, zużywając za każdym razem od 10000 do 20000 cykli. Wykres 2 pokazuje test, polegający na zadawaniu coraz dłuższych ciągów. Za każdym razem losowane były dwa ciągi tej samej długości. Pierwsza próba przeprowadzona została na łańcuchach długości 1000 znaków. Następne ciągi były dłuższe o 10 znaków w każdej próbie, aż do osiągnięcia wartości 100000. Liczba cykli przy największych długościach tego testu w przypadku libstree przekracza 7500000, podczas gdy liblcs oscyluje w granicach 500000. Widoczny jest niewielki spadek przewagi liblcs przy rosnącej długości argumentu, od 20-krotnej przy 10000 znakach do 15-krotnej dla ciągów długości 100000. Kolejny wykres przedstawia wyniki testu analogicznego do poprzedniego, ale przeprowadzonego na łańcuchach o długościach zwiększających się od 900000 do 1000000. Ze względu na czas potrzebny na wykonanie tego testu, skok pomiędzy kolejnymi wielkościami długości został zwiększony z 10 do 100. Na wykresie 3 widać, że liczba cykli zużywana przy przetworzeniu kolejnych ciągów rośnie zdecydowanie

wolniej, niż miało to miejsce na wykresie 2. liblcs w trakcie tego testu zużywał 12 do 14 razy mniej cykli procesora od libstree. Wykres 4 ilustruje porównanie czasów zużywanych przez dwa zasadnicze kroki mechanizmu znajdowania wspólnego podciągu: 1. zbudowanie struktury drzewa sufiksowego, 2. wyznaczenie wyniku na przygotowanej strukturze. Z wykresu wynika, że biblioteka libstree zużywa około 15 razy więcej cykli na budowę drzewa (średnio 7500000 w stosunku do 500000; test przeprowadzono na łańcuchach długości 100000 znaków). Wklejony wykres biblioteki liblcs pokazuje, że czasy potrzebne do obu operacji są w przybliżeniu takie same. Wykres 3: Porównanie wydajności bibliotek przy długich łańcuchach znaków Wykres 4: Czasy używane przez biblioteki w różnych fazach przetwarzania łańcucha Podsumowanie W pracy przedstawiony został algorytm pozwalający na szybkie wyszukiwanie wspólnych łańcuchów dwóch ciągów bajtów. Algorytm jest zaimplementowany w postaci biblioteki liblcs, która może posłużyć jako jeden z głównych modułów do budowy systemów automatycznej generacji sygnatur. Przeprowadzone testy wykazały, iż liblcs jest rozwiązaniem zdecydowanie szybszym od biblioteki libstree. Narzędzia umożliwiające automatyczną generację sygnatur są w tej chwili szeroko wykorzystywaną bronią w walce z automatycznymi intruzami. Biorąc pod uwagę wciąż rosnącą liczbę zagrożeń oraz coraz większą przepustowość łącz, optymalizacja tych narzędzi jest czynnikiem krytycznym, który wpływa na ich skuteczność. Przedstawiona biblioteka lisblcs ma szansę przyczynić się do wzrostu

wydajności tych systemów, ponieważ została zoptymalizowana właśnie pod kątem pracy z ruchem sieciowym. LITERATURA [1] Jon Crowcroft, Christian Kreibich.: Honeycomb creating intrusion detection signatures using honeypots. ACM SIGCOMM Computer Communication Review(43), 2004. [2] Dan Gusfield.: Algorithms on strings, trees and sequences. Cambridge, 1997 [3] Lance Spitzner.: Honeypots: Tracking hackers. Addison-Wesley, 2002. [4] Christian Kreibich.: libstree manual [5] Ronald R. Rivest, Thomas H. Cormen, Charles E. Leiserson.: Wprowadzenie do algorytmów. WNT, 2001 [6] http://www.cl.cam.ac.uk/~cpk25/honeycomb/ [7] Cezary Rzewuski.: System automatycznej generacji sygnatur ataków sieciowych. praca dyplomowa. Politechnika Warszawska 2006.