Tworzenie narzędzi sieciowych w Pythonie z użyciem socketów

13 marca 2014, 10:21 | Teksty | komentarzy 10
: zin o bezpieczeństwie - pobierz w pdf/epub/mobi.

Wstęp

W poprzednim wpisie poświęconym tworzeniu narzędzi w Pythonie przedstawiłem kilka prostych skryptów korzystających z modułów sys, os oraz httplib. W tym wpisie „zejdziemy” nieco niżej i zobaczymy, jak tworzyć skrypty, które komunikują się z siecią przez sockety.

Python posiada wbudowany moduł o nazwie socket, który zawiera wszystkie niezbędne definicje i funkcje pozwalające na otwieranie gniazd i komunikację sieciową z ich wykorzystaniem.

Czym są sockety (gniazda)?

Jest to pewien mechanizm umożliwiający otwarcie kanału komunikacji pomiędzy hostami. Każde gniazdo posiada adres IP oraz numer portu i może przesyłać trzy rodzaje pakietów:

  • datagramy (pakiety UDP) — są to tzw. „datagram sockets” — ten kanał komunikacji wykorzystuje protokół UDP, czyli połączenie jest bezstanowe. Gniazdko wysyła pakiet UDP do docelowego hosta/portu i nie sprawdza, czy komunikacja zakończyła się powodzeniem. Przykładem usługi działającej z użyciem UDP jest DNS (Domain Name Server, port 53);
  • strumienie (pakiety TCP) — czyli „stream sockets” — te gniazdka wysyłają dane za pomocą protokołu TCP, a zatem otwierają kanał komunikacji na określonym porcie, dane wysyłane są w pakietach TCP, gniazdko (socket) nie zamyka połączenia i oczekuje na odpowiedź od hosta docelowego. Przykładem usługi korzystającej z TCP jest HTTP (HyperText Transfer Protocol, port 80 — gdy przeglądarka internetowa chce pobrać zasób z serwera WWW, robi to właśnie poprzez zestawienie kanału komunikacji TCP na porcie 80);
  • raw sockets (IP sockets) — ostatni rodzaj to gniazdka, które są w stanie wysyłać pakiety IP z pominięciem obu przedstawionych powyżej rodzajów pakietów, które są niejako opakowaniem dla danych (UDP i TCP). Raw IP sockets pozwalają na zdefiniowanie pakietu IP z dowolnym nagłówkiem i przesłanie go dowolnie zdefiniowanym protokołem (np. ICMP).
Podobnie jak w poprzedniej części, wszystkie przykłady zostały napisane w Pythonie w wersji 2.7.6 oraz przetestowane i uruchomione na komputerze działającym pod kontrolą systemu OS X 10.9. Nie powinno być jednak problemów z uruchomieniem ich na dowolnym komputerze z zainstalowanym interpreterem Pythona.

 

Pierwszy skrypt

Przejdźmy zatem do praktyki. Na początek zobaczmy, co potrafi moduł socket — napiszemy prosty odpowiednik polecenia host, które w przypadku podania nazwy domenowej serwera zwraca nam jego adres IP.

Po zapisaniu i uruchomieniu skryptu, jako argument podajmy mu domenę serwisu Sekurak, czyli sekurak.pl. Powinniśmy otrzymać rezultat analogiczny do poniższego:

Po zaimportowaniu modułu socket [1], w funkcji getHostIP() użyliśmy jednej z jego metod, [2], by uzyskać adres IP nazwy domenowej.

Oczywiście to tylko wycinek możliwości modułu socket. Warto przyjrzeć się dostępnej  dokumentacji.

 

Architektura klient-serwer

Pora na coś bardziej złożonego. Napiszmy prostą implementację klienta, który będzie wysyłał dane przez protokół TCP, oraz serwera, który te dane odbierze. Zacznijmy od strony klienckiej:

Pierwszą rzeczą, jaką musimy zrobić, zanim zdefiniujemy gniazdo, jest wybór protokołu, w jakim będzie pracować. W tym przykładzie będzie to TCP [1]. Następnie tworzymy gniazdko [2], podając niezbędne parametry: socket.AF_INET określa, że korzystamy z adresacji IPv4, socket.SOCK_STREAM to gniazdo przesyłające dane strumieniowo, ostatnim argumentem jest wybrany wcześniej protokół.

W [3] nawiązujemy połączenie z serwerem, który będzie nasłuchiwał połączeń na porcie 2222. Roboczo możemy zastosować np. netcata i nakazać mu nasłuchiwanie na docelowym porcie:

Po uruchomieniu przykładu powinniśmy uzyskać rezultat podobny do zamieszczonego poniżej:

python-screen1

Screen 1. Górna konsola to uruchomiony skrypt Pythona z listingu powyżej; w dolnej – nasłuchujący na porcie 2222 netcat i odebrana wiadomość.

Spróbujmy teraz zastąpić netcata skryptem napisanym w Pythonie. W tym celu musimy stworzyć prosty serwer, który będzie w stanie odebrać i wyświetlić dane wysłane klientem z poprzedniego przykładu.

Podobnie jak w poprzednim przykładzie, tworzymy gniazdo TCP [1], ale tym razem, zamiast inicjować połączenie, otwieramy port 2222 [2], do którego „podpinamy” nasze gniazdo, od tego momentu nasłuchując danych, które będą zaadresowane na ten port [3]. Argument metody listen(1) to ilość równoczesnych połączeń, jaką serwer ma obsłużyć — dla naszego przykładu wystarczy jedno.

Po uruchomieniu serwera i nawiązaniu połączenia (które tworzy identyfikator conn — po tym identyfikatorze będziemy mogli odczytać dane, które nim nadeszły) odczytujemy [4] ze strumienia po 64 bajty [5] tak długo, jak pojawiają się nowe dane (w naszym przykładzie jest to tylko ciąg „Hello World!!!”, więc pętla zakończy się po pierwszej iteracji). Odebrane dane odsyłamy z powrotem, korzystając z identyfikatora połączenia conn — ponieważ klient ma zaimplementowane odbieranie danych, rezultatem będzie wyświetlenie zwróconego przez serwer ciągu w konsoli klienta (listing KLIENT, linijka [5]).

python-screen2

Screen 2. Górna konsola to nasz serwer, w dolnej – ponownie uruchomiony klient oraz odpowiedź serwera.

 

Prosta implementacja polecenia traceroute

Pora na bardziej użyteczny przykład. Poniżej znajduje się kod źródłowy implementujący funkcjonalność polecenia traceroute (traceroute(8) – Linux man page, jego odpowiednikiem w systemach z rodziny Windows jest tracert). Oczywiście jest to bardzo uproszczony odpowiednik, ale doskonale nadaje się na przedstawienie zagadnienia definiowania gniazd sieciowych.

Przykład pochodzi ze strony Learning by doing: Writing your own traceroute in 8 easy steps.

Na początek kod źródłowy:

Jak widzimy, wykorzystane zostały wszystkie poznane do tej pory mechanizmy socketów — odczytanie adersu IP z nazwy domenowej [1], utworzenie gniazd korzystających z protokołu ICMP (do wysyłania pakietów) oraz UDP [2], ustawianie opcji TTL (Time To Live) dla wysyłanych pakietów [3] (o tym akurat nie mówiliśmy, ale opis funkcji setsockopt()można znaleźć w dokumentacji modułu socket — socket.setsockopt(level, optname, value)).

Jak widać, dzięki bardzo dużej elastyczności modułu socket, jesteśmy w stanie operować na różnych typach gniazd [4] czy kierunku komunikacji [5], nie jesteśmy także ograniczeni tylko przez jeden protokół.

 

Zakończenie

Przedstawione w artykule zagadnienia to tak naprawdę tylko czubek czubka góry lodowej, jaką jest programowanie (nie tylko w Pythonie i nie tylko sieciowe). Niewątpliwą zaletą opanowania mechanizmu socketów i sposobu pisania programów, które z nich korzystają, jest fakt, że implementacje w innych językach nie różnią się zbytnio (np. C.C++), oczywiście w ogólnych założeniach. To niewątpliwie bardzo ułatwia choćby przenoszenie kodu pomiędzy różnymi platformami.

Gorąco zachęcam do nauki Pythona. Nawet, jeśli nie jesteś programistą(-tką) – umiejętność tworzenia różnych skryptów w popularnym, interpretowanym języku niewątpliwie wzbogaci warsztat każdego pentestera czy specjalisty od bezpieczeństwa teleinformatyczngo
.

 

Źródła

 

Rafał ‚bl4de’ Janicki– bloorq[at]gmail.com

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Janusz

    „(…) implementacje w innych językach nie różnią się zbytnio (np. C.C++), [b]oczwyiście[/b] w ogólnych założeniach.”

    Odpowiedz
    • bl4de

      Tak, oczywiście chodzi o samą koncepcję (tworzenie gniazd, otwieranie połączeń, zapis do/odczyt z gniazd), nie o konkretną implementację w danym języku :)

      Odpowiedz
  2. MateuszM

    Trochę niefortunnie brzmi część o „trzech rodzajach pakietów”. Chyba raczej chodzi o trzy rodzaje (lub idąc za dokumentacją typy) gniazd: dla pakietów TCP, UDP i bez wskazania konkretnego protokołu warstwy transportowej.

    Odpowiedz
    • bl4de

      @MateuszM

      Tak, dokładnie o to chodzi.

      Odpowiedz
  3. józek

    Nie lubię się powtarzać, ale…
    Ruby!
    ;)

    Odpowiedz
  4. Zabiliście pythona – rozszerzcie trochę tą ostatnią ramkę z kodem źródłowym.

    Odpowiedz
    • Obejście – zmniejszona czcionka :)

      Odpowiedz
    • bl4de

      Po skopiowaniu i wklejeniu do edytora (Sublime Text) formatowanie jest zachowane :)

      Odpowiedz
  5. Leszek

    Świetny artykuł.
    Pozdrawiam! L.

    Odpowiedz
  6. Patryk

    Świetna robota! Bardzo pomocny artykuł.

    Odpowiedz

Odpowiedz