Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
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.
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:
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.
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:

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]).

Screen 2. Górna konsola to nasz serwer, w dolnej – ponownie uruchomiony klient oraz odpowiedź serwera.
traceroutePora 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ół.
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.
Rafał ‘bl4de’ Janicki– bloorq[at]gmail.com
“(…) implementacje w innych językach nie różnią się zbytnio (np. C.C++), [b]oczwyiście[/b] w ogólnych założeniach.”
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 :)
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.
@MateuszM
Tak, dokładnie o to chodzi.
Nie lubię się powtarzać, ale…
Ruby!
;)
Zabiliście pythona – rozszerzcie trochę tą ostatnią ramkę z kodem źródłowym.
Obejście – zmniejszona czcionka :)
Po skopiowaniu i wklejeniu do edytora (Sublime Text) formatowanie jest zachowane :)
Świetny artykuł.
Pozdrawiam! L.
Świetna robota! Bardzo pomocny artykuł.