Fuzzing de la bibliothèque
Vos collègues vous rappellent que leur application est vraiment critique et vous demandent si votre bibliothèque est robuste. Avant de leur affirmer que oui (après tout elle est simplissime), vous souhaitez le vérifier grâce à du fuzzing.
- Installez rust nightly (nécessaire pour
cargo fuzz
) et initialisez l'infrastructure de fuzzing decargo-fuzz
:
$ rustup toolchain add nightly
$ cargo +nightly fuzz init
$ ls fuzz/fuzz_targets/
fuzz_target_1.rs
-
Éditez
fuzz_target_1.rs
pour passer le tableau deu8
qui sera généré par l'infrastructure decargo-fuzz
à votre fonctioncheck_elf::is_valid()
. Vous pouvez ignorer le résultat, ce qui vous intéresse c'est de savoir si cette fonction peut générer une erreur ou non. -
Lancez le fuzzing sur cette cible avec
cargo +nightly fuzz run fuzz_target_1
.
Aïe. Il semblerait que cargo-fuzz
ait trouvé quelque chose. Dans son message, on trouve notamment :
SUMMARY: AddressSanitizer: heap-buffer-overflow (…/check_elf/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0xf13c3) in elf_rs::Elf::from_bytes::h873e2a0b13bfed22
Tentative d'identification du problème
- Lancez la commande proposée pour essayer de réduire la taille de l'exemple (
cargo +nightly fuzz tmin …
).
À ce stade, on dispose normalement d'un fichier de 52 octets qui provoque un buffer overflow dans notre fonction check_elf::is_valid()
. Pourtant, on peut constater que notre programme d'exemple a l'air de bien se comporter (le nom du fichier peut être différent chez vous) :
$ cargo run --example check_elf fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77 is a valid ELF file
Mais est-ce vraiment le cas ? On peut utiliser valgrind
pour détecter les accès à des zones mémoires protégées ou non initialisées :
$ valgrind target/debug/examples/check_elf fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
==3673885== Memcheck, a memory error detector
==3673885== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3673885== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==3673885== Command: target/debug/examples/check_elf fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
==3673885==
==3673885== Invalid read of size 2
==3673885== at 0x113495: elf_rs::elf_header::ElfHeaderGen<T>::elf_header_size (elf_header.rs:79)
==3673885== by 0x113235: elf_rs::Elf::from_bytes (lib.rs:51)
==3673885== by 0x11307E: check_elf::is_valid_elf (lib.rs:6)
==3673885== by 0x1114CD: check_elf::is_valid_elf_file (lib.rs:13)
==3673885== by 0x111C26: check_elf::main (check_elf.rs:3)
[…]
fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77 is a valid ELF file
Il semblerait que cela passe inaperçu dans notre programme d'exemple car on accède à de la mémoire qui est dans une page lisible, mais valgrind
nous indique qu'en vérité nous lisons deux octets dans une zone invalide et que cette erreur survient dans le code d'une fonction interne elf_header_size()
du crate elf_rs
que nous utilisons. C'est un véritable problème, car si le fichier avait été chargé à la fin d'une page mémoire et que la page suivante n'est pas accessible, nous aurions eu un segmentation fault.
D'ailleurs contrairement à ce que dit notre programme d'exemple le fichier n'est pas un fichier ELF valide. On peut utiliser le programme readelf
(qui fait partie des binutilshttps://fr.wikipedia.org/wiki/GNU_Binutils) pour s'en convaincre :
$ readelf -a fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
readelf: Error: fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77: Failed to read file header
Correction du problème
En fouillant un peu, on s'aperçoit qu'une bonne âme a signalé et corrigé le problème en juillet 2020 mais que la personne en charge du crate n'a pas une nouvelle release (0.2.0) qu'en décembre 2021.
- Indiquez dans
Cargo.toml
que vous souhaitez utiliser une version incorporant cette correction en provenance du dépôt git :
[dependencies]
elf_rs = { git = "https://github.com/vincenthouyi/elf_rs", rev = "d040e18b4cc6b87139f6856802e9dca3d298f726" }
On remarquera déjà un changement de comportement de notre programme d'exemple :
$ cargo run --example check_elf fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77 is not a valid ELF file
Nous pouvons maintenant relancer cargo-fuzz
pour vérifier qu'aucune autre erreur fatale n'est trouvée. Nous pouvons le laisser tourner longtemps ; même si cela ne prouve rien (l'absence de preuve de bug n'est pas une preuve de l'absence de bug) cela nous rend confiance. Vous pouvez maintenant livrer cette formidable bibliothèque check_elf
à vos collègues.
Remarques sur le fuzzer
On peut constater que le fuzzer a rapidement trouvé un cas problématique. Si on regarde le fichier plus en détail, on s'aperçoit qu'il ressemble à un fichier ELF classiques :
$ file fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77: ELF 64-bit (embedded)
$ hexdump -C fuzz/artifacts/fuzz_target_1/crash-e5ae4a7fe39c514786461c1fa699479e58f0eb77
00000000 7f 45 4c 46 02 ff ff ff 46 ff ff 28 28 28 28 cd |.ELF....F..((((.|
00000010 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 |((((((((((((((((|
00000020 28 28 28 28 28 28 28 28 28 ff ff ff ff ff ff ff |(((((((((.......|
00000030 ff ff ff ff |....|
00000034
On remarque notamment le marqueur "magique" d'un fichier ELF sur les 4 premiers caractères suivis de l'indication du format 64 bits. Le fuzzer a vite produit un résultat plausible en explorant les branchements.
Conclusion
La conclusion de ce travail ne fait pas trop de doute : il vaut mieux oublier complètement le crate elf_rs
qui ne semble pas activement maintenu et utiliser plutôt le crate xmas-elf
qui semble de meilleure facture, voire le crate goblin
qui est la Rolls-Royce de la manipulation de fichiers exécutables.