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
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;`.
Actions