Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
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):
securitum-t1:~# scapy Welcome to Scapy (2.1.0) >>> ls() ARP : ARP ASN1_Packet : None 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ą:
>>> 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:
>>> p = ICMP()
Wyświetlenie szczegółów pakietu
>>> 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:
>>> 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.
>>> 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():
>>> 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):
>>> 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():
>>> 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:
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.
>>> 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:
>>> 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():
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.:
>>> 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:
>>> 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()
>>> 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ą:
i[IP].chksum=None
Odczytanie podsłuchanych pakietów ze zrzutu w formacie libpcap jest równie proste:
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:
>>> 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:
>>> 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:
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ą:
>>> 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):
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