https://sourceware.org/bugzilla/show_bug.cgi?id=34341
Bug ID: 34341
Summary: readelf: heap-buffer-overflow in
byte_get_little_endian via unchecked GOT section
sh_entsize (binutils/elfcomm.c:134)
Product: binutils
Version: 2.47 (HEAD)
Status: UNCONFIRMED
Severity: normal
Priority: P2
Component: binutils
Assignee: unassigned at sourceware dot org
Reporter: lswang1112 at gmail dot com
Target Milestone: ---
Created attachment 16812
--> https://sourceware.org/bugzilla/attachment.cgi?id=16812&action=edit
poc.elf (273 bytes) - minimal ELF triggering the heap-buffer-overflow, see "HOW
TO REPRODUCE"
Affected version: GNU Binutils 2.46.50.20260629 (git HEAD 18ca0215)
Confirmed reproduced on latest upstream HEAD as of 2026-07-02.
------------------------------------------------------------------------
ROOT CAUSE
------------------------------------------------------------------------
process_got_section_contents() (binutils/readelf.c) dumps every section
whose name starts with ".got". It derives the per-entry stride and the
entry count directly from the section header, with no sanity check
against the actual per-entry width the code below uses to walk the
buffer:
/* binutils/readelf.c, in process_got_section_contents() */
uint32_t entsz = section->sh_entsize; /* fully attacker-controlled */
if (entsz == 0)
{ /* only a real GOT entry size (4/8) is substituted when entsz==0 */ }
entries = section->sh_size / entsz; /* no upper/lower bound check */
...
struct got64
{
unsigned char bytes[4]; /* NB: 4, not 8 -- see below */
} *got;
...
got = (struct got64 *) data;
for (j = 0; j < entries; j++)
{
g = BYTE_GET (got[j].bytes); /* byte_get(f, sizeof(f)) =
byte_get(f, 4) */
...
}
`data` is a heap buffer holding exactly `section->sh_size` (+1, see
below) bytes, read straight from the file. `entsz` is `section->
sh_entsize`, read straight from the section header with no validation
that it matches a real GOT entry width (4 or 8 bytes) -- nothing stops a
crafted file from declaring `sh_entsize = 1`. When it does, `entries =
sh_size / entsz` massively overcounts how many actual (4-byte-strided,
see the struct note below) records the buffer can hold, and the loop
above reads `byte_get_little_endian(got[j].bytes, 4)` for `j` values that
walk `got` well past the end of `data`.
Secondary, related observation: `struct got64.bytes` is declared `[4]`,
not `[8]` -- it reuses the exact same 4-byte layout as `struct got32`
just above it, even though it's meant to represent a 64-bit GOT entry.
This isn't the primary trigger for this crash (the primary issue is the
unchecked `sh_entsize`), but it means `sizeof(struct got64) == 4`, so
even a "reasonable" `sh_entsize` doesn't guarantee the loop's actual
memory stride matches -- any fix needs to bound the loop by the real
struct stride (`sizeof(*got)`), not just by `entsz`.
Call chain:
readelf -a poc.elf
process_object readelf.c:24981
process_got_section_contents readelf.c:21665
byte_get_little_endian elfcomm.c:134 <-- OOB read
------------------------------------------------------------------------
ASAN OUTPUT
------------------------------------------------------------------------
==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000132
READ of size 1 at 0x502000000132 thread T0
#0 byte_get_little_endian binutils/elfcomm.c:134
#1 process_got_section_contents binutils/readelf.c:21665
#2 process_object binutils/readelf.c:24981
#3 process_file binutils/readelf.c:25404
#4 main binutils/readelf.c:25470
0x502000000132 is located 0 bytes after 2-byte region [...130,...132)
allocated by thread T0 here:
#0 malloc
#1 get_data binutils/readelf.c:555
SUMMARY: AddressSanitizer: heap-buffer-overflow binutils/elfcomm.c:134
in byte_get_little_endian
(The allocated region is 2 bytes, not 1, because get_data() allocates
`sh_size + 1` bytes so string-table sections can be NUL-terminated --
irrelevant here, just explains why the crash reads offset 2 rather than
offset 1.)
------------------------------------------------------------------------
HOW TO REPRODUCE
------------------------------------------------------------------------
Build from latest HEAD:
git clone --depth 1 https://sourceware.org/git/binutils-gdb.git
cd binutils-gdb
mkdir build && cd build
../configure --disable-gdb \
CFLAGS="-g -O1 -fsanitize=address,undefined -Wno-error=format-overflow"
make -j$(nproc) all-binutils
The attached poc.elf is 273 bytes. Run with:
./binutils/readelf -a poc.elf
The file is a hand-crafted, minimal ELF64: a bare ELF header, a 3-entry
section header table (the mandatory NULL section, one SHT_PROGBITS
section named ".got" with sh_size=1 and sh_entsize=1, and a .shstrtab
holding the section name strings), followed by the 1 byte of ".got" data
and the string table bytes. No program headers, symbol tables, or
dynamic section are needed -- process_got_section_contents() only
depends on filedata->section_headers.
With sh_size=1 and sh_entsize=1, `entries = 1/1 = 1`, and the very first
loop iteration already reads 4 bytes from a region that only holds 1
real (+1 padding) byte -- the smallest possible trigger for this bug.
Plain (no-sanitizer) build was not tested in this environment (only an
ASan+UBSan build was available), but this is a genuine heap OOB read, so
a plain build should be expected to at minimum leak adjacent heap bytes
into the printed GOT dump, and may crash depending on heap layout.
------------------------------------------------------------------------
Build & Platform
------------------------------------------------------------------------
binutils version: GNU Binutils 2.46.50.20260629 (git HEAD 18ca0215)
component: readelf
OS: Ubuntu 24.04.4 LTS
arch: x86_64
------------------------------------------------------------------------
SUGGESTED FIX
------------------------------------------------------------------------
Clamp the loop bound to what the buffer can actually supply at the
stride the code uses to walk it (`sizeof(*got)`), instead of trusting
`entries`/`n` as computed from the attacker-controlled `sh_entsize`:
/* 32-bit branch */
addr = section->sh_addr;
got = (struct got32 *) data;
+ if ((uint64_t) n * sizeof (*got) > section->sh_size)
+ n = section->sh_size / sizeof (*got);
for (j = 0; j < n; j++)
...
/* 64-bit branch */
addr = section->sh_addr;
got = (struct got64 *) data;
+ if (entries * sizeof (*got) > section->sh_size)
+ entries = section->sh_size / sizeof (*got);
for (j = 0; j < entries; j++)
...
Verified: with this patch applied and rebuilt, `readelf -a poc.elf` no
longer crashes (exit 0, no ASan report). The "Global Offset Table"
header line still prints the original (attacker-controlled) entry count
for information, but the dump loop itself is now bounded to 0 iterations
for this PoC (1 byte / 4-byte stride = 0), so it prints the header and
nothing unsafe after it.
Regression-tested against three well-formed ELF files with real GOT
sections (the freshly built readelf binary itself, /bin/ls, /bin/cat):
`readelf -a` output is byte-for-byte identical between the patched and
unpatched binaries on all three, confirming the clamp does not affect
normal files where sh_entsize is already correct.
------------------------------------------------------------------------
IMPACT
------------------------------------------------------------------------
Out-of-bounds heap read (CWE-125), triggered by any ELF file with a
section named ".got"/".got.plt" whose sh_entsize does not match the
size the code actually reads with. Reachable via the common `-a` flag
(and any other flag combination that sets do_got_section_contents).
Reproducible impact is denial of service and/or disclosure of adjacent
heap memory content through the printed GOT dump.
--
You are receiving this mail because:
You are on the CC list for the bug.