Networks
Step 156. MITM Primer — ARP Spoofing 1: A Protocol with No ID Card
Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★☆☆ | Estimated time: 3 hours
Prerequisites: Steps 80~85’s packet basics (layers, capture, filters) complete. You can use a WSL Linux terminal.
- What you need: WSL Linux + Python venv (scapy). If you have the MS2/Kali lab, you can go through the final section’s lab procedure too — but even without it, the packet-level principles are all measured.
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- Safety limit: the packets you assemble with scapy today are never sent — you only assemble and dissect them (
.show()). The actual spoofing scene (the MS2 lab) is marked as a "Screen example." Spraying ARP replies onto any network outside your own lab is absolutely forbidden.
It’s easy to assume that identity verification in internet communication is encryption’s job — but one layer below, the protocol that asks "where physically is the device with this IP?" inside the same network has no identity verification at all. That protocol is ARP. Anyone can answer "I’m the router," and the listener believes it without checking.
Today, before firing up any attack tool, we dissect this structure of trust at the level of a single packet. Assemble a normal ARP and a forged ARP by hand with scapy, place them side by side, and you see — the two are not a hair different, which is exactly why this attack is hard to block. Only once the principle is visible does the next chapter’s traffic interception stop being "magic" and become "a packet with two fields changed."
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain the structure and fields of ARP requests (who-has) and replies (is-at)
- Explain what the ARP table (cache) is and why it’s needed, checking with
ip neigh - Explain what attack ARP’s lack of authentication makes possible
- Assemble a normal ARP reply and a forged ARP reply with scapy and compare the difference
- Draw the MITM (man-in-the-middle) structure as a triangle of victim, attacker, and gateway
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | WSL Linux (Ubuntu 24.04), Python 3 + scapy 2.7 |
| Today’s commands | ip neigh show, arp -a (Windows), scapy Ether()/ARP(), .show() |
| Concepts needed | IP address vs. MAC address, broadcast, ARP cache, MITM |
| Today’s artifact | A normal/forged ARP packet comparison note — a write-up of "why it can’t help but believe" |
2-1. IP Is a Logical Address, MAC Is a Physical Address
Inside the same network, data is actually delivered by looking at MAC addresses. An IP address is a logical address — "which device on which network" — while the actual delivery address on switches and cables is the MAC stamped into the network card. In courier terms, the IP is the street address and the MAC is the building’s unit number.
But your device knows only the other side’s IP, not its MAC. So it has to ask — and that question protocol is ARP.
2-2. ARP’s Two Messages — who-has and is-at
ARP conversation comes in just two kinds.
who-has (op=1): "Device holding 192.168.56.1, please tell your MAC to 192.168.56.101"
→ broadcast to the entire network
is-at (op=2): "192.168.56.1 is at 08:00:27:aa:bb:cc"
→ reply to the asking device
There’s a fatal design here. First, a reply has no means of proving "I really am that device." Second, even an unsolicited is-at gets written into the receiver’s table (gratuitous ARP). Third, a later reply overwrites the earlier one. Combined, these three create a structure where "whoever lies latest and most often wins."
2-3. The ARP Cache — A Table That Believes and Stores What It Hears
Asking every time would be wasteful, so the OS stores the answers it hears in the ARP cache (table). An IP → MAC mapping. If this table gets poisoned, every packet afterward goes out the wrong door — which is why the cache is both the storehouse of trust and the target of attack.
2-4. MITM — The Attack That Slips into the Middle
The structure of MITM (Man-in-the-Middle) is simple.
Normal: victim ←——————→ gateway
Spoofing: victim ←——→ attacker ←——→ gateway
Once "gateway IP = attacker’s MAC" is written in the victim’s ARP table, every packet the victim sends to the internet gets delivered to the attacker’s device first. If the attacker then forwards it to the gateway, the victim’s internet keeps working — while the attacker reads everything. Today we hand-build the raw material of this picture: the ARP packet itself.
3. Follow Along
3-1. Reading Your Own Device’s ARP Table
In a WSL terminal:
ip neigh show
Output (measured 2026-09-09, MAC partially masked):
192.168.32.1 dev eth0 lladdr 00:15:5d:xx:xx:xx DELAY
On the Windows side, arp -a shows the same table.
How to read it: one recorded line saying "IP 192.168.32.1 is at MAC 00:15:5d:…" — the mapping for the gateway as WSL sees it. lladdr (link-layer address) is the MAC address, and the state after it (DELAY, STALE, REACHABLE, etc.) is the freshness of this record. An attacker swapping out one line of this table is today’s subject.
3-2. Assembling a Normal ARP Request — who-has
With scapy, build a request packet asking "tell me the MAC of 192.168.56.1." Assemble only — do not send.
from scapy.layers.l2 import Ether, ARP
req = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(op=1, pdst="192.168.56.1")
req.show()
Output (measured 2026-09-09, MAC partially masked):
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = 00:15:5d:xx:xx:xx
type = ARP
###[ ARP ]###
hwtype = Ethernet (10Mb)
ptype = IPv4
op = who-has
hwsrc = 00:15:5d:xx:xx:xx
psrc = 192.168.39.82
hwdst = 00:00:00:00:00:00
pdst = 192.168.56.1
How to read it: look at three parts. ① The Ethernet envelope — dst=ff:ff:ff:ff:ff:ff is a broadcast, "to everyone on the network." ② op=who-has — the mark that this is an "asking packet." ③ The four address fields — hwsrc/psrc (my MAC/IP) and hwdst/pdst (the other’s MAC/IP). The other’s MAC is unknown, so it sits empty as 00:00:00:00:00:00. Broadcasting to fill in that blank slot — that’s an ARP request. (The address in psrc was auto-filled by scapy from my device’s address — it differs per environment.)
3-3. Assembling a Normal ARP Reply — is-at
Build a normal reply saying "192.168.56.1 is at 08:00:27:aa:bb:cc" (all addresses are fictional example values).
rep = ARP(op=2, psrc="192.168.56.1", hwsrc="08:00:27:aa:bb:cc",
pdst="192.168.56.101", hwdst="08:00:27:11:22:33")
rep.show()
Output (measured 2026-09-09):
###[ ARP ]###
hwtype = Ethernet (10Mb)
ptype = IPv4
op = is-at
hwsrc = 08:00:27:aa:bb:cc
psrc = 192.168.56.1
hwdst = 08:00:27:11:22:33
pdst = 192.168.56.101
How to read it: op=is-at is the mark of a reply. The core is the pair psrc (IP) and hwsrc (MAC) — a single-sentence claim: "this IP is at this MAC." The receiving device writes this claim into its cache without verification.
3-4. Assembling a Forged Reply — Change One Field and It’s an Attack Packet
Now the attacker (MAC aa:bb:cc:dd:ee:ff) builds a false reply saying "the gateway is me."
fake = ARP(op=2, psrc="192.168.56.1", hwsrc="aa:bb:cc:dd:ee:ff",
pdst="192.168.56.101", hwdst="08:00:27:11:22:33")
fake.show()
print("normal:", rep.psrc, "->", rep.hwsrc)
print("forged:", fake.psrc, "->", fake.hwsrc)
print("size:", len(rep), "bytes /", len(fake), "bytes")
Output (measured 2026-09-09):
normal: 192.168.56.1 -> 08:00:27:aa:bb:cc
forged: 192.168.56.1 -> aa:bb:cc:dd:ee:ff (IP is the gateway, MAC is the attacker)
size: 28 bytes / 28 bytes
How to read it: the forged reply differs in exactly one field, hwsrc — the IP points to the gateway but the MAC is the attacker’s. Structure, size, and shape are all identical to the normal packet. That one field is the whole of ARP spoofing. Place the two side by side and you see immediately why the receiving device has no means of telling them apart.
3-5. Why "Verification" Is Impossible — From the Protocol’s Perspective
A question should arise: "can’t the receiver just verify?" But ARP has no material to verify with. No signature, no password, no session in a reply. It even accepts replies it never asked for. This isn’t a bug — it’s a 1982 design, a protocol built on the premise that everyone inside the same LAN is a trustworthy colleague. The moment that premise breaks (the moment an attacker enters the same network), the protocol is defenseless.
This is why defense isn’t hard so much as it must live outside the protocol — Dynamic ARP Inspection (DAI), static ARP entries, and anomaly detection of "two MACs fighting over one IP" (the ARP edition of Step 84’s ‘answer without a pair’) are those outside devices.
3-6. (If You Have the Lab) One-Direction Spoofing Demo — Screen Example
If MS2 and Kali are on the same Host-only network, the actual poisoning scene goes like this. Below is a Screen example — how it looks when reproduced in your lab; no packets were transmitted in this book’s measurement environment.
# From Kali — keep telling the victim (MS2) "the gateway is me"
sudo arpspoof -i eth0 -t 192.168.56.101 192.168.56.1
# arp -a on MS2 before the attack (Screen example)
gateway 192.168.56.1 → 08:00:27:aa:bb:cc
# arp -a during the attack
gateway 192.168.56.1 → 08:00:27:cc:dd:ee ← changed to Kali's MAC
This one-line change — the MAC bound to the gateway IP becoming the attacker’s — is the result of the forged packet you hand-built in 3-4 being written into the victim’s cache. The tool (arpspoof) merely auto-repeats the packet we assembled. The moment the tool’s magic disappears.
4. Missions & Exercises
Mission — Writing the ARP Comparison Note
- Read your device’s ARP cache with
ip neigh show(or Windowsarp -a) and copy the gateway row into your note - Assemble a who-has request with scapy, and annotate what each of the four address fields (hwsrc/psrc/hwdst/pdst) is
- Assemble a normal is-at and a forged is-at side by side, and answer in one line which field differs
- Describe in terms of packet flow "what happens after a forged reply is written into the victim’s cache" — where packets that should go where actually end up going
- One paragraph from the defense perspective: "to detect or block this attack, what must you watch or configure?"
Exercises
Exercise 1. Explain the role difference between IP addresses and MAC addresses, from the perspective of delivery inside the same network.
Exercise 2. Explain each of the three design reasons why a device receiving an ARP reply (is-at) believes it immediately (no authentication, unsolicited replies accepted, overwriting).
Exercise 3. Write how the fact that the normal reply and forged reply in 3-4 are identical at 28 bytes connects to "why detection is difficult."
Exercise 4. Why must the attacker send the forged reply repeatedly, not just once? (Hint: the genuine device also sends replies; cache freshness.)
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answer
Example comparison note for item 3 (based on the 2026-09-09 measurement):
[ARP packet comparison]
Normal is-at: psrc=192.168.56.1 / hwsrc=08:00:27:aa:bb:cc ← IP and MAC match
Forged is-at: psrc=192.168.56.1 / hwsrc=aa:bb:cc:dd:ee:ff ← IP unchanged, only MAC is the attacker's
Differing field: hwsrc (responder's MAC) only. op, addressing, size (28B) all identical.
Conclusion: the receiving device has no protocol-level means of telling the two apart.
Example narrative for item 4: "Once ‘gateway IP = attacker MAC’ is written in the cache, packets the victim sends to the internet have the attacker as the destination MAC in the Ethernet envelope, so they’re physically delivered to the attacker’s device. If the attacker has forwarding on, packets are relayed on to the gateway so communication looks normal — but the contents pass through the attacker."
Example for item 5: "Monitor whether two MACs alternately claim the same IP (ARP monitoring), pin important devices (the gateway) with static ARP entries, use the switch’s Dynamic ARP Inspection (DAI). Fundamentally, eliminating plaintext protocols so that visible content is still safe (HTTPS) is the complementary defense."
How to verify: ① are the annotations of who-has’s four address fields accurate (including why hwdst is empty)? ② did you answer "the only differing field is hwsrc" in the normal/forged comparison? ③ does the flow narrative contain the causal chain "the destination MAC changes, so delivery changes"?
Exercise Answers
Answer 1. An IP address is a logical destination that crosses networks, while delivery on the final leg (inside the same LAN) is handled by the Ethernet frame’s MAC address. Crossing a router, the IP stays but the MAC changes at each leg — which is why a procedure (ARP) is needed to ask "what’s the MAC for this IP?" inside the same network.
Answer 2. First, no authentication — a reply has no cryptographic device proving "I’m the real one." Second, unsolicited replies accepted — gratuitous ARP: even when nobody asked, announcing "my MAC changed" gets written into the cache. Third, overwriting — when a new reply arrives for the same IP, the old record is replaced without verification. Combined, they create a structure where "the latest lie wins."
Answer 3. Because at the packet level there’s no marker at all — no flag, no size difference, no anomalous field — that distinguishes normal from forged. Detection can’t be done by looking at one packet; it must be done by looking at context — history comparisons like "this IP used to be that MAC," the frequency of unsolicited replies, and so on. In other words, detection is the job of observation (monitoring), not of the protocol.
Answer 4. Because the genuine gateway also sends normal replies, and when such a reply overwrites the cache, the poisoning clears (natural recovery to normal). Cache entries also expire (STALE) over time and get re-asked. So spoofing keeps repeating forged replies to "keep my lie last at all times" — the field form of 2-2’s "latest and most frequent" rule.
Completion Criteria Checklist
- [ ] I can read the ARP cache with
ip neighorarp -a - [ ] I can explain the difference between who-has and is-at (the op field)
- [ ] I know the ARP packet’s four address fields (hwsrc/psrc/hwdst/pdst)
- [ ] I assembled normal and forged replies with scapy and confirmed the difference
- [ ] I can explain why a forged packet is indistinguishable from a normal one
- [ ] I can draw the MITM triangle (victim ↔ attacker ↔ gateway)
- [ ] Mission: ARP comparison note complete
6. Common Pitfalls & Fixes
Wall 1. scapy is missing — ModuleNotFoundError: No module named 'scapy'
Cause: you ran a Python that doesn’t have scapy installed.
Fix: use this book’s lab venv (e.g., /opt/codex-security-lab/venv/bin/python), or install with pip install scapy. Making which python3 your first check is the prevention for this wall.
Wall 2. The psrc in .show() differs from what I typed
Symptom: the request packet’s psrc is filled with a strange address (my network address).
Cause: scapy auto-completes empty fields with your device’s real values — that’s how 192.168.39.82 got filled in 3-2 (measured 2026-09-09).
Fix: it’s not a bug; it’s kindness. If you want it pinned for a demo, specify it explicitly, like ARP(op=1, psrc="...", hwsrc="...", pdst="...").
Wall 3. ip neigh is completely empty
Cause: the cache is created only when there’s communication and is erased over time. On a quiet device, an empty table is normal.
Fix: send one ping to the gateway and look again — records are born only after a conversation. Understanding that "the cache is a trace of conversations" is itself the concept.
Wall 4. I feel the urge to actually send the packets
Cause: curiosity is a good thing. The boundary is the problem.
Fix: run transmission experiments only inside your isolated lab (MS2 and Kali inside a VirtualBox Host-only network). Spraying ARP replies onto a home router, company network, or dorm network seizes the communications of every device in that space — without permission, it’s a crime. Today’s assemble-and-dissect measurements alone confirm the entire principle.
Wall 5. I confuse ARP with DNS
Symptom: you know "ARP converts addresses" but mix it up with DNS.
Cause: both use the word "convert."
Fix: the direction of conversion differs — DNS converts names → IPs (a phone book); ARP converts IPs → MACs (finding the room inside the same building). DNS asks anywhere on the internet; ARP works only inside the same network.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| ARP | The protocol asking IP→MAC inside the same network — no ID card |
| who-has / is-at | ARP’s two messages — a broadcast question and an unauthenticated answer |
| ARP cache | A table storing heard answers — once poisoned, every packet goes out the wrong door |
| gratuitous ARP | An unsolicited self-introduction — accepted without verification |
| ARP spoofing | Swapping one cache line with a forged is-at — the only changed field is hwsrc |
| MITM | An attack that slips between victim and gateway and plays the relay |
Today’s Commands & Tools
| Command/tool | What it does |
|---|---|
ip neigh show |
Reads the Linux ARP cache |
arp -a |
Reads the Windows ARP cache |
Ether()/ARP() + .show() |
Assemble/dissect ARP packets (transmission forbidden) |
op=1 / op=2 |
who-has / is-at discriminator |
arpspoof -i eth0 -t victim gateway |
Lab-only spoofing tool (Screen example only) |
An Instinct More Important Than Commands
Today’s key scene is that comparison: the normal packet and the forged packet standing side by side at 28 bytes. An "attacker’s packet" isn’t a special object — it’s the exact same mold as a normal packet with one field different, as is true of so many attacks in security. Which is why defense isn’t looking at packets but looking at context: which MAC did this IP used to have, who asked for this reply, is the frequency normal. And remember — the license to use this technique is a boundary line called "my lab," and fully understanding the principle inside that boundary was today’s goal.
Once every box is checked, Step 156 is complete.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.