https://sourceware.org/bugzilla/show_bug.cgi?id=34725
Bug ID: 34725
Summary: mach-o: OOB read in bfd_mach_o_read_prebound_dylib()
(LC_PREBOUND_DYLIB), heap disclosure via objdump -P
load_commands
Product: binutils
Version: 2.48 (HEAD)
Status: UNCONFIRMED
Severity: normal
Priority: P2
Component: binutils
Assignee: unassigned at sourceware dot org
Reporter: venkatchallaceh at gmail dot com
Target Milestone: ---
Created attachment 17066
--> https://sourceware.org/bugzilla/attachment.cgi?id=17066&action=edit
Patch: read name_str/linked_modules at validated file offsets (git-am
format-patch)
bfd_mach_o_read_prebound_dylib() in bfd/mach-o.c parses an LC_PREBOUND_DYLIB
Mach-O load command. It reads two file-controlled 32-bit offsets (nameoff,
modoff), validates only that neither is greater than the load command's length,
then derives two pointers by subtracting a fixed constant from a pointer
computed using those offsets:
cmd->name_str = (char *)str + nameoff - (sizeof (raw) +
BFD_MACH_O_LC_SIZE);
cmd->linked_modules = str + modoff - (sizeof (raw) +
BFD_MACH_O_LC_SIZE);
Because there is no lower-bound check, any nameoff/modoff value smaller than
sizeof(raw) + BFD_MACH_O_LC_SIZE (20) makes the resulting pointer point before
the start of the `str` heap allocation -- up to 20 bytes of out-of-bounds read.
Separately, `str` is allocated via _bfd_alloc_and_read(str_len, str_len) with
no extra byte for a NUL terminator, so even an in-bounds name_str is not
guaranteed to be NUL-terminated, letting a subsequent %s print run past the end
of the allocation until it hits a stray NUL in adjacent heap.
Both derived pointers are used to produce visible output in
binutils/od-macho.c's "-P load_commands" dumper:
printf (" dylib: %s\n", pbdy->name_str); /* unbounded %s */
printf ("%u", (lm[j >> 3] >> (j & 7)) & 1); /* lm = linked_modules
*/
So a crafted LC_PREBOUND_DYLIB load command in an otherwise well-formed Mach-O
file makes "objdump -P load_commands <file>" read and print heap memory outside
the buffer that was actually allocated for this field -- an out-of-bounds read
whose bytes are disclosed to the operator's terminal, not merely a crash.
Reachability: LC_PREBOUND_DYLIB is parsed on open for any Mach-O file via the
generic load-command dispatch (case BFD_MACH_O_LC_PREBOUND_DYLIB). The mach-o
BFD target and od-macho dumper are built into any --enable-targets=all binutils
(e.g. distro binutils-multiarch), so this is reachable on shipped toolchains
without special configuration.
Root cause: the sibling readers for the structurally identical dylib/dylinker
path-name fields -- bfd_mach_o_read_dylib() and bfd_mach_o_read_dylinker() --
do not have this problem: they compute each field's own absolute file position
and re-read it fresh via bfd_mach_o_alloc_and_read(), which appends and zeroes
a trailing NUL byte. read_prebound_dylib is the one reader left slicing a
single already-read buffer with raw pointer arithmetic.
A patch is attached that makes bfd_mach_o_read_prebound_dylib() follow the same
pattern as its siblings: read name_str and the linked_modules bit vector each
at its own validated, load-command-relative file offset via
bfd_mach_o_alloc_and_read(), instead of pointer arithmetic into a shared
buffer. The linked_modules read is bounded to ceil(nmodules / 8) bytes
(computed in a 64-bit type so it cannot wrap on an ILP32 host) and checked
against the remaining command length. The same patch was also sent to the
[email protected] mailing list.
AI assistance was used in preparing this report and patch; both were reviewed
and verified by a human.
--
You are receiving this mail because:
You are on the CC list for the bug.