ICMP Tunneling
ICMP tunneling embeds data in ICMP Echo Request and Echo Reply packets (ping). Because ICMP is a network-layer protocol not tied to TCP or UDP, many firewalls allow ICMP through without inspection. Tools like ptunnel-ng and ICMPsh implement full shell sessions over ICMP. ICMP tunneling is less common than DNS tunneling but appears in specific scenarios: airgapped or heavily firewalled environments where even DNS is blocked but ICMP is allowed for network diagnostics.
A red team exercise tests whether your detection stack catches ICMP tunneling. The attacker uses ptunnel-ng to tunnel a TCP connection over ICMP Echo Request packets. In PCAP you see: a sustained exchange of large ICMP Echo packets (1400-byte payload each, not 64 bytes like a normal ping), bidirectional between one internal host and one external IP, at a consistent rate. Normal ping: 4 packets, 64-byte payloads. ICMP tunnel: thousands of packets, 1400-byte payloads, bidirectional, sustained.
Normal ICMP vs ICMP Tunnel
ICMP Echo — Normal vs Tunneled
═══════════════════════════════════════════════════════════════════
Normal ping (Windows/Linux):
├── 4-8 packets total
├── Payload: 32-56 bytes ("abcdefghijklmnopqrstuvwxyz...")
├── Type: 8 (Echo Request) from client; 0 (Echo Reply) from server
├── Sequence number: increments 1-4
└── Bidirectional: one response per request
ICMP tunnel (ptunnel-ng, ICMPsh):
├── Many thousands of packets (sustained session)
├── Payload: 1400+ bytes (near MTU, carrying TCP data)
├── Payload entropy: high (compressed/encrypted TCP data)
├── Sequence number: large values or wraps (not 1-4)
├── Consistent timing (not human-paced ping)
└── Both directions carry data (not just echo request → reply)
ICMP types used in tunneling:
├── Type 8 (Echo Request): most common for client→server data
├── Type 0 (Echo Reply): most common for server→client data
└── Type 3 (Destination Unreachable): some tools abuse this type
Identifier field forensics:
├── Normal ping: OS-assigned (PID on Linux, random on Windows)
└── ICMP tunnel: fixed identifier used as session ID across all packets
Forensic query: ICMP packets with payload > 100 bytes, from same pair,
sustained over minutes — nearly certain to be a tunnel.
ICMP Tunneling Detection
#!/bin/bash
PCAP="$1"
echo "=== ICMP packet summary ==="
tshark -r "$PCAP" -n -Y "icmp" -T fields \
-E separator="\t" \
-e ip.src -e ip.dst -e icmp.type \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== Large ICMP payloads (>100 bytes, tunneling indicator) ==="
tshark -r "$PCAP" -n \
-Y "icmp and data.len > 100" \
-T fields \
-E separator="\t" \
-e frame.time_epoch -e ip.src -e ip.dst -e icmp.type -e data.len \
| head -50
echo ""
echo "=== Sustained ICMP sessions (same pair, many packets) ==="
tshark -r "$PCAP" -n -Y "icmp" -T fields \
-e ip.src -e ip.dst \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== ICMP Echo with non-standard payload size ==="
tshark -r "$PCAP" -n \
-Y "icmp.type==8 and (data.len < 8 or data.len > 100)" \
-T fields \
-E separator="\t" \
-e frame.time_epoch -e ip.src -e ip.dst -e data.len \
| head -30
echo ""
echo "=== ICMP payload entropy check ==="
tshark -r "$PCAP" -n -Y "icmp.type==8 and data.len > 100" \
-T fields -e data.data \
| head -20 | python3 -c "
import sys, math
from collections import Counter
def entropy(hex_str):
if not hex_str:
return 0
# Decode hex, compute byte entropy
try:
b = bytes.fromhex(hex_str.replace(':',''))
counts = Counter(b)
total = len(b)
return -sum((c/total)*math.log2(c/total) for c in counts.values())
except:
return 0
for line in sys.stdin:
h = line.strip()
e = entropy(h)
print(f'entropy={e:.2f} len={len(h)//2} {h[:40]}...')
"
Three statistics are sufficient to identify ICMP tunneling in any capture: (1) Sustained pair count: normal ping sends 4–8 packets between any two hosts. A tunnel sends thousands. If you see more than 100 ICMP packets between the same two IPs, it's either a network test or a tunnel — and network tests don't run for hours. (2) Payload size: normal pings use 32–64 byte payloads. Tunneling tools fill packets to near-MTU (1400 bytes) to maximize throughput. Any ICMP packet over 200 bytes of payload deserves inspection. (3) Payload entropy: the classic ping payload is abcdefghijklmnopqrstuvwxyz repeated — very low entropy, predictable. A tunnel carries encrypted TCP data — high entropy, looks random. Computing the entropy of the ICMP data field with Python and flagging payloads with entropy > 7.0 bits/byte will catch virtually all ICMP tunneling. The combination of all three makes false positives extremely unlikely.
Q & A
Q: Our firewall blocks all outbound ICMP. Does that mean ICMP tunneling is impossible in our environment?
If your firewall truly drops all outbound ICMP — including Echo Request and Echo Reply — then external ICMP tunneling is not possible through that firewall. However, several caveats: (1) Verify the policy: "firewall blocks ICMP" often means "firewall blocks inbound ICMP to internal IPs" or "firewall blocks ICMP flood patterns." Test from an internal machine whether you can ping an external IP (8.8.8.8) — if you get replies, ICMP outbound is not blocked. (2) Internal-to-internal ICMP tunneling: even with outbound ICMP blocked, an attacker who has already compromised an internal machine (or gets in via phishing) can use ICMP tunneling internally between compromised machines. Internal network monitoring may not inspect ICMP the same way. (3) Alternative ICMP types: some tools use ICMP Type 3 (Destination Unreachable) or Type 11 (Time Exceeded) which may not be blocked by rules targeting Echo Request/Reply specifically. Always test whether your block policy covers all ICMP types, not just ping.