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

Generator pakietów scapy

23 kwietnia 2013, 09:43 | Narzędzia | 0 komentarzy

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

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



Komentarze

Odpowiedz