Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

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

13 marca 2014, 10:21 | Teksty | komentarzy 10

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.

#!/usr/bin/python
#
#  uzycie: 
#  ./host.py domena
#

import socket, sys                                                  # [1]

def getHostIP(domain_name):
    ip_addr = socket.gethostbyname(domain_name)                     # [2]
    return ip_addr

if __name__ == '__main__':
    domain = sys.argv[1]
    print "Adres IP dla domeny %s to %s" % (domain, getHostIP(domain))

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

$ ./host.py sekurak.pl
Adres IP dla domeny sekurak.pl to 178.32.219.59

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:

#!/usr/bin/python
#
# KLIENT
#
import socket

def sendPacket():
    proto = socket.getprotobyname('tcp')                             # [1]
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto)     # [2]
    try:
        s.connect(("127.0.0.1", 2222))                               # [3]
        s.send("Hello world")                                        # [4]

        resp = s.recv(1024)                                          # [5]
        print resp
    except socket.error:
        pass
    finally:
        s.close()

if __name__ == '__main__':
    sendPacket()

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:

        $ nc -l 2222

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.

#!/usr/bin/python
#
# SERWER
#

import socket

def server():
    proto = socket.getprotobyname('tcp')                             # [1]
    serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto)     

    serv.bind(("localhost", 2222))                                   # [2]
    serv.listen(1)                                                   # [3]
    return serv

serv = server()

while 1:
    conn, addr = serv.accept()                                       # [4]
    while 1:
        message = conn.recv(64)                                      # [5]
        if message:
            conn.send('Hi, I am a server, I received: ' + message) 
        else:
            break
    conn.close()

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:

#!/usr/bin/python

import socket,sys

def main(dest_name):
    dest_addr = socket.gethostbyname(dest_name)                                 # [1]
    port = 33434
    max_hops = 30
    icmp = socket.getprotobyname('icmp')                                        
    udp = socket.getprotobyname('udp')                                          
    ttl = 1                                                                     
    while True:
        recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)      # [2]
        send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, udp)     
        send_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl)               # [3]
        recv_socket.bind(("", port))                                            # [4]
        send_socket.sendto("", (dest_name, port))                               # [5]
        curr_addr = None
        curr_name = None
        try:
            _, curr_addr = recv_socket.recvfrom(512)
            curr_addr = curr_addr[0]
            try:
                curr_name = socket.gethostbyaddr(curr_addr)[0]
            except socket.error:
                curr_name = curr_addr
        except socket.error:
            pass
        finally:
            send_socket.close()
            recv_socket.close()

        if curr_addr is not None:
            curr_host = "%s (%s)" % (curr_name, curr_addr)
        else:
            curr_host = "*"
        print "%d\t%s" % (ttl, curr_host)

        ttl += 1
        if curr_addr == dest_addr or ttl > max_hops:
            break

if __name__ == "__main__":
    host = sys.argv[1]
    main(host)

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