Skip to content

Fix heap-buffer-overflow READ in LightPcapNg parse_by_block_type#2160

Open
trailfork wants to merge 1 commit into
seladb:masterfrom
trailfork:fix-lightpcapng-oob-read
Open

Fix heap-buffer-overflow READ in LightPcapNg parse_by_block_type#2160
trailfork wants to merge 1 commit into
seladb:masterfrom
trailfork:fix-lightpcapng-oob-read

Conversation

@trailfork

Copy link
Copy Markdown

Summary

parse_by_block_type in the bundled LightPcapNg
(3rdParty/LightPcapNg/LightPcapNg/src/light_pcapng.c) reads each block's
mandatory fixed header with no minimum-size check. light_read_record
(light_pcapng.c:353) sizes the block body from the attacker-controlled
block_total_length and allocates exactly that many bytes:

const uint32_t bytesToRead = current->block_total_length - 2 * sizeof(blockSize) - sizeof(blockType);
uint32_t *local_data = calloc(bytesToRead, 1);

parse_by_block_type then reads each block type's fixed header out of that
buffer without verifying it is large enough. With block_total_length == 16,
bytesToRead == 4, so the second 4-byte field read runs one word past the
allocation — e.g. the Section Header Block version field
(light_pcapng.c:115, after byteorder_magic) or the Interface Description
Block snapshot_length field (light_pcapng.c:140, after
link_type/reserved). A crafted .pcapng / .zstd / .zst opened via
PcapNgFileReaderDevice (the standard IFileReaderDevice::getReader(path)->open()
path) triggers a heap-buffer-overflow READ.

Impact

Attack surface: file parsing only — not network-reachable. The faulting
fields (SHB version, IDB snapshot_length, block_total_length) are pcapng
container metadata emitted by the capture writer, not captured wire data.
Reachable through the library's default consumption pattern:
IFileReaderDevice::getReader dispatches on file extension, so any
.pcapng/.zstd/.zst path is routed to the vulnerable reader, and at least
eight shipped examples use getReader(path)->open() (PcapPrinter, PcapSplitter,
PcapSearch, HttpAnalyzer, TcpReassembly, SSLAnalyzer, TLSFingerprinting,
IPFragUtil/IPDefragUtil).

Trigger: the victim opens or scans a crafted capture file. PcapPrinter opens a
single CLI-named file (crash during open(), matching poc.pcapng);
PcapSearch recursively sweeps every capture under a -d directory with no
per-file interaction (the unattended-ingest case).

No disclosure: the over-read is read-only with no write primitive. The over-read
value is consumed internally — it becomes the parsed SHB version / IDB
snapshot_length, surfaced to callers only as opaque 64-bit numbers — and is
never returned to the attacker on an observable channel. There is no demonstrated
information-disclosure primitive and no path to memory-corruption RCE.

Affected versions: present at master HEAD (commit af83983b) and at the
originally reported commit 12cdbd5b. The vendored LightPcapNg sources under
3rdParty/ are the only copy on the read path; the seladb LightPcapNg fork they
derive from has had no upstream maintainer since 2017, so the fix is proposed
here, against PcapPlusPlus.

Severity: Medium — denial of service (crash on an unmapped page; deterministic
abort under ASan / hardened builds). Read-only, no write, no disclosure.

  • Typical (local file the victim opens):
    CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H (5.5)
  • Theoretical unattended consumer ingesting untrusted files with no per-file
    interaction (e.g. a recursive directory sweep like the PcapSearch example):
    CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5)

Reproduction

Two minimal inputs exercise the two crash paths (base64 below):

  1. poc.pcapng (16 bytes) — undersized Section Header Block; crashes during
    open() -> light_pcapng_open_read -> light_read_record ->
    parse_by_block_type (SHB, line 115).
  2. evidence_bundle.bin (44 bytes) — valid SHB followed by an undersized
    Interface Description Block; crashes later via
    getNextPackets -> getNextPacketInternal -> light_read_record ->
    parse_by_block_type (IDB, line 140).

Build the pcapng fuzz target with ASan and run each input:

git clone https://github.com/seladb/PcapPlusPlus /tmp/pcapplusplus-src
cd /tmp/pcapplusplus-src

cmake -S . -B build \
  -DPCAPPP_BUILD_FUZZERS=ON -DPCAPPP_BUILD_TESTS=OFF -DPCAPPP_BUILD_EXAMPLES=OFF \
  -DCMAKE_C_FLAGS="-g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined" \
  -DCMAKE_CXX_FLAGS="-g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"

cmake --build build -j --target FuzzTargetNg
./build/Tests/Fuzzers/FuzzTargetNg poc.pcapng          # SHB path
./build/Tests/Fuzzers/FuzzTargetNg evidence_bundle.bin # IDB path

The fuzz target links libFuzzer (-fsanitize=fuzzer), which ships with the
default clang on Linux. On macOS, Apple's clang lacks libFuzzer: install
Homebrew LLVM (brew install llvm) and add
-DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++"
to the cmake invocation above.

Expected: heap-buffer-overflow ... READ of size 4, 0 bytes after 4-byte region, allocated in light_read_record.

To recreate the input files:

# poc.pcapng
base64 -d > poc.pcapng <<'EOF'
Cg0NChAAAABNPCsaEAAAAA==
EOF

# evidence_bundle.bin
base64 -d > evidence_bundle.bin <<'EOF'
Cg0NChwAAABNPCsaAQAAAP//////////HAAAAAEAAAAQAAAAAQD//xAAAAA=
EOF

Fix

Reject blocks whose body is too small to hold the mandatory fixed fields, per
block type, before parsing them. The patch derives body_size = block_total_length - 12 (mirroring light_read_record's bytesToRead) at the
top of parse_by_block_type and validates it against each block type's
fixed-header size (16 for SHB, 8 for IDB, 20 for EPB, 4 for SPB, 12 for CUSTOM)
before reading any field. On an insufficient body the case allocates a
zero-initialised header struct (and, where applicable, an empty option) and
returns, so downstream consumers read a well-formed but empty record instead of
dereferencing NULL. After the fix, both inputs are rejected gracefully
("Couldn't read the first packet in the file") with no ASan report. See the
commit in this PR.

Credit

Found by Claude during an automated review conducted by Anthropic; manually
validated and patched by Trail of Bits. Reference: ANT-2026-01245.

Signed-off-by: trailfork <tyler.rolling@trailofbits.com>
@trailfork trailfork requested a review from seladb as a code owner June 5, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant