
UAFuzz: Fuzzing dirigé au niveau binaire pour les vulnérabilités Use-After-Free
Le fuzzing dirigé en boîte grise (Directed Greybox Fuzzing, DGF) comme AFLGo a pour but d’effectuer des tests de contrainte sur des emplacements cibles potentiellement vulnérables présélectionnés, avec des applications dans différents contextes de sécurité : (1) reproduction de bugs, (2) test de correctifs ou (3) vérification de rapports d’analyse statique. Des travaux de recherche récents ont amélioré l’efficacité et l’efficience du fuzzing dirigé (voir awesome-directed-fuzzing).
Nous proposons UAFuzz, un fuzzer dirigé dédié aux bugs Use-After-Free (UAF) au niveau binaire en ajustant soigneusement les composants clés du fuzzing dirigé pour répondre aux caractéristiques spécifiques de cette classe de bugs. Les bugs UAF surviennent lorsqu’un élément du tas est utilisé après avoir été libéré. Détecter les bugs UAF est difficile : (1) complexité car une entrée de preuve de concept (PoC) doit déclencher une séquence de trois événements – allocation, libération et utilisation – sur le même emplacement mémoire, traversant plusieurs fonctions du programme testé, et (2) silence sans défaut de segmentation.
Globalement, UAFuzz suit un flux de travail similaire à celui des fuzzers dirigés, nos modifications étant surlignées en orange tout au long du processus de fuzzing, comme le montre la figure suivante. Comme nous nous concentrons sur (1) la reproduction de bugs et (2) le test de correctifs, il est plus probable que nous disposions de traces de pile (presque) complètes de tous les événements UAF liés à la mémoire. Contrairement aux approches dirigées générales existantes où les cibles peuvent être sélectionnées indépendamment, nous prenons en compte la relation entre les cibles (par exemple, l’ordre, essentiel pour les UAF) pour améliorer le caractère dirigé. Premièrement, le précalcul statique d’UAFuzz est rapide au niveau binaire. Deuxièmement, nous introduisons de nouvelles métriques d’entrée conscientes de l’ordre pour guider le fuzzer vers les cibles à l’exécution. Enfin, nous ne trions que les entrées potentielles couvrant toutes les cibles dans la trace attendue et nous pré-filtrons les entrées libres qui sont moins susceptibles de déclencher le bug.
Plus de détails dans notre article à RAID'20 et notre présentation à Black Hat USA'20. Merci également à Sébastien Bardin, Matthieu Lemerre, le Prof. Roland Groz et tout particulièrement Richard Bonichon (@rbonichon) pour son aide sur Ocaml.
Notre environnement de test est Ubuntu 16.04 64 bits.
# Installer Ocaml et les paquets prérequis pour BINSEC via OPAM
sudo apt update
sudo apt update
sudo apt install ocaml ocaml-native-compilers camlp4-extra opam emacs llvm-6.0-dev pkg-config protobuf-compiler libgmp-dev libzmq3-dev cmake valgrind
opam init
opam switch 4.05.0
opam depext conf-m4.1
opam install merlin ocp-indent caml-mode tuareg menhir ocamlgraph ocamlfind piqi zmq.5.0.0 zarith llvm.6.0.0
eval `opam config env`
# Installer les paquets Python (Python 2 pour les scripts IDA)
sudo python -m pip install networkx pydot
sudo apt install graphviz
# Installer Graph Easy
wget https://cpan.metacpan.org/authors/id/S/SH/SHLOMIF/Graph-Easy-0.76.tar.gz
tar xzf Graph-Easy-0.76.tar.gz
cd Graph-Easy-0.76
perl Makefile.PL; make test; sudo make install
export GRAPH_EASY_PATH=/usr/local/bin/graph-easy
# Récupérer le code source
git clone https://github.com/strongcourage/uafuzz.git
# Variables d’environnement
export IDA_PATH = /path/to/ida-6.9/idaq
export GRAPH_EASY_PATH=/path/to/graph-easy
cd uafuzz; export UAFUZZ_PATH=`pwd`
# Compiler le code source
./scripts/build.sh uafuzz
# Aide pour l’interface IDA/UAFuzz
./binsec/src/binsec -ida-help
./binsec/src/binsec -uafuzz-help
Notre fuzzer est construit sur AFL v2.52b en mode QEMU pour le fuzzing et sur BINSEC pour l’analyse statique légère (voir uafuzz/README.md). Nous utilisons actuellement IDA Pro v6.9 pour extraire les graphes de flot de contrôle (CFG) et le graphe d’appels du binaire testé (voir ida/README.md).
uafuzz
├── binsec/src
│ └── ida : un plugin pour importer et traiter les CFG et le graphe d’appels d’IDA
│ └── uafuzz : code de fuzzing
│ │ └── afl-2.52b : noyau de fuzzing construit sur AFL-QEMU
│ │ └── uafuzz_*.ml(i) : un plugin pour calculer les informations statiques et communiquer avec AFL-QEMU
└── scripts : quelques scripts pour la construction et le tri des bugs
Considérons d’abord un bug UAF simple. AFL-QEMU et même le fuzzer dirigé AFLGo avec des cibles au niveau source ne peuvent pas détecter ce bug en 6 heures, alors qu’UAFuzz peut le détecter en quelques minutes à l’aide d’un rapport UAF de Valgrind.
# Exécuter AFL-QEMU
$UAFUZZ_PATH/tests/example.sh aflqemu 360
# Exécuter AFLGo avec des cibles au niveau source
$UAFUZZ_PATH/tests/example.sh aflgo 360
# Exécuter UAFuzz
$UAFUZZ_PATH/tests/example.sh uafuzz 360 $UAFUZZ_PATH/tests/example/example.valgrind
Pour les programmes réels, nous utilisons le UAF Fuzzing Benchmark pour nos évaluations.
# Récupérer le benchmark
git clone https://github.com/strongcourage/uafbench.git
cd uafbench; export UAFBENCH_PATH=`pwd`
Nous détaillons comment exécuter UAFuzz pour la reproduction du bug CVE-2018-20623 de readelf (Binutils). Les traces de pile de ce bug UAF obtenues par Valgrind sont les suivantes :
// trace de pile pour la mauvaise Utilisation
==5358== Invalid read of size 1
==5358== at 0x40A9393: vfprintf (vfprintf.c:1632)
==5358== by 0x40A9680: buffered_vfprintf (vfprintf.c:2320)
==5358== by 0x40A72E0: vfprintf (vfprintf.c:1293)
[6] ==5358== by 0x80AB881: error (elfcomm.c:43)
[5] ==5358== by 0x8086217: process_archive (readelf.c:19409)
[1] ==5358== by 0x80868EA: process_file (readelf.c:19588)
[0] ==5358== by 0x8086B01: main (readelf.c:19664)
// trace de pile pour la Libération
==5358== Address 0x4221dc0 is 0 bytes inside a block of size 80 free'd
==5358== at 0x402D358: free (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
[4] ==5358== by 0x8086647: process_archive (readelf.c:19524)
[1] ==5358== by 0x80868EA: process_file (readelf.c:19588)
[0] ==5358== by 0x8086B01: main (readelf.c:19664)
// trace de pile pour l'Allocation
==5358== Block was alloc'd at
==5358== at 0x402C17C: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
[3] ==5358== by 0x80AD97E: make_qualified_name (elfcomm.c:906)
[2] ==5358== by 0x8086350: process_archive (readelf.c:19435)
[1] ==5358== by 0x80868EA: process_file (readelf.c:19588)
[0] ==5358== by 0x8086B01: main (readelf.c:19664)
Le script de prétraitement prend le binaire testé en x86 et les traces de pile de Valgrind comme entrées, puis génère la trace de bug UAF, qui est une séquence d’emplacements cibles au format (basic_block_address,function_name) comme suit :
[0] (0x8086ae1,main) -> [1] (0x80868de,process_file) -> [2] (0x808632c,process_archive) ->
[3, alloc] (0x80ad974,make_qualified_name) -> [4, free] (0x808663a,process_archive) ->
[5] (0x808620b,process_archive) -> [6, use] (0x80ab86a,error)
Nous fournissons un modèle de script de fuzzing avec plusieurs paramètres d’entrée, par exemple le fuzzer à exécuter, le délai d’attente en minutes et les cibles prédéfinies (par exemple, extraites du rapport de bug). Pour l’exemple ci-dessus, nous utilisons le script CVE-2018-20623.sh et exécutons UAFuzz comme suit :
# Exécuter UAFuzz avec un délai d’attente de 60 minutes
$UAFBENCH_PATH/CVE-2018-20623.sh uafuzz 60 $UAFBENCH_PATH/valgrind/CVE-2018-20623.valgrind
Après le délai d’attente du fuzzing, UAFuzz peut identifier quelles entrées couvrent séquentiellement tous les emplacements cibles de la trace de bug UAF attendue (par exemple, nom d’entrée se terminant par ',all'). Ainsi, UAFuzz ne trie que ces types d’entrées susceptibles de déclencher le bug désiré en utilisant des outils de profilage existants comme Valgrind ou AddressSanitizer.
Nous utilisons CVE-2018-6952 de GNU Patch pour illustrer l’importance de produire différentes entrées uniques déclencheuses de bugs pour favoriser le processus de correction. Il y avait un double free dans GNU Patch qui a été corrigé par les développeurs (commit 9c98635). Cependant, en utilisant les traces de pile de CVE-2018-6952, UAFuzz a découvert une correction de bug incomplète CVE-2019-20633 de la dernière version 2.7.6 (commit 76e7758), avec une légère différence dans la trace du bug. Globalement, le processus est similaire à l’application de reproduction de bugs, sauf qu’un certain travail manuel peut être nécessaire pour identifier la trace de bug UAF cible. Nous utilisons les entrées PoC des bugs existants et des fichiers valides de fuzzing-corpus comme graines de haute qualité.
# Fuzzer la version corrigée de CVE-2018-6952
$UAFBENCH_PATH/CVE-2019-20633.sh uafuzz 360 $UAFBENCH_PATH/valgrind/CVE-2018-6952.valgrind
Une approche hybride possible consiste à combiner UAFuzz avec GUEB, qui est le seul analyseur statique au niveau binaire pour UAF écrit en Ocaml. Cependant, GUEB produit de nombreux faux positifs et n’est actuellement pas capable de fonctionner correctement avec des binaires complexes. Nous améliorons donc et intégrons actuellement GUEB dans BINSEC, puis utilisons les cibles extraites des rapports de GUEB pour guider UAFuzz. Restez à l’écoute !