layers/geneve: uint8 offset overflow moves the inner-frame boundary 256 bytes — everything in the tunnel is decoded from the wrong bytes #9

Open
opened 2026-08-26 09:57:29 +00:00 by claude · 0 comments
Collaborator

Severity: high · layers/geneve.go:93,106-110

A third defect in Geneve.DecodeFromBytes, distinct from the two off-by-ones in #8. This one does not crash — it silently hands the wrong bytes to the inner decoder, which is worse for a monitoring tool.

Mechanism

offset, length := uint8(8), int32(gn.OptionsLength)
//     ^^^^^ uint8
for length > 0 {
        opt, len, err := decodeGeneveOption(data[offset:], gn, df)
        ...
        length -= int32(len)
        offset += len                                  // uint8 arithmetic
}
gn.BaseLayer = BaseLayer{data[:offset], data[offset:]}

offset is a uint8. The options area starts at byte 8 and gn.OptionsLength is (data[0] & 0x3f) * 4, so it ranges up to 252. The end of the options is therefore up to 8 + 252 = 260 — which does not fit in a uint8.

At 252 bytes of options offset wraps to 260 - 256 = 4. gn.Contents becomes data[:4] and gn.Payload becomes data[4:], so the bytes handed to the inner decoder begin 256 bytes before the real encapsulated frame, in the middle of the Geneve option area.

RFC 8926 gives Opt Len 6 bits, so 252 is the protocol's own maximum. This is not an exotic value — it is the top of the legal range.

Reproduction

A Geneve packet with 252 bytes of options carrying a real inner Ethernet frame at offset 260:

=== B: 302-byte Geneve payload, 252 B of options, real inner frame at offset 260 ===
  gn.Contents = 4 bytes (should be 260)
  gn.Payload  = 298 bytes (should be 42)
  bytes handed to the inner decoder start with: "\x00\x00\x00\x00\x00\x00\x00\x0fOPTION-BYTES-NOT-THE-INNER-FRAME"
  the REAL inner frame starts with:             deadbeef0001deadbeef00020806

No error. No SetTruncated. Options is populated correctly with all four options — the parse looks completely successful. Only the boundary is wrong.

Impact

This is a clean tunnel evasion. Everything the attacker puts inside a Geneve tunnel becomes invisible to the analyser, while the tunnel endpoint decapsulates it correctly:

  • The analyser parses 256 bytes of attacker-chosen option padding as the inner Ethernet frame. Since the attacker writes those bytes, they choose what the analyser believes the tunnel contains — a benign inner frame of their design, decoded with a valid EtherType, valid IP header, valid ports.
  • The real inner frame is never parsed as a frame at all. It sits 256 bytes into what gopacket labelled "payload" and is decoded as the tail of the attacker's cover frame.

So it is not merely blinding, it is substitution — the same shape as #3, in a different subsystem.

Cost: setting the Opt Len field to 0x3f and padding to 252 bytes of options. One packet.

Fix

Widen the cursor to a type that can hold the range, and check the end against the buffer:

offset, length := 8, int(gn.OptionsLength)
if len(data) < offset+length {
        df.SetTruncated()
        return errors.New("geneve packet too short")
}
for length > 0 {
        opt, olen, err := decodeGeneveOption(data[offset:], gn, df)
        if err != nil {
                return err
        }
        gn.Options = append(gn.Options, opt)
        length -= int(olen)
        offset += int(olen)
}
if length != 0 {
        return errors.New("geneve options do not tile the options area")
}

Note the length != 0 check as well: options are 4-byte aligned and OptionsLength is a multiple of 4, but nothing currently asserts that the options actually sum to the declared area, so length can go negative and the final offset can land past the declared end of the options.

SerializeTo has the same shape (plen := int(gn.OptionsLength + 8)uint8 arithmetic, wraps for OptionsLength >= 248) and should be fixed alongside it.

Worth a test at OptionsLength = 252 specifically; the existing geneve_test.go fixtures use small option sets.


Verified against b7d9dbd on Go 1.24.4. PoCs: geneve, geneve2. Related: #8 (two off-by-one guards in the same function).

**Severity: high** · `layers/geneve.go:93,106-110` A third defect in `Geneve.DecodeFromBytes`, distinct from the two off-by-ones in #8. This one does not crash — it silently hands the wrong bytes to the inner decoder, which is worse for a monitoring tool. ## Mechanism ```go offset, length := uint8(8), int32(gn.OptionsLength) // ^^^^^ uint8 for length > 0 { opt, len, err := decodeGeneveOption(data[offset:], gn, df) ... length -= int32(len) offset += len // uint8 arithmetic } gn.BaseLayer = BaseLayer{data[:offset], data[offset:]} ``` `offset` is a `uint8`. The options area starts at byte 8 and `gn.OptionsLength` is `(data[0] & 0x3f) * 4`, so it ranges up to **252**. The end of the options is therefore up to `8 + 252 = 260` — which does not fit in a `uint8`. At 252 bytes of options `offset` wraps to `260 - 256 = 4`. `gn.Contents` becomes `data[:4]` and `gn.Payload` becomes `data[4:]`, so the bytes handed to the inner decoder begin **256 bytes before** the real encapsulated frame, in the middle of the Geneve option area. RFC 8926 gives Opt Len 6 bits, so 252 is the protocol's own maximum. This is not an exotic value — it is the top of the legal range. ## Reproduction A Geneve packet with 252 bytes of options carrying a real inner Ethernet frame at offset 260: ``` === B: 302-byte Geneve payload, 252 B of options, real inner frame at offset 260 === gn.Contents = 4 bytes (should be 260) gn.Payload = 298 bytes (should be 42) bytes handed to the inner decoder start with: "\x00\x00\x00\x00\x00\x00\x00\x0fOPTION-BYTES-NOT-THE-INNER-FRAME" the REAL inner frame starts with: deadbeef0001deadbeef00020806 ``` No error. No `SetTruncated`. `Options` is populated correctly with all four options — the parse *looks* completely successful. Only the boundary is wrong. ## Impact This is a clean **tunnel evasion**. Everything the attacker puts inside a Geneve tunnel becomes invisible to the analyser, while the tunnel endpoint decapsulates it correctly: - The analyser parses 256 bytes of attacker-chosen option padding as the inner Ethernet frame. Since the attacker writes those bytes, they choose what the analyser believes the tunnel contains — a benign inner frame of their design, decoded with a valid EtherType, valid IP header, valid ports. - The real inner frame is never parsed as a frame at all. It sits 256 bytes into what gopacket labelled "payload" and is decoded as the tail of the attacker's cover frame. So it is not merely blinding, it is substitution — the same shape as #3, in a different subsystem. Cost: setting the Opt Len field to `0x3f` and padding to 252 bytes of options. One packet. ## Fix Widen the cursor to a type that can hold the range, and check the end against the buffer: ```go offset, length := 8, int(gn.OptionsLength) if len(data) < offset+length { df.SetTruncated() return errors.New("geneve packet too short") } for length > 0 { opt, olen, err := decodeGeneveOption(data[offset:], gn, df) if err != nil { return err } gn.Options = append(gn.Options, opt) length -= int(olen) offset += int(olen) } if length != 0 { return errors.New("geneve options do not tile the options area") } ``` Note the `length != 0` check as well: options are 4-byte aligned and `OptionsLength` is a multiple of 4, but nothing currently asserts that the options actually sum to the declared area, so `length` can go negative and the final `offset` can land past the declared end of the options. `SerializeTo` has the same shape (`plen := int(gn.OptionsLength + 8)` — `uint8` arithmetic, wraps for `OptionsLength >= 248`) and should be fixed alongside it. Worth a test at `OptionsLength = 252` specifically; the existing `geneve_test.go` fixtures use small option sets. --- *Verified against `b7d9dbd` on Go 1.24.4. PoCs: `geneve`, `geneve2`. Related: #8 (two off-by-one guards in the same function).*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
noi/gopacket#9
No description provided.