Project

General

Profile

Actions

Bug #8836

open
SB SD

erf/file: sets packet length from unvalidated wlen, causing OOB heap read in decoder

Bug #8836: erf/file: sets packet length from unvalidated wlen, causing OOB heap read in decoder

Added by Shivani Bhardwaj 9 days ago. Updated about 20 hours ago.

Status:
Assigned
Priority:
Normal
Target version:
Affected Versions:
Effort:
Difficulty:
Label:

Description

Reported by Communications Security Establishment (CSE):

## Summary

The ERF file capture source (`--erf-in`) trusts the 16-bit `wlen` (wire length) field from the on-disk DAG record header verbatim and assigns it to `GET_PKT_LEN(p)` without ever checking it against the number of bytes actually read into the packet buffer or against `default_packet_size`. The packet is then handed to `DecodeEthernet()` with this attacker-controlled length, so the Ethernet/IP decoders and downstream payload inspection treat up to 65 535 bytes as valid while the underlying `p->pkt_data[]` heap buffer is only ~1514 bytes. The result is a heap out-of-bounds read of up to ~64 KB whenever Suricata is run in offline mode against a crafted ERF file.

## Affected Piece of Code

- **File:** `src/source-erf-file.c`
- **Function / Location:** `ReadErfRecord()` ~L171–196 and `DecodeErfFile()` ~L282–297
- **Subsystem:** capture — Capture sources (af-packet, af-xdp, dpdk, netmap, nfq, pcap, etc.) and packet queue

```c
src/source-erf-file.c:
171     uint16_t rlen = SCNtohs(dr.rlen);
172     uint16_t wlen = SCNtohs(dr.wlen);
173     if (rlen < sizeof(DagRecord)) {
174         SCLogError("Bad ERF record, " 
175                    "record length less than size of header");
176         SCReturnInt(TM_ECODE_FAILED);
177     }
178     r = fread(GET_PKT_DATA(p), rlen - sizeof(DagRecord), 1, etv->erf);
179     if (r < 1) {
...
189     /* Only support ethernet at this time. */
190     if (dr.type != DAG_TYPE_ETH) {
191         SCLogError("DAG record type %d not implemented.", dr.type);
192         SCReturnInt(TM_ECODE_FAILED);
193     }
194
195     GET_PKT_LEN(p) = wlen;
196     p->datalink = LINKTYPE_ETHERNET;
...
292     DecodeEthernet(tv, dtv, p, GET_PKT_DATA(p), GET_PKT_LEN(p));
```

## The Bug

After reading the record body, `ReadErfRecord()` executes `GET_PKT_LEN(p) = wlen;` (L195), where `wlen` is the 16-bit wire-length field taken byte-for-byte from the input file. There is **no** check that `wlen <= rlen - sizeof(DagRecord)` (the number of bytes actually `fread()` into the buffer) and **no** check that `wlen <= default_packet_size`. `DecodeErfFile()` subsequently calls `DecodeEthernet(tv, dtv, p, GET_PKT_DATA(p), GET_PKT_LEN(p))` (L292), so the Ethernet/IP decode chain is told the packet is up to 65 535 bytes long while the underlying `pkt_data[]` heap buffer is only `default_packet_size` (typically 1514) bytes and may contain even fewer valid bytes. This is a heap out-of-bounds read of up to ~64 KB during decoding.

The bug is independent of the separate `fread` overflow on L178: by choosing a small `rlen` (e.g. 32, so only 14 body bytes are read) and `wlen = 0xFFFF`, the OOB read fires purely from the unvalidated `wlen`.

The contrast with the live-DAG sibling is telling: `ProcessErfDagRecord()` in `src/source-erf-dag.c:481` explicitly guards with `if (rlen < dag_record_size + ... + wlen)` before trusting `wlen`, and uses `PacketCopyData()` so oversized payloads grow into `ext_pkt`. The file reader has no equivalent guard.

**Call chain to the vulnerable site:**

1. Entry: `suricata --erf-in <file>` (`src/suricata.c:1763`) sets `RUNMODE_ERF_FILE`.
2. `RunModeErfFileSingle()` / `RunModeErfFileAutoFp()` (`src/runmode-erf-file.c:50–98`) wire `TMM_RECEIVEERFFILE` + `TMM_DECODEERFFILE` into the threading pipeline.
3. `ReceiveErfFileLoop()` (`src/source-erf-file.c:112`) calls `PacketGetFromQueueOrAlloc()` (`decode.c:299`). The returned `Packet` has `ext_pkt == NULL`, so `GET_PKT_DATA(p)` resolves to the inline flexible-array `p->pkt_data[]`, sized `default_packet_size`. For `RUNMODE_ERF_FILE` the default-case in `src/suricata.c:2662` applies, giving `default_packet_size = DEFAULT_PACKET_SIZE = 1514`.
4. `ReadErfRecord()` (`src/source-erf-file.c:154`) `fread`s the 18-byte `DagRecord` header, computes `rlen = SCNtohs(dr.rlen)` and `wlen = SCNtohs(dr.wlen)`, checks only `rlen < sizeof(DagRecord)` (L173), `fread`s `rlen − 18` body bytes directly into `p->pkt_data`, then unconditionally sets `GET_PKT_LEN(p) = wlen;` (L195) with no comparison of `wlen` against `rlen − 18` or `default_packet_size`.
5. The packet flows via `TmThreadsSlotProcessPkt()` to `DecodeErfFile()` (L282), which calls `DecodeEthernet(tv, dtv, p, GET_PKT_DATA(p), GET_PKT_LEN(p))` (L292) → `DecodeNetworkLayer()` (`decode-ethernet.c:61`) with `len` up to 65 535 while the backing heap buffer is only 1514 bytes — a heap OOB read of up to ~64 KB.

**Required field values to reach the bug:** `dr.type = 0x02` (`DAG_TYPE_ETH`); `rlen ≥ 19` (note `rlen = 18` fails because `fread(ptr, 0, 1, f)` returns 0 and bails at L179) and `rlen ≤ 18 + 1514` to keep this purely the `wlen` issue; `wlen = 0xFFFF`.

**Vulnerability class:** OOB-read.

## Reproduction Results

The trigger below is **analytically derived** from source review of the call chain above; it has not been executed against a live ASAN build in this environment. Every guard on the path (L173 `rlen` check, L179 `fread` return check, L190 `type` check) has been accounted for, and no additional validation of `wlen` exists between L172 and L292, so confidence that the OOB read fires is high.

1. Create a 32-byte ERF file `evil.erf` containing exactly one `DagRecord` header plus a 14-byte body:

   | Offset | Bytes                       | Meaning                                                         |
   |--------|-----------------------------|-----------------------------------------------------------------|
   | 0x00   | `00 00 00 00 00 00 00 00`   | `ts` (don't care)                                               |
   | 0x08   | `02`                        | `type = DAG_TYPE_ETH`                                           |
   | 0x09   | `00`                        | `flags`                                                         |
   | 0x0A   | `00 20`                     | `rlen = 32` (big-endian) → body = 32 − 18 = 14 bytes            |
   | 0x0C   | `00 00`                     | `lctr`                                                          |
   | 0x0E   | `FF FF`                     | `wlen = 65535` (big-endian)                                     |
   | 0x10   | `00 00`                     | `pad`                                                           |
   | 0x12   | `00 00 00 00 00 00`         | dst MAC (body start → start of `pkt_data`)                     |
   | 0x18   | `00 00 00 00 00 00`         | src MAC                                                         |
   | 0x1E   | `08 00`                     | EtherType = IPv4 → forces `DecodeNetworkLayer → DecodeIPV4` with `len = 65521` |

   One-liner:

   ```sh
   printf '\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x20\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00' > evil.erf
   ```

2. Run Suricata (ideally built with AddressSanitizer for a clean signal) with the default config:

   ```sh
   ./src/suricata -c suricata.yaml --erf-in evil.erf -l /tmp
   ```

3. Execution path: `ReceiveErfFileLoop` → `ReadErfRecord` reads the header; `rlen = 32 ≥ 18` passes L173; `fread` reads 14 body bytes into `p->pkt_data` (in-bounds); `type == 2` passes L190; L195 sets `p->pktlen = 65535`. `DecodeErfFile` L292 calls `DecodeEthernet(..., p->pkt_data, 65535)`. `DecodeEthernet` sees `len ≥ 14`, reads EtherType `0x0800`, and calls `DecodeNetworkLayer` / `DecodeIPV4` with `pkt = p->pkt_data + 14`, `len = 65521`. The IPv4 decoder (and any subsequent payload/content inspection) treats 65 521 bytes as in-bounds while the heap allocation behind `pkt_data` is only 1514 bytes → ASAN reports a `heap-buffer-overflow READ` past the `Packet` allocation. Without ASAN the process may read adjacent heap memory and either crash or silently inspect/match on out-of-bounds bytes.

No special rules or YAML settings are required; `default_packet_size` falls through to `DEFAULT_PACKET_SIZE` (1514) for `RUNMODE_ERF_FILE` (`src/suricata.c:2662`).

## Severity

**LOW** — Heap out-of-bounds read of up to ~64 KB during packet decoding and subsequent payload inspection. Consequences are process crash / DoS when the read crosses into an unmapped page, and potential information disclosure: adjacent heap contents become the "packet payload" and can be matched by detection rules, logged by eve-log/pcap-log, or influence flow/stream state. This is not a memory write and not RCE on its own. The trigger requires an operator to invoke `suricata --erf-in` on an attacker-supplied ERF file — i.e., offline/local input rather than live network traffic — which substantially limits exposure.

Note: line 178 separately allows a heap-buffer-overflow **write** when `rlen − 18 > default_packet_size`, but that is a distinct issue; the reproduction above deliberately keeps `rlen` small so only the `wlen`-driven OOB read fires.

## Suggested Fix

In `ReadErfRecord()` (`src/source-erf-file.c`), after computing `rlen`/`wlen` and before the body `fread`, validate both lengths and clamp `wlen` to what was actually captured — mirroring the guard in `src/source-erf-dag.c:481` — and use `PacketCopyData()` / `SET_PKT_LEN` so `ext_pkt` growth is handled correctly:

```c
     uint16_t rlen = SCNtohs(dr.rlen);
     uint16_t wlen = SCNtohs(dr.wlen);
-    if (rlen < sizeof(DagRecord)) {
+    if (rlen <= sizeof(DagRecord)) {
         SCLogError("Bad ERF record, record length less than size of header");
         SCReturnInt(TM_ECODE_FAILED);
     }
-    r = fread(GET_PKT_DATA(p), rlen - sizeof(DagRecord), 1, etv->erf);
+    uint32_t caplen = rlen - sizeof(DagRecord);
+    if (caplen > MAX_PAYLOAD_SIZE) {
+        SCLogError("Bad ERF record, capture length %u exceeds max", caplen);
+        SCReturnInt(TM_ECODE_FAILED);
+    }
+    /* wire length cannot exceed what we actually captured for decoding */
+    if (wlen > caplen) {
+        wlen = (uint16_t)caplen;
+    }
+    uint8_t buf[caplen];               /* or a per-thread scratch buffer */
+    r = fread(buf, caplen, 1, etv->erf);
     if (r < 1) { ... }
 ...
-    GET_PKT_LEN(p) = wlen;
+    if (PacketCopyData(p, buf, wlen) != 0)
+        SCReturnInt(TM_ECODE_FAILED);
```

Minimal alternative, if the direct `fread` into `pkt_data` is to be kept: add `if (caplen > GET_PKT_DIRECT_MAX_SIZE(p)) fail;` before the `fread`, and `if (wlen > caplen) wlen = caplen;` before `GET_PKT_LEN(p) = wlen;`.

Related issues 1 (1 open0 closed)

Related to Suricata - Bug #8865: erf/file: Heap buffer overflow in ERF file reader via untrusted rlen fieldNewOISF DevActions

VJ Updated by Victor Julien 9 days ago Actions #1

  • Subject changed from ERF file reader sets packet length from unvalidated wlen, causing OOB heap read in decoder to erf/file: sets packet length from unvalidated wlen, causing OOB heap read in decoder

VJ Updated by Victor Julien 2 days ago Actions #2

  • Tracker changed from Security to Bug
  • Private changed from Yes to No

Considering this a bug.

VJ Updated by Victor Julien 2 days ago Actions #3

@Stephen Donnelly do you have interest in looking into this?

VJ Updated by Victor Julien 2 days ago Actions #4

  • Related to Bug #8865: erf/file: Heap buffer overflow in ERF file reader via untrusted rlen field added

SD Updated by Stephen Donnelly 1 day ago Actions #5

  • Assignee changed from OISF Dev to Stephen Donnelly

Victor Julien wrote in #note-3:

@Stephen Donnelly do you have interest in looking into this?

Yes I can take a look.

I agree there is a bug, but the analysis looks a little off.

We should be using a derivative of rlen for DecodeEthernet(), not wlen.

In ERF the wlen is the packet wire length, similar to len in pcap. The captured/available length (pcap caplen ) must be calculated from rlen - headers - padding.

VJ Updated by Victor Julien about 20 hours ago Actions #6

  • Status changed from New to Assigned

Thanks! Yeah the analysis is by AI, so it's not always correct. Btw the related ticket #8865 is also about ERF.

Actions

Also available in: PDF Atom