
EF/CF - Fuzzing de contrats intelligents extrêmement rapide
EF/CF est une nouvelle approche du fuzzing de contrats intelligents : au lieu d'utiliser un nouveau fuzzer développé sur mesure, il réutilise l'infrastructure de fuzzing existante pour le code C/C++ et l'applique aux contrats intelligents. Actuellement, AFL++ est le fuzzer principalement pris en charge, bien qu'il existe également un support très rudimentaire pour libfuzzer et honggfuzz.
Pourquoi utiliser l'infrastructure de fuzzing existante ?
Quels sont les problèmes que nous rencontrons en cours de route ?
./src/ethmutator/./src/evm2cpp/Ce dépôt est le point d'entrée principal du projet EF/CF. Il contient tout le code pertinent sous forme de sous-projets dans ./src/ et plusieurs scripts pratiques pour l'installation, des scripts pour lancer des campagnes de fuzzing et divers ensembles de données pour tester le fuzzer (et le comparer à d'autres outils).
./src/ - contient tout le code source nécessaire pour construire et exécuter EF/CF ; pour la reproductibilité, toutes les dépendances directes sont ajoutées en tant que sous-modules git../data/ - contient les ensembles de données utilisés lors de l'évaluation./scripts - contient des scripts pour exécuter les expériences, l'installation, etc../docker - Dockerfile pour un flux de travail basé sur des conteneurs
./docker/tools/ contient des dockerfiles pour les outils auxquels nous avons comparé EF/CF. Nous avons fait de notre mieux pour fixer dans les dockerfiles les versions que nous avons évaluées dans notre article../EXPERIMENTS.md - contient un guide pour reproduire les expériences de notre article../examples - contient des exemples de sorties produites par EF/CFNous décrivons l'architecture et l'implémentation d'EF/CF, et résumons les résultats de notre évaluation dans notre article : preprint arxiv.org
Lorsque vous référencez EF/CF dans des travaux académiques, veuillez utiliser l'entrée bibtex suivante pour la citation :```bibtex @InProceedings{efcf2023, author = "Michael Rodler and David Paaßen and Wenting Li and Lukas Bernhard and Thorsten Holz and Ghassan Karame and Lucas Davi", title = "EF/CF: High Performance Smart Contract Fuzzing for Exploit Generation", booktitle = "{IEEE} European Symposium on Security and Privacy ({EuroS&P})", publisher = "{IEEE}", year = "2023", }
## Démarrage rapide
La méthode recommandée est d'exécuter EF/CF dans un conteneur docker interactif.
1. Entrez dans le conteneur avec un shell ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
ou construire le conteneur à partir du dépôt cloné ``` make gitmodules # to fetch the git submodules make container-enter
1. Compilez puis fuzz un contrat Solidity jusqu'à ce que le premier crash/bug soit
découvert : ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
Pas de git ? si vous utilisez une version tarball/docker, ignorez ceci.
Exécutez git submodule update --init pour récupérer les derniers commits des sous-modules sur les dépôts déjà clonés.
Assurez-vous d'exécuter ceci aussi dans ./src/eEVM.```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*Attention :* Exécuter `git clone --recursive $repo` ou passer l'argument `--recursive` à `git sumbodule (update|init)` fera récurser git dans les sous-modules du dépôt AFL++, qui ne sont pas nécessaires pour ce projet. Ainsi, pour économiser de l'espace, il est préférable d'éviter les vérifications récursives des sous-modules.
### Conteneur
Nous fournissons les cibles make pratiques suivantes pour les flux de travail basés sur des conteneurs :```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
Si vous souhaitez garantir une compilation propre, vous pouvez utiliser la commande suivante```sh make container-build CLEAN_CHECKOUT=1
Alternativement, le conteneur peut être construit avec la commande docker suivante :```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
Notez qu'il existe également des Dockerfiles basés sur Archlinux et Fedora. Ils devraient fonctionner, mais ne sont pas aussi bien testés.
Pour distribuer manuellement une image docker (p. ex., si vous incluez des modifications locales), utilisez :``` make container-release docker load -i ./efcf*.tar
Nous recommandons les options Docker suivantes pour le lancement :
* `--security-opt seccomp=unconfined` - meilleures performances de fuzzing
* `--net=host` - pour un accès facile à un nœud Ethereum local
* `--tmpfs "/tmp/efcf/":exec,size=6g` - placer les fichiers temporaires d'EF/CF sur un ramdisk si possible (moins d'usure du disque)
* `--privileged` - pour exécuter `afl-system-config` ou `efcfuzz --configure-system`
* `-v` - pour conserver les données de sortie d'EF/CF
### VM / Bare-Metal
Pour les workflows basés sur une VM ou sur du bare-metal :```sh
make system-install # install efcf to current system (requires root or sudo rights)
Notez que beaucoup de scripts fonctionnent de toute façon avec la structure de répertoires relative, donc
cela installe principalement des dépendances et quelques outils pratiques à avoir dans votre
PATH. Nous avons testé l'exécution d'EF/CF sur les distributions Linux suivantes :
(La distribution n'a pas beaucoup d'importance, nous avons testé LLVM 13 et 14, 14 étant le
choix préféré. LLVM 11 ou 12 pourrait également encore fonctionner, mais comme toujours - plus c'est récent,
mieux c'est. L'important est qu'il y ait un LLVM compatible avec notre fork d'AFL++.)
Nous n'avons pas testé EF/CF nativement sur Mac OS. Il est probable que certaines choses ne fonctionneront pas (par exemple, afl-clang-lto sur Mac OS semble ne pas fonctionner). La meilleure option est d'utiliser docker.```sh
make gitmodules
docker pull ubuntu:jammy --platform linux/amd64
docker build -t efcf:latest -f docker/ubuntu.Dockerfile --platform linux/amd64 .
docker run --tmpfs "/tmp/efcf/":exec,size=8g --platform linux/amd64 --rm -it -v $(pwd):$(pwd) -w $(pwd) efcf:latest
Nous avons testé avec docker desktop v4.21.1 et l'utilisation de base d'EF/CF fonctionne. Cependant, considérez ce qui suit :
* Si vous voyez des segfaults lors de la compilation : essayez d'augmenter la limite de mémoire de la machine virtuelle que Docker utilise sur Mac OS.
* Essayez d'activer l'accélération à l'aide de Rosetta dans Docker - espérons que ce soit un peu plus rapide.
### Configuration de développement
Les outils n'ont généralement pas besoin d'être installés. Installez les dépendances requises
comme dans le script `system-install.sh` ou comme dans les Dockerfiles.
Pour plus de commodité, nous avons quelques scripts pour mettre à jour votre `PATH` :```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
Certains scripts nécessitent une clé API pour récupérer les métadonnées (par exemple, l'ABI) depuis
le service Etherscan. Si vous avez une clé API, vous devez définir la
variable d'environnement ETHERSCAN_API_KEY pour la transmettre aux scripts. Pour
un flux de travail basé sur Docker, vous pouvez soit lancer le conteneur Docker avec
l'option --env, soit placer votre clé API dans le fichier .etherscan_api_key, ce qui
intégrera la clé API dans le conteneur Docker.
Par souci de commodité, nous utilisons un script wrapper qui s'occupe de tous les détails
pour vous, lors du lancement du fuzzer EF/CF : efcfuzz
Vous pouvez définir de nombreuses options de ligne de commande pour configurer le comportement du fuzzer en
ce qui concerne le processus de compilation et de fuzzing. Consultez efcfuzz --help pour une
liste des options.
Exemples
Compilez le code source Solidity en code natif EF/CF et commencez le fuzzing pendant 5 minutes (c'est-à-dire 300 secondes).```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
Alternativement, lancez avec une sortie de fuzzing réduite (`--quiet` supprime la
sortie du fuzzer de base, tandis que `--print-progress` affichera un court résumé de
la progression du fuzzing), et lancez le fuzzer sur 4 cœurs.```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
Utilisez le bytecode déjà compilé, compilez-le en code natif EF/CF et lancez le fuzzing.```bash
pushd ./data/examples/; make baby_bank.combined.json; popd efcfuzz --timeout 300 --bin-runtime ./data/examples/baby_bank.combined.json
pushd ./data/examples/; make baby_bank; popd
efcfuzz --timeout 300
--bin-runtime ./data/examples/baby_bank.bin-runtime
--bin-deploy ./data/examples/baby_bank.bin
--abi ./data/examples/baby_bank.abi
Le wrapper peut exporter l'état d'un contrat depuis un nœud go-ethereum/erigon et commencer
le fuzzing à partir de là.```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
De plus, vous pouvez passer --include-address-deps=y pour rechercher
récursivement les adresses d'autres comptes dans le stockage du contrat
exporté et inclure également celles-ci dans l'export d'état. Cependant, cela
n'inclut pas les autres contrats stockés dans des types mapping de Solidity.
Pour vraiment exporter tout l'état de manière récursive, passez également
l'option --include-mapping-deps=y.
Mais attention, cette recherche récursive peut entraîner de longs temps de
compilation et de mauvaises performances de fuzzing. En particulier, les
contrats fréquemment utilisés peuvent avoir beaucoup d'état interne et
l'utilisation de leur état exporté peut ralentir le fuzzing. Vérifiez si le
fuzzer peut atteindre plus de 1 000 exécutions/s. Si ce n'est pas le cas,
vous devriez plutôt essayer de créer un état artificiel et plus petit.
Essayez d'exécuter un nœud go-ethereum local en mode --dev et d'y déployer
vos contrats. Exportez ensuite l'état actuel depuis ce nœud.
Le wrapper met en cache les builds, donc une deuxième exécution de fuzzing
devrait démarrer beaucoup plus rapidement, car le temps de compilation initial
n'est plus nécessaire. Si vous voulez uniquement compiler et placer le résultat
dans le cache, vous pouvez passer l'argument --build-only.
Exemple : fuzzing avec des propriétés
EF/CF prend également en charge le fuzzing basé sur les propriétés, en utilisant la même définition de propriétés que le fuzzer echidna. Les propriétés (ou invariants) sont exprimées sous forme de fonctions Solidity qui agissent comme un oracle de bugs pour le fuzzer. Par exemple, vous pouvez ajouter une fonction Solidity :```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
Ce qui représente la propriété que le total_balance doit toujours être inférieur à
1000. EF/CF signalera alors un bug s'il parvient à violer cette propriété en utilisant
une séquence de transactions, c'est-à-dire si l'oracle renvoie `false`.
Pour indiquer à EF/CF qu'il s'agit d'une propriété, vous devez spécifier une liste de signatures
de fonctions dans un fichier, qui sera prise en compte par EF/CF comme une liste de propriétés
à vérifier pendant le fuzzing.
Le moyen le plus simple est d'obtenir les signatures pertinentes à l'aide du drapeau `--hashes`
du compilateur Solidity, par exemple,```
solc --hashes ./path/to/your.sol | grep test_property > property_list
Maintenant, vous pouvez lancer le fuzzer avec :``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
Vous pouvez également ajouter `--disable-detectors` pour désactiver les oracles de bogues intégrés basés sur ether.
Vous pouvez essayer l'exemple suivant pour le fuzzing basé sur les propriétés :```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
Exemple : Fuzzing des événements
EF/CF prend en charge le fuzzing pour les violations d'assertion exprimées par
des événements. En fait, nous prenons également en charge l'utilisation d'événements personnalisés arbitraires comme oracle de bogues.
Par défaut, EF/CF identifie un bogue si le contrat cible a journalisé l'un
des événements suivants : AssertionFailed(), AssertionFailed(uint256),
AssertionFailed(string), et Panic(uint256).```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
Vous pouvez également spécifier des événements/sujets personnalisés supplémentaires à surveiller dans un
fichier avec `--event-assertions-list ./path/to/eventslist.txt`. Comme pour la
liste de propriétés précédente, vous pouvez obtenir le format en utilisant `solc --hashes` et
en copiant les hachages et noms d'événements dans le fichier de liste d'événements.
Par défaut, EF/CF ignore les événements qui n'ont pas été émis par le contrat
cible. Si vous souhaitez modifier cela, utilisez `--event-assertions-target-only=n`.
(Remarque : vous pouvez utiliser `--assertions` pour activer à la fois la vérification des événements et des assertions Solidity)
**Exemple : Fuzzing pour les assertions Solidity ^0.8**
Actuellement, nous ne prenons pas en charge le fuzzing pour des assertions arbitraires dans le code Solidity
pour les versions de Solidity inférieures à 0.8. Auparavant, les assertions Solidity déclenchaient simplement
un opcode `invalid`, entraînant un revert plutôt brutal. La version 0.8 de Solidity
a changé le comportement : au lieu d'utiliser l'opcode `invalid` pour
annuler les transactions, ils utilisent désormais le mécanisme `revert` et signalent les erreurs
à l'appelant. Nous pouvons utiliser ce type de propagation d'erreur comme oracle de bug
dans EF/CF. Actuellement, EF/CF prend en charge la vérification du type d'erreur Solidity
`Panic(uint256)`. [Plus d'informations sur les erreurs
Solidity.](https://docs.soliditylang.org/en/v0.8.0/control-structures.html?highlight=assert#panic-via-assert-and-error-via-require)```
efcfuzz --sol-assertions \
--timeout 120 --until-crash \
--source ./data/assertions-tests/overflow.sol
(Remarque : vous pouvez utiliser --assertions pour activer la vérification
des assertions d'événements et de solidité)
Spécifications système et configuration
Nous recommandons d'allouer de 4 à 16 cœurs et environ 1 Go de mémoire par cœur. Vous
pouvez utiliser l'option --configure-system pour configurer votre système pour
un fuzzing à haute vitesse, ou le configurer vous-même. Dans les conteneurs Docker, vous devez également
configurer l'hôte pour des performances optimales. S'il s'agit d'un hôte non critique, vous pouvez
lancer le conteneur avec --privileged et utiliser
/usr/local/bin/afl-system-config pour configurer le système afin d'obtenir un fuzzing
à haute vitesse (notez que cela exécute essentiellement le conteneur en tant que root).```
docker run --rm -it --privileged efcf afl-system-config
docker run --rm -it
--security-opt seccomp=unconfined
--tmpfs "/tmp/efcf/":exec,size=6g
efcf
## Exécution d'une expérience de fuzzing
Pour exécuter l'expérience sur le jeu de données `data/tests/`, vous pouvez utiliser
la commande suivante pour compiler les contrats ainsi que leur harnais de fuzzing, puis
lancer le fuzzer avec différents réglages, plusieurs répétitions, etc. Comme cette
opération peut prendre un certain temps, nous pouvons exécuter ces expériences en parallèle. Nous divisons
les expériences de fuzzing en une étape de compilation et une étape de fuzzing. Les étapes de compilation
compilent tous les contrats intelligents séquentiellement (la compilation elle-même utilise toutefois plusieurs cœurs).
Ensuite, nous lançons 8 instances du fuzzer en arrière-plan, qui utilisent
les artefacts de compilation issus de l'étape de compilation et démarrent les sessions de fuzzing. Le Makefile
tentera automatiquement de lancer le tout dans le conteneur approprié si
`docker` ou `podman` est disponible.```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
Nous désactivons seccomp et le sandboxing réseau lors du lancement des conteneurs en arrière-plan. Désactiver le sandboxing seccomp améliore les performances de fuzzing. L'utilisation du réseau hôte permet à EF/CF d'accéder aux nœuds Ethereum du réseau local sans configuration supplémentaire.
Nous avons utilisé le script ./scripts/run-tools-on-dataset.py pour exécuter les autres outils
dans des conteneurs docker sur ces ensembles de données, par ex. avec ces commandes pour le
dataset multi :```bash
python3 ./scripts/run-tools-on-dataset.py ./data/multi/
cd ./results/tools-multi/
python3 ../../scripts/get-tools-on-dataset-stats.py
head stats.csv
Vous devez adapter le script pour configurer les outils et le nombre d'exécutions.
### Mise en place d'une expérience de fuzzing
Ici, nous utilisons l'expérience `tests` comme exemple. Remplacez simplement la chaîne `tests` par le nom de l'expérience dans les étapes suivantes :
1. Rassemblez votre jeu de données dans `./data/`, par exemple le jeu de données `./data/tests` avec des contrats de test. Pour les contrats Solidity, nous disposons d'un `Makefile` générique pour compiler les contrats : `sol.Makefile`. Vous pouvez le réutiliser si vous le souhaitez, voir `./data/tests/Makefile` pour un exemple.
2. Créez un script pour générer les artefacts de compilation, y compris toute étape de pré-traitement/collecte nécessaire. Par exemple, pour le jeu de données `tests`, nous avons le script `./scripts/build-tests.sh`. Les artefacts de compilation doivent être stockés dans `./builds/tests/${contract}.build.tar.xz`.
3. Créez un script pour lancer la campagne de fuzzing, par exemple pour le jeu de données `tests`, créez un script appelé `./scripts/fuzz-tests.sh`. En général, vous pouvez utiliser la fonction commune de campagne de fuzzing de `./scripts/common.sh`. Regardez `fuzz-tests.sh` comme modèle.
4. Les résultats de `fuzz-tests.sh` seront stockés dans `./results/run-fuzz-tests/`.
5. Pour résumer les résultats, nous fournissons `./scripts/summarize.py` pour les scripts de lancement basés sur bash et `./scripts/summarize_l.py` pour les scripts de lancement Python (l'outil `efcfuzz`). Vous devrez peut-être adapter ces scripts en fonction de votre étape 3.
### Expériences de fuzzing existantes
#### Benchmarks
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/multi">`./data/multi`</a> contient le benchmark de scalabilité
que nous avons utilisé pour évaluer comment un outil d'analyse passe à l'échelle sur des séquences
de transactions plus longues. Il se compose de trois types de contrats :
* `multi_gen_*.sol` - des contrats synthétisés automatiquement, qui effectuent une série
de `require(input <= MAGIC)` puis définissent une variable d'état interne. Si
toutes les variables d'état sont définies, alors le `selfdestruct` (ou oracle echidna)
peut être déclenché.
* `multi_man_complex_*.sol` - des variantes créées manuellement qui fonctionnent de manière similaire
aux contrats de type `multi_gen`, mais présentent des contraintes un peu plus délicates
(par exemple, d'autres choses que l'égalité et l'inégalité avec une valeur
magique)
* `justlen_*.sol` - ceux-ci sont tirés de l'[exemple echidna-parade](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - des vérifications de cohérence qui vérifient qu'un fuzzer/outil peut
en théorie trouver des bogues nécessitant 9 ou 10 transactions. Ici, l'analyseur
doit simplement appeler 10 fonctions dans le bon ordre sans aucun
argument. C'est assez facile pour la plupart des outils d'analyse.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/throughput">`./data/throughput`</a> contient les contrats que nous
avons utilisés pour évaluer le débit. Il s'agit d'une sélection de contrats de
tailles variées. Notez que nous avons corrigé toutes les vulnérabilités de
ces contrats, afin que les vulnérabilités trouvées n'affectent pas les
mesures de débit.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/cov-max-testset">`./data/cov-max-testset`</a> contient les
contrats que nous avons utilisés pour la comparaison des fuzzers basés sur la
couverture de code.
#### Détection de bogues
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/ethbmc-vuln">`./data/ethbmc-vuln`</a> liste des contrats
qu'EthBMC a détectés comme vulnérables.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> liste des
contrats pour lesquels EthBMC a arrêté l'analyse en raison d'un dépassement de délai.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/reentrancy">`./data/reentrancy`</a> un ensemble de contrats
vulnérables aux attaques de réentrance.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> un ensemble de contrats
qui ont été vérifiés comme contenant un bogue de réentrance dans le cadre de
l'[étude sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth).
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/sailfish-dao">`./data/sailfish-dao`</a> liste de tous les contrats,
pour lesquels
[sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs)
a trouvé un bogue de réentrance.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/sereum">`./data/sereum`</a> une liste de contrats
vulnérables aux attaques de réentrance selon [Sereum](https://github.com/uni-due-syssec/sereum-results).
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a>
contrats issus des smartbugs curatés, classés comme bogues de « contrôle d'accès »
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a>
contrats issus des smartbugs curatés, classés comme bogues de « réentrance »
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### Tests
Les jeux de données suivants contiennent des contrats de test synthétiques de base pour tester les capacités du fuzzer :
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/tests">`./data/tests`</a> tests de base rassemblés à partir de
plusieurs sources qui vérifient les capacités de base d'un fuzzer. Tous utilisent un
oracle selfdestruct.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/tests-not-vuln">`./data/tests-not-vuln`</a> identique aux tests, mais
ne devrait pas être détecté comme vulnérable.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/properties-tests">`./data/properties-tests`</a> tests pour
le fuzzing basé sur les propriétés
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/assertions-tests">`./data/assertions-tests`</a> tests pour
le fuzzing des assertions.
## Fuzzing plus en détail
Nous utilisons des scripts wrapper pour lancer le fuzzer réel (AFL++ dans notre cas). Cela se fait automatiquement lors de l'utilisation du lanceur `efcfuzz`.```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
Si vous avez tmux et tmuxp installés, alors pour le développement et l'inspection, la version interactive du script peut être utile :```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
Cela compilera ensuite et fuzzera pendant un certain temps. Vous pouvez ensuite
`cd ./fuzz/out/SimpleDAO*` pour visualiser les résultats du fuzzing. Nos scripts d'encapsulation
effectuent un travail supplémentaire en plus de lancer le programme `afl-fuzz`, c'est-à-dire principalement
le post-traitement des résultats. De plus, ils généreront plusieurs scripts pratiques
pour analyser les cas de test générés.
* `./a.sh` - Affiche une forme lisible d'un cas de test, une surcouche autour de
`efuzzcaseanalyzer`.
* `./r.sh` - exécute un cas de test avec les mêmes paramètres que lors de l'exécution du fuzzer.
* `./m.sh` - minimise un cas de test avec les mêmes paramètres que lors de l'exécution du
fuzzer.
* `./c.sh` - analyse la « chaîne » de cas de test qui a mené au cas de test donné.
Utile pour analyser/optimiser le fuzzer. Vous pouvez rapidement voir quel
cas de test a été produit par quelle chaîne de mutations sur quelles entrées de la file.
Nécessite `fzf`.
Il existe également d'autres rapports pratiques, tels que
* `./bugs` et `./bugtypes` qui résument tous les bugs identifiés.
* `./crashes_min`, qui contient les crashes minimisés de toutes les instances `afl-fuzz`.
**Voir la couverture de code par blocs de base EVM**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
Le script fuzz/evm-bb-coverage.sh calculera la couverture des blocs de base
à partir d'un répertoire de sortie AFL. Le harnais peut éventuellement générer une trace de blocs
de base, qui sont ensuite comparés à une liste de blocs de base produite par
evm2cpp (c'est-à-dire les fichiers .bb_list dans eEVM/contracts/).
Par défaut, nous calculons également la couverture que nos graines génériques par défaut (voir
eEVM/fuzz/generic_seeds produisent:```bash
$ cat coverage-percent-seeds.evmcov
10.5890
La liste des blocs de base couverts est stockée dans le fichier `all.evmcov`.
**Afficher le résumé des cas de test générés**
`efuzzcaseanalyzer` peut être utilisé pour afficher/résumer les cas de test générés,
par exemple,```
$ efuzzcaseanalyzer -a ./contract.abi -s ./crashes_min/
Transactions Sequences:
--------------------------------------------------------------
TX [🪙]
deposit()[🪙];
withdraw(uint256)[↕️ ↩️ ];
withdraw(uint256)[];
--------------------------------------------------------------
Number of fuzzcases: 1
Average number of TXs: 3
Number of unique TX sequences: 1
Number of unique TX sequences (consecutive deduplicated): 1
Les résumés sont généralement stockés dans les fichiers crashes_tx_summary et
queue_tx_summary, mais ce dernier peut être un peu verbeux.
Analyser un seul cas de test crashant``` $ ./a.sh default/crashes/id:000000,...
$ efuzzcaseanalyzer -a ./contract.abi default/crashes/id:000000,... Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 0
TX with tx_sender: 54 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(80), } TX with tx_sender: 238 (selector); call_value: 0x246ddf979; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 153 (selector); call_value: 0x3860e6373; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x0000000000000000000000000000000000000000000000000000000000000001 TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
Et pour obtenir le résultat réel de la cible de fuzzing, vous pouvez exécuter :```
$ ./r.sh default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
# roughly equivalent to running
$ env EVM_DEBUG_PRINT=1 ./build/fuzz_multitx default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
[...]
account 0xc4b803ea8bc30894cc4672a9159ca000d377d9a3 has balance 0x100000000000000000000000000000001bc16d67562e80000( > 0x1000000000000000000000000000000000000000000000000)
Aborted (core dumped)
Cela vous donne beaucoup de sortie verbeuse, y compris certaines parties des traces d'exécution des contrats et le résultat de la vérification de solde que le harnais effectue.
Minimisation des entrées provoquant un crash
Les entrées provoquant un crash contiennent souvent des transactions sans rapport en raison de l'approche de test aléatoire. Cela peut être atténué en effectuant une minimisation sur l'entrée qui provoque le crash (c'est-à-dire réduire l'entrée tant qu'elle provoque toujours un crash). Si vous souhaitez minimiser des entrées qui ne provoquent pas de crash, vous pouvez utiliser l'option -M pour activer la minimisation selon la couverture comme critère de minimisation.
La commande suivante réduira le cas de test et écrasera le fichier :``` $ efuzzcaseminimizer -oa ./contract.abi ./build/fuzz_multitx ./default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
[..]
=== Before minimizing: === Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 0
TX with tx_sender: 54 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(80), } TX with tx_sender: 238 (selector); call_value: 0x246ddf979; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 153 (selector); call_value: 0x3860e6373; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x0000000000000000000000000000000000000000000000000000000000000001 TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
=== After minimizing: === Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 15133991795
TX with tx_sender: 4 (selector); call_value: 0x246ddf979; length: 4; block+=0; #returns=0 func: deposit() input: { } TX with tx_sender: 4 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x TX with tx_sender: 0 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
## Lecture du format des cas de test
Le format des cas de test est conçu pour le fuzzing et n'est pas aussi simple
à lire. Vous devez être conscient de plusieurs subtilités.
* Le format des cas de test est considéré comme une « file » de transactions
pouvant être exécutées. Dès qu'un problème est rencontré, le traitement du
cas de test s'arrête. Cela inclut :
* Lorsqu'une transaction est annulée (revert).
* Toute erreur rencontrée par le code de harnais (harness code).
* Tout bug déclenché et détecté.
Par conséquent, un cas de test imprimé ne correspond pas nécessairement à ce
qui est exécuté : il peut y avoir des transactions à la fin qui ne sont pas
exécutées. Vérifiez la sortie verbeuse et utilisez le minimizer pour vous en
débarrasser !
* De même, il peut y avoir trop de `returns` ou des drapeaux `reenter` erronés.
Utilisez le minimizer de cas de test pour éliminer cela.
* Un contrat n'est ré-entré que lorsqu'il y a une autre transaction dans la liste
après la transaction censée effectuer la réentrance (c'est-à-dire qu'il y a
une autre entrée suivante dans la file).
* Même si le drapeau `reenter` est défini sur une certaine valeur, cela ne signifie
pas nécessairement que le contrat est ré-entré, seulement que le code du harnais
essaiera de le faire si possible. Par exemple, si le contrat n'effectue pas
d'appel, le drapeau `reenter` est ignoré car aucune réentrance n'est possible.
En général, le minimizer supprimera les drapeaux `reenter` erronés.
En général, la plupart de ces problèmes disparaissent lorsque vous utilisez le
minimizer de cas de test, il est donc toujours bon d'utiliser celui-ci avant
d'analyser les cas de test générés.
## Fausses alertes connues
Nous avons observé plusieurs types de fausses alertes qui semblent récurrentes
lors du fuzzing de contrats avec EF/CF.
* Les contrats qui versent de l'Ether par conception. L'oracle de bugs de gain
d'Ether d'EF/CF identifiera ces contrats comme vulnérables, alors qu'ils
fonctionnent comme prévu :
* Contrats de jeu : de nombreux contrats de jeu comportent une forme
d'aléatoire, ce qui est déjà une mauvaise pratique sur Ethereum. Cependant,
certains contrats de jeu sont implémentés d'une manière qui vous oblige à
deviner, par exemple, les deux derniers chiffres du hash du bloc suivant
ou quelque chose de similaire. Cela peut être activé via un schéma
d'engagement (commitment scheme), c'est-à-dire que la première transaction
engage l'utilisateur sur une certaine valeur et la seconde déclenche la
devinette et le paiement en cas de victoire. Ces contrats ne sont
généralement pas exploitables sur une véritable blockchain. Cependant,
dans la blockchain simulée d'EF/CF, le fuzzer peut adapter l'engagement
après avoir observé la valeur de la seconde transaction. Ce fait est
important pour qu'EF/CF atteigne une meilleure couverture de code.
Cependant, cela facilite également l'identification par EF/CF d'une
séquence de transactions (TX) qui permet au fuzzer de gagner de manière
déterministe dans le contrat de jeu.
* Contrats qui versent des intérêts : il existe de nombreux petits contrats
qui vous permettent d'investir de l'Ether puis de verser un certain
pourcentage d'intérêts tous les `N` blocs. L'attaquant simulé d'EF/CF est
capable d'attendre `N` blocs puis de recevoir le paiement des intérêts,
ce qui est à nouveau détecté par l'oracle de bugs de gain d'Ether.
* Airdrops : certains contrats de jetons permettent des airdrops, c'est-à-dire
qu'ils distribuent des jetons à toute personne qui en fait la demande
jusqu'à ce que certaines limites soient atteintes. Par exemple, les airdrops
ne sont souvent activés que pendant une courte période. Si un tel contrat
est déployé dans EF/CF, il y a de fortes chances que la limite de temps
soit définie de manière à ce que les airdrops soient toujours actifs.
EF/CF détecte alors un gain d'Ether si les jetons distribués peuvent être
revendus.
* Signalement hâtif des `DELEGATECALL` contrôlables : actuellement, nous signalons
un delegatecall contrôlable dès qu'il est invoqué. Cependant, il existe de
nombreux contrats qui comportent des fonctions permettant intentionnellement
à l'appelant d'effectuer un delegatecall vers une adresse arbitraire. Ces
fonctions annulent cependant inconditionnellement la transaction immédiatement
après le delegatecall. Cela empêche toute mise à jour d'état ou tout transfert
d'Ether de persister. Généralement, ces fonctions contiennent des mots comme
« simulate » dans leur nom de fonction, ce qui les rend faciles à repérer.
* Cela pourrait être corrigé dans EF/CF en reportant le signalement jusqu'à
la fin de l'exécution. Cependant, cela complique pas mal l'oracle de bugs.
* Actuellement, il n'y a aucun plan pour corriger cela.
* Initialiseur appelable : nous avons observé qu'en fuzziant des contrats
exportés depuis la blockchain, EF/CF est parfois capable d'appeler des
fonctions d'initialisation, même si le contrat a déjà été initialisé.
Normalement, cela devrait déclencher un revert, mais ce n'est pas le cas dans
l'environnement EVM d'EF/CF. Appeler à nouveau l'initialiseur conduit souvent
à des gains d'Ether triviaux, car par exemple l'initialiseur définit une
variable *owner* ou quelque chose de similaire.
* Nous ne sommes pas encore certains de la cause racine de ce problème.
Cependant, il est généralement facile à repérer, car la fonction
d'initialisation est typiquement appelée `initializer`, `init` ou similaire.
## Pièges courants
Nous avons fait de notre mieux pour rendre cet outil quelque peu utilisable,
mais il s'agit encore d'un prototype de recherche. Attendez-vous à ce que des
choses cassent. Voici quelques problèmes courants que nous avons observés :
* *Q : J'obtiens une erreur de compilation étrange due à une macro `TOKENPASTE`.*
R : Cela arrive souvent lorsque `efcfuzz` devine le mauvais nom de contrat
(c'est-à-dire qu'il devine un contrat abstrait) ; essayez de passer
`--name YourContract` pour spécifier le contrat cible.