
american fuzzy lop - un fuzzer orienté sécurité
Initialement développé par Michal Zalewski [email protected].
Voir QuickStartGuide.txt si vous n'avez pas le temps de lire ce fichier.
Le fuzzing est l'une des stratégies les plus puissantes et éprouvées pour identifier des problèmes de sécurité dans des logiciels réels ; il est responsable de la vaste majorité des bugs d'exécution de code à distance et d'escalade de privilèges découverts à ce jour dans les logiciels critiques pour la sécurité.
Malheureusement, le fuzzing est également relativement superficiel ; des mutations aléatoires aveugles rendent très improbable l'atteinte de certains chemins de code dans le code testé, laissant certaines vulnérabilités hors de portée de cette technique.
De nombreuses tentatives ont été faites pour résoudre ce problème. L'une des premières approches - initiée par Tavis Ormandy - est la distillation de corpus. La méthode repose sur des signaux de couverture pour sélectionner un sous-ensemble de graines intéressantes à partir d'un corpus massif et de haute qualité de fichiers candidats, puis les fuzzer par des moyens traditionnels. Cette approche fonctionne exceptionnellement bien, mais exige qu'un tel corpus soit facilement disponible. De plus, les mesures de couverture de blocs ne fournissent qu'une compréhension très simpliste de l'état du programme et sont moins utiles pour guider l'effort de fuzzing sur le long terme.
D'autres recherches plus sophistiquées se sont concentrées sur des techniques telles que l'analyse de flux de programme (« exécution concolique »), l'exécution symbolique ou l'analyse statique. Toutes ces méthodes sont extrêmement prometteuses dans des contextes expérimentaux, mais tendent à souffrir de problèmes de fiabilité et de performance en usage pratique - et n'offrent actuellement pas d'alternative viable aux techniques de fuzzing « stupide ».
American Fuzzy Lop est un fuzzer par force brute couplé à un algorithme génétique guidé par instrumentation, extrêmement simple mais d'une solidité à toute épreuve. Il utilise une forme modifiée de couverture d'arêtes pour détecter sans effort les changements subtils et à petite échelle du flux de contrôle du programme.
En simplifiant un peu, l'algorithme global peut se résumer ainsi :
Charger les cas de test initiaux fournis par l'utilisateur dans la file d'attente,
Prendre le fichier d'entrée suivant dans la file d'attente,
Tenter de réduire le cas de test à la plus petite taille qui ne modifie pas le comportement mesuré du programme,
Muter le fichier de manière répétée en utilisant une variété équilibrée et bien étudiée de stratégies de fuzzing traditionnelles,
Si l'une des mutations générées entraîne une nouvelle transition d'état enregistrée par l'instrumentation, ajouter la sortie mutée comme nouvelle entrée dans la file d'attente.
Aller en 2.
Les cas de test découverts sont également purgés périodiquement pour éliminer ceux qui ont été rendus obsolètes par des découvertes plus récentes et à plus haute couverture ; ils subissent également plusieurs autres étapes de minimisation de l'effort pilotées par l'instrumentation.
Comme résultat secondaire du processus de fuzzing, l'outil crée un petit corpus autonome de cas de test intéressants. Ceux-ci sont extrêmement utiles pour alimenter d'autres régimes de test exigeants en ressources ou en main-d'œuvre - par exemple, pour les tests de résistance des navigateurs, des applications bureautiques, des suites graphiques ou des outils à code source fermé.
Le fuzzer est minutieusement testé pour offrir des performances immédiates nettement supérieures au fuzzing aveugle ou aux outils basés uniquement sur la couverture.
Lorsque le code source est disponible, l'instrumentation peut être injectée par un outil compagnon qui fonctionne comme un remplacement direct de gcc ou clang dans tout processus de compilation standard pour du code tiers.
L'instrumentation a un impact sur les performances assez modeste ; combinée aux autres optimisations implémentées par afl-fuzz, la plupart des programmes peuvent être fuzzés aussi vite, voire plus vite, qu'avec les outils traditionnels.
La manière correcte de recompiler le programme cible peut varier selon les spécificités du processus de compilation, mais une approche quasi universelle serait :```shell $ CC=/path/to/afl/afl-gcc ./configure $ make clean all
Pour les programmes C++, vous voudrez également définir `CXX=/path/to/afl/afl-g++`.
Les wrappers clang (afl-clang et afl-clang++) peuvent être utilisés de la même manière ;
les utilisateurs de clang peuvent également opter pour un mode d'instrumentation plus performant,
comme décrit dans llvm_mode/README.llvm.
Lors du test de bibliothèques, vous devez trouver ou écrire un programme simple qui lit
les données depuis stdin ou depuis un fichier et les transmet à la bibliothèque testée. Dans un
tel cas, il est essentiel de lier cet exécutable à une version statique de la
bibliothèque instrumentée, ou de s'assurer que le bon fichier .so est chargé au
runtime (généralement en définissant `LD_LIBRARY_PATH`). L'option la plus simple est une
compilation statique, généralement possible via :```shell
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Définir AFL_HARDEN=1 lors de l'appel de 'make' fera que le wrapper CC
activera automatiquement les options de durcissement du code qui facilitent la détection
de simples bugs mémoire. Libdislocator, une bibliothèque d'assistance incluse avec AFL (voir
libdislocator/README.dislocator) peut également aider à découvrir les problèmes de corruption du tas.
PS. Il est conseillé aux utilisateurs d'ASAN de consulter le fichier notes_for_asan.txt pour des mises en garde importantes.
Lorsque le code source n'est PAS disponible, le fuzzer offre un support expérimental pour l'instrumentation rapide et à la volée des binaires en boîte noire. Ceci est accompli avec une version de QEMU fonctionnant dans le mode moins connu d'"émulation d'espace utilisateur".
QEMU est un projet distinct d'AFL, mais vous pouvez facilement construire la fonctionnalité en faisant :```shell $ cd qemu_mode $ ./build_qemu_support.sh
Pour des instructions et mises en garde supplémentaires, voir qemu_mode/README.qemu.
Le mode est environ 2 à 5 fois plus lent que l'instrumentation au moment de la compilation, se prête moins bien à la parallélisation et peut présenter quelques autres particularités.
## 5) Choisir les cas de test initiaux
Pour fonctionner correctement, le fuzzer nécessite un ou plusieurs fichiers de départ contenant un bon exemple des données d'entrée normalement attendues par l'application ciblée. Il y a deux règles de base :
- Gardez les fichiers petits. Sous 1 kB est idéal, bien que ce ne soit pas strictement nécessaire.
Pour une discussion sur l'importance de la taille, voir [perf_tips.txt](https://github.com/google/afl/blob/HEAD/docs/perf_tips.txt).
- N'utilisez plusieurs cas de test que s'ils sont fonctionnellement différents les uns des autres.
Il est inutile d'utiliser cinquante photos de vacances différentes pour fuzzer une bibliothèque d'images.
Vous pouvez trouver de nombreux bons exemples de fichiers de départ dans le sous-répertoire testcases/ fourni avec cet outil.
PS. Si un grand corpus de données est disponible pour le filtrage, vous pouvez utiliser l'utilitaire afl-cmin pour identifier un sous-ensemble de fichiers fonctionnellement distincts qui exercent différents chemins de code dans le binaire cible.
## 6) Fuzzer des binaires
Le processus de fuzzing lui-même est effectué par l'utilitaire afl-fuzz. Ce programme nécessite un répertoire en lecture seule contenant les cas de test initiaux, un emplacement séparé pour stocker ses résultats, ainsi qu'un chemin vers le binaire à tester.
Pour les binaires cibles qui acceptent l'entrée directement depuis stdin, la syntaxe habituelle est :```shell
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
Pour les programmes qui prennent leur entrée depuis un fichier, utilisez '@@' pour indiquer l'emplacement dans la ligne de commande de la cible où le nom du fichier d'entrée doit être placé. Le fuzzer le remplacera pour vous :```shell $ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
Vous pouvez également utiliser l'option -f pour que les données mutées soient écrites dans un
fichier spécifique. Cela est utile si le programme attend une extension de fichier particulière ou quelque chose du genre.
Les binaires non instrumentés peuvent être fuzzés en mode QEMU (ajoutez -Q dans la ligne
de commande) ou dans un mode traditionnel de fuzzer aveugle (spécifiez -n).
Vous pouvez utiliser -t et -m pour remplacer le délai d'attente et la limite de mémoire par défaut pour le
processus exécuté ; de rares exemples de cibles qui peuvent nécessiter de modifier ces paramètres
incluent les compilateurs et les décodeurs vidéo.
Des conseils pour optimiser les performances du fuzzing sont abordés dans [perf_tips.txt](https://github.com/google/afl/blob/HEAD/docs/perf_tips.txt).
Notez qu'afl-fuzz commence par exécuter une série d'étapes de fuzzing déterministes,
qui peuvent prendre plusieurs jours, mais qui tendent à produire des cas de test soignés. Si vous
voulez des résultats rapides et grossiers immédiatement - à l'instar de zzuf et d'autres
fuzzers traditionnels - ajoutez l'option -d à la ligne de commande.
## 7) Interprétation de la sortie
Consultez le fichier [status_screen.txt](https://github.com/google/afl/blob/HEAD/docs/status_screen.txt) pour des informations sur
la façon d'interpréter les statistiques affichées et de surveiller la santé du processus.
Assurez-vous de consulter ce fichier en particulier si des éléments de l'interface utilisateur sont surlignés en
rouge.
Le processus de fuzzing se poursuivra jusqu'à ce que vous appuyiez sur Ctrl-C. Au minimum, vous voulez
permettre au fuzzer de terminer un cycle de file d'attente, ce qui peut prendre de quelques
heures à une semaine environ.
Trois sous-répertoires sont créés dans le répertoire de sortie et mis à jour
en temps réel :
- queue/ - cas de test pour chaque chemin d'exécution distinct, plus tous les
fichiers de départ fournis par l'utilisateur. Il s'agit du corpus synthétisé
mentionné dans la section 2.
Avant d'utiliser ce corpus à d'autres fins, vous pouvez le réduire
à une taille plus petite à l'aide de l'outil afl-cmin. L'outil trouvera
un sous-ensemble plus petit de fichiers offrant une couverture d'arêtes équivalente.
- crashes/ - cas de test uniques qui amènent le programme testé à recevoir un
signal fatal (par ex., SIGSEGV, SIGILL, SIGABRT). Les entrées sont
groupées par signal reçu.
- hangs/ - cas de test uniques qui provoquent un dépassement de délai du programme testé. La
limite de temps par défaut avant qu'un incident soit classé comme un hang est
le maximum entre 1 seconde et la valeur du paramètre -t.
La valeur peut être affinée en définissant AFL_HANG_TMOUT, mais cela
est rarement nécessaire.
Les crashes et les hangs sont considérés comme « uniques » si les chemins d'exécution associés
impliquent des transitions d'état non observées dans les fautes précédemment enregistrées. Si un
même bug peut être atteint de plusieurs manières, il y aura une certaine inflation du compteur
au début du processus, mais cela devrait rapidement diminuer.
Les noms de fichiers des crashes et des hangs sont corrélés avec les entrées
de file d'attente parentes et non fautives. Cela devrait aider au débogage.
Lorsque vous ne pouvez pas reproduire un crash trouvé par afl-fuzz, la cause la plus probable est
que vous ne définissez pas la même limite de mémoire que celle utilisée par l'outil. Essayez :```shell
$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10]; /path/to/tested_binary ... )
Modifiez LIMIT_MB pour qu'il corresponde au paramètre -m passé à afl-fuzz. Sous OpenBSD, changez aussi -Sv en -Sd.
Un répertoire de sortie existant peut également être utilisé pour reprendre des tâches interrompues ; essayez :```shell $ ./afl-fuzz -i- -o existing_output_dir [...etc...]
Si vous avez gnuplot installé, vous pouvez également générer quelques jolis graphiques pour n'importe quelle
tâche de fuzzing active à l'aide d'afl-plot. Pour un exemple de ce à quoi cela ressemble,
voir [http://lcamtuf.coredump.cx/afl/plot/](http://lcamtuf.coredump.cx/afl/plot/).
## 8) Fuzzing parallélisé
Chaque instance d'afl-fuzz occupe à peu près un cœur. Cela signifie que sur les
systèmes multi-cœurs, la parallélisation est nécessaire pour utiliser pleinement le matériel.
Pour des conseils sur la façon de fuzzer une cible commune sur plusieurs cœurs ou sur plusieurs machines
en réseau, veuillez consulter [parallel_fuzzing.txt](https://github.com/google/afl/blob/HEAD/docs/parallel_fuzzing.txt).
Le mode de fuzzing parallèle offre également un moyen simple d'interfacer AFL avec d'autres
fuzzers, des moteurs d'exécution symbolique ou concolique, et ainsi de suite ; encore une fois, voir la
dernière section de [parallel_fuzzing.txt](https://github.com/google/afl/blob/HEAD/docs/parallel_fuzzing.txt) pour des conseils.
## 9) Dictionnaires de fuzzing
Par défaut, le moteur de mutation d'afl-fuzz est optimisé pour les formats de données compacts —
par exemple, les images, le multimédia, les données compressées, la syntaxe des expressions régulières ou les
scripts shell. Il est un peu moins adapté aux langages avec un vocabulaire particulièrement verbeux et
redondant — notamment HTML, SQL ou JavaScript.
Pour éviter les tracas liés à la création d'outils conscients de la syntaxe, afl-fuzz fournit un moyen de
semer le processus de fuzzing avec un dictionnaire facultatif de mots-clés de langage,
d'en-têtes magiques ou d'autres jetons spéciaux associés au type de données ciblé
— et de l'utiliser pour reconstruire la grammaire sous-jacente à la volée :
[http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html](http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html)
Pour utiliser cette fonctionnalité, vous devez d'abord créer un dictionnaire dans l'un des deux
formats discutés dans dictionaries/README.dictionaries ; puis pointer le fuzzer
vers celui-ci via l'option -x dans la ligne de commande.
(Plusieurs dictionnaires courants sont déjà fournis dans ce sous-répertoire, également.)
Il n'y a aucun moyen de fournir des descriptions plus structurées de la
syntaxe sous-jacente, mais le fuzzer comprendra probablement une partie de cela sur la base du
retour d'instrumentation seul. Cela fonctionne en pratique, par exemple :
[http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html](http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html)
PS. Même lorsqu'aucun dictionnaire explicite n'est fourni, afl-fuzz essaiera d'extraire
les jetons de syntaxe existants dans le corpus d'entrée en observant l'instrumentation
de très près lors des basculements de bits déterministes. Cela fonctionne pour certains types de
parseurs et de grammaires, mais ce n'est pas aussi efficace que le mode -x.
Si un dictionnaire est vraiment difficile à obtenir, une autre option est de laisser AFL fonctionner
pendant un certain temps, puis d'utiliser la bibliothèque de capture de jetons fournie comme utilitaire
compagnon d'AFL. Pour cela, voir libtokencap/README.tokencap.
## 10) Triage des crashes
Le regroupement des crashes basé sur la couverture produit généralement un petit ensemble de données qui
peut être rapidement trié manuellement ou avec un script GDB ou Valgrind très simple.
Chaque crash est également traçable jusqu'à son cas de test parent non crashant dans la
file d'attente, ce qui facilite le diagnostic des défauts.
Cela dit, il est important de reconnaître que certains crashes de fuzzing peuvent être
difficiles à évaluer rapidement pour leur exploitabilité sans beaucoup de débogage et de
travail d'analyse de code. Pour faciliter cette tâche, afl-fuzz prend en charge un mode très
unique d'"exploration de crash" activé avec le drapeau -C.
Dans ce mode, le fuzzer prend un ou plusieurs cas de test crashants comme entrée,
et utilise ses stratégies de fuzzing guidées par le retour d'information pour énumérer très
rapidement tous les chemins de code qui peuvent être atteints dans le programme tout en le maintenant dans
l'état de crash.
Les mutations qui n'entraînent pas de crash sont rejetées ; de même, toute modification qui
n'affecte pas le chemin d'exécution est rejetée.
La sortie est un petit corpus de fichiers qui peuvent être examinés très rapidement pour voir
quel degré de contrôle l'attaquant a sur l'adresse fautive, ou s'il
est possible de dépasser une lecture hors limites initiale — et de voir ce qui se trouve
en dessous.
Oh, encore une chose : pour la minimisation des cas de test, essayez afl-tmin. L'outil
peut être utilisé de manière très simple :```shell
$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]
L'outil fonctionne aussi bien avec des cas de test crashants que non crashants. En mode crash, il accepte volontiers les binaires instrumentés et non instrumentés. En mode non crashant, le minimiseur s'appuie sur l'instrumentation AFL standard pour simplifier le fichier sans modifier le chemin d'exécution.
Le minimiseur accepte la syntaxe -m, -t, -f et @@ d'une manière compatible avec afl-fuzz.
Une autre nouveauté récente d'AFL est l'outil afl-analyze. Il prend un fichier d'entrée, tente d'inverser séquentiellement les octets, et observe le comportement du programme testé. Il colore ensuite l'entrée en fonction des sections qui semblent critiques et de celles qui ne le sont pas ; sans être infaillible, il peut souvent offrir des indications rapides sur des formats de fichiers complexes. Plus d'informations sur son fonctionnement se trouvent vers la fin de technical_details.txt.
Le fuzzing est une technique merveilleuse et sous-exploitée pour découvrir également des erreurs de conception et d'implémentation non crashantes. Un certain nombre de bugs intéressants ont été trouvés en modifiant les programmes cibles pour appeler abort() lorsque, par exemple :
Deux bibliothèques bignum produisent des sorties différentes lorsqu'on leur fournit la même entrée générée par le fuzzer,
Une bibliothèque d'images produit des sorties différentes lorsqu'on lui demande de décoder la même image d'entrée plusieurs fois de suite,
Une bibliothèque de sérialisation / désérialisation ne parvient pas à produire des sorties stables lors de sérialisations et désérialisations itératives de données fournies par le fuzzer,
Une bibliothèque de compression produit une sortie incohérente avec le fichier d'entrée lorsqu'on lui demande de compresser puis décompresser un blob particulier.
La mise en œuvre de ces contrôles de cohérence ou de contrôles similaires prend généralement très peu de temps ; si vous êtes le mainteneur d'un paquet particulier, vous pouvez rendre ce code conditionnel avec #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION (un indicateur également partagé avec libfuzzer) ou #ifdef __AFL_COMPILER (celui-ci est spécifique à AFL).
Veuillez garder à l'esprit que, comme pour de nombreuses autres tâches intensives en calcul, le fuzzing peut mettre votre matériel et le système d'exploitation à rude épreuve. En particulier :
Votre processeur va chauffer et nécessitera un refroidissement adéquat. Dans la plupart des cas, si le refroidissement est insuffisant ou cesse de fonctionner correctement, la vitesse du processeur sera automatiquement réduite. Cela dit, surtout lors de fuzzing sur du matériel moins adapté (ordinateurs portables, smartphones, etc.), il n'est pas totalement impossible que quelque chose grille.
Les programmes ciblés peuvent finir par s'accaparer de manière erratique des gigaoctets de mémoire ou remplir l'espace disque de fichiers indésirables. AFL essaie d'imposer des limites de mémoire de base, mais ne peut pas empêcher chaque incident possible. En fin de compte, vous ne devriez pas faire de fuzzing sur des systèmes où la perspective d'une perte de données n'est pas un risque acceptable.
Le fuzzing implique des milliards de lectures et d'écritures sur le système de fichiers. Sur les systèmes modernes, cela sera généralement fortement mis en cache, ce qui donne des E/S « physiques » assez modestes - mais de nombreux facteurs peuvent modifier cette équation. Il vous incombe de surveiller les problèmes potentiels ; avec des E/S très lourdes, la durée de vie de nombreux HDD et SSD peut être réduite.
Une bonne façon de surveiller les E/S disque sous Linux est la commande 'iostat' :```shell $ iostat -d 3 -x -k [...optional disk ID...]
## 13) Limitations connues et pistes d'amélioration
Voici quelques-unes des mises en garde les plus importantes concernant AFL :
- AFL détecte les défauts en vérifiant si le premier processus engendré meurt
à cause d'un signal (SIGSEGV, SIGABRT, etc.). Les programmes qui installent
des gestionnaires personnalisés pour ces signaux peuvent nécessiter que le
code correspondant soit commenté. De même, les défauts dans les processus
enfants engendrés par la cible fuzzée peuvent échapper à la détection, à
moins d'ajouter manuellement du code pour les intercepter.
- Comme pour tout autre outil de force brute, le fuzzer offre une couverture
limitée si le chiffrement, les sommes de contrôle, les signatures
cryptographiques ou la compression sont utilisés pour envelopper entièrement
le format de données réel à tester.
Pour contourner ce problème, vous pouvez commenter les vérifications
concernées (voir experimental/libpng_no_checksum/ pour vous inspirer) ;
si cela n'est pas possible, vous pouvez également écrire un postprocesseur,
comme expliqué dans experimental/post_library/.
- Il existe quelques compromis regrettables entre ASAN et les binaires 64 bits.
Cela n'est pas dû à un défaut spécifique d'afl-fuzz ; voir [notes_for_asan.txt](https://github.com/google/afl/blob/HEAD/docs/notes_for_asan.txt)
pour des conseils.
- Il n'existe pas de prise en charge directe pour fuzzer les services réseau,
les démons en arrière-plan ou les applications interactives qui nécessitent
une interaction avec l'interface utilisateur. Vous devrez peut-être apporter
de simples modifications au code pour qu'ils se comportent de manière plus
traditionnelle. Preeny peut également offrir une option relativement simple
- voir : https://github.com/zardus/preeny
Quelques conseils utiles pour modifier les services réseau sont également
disponibles sur :
https://www.fastly.com/blog/how-to-fuzz-server-american-fuzzy-lop
- AFL ne produit pas de données de couverture lisibles par un humain. Si vous
souhaitez surveiller la couverture, utilisez afl-cov de Michael Rash :
https://github.com/mrash/afl-cov
- Parfois, les machines douées de conscience se soulèvent contre leurs
créateurs. Si cela vous arrive, veuillez consulter
http://lcamtuf.coredump.cx/prep/.
Au-delà de cela, voir INSTALL pour des conseils spécifiques à chaque plateforme.
## 14) Remerciements particuliers
Nombre des améliorations apportées à afl-fuzz n'auraient pas été possibles sans
les retours, les rapports de bogues ou les correctifs de :```
Jann Horn Hanno Boeck
Felix Groebert Jakub Wilk
Richard W. M. Jones Alexander Cherepanov
Tom Ritter Hovik Manucharyan
Sebastian Roschke Eberhard Mattes
Padraig Brady Ben Laurie
@dronesec Luca Barbato
Tobias Ospelt Thomas Jarosch
Martin Carpenter Mudge Zatko
Joe Zbiciak Ryan Govostes
Michael Rash William Robinet
Jonathan Gray Filipe Cabecinhas
Nico Weber Jodie Cunningham
Andrew Griffiths Parker Thompson
Jonathan Neuschfer Tyler Nighswander
Ben Nagy Samir Aguiar
Aidan Thornton Aleksandar Nikolich
Sam Hakim Laszlo Szekeres
David A. Wheeler Turo Lamminen
Andreas Stieger Richard Godbee
Louis Dassy teor2345
Alex Moneger Dmitry Vyukov
Keegan McAllister Kostya Serebryany
Richo Healey Martijn Bogaard
rc0r Jonathan Foote
Christian Holler Dominique Pelle
Jacek Wielemborek Leo Barnes
Jeremy Barnes Jeff Trull
Guillaume Endignoux ilovezfs
Daniel Godas-Lopez Franjo Ivancic
Austin Seipp Daniel Komaromy
Daniel Binderman Jonathan Metzman
Vegard Nossum Jan Kneschke
Kurt Roeckx Marcel Bohme
Van-Thuan Pham Abhik Roychoudhury
Joshua J. Drake Toby Hutton
Rene Freingruber Sergey Davidoff
Sami Liedes Craig Young
Andrzej Jackowski Daniel Hodson
Merci !
Des questions ? Des préoccupations ? Des rapports de bogues ? Veuillez utiliser GitHub.
Il existe également une liste de diffusion pour le projet ; pour vous y inscrire, envoyez un courriel à [email protected]. Ou, si vous préférez consulter les archives d'abord, essayez : https://groups.google.com/group/afl-users.