Scapy – Network Packet Toolkit
Welkom terug bij het 2e deel van deze Scapy post. In het eerste deel hebben we al erg veel geleerd over Scapy. We weten wat de applicatie kan en hoe deze globaal is opgebouwd. In deze post gaan we een veel gedetailleerdere kijk nemen binnen het Scapy framework. Na deze post moeten je meeste vragen beantwoord zijn en kun je zelf gemakkelijk je eigen netwerkpakketten schrijven voor diverse doeleinden. Type je mee?
Op het einde van de vorige post hebben we geleerd om verkeer te sniffen en om de details van gesnifte pakketten te bekijken.
Soms kan het handig zijn om je gesnifte verkeer weg te schrijven naar een PCAP bestand voor vastlegging / latere analyse. PCAP bestanden zijn prima te analyseren in andere tools zoals b.v. Wireshark. Om het sniffed verkeer weg te schrijven gebruiken we het “wrpcap” commando:
wrpcap("sniffed-capture.pcap", capture) |
Om een PCAP bestand in te lezen gebruiken we het “rdpcap” commando. Eerst defieren we een variabele waar we de PCAP naartoe lezen en dan lezen we de PCAP in:
capture2=rdpcap("/home/user/sniffed-capture.pcap") |
Je kun took meteen je sniffed verkeer openen in Wireshark met het “wireshark” commando.
wireshark(capture) |
Tot zover het werken met de Scapy sniff functie. Op dit moment moet het duidelijk zijn hoe een pakket opgebouwd is. Met deze informatie kunnen we natuurlijk ook zelf pakketten maken.
Scapy, maak je eigen pakket
In het vorige hoofdstuk zag je dat we 10 pakketten ontvangen (gesniffed) hebben en deze in de “capture” variabele hebben gestopt. Stel je voor dat je pakket 1 (0) wilt namaken dan kent Scapy hiervoor een handig commando, namelijk de “command” functie:
capture[0].command() |
Om het eerste pakket zelf na te maken moet je dus het commando gebruiken welke begint met “ETHER(src=” en eindigt op “\\x00″)”.
Maar, laten we simpel beginnen.
Een IP pakket bestaat dus uit lagen. In het vorige hoofdstuk zagen we ook 3 lagen, namelijk als eerste de Ether, gevolgd door de IP en tenslotte te TCP laag. Binnen Scapy scheiden we lagen met de “/” operator. Om dus een pakket met 3 lagen te maken die alle 3 bestaan uit hun default values gebruiken we:
Ether()/IP()/TCP() |
Laten we deze eens in een variabele stoppen. Deze variabele bestaat dus uit het pakket dat we gaan maken. We noemen de variabele “tosend”:
tosend=Ether()/IP()/TCP() |
We kunnen de details van het zojuist aangemaakt pakket weer bekijken zoals we geleerd hebben, met het “show” commando:
tosend.show() |
We kunnen de waardes nu prima definieren door ze aan te reopen:
tosend[TCP].sport=3389 |
We kunnen zelfs alle waardes verwijderen welke bestaan uit de default waardes (dus we houden alleen de custom ingestelde waardes over):
tosend[TCP].hide_defaults |
We kunnen ook gemakkelijk een referentie maken naar dit pakket. Laten we de referentie “tosend2” noemen:
tosend2=(tosend) |
Bovenstaande commando maakt een nieuwe referentie (tosend2) aan met dezelfde waardes van het “tosend” pakket. Als je een van beide pakketten aanpast dan zullen de waardes meteen doorgezet worden naar de andere variabele (deze zijn dus altijd gelijk).
Om een copy te maken naar een andere variabele welke volledig uniek is en naar eigen wens bewerkt kan worden gebruiken we de “copy.deepcopy” functie:
tosend2=copy.deepcopy(tosend) |
Maar dat was even een zijstap. We gaan nog een paar velden invullen en daarna gaan we ons pakket verzenden:
tosend[IP].src="192.168.100.20" tosend[IP].dst="10.180.32.40" tosend[TCP].dport=8890 |
Nu we ons pakketje klaar hebben is het tijd om deze te verzenden. Om een pakket te verzenden hebben we natuurlijk ook weer een paar opties, zoals:
- send = Verzend een gedefinieerd pakket over layer 3.
- sendp = Verzend een gedefinieerd pakket over layer 2.
- sr() = Verzend 1 of meerdere pakketten en ontvang de antwoorden (beantwoorde en niet beantwoorde pakketten). De sr() moet worden gebruikt als je een antwoord terug verwacht,
- sr1() = Dit is een variant op de sr() functie. De sr1() functie mag alleen gebruikt worden bij een layer3 pakket en wordt gebruikt om het pakket te retourneren welke antwoorde op het verzendende pakket. Deze functie wacht in tegenstelling van sr() slechts op 1 antwoord.
- srp() = De srp() functie komt overeen met de sr() functie maar dan voor layer 2 pakketten. De p op het einde van het commando geeft aan dat het over layer 2 gaat.
- srp1() = De srp1() functie komt overeen met de sr1() functie maar dan voor layer 2 pakketten.
- srloop() = Verzend een layer 3 pakket en als er een antwoord is ontvangen wordt het pakket weer verzonden (in een loop).
- srploop() = Idem als “srloop()” maar dan voor layer 2. Dus verzend een layer 2 pakket en als er een antwoord is ontvangen wordt het pakket weer verzonden (in een loop).
- srflood() = flood en ontvang pakketten op laag 32
- srpflood() = flood en ontvang pakketten op laag 3
Om simpelweg het pakket te verzenden kunnen we dus het “send” commando gebruiken:
send(tosend) |
We kunnen ook een volledig pakket definiëren in het send commando:
send(Ether(src="32:4D:B8:54:A1:5B:6C:8A", dst="22:11:32:84:B8:4A:AA:22")/IP(src="192.168.100.20", dst="10.180.32.40")/TCP(sport=3389, dport=8890)) |
Laten we nu eens kijken naar het sr1() commando. We definiëren een variabele “ping” en deze variabele willen we met een ICMP layer met als destination 8.8.8.8. Wanneer we de variabele versturen met “sr1” print Scapy het pakket welke hij terug ontvangen heeft.
ping = IP(dst="8.8.8.8")/ICMP() sr1(ping) |
In bovenstaande voorbeeld zien we “Received 2 packets” staan. Dit zijn de het aantal non-responspakketten welke Scapy gesniffed heeft tijdens het wachten op een antwoord. Hoe drukker het netwerk is des te meer pakketten er ondertussen gesniffed kunnen worden. We kunnen het ICMP-pakket ook rechtstreeks definiëren in de “sr1()” functie net zoals we dat in de send() functie deden:
sr1(IP(dst="8.8.8.8")/ICMP()) |
Om het antwoord pakket in een variabele op te slaan definiëren we de variabele alvorens we het commando aanroepen:
ans = sr1(IP(dst="8.8.8.8")/ICMP()) |
Bovenstaande commando is een eenmalige ping. Om een meermalige ping te genereren kunnen we het “srloop()” commando gebruiken. We starten een “srloop()” en stellen de counter op 5 zodat het commando stopt na 5x een ontvangen pakket.
Het “sr()” commando retourneer een tuple van twee lijsten. Het eerste element is een lijst met tuples (verzonden pakket, antwoord) en het tweede element is een lijst met onbeantwoorde pakketten. Deze twee elementen zijn lijsten, maar ze zijn ingepakt door object zodat ze beter gepresenteerd kunnen worden.
Laten we eens een nieuw “sr()” commando uitvoeren:
sr(IP(dst="192.168.178.1")/TCP(dport=[80])) |
Zoals je ziet hebben we 1 TCP pakket ontvangen maar meer informatie kunnen we hier niet uithalen. De geretourneerde tuple bevat 2 lijsten zonder naam en dus kunnen we hier niet naar refereren. We moeten het commando uitvoeren waarbij we de lijsten voorzien van een naam, de eerste “antwoorden” lijst wordt “ans” en de 2e wordt “unans”. Als we dan het commando nogmaals uitvoeren kunnen we deze lijsten wel opvragen met de juiste benaming.
ans, unans = sr(IP(dst="192.168.178.1")/TCP(dport=[80])) ans.show() unans.show() |
We kunnen het “sr()” commando nog uitbreiden met verschillende parameters zoals:
- inter = Interval. Dit wil zeggen een tijdsinterval. Dit is de tijd die tussen het verzenden van pakketten zit (in seconden).
- retry = Het aantal keren dat een onbeantwoord pakket nogmaals verzonden moet worden.
- timeout = De tijd die gewacht moet worden nadat het laatste pakket verzonden is.
ans, unans = sr(IP(dst="192.168.178.1")/TCP(dport=[80]),inter=0.5, retry=2, timeout=1) |
Tot nu toe hebben we slechts 1 pakket gemaakt. Maar wat als we meerdere pakketten willen verzenden. Laten we zeggen dat we naar pakketten willen versturen met de volgende waardes:
Destination IP’s:
192.168.100.1/24
Destination poorten:
80
443
1100-1200
We gaan nu 2 variabelen definiëren, namelijk variabele “dest-ip” en “dest-port”:
dest_ip = IP(dst="192.168.100.1/30") dest_port = TCP(dport=[80,443,(1100, 1110)]) |
Zoals je in bovenstaande screenshot ziet kunnen we de seperate waardes van de variabele opvragen met het volgende commando:
[p for p in %variabele%] |
We kunnen nu combinaties maken van deze variabele, namelijk:
[p for p in %variabele1% / %variabele2%] |
En dus kunnen we nu een pakket maken welke we naar al deze IP adressen op al deze poorten sturen:
send ([p for p in %variabele1% / %variabele2%]) |
Mocht je de output van een eigen of ontvangen pakket graag bekijken in hexadecimale waardes dan gebruik je het “hexdump()” commando:
hexdump ([p for p in a]) |
Het “hexdump” commando retourneerd de hexadecimal waarde in “per-byte view”. Scapy geeft je zelfs de mogelijkheid om verschillen te bekijken tussen 2 pakketten in hexadecimale weergave, namelijk:
hexdiff (a,b) |
Geavanceerde Scapy Commando’s
Ondertussen heb je al een aardig idee van Scapy en de kracht van het compleet in controle zijn van je netwerkpakketten. Nu gaan we nog een paar oneliners bekijken waarmee Scapy’s kracht nog duidelijker wordt.
1.
send(IP(dst="10.10.100.95")/fuzz(UDP()/NTP(version=4)),loop=1) |
Bovenstaande commando stuurt een pakketje naar 10.10.100.95 en gebruikt hiervoor het “fuzz” commando. Het “Fuzz” commando zorgt ervoor dat alle standaardwaardes die niet berekend hoeven te worden door een willekeurige waarde. Denk bijvoorbeeld aan de “checksum”. In bovenstaande commando wordt de IP laag normaal verzonden maar de UDP en NTP laag worden verzonden met willekeurige waardes met als uitzondering dat de NTP laag altijd als V4 verzonden wordt. Het fuzz commando maakt het mogelijk om snel fuzzing sjablonen te maken om random data te versturen.
2.
ans, unans = sr(IP(dst="10.10.100.99", ttl=(4,25),id=RandShort())/TCP(flags=0x2)) for snd,rcv in ans: |
Bovenstaande commando is een traceroute commando. Allereerst definiëren we de “ans” (beantwoorde) en “unans” (onbeantwoorde) variabele. Vervolgens gaan we een pakket verzenden en de antwoorden ontvangen met het “sr()”. Dan definiëren we het pakket. Hier gebruiken we de “RandShort” functie welke een random integer genereerd van maximaal 3 cijfers. Variaties van de “RandShort” functie zijn o.a. “RandInt” en “RandByte”. Nadat het pakket verstuurd is en we de retourberichten ontvangen hebben vragen we de beantwoorde pakketten op welke het send (snd) en receive (rcv) kenmerk hebben uit de “ans” variabele.
Het voordeel van deze traceroute is net zoals bij iedere klassieke traceroute functie dat er bij het versturen van een pakketje gewacht word op antwoord. Scapy kent echter ook een eigen “traceroute” functie welke alle pakketten meteen verstuurd. Het voordeel is dat deze traceroute veel sneller is. Het nadeel is echter dat deze functie niet weet wanneer hij klaar is en dus moeten we een “maxttl” waarde opgeven. Het volgende voordeel is dat de Scapy tracerout functie multi-traceroutes kan uitvoeren in 1 commando:
traceroute(["www.google.com","wwwjarnobaselier.nl","10.10.100.99"],maxttl=20) |
3.
ans, unans = sr(IP(dst="10.10.100.99")/TCP(dport=53,flags="FPU") ) |
Bovenstaande scan staat bekend als een zogenaamde XMAS scan. Deze scan heeft alle flags ingesteld, dus “FIN”, “PSH” en “URG”. De Xmas-scan is een speciaal pakket. Als de poort open is op het doelsysteem worden de pakketten genegeerd en als deze gesloten is wordt er een “RST” of “No-Response” verwacht. Op deze manier kunnen OS systemen geïdentificeerd worden (Windows volgt namelijk de RFC niet helemaal) en kunnen verborgen poorten ontdekt worden. XMAS Scans waren enorm populair vanwege hun snelheid en de mogelijkheid waarmee ze stateless-firewalls en ACL-filters kunnen omzeilen. Tegenwoordig zijn ze minder effectief maar nog zeker niet helemaal vergeten. Ze zijn vergelijkbaar met een FIN scan en een NULL scan maar hebben wel een hoger risico gedetecteerd te worden.
4.
send(fragment(IP(dst="10.10.100.99")/ICMP()/("X"*60000)) ) |
Bovenstaande commando is de zogenaamde “ping-of-death”. Wat we hier doen is relatief simpel. We sturen een ICMP pakket naar een specifieke bestemming. Hierdoor ontstaat een DOS attack. Deze aanval is mogelijk omdat we gebruik maken van fragmentatie “fragment()”. Een ICMP pakket welke groter is dan de toegestane 65535 byte wordt verstuurd in fragmenten. Wanneer de ontvangen fragmenten bij ontvangst opnieuw samengevoegd worden, ontstaat een bufferoverloop, waardoor de TCP/IP-stack kan crashen. Bovenstaande commando stuurt 60000 fragmenten.
5.
send(Ether(dst=A4:B5:CC:D1:4A:5B:AA:1D)/ARP(op="who-has", psrc=gateway, pdst=client),inter=RandNum(10,40), loop=1 ) |
Het commando hierboven illustreert een ARP Cache Poisoning Attack. Een ARP Cache Poisoning Attack is een aanval waarbij een aanvaller ARP replies van devices beantwoord met een vervalst MAC-adres. Hiermee wordt het Ethernet MAC-adres in de ARP tabel van het device veranderd naar het MAC-adres van de hacker. Nu zal het device frames naar de computer van de hacker versturen i.p.v. naar het legitieme apparaat. Een effectieve ARP Cache Poison is niet detecteerbaar voor de gebruiker. We kennen ARP Cache Poison ook wel als “ARP Spoofing”. Hierboven zien we dat er een ethernet pakket gemaakt wordt met een specifiek destination MAC (van de hacker). Het ARP gedeelte heeft een “op” (opcode) veld welke definieerd wat voor soort pakket het is. In dit geval is het een “who-has” pakket ofwel een reply pakket op de vraag van een device “who has this ip address”. Het psrc is het bronadres ofwel zoals het device zichzelf voordoet. In dit geval doet de verzender van dit ARP pakket zich voor als gateway (omdat de cliënt daar om vroeg). En het “pdst” adres geeft aan wat de bestemming is voor het ARP pakket en dat is natuurlijk de cliënt. Het Ether interval (inter) veld krijgt een random waarde tussen de 10 en 40. Bovenstaande pakket doet zich dus voor als gateway met het MAC adres van de hacker.
Nawoord
Deze 2 posts hebben hopelijk een duidelijk inzicht gegeven in Scapy en de onbeperkte mogelijkheden die je krijgt als je je eigen pakketten kunt craften. In een toekomstige post zullen we Scapy gebruiken binnen Python scripts om op die manier bepaalde processen volledig te automatiseren. Hopelijk heb je het e.e.a. geleerd over netwerken en Scapy en was deze post waardevol voor je. Als je deze post leuk vond laat me dit dan even weten. Ik schrijf graag maar door jullie altijd leuke waardering blijf ik geïnspireerd om dit soort zaken op te schrijven zodat iedereen het rustig na kan lezen. Geef mijn post een “like” of deel hem op je eigen website of social media met een linkje naar het origineel. Dankjewel!!