1 Pentest 2026-08
Claude edited this page 2026-08-26 10:12:34 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Attack-surface assessment of gopacket at b7d9dbd, August 2026. Every finding below was reproduced with a runnable program on Go 1.24.4, linux/amd64, 4 cores / 8 GB. PoCs: branch pentest/2026-08-poc.

Threat model

A rival team on the game network can send arbitrary bytes. tcpdump captures them, gopacket parses them. The attacker never touches the analyser's API, config, host or the operator's shell.

Two consequences of that shape drive most of what follows:

  • The packets do not have to be accepted by anything. A fragment addressed to an unused IP, a TCP segment that no host will ever ACK, a UDP datagram to a closed port — all are captured and all are parsed. "A real stack would drop this" is not a mitigation.
  • The capture is lossy in a specific way. Snaplen truncation and short frames mean a declared length field routinely disagrees with the bytes actually present. Any code that guards on one and indexes the other is a crash primitive.

Findings

# Finding Severity Cost to attacker
#1 ip4defrag panic — two independent root causes critical 3 fragments, 254-byte pcap
#13 DNS name-decompression bomb high 1.4 MB → 8 GB
#3 SYN payload rewrites the reconstructed request high 71 bytes on a packet already being sent
#4 Quadratic tcpassembly insertion, unbounded buffering high ~1.2 Mbit/s per core
#2 Overlapping fragments never reassemble high 1 packet
#8 Geneve: two off-by-one guards high 7 bytes
#9 Geneve: uint8 overflow shifts the inner frame 256 B high 1 packet
#11 26 layer decoders index past their own guard medium 1566 byte frame
#12 No layer-nesting cap; Dump() amplifies 744× medium ~8 Mbit/s per core
#5 FastHash merges four 4-tuples into one key medium 1 extra connection
#6 Reassembled IPv4 Length lies about its payload medium 2 fragments
#7 "Fragment will overrun" check is vacuous (uint16) medium latent
#10 pcapgo: 40-byte file → 1 GB alloc; snoop panics medium needs an untrusted pcap

Status of the four findings inherited from daisy-findings.md

daisy # Verdict
2 — overlapping-fragment panic Confirmed, and worse. Minimised from 4 fragments / 2.2 KB to 3 fragments / 138 wire bytes. A second, independent root cause needs no truncation at all. The fix proposed in that document does not stop it — see below. → #1
4 — quadratic reassembly Confirmed. 4.35 MB of descending-sequence segments = 35.9 s of CPU, 780× the in-order cost, with 28× memory amplification. → #4
8connKey collision Confirmed, and broader. Because both halves of netFlow.FastHash() ^ tcpFlow.FastHash() are commutative, it collapses four distinct 4-tuples, not two. → #5
9 — SYN-payload desync Confirmed, and materially worse than described. Not a truncation that breaks HTTP parsing — a byte-for-byte substitution that lets the attacker choose a complete, well-formed, benign request for the analyser to record. → #3

⚠ The proposed fix for finding 2 is insufficient

daisy-findings.md proposes, before handing a fragment to ip4defrag:

if want := int(ip.Length) - int(ip.IHL)*4; want > 0 && len(ip.Payload) < want {
        return nil, false
}

That guard is necessary but not sufficient. There is a second root cause: fragmentList.insert() computes the fragment size as in.Length - 20 (hardcoded) while securityChecks() uses ip.Length - IHL*4. For any fragment carrying IP options these disagree, and the payload is exactly its declared length, so the guard passes:

frag1 IHL=5  Length=44 want=24 len(Payload)=24 -> proposed fix ACCEPTS
frag2 IHL=5  Length=28 want=8  len(Payload)=8  -> proposed fix ACCEPTS
frag3 IHL=15 Length=68 want=8  len(Payload)=8  -> proposed fix ACCEPTS
panic: runtime error: slice bounds out of range [16:8]

Fix insert() to use in.Length - uint16(in.IHL)*4, and bound the splice in build() by len(frag.Payload) rather than by a wire field. Details in #1.

Where the panic recovery actually is

Worth being precise, because it separates the critical findings from the medium ones.

Path Recovers? Opt-out
gopacket.NewPacket yes, by default DecodeOptions{SkipDecodeRecovery: true}
DecodingLayerParser.DecodeLayers yes, by default (parser.go:304) DecodingLayerParserOptions{IgnorePanic: true}
ip4defrag no recovery anywhere
tcpassembly / reassembly no recovery anywhere
pcapgo readers no recovery anywhere

So the layer-decoder panics (#8, #11) are hard crashes only for callers who opted out of recovery for performance — which gopacket's own documentation encourages ("Handling errors does add latency"). The ip4defrag panic (#1) is a hard crash for everyone, which is why it is the only critical.

A caller must additionally place its own recover around the reader and the defragmenter, not just around the decode — a guard that wraps only DecodeLayers / NewPacket does not cover the path that actually panics.

What was checked and found sound

Negative results, so the next pass does not repeat them:

  • TLV and option loops are well guarded. A structured sweep over TCP options (DataOffset 6/7/10/15), IPv4 options (IHL 6/7/10/15), IPv6 hop-by-hop TLVs, DHCPv4, DHCPv6, LLDP, TLS records, SIP headers, NTP, RADIUS and MLDv2 — with zero-length options, lengths past the end, and repeated malformed kinds — produced no panics and no non-terminating loops. The bugs are concentrated in fixed-header decoders, not in the loops.
  • DNS pointer-chase depth is capped at maxRecursionLevel = 255 and self-referential loops are caught cleanly. The bomb in #13 works by staying under the cap and multiplying across records, not by defeating it.
  • pcapgo/read.go does bound CaptureLength — the problem in #10 is that it bounds it against two other fields from the same file, not that the check is missing.
  • IPv4's own DecodeFromBytes is careful: it rejects Length < 20, IHL < 5 and IHL*4 > Length, and sets SetTruncated() correctly. The defrag bugs are downstream of a correct decode.

Method

  • Deterministic short-input sweep (sweep): every registered LayerType × lengths 080 × six byte patterns, with recovery off — 972,000 probes, 26 layers panicking. This found more than fuzzing would have in the same wall-clock, and it is reproducible.
  • Structured option sweep (optsweep): valid outer header, hostile option area, with a 2-second watchdog per probe so a non-terminating loop is distinguishable from a pass.
  • Targeted construction for the amplification findings, measured with runtime.MemStats.TotalAlloc for churn and HeapAlloc after a forced GC for retention — the distinction matters, and #13 is retention.
  • End-to-end replay through pcapgo.Readergopacket.NewPacketip4defrag.DefragIPv4 for #1, so the finding is not an artifact of calling an internal API directly.

Suggested order of work

  1. #1 — the only unrecoverable remote crash, and the cheapest to trigger. Fix insert()'s length arithmetic and bound the splice by the payload.
  2. #13 and #4 — the two resource exhaustions that need only a few Mbit/s.
  3. #3 and #9 — the two forgery primitives. These are the ones that produce wrong answers rather than no answers, which is the harder failure to notice.
  4. #11 — add the short-input test to CI first, then fix the 26 decoders it flags. The test is worth more than any individual fix.
  5. The rest.

Two changes worth making regardless of the individual fixes:

  • Add the short-input sweep from #11 as a CI test. It is ~60 lines, runs in under a minute, is fully deterministic, and would have caught 39 of these.
  • Compute lengths in int, never in the header field's own width. uint8/uint16 arithmetic on wire fields is the direct cause of #1, #7, #9 and much of #11.