Outils pour utilisateurs

Outils du site


prog:fuzzer:afl

AFL++ cherche à vérifier l'intégrité d'un logiciel ou d'une librairie dans le cas d'ouverture de fichiers.

Il est le successeur de AFL qui a été abandonné.

Compilation de AFL++

La compilation nécessite Python et donc l'utilisation d'un environnement virtuel.

cd AFLplusplus
python3 -m venv .venv
source .venv/bin/activate
pip install wheel
make distrib

INSTALL.md Archive du 19/09/2023 le 05/10/2023

Programme à tester

Cas général

Configurer la compilation

cd program

puis compiler au choix en fonction du programme :

  • configure
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as ./configure
  • cmake
mkdir build
cd build
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as cmake ..

Compiler

make -j 8

Lancer le fuzzing

Il faut mettre une liste de fichiers de base dans le dossier testcases.

Dans le dossier findings seront listés tous les cas altérés ayant eux un chemin unique dans le programme.

mkdir findings testcases
afl-fuzz -i testcases -o findings -- program/bin/executable @@

Ici, le double arobase @@ représente le fichier altéré généré par AFL.

Toutes les informations après ce tag concerne l'ancienne version de afl.

Sanitizers

Un programme compilé peut ne pas forcément planter en cas d'accès à une zone en dehors d'un tableau (par exemple) si cette zone reste dans la même page mémoire ou si la page suivante est aussi allouée par le même processus.

Les sanitizers en 64 bits demandent une taille de mémoire virtuelle de 20To. Il ne sera pas possible de sécuriser AFL en cas de bug d'allocation mémoire.

  • AFL_USE_ASAN : -U_FORTIFY_SOURCE -fsanitize=address. Incompatible avec AFL_USE_MSAN et AFL_HARDEN.
  • AFL_USE_MSAN : -U_FORTIFY_SOURCE -fsanitize=memory. Incompatible avec AFL_USE_ASAN et AFL_HARDEN.
  • AFL_HARDEN : -fstack-protector-all -D_FORTIFY_SOURCE=2
  • AFL_USE_UBSAN : -fsanitize=undefined -fsanitize-undefined-trap-on-error -fno-sanitize-recover=all -fno-omit-frame-pointer
  • AFL_USE_TSAN : -fsanitize=thread -fno-omit-frame-pointer
  • AFL_USE_LSAN : -fsanitize=leak
  • AFL_USE_CFISAN : -fcf-protection=full pour gcc, -fsanitize=cfi -fvisibility=hidden pour clang

Comme on est dans un environnement multithread, on compile une version de programme pour chaque sanitizer qui serviront lors du fuzzing (voir ci-après).

CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_USE_ASAN=1 AFL_INST_RATIO=100 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_USE_MSAN=1 AFL_INST_RATIO=100 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_HARDEN=1 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_USE_UBSAN=1 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_USE_TSAN=1 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_USE_LSAN=1 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar AS=llvm-as AFL_USE_CFISAN=1 CFLAGS="-g -fno-omit-frame-pointer" CXXFLAGS="-g -fno-omit-frame-pointer" cmake ..

Si AFL_USE_ASAN ou AFL_USE_MSAN est utilisé, automatiquement, afl ne surveille de 33% (aléatoirement) des branches. Il faut mettre AFL_INST_RATIO à 100 pour surveiller toutes les branches.

Une chute de performance est à prévoir. Je conseille donc de lancer une exécution de afl-fuzz en mode esclave de chaque sanitizer et toutes les autres instances en parallèle sans ces vérifications.

Dictionnaire

Il est possible de donner des mots clés prédéfinis. Il suffit de mettre les mots-clés, un par ligne avec un “ en début de fin de ligne.

Pour les PDF, une extraction presque automatique est possible :

# Extraction des mots-clés
grep lookup * -R |grep "(\"" > ../dict
grep strcmp * -R >> ../dict
grep strncmp * -R >> ../dict
 
# Isolation des mots-clés
:%s/^.*lookup("\(.*\)".*$/\/\1/cg
:%s/^.*lookupNF("\(.*\)".*$/\/\1/cg
:%s/^.*"\(.*\)".*$/\/\1/cg
 
# Ajout des guillemets
:%s/^\(.*\)$/"\1"/cg

Ce qui donne le fichier dict_pdf.txt

Et on l'ajoute à afl-fuzz avec l'argument -x.

Le dictionnaire n'est géré que par le maitre.

Si on analyse des fichiers texte, ajouter la variable d'environnement AFL_NO_ARITH=1 à afl-fuzz pour le maître évite les manipulations de nombres binaires arith et interest.

Si on dépasse 200 mots-clés (MAX_DET_EXTRAS du fichier config.h), afl-fuzz ne travaille plus de façon déterministe. Les 200 premiers plus fréquents sont pris puis les suivants ont une probabilité de 200/n d'être analyser.

memcmp, strcmp et ses dérivées

Ces fonctions ne sont pas bien optimisées par afl car elles sont opaques (librairie système, utilisation des SSE4.2). Il est peut nécessaire de remplacer par une fonction basique : une boucle.

Cette technique est nécessaire si on n'utilise pas le dictionnaire. Si les mots sont connus à l'avance, l'utilisation d'une boucle va ralentir le programme et augmenter fortement le nombre de chemins.

Hack du préprocesseur

La solution, modifier le préprocesseur pour surcharger ces fonctions : Custom gcc preprocessor Archive du 23/08/2010 le 27/04/2020

Ci-dessous pour la version 7.3.0 de gcc.

Le script ci-dessous peut poser des problèmes si configure ou autre utilise gcc -E. Il faudrait corriger ça quand le problème sera identifié.

cc1
#!/bin/sh
 
#echo "My own special preprocessor -- $@"
 
/usr/libexec/gcc/x86_64-pc-linux-gnu/7.3.0/cc1 "$@"
r=$?
if [ "$1" = "-E" ]; then
 
  sed -i "s/\(extern int strcasecmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strcasecmp_disable\)/extern int tolower (int __c); static int strcasecmp(const char* str1, const char* str2) { while (1) { unsigned char c1 = tolower(*str1), c2 = tolower(*str2); if (c1 \!= c2) return (c1 > c2) ? 1 : -1; if (\!c1) return 0; str1++; str2++; } }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int strncasecmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strncasecmp_disable\)/static int strncasecmp(const char* str1, const char* str2, size_t len) { char u1, u2; for (; len \!= 0; --len) { u1 = *str1++; u2 = *str2++; if (tolower(u1) \!= tolower(u2)) { return tolower(u1) - tolower(u2); } if (u1 == '\\\0') { return 0; } } return 0; }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int memcmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*memcmp_disable\)/static int memcmp(const void* mem1, const void* mem2, size_t len) { while (len--) { unsigned char c1 = *(const char*)mem1, c2 = *(const char*)mem2; if (c1 \!= c2) return (c1 > c2) ? 1 : -1; mem1++; mem2++; } return 0; }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int strcmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strcmp_disable\)/static int strcmp(const char* str1, const char* str2) { while (1) { unsigned char c1 = *str1, c2 = *str2; if (c1 \!= c2) return (c1 > c2) ? 1 : -1; if (\!c1) return 0; str1++; str2++; } }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int strncmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strncmp_disable\)/static int strncmp(const char* str1, const char* str2, size_t len) { while ( *str1 \&\& len \&\& ( *str1 == *str2 ) ) { ++str1; ++str2; --len; } if ( ( len == 0 ) ) { return 0; } else { return ( *(unsigned char *)str1 - *(unsigned char *)str2 ); } }\n\1/g" ${@: -1}
 
fi
 
exit $r
cc1plus
#!/bin/sh
 
#echo "My own special preprocessor -- $@"
 
/usr/libexec/gcc/x86_64-pc-linux-gnu/7.3.0/cc1plus "$@"
r=$?
if [ "$1" = "-E" ]; then
 
  sed -i "s/\(extern int strcasecmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strcasecmp_disable\)/extern int tolower (int __c); static int strcasecmp(const char* str1, const char* str2) { while (1) { unsigned char c1 = tolower(*str1), c2 = tolower(*str2); if (c1 \!= c2) return (c1 > c2) ? 1 : -1; if (\!c1) return 0; str1++; str2++; } }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int strncasecmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strncasecmp_disable\)/static int strncasecmp(const char* str1, const char* str2, size_t len) { char u1, u2; for (; len \!= 0; --len) { u1 = *str1++; u2 = *str2++; if (tolower(u1) \!= tolower(u2)) { return tolower(u1) - tolower(u2); } if (u1 == '\\\0') { return 0; } } return 0; }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int memcmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*memcmp_disable\)/static int memcmp(const void* mem1, const void* mem2, size_t len) { while (len--) { unsigned char c1 = *(const char*)mem1, c2 = *(const char*)mem2; if (c1 \!= c2) return (c1 > c2) ? 1 : -1; mem1++; mem2++; } return 0; }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int strcmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strcmp_disable\)/static int strcmp(const char* str1, const char* str2) { while (1) { unsigned char c1 = *str1, c2 = *str2; if (c1 \!= c2) return (c1 > c2) ? 1 : -1; if (\!c1) return 0; str1++; str2++; } }\n\1/g" ${@: -1}
 
  sed -i "s/\(extern int strncmp\>\)/\1_disable/g" ${@: -1}
  sed -i "s/^\(.*strncmp_disable\)/static int strncmp(const char* str1, const char* str2, size_t len) { while ( *str1 \&\& len \&\& ( *str1 == *str2 ) ) { ++str1; ++str2; --len; } if ( ( len == 0 ) ) { return 0; } else { return ( *(unsigned char *)str1 - *(unsigned char *)str2 ); } }\n\1/g" ${@: -1}
 
fi
 
exit $r

testcase.c pour vérifier que les boucles précédentes marchent bien.

Il va falloir copier les fichiers cc1 et cc1plus dans le même dossier que le fichier as de afl (le dossier de compilation ou /usr/lib64/afl/ ou autre).

Ne pas oublier :

chmod +x /usr/lib64/afl/cc1*

Et ajouter pour les options de compilation -no-integrated-cpp pour forcer l'utilisation d'une version spécifique de cc1 et cc1plus.

Exemple :

main.c
#include "string.h"
 
int main ()
{
  return strcmp("eiau", "eiuaeu");
}

Sans -no-integrated-cpp :

afl-cc 2.07b by <lcamtuf@google.com>
afl-as 2.07b by <lcamtuf@google.com>
[+] Instrumented 1 locations (64-bit, non-hardened mode, ratio 100%).

Et avec -no-integrated-cpp :

afl-cc 2.07b by <lcamtuf@google.com>
afl-as 2.07b by <lcamtuf@google.com>
[+] Instrumented 122 locations (64-bit, non-hardened mode, ratio 100%).

Ainsi, la boucle est bien surveillée. L'intérêt de convertir les strcmp* par des boucles, c'est quand le contenu de la comparaison est dynamique. Si le contenu est statique et en dur dans le programme, on peut les récupérer via la libtokencap (cf ci-dessous).

libtokencap

Une librairie inclue dans afl a été développée. Voir le REAMDE dans le code source.

Prenons le cas de l'exemple ci-après (str_cmp.c) qui contient le mot de passe en dur.

  1. Une compilation : AFL_NO_BUILTIN=1 afl-gcc str_cmp.c
  2. Un fichier texte qui contient une longueur suffisante pour le test du mot de passe.
  3. Une exécution : AFL_TOKEN_FILE="/tmp/dictionary" LD_PRELOAD=libtokencap.so ./a.out testfile
  4. Un fichier généré qui contient : "sesameetiusraneitusrneiuraetsrinau"
  5. Une recompilation : afl-gcc str_cmp.c
  6. Lancement du fuzzing : AFL_NO_ARITH=1 afl-fuzz -i testcases -o findings -x dictionary – ./a.out @@

Le mot de passe est trouvé quasi instantanément.

La librairie prend tous les textes qui passent par strcmp et compagnie et si le pointeur se trouve dans une zone mémoire en lecture seule, cela signifie que c'est du texte en dur dans une librairie ou un exécutable.

Je trouve l'idée intéressante.

Mais le même résultat aurait pu être trouvé en créant de dictionnaire avec l'une des commandes :

  1. readelf -p .rodata a.out (de préférence),
  2. strings a.out ou strings -d a.out (à défaut).

Le dictionnaire est beaucoup plus gros (tous les strings sont répertoriés et pas seulement ceux utilisés dans les strcmp*) mais ça peut être une méthode alternative.

Je conseille donc d'utiliser libtokencap quand on possède le code source et une base de données d'exemples suffisamment remplie et d'utiliser les autres commandes si l'une des deux conditions n'est pas respectée.

Sans code source

afl-fuzz utilise qemu.

Depuis le code source, il suffit d'exécuter le script qemu_mode/build_qemu_support.sh et le programme afl-qemu-trace se télécharge et se compile tout seul.

L'utilisation d'afl est absolument identique. Il suffit d'ajouter l'argument -Q à afl-fuzz.

Sur le plan théorique, ça marche, je n'ai jamais testé.

TODO

Application Windows : Wine

Cela ne marche pas encore c'est un bon début. Wine version 2.6.

TODO : à mettre à jour.

Préparation

tar xvaf wine-2.6.tar.xz
mkdir build
cd build
../wine-2.6/configure --disable-tests

Modifs

1

Il faut juste applique ce patch :

vim tools/winegcc/Makefile
-DCC="\"gcc -m32\" => "\"gcc\"
DCXX="\"g++ \
  -m32\"" -DLD
=> DCXX="\"g++\"" -DLD

pour éviter un message d'erreur :

cc1: erreur : unrecognized command line option « -m32" »
2
sed -i "s/-nodefaultlibs//g" loader/Makefile
3

wine ne compile pas en version 64 bits : wine-2.7/tools/winebuild/import.c:628 génère un :

output_get_pc_thunk: Assertion `target_cpu == CPU_x86' failed.
4

wine possède un debugger interne. Il faut l'empêcher de fonctionner. La méthode la plus simple est de forcer une exception dans le fichier dlls/kernel32/except.c, méthode start_debugger :

 format_exception_msg( epointers, buffer, sizeof(buffer) );
+int* x = (int*)0x12345678;
+*x = 1;
 MESSAGE("wine: %s (thread %04x), starting debugger...\n", buffer, GetCurrentThreadId());

fichier dlls/ntdll/signal_i386.c, méthode raise_exception :

 if (status != DBG_CONTINUE)
 {
+  int* x = (int*)0x12345678;
+  *x = 1;
   if (rec->ExceptionFlags & EH_STACK_INVALID)

et dans la méthode setup_exception_record, avant les deux WINE_ERR( "stack overflow :

int* x = (int*)0x12345678;
*x = 1;
5 - Désactiver l'utilisation du preloader sous Linux

Fichier : libs/wine/config.c, méthode wine_exec_wine_binary

//#ifdef linux
//    use_preloader = !strendswith( name, "wineserver" );
//#else
    use_preloader = 0;
//#endif

Compilation

CC="afl-gcc -m32" CXX="afl-g++ -m32" CFLAGS="-g -O0 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -no-integrated-cpp" CXXFLAGS="-g -O0 -no-integrated-cpp" make -j9

Conclusion

Mais l'exécution de la commande de fuzzing ne trouve qu'un seul chemin car l'exécutable n'est pas instrumenté.

afl-fuzz -t 500 -i testcases -o findings -- .../wine-afl/build/loader/wine main.exe @@

et malgré le plantage avec succès de wine, il n'y a pas d'information de debug.

Cas d'études

Placer dans testcases un ou plusieurs exemples valides ou non (mais pas jusqu'au crash de l'application) de taille la plus petite possible (1024 octets maximum de préférence, grand maxi 10ko). Il est inutile de mettre deux exemples si ce n'est pas dans un objectif précis. Deux photos d'un même appareil sont inutiles. Par contre, générer un PDF avec seulement du texte puis un autre avec seulement une petite image peut présenter un intérêt.

Fuzzing

Exemple

Source

str_cmp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
char *tmp;
 
int main(int argc, char** argv) {
  FILE *f = fopen(argv[1], "rb");
  fseek(f, 0, SEEK_END);
  long fsize = ftell(f);
  fseek(f, 0, SEEK_SET);  //same as rewind(f);
 
  tmp = malloc(fsize + 1);
  fread(tmp, fsize, 1, f);
  fclose(f);
 
  tmp[fsize] = 0;
 
  if (fsize < sizeof("sesameetiusraneitusrneiuraetsrinau")-1) {
    exit(1);
  }
  if (!memcmp(tmp, "sesameetiusraneitusrneiuraetsrinau", sizeof("sesameetiusraneitusrneiuraetsrinau")-1)) abort();
  else printf("Bad password.\n");
 
  return 0;
 
}

Compilation

Pas besoin des sanitizers vu qu'on analyse uniquement memcmp.

afl-gcc str_cmp.c -no-integrated-cpp -g -fno-omit-frame-pointer -o str_cmp_complet

Fuzzing

afl-fuzz -i testcases -o findings -- ./str_cmp_complet @@

Résultat

Trouver un texte de plus de 32 caractères est compliqué :

  • Ça a demandé plus de 3 millions de test !! Une autre exécution a trouvé le crash en 1,3 million de test, ce qui reste beaucoup.
  • Il faut patcher le programme. Par défaut, afl bloque la taille des chaînes de caractères à 32. Il faut modifier le #define MAX_AUTO_EXTRA 32 à une valeur plus grande. Par exemple #define MAX_AUTO_EXTRA 40.

Cependant, il est possible d'accélérer le processus en utilisant un dictionnaire qui contient 26 entrées : les lettres de l’alphabet. Résultat : 850k de tests.

AFL_NO_ARITH=1 afl-fuzz -i testcases -o findings -x dict -- ./str_cmp_complet @@

La non utilisation de AFL_NO_ARITH=1 va entrainer la nécessité de 1,5M de tests.

afl-fuzz

Écran

Pour que l'analyse soit au minimum suffisante, il faut que le nombre de cycles done en haut à droite soit au minimum de 1 ce qui peut prendre plusieurs mois sur des programmes non triviaux. Ensuite, une fois le premier cycle passé, les suivants sont un peu plus rapide : les chemins parcourus sont souvent les mêmes que dans les cycles précédents.

Dans l'idéal, il faudrait attendre que le pending paths atteigne 0 mais là, il faudrait peut-être attendre plusieurs années.

TODO

calibration splice trim

bitflip arith (ne se fait pas si la variable environnement AFL_NO_ARITH est définie. Utile pour des fichiers texte). interest (ne se fait pas si la variable environnement AFL_NO_ARITH est définie. Utile pour des fichiers texte). extras (dictionnary)

havoc splice sync

Environnement multithread

Il convient alors de lancer un processus afl-fuzz en mode maitre (-M fuzzer01) et tous les autres en mode esclave (-S fuzzerXX). Le nom donné servira de sous-dossier à l'intérieur de findings. Par exemple dans le cas de 8 threads :

  • Pour tous les threads :

Ajouter AFL_NO_ARITH=1 avant afl-fuzz si le fichier à analyser est totalement sous forme de texte.

  • Le maître :
afl-fuzz -M fuzzer01 -i testcases -o findings -- program/bin/program-normal @@

Si nécessaire rajouter l'option -x dict si un dictionnaire peut aider.

  • Les esclaves sanitizer
afl-fuzz -S fuzzer02 -i testcases -o findings -m none -- program/bin/program-asan @@
afl-fuzz -S fuzzer03 -i testcases -o findings -m none -- program/bin/program-msan @@
afl-fuzz -S fuzzer04 -i testcases -o findings -- program/bin/program-harden @@
afl-fuzz -S fuzzer05 -i testcases -o findings -- program/bin/program-undefined @@
afl-fuzz -S fuzzer06 -i testcases -o findings -- program/bin/program-cfi @@
  • Les autres esclaves
afl-fuzz -S fuzzer07 -i testcases -o findings -- program/bin/program-normal @@
afl-fuzz -S fuzzer08 -i testcases -o findings -- program/bin/program-normal @@

Et si on veut éviter que le système ne soit pas bloqué, on peut ajouter un nice -n 19 avant.

Exploitations diverses des résultats

Le dossier queue

Il contient l'ensemble de tous les fichiers ayant permis d'atteindre un chemin unique. Il peut être utilisé de manière intelligente pour auditer un logiciel fermé lorsque celui-ci possède un équivalent open-source. En commençant par travailler avec la version open-source, on obtient une liste de fichiers menant à de très nombreux chemins tous différents (pour le logiciel open-source). Ensuite, il convient d'auditer le logiciel fermé sur la base de l'ensemble de fichiers précédemment générés. Il est possible que certains fichiers fasse crasher l'application dès le départ ou, à défaut, permettent de gagner du temps lors de l'analyse du logiciel au code source fermé.

Le dossier hangs

Il contient les fichiers dont le temps d'exécution de l'ouverture dépasse le timeout.

Le dossier crashes

Il contient les fichiers dont l'ouverture crashe l'application.

Analyse graphique

afl-plot permet aussi de voir l'avancement de l'analyse en fonction du temps.

mkdir output_html
afl-plot findings output_html

Crash trouvé

Une fois un ou plusieurs crashs trouvés, il peut être intéressant de rechercher sur la base de ces fichiers les autres crashs à proximité ayant un chemin différent. Pour cela, enlever les tests sources et les remplacer par ceux ayant entrainés un crash. Vider le dossier findings et relancer la commande d'analyse en rajoutant l'option -C à afl-fuzz.

Réduire la taille des cas

On cherche un fichier qui possède le même chemin qui le cas étudié mais de la taille la plus petite possible.

afl-tmin -t 1000+ -m none -i ../../findings/fuzzer02/crashes/id\:000001\,sig\:06\,src\:000006\,op\:havoc\,rep\:8 -o 000001.small.pdf -- ./pdftohtml @@ /tmp/

ou sa version parallèle :

find crashes -name "id:*" | parallel "afl-tmin -t 1000+ -m none -i {} -o {}.small.pdf -- ./pdftohtml @@ /tmp/"
[+] Read 3466 bytes from '../../findings/fuzzer02/crashes/id:000001,sig:06,src:000006,op:havoc,rep:8'.
[*] Performing dry run (mem limit = 31457280 MB, timeout = 1000 ms)...
[+] Program exits with a signal, minimizing in crash mode.
[*] Stage #0: One-time block normalization...
[+] Block normalization complete, 2890 bytes replaced.
[*] --- Pass #1 ---
[*] Stage #1: Removing blocks of data...
    Block length = 256, remaining size = 3466
    Block length = 128, remaining size = 1674
    Block length = 64, remaining size = 1280
    Block length = 32, remaining size = 768
    Block length = 16, remaining size = 576
    Block length = 8, remaining size = 464
    Block length = 4, remaining size = 416
    Block length = 2, remaining size = 332
    Block length = 1, remaining size = 280
[+] Block removal complete, 3237 bytes deleted.
[*] Stage #2: Minimizing symbols (48 code points)...
[+] Symbol minimization finished, 2 symbols (2 bytes) replaced.
[*] Stage #3: Character minimization...
[+] Character minimization done, 1 byte replaced.
[*] --- Pass #2 ---
[*] Stage #1: Removing blocks of data...
    Block length = 16, remaining size = 229
    Block length = 8, remaining size = 229
    Block length = 4, remaining size = 229
    Block length = 2, remaining size = 229
    Block length = 1, remaining size = 227
[+] Block removal complete, 2 bytes deleted.
[*] Stage #2: Minimizing symbols (46 code points)...
[+] Symbol minimization finished, 0 symbols (0 bytes) replaced.
[*] Stage #3: Character minimization...
[+] Character minimization done, 0 bytes replaced.
[*] --- Pass #3 ---
[*] Stage #1: Removing blocks of data...
    Block length = 16, remaining size = 227
    Block length = 8, remaining size = 227
    Block length = 4, remaining size = 227
    Block length = 2, remaining size = 227
    Block length = 1, remaining size = 227
[+] Block removal complete, 0 bytes deleted.

     File size reduced by : 93.45% (to 227 bytes)
    Characters simplified : 1274.45%
     Number of execs done : 2183
          Fruitless execs : path=1951 crash=0 hang=0

[*] Writing output to '000001.pdf'...
[+] We're done here. Have a nice day!

Couverture de code

Réduire la liste des fichiers à analyser

afl-cmin est là pour ça.

L'utilisation de l'option -e va considérer que deux fichiers génèrent une couverture identique en fonction des lignes couvertes.

Si l'option -e n'est pas utilisé, deux fichiers génèrent une couverture identique en fonction des lignes couvertes et du nombre de passes de chaque ligne. Dans ce cas, le nombre de cas unique va être beaucoup plus important.

afl-cmin utilise des commandes shell pour trier des fichiers.

La première étape est de créer un fichier par source et de mettre une ligne par ligne de code couverte.

La deuxième étape est de créer un fichier unique qui contiendra une concaténation de tous les fichiers créés précédemment en y ajoutant le nom du fichier. Pour 20000 fichiers, j'ai eu droit à un fichier de 4G à trier.

Renommer les fichiers en les numérotant permet de réduire très fortement la taille de ce fichier :

ls | cat -n | while read n f; do mv "$f" "$n"; done

Renaming files in a folder to sequential numbers Archive du 09/07/2010 le 26/04/2020

Ou sinon, on peut décommenter #define SIMPLE_FILES dans le fichier config.h de afl avant l'analyse.

mkdir testcase.cov
afl-cmin -e -i findings/fuzzer02/queue/ -o testcase.cov/ -m 1000000000 -t 1000 -- ./pdftohtml @@ /tmp/fuzz/ii

Par défaut, afl-cmin tri les fichiers pour favoriser les cas les plus petits. Si on souhaite favoriser les fichiers ayant la plus grande couverture de code (en espérant avoir une liste plus petite), il faut appliquer le patch suivant au STEP 3 de afl-cmin.

  -done < <(ls -rS "$IN_DIR")
  +done < <(ls -S "$TRACE_DIR")
[*] Testing the target binary...
[+] OK, 2970 tuples recorded.
[*] Obtaining traces for input files in 'findings/fuzzer02/queue/'...
    Processing file 28200/28200... 
[*] Sorting trace sets (this may take a while)...
[+] Found 28343 unique tuples across 28200 files.
[*] Finding best candidates for each tuple...
    Processing file 28200/28200... 
[*] Sorting candidate list (be patient)...
[*] Processing candidates and writing output files...
    Processing tuple 28343/28343... 
[+] Narrowed down to 2308 files, saved in 'testcase.cov/'.

Options de compilation

Il ne faut pas exécuter le fuzzing avec les options de couverture de code. Il faut analyser les fichiers du dossier queue après l'analyse terminée.

CFLAGS="-fprofile-arcs -ftest-coverage" CXXFLAGS="-fprofile-arcs -ftest-coverage" LDFLAGS="-fprofile-arcs -ftest-coverage"

Création du rapport de couverture

Une fois le programme compilé avec la couverture de code, il faut l'exécuter une fois pour chaque cas :

find testcase.cov/ -exec ./pdftohtml {} /tmp/iie \;

Ne pas utiliser parallel sinon l'écriture des informations de couverture de code sera corrompu.

Code coverage using gcov on parallel run Archive du 01/02/2013 le 26/04/2020

Le rapport final s'obtient en lançant depuis le dossier de compilation :

lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory out_html
firefox out_html/index.html

Il est aussi possible d'afficher les différentes branches de chaque ligne en ajoutant --rc lcov_branch_coverage=1 à lcov et --branch-coverage à genhtml

Fuites mémoire

Les trois dossiers contenant un nombre important d'exemples, il est intéressant de lancer l'application sous surveillance de Valgrind. Si l'application de base possède déjà des fuites mémoires ou est difficilement analysable (GTK, Qt, …), je recommande d'enregistrer tous les logs valgrind et de faire un diff -pu avec le programme de base.

Exemple d'argument pour valgrind (avec utilisation de parallel) :

cd findings
# version hardcore
find . -name "id:*" | LD_LIBRARY_PATH=".../afl-popple/poppler-0.53.0/poppler/.libs:$LD_LIBRARY_PATH" parallel "valgrind --trace-children=yes --track-fds=yes --num-callers=500 --error-limit=no --show-below-main=yes --max-stackframe=2000000 --smc-check=all --read-var-info=yes -v --leak-check=full --show-possibly-lost=yes --leak-resolution=high --show-reachable=yes --undef-value-errors=yes --track-origins=yes ../poppler-0.53.0/utils/.libs/pdftohtml {} /tmp/ &> {}.log"
# version plus rapide sans read-var-info, track-origins surtout :
find . -name "id:*" | LD_LIBRARY_PATH=".../afl-popple/poppler-0.53.0/poppler/.libs:$LD_LIBRARY_PATH" parallel "valgrind -v --leak-check=full --show-leak-kinds=all ../poppler-0.53.0/utils/.libs/pdftohtml {} /tmp/ &> {}.log"

Cas pratique

Prenons le cas de la librairie poppler qui permet de manipuler des fichiers au format PDF.

Commençons par télécharger et installer la librairie

git clone https://anongit.freedesktop.org/git/poppler/poppler.git
cd poppler
mkdir build
cd build
CC=afl-gcc CXX=afl-g++ CFLAGS="-no-integrated-cpp -g -fno-omit-frame-pointer" CXXFLAGS="-no-integrated-cpp -g -fno-omit-frame-pointer" cmake ..

Explications :

  • -no-integrated-cpp : pour forcer l'utilisation des cc1 et cc1plus personnalisé ci-avant pour memcmp, etc…
  • -fno-omit-frame-pointer : pour que les informations de débogage soient optimales.

-fsanitize=address,undefined : n'est pas utilisé car il ralenti de façon non négligeable les délais d'exécution.

Les options seront utilisées sur les cas retenus par afl-fuzz. Il n'est pas impossible que certains cas qui génèrent des violations de mémoire possèdent la même empreinte que les cas n'en générant pas et soient donc ignorés.

make -j9cd utils
mkdir testcases findings

Si on possède une base de données d'exemples, on peut décider d'en analyser le contenu pour ne retenir que les éléments intéressants.

mkdir testcases.vrac
cp … testcases.vrac
mkdir /tmp/fuzzing
afl-cmin -i testcases.vrac/ -o testcases -m 2000 -t 1000 -- ./pdftohtml @@ /tmp/fuzzing/ff

Archive de poppler

Ensuite, il faut générer les fichiers PDF intéressants. Dans le cas présent, j'ai décidé d'utiliser deux fichiers, l'un contenant uniquement un caractère, l'autre uniquement une image de 1 pixel de coté. Il est possible que le jeu de fichiers aurait pu être réduit à un seul fichier contenant l'unique caractère et l'image. PDF1, PDF2

Pour des raisons de simplicité, l'outil pdftohtml sera utilisé. Il décode les textes, images, formes, etc…

Problème si le logiciel a été compilé avec make :

utils/pdftohtml est un wrapper shell. Pour permettre l'exécution correct du programme (qui se trouve dans un dossier caché utils/.libs) par afl-fuzz, il faut modifier la variable d'environnement LD_LIBRARY_PATH. Pour avoir la bonne modification, lire la fin du wrapper et obtenir la ligne

# Add our own library path to LD_LIBRARY_PATH
LD_LIBRARY_PATH=".../afl-2lgc/poppler-0.53.0/poppler/.libs:$LD_LIBRARY_PATH"

Enfin, exécution du maitre et des slaves en multithread :

AFL_NO_ARITH=1 nice -n 19 afl-fuzz -m 1G -t 1000+ -M fuzzer01 -i testcases -o findings -- ./utils/pdftohtml @@ /tmp/fuzzing/ff1
nice -n 19 afl-fuzz -m 1G -t 1000+ -S fuzzer02 -i testcases -o findings -- ./utils/pdftohtml @@ /tmp/fuzzing/ff2
nice -n 19 afl-fuzz -m 1G -t 1000+ -S fuzzer03 -i testcases -o findings -- ./utils/pdftohtml @@ /tmp/fuzzing/ff3
nice -n 19 afl-fuzz -m 1G -t 1000+ -S fuzzer04 -i testcases -o findings -- ./utils/pdftohtml @@ /tmp/fuzzing/ff4

Exemple de rendu en cas d'application en mode multithread :

afl-multithread

On constate que le maître est beaucoup plus lent que les autres puisqu'à mon avis, il s'occupe d'approfondir les cas étudiés par ses esclaves.

On constate aussi que même si les esclaves ont eu le temps de faire 6 cycles complets, le maître n'a pas eu le temps d'en finir un seul (21% environ). L'analyse n'a donc pas terminé un cycle complet puisque c'est le maître qui est le plus important car il effectue les opérations finales d'altération des fichiers. Il est à noter qu'il est possible d'interrompre un esclave (lorsqu'il ne découvre plus de nouveau chemin) sans perturber le maitre.

Analyse graphique dans le cas de l'analyse de poppler :

afl-plot 1afl-plot 2afl-plot 3afl-plot 4afl-plot 5afl-plot 6afl-plot 7afl-plot 8

L'après afl-fuzz

Trier les crash

Pour cela, il est possible d'utiliser le logiciel triage-crash

La syntaxe est inspirée de afl.

Il faut commencer par récupérer l'état de la pile via gdb.

triage-crash gdb --parallel --regex=^id\(?\!.*btfull\).*$ --folder=findings/fuzzer01/crashes --folder=findings/fuzzer02/crashes -- ./utils/pdftohtml @@ /tmp/fuzzing/ff4

Cette commande va créer un fichier .btfull pour chaque fichier id* analysé.

Ensuite, on tri les résultats :

triage-crash sort --parallel --regex=^id.*\.btfull$ --print-one-by-group --folder=findings/fuzzer01/crashes --folder=findings/fuzzer02/crashes

La sortie standard va ressembler à :

Groupe 0
  .../retdec/build/src/fileinfo/crash/fuzzer02/crashes/id_000063_06.btfull
Groupe 1
  .../retdec/build/src/fileinfo/crash/fuzzer02/crashes/id_000419_06.btfull
Groupe 2
  .../retdec/build/src/fileinfo/crash/fuzzer02/crashes/id_000384_06.btfull
Groupe 3
  .../retdec/build/src/fileinfo/crash/fuzzer02/crashes/id_000241_06.btfull
Groupe 4
  .../retdec/build/src/fileinfo/crash/fuzzer02/crashes/id_000356_06.btfull
Groupe 5
  .../retdec/build/src/fileinfo/crash/fuzzer02/crashes/id_000016_06.btfull

Il n'y a donc plus que 6 fichiers à analyser et à corriger. Le tri s'effectue en vérifiant que les deux piles sont identiques, d'où la nécessité des fichiers .btfull.

Vérifier les modifications

Une fois les modifications de code terminées, il faut vérifier à nouveau l'ensemble des fichiers id_* dans le dossier crashes.

Quand un programme plante, bash reçoit le code erreur 139.

retest.sh
./utils/pdftohtml $1 /tmp/fuzzing/ff4
if [ $? -eq 139 ]; then
  echo $1.btfull >> pb.list;
fi

Et lancer la commande :

find findings -name "id_*" -a -not -name "*.btfull" -exec ./retest.sh {} \;

Et trier à nouveau les résultats :

triage-crash sort --parallel --print-one-by-group --list=pb.list

Bugs

TLS transition from ... failed

C'est un bug dans GCC (7.2 pour moi). Il ne supporte pas très bien les 2 sanitizers address et undefined en même temps. Il faut en sacrifier un. Je conseille de garder address pour le fuzzing puis de recompiler l'application avec le sanitizer undefined et d'exécuter via un script tous les queue, hangs et crashes.

afl-cc 2.51b by <lcamtuf@google.com>
/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: ./.libs/libutils.a(utils.o): TLS transition from R_X86_64_TLSGD to R_X86_64_GOTTPOFF against `prng_state' at 0x2108b in section `.text' failed
/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status
make: *** [Makefile:1050: stress-test] Error 1
prog/fuzzer/afl.txt · Dernière modification : 2023/10/06 09:55 de root