[[https://aflplus.plus/|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 [[https://lcamtuf.coredump.cx/afl/|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 [[https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/INSTALL.md|INSTALL.md]] {{ :prog:fuzzer:afl:aflplusplus_docs_install.md_at_stable_aflplusplus_aflplusplus_05_10_2023_11_29_47_.html |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 {{ prog:fuzzer:afl:dict_pdf.txt |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 : [[https://stackoverflow.com/questions/3545875/custom-gcc-preprocessor|Custom gcc preprocessor]] {{ :prog:fuzzer:afl:custom_gcc_preprocessor_-_stack_overflow_2020-04-27_10_50_12_am_.html |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é. #!/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 #!/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 {{ prog:fuzzer:afl:testcase.c |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 : #include "string.h" int main () { return strcmp("eiau", "eiuaeu"); } Sans ''-no-integrated-cpp'' : afl-cc 2.07b by afl-as 2.07b by [+] Instrumented 1 locations (64-bit, non-hardened mode, ratio 100%). Et avec ''-no-integrated-cpp'' : afl-cc 2.07b by afl-as 2.07b by [+] 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. - Une compilation : ''AFL_NO_BUILTIN=1 afl-gcc str_cmp.c'' - Un fichier texte qui contient une longueur suffisante pour le test du mot de passe. - Une exécution : ''AFL_TOKEN_FILE=%%"%%/tmp/dictionary%%"%% LD_PRELOAD=libtokencap.so ./a.out testfile'' - Un fichier généré qui contient : ''%%"%%sesameetiusraneitusrneiuraetsrinau%%"%%'' - Une recompilation : ''afl-gcc str_cmp.c'' - 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 : - ''readelf -p .rodata a.out'' (de préférence), - ''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 [[http://www.qemu.org|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=== #include #include #include #include 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=== {{:prog:fuzzer:afl:str_cmp_fuzzing.png|}} {{:prog:fuzzer:afl:str_cmp_plot.png|}} 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 : {{ prog:fuzzer:afl:dict_alphabet.txt |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 [[https://stackoverflow.com/questions/3211595/renaming-files-in-a-folder-to-sequential-numbers/37826687|Renaming files in a folder to sequential numbers]] {{ :prog:fuzzer:afl:bash_-_renaming_files_in_a_folder_to_sequential_numbers_-_stack_overflow_2020-04-26_7_09_25_pm_.html |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. [[https://stackoverflow.com/questions/14643589/code-coverage-using-gcov-on-parallel-run|Code coverage using gcov on parallel run]] {{ :prog:fuzzer:afl:c_-_code_coverage_using_gcov_on_parallel_run_-_stack_overflow_2020-04-26_7_09_43_pm_.html |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 -j9 … cd 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 {{prog:fuzzer:afl:poppler-0.43.0.tar.xz|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. {{prog:fuzzer:afl:1.pdf|PDF1}}, {{prog:fuzzer:afl:2.pdf|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 : {{prog:fuzzer:afl:afl-multithread.png|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 : {{prog:fuzzer:afl:1.png?200|afl-plot 1}}{{prog:fuzzer:afl:2.png?200|afl-plot 2}}{{prog:fuzzer:afl:3.png?200|afl-plot 3}}{{prog:fuzzer:afl:4.png?200|afl-plot 4}}{{prog:fuzzer:afl:5.png?200|afl-plot 5}}{{prog:fuzzer:afl:6.png?200|afl-plot 6}}{{prog:fuzzer:afl:7.png?200|afl-plot 7}}{{prog:fuzzer:afl:8.png?200|afl-plot 8}} =====L'après afl-fuzz===== ====Trier les crash==== Pour cela, il est possible d'utiliser le logiciel [[https://github.com/bansan85/triage-crash|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. ./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 /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