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.

  1. Installez rust nightly (nécessaire pour cargo fuzz) et initialisez l'infrastructure de fuzzing de cargo-fuzz :
$ rustup toolchain add nightly
$ cargo +nightly fuzz init
$ ls fuzz/fuzz_targets/
fuzz_target_1.rs
  1. Éditez fuzz_target_1.rs pour passer le tableau de u8 qui sera généré par l'infrastructure de cargo-fuzz à votre fonction check_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.

  2. 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

  1. 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.

  1. 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.