Generator pakietów scapy

Czy istnieje łatwy sposób na wygenerowanie niemal dowolnych pakietów? Czy można bez większych trudności zmodyfikować przechwycony pakiet i wysłać go ponownie? Czy nieskomplikowanym zadaniem jest przygotowanie fuzzera protokołu sieciowego?
Na te wszystkie pytania możemy odpowiedzieć: tak – mając do dyspozycji darmowe oprogramowanie scapy. Jedną możliwości oferowanych przez to narzędzie jest generowanie pakietów – przy czym, w odróżnieniu od innych znanych generatorów pakietów (takich jak: sendip, nemesis, netdude, czy hping), scapy:
- posiada bardzo dużą bazę obsługiwanych protokołów,
- jest prosty i szybki w obsłudze,
- zapewnia użytkownikowi wysoką elastyczność podczas korzystania z oferowanej przez siebie funkcjonalności,
- jest aktywnie rozwijany,
- oferuje bardzo szerokie możliwości skryptowania,
- zapewnia relatywnie proste możliwości implementacji obsługi zupełnie nowego rodzaju pakietów i protokołów (wymagana jest przy tym jednak znajomość języka python).
Sam autor narzędzia twierdzi, że jest ono w stanie zastąpić takie aplikacje jak: hping, 85% funkcjonalności nmap-a, arpspoof, arp-sk, arping, tcpdump, tethereal czy p0f. Nie będziemy polemizować z tym twierdzeniem – zaprezentujemy za to praktyczne przykłady wykorzystania scapy, niech one same będą komentarzem do możliwości tego oprogramowania.
ls() – wyświetlenie obsługiwanych pakietów
Scapy w wersji 2.1.0 obsługuje przeszło 300 rodzajów pakietów, przy czym pakiet rozumiemy tutaj bardzo ogólnie – jako pewien odpowiednio sformatowany zbiór danych, który może być przesyłany przez sieć (stosowne formatowanie określa odpowiedni protokół sieciowy). Na listingu poniżej prezentujemy niewielki fragment listy pakietów zaimplementowanych w scapy, przy czym dostępne są dodatkowe pluginy uzupełniające tą listę (przykład – protokół routingu OSPF):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
securitum-t1:~# scapy Welcome to Scapy (2.1.0) >>> ls() ARP : ARP ASN1_Packet : None[box kolor="szary"][/box] BOOTP : BOOTP CookedLinux : cooked linux DHCP : DHCP options DHCP6 : DHCPv6 Generic Message) ... DNS : DNS DNSQR : DNS Question Record DNSRR : DNS Resource Record DUID_EN : DUID - Assigned by Vendor Based on Enterprise Number Dot11 : 802.11 Dot11ATIM : 802.11 ATIM Dot11AssoReq : 802.11 Association Request Dot11AssoResp : 802.11 Association Response ... Dot1Q : 802.1Q Dot3 : 802.3 EAP : EAP EAPOL : EAPOL Ether : Ethernet GPRS : GPRSdummy GRE : GRE ... IP : IP IPOption : None IPOption_Address_Extension : IP Option Address Extension ... PPP : PPP Link Layer PPP_ECP : None PPP_ECP_Option : PPP ECP Option PPP_ECP_Option_OUI : PPP ECP Option ... RIP : RIP header RIPEntry : RIP entry RTP : RTP RadioTap : RadioTap dummy Radius : Radius Raw : Raw RouterAlert : Router Alert STP : Spanning Tree Protocol SebekHead : Sebek header TCP : TCP TCPerror : TCP in ICMP TFTP : TFTP opcode ... |
ls() – wyświetlenie szczegółów budowy pakietu
Przed utworzeniem pakietu w scapy warto zapoznać się z jego strukturą:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> ls(ICMP) type : ByteEnumField = (8) code : MultiEnumField = (0) chksum : XShortField = (None) id : ConditionalField = (0) seq : ConditionalField = (0) ts_ori : ConditionalField = (50705671) ts_rx : ConditionalField = (50705671) ts_tx : ConditionalField = (50705671) gw : ConditionalField = ('0.0.0.0') ptr : ConditionalField = (0) reserved : ConditionalField = (0) addr_mask : ConditionalField = ('0.0.0.0') unused : ConditionalField = (0) |
W pierwszej kolumnie podawana jest nazwa każdego pola, w drugiej jego typ, a w trzeciej – wartość domyślna przy tworzeniu nowego pakietu.
Tworzenie nowego pakietu
W scapy jest to prosta operacja:
1 |
>>> p = ICMP() |
Wyświetlenie szczegółów pakietu
1 2 3 4 5 6 7 |
>>> p.show() ###[ ICMP ]### type= echo-request code= 0 chksum= None id= 0x0 seq= 0x0 |
W przykładzie powyżej jako domyślny typ ICMP został ustawiony 'echo-request' (znany z popularnego ping-a). Jeśli przy budowie pakietu chcielibyśmy podać inny typ, możemy zrobić to w sposób następujący:
1 2 3 4 5 6 7 8 9 10 |
>>> p = ICMP(type=0) >>> ICMP.type.i2s {0: 'echo-reply', 3: 'dest-unreach', 4: 'source-quench', 5: 'redirect', 8: 'echo-request', 9: 'router-advertisement', 10: 'router-solicitation', 11: 'time-exceeded', 12: 'parameter-problem', 13: 'timestamp-request', 14: 'timestamp-reply', 15: 'information-request', 16: 'information-response', 17: 'address-mask-request', 18: 'address-mask-reply'} >>> p = ICMP(type='information-request') |
Skorzystaliśmy więc z podania kodu bezpośrednio lub za pośrednictwem mnemonika. Mnemoniki dla pól typu enum możemy wyświetlić za pomocą składni: protokół.nazwa_pola.i2s
Łączenie pakietów
Jak wiemy, pełen prawidłowy pakiet ICMP, składa się z kilku warstw: ramka (na potrzebny artykułu nazywamy ją „pakietem”) warstwy drugiej modelu OSI, pakiet IP oraz pakiet ICMP. Złóżmy dwa pakiety – IP oraz wcześniej stworzony ICMP. Ramka ethernetowa zostanie dodana później – już przez scapy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
>>> p1 = IP()/p >>> p1.show() ###[ IP ]### version= 4 ihl= None tos= 0x0 len= None id= 1 flags= frag= 0 ttl= 64 proto= icmp chksum= None src= 127.0.0.1 dst= 127.0.0.1 \options\ ###[ ICMP ]### type= echo-request code= 0 chksum= None id= 0x0 seq= 0x0 |
Zauważmy, że wszystkie pola zostały wypełnione zgodnie z wartościami domyślnymi (polecenie ls(typ_pakietu)) – a dodatkowo pole IP.proto zostało wypełnione poprawnie – tj. pakiet protokołu IP w powyższym pakiecie transportuje pakiet protokółu ICMP. Zwróćmy również uwagę na pola chksum – przyjęły one wartość None. W tym przypadku oznacza to, że sumy kontrolne zostaną wyliczone na podstawie tego jak zbudujemy pakiet – a wykona to za nas scapy. Aktualną sumę kontrolną można zobaczyć wykorzystując metodę show2():
1 2 3 4 5 6 7 8 9 10 11 |
>>> i.show2() ###[ IP ]### version= 4L ... chksum= 0x7cde ... \options\ ###[ ICMP ]### ... chksum= 0xf7ff >>> |
Przy okazji wspomnijmy, że dostępne w danej klasie zmienne i metody możemy uzyskać poleceniem dir (wynik tego polecania poniżej, został skrócony w celu ułatwienia prezentacji wyników):
1 2 3 4 |
>>> dir(p) ['__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', [...] 'add_payload', 'add_underlayer', 'aliastypes', 'answers', 'build', [...] 'show', 'show2', 'show_indent', 'sprintf', 'summary', 'time', [...] |
Z kolei pomoc dotyczącą interesującej nas metody możemy uzyskać poleceniem help():
1 2 3 4 5 6 7 |
>>> help(p.show2) Help on method show2 in module scapy.packet: show2(self) method of scapy.layers.inet.ICMP instance Prints a hierarchical view of an assembled version of the packet, so that automatic fields are calculated (checksums, etc.) |
Modyfikacja zawartości pakietu
Aby zmodyfikować pole w interesującej nas warstwie, stosujemy notację: pakiet[warstwa].pole = wartość. Na przykład:
1 |
p1[IP].dst = '192.168.0.1' |
Wysłanie pakietu oraz odebranie odpowiedzi
Istnieje kilka funkcji wysyłających oraz odbierających pakiety – można je poznać wydając polecenie lsc() oraz help(nazwa_funkcji). W przykładzie poniżej wykorzystamy funkcję sr1(), która wysyła pakiety w warstwie 3 modelu OSI (tj. ramka warstwy drugiej zostanie za nas uzupełniona) oraz odbiera pierwszy pakiet odpowiedzi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
>>> r = sr1(p1) Begin emission: Finished to send 1 packets. .* Received 2 packets, got 1 answers, remaining 0 packets >>> r.show() ###[ IP ]### version= 4L ihl= 5L tos= 0x0 len= 28 id= 30733 flags= frag= 0L ttl= 64 proto= icmp chksum= 0x8106 src= 192.168.0.1 dst= 192.168.0.124 \options\ ###[ ICMP ]### type= echo-reply code= 0 chksum= 0xffff id= 0x0 seq= 0x0 ###[ Padding ]### load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
Zauważmy, że nie musieliśmy uzupełniać pakietu o ramkę ethernetową, tę pracę wykonała za nas funkcja sr1(). Jeśli chcielibyśmy jednak kontrolować dane również w warstwie 2, musielibyśmy skorzystać z funkcji srp1() oraz uzupełnić pakiet o ramkę Ethernet. Przy założeniu, że w zmiennej i zdefiniowany mamy pakiet IP()/ICMP(), dołożenie ramki ethernetowej wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
>>> i2 = Ether(dst='00:19:5b:dc:b7:ec')/i >>> i2.show() ###[ Ethernet ]### dst= 00:19:5b:dc:b7:ec src= 00:0c:29:7f:48:3f type= 0x800 ###[ IP ]### version= 4 ihl= None tos= 0x0 len= None id= 1 flags= frag= 0 ttl= 64 proto= icmp chksum= None src= 192.168.0.124 dst= 192.168.0.1 \options\ ###[ ICMP ]### type= echo-request code= 0 chksum= None id= 0x0 seq= 0x0 >>> r2 = srp1(i2) Begin emission: Finished to send 1 packets. * Received 1 packets, got 1 answers, remaining 0 packets |
Dla dalszego zobrazowania możliwości oferowanych przez scapy, prezentujemy wynik wspomnianego wyżej polecenia lsc():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
arpcachepoison: Poison target's cache with (your MAC,victim's IP) couple arping : Send ARP who-has requests to determine which hosts are up defrag : defrag(plist) -> ([not fragmented], [defragmented], defragment : defrag(plist) -> plist defragmented as much as possible dyndns_del : Send a DNS delete message to a nameserver for "name" etherleak : Exploit Etherleak flaw fragment : Fragment a big IP datagram fuzz : Transform a layer into a fuzzy layer by replacing some default values by random objects getmacbyip : Return MAC address corresponding to a given IP address hexdiff : Show differences between 2 binary strings ls : List available layers, or infos on a given layer rdpcap : Read a pcap file and return a packet list send : Send packets at layer 3 sendp : Send packets at layer 2 sendpfast : Send packets at layer 2 using tcpreplay for performance sniff : Sniff packets split_layers : Split 2 layers previously bound sr : Send and receive packets at layer 3 sr1 : Send packets at layer 3 and return only the first answer srflood : Flood and receive packets at layer 3 srloop : Send a packet at layer 3 in loop and print the answer each time srp : Send and receive packets at layer 2 srp1 : Send and receive packets at layer 2 and return only the first answer srpflood : Flood and receive packets at layer 2 srploop : Send a packet at layer 2 in loop and print the answer each time traceroute : Instant TCP traceroute tshark : Sniff packets and print them calling pkt.show(), a bit like text wireshark wireshark : Run wireshark on a list of packets wrpcap : Write a list of packets to a pcap file |
Wygenerowanie grupy pakietów
Spróbujmy tym razem wygenerować nie jeden pakiet – a całą ich grupę, mianowicie 18 pakietów ICMP o polu type przyjmującym wartości od 0 do 18 (pozostałe pola przyjmą wartości domyślne ustalone w scapy). Tym razem skorzystamy z funkcji sr() (jej nazwa to skrót od send-receive), wysyłającej w warstwie 3. Funkcja ta zwraca dwie wartości: pakiety z wysyłanego zbioru, które doczekały się odpowiedzi oraz te na które odpowiedź nie przyszła.:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
>>> pi = IP(dst='192.168.0.1')/ICMP(type=(0,18)) >>> answered,uanswered = sr(pi) Begin emission: ...*.Finished to send 19 packets. .......^C Received 12 packets, got 1 answers, remaining 18 packets >>> answered.show() 0000 IP / ICMP 192.168.0.124 > 192.168.0.1 echo-request 0 ==> IP / ICMP 192.168.0.1 > 192.168.0.124 echo-reply 0 / Padding >>> uanswered.show() 0000 IP / ICMP 192.168.0.124 > 192.168.0.1 dest-unreach network-unreachable 0001 IP / ICMP 192.168.0.124 > 192.168.0.1 source-quench 0 0002 IP / ICMP 192.168.0.124 > 192.168.0.1 redirect network-redirect 0003 IP / ICMP 192.168.0.124 > 192.168.0.1 time-exceeded ttl-zero-during-transit 0004 IP / ICMP 192.168.0.124 > 192.168.0.1 parameter-problem ip-header-bad 0005 IP / ICMP 192.168.0.124 > 192.168.0.1 1 0 0006 IP / ICMP 192.168.0.124 > 192.168.0.1 2 0 0007 IP / ICMP 192.168.0.124 > 192.168.0.1 6 0 0008 IP / ICMP 192.168.0.124 > 192.168.0.1 7 0 0009 IP / ICMP 192.168.0.124 > 192.168.0.1 router-advertisement 0 0010 IP / ICMP 192.168.0.124 > 192.168.0.1 router-solicitation 0 0011 IP / ICMP 192.168.0.124 > 192.168.0.1 echo-reply 0 0012 IP / ICMP 192.168.0.124 > 192.168.0.1 timestamp-request 0 0013 IP / ICMP 192.168.0.124 > 192.168.0.1 timestamp-reply 0 0014 IP / ICMP 192.168.0.124 > 192.168.0.1 information-request 0 0015 IP / ICMP 192.168.0.124 > 192.168.0.1 information-response 0 0016 IP / ICMP 192.168.0.124 > 192.168.0.1 address-mask-request 0 0017 IP / ICMP 192.168.0.124 > 192.168.0.1 address-mask-reply 0 |
Proste skanowanie portów
Wykorzystując wiedzę z powyższego przykładu wykonajmy prosty skaner portów. W naszym przypadku będzie to wysłanie 4 pakietów TCP z ustawioną flagą Syn, na cztery różne porty:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
>>> packets = IP(dst='192.168.0.1')/TCP(flags='S',dport=[22,23,80,515]) >>> a,ua=sr(packets) Begin emission: **..Finished to send 4 packets. *.* Received 7 packets, got 4 answers, remaining 0 packets >>> a.show() 0000 IP / TCP 192.168.0.124:ftp_data > 192.168.0.1:ssh S ==> IP / TCP 192.168.0.1:ssh > 192.168.0.124:ftp_data RA / Padding 0001 IP / TCP 192.168.0.124:ftp_data > 192.168.0.1:telnet S ==> IP / TCP 192.168.0.1:telnet > 192.168.0.124:ftp_data RA / Padding 0002 IP / TCP 192.168.0.124:ftp_data > 192.168.0.1:www S ==> IP / TCP 192.168.0.1:www > 192.168.0.124:ftp_data SA / Padding 0003 IP / TCP 192.168.0.124:ftp_data > 192.168.0.1:printer S ==> IP / TCP 192.168.0.1:printer > 192.168.0.124:ftp_data SA / Padding >>> for req,resp in a: ... print "port = "+str(req.dport) ... resp.sprintf('%TCP.flags%') ... port = 22 'RA' port = 23 'RA' port = 80 'SA' port = 515 'SA' |
W pętli powyżej przeglądamy całą zwróconą tablicę o nazwie a. Każdy element tej tablicy posiada dwa kolejne elementy – pakiet wysłany, oraz otrzymana na niego odpowiedź (w pętli odpowiadają tym elementom zmienne req oraz resp). Jeśli odpowiedź jest pakietem TCP z flagą SA, oznacza to że badany port jest otwarty (otrzymaliśmy segment TCP z flagami SA w odpowiedzi na segment z flagą S).
Podsłuchiwanie ruchu oraz modyfikacja komunikacji sieciowej
Aby podsłuchać ruch, możemy skorzystać z wbudowanej w scapy funkcji sniff(), możemy również wczytać zbiór pakietów w formacie libpcap – funkcja rdpcap()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
>>> res = sniff() ^C>>> res.show() 0000 Ether / IP / TCP 192.168.0.124:ssh > 192.168.0.107:3242 PA / Raw 0001 Ether / IP / TCP 192.168.0.107:3242 > 192.168.0.124:ssh A 0002 Ether / ARP who has 192.168.0.1 says 192.168.0.124 0003 Ether / ARP is at 00:19:5b:dc:b7:ec says 192.168.0.1 / Padding 0004 Ether / IP / UDP / DNS Qry "www.onet.pl." 0005 Ether / IP / UDP / DNS Ans "213.180.146.27" 0006 Ether / IP / ICMP 192.168.0.124 > 213.180.146.27 echo-request 0 / Raw 0007 Ether / IP / ICMP 213.180.146.27 > 192.168.0.124 echo-reply 0 / Raw 0008 Ether / IP / UDP / DNS Qry "27.146.180.213.in-addr.arpa." 0009 Ether / IP / UDP / DNS Ans "s4.m1r2.onet.pl." 0010 Ether / IP / ICMP 192.168.0.124 > 213.180.146.27 echo-request 0 / Raw 0011 Ether / IP / ICMP 213.180.146.27 > 192.168.0.124 echo-reply 0 / Raw 0012 Ether / IP / UDP / DNS Qry "27.146.180.213.in-addr.arpa." 0013 Ether / IP / UDP / DNS Ans "s4.m1r2.onet.pl." 0014 Ether / IP / ICMP 192.168.0.124 > 213.180.146.27 echo-request 0 / Raw 0015 Ether / IP / ICMP 213.180.146.27 > 192.168.0.124 echo-reply 0 / Raw 0016 Ether / IP / UDP / DNS Qry "27.146.180.213.in-addr.arpa." 0017 Ether / IP / UDP / DNS Ans "s4.m1r2.onet.pl." >>> i = res[6] >>> i.show() ###[ Ethernet ]### dst= 00:19:5b:dc:b7:ec src= 00:0c:29:7f:48:3f type= 0x800 ###[ IP ]### version= 4L ihl= 5L tos= 0x0 len= 84 id= 0 flags= DF frag= 0L ttl= 64 proto= icmp chksum= 0x11b5 src= 192.168.0.124 dst= 213.180.146.27 \options\ ###[ ICMP ]### type= echo-request code= 0 chksum= 0x1dbc id= 0xa30d seq= 0x1 ###[ Raw ]### load= '\xf3R\xd8K~\x93\x02\x00\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567' >>> i[IP].dst='87.98.189.148' >>> i[IP].chksum=None >>> r = srp1(i) Begin emission: Finished to send 1 packets. .* Received 2 packets, got 1 answers, remaining 0 packets >>> r.show() ###[ Ethernet ]### dst= 00:0c:29:7f:48:3f src= 00:19:5b:dc:b7:ec type= 0x800 ###[ IP ]### version= 4L ihl= 5L tos= 0x0 len= 84 id= 40889 flags= frag= 0L ttl= 0 proto= icmp chksum= 0x44d5 src= 87.98.189.148 dst= 192.168.0.124 \options\ ###[ ICMP ]### type= echo-reply code= 0 chksum= 0x25bc id= 0xa30d seq= 0x1 ###[ Raw ]### load= '\xf3R\xd8K~\x93\x02\x00\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567' |
Zauważmy, że przechwycony przez nas pakiet IP zawiera początkowo poprawną sumę kontrolną, jednak po zmianie docelowego adresu IP, suma kontrolna będzie błędna! Dlatego aby wysłać pakiet bez błędów poprosiliśmy scapy o automatyczne wygenerowanie prawidłowej sumy za nas, podstawiając jej wartość na pustą:
1 |
i[IP].chksum=None |
Odczytanie podsłuchanych pakietów ze zrzutu w formacie libpcap jest równie proste:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
securitum-t1:~# tcpdump -n -s 0 -i eth0 -U -w out.pcap tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes ^C20 packets captured 21 packets received by filter securitum-t1:~# scapy Welcome to Scapy (2.1.0) >>> packets = rdpcap('out.pcap') >>> packets.show() 0000 Ether / IP / TCP 192.168.0.124:ssh > 192.168.0.107:3242 PA / Raw 0001 Ether / IP / TCP 192.168.0.124:ssh > 192.168.0.107:3242 PA / Raw 0002 Ether / IP / TCP 192.168.0.107:3242 > 192.168.0.124:ssh A / Padding 0003 Ether / IP / UDP / DNS Qry "www.onet.pl." 0004 Ether / IP / UDP / DNS Ans "213.180.146.27" 0005 Ether / IP / ICMP 192.168.0.124 > 213.180.146.27 echo-request 0 / Raw 0006 Ether / IP / ICMP 213.180.146.27 > 192.168.0.124 echo-reply 0 / Raw 0007 Ether / IP / UDP / DNS Qry "27.146.180.213.in-addr.arpa." 0008 Ether / IP / UDP / DNS Ans "s4.m1r2.onet.pl." 0009 Ether / ARP who has 192.168.0.124 says 192.168.0.107 / Padding 0010 Ether / ARP is at 00:0c:29:7f:48:3f says 192.168.0.124 0011 Ether / IP / ICMP 192.168.0.124 > 213.180.146.27 echo-request 0 / Raw 0012 Ether / IP / ICMP 213.180.146.27 > 192.168.0.124 echo-reply 0 / Raw 0013 Ether / IP / UDP / DNS Qry "27.146.180.213.in-addr.arpa." 0014 Ether / IP / UDP / DNS Ans "s4.m1r2.onet.pl." 0015 Ether / IP / ICMP 192.168.0.124 > 213.180.146.27 echo-request 0 / Raw 0016 Ether / IP / ICMP 213.180.146.27 > 192.168.0.124 echo-reply 0 / Raw 0017 Ether / IP / UDP / DNS Qry "27.146.180.213.in-addr.arpa." 0018 Ether / IP / UDP / DNS Ans "s4.m1r2.onet.pl." 0019 Ether / IP / TCP 192.168.0.107:3242 > 192.168.0.124:ssh PA / Raw |
Przy okazji warto również wspomnieć o funkcji wrpcap() – zapisującej pakiety w formacie libpcap. Taka możliwość może okazać się przydatna, jeśli chcielibyśmy wybrane pakiety poddać obróbce lub analizie w innym, zewnętrznym narzędziu. Poniżej prezentujemy przykład sfragmentowania komunikacji IP, z wykorzystaniem narzędzia tcprewrite, które posiada wkompilowaną obsługę mechanizmu fragroute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
>>> pkt = Ether()/IP(dst='192.168.0.1')/ICMP()/("X"*100) >>> wrpcap('to_fragment.pcap', pkt) securitum-t2:~# tcprewrite --infile=to_fragment.pcap --outfile=fragmented.pcap --fragroute=/usr/local/etc/fragroute.conf 192.168.0.104 > 192.168.0.1: icmp: type 8 code 0 (frag 1:24@0+) 192.168.0.104 > 192.168.0.1: (frag 1:24@24+) 192.168.0.104 > 192.168.0.1: (frag 1:24@48+) [delay 0.001 ms] 192.168.0.104 > 192.168.0.1: (frag 1:12@96) [delay 0.001 ms] 192.168.0.104 > 192.168.0.1: (frag 1:24@72+) [delay 0.001 ms] 192.168.0.104 > 192.168.0.1: (frag 1:24@72+) 192.168.0.104 > 192.168.0.1: icmp: type 65 code 71 (frag 1:24@0+) [delay 0.001 ms] 192.168.0.104 > 192.168.0.1: (frag 1:12@96) 192.168.0.104 > 192.168.0.1: (frag 1:24@24+) [delay 0.001 ms] 192.168.0.104 > 192.168.0.1: (frag 1:24@48+) securitum-t2:~# scapy >>> f = rdpcap('fragmented.pcap') >>> f.show() 0000 Ether / IP / ICMP 192.168.0.104 > 192.168.0.1 echo-request 0 / Raw 0001 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:3 / Raw 0002 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:6 / Raw 0003 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:12 / Raw 0004 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:9 / Raw 0005 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:9 / Raw 0006 Ether / IP / ICMP 192.168.0.104 > 192.168.0.1 65 71 / Raw 0007 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:12 / Raw 0008 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:3 / Raw 0009 Ether / 192.168.0.104 > 192.168.0.1 icmp frag:6 / Raw |
Fuzzing protokołów sieciowych
W skrócie, fuzzing protokołów sieciowych, polega na wygenerowaniu pakietów nie koniecznie zgodnych ze specyfikacją protokołu oraz sprawdzeniu w jaki sposób na taką komunikację zareaguje docelowy system. Przykładowo, niepoprawnym pakietem może być pakiet IP z ustaloną nietypową wartością w polu IP.version. Często w ten sposób wykryta może być błędna obsługa danego pakietu przez docelowy system, co niekiedy kończy się jego kompromitacją (patrz np. pracę Wifi Advanced Fuzzing @ Blackhat EU 07, pokazującą podatności umożliwiające wykonanie wrogiego kodu na bezprzewodowym punkcie dostępowym z uprawnieniami jądra systemu operacyjnego – potencjalnie nawet bez konieczności uwierzytelnienia się w urządzeniu access point!). Najprostsza postać realizacji fuzzingu w scapy wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
>>> p = IP(dst='192.168.0.1')/ICMP() >>> p = fuzz(p) >>> p.show() ###[ IP ]### version= <RandNum> ihl= None tos= 31 len= None id= <RandShort> flags= frag= 0 ttl= <RandByte> proto= icmp chksum= None src= 192.168.0.124 dst= 192.168.0.1 \options\ ###[ ICMP ]### type= <RandByte> code= 207 chksum= None unused= <RandInt> >>> send(p,loop=1) ............................................................................................................................................................................................ |
Odpowiadający takiej komunikacji, zrzut wykonany przy pomocy tcpdump-a wygląda z kolei tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
securitum-t1:~# tcpdump -v -n -i eth0 icmp tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes 18:37:00.152656 IP7 (tos 0x8b,CE, ttl 63, id 19269, offset 0, flags [+, rsvd], proto ICMP (1), length 28) 192.168.0.124 > 192.168.0.1: ICMP type-#170, length 8 IP5 [|ip] 18:37:00.156424 IP14 (tos 0x99,ECT(1), ttl 128, id 46969, offset 0, flags [+, DF, rsvd], proto ICMP (1), length 28) 192.168.0.124 > 192.168.0.1: ICMP type-#180, length 8 IP5 [|ip] 18:37:00.159799 IP9 (tos 0x7a,ECT(0), ttl 114, id 53503, offset 0, flags [DF], proto ICMP (1), length 28) 192.168.0.124 > 192.168.0.1: ICMP type-#105, length 8 IP5 [|ip] 18:37:00.163474 IP14 (tos 0x18, ttl 163, id 24203, offset 0, flags [+, rsvd], proto ICMP (1), length 28) 192.168.0.124 > 192.168.0.1: ICMP type-#61, length 8 IP5 [|ip] 18:37:00.167175 IP5 (tos 0x1,ECT(1), ttl 13, id 59219, offset 0, flags [none], proto ICMP (1), length 28) 192.168.0.124 > 192.168.0.1: ICMP type-#180, length 8 IP5 [|ip] ... |
Analogicznie do przykładu wyżej, istnieje możliwość ręcznego podstawienia danego pola, nie wartością, a funkcją, zwracającą wartość losową:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
>>> i = IP()/ICMP() >>> i[IP].version = RandShort() >>> i.show() ###[ IP ]### version= <RandShort> ihl= None tos= 0x0 len= None id= 1 flags= frag= 0 ttl= 64 proto= icmp chksum= None src= 127.0.0.1 dst= 127.0.0.1 \options\ ###[ ICMP ]### type= echo-request code= 0 chksum= None id= 0x0 seq= 0x0 |
Prosty program w pythonie
Jako ilustrację do możliwości skryptowania z wykorzystaniem scapy, przedstawiamy szkic bardzo prostego skanera portów napisanego w pythonie. Komentarzem do skryptu niech będzie sam kod źródłowy:
scapy-scanner.py (uruchomienie: python scapy-scanner.py):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from scapy.all import * #zakres portow do skanowania ports = (1,1024) #cel skanowania dst_ip = '192.168.0.1' # definicja pakietow syn_packets = IP(dst=dst_ip)/TCP(flags='S',dport=ports) # wyslanie pakietow oraz odebranie odpowiedzi answered, uanaswered = sr(syn_packets, verbose=0) # analiza odpowiedzi for request, response in answered: response_flags = response.sprintf('%TCP.flags%') if response_flags == 'SA': print "Port TCP: " + str(request.dport) + " otwarty" |
Podsumowanie
Dociekliwych czytelników zachęcamy do własnego eksperymentowania z oprogramowaniem scapy. Mamy nadzieję, że pokazaliśmy moc tego narzędzia, którego rozmaite zastosowania są ograniczone niemal tylko pomysłowością użytkownika :-)
Jednocześnie zapraszamy na nasze szkolenie bezpieczeństwa sieci i testów penetracyjnych, gdzie w ramach kilku ćwiczeń pokazujemy wykorzystanie tego potężnego narzędzia.
Dodatkowe informacje
–michal.sajdak<at>sekurak.pl