Skip to content

ARP and ARP spoofing: how an address is resolved, and how the answer gets forged

A forged ARP reply nobody asked for rewrites a neighbour cache, captured on a three-host lab bridge, and the next packet host A sends goes to the wrong host.

5 min read

The Address Resolution Protocol (ARP) finds the media access control (MAC) address that belongs to a known IPv4 address on the local link. Ethernet delivers a frame by MAC address; IP addresses a packet by IP address; ARP is the step that turns the second into the first.

A device that wants to reach 192.0.2.53 does not know a MAC address to put in the frame it is about to build. It has an IP address, and IP addresses mean nothing to a switch. So before the frame can exist, the sender asks the only question that matters on a local link: who has this address?

Asking, and being answered

The question goes out as a broadcast, because the sender has no unicast address to send it to yet.

text
who has 192.0.2.53? tell 192.0.2.10

Every device on the link receives it, since a broadcast is flooded to all of them. Only the one that owns 192.0.2.53 answers, and it answers directly rather than broadcasting the reply:

text
192.0.2.53 is at 00:00:5e:00:53:0b

The requester stores that pairing in its neighbour cache (the ARP cache), so the next packet to the same address skips the question entirely. On Linux, the cache is read with:

bash
ip neigh show
Wireshark showing one ARP reply packet, filtered to eth.src == 00:00:5e:00:53:0b and arp.opcode == 2. The Address Resolution Protocol layer is fully expanded: hardware type Ethernet, protocol type IPv4, opcode reply, sender MAC address 00:00:5e:00:53:0b, sender IP address 192.0.2.53, target MAC address 00:00:5e:00:53:0a, target IP address 192.0.2.10. A red callout marks the filter bar
The whole reply, one packet: 192.0.2.53 is at this MAC address, sent straight to the device that asked. Marked in red: the filter that isolated it

That reply is the entire protocol. No handshake, and no field that says which request this is answering - a reply arrives, and whatever it claims goes into the cache.

Why the reply is trusted

ARP was built to resolve addresses on a link where everyone was assumed to belong there, not to authenticate who is allowed to answer for an address. Nothing in the reply above proves that 00:00:5e:00:53:0b is really the interface with 192.0.2.53; it proves that something on the link said so, and the receiver has no way to check further than that.

That is the whole opening a machine-in-the-middle (MITM) attack needs. If any device on the link can send a reply, any device on the link can claim any address - not just its own.

The forged reply

The lab for this puts three hosts on one bridge: host A wants 192.0.2.53, which really belongs to host B, and host C sits on the same segment doing nothing it was asked to do.

bash
./arpspoof.sh
output
Terminal output of arpspoof.sh: host A resolves host B normally and the cache shows the real MAC address 00:00:5e:00:53:0b, then host C sends one ARP reply host A never asked for claiming 192.0.2.53 is at 00:00:5e:00:53:0c, and the cache now shows that MAC address instead, then host A sends toward host B again and the frame is captured arriving at host C's own interface, addressed to host B's IP
Same neighbour, same address, and the only thing that changed between the two 'lladdr' lines is one packet nobody asked for

Host C's reply is built with scapy, because it has to claim an identity the sender's own interface does not have:

python
frame = Ether(src=claimed_mac, dst=dest_mac) / ARP(
    op=2,  # reply
    hwsrc=claimed_mac, psrc=claimed_ip,
    hwdst=dest_mac, pdst=dest_ip,
)

psrc is 192.0.2.53, host B's real address. hwsrc is host C's own MAC address. Nothing else in the packet says this is a lie.

Wireshark showing the forged ARP reply, filtered to eth.src == 00:00:5e:00:53:0c. The Address Resolution Protocol layer is expanded: opcode reply, sender MAC address 00:00:5e:00:53:0c, sender IP address 192.0.2.53, target MAC address 00:00:5e:00:53:0a, target IP address 192.0.2.10. A red callout marks the filter bar
Byte for byte, the same shape as the real reply two figures up. The only difference an analyser can point to is which MAC address is now attached to 192.0.2.53

Run this against a cache that resolved seconds ago and it does nothing: Linux locks a fresh entry against a MAC change for close to a second, net.ipv4.neigh.default.locktime (measured in hundredths of a second, 99 by default). A reply sent 0.74 seconds after the baseline resolution was silently ignored; the same reply sent 1.18 seconds after it went straight into the cache. That lock is short, resets on every legitimate re-resolution, and stops nothing but a reply that arrives too eagerly. An attacker only has to wait it out.

Where the next packet goes

The cache now says 192.0.2.53 is at host C's address. Host A has no reason to doubt it, and builds its next frame accordingly.

Wireshark showing host A's own outgoing packet after the cache was poisoned, filtered to frame.number == 6. The packet list shows a normal-looking ICMP echo request from 192.0.2.10 to 192.0.2.53. The Ethernet II layer beneath it is expanded and highlighted in red: Destination 00:00:5e:00:53:0c, which is host C's address, not host B's
One layer says 192.0.2.53. The layer underneath, marked in red, says host C. Every switch and every piece of physical wire between here and host B only ever reads the second one

The IP layer still says 192.0.2.53, because host A's poisoned cache never touched that field, and neither this frame nor host A itself knows anything is wrong. The Ethernet layer is what a switch forwards on, and it now says host C. The packet was captured arriving there, not at host B:

text
00:00:5e:00:53:0a  00:00:5e:00:53:0c  192.0.2.53  8

Source host A, destination host C, still carrying an IP packet addressed to host B, still an echo request. Host B never saw it. It was never sent there.

Redirection is the goal: making the cache point somewhere the attacker controls. An attacker whose own interface is configured to forward traffic can relay each packet on to the real host after reading it, so the sender never learns anything changed - this lab stops at the redirection itself and does not build that relay. Poisoning the other side too, so the reply on the way back is intercepted as well, is the same technique run twice, once against each half of the conversation.

A widely used tool for exactly that, bettercap, does it with a command instead of a script, and by default runs the two-way version: spoofing the target and the gateway at once. The banner at the top of this page is its own log, net.probe finding host A and arp.spoof starting against it, on the same lab link and the same addresses as the rest of this post - marked in red is the exact line where it finds host A and reads back the same MAC address host A introduced itself with all the way up in the first figure. No traffic exists on this synthetic link for it to intercept, so its own "could not find spoof targets" line is left in rather than cut.

Detecting and limiting it

The tell is the same neighbour cache, read from the other direction: an address that should belong to one device suddenly belongs to another, or one MAC address turns up against IP addresses it has no reason to answer for.

bash
tcpdump -ni eth0 arp

watching the link directly shows a reply exactly like the forged one above arriving with no matching request in front of it.

None of this needs encryption to matter less. HTTPS and the Secure Shell (SSH) protocol protect what the redirected traffic contains; they do not stop the redirection. What narrows the opening:

DefenceWhat it does
Dynamic ARP InspectionA managed switch validates ARP replies against a known-good table before forwarding them
Dynamic Host Configuration Protocol (DHCP) snoopingBuilds that known-good table from DHCP activity rather than trusting ARP itself
Static ARP entriesFixes a mapping so no reply, forged or real, can change it - for the handful of hosts worth the maintenance

None of them are things ARP does for itself. All of them exist because the protocol never checks who is allowed to answer.