Mega Sekurak Hacking Party w Krakowie! 26-27.10.2026 r.
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
