Chapter 19

Scapy Fundamentals

Scapy is a Python library for packet manipulation — building raw packets from scratch, reading and iterating PCAP files at the byte level, dissecting protocols by layer, and crafting custom dissectors for unknown protocols. It's the tool you reach for when tshark can't decode a protocol, when you need byte-level packet manipulation, or when you need to craft test packets for your detection stack.

Scenario

You've captured traffic from a C2 session that uses a custom binary protocol on port 9443. tshark shows it as raw TCP — no protocol dissection. You need to reverse-engineer the protocol from the packet bytes. Scapy lets you iterate every packet, print the raw payload bytes, look for patterns (length prefixes, magic bytes, command opcodes), and then write a custom Scapy dissector that decodes the protocol for future analysis.

Reading PCAP Files with Scapy

Pythonscapy-read-pcap.py
from scapy.all import rdpcap, IP, TCP, UDP, DNS, Raw
from scapy.layers.http import HTTPRequest, HTTPResponse
import binascii

# Read a pcap file — returns a list of packets
packets = rdpcap("capture.pcap")
print(f"Total packets: {len(packets)}")

# Iterate packets
for pkt in packets:
    # Check if packet has specific layers
    if pkt.haslayer(IP):
        src = pkt[IP].src
        dst = pkt[IP].dst
        ttl = pkt[IP].ttl

    if pkt.haslayer(TCP):
        sport = pkt[TCP].sport
        dport = pkt[TCP].dport
        flags = pkt[TCP].flags
        seq = pkt[TCP].seq
        ack = pkt[TCP].ack

    # Access raw payload (bytes after all decoded headers)
    if pkt.haslayer(Raw):
        payload = bytes(pkt[Raw].load)
        print(f"Raw payload ({len(payload)} bytes): {payload[:32].hex()}")

# Filter packets by layer
syn_packets = [p for p in packets if p.haslayer(TCP) and p[TCP].flags & 0x02]
print(f"SYN packets: {len(syn_packets)}")

# Filter by field value
http_packets = [p for p in packets if p.haslayer(TCP) and p[TCP].dport == 80]

# Access DNS queries
dns_queries = []
for pkt in packets:
    if pkt.haslayer(DNS) and pkt[DNS].qr == 0:  # 0 = query, 1 = response
        for i in range(pkt[DNS].qdcount):
            dns_queries.append(pkt[DNS].qd.qname.decode().rstrip('.'))

print(f"DNS queries: {len(dns_queries)}")
for q in sorted(set(dns_queries))[:20]:
    print(f"  {q}")

Writing a Custom Dissector

Pythonscapy-custom-dissector.py
from scapy.all import *
from scapy.fields import *

# Scenario: custom C2 protocol on port 9443
# Reverse engineered structure:
#   Byte 0:    magic byte (0xCA) - identifies protocol
#   Byte 1:    message type (0x01=check-in, 0x02=cmd, 0x03=data, 0x04=ack)
#   Bytes 2-3: payload length (big-endian uint16)
#   Bytes 4-N: payload

class C2Protocol(Packet):
    name = "C2Protocol"
    fields_desc = [
        ByteField("magic", 0xCA),
        ByteEnumField("msg_type", 0, {
            0x01: "check-in",
            0x02: "command",
            0x03: "data",
            0x04: "ack"
        }),
        ShortField("payload_length", 0),
        StrLenField("payload", b"", length_from=lambda x: x.payload_length)
    ]

    def mysummary(self):
        return f"C2 [{self.msg_type.name}] len={self.payload_length}"

# Register dissector for port 9443
bind_layers(TCP, C2Protocol, dport=9443)
bind_layers(TCP, C2Protocol, sport=9443)

# Now read the capture with the custom dissector active
packets = rdpcap("c2_traffic.pcap")
for pkt in packets:
    if pkt.haslayer(C2Protocol):
        c2 = pkt[C2Protocol]
        print(f"[{pkt[IP].src}→{pkt[IP].dst}] type={c2.msg_type} "
              f"len={c2.payload_length} payload={c2.payload[:20]}")

# Analyze C2 check-in frequency
import time
checkins = []
for pkt in packets:
    if pkt.haslayer(C2Protocol) and pkt[C2Protocol].msg_type == 0x01:
        checkins.append(float(pkt.time))

if len(checkins) > 1:
    intervals = [checkins[i+1]-checkins[i] for i in range(len(checkins)-1)]
    mean_interval = sum(intervals) / len(intervals)
    import statistics
    if len(intervals) > 1:
        cv = statistics.stdev(intervals) / mean_interval
        print(f"\nCheck-in analysis: {len(checkins)} check-ins")
        print(f"Mean interval: {mean_interval:.1f}s  CV: {cv:.3f}")
        if cv < 0.2:
            print("LOW CV — consistent beaconing")

Packet Crafting for Testing

Pythonscapy-crafting.py
from scapy.all import *

# Build a TCP SYN packet with specific options (for detection stack testing)
syn = (
    Ether(dst="ff:ff:ff:ff:ff:ff") /
    IP(dst="192.168.1.1", ttl=64) /
    TCP(
        dport=443,
        sport=RandShort(),
        flags="S",
        seq=RandInt(),
        options=[
            ("MSS", 1460),
            ("SAckOK", b""),
            ("Timestamp", (int(time.time()), 0)),
            ("NOP", None),
            ("WScale", 7)
        ]
    )
)

# Craft a malformed packet (SYN+FIN) for evasion detection testing
syn_fin = (
    IP(dst="10.0.0.1") /
    TCP(dport=80, flags="SF")  # Illegal flag combination
)

# Build a PCAP of test packets
test_packets = PacketList([
    IP(dst="1.2.3.4") / TCP(dport=443, flags="S"),   # Normal SYN
    IP(dst="1.2.3.4") / TCP(dport=443, flags="SF"),  # Malformed SYN+FIN
    IP(dst="1.2.3.4") / ICMP() / (b"A" * 200),       # Large ICMP (tunnel test)
])
wrpcap("test_packets.pcap", test_packets)
print("Created test_packets.pcap for detection testing")
Why Scapy beats tshark for unknown protocol analysis

tshark's protocol dissection is only as good as its built-in dissectors — if a protocol isn't in Wireshark's library, tshark shows Raw bytes and nothing more. Scapy lets you iterate packets and write arbitrary Python to analyze the raw payload bytes: look for magic bytes, compute length-prefixed fields, group packets by conversation and reconstruct the full binary exchange. Once you've reverse-engineered the protocol structure, writing a Scapy dissector documents your understanding and makes future captures of the same protocol automatically decoded. This is the workflow for analyzing custom C2 protocols, novel malware network communications, and proprietary industrial protocols.

Q & A

Q: Scapy is very slow reading a 2 GB PCAP. Is there a faster approach?

Scapy's rdpcap() loads the entire file into memory — for a 2 GB PCAP, this allocates 2+ GB RAM and takes several minutes. Faster approaches for large files: (1) Use PcapReader (streaming iterator) instead of rdpcap(): for pkt in PcapReader("capture.pcap"): ... — this processes one packet at a time without loading the entire file. (2) Pre-filter with tshark first: extract only the packets you need into a smaller PCAP, then use Scapy on that. (3) Use pyshark or dpkt for read-only analysis — they have lower overhead than Scapy for pure parsing use cases. Scapy's strength is protocol manipulation and custom dissectors, not high-speed bulk reading. For iterating 10 million packets to extract IP addresses, tshark with -T fields -e ip.dst is 10–50x faster than Scapy.