
EF/CF - Extrem schnelles Smart-Contract-Fuzzing
EF/CF ist ein neuer Ansatz zum Fuzzing von Smart Contracts: Anstatt einen neuen eigens entwickelten Fuzzer zu verwenden, wird die bestehende Fuzzing-Infrastruktur für C/C++-Code auf Smart Contracts übertragen. Derzeit ist AFL++ der primär unterstützte Fuzzer, obwohl es auch eine sehr rudimentäre Unterstützung für libfuzzer und honggfuzz gibt.
Warum bestehende Fuzzing-Infrastruktur verwenden?
Welche Probleme treten dabei auf?
./src/ethmutator/./src/evm2cpp/Dieses Repository ist der primäre Einstiegspunkt für das EF/CF-Projekt. Es enthält den gesamten
relevanten Code als Unterprojekte in ./src/ sowie mehrere praktische Skripte für die
Installation, Skripte zum Starten von Fuzzing-Kampagnen und verschiedene Datensätze zum Testen
des Fuzzers (und zum Vergleich mit anderen Werkzeugen).
./src/ - enthält den gesamten Quellcode, der zum Erstellen und Ausführen von EF/CF erforderlich ist;
zur Reproduzierbarkeit sind alle direkten Abhängigkeiten als Git-Submodule hinzugefügt../data/ - enthält die Datensätze, die bei der Evaluierung verwendet wurden./scripts - enthält Skripte zum Ausführen von Experimenten, zur Installation usw../docker - Dockerfile für einen containerbasierten Workflow
./docker/tools/ enthält Dockerfiles für Werkzeuge, mit denen wir EF/CF verglichen
haben. Wir haben unser Bestes getan, um die Versionen zu fixen, die wir in unserer Arbeit
evaluiert haben, in den Dockerfiles../EXPERIMENTS.md - enthält eine Anleitung zur
Reproduktion der Experimente aus unserer Arbeit../examples - enthält Beispielausgaben, die von EF/CF erzeugt wurdenWir beschreiben die Architektur und Implementierung von EF/CF und fassen unsere Evaluierungsergebnisse in unserem Papier zusammen: arxiv.org-Preprint
Wenn Sie sich in wissenschaftlichen Arbeiten auf EF/CF beziehen, verwenden Sie bitte den folgenden BibTeX-Eintrag für die Zitierung:```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", }
## Quickstart
Die empfohlene Methode ist, EF/CF als interaktiven Docker-Container auszuführen.
1. Betreten Sie den Container mit einer Shell ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
oder baue den Container aus dem geklonten Repository ``` make gitmodules # to fetch the git submodules make container-enter
1. Kompiliere und fuzze dann einen Solidity-Vertrag, bis der erste Absturz/Fehler
entdeckt wird: ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
Kein Git? Wenn du ein Tarball-/Docker-Release verwendest, ignoriere das.
Führe git submodule update --init aus, um die neuesten Submodule-Commits in bereits geklonten Repositorys abzurufen.
Stelle sicher, dass du dies auch in ./src/eEVM ausführst.```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*Warnung:* Die Ausführung von `git clone --recursive $repo` oder die Übergabe des Arguments `--recursive` an `git sumbodule (update|init)` veranlasst git, rekursiv in Submodule des AFL++-Repositorys einzudringen, die für dieses Projekt nicht benötigt werden. Um Speicherplatz zu sparen, ist es daher besser, die rekursiven Submodule-Checkouts zu vermeiden.
### Container
Wir bieten die folgenden praktischen Make-Targets für containerbasierte Workflows an:```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
Wenn Sie einen sauberen Build sicherstellen möchten, können Sie den folgenden Befehl verwenden```sh make container-build CLEAN_CHECKOUT=1
Alternativ kann der Container mit folgendem Docker-Befehl erstellt werden:```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
Beachte, dass es auch ein Archlinux- und Fedora-basiertes Dockerfile gibt. Sie sollten ebenfalls funktionieren, sind aber nicht so gut getestet.
Zur manuellen Verteilung eines Docker-Images (z. B. wenn lokale Änderungen enthalten sein sollen), verwende:``` make container-release docker load -i ./efcf*.tar
Wir empfehlen die folgenden Docker-Optionen zum Start:
* `--security-opt seccomp=unconfined` – bessere Fuzzing-Leistung
* `--net=host` – für einfachen Zugriff auf einen lokalen Ethereum-Knoten
* `--tmpfs "/tmp/efcf/":exec,size=6g` – legt die temporären Dateien von EF/CF wenn möglich auf eine RAM-Disk (weniger Verschleiß der Festplatte)
* `--privileged` – zum Ausführen von `afl-system-config` oder `efcfuzz --configure-system`
* `-v` – zum Speichern der Ausgabedaten von EF/CF
### VM / Bare-Metal
Für Workflows auf VM- oder Bare-Metal-Basis:```sh
make system-install # install efcf to current system (requires root or sudo rights)
Beachte, dass viele der Skripte ohnehin mit dem relativen Verzeichnislayout funktionieren, daher installiert dies hauptsächlich Abhängigkeiten und einige Werkzeuge, die nützlich in deinem PATH zu haben sind. Wir haben EF/CF auf den folgenden Linux-Distributionen getestet:
(Die Distribution spielt keine so große Rolle, wir haben LLVM 13 und 14 getestet, wobei 14 die bevorzugte Wahl ist. LLVM 11 oder 12 könnte ebenfalls noch funktionieren, aber wie immer – je neuer, desto besser. Wichtig ist, dass es ein LLVM gibt, das mit unserem Fork von AFL++ kompatibel ist.)
Wir haben EF/CF nicht nativ auf Mac OS getestet. Wahrscheinlich werden Dinge nicht funktionieren (z. B. scheint afl-clang-lto auf Mac OS nicht zu funktionieren). Die beste Option ist, docker zu verwenden.```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
Wir haben mit Docker Desktop v4.21.1 getestet und die grundlegende EF/CF-Nutzung funktioniert. Beachte jedoch Folgendes:
* Wenn du beim Erstellen Segfaults siehst: versuche, das Speicherlimit der VM zu erhöhen, die Docker unter macOS verwendet.
* Versuche, die Beschleunigung mit rosetta in docker zu aktivieren – hoffentlich ist das etwas schneller.
### Entwicklungs-Setup
Die Werkzeuge müssen in der Regel nicht installiert werden. Installiere die benötigten
Abhängigkeiten wie im Skript `system-install.sh` oder wie in den Dockerfiles.
Der Einfachheit halber haben wir einige Skripte, um deinen `PATH` zu aktualisieren:```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
Einige der Skripte benötigen einen API-Schlüssel, um Metadaten (z. B. ABI) vom
Etherscan-Dienst abzurufen. Falls du einen API-Schlüssel hast, musst du die
Umgebungsvariable ETHERSCAN_API_KEY setzen, um diesen an die Skripte weiterzugeben. Für
einen Docker-basierten Workflow kannst du den Docker-Container entweder mit dem
--env-Flag starten oder deinen API-Schlüssel in die Datei .etherscan_api_key legen, wodurch
der API-Schlüssel in den Docker-Container eingebacken wird.
Der Einfachheit halber verwenden wir ein Wrapper-Skript, das sich um alle Details
für dich kümmert, wenn du den EF/CF-Fuzzer startest: efcfuzz
Du kannst viele Befehlszeilenoptionen setzen, um das Verhalten des Fuzzers in
Bezug auf den Build- und Fuzzing-Prozess zu konfigurieren. Wirf einen Blick auf efcfuzz --help für eine
Liste der Optionen.
Beispiele
Kompiliere den Solidity-Quellcode in nativen EF/CF-Code und starte das Fuzzing für 5 Minuten (entspricht 300 Sekunden).```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
Alternativ können Sie den Fuzzer mit reduzierter Ausgabe starten (`--quiet` unterdrückt die
Ausgabe des Basis-Fuzzers, während `--print-progress` eine kurze Zusammenfassung des
Fuzzing-Fortschritts ausgibt), und den Fuzzer auf 4 Kernen starten.```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
Verwende bereits kompilierten Bytecode, kompiliere den Bytecode zu EF/CF-Nativecode und starte das 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
Der Wrapper kann den Zustand eines Vertrags aus einem go-ethereum/erigon-Knoten exportieren und von dort aus mit dem Fuzzing beginnen.```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
Zusätzlich kannst du --include-address-deps=y übergeben, um rekursiv nach
Adressen anderer Konten im Speicher des exportierten Vertrags zu suchen und diese
ebenfalls in den State-Export aufzunehmen. Dies umfasst jedoch keine anderen
Verträge, die in Solidity-mapping-Typen gespeichert sind. Um wirklich den
gesamten Zustand rekursiv zu exportieren, übergib zusätzlich das Flag --include-mapping-deps=y.
Sei jedoch vorsichtig: Diese rekursive Suche kann zu langen Kompilierzeiten und
schlechter Fuzzing-Leistung führen. Insbesondere häufig verwendete Verträge können
viel internen Zustand haben, und die Verwendung ihres exportierten Zustands kann
das Fuzzing verlangsamen. Prüfe, ob der Fuzzer mehr als 1k execs/sec erreicht.
Wenn nicht, solltest du besser versuchen, einen künstlichen und kleineren Zustand
zu erstellen. Versuche, einen lokalen go-ethereum-Knoten im --dev-Modus
auszuführen und deine Verträge dort bereitzustellen. Exportiere dann den Live-Zustand von dort.
Der Wrapper legt Builds in einem Cache ab, sodass ein zweiter Fuzzing-Lauf viel
schneller starten sollte, da die anfängliche Kompilierzeit nicht mehr benötigt
wird. Wenn du nur bauen und in den Cache legen möchtest, kannst du das Argument
--build-only übergeben.
Beispiel: Fuzzing mit Eigenschaften
EF/CF unterstützt auch eigenschaftsbasiertes Fuzzing mit derselben Eigenschaftsdefinition wie der echidna-Fuzzer. Eigenschaften (oder Invarianten) werden als Solidity-Funktionen ausgedrückt, die als Fehler-Orakel für den Fuzzer dienen. Du kannst zum Beispiel eine Solidity-Funktion hinzufügen:```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
Dies repräsentiert die Eigenschaft, dass der `total_balance` immer unter 1000 liegen sollte. EF/CF meldet dann einen Fehler, wenn es gelingt, diese Eigenschaft mithilfe einer Transaktionssequenz zu verletzen, d. h. das Orakel gibt `false` zurück.
Um EF/CF mitzuteilen, dass dies eine Eigenschaft ist, müssen Sie eine Liste von Funktionssignaturen in einer Datei angeben, die von EF/CF als Liste von Eigenschaften übernommen wird, die während des Fuzzings geprüft werden sollen.
Der einfachste Weg ist, die relevanten Signaturen mithilfe des Flags `--hashes` des Solidity-Compilers zu erhalten, z. B.```
solc --hashes ./path/to/your.sol | grep test_property > property_list
Jetzt können Sie den Fuzzer mit Folgendem starten:``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
Sie können auch `--disable-detectors` hinzufügen, um die integrierten ether-basierten Bug-Orakel zu deaktivieren.
Sie können das folgende Beispiel für property-basiertes Fuzzing ausprobieren:```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
Beispiel: Fuzzing auf Events
EF/CF unterstützt Fuzzing auf Assertionsverletzungen, die über Events ausgedrückt werden. Tatsächlich unterstützen wir auch die Verwendung beliebiger benutzerdefinierter Events als Bug-Oracle. Standardmäßig erkennt EF/CF einen Fehler, wenn der Zielvertrag eines der folgenden Events protokolliert hat: AssertionFailed(), AssertionFailed(uint256), AssertionFailed(string) und Panic(uint256).```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
Sie können auch zusätzliche benutzerdefinierte Ereignis-Topics/-Hashes angeben, nach denen gesucht werden soll, in einer Datei mit `--event-assertions-list ./path/to/eventslist.txt`. Wie bei der zuvor genannten Eigenschaftsliste können Sie das Format erhalten, indem Sie `solc --hashes` verwenden und die Ereignis-Hashes und -Namen in die Ereignislistendatei kopieren.
Standardmäßig ignoriert EF/CF Ereignisse, die nicht vom Zielvertrag ausgegeben wurden. Wenn Sie das ändern möchten, verwenden Sie `--event-assertions-target-only=n`.
(Hinweis: Sie können `--assertions` verwenden, um sowohl die Ereignis- als auch die Solidity-Assertionsprüfung zu aktivieren.)
**Beispiel: Fuzzing für Solidity-^0.8-Assertions**
Derzeit unterstützen wir kein Fuzzing für beliebige Assertions im Solidity-Code für Solidity-Versionen unter 0.8. Früher lösten Solidity-Assertions einfach einen `invalid`-Opcode aus, was zu einem recht heftigen Revert führte. Solidity-Version 0.8 hat das Verhalten geändert: Anstatt den `invalid`-Opcode zu verwenden, um Transaktionen zurückzurollen, nutzen sie nun den `revert`-Mechanismus und signalisieren Fehler an den Aufrufer zurück. Wir können diese Art der Fehlerausbreitung als Bug-Oracle in EF/CF nutzen. Derzeit unterstützt EF/CF die Prüfung auf den Solidity-Fehlertyp `Panic(uint256)`. [Weitere Informationen zu Solidity-Fehlern.](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
(Hinweis: Du kannst --assertions verwenden, um sowohl Event- als auch Solidity-Assertionsprüfung zu aktivieren)
Systemanforderungen und Konfiguration
Wir empfehlen, 4 bis 16 Kerne und ungefähr 1 GB Speicher pro Kern zuzuweisen. Du
kannst das Flag --configure-system verwenden, um dein System für schnelles
Fuzzing zu konfigurieren, oder es selbst konfigurieren. In Docker-Containern musst du
auch den Host für beste Leistung konfigurieren. Wenn es sich um einen unkritischen Host handelt, kannst du
den Container als --privileged starten und
/usr/local/bin/afl-system-config verwenden, um das System für schnelles
Fuzzing zu konfigurieren (beachte, dass dies den Container im Wesentlichen als root ausführt).```
docker run --rm -it --privileged efcf afl-system-config
docker run --rm -it
--security-opt seccomp=unconfined
--tmpfs "/tmp/efcf/":exec,size=6g
efcf
## Ein Fuzzing-Experiment ausführen
Um das Experiment mit dem Datensatz `data/tests/` auszuführen, kannst du den
folgenden Befehl verwenden, um die Contracts und ihre Fuzzing-Harnesses zu
erstellen und anschließend den Fuzzer mit verschiedenen Einstellungen, mehreren
Wiederholungen usw. auszuführen. Da dies eine Weile dauern würde, können wir
diese Experimente parallel ausführen. Wir teilen die Fuzzing-Experimente in
einen Build-Schritt und einen Fuzz-Schritt auf. Die Build-Schritte erstellen
alle Smart Contracts sequenziell (wobei der Build selbst mehrere Kerne nutzt).
Anschließend starten wir 8 Fuzzer-Instanzen im Hintergrund, die die Build-
Artefakte aus dem Build-Schritt übernehmen und Fuzzing-Läufe starten. Das
Makefile versucht automatisch, alles im passenden Container zu starten, falls
entweder `docker` oder `podman` verfügbar ist.```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
Wir deaktivieren seccomp- und Netzwerk-Sandboxing, wenn wir die Container im Hintergrund starten. Das Deaktivieren des seccomp-Sandboxing verbessert die Fuzzing-Leistung. Durch die Verwendung des Host-Netzwerks kann EF/CF auf Ethereum-Knoten im lokalen Netzwerk zugreifen, ohne weitere Konfiguration.
Wir verwendeten das Skript ./scripts/run-tools-on-dataset.py, um die anderen Tools
in Docker-Containern auf diesen Datensätzen auszuführen, z. B. mit diesen Befehlen für den
Multi-Datensatz:```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
Sie müssen das Skript anpassen, um die Werkzeuge und die Anzahl der Läufe zu konfigurieren.
### Ein Fuzzing-Experiment einrichten
Hier verwenden wir das Experiment `tests` als Beispiel. Ersetzen Sie einfach die Zeichenkette `tests` in den folgenden Schritten durch den Namen des Experiments:
1. Sammeln Sie Ihren Datensatz in `./data/`, z. B. den Datensatz `./data/tests` mit Testverträgen. Für Solidity-Verträge haben wir ein generisches `Makefile` zum Erstellen der Verträge: `sol.Makefile`. Sie können dies bei Bedarf wiederverwenden, siehe `./data/tests/Makefile` für ein Beispiel.
2. Erstellen Sie ein Skript, um die Build-Artefakte zu erzeugen, einschließlich aller erforderlichen Vorverarbeitungs-/Scraping-Schritte. Zum Beispiel haben wir für den Datensatz `tests` das Skript `./scripts/build-tests.sh`. Die Build-Artefakte sollten in `./builds/tests/${contract}.build.tar.xz` gespeichert werden.
3. Erstellen Sie ein Skript, um die Fuzzing-Kampagne zu starten, z. B. erstellen Sie für den Datensatz `tests` ein Skript namens `./scripts/fuzz-tests.sh`. In der Regel können Sie die gemeinsame Fuzzing-Kampagnenfunktion aus `./scripts/common.sh` verwenden. Schauen Sie sich `fuzz-tests.sh` als Vorlage an.
4. Die Ergebnisse von `fuzz-tests.sh` werden in `./results/run-fuzz-tests/` gespeichert.
5. Zur Zusammenfassung der Ergebnisse stellen wir `./scripts/summarize.py` für die Bash-basierten Startskripte und `./scripts/summarize_l.py` für Python-Startskripte (das `efcfuzz`-Werkzeug) bereit.
Möglicherweise müssen Sie diese Skripte abhängig von Ihrem Schritt 3 anpassen.
### Vorhandene Fuzzing-Experimente
#### Benchmarks
* <a href="./data/multi/">`./data/multi`</a> enthält den Skalierbarkeits-Benchmark,
den wir verwendet haben, um zu bewerten, wie gut ein Analysetool mit längeren
Transaktionssequenzen skaliert. Er besteht aus drei Arten von Verträgen:
* `multi_gen_*.sol` - automatisch synthetisierte Verträge, die eine Reihe
von `require(input <= MAGIC)` ausführen und dann eine interne Zustandsvariable setzen. Wenn
alle Zustandsvariablen gesetzt sind, kann `selfdestruct` (oder das Echidna-Orakel)
ausgelöst werden.
* `multi_man_complex_*.sol` - manuell erstellte Varianten, die ähnlich wie
die Verträge vom Typ `multi_gen` funktionieren, aber etwas kniffligere
Einschränkungen aufweisen (z. B. andere Dinge als Gleichheit und Ungleichheit mit einem
magischen Wert)
* `justlen_*.sol` - diese stammen aus dem [echidna-parade-Beispiel](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - Sanity Checks, die überprüfen, ob ein Fuzzer/Werkzeug
theoretisch Fehler finden kann, die 9 oder 10 Transaktionen erfordern. Hier
muss der Analysator nur 10 Funktionen in der richtigen Reihenfolge ohne
Argumente aufrufen. Das ist für die meisten Analysetools ziemlich einfach.
* <a href="./data/throughput/">`./data/throughput`</a> enthält die Verträge, die wir
zur Bewertung des Durchsatzes verwendet haben. Es handelt sich um eine Auswahl von Verträgen
unterschiedlicher Größe. Beachten Sie, dass wir alle Schwachstellen in diesen Verträgen
behoben haben, sodass gefundene Schwachstellen die Durchsatzmessungen nicht beeinflussen.
* <a href="./data/cov-max-testset">`./data/cov-max-testset`</a> enthält die
Verträge, die wir für den Vergleich von Fuzzern auf Basis der Codeabdeckung verwendet haben.
#### Fehlererkennung
* <a href="./data/ethbmc-vuln">`./data/ethbmc-vuln`</a> Liste von Verträgen, die
EthBMC als verwundbar erkannt hat.
* <a href="./data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> Liste von
Verträgen, bei denen EthBMC die Analyse wegen eines Timeouts abgebrochen hat.
* <a href="./data/reentrancy">`./data/reentrancy`</a> eine Reihe von Verträgen,
die anfällig für Reentrancy-Angriffe sind.
* <a href="./data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> eine Reihe von Verträgen,
bei denen im Rahmen der [sailfish-Studie](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth)
bestätigt wurde, dass sie einen Reentrancy-Fehler enthalten.
* <a href="./data/sailfish-dao">`./data/sailfish-dao`</a> Liste aller Verträge,
bei denen
[sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs)
einen Reentrancy-Fehler gefunden hat.
* <a href="./data/sereum">`./data/sereum`</a> eine Liste von Verträgen,
die laut [Sereum](https://github.com/uni-due-syssec/sereum-results) anfällig für Reentrancy-Angriffe sind.
* <a href="./data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a>
Verträge aus den kuratierten SmartBugs, die als "access control"-Fehler klassifiziert sind
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="./data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a>
Verträge aus den kuratierten SmartBugs, die als "reentrancy"-Fehler klassifiziert sind
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### Tests
Die folgenden Datensätze enthalten einfache synthetische Testverträge, um die Fähigkeiten des Fuzzers zu testen:
* <a href="./data/tests">`./data/tests`</a> grundlegende Tests aus mehreren
Quellen, die die Basisfähigkeiten eines Fuzzers überprüfen. Alle verwenden ein
selfdestruct-Orakel.
* <a href="./data/tests-not-vuln">`./data/tests-not-vuln`</a> dasselbe wie tests, sollte aber
nicht als verwundbar erkannt werden.
* <a href="./data/properties-tests">`./data/properties-tests`</a> Tests für
eigenschaftsbasiertes Fuzzing
* <a href="./data/assertions-tests">`./data/assertions-tests`</a> Tests für
das Fuzzing auf Assertions.
## Fuzzing im Detail
Wir verwenden Wrapper-Skripte, um den eigentlichen Fuzzer zu starten (in unserem Fall AFL++).
Dies geschieht automatisch, wenn der `efcfuzz`-Launcher verwendet wird.```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
Wenn du tmux und tmuxp installiert hast, könnte die interaktive Version des Skripts für Entwicklung und Inspektion nützlich sein:```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
Das wird dann eine ganze Weile kompilieren und fuzzen. Sie können dann
`cd ./fuzz/out/SimpleDAO*` ausführen, um die Fuzzing-Ergebnisse anzusehen. Unsere Wrapper-Skripte
leisten beim Start des `afl-fuzz`-Programms zusätzliche Arbeit, d. h. hauptsächlich
die Nachbearbeitung der Ergebnisse. Darüber hinaus werden mehrere praktische Skripte erzeugt,
um die generierten Testfälle zu analysieren.
* `./a.sh` - Gibt einen Testfall in menschenlesbarer Form aus, ein Wrapper um
`efuzzcaseanalyzer`.
* `./r.sh` - führt einen Testfall mit denselben Einstellungen aus, mit denen der Fuzzer ausgeführt wurde.
* `./m.sh` - minimiert einen Testfall mit denselben Einstellungen, mit denen der Fuzzer
ausgeführt wurde.
* `./c.sh` - analysiert die „Kette“ von Testfällen, die zu dem angegebenen Test
führen. Nützlich zum Analysieren/Optimieren des Fuzzers. Sie können schnell sehen, welcher
Testfall durch welche Mutationskette auf welchen Queue-Einträgen erzeugt wurde.
Erfordert `fzf`.
Es gibt außerdem einige weitere praktische Berichte, wie zum Beispiel
* `./bugs` und `./bugtypes`, die alle identifizierten Fehler zusammenfassen.
* `./crashes_min`, das die minimierten Abstürze aller `afl-fuzz`
Instanzen enthält.
**EVM-Basisblock-Codeabdeckung anzeigen**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
Das Skript fuzz/evm-bb-coverage.sh berechnet die Basic-Block-Abdeckung für ein AFL-Ausgabeverzeichnis. Der Harness kann optional einen Trace von Basic-Blöcken ausgeben, der dann mit einer Liste von Basic-Blöcken verglichen wird, die von evm2cpp ausgegeben wird (d. h. die .bb_list-Dateien in eEVM/contracts/).
Standardmäßig berechnen wir auch die Abdeckung, die unsere standardmäßigen generischen Seeds (siehe eEVM/fuzz/generic_seeds erzeugen:```bash
$ cat coverage-percent-seeds.evmcov
10.5890
Die Liste der abgedeckten Basisblöcke ist in der Datei `all.evmcov` gespeichert.
**Zusammenfassung der generierten Testfälle anzeigen**
`efuzzcaseanalyzer` kann verwendet werden, um die generierten Testfälle anzuzeigen/zusammenzufassen,
z. B.,```
$ 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
Die Zusammenfassungen werden normalerweise in den Dateien crashes_tx_summary und
queue_tx_summary gespeichert, aber Letztere kann etwas ausführlich sein.
Einen einzelnen abstürzenden Testfall analysieren``` $ ./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), }
Und um das tatsächliche Ergebnis des Fuzz-Targets zu erhalten, kannst du Folgendes ausführen:```
$ ./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)
Dies führt zu einer Menge ausführlicher Ausgabe, einschließlich einiger Teile der Ausführungsspuren der Verträge und des Ergebnisses der Kontostandsprüfung, die der Harness durchführt.
Minimierung abstürzender Eingaben
Abstürzende Eingaben enthalten aufgrund des randomisierten Testansatzes oft auch unzusammenhängende Transaktionen. Dies kann abgemildert werden, indem eine Minimierung an der abstürzenden Eingabe durchgeführt wird (d. h., die Eingabe so lange reduziert wird, wie sie weiterhin einen Absturz verursacht). Wenn Sie nicht-abstürzende Eingaben minimieren möchten, können Sie das Flag -M verwenden, um die Minimierung anhand der Abdeckung als Minimierungskriterium zu aktivieren.
Der folgende Befehl reduziert den Testfall und überschreibt die Datei:``` $ 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), }
## Lesen des Testfall-Formats
Das Testfall-Format ist auf Fuzzing ausgelegt und nicht unbedingt einfach zu lesen. Es gibt einige Feinheiten, die Sie beachten müssen.
* Das Testfall-Format wird als eine "Warteschlange" von Transaktionen betrachtet, die ausgeführt werden können. Sobald ein Problem auftritt, stoppt die Verarbeitung des Testfalls. Dazu gehören:
* Wenn eine Transaktion zurückgesetzt wird (revert).
* Wenn ein Fehler vom Harness-Code erkannt wird.
* Wenn ein Bug ausgelöst und erkannt wird.
Daher entspricht ein gedruckter Testfall nicht unbedingt dem, was ausgeführt wird – es können am Ende Transaktionen vorhanden sein, die nicht ausgeführt werden. Überprüfen Sie die ausführliche Ausgabe und verwenden Sie den Minimizer, um diese zu entfernen!
* Ebenso kann es zu viele `returns` oder überflüssige `reenter`-Flags geben. Verwenden Sie den Testfall-Minimizer, um diese zu entfernen.
* Ein Contract wird nur dann erneut betreten, wenn sich in der Liste nach der Transaktion, die das Reentrancy ausführen soll, eine weitere Transaktion befindet (d. h. ein weiterer Eintrag in der Warteschlange).
* Selbst wenn das `reenter`-Flag auf einen bestimmten Wert gesetzt ist, bedeutet das nicht unbedingt, dass der Contract erneut betreten wird, sondern nur, dass der Harness-Code es versuchen wird, wenn möglich. Wenn der Contract beispielsweise keinen Aufruf ausführt, wird das `reenter`-Flag ignoriert, da keine Möglichkeit zum erneuten Betreten besteht. Normalerweise entfernt der Minimizer überflüssige `reenter`-Flags.
Im Allgemeinen verschwinden viele dieser Probleme, wenn Sie den Testfall-Minimizer verwenden. Daher ist es immer eine gute Idee, diesen zu verwenden, bevor Sie generierte Testfälle analysieren.
## Bekannte Fehlalarme
Wir haben verschiedene Arten von Fehlalarmen beobachtet, die beim Fuzzen von Contracts mit EF/CF scheinbar wiederkehrend auftreten.
* Contracts, die von Natur aus Ether auszahlen. Der Ether-gains-Bug-Oracle von EF/CF erkennt diese Contracts als verwundbar, obwohl sie wie vorgesehen funktionieren:
* Glücksspiel-Contracts: Viele Glücksspiel-Contracts nutzen eine Form von Zufälligkeit, was bereits eine schlechte Praxis in Ethereum ist. Einige Glücksspiel-Contracts sind jedoch so implementiert, dass Sie gezwungen sind, z. B. die letzten beiden Ziffern des nächsten Blockhashs oder Ähnliches zu erraten. Dies kann durch ein Commitment-Schema ermöglicht werden, d. h., die erste Transaktion verpflichtet den Benutzer auf einen bestimmten Wert und die zweite löst die Schätzung und Auszahlung im Gewinnfall aus. Diese Contracts sind in einer echten Blockchain normalerweise nicht ausnutzbar. In der simulierten Blockchain von EF/CF kann der Fuzzer das Commitment jedoch anpassen, nachdem der Wert in der zweiten Transaktion beobachtet wurde. Diese Tatsache ist wichtig, damit EF/CF eine bessere Codeabdeckung erreicht. Sie macht es EF/CF aber auch leicht, eine TX-Sequenz zu identifizieren, mit der der Fuzzer im Glücksspiel-Contract deterministisch gewinnt.
* Contracts, die Zinsen auszahlen: Es gibt viele kleine Contracts, die es Ihnen ermöglichen, Ether zu investieren und dann alle `N` Blöcke einen bestimmten Prozentsatz an Zinsen auszuzahlen. Der simulierte Angreifer von EF/CF kann `N` Blöcke warten und erhält dann die Zinszahlung, die wiederum vom Ether-gains-Bug-Oracle erkannt wird.
* Airdrops: Einige Token-Contracts ermöglichen Airdrops, d. h., sie geben Token an jeden aus, der sie anfordert, bis bestimmte Grenzen erreicht sind. Beispielsweise sind Airdrops oft nur für einen kurzen Zeitraum aktiviert. Wenn ein solcher Contract innerhalb von EF/CF bereitgestellt wird, ist die Wahrscheinlichkeit hoch, dass das Zeitlimit so gesetzt ist, dass Airdrops noch aktiviert sind. EF/CF erkennt dann einen Ether-gains, wenn die per Airdrop erhaltenen Token wieder verkauft werden können.
* Voreilige Meldung von kontrollierbarem `DELEGATECALL`: Derzeit melden wir einen kontrollierbaren Delegatecall, sobald er aufgerufen wird. Es gibt jedoch mehrere Contracts mit Funktionen, die dem Aufrufer absichtlich erlauben, einen Delegatecall an eine beliebige Adresse durchzuführen. Diese Funktionen setzen die Transaktion jedoch unmittelbar nach dem Delegatecall bedingungslos zurück (revert). Dadurch wird verhindert, dass Zustandsänderungen oder Ether-Transfers persistent werden. Typischerweise enthalten solche Funktionen Wörter wie "simulate" in ihren Funktionsnamen und sind daher leicht zu erkennen.
* Dies könnte in EF/CF behoben werden, indem die Meldung bis zum Ende der Ausführung verzögert wird. Dies verkompliziert den Bug-Oracle jedoch erheblich.
* Derzeit gibt es keine Pläne, dies zu beheben.
* Initializer aufrufbar: Wir haben beobachtet, dass EF/CF beim Fuzzen von aus der Blockchain exportierten Contracts manchmal in der Lage ist, Initializer-Funktionen aufzurufen, obwohl der Contract bereits initialisiert wurde. Normalerweise sollte dies einen Revert auslösen, tut dies aber in der EVM-Umgebung von EF/CF nicht. Das erneute Aufrufen des Initializers führt oft zu trivialen Ether-Gewinnen, weil der Initializer z. B. eine *owner*-Variable oder Ähnliches setzt.
* Wir sind uns noch nicht sicher, was die Ursache dieses Problems ist. Es ist jedoch normalerweise leicht zu erkennen, da die Initializer-Funktion typischerweise `initializer`, `init` oder Ähnliches heißt.
## Häufige Fallstricke
Wir haben unser Bestes getan, um dies einigermaßen nutzbar zu machen, aber es ist immer noch ein Forschungsprototyp. Erwarten Sie, dass Dinge kaputtgehen. Hier sind einige häufige Probleme, die wir beobachtet haben:
* *F: Ich erhalte einen seltsamen Kompilierungsfehler aufgrund eines `TOKENPASTE`-Makros.*
A: Dies passiert oft, wenn `efcfuzz` den falschen Contractnamen errät (d. h. es rät einen abstrakten Contract). Versuchen Sie, `--name YourContract` zu übergeben, um den Zielcontract anzugeben.