
EF/CF - Fuzzing di smart contract estremamente veloce
EF/CF è un nuovo approccio al fuzzing di smart contract: invece di usare un nuovo fuzzer costruito su misura, riutilizza l'infrastruttura di fuzzing esistente per codice C/C++ applicandola agli smart contract. Attualmente, AFL++ è il fuzzer supportato principalmente, sebbene esista anche un supporto molto rudimentale per libfuzzer e honggfuzz.
Perché usare l'infrastruttura di fuzzing esistente?
Quali sono alcuni problemi che incontriamo lungo il percorso?
./src/ethmutator/./src/evm2cpp/Questo repository è il punto di ingresso principale per il progetto EF/CF. Contiene tutto
il codice rilevante come sotto-progetti in ./src/ e diversi script di comodità per
l'installazione, script per avviare campagne di fuzzing e vari dataset per
testare il fuzzer (e confrontarlo con altri strumenti).
./src/ - contiene tutto il codice sorgente necessario per compilare ed eseguire EF/CF; per
riproducibilità tutte le dipendenze dirette sono aggiunte come sottomoduli git../data/ - contiene i dataset usati durante la valutazione./scripts - contiene script per eseguire esperimenti, installazione, ecc../docker - Dockerfile per un flusso di lavoro basato su container
./docker/tools/ contiene i dockerfile per gli strumenti con cui abbiamo confrontato EF/CF.
Abbiamo fatto del nostro meglio per fissare le versioni che abbiamo valutato nel nostro
articolo nei dockerfile../EXPERIMENTS.md - contiene una guida per
riprodurre gli esperimenti del nostro articolo../examples - contiene output di esempio prodotti da EF/CFDescriviamo l'architettura di EF/CF, l'implementazione e riassumiamo i risultati della nostra valutazione nel nostro articolo: preprint su arxiv.org
Quando si fa riferimento a EF/CF in lavori accademici, si prega di usare la seguente voce bibtex per la citazione:```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", }
## Avvio rapido
Il metodo consigliato è eseguire EF/CF come container docker interattivo.
1. Entra nel container con una shell ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
oppure crea il container dal repository clonato ``` make gitmodules # to fetch the git submodules make container-enter
1. Compila e poi fai fuzzing su un contratto solidity finché non viene scoperto il primo
crash/bug: ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
Niente git? se usi una release tarball/docker, ignora questo.
Esegui git submodule update --init per recuperare gli ultimi commit dei sottomoduli su repository già clonati.
Assicurati di eseguirlo anche in ./src/eEVM.```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*Avvertenza:* Eseguire `git clone --recursive $repo` o passare l'argomento `--recursive` a `git sumbodule (update|init)` farà sì che git entri ricorsivamente nei sottomoduli del repository AFL++, che non sono necessari per questo progetto. Quindi, per risparmiare spazio, è meglio evitare i checkout ricorsivi dei sottomoduli.
### Container
Forniamo i seguenti comodi target make per workflow basati su container:```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
Se vuoi assicurarti una build pulita, puoi usare il seguente comando```sh make container-build CLEAN_CHECKOUT=1
In alternativa, il container può essere creato con il seguente comando docker:```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
Nota che esiste anche un Dockerfile basato su Archlinux e Fedora. Dovrebbero funzionare anche loro, ma non sono altrettanto testati.
Per distribuire manualmente un'immagine docker (ad es., se si includono alcune modifiche locali), usa:``` make container-release docker load -i ./efcf*.tar
Consigliamo le seguenti opzioni docker per l'avvio:
* `--security-opt seccomp=unconfined` - prestazioni di fuzzing migliori
* `--net=host` - per un facile accesso a un nodo ethereum locale
* `--tmpfs "/tmp/efcf/":exec,size=6g` - metti i file temporanei di EF/CF su un ramdisk se possibile (minore usura del disco)
* `--privileged` - per eseguire `afl-system-config` o `efcfuzz --configure-system`
* `-v` - per persistere i dati di output di EF/CF
### VM / Bare-Metal
Per flussi di lavoro basati su VM o bare-metal:```sh
make system-install # install efcf to current system (requires root or sudo rights)
Nota che molti degli script funzionano comunque con il layout di directory relativo, quindi questo installa principalmente dipendenze e alcuni strumenti utili da avere nel tuo PATH. Abbiamo testato l'esecuzione di EF/CF sulle seguenti distribuzioni Linux:
(La distro non ha molta importanza: abbiamo testato LLVM 13 e 14, con 14 come scelta preferita. LLVM 11 o 12 potrebbero funzionare ancora, ma come sempre - più nuovo è, meglio è. La parte importante è che ci sia un LLVM compatibile con il nostro fork di AFL++.)
Non abbiamo testato EF/CF nativamente su Mac OS. Probabilmente molte cose non funzioneranno (ad es., afl-clang-lto su Mac OS sembra non funzionare). L'opzione migliore è utilizzare 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
Abbiamo testato utilizzando docker desktop v4.21.1 e l'uso di base di EF/CF funziona. Tuttavia, considera quanto segue:
* Se riscontri segfault durante la compilazione: prova ad aumentare il limite di memoria della VM che docker usa su Mac OS.
* Prova ad abilitare l'accelerazione usando rosetta in docker - speriamo che sia un po' più veloce.
### Configurazione di sviluppo
Gli strumenti generalmente non necessitano di essere installati. Installa le dipendenze richieste come nello script `system-install.sh` o come nei Dockerfile.
Per comodità abbiamo alcuni script per aggiornare il tuo `PATH`:```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
Alcuni script richiedono una chiave API per recuperare metadati (es. ABI) dal
servizio Etherscan. Se disponi di una chiave API, devi impostare la
variabile d'ambiente ETHERSCAN_API_KEY per passarla agli script. Per
un flusso di lavoro basato su Docker, puoi avviare il contenitore Docker con il
flag --env oppure inserire la tua chiave API nel file .etherscan_api_key, che
incorporerà la chiave API nel contenitore Docker.
Per comodità, utilizziamo uno script wrapper che si occupa di tutti i dettagli
per te, quando avvii il fuzzer EF/CF: efcfuzz
Puoi impostare molte opzioni da riga di comando per configurare il comportamento del fuzzer
per quanto riguarda il processo di build e fuzzing. Dai un'occhiata a efcfuzz --help per un
elenco di opzioni.
Esempi
Compila il codice sorgente solidity in codice nativo EF/CF e avvia il fuzzing per 5 minuti (ovvero 300 secondi).```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
In alternativa, puoi avviare con output di fuzzing ridotto (`--quiet` sopprime l'output del fuzzatore di base, mentre `--print-progress` stamperà un breve riepilogo dell'avanzamento del fuzzing), avviando il fuzzatore su 4 core.```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
Usa il bytecode già compilato e compila il bytecode in codice nativo EF/CF e inizia il 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
Il wrapper può esportare lo stato di un contratto da un nodo go-ethereum/erigon e avviare
il fuzzing da lì.```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
Inoltre, puoi passare --include-address-deps=y per cercare ricorsivamente gli
indirizzi di altri account nello storage del contratto esportato e includere
anche quelli nell'esportazione dello stato. Tuttavia, questo non include altri
contratti memorizzati in tipi mapping di Solidity. Per esportare davvero
l'intero stato in modo ricorsivo, passa anche il flag --include-mapping-deps=y.
Ma attenzione: questa ricerca ricorsiva può portare a tempi di compilazione
lunghi e a scarse prestazioni di fuzzing. In particolare, i contratti usati di
frequente possono avere molto stato interno e usare il loro stato esportato può
rendere il fuzzing lento. Controlla se il fuzzer riesce a superare 1k execs/sec.
Se non ci riesce, dovresti piuttosto provare a creare uno stato artificiale e
più piccolo. Prova a eseguire un nodo go-ethereum locale in modalità --dev e
distribuisci lì i tuoi contratti. Poi esporta lo stato live da lì.
Il wrapper memorizza le build nella cache, quindi una seconda esecuzione di
fuzzing dovrebbe avviarsi molto più velocemente, perché il tempo di compilazione
iniziale non è più necessario. Se vuoi solo compilare e inserire il risultato
nella cache, puoi passare l'argomento --build-only.
Esempio: Fuzzing con proprietà
EF/CF supporta anche il fuzzing basato su proprietà usando la stessa definizione di proprietà del fuzzer echidna. Le proprietà (o invarianti) sono espresse come funzioni Solidity che fungono da oracolo di bug per il fuzzer. Ad esempio, puoi aggiungere una funzione Solidity:```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
Che rappresenta la proprietà che `total_balance` dovrebbe essere sempre inferiore a 1000. EF/CF riporterà quindi un bug se riesce a violare questa proprietà usando una qualche sequenza di transazioni, cioè se l'oracolo restituisce `false`.
Per comunicare a EF/CF che questa è una proprietà, devi specificare un elenco di firme di funzione in un file, che verrà raccolto da EF/CF come elenco di proprietà da controllare durante il fuzzing.
Il modo più semplice è ottenere le firme pertinenti usando il flag `--hashes` del compilatore Solidity, ad esempio,```
solc --hashes ./path/to/your.sol | grep test_property > property_list
Ora puoi avviare il fuzzer con:``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
Puoi anche aggiungere `--disable-detectors` per disabilitare gli oracoli di bug integrati basati su ether.
Puoi provare il seguente esempio per il fuzzing basato su proprietà:```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
Esempio: Fuzzing per eventi
EF/CF supporta il fuzzing per violazioni di asserzioni espresse tramite
eventi. In realtà, supportiamo anche l'uso di eventi personalizzati arbitrari come oracolo
per i bug. Per impostazione predefinita, EF/CF identifica un bug se il contratto target ha registrato uno
dei seguenti eventi: AssertionFailed(), AssertionFailed(uint256),
AssertionFailed(string) e Panic(uint256).```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
Puoi anche specificare argomenti/hash di eventi personalizzati aggiuntivi da monitorare in un
file con `--event-assertions-list ./path/to/eventslist.txt`. Come per la
lista di proprietà precedente, puoi ottenere il formato usando `solc --hashes` e
copiando gli hash e i nomi degli eventi nel file della lista degli eventi.
Per impostazione predefinita, EF/CF ignorerà gli eventi che non sono stati emessi dal contratto
target. Se vuoi cambiare questo comportamento, usa `--event-assertions-target-only=n`.
(Nota: puoi usare `--assertions` per abilitare sia il controllo degli eventi che quello delle asserzioni
di solidity)
**Esempio: Fuzzing per asserzioni di Solidity ^0.8**
Attualmente, non supportiamo il fuzzing per asserzioni arbitrarie nel codice solidity
per versioni di solidity precedenti alla 0.8. In precedenza, le asserzioni di solidity attivavano semplicemente
un opcode `invalid`, con conseguente revert piuttosto brusco. La versione
0.8 di solidity ha cambiato il comportamento: invece di usare l'opcode `invalid` per
eseguire il revert delle transazioni, ora viene utilizzato il meccanismo `revert` e gli errori vengono segnalati
al chiamante. Possiamo utilizzare questo tipo di propagazione degli errori come oracolo
di bug in EF/CF. Attualmente, EF/CF supporta il controllo del tipo di errore di solidity
`Panic(uint256)`. [Maggiori informazioni sugli errori
di 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
(Nota: puoi usare --assertions per abilitare sia il controllo delle asserzioni
di eventi che di Solidity)
Specifiche di sistema e configurazione
Consigliamo di allocare da 4 a 16 core e circa 1 GB di memoria per core. Puoi
utilizzare il flag --configure-system per configurare il sistema per il
fuzzing ad alta velocità, oppure configurarlo manualmente. Nei container Docker
devi anche configurare l'host per ottenere le migliori prestazioni. Se si
tratta di un host non critico, puoi avviare il container come --privileged e usare
/usr/local/bin/afl-system-config per configurare il sistema per il fuzzing ad alta
velocità (nota che questo di fatto esegue il container come 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
## Esecuzione di un esperimento di fuzzing
Per eseguire l'esperimento sul dataset `data/tests/` puoi usare il seguente
comando per compilare i contratti e il loro harness di fuzzing e poi eseguire il
fuzzer con diverse impostazioni, e molteplici ripetizioni, ecc. Poiché questo
richiederebbe parecchio tempo, possiamo eseguire questi esperimenti in parallelo. Suddividiamo gli
esperimenti di fuzzing in una fase di build e una fase di fuzz. Le fasi di build
compileranno tutti gli smart contract in sequenza (la compilazione stessa usa più core,
comunque). Poi avviamo 8 istanze del fuzzer in background, che prenderanno
gli artefatti di build dalla fase di build e avvieranno le esecuzioni di fuzzing. Il Makefile
tenterà automaticamente di avviare tutto nel container appropriato se
è disponibile `docker` o `podman`.```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
Disabilitiamo il sandboxing seccomp e di rete quando avviamo i contenitori in background. Disabilitare il sandboxing seccomp migliora le prestazioni di fuzzing. L'utilizzo della rete host consente a EF/CF di accedere ai nodi Ethereum nella rete locale senza ulteriore configurazione.
Abbiamo usato lo script ./scripts/run-tools-on-dataset.py per eseguire gli altri strumenti all'interno dei contenitori docker su questi dataset, ad esempio con questi comandi per il 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
Devi adattare lo script per configurare gli strumenti e il numero di esecuzioni.
### Configurare un Esperimento di Fuzzing
Qui usiamo l'esperimento `tests` come esempio. Basta sostituire la stringa
`tests` con il nome dell'esperimento nei seguenti passaggi:
1. Raccogli il tuo dataset in `./data/`, ad esempio il dataset `./data/tests` con test
contratti. Per i contratti Solidity abbiamo un `Makefile` generico per compilare i
contratti: `sol.Makefile`. Puoi riutilizzarlo se vuoi, vedi
`./data/tests/Makefile` per un esempio.
2. Crea uno script per compilare gli artefatti di build includendo qualsiasi
passaggio di pre-processing/scraping necessario. Ad esempio per il dataset `tests` abbiamo
lo script `./scripts/build-tests.sh`. Gli artefatti di build dovrebbero essere
salvati in `./builds/tests/${contract}.build.tar.xz`.
3. Crea uno script per lanciare la campagna di fuzzing, ad esempio per il dataset `tests`
crea uno script chiamato `./scripts/fuzz-tests.sh`. Di solito puoi usare la
funzione comune di campagna di fuzzing da `./scripts/common.sh`. Dai un'occhiata a
`fuzz-tests.sh` come modello.
4. I risultati di `fuzz-tests.sh` verranno salvati in `./results/run-fuzz-tests/`.
5. Per riassumere i risultati forniamo `./scripts/summarize.py` per gli script di lancio
basati su bash e `./scripts/summarize_l.py` per gli script di lancio python
(lo strumento `efcfuzz`).
Potresti dover adattare questi script a seconda del passaggio 3.
### Esperimenti di Fuzzing Esistenti
#### Benchmark
* <a href="./data/multi/">`./data/multi`</a> contiene il benchmark di scalabilità
che abbiamo usato per valutare quanto bene uno strumento di analisi scala a
sequenze di transazioni più lunghe. Consiste in tre tipi di contratti:
* `multi_gen_*.sol` - contratti sintetizzati automaticamente, che eseguono una serie di
`require(input <= MAGIC)` e poi impostano una variabile di stato interna. Se
tutte le variabili di stato sono impostate, allora `selfdestruct` (o l'oracolo echidna)
può essere attivato.
* `multi_man_complex_*.sol` - varianti create manualmente che funzionano in modo simile
ai contratti di tipo `multi_gen`, ma presentano vincoli un po' più complessi
(ad esempio, cose diverse dall'uguaglianza e disuguaglianza con un valore
magico)
* `justlen_*.sol` - questi sono presi dall'[esempio echidna-parade](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - sanity check che verificano che un fuzzer/strumento possa
in teoria trovare bug che richiedono 9 o 10 transazioni. Qui
l'analizzatore deve solo chiamare 10 funzioni nell'ordine giusto senza
argomenti. Questo è abbastanza facile per la maggior parte degli strumenti di analisi.
* <a href="./data/throughput/">`./data/throughput`</a> contiene i contratti che abbiamo
usato per valutare il throughput. Questa è una selezione di contratti di
dimensioni variabili. Nota che abbiamo corretto tutte le vulnerabilità in questi contratti,
così le vulnerabilità trovate non influiscono sulle misurazioni del throughput.
* <a href="./data/cov-max-testset">`./data/cov-max-testset`</a> contiene i
contratti che abbiamo usato per il confronto di fuzzer basati sulla copertura del codice.
#### Rilevamento di Bug
* <a href="./data/ethbmc-vuln">`./data/ethbmc-vuln`</a> elenco di contratti che
EthBMC ha rilevato come vulnerabili.
* <a href="./data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> elenco di
contratti in cui EthBMC ha interrotto l'analisi a causa di un timeout.
* <a href="./data/reentrancy">`./data/reentrancy`</a> un insieme di contratti
vulnerabili ad attacchi di rientranza.
* <a href="./data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> un insieme di contratti
che sono stati verificati contenere un bug di rientranza come parte dello
[studio sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth).
* <a href="./data/sailfish-dao">`./data/sailfish-dao`</a> elenco di tutti i contratti,
in cui
[sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs)
ha trovato un bug di rientranza.
* <a href="./data/sereum">`./data/sereum`</a> un elenco di contratti
vulnerabili ad attacchi di rientranza secondo [Sereum](https://github.com/uni-due-syssec/sereum-results).
* <a href="./data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a>
contratti dalla raccolta curata smartbugs, classificati come bug di "controllo accessi"
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="./data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a>
contratti dalla raccolta curata smartbugs, classificati come bug di "rientranza"
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### Test
I seguenti dataset contengono contratti di test sintetici di base per testare le
capacità del fuzzer:
* <a href="./data/tests">`./data/tests`</a> test di base raccolti da più
fonti che verificano le capacità di base di un fuzzer. Tutti usano un oracolo
selfdestruct.
* <a href="./data/tests-not-vuln">`./data/tests-not-vuln`</a> come tests, ma non dovrebbe
essere rilevato come vulnerabile.
* <a href="./data/properties-tests">`./data/properties-tests`</a> test per
fuzzing basato su proprietà
* <a href="./data/assertions-tests">`./data/assertions-tests`</a> test per
fuzzing per le asserzioni.
## Fuzzing in maggiore dettaglio
Utilizziamo script wrapper per lanciare il vero fuzzer (AFL++ nel nostro caso).
Questo viene fatto automaticamente quando si usa il launcher `efcfuzz`.```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
Se hai tmux e tmuxp installati, allora per lo sviluppo e l'ispezione la versione interattiva dello script potrebbe essere utile:```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
Questo compilerà e fuzzerà per un bel po' di tempo. Potrai poi
`cd ./fuzz/out/SimpleDAO*` per visualizzare i risultati del fuzzing. I nostri script wrapper fanno
un lavoro extra oltre al lancio del programma `afl-fuzz`, cioè principalmente
post-processing dei risultati. Inoltre genererà diversi script di comodo per analizzare i test case generati.
* `./a.sh` - Stampa una forma leggibile di un test case, wrapper attorno a
`efuzzcaseanalyzer`.
* `./r.sh` - esegue un test case con le stesse impostazioni di quando il fuzzer è stato eseguito.
* `./m.sh` - minimizza un testcase con le stesse impostazioni di quando il fuzzer è stato
eseguito.
* `./c.sh` - analizza la "catena" di test case che portano al test case dato. Utile per analizzare/ottimizzare il fuzzer. Puoi vedere rapidamente, quale
test case è stato prodotto da quale catena di mutazioni su quali voci della coda.
Richiede `fzf`.
Ci sono anche altri report di comodo, come
* `./bugs` e `./bugtypes` che riassumono eventuali bug identificati.
* `./crashes_min`, che contiene i crash minimizzati di tutte le istanze
`afl-fuzz`.
**Visualizza la copertura del codice dei blocchi di base EVM**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
Lo script fuzz/evm-bb-coverage.sh calcola la copertura dei basic block
dato una directory di output di AFL. L'harness può opzionalmente scaricare una traccia dei basic
block, che vengono poi confrontati con un elenco di basic block generato da
evm2cpp (cioè i file .bb_list in eEVM/contracts/).
Per impostazione predefinita calcoliamo anche la copertura che i nostri seed generici predefiniti (vedi
eEVM/fuzz/generic_seeds) producono:```bash
$ cat coverage-percent-seeds.evmcov
10.5890
L'elenco dei blocchi di base coperti è salvato nel file `all.evmcov`.
**Visualizzare il Riepilogo dei Testcase Generati**
`efuzzcaseanalyzer` può essere usato per visualizzare/riepilogare i testcase generati,
ad es.,```
$ 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
Le sintesi sono solitamente archiviate nei file crashes_tx_summary e
queue_tx_summary, ma quest'ultimo può essere un po' prolisso.
Analizzare un singolo testcase di crash``` $ ./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), }
E per ottenere il risultato effettivo del target di fuzzing, puoi eseguire:```
$ ./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)
Questo produce un output molto verboso, incluse alcune parti delle tracce di esecuzione dei contratti e il risultato del controllo del saldo eseguito dall'harness.
Minimizzazione degli input che causano crash
Gli input che causano crash spesso contengono transazioni non correlate a causa dell'approccio di test randomizzato. Questo può essere mitigato eseguendo una minimizzazione sull'input che causa il crash (cioè ridurre l'input finché causa ancora un crash). Se si desidera minimizzare input che non causano crash, è possibile utilizzare il flag -M per abilitare la minimizzazione in base alla copertura come criterio di minimizzazione.
Il seguente comando ridurrà il testcase e sovrascriverà il file:``` $ 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), }
## Lettura del formato dei casi di test
Il formato dei casi di test è pensato per il fuzzing e non è immediato da
leggere. Ci sono diverse sottigliezze di cui devi essere a conoscenza.
* Il formato dei casi di test è considerato come una "coda" di transazioni che
possono essere eseguite. Non appena si verifica un qualsiasi problema, l'elaborazione
del caso di test si interrompe. Questo include:
* Quando una transazione viene revertita.
* Qualsiasi errore riscontrato dal codice di harness.
* Qualsiasi bug attivato e rilevato.
Di conseguenza, un caso di test stampato non corrisponde necessariamente a ciò che
viene eseguito: possono esserci transazioni alla fine che non vengono eseguite.
Controlla l'output verbose e usa il minimizer per eliminare quelle!
* Allo stesso modo, possono esserci troppi `returns` o flag `reenter` spurii. Usa
il minimizer dei casi di test per eliminarli.
* Un contratto viene rientrato solo quando nella lista è presente un'altra transazione
dopo la transazione che dovrebbe eseguire il reentrancy (cioè, c'è
un'altra voce successiva nella coda).
* Anche se il flag `reenter` è impostato su un valore, questo non significa
necessariamente che il contratto venga rientrato, solo che il codice di harness
proverà a farlo se possibile. Ad esempio, se il contratto non esegue una call, il
flag `reenter` viene ignorato perché non c'è possibilità di rientro. Di solito il
minimizer rimuove eventuali flag `reenter` spurii.
In generale molti di questi problemi scompaiono quando usi il minimizer dei casi di
test, quindi è sempre una buona idea usarlo prima di analizzare i casi di test
generati.
## Falsi allarmi noti
Abbiamo osservato diversi tipi di falsi allarmi che sembrano ricorrenti quando si
esegue il fuzzing di contratti con EF/CF.
* Contratti che pagano Ether per progettazione. L'oracolo dei bug per i guadagni di
Ether di EF/CF segnala questi contratti come vulnerabili, anche se operano come
previsto:
* Contratti di gioco d'azzardo: molti contratti di gioco d'azzardo presentano
una qualche forma di casualità, che è già una cattiva pratica su Ethereum.
Tuttavia, alcuni contratti di gioco d'azzardo sono implementati in modo da
obbligarti a indovinare, ad esempio, le ultime due cifre del blockhash
successivo o qualcosa di simile. Questo può essere abilitato tramite uno
schema di impegno (commitment scheme), cioè la prima transazione impegna
l'utente a un certo valore e la seconda attiva il tentativo e il pagamento
in caso di vittoria. Questi contratti di solito non sono sfruttabili in una
blockchain reale. Tuttavia, nella blockchain simulata di EF/CF, il fuzzer
può adattare l'impegno dopo che il valore nella seconda transazione è stato
osservato. Questo fatto è importante per EF/CF per raggiungere una migliore
copertura del codice. Tuttavia, rende anche facile per EF/CF identificare
una sequenza di TX che consente al fuzzer di vincere deterministicamente nel
contratto di gioco d'azzardo.
* Contratti che pagano interessi: esistono molti piccoli contratti che ti
consentono di investire Ether e poi pagano una certa percentuale di interessi
ogni `N` blocchi. L'attaccante simulato di EF/CF è in grado di attendere `N`
blocchi e poi riceve il pagamento degli interessi, che viene nuovamente
rilevato dall'oracolo dei bug per i guadagni di Ether.
* Airdrop: alcuni contratti token abilitano gli airdrop, cioè distribuiscono
token a chiunque li richieda fino al raggiungimento di determinati limiti.
Ad esempio, gli airdrop sono spesso abilitati solo per un breve periodo di
tempo. Se un contratto di questo tipo viene distribuito all'interno di EF/CF,
è molto probabile che il limite di tempo sia impostato in modo che gli
airdrop siano ancora attivi. EF/CF rileva quindi un guadagno di Ether se i
token distribuiti possono essere rivenduti.
* Segnalazione anticipata di `DELEGATECALL` controllabile: attualmente segnaliamo
una delegatecall controllabile non appena viene invocata. Tuttavia, esistono
diversi contratti che presentano funzioni che consentono intenzionalmente al
chiamante di eseguire una delegatecall verso un indirizzo arbitrario. Tuttavia,
queste funzioni revertiscono incondizionatamente la transazione immediatamente
dopo la delegatecall. Questo impedisce che eventuali aggiornamenti di stato o
trasferimenti di ether persistano. Tipicamente funzioni di questo tipo contengono
parole come "simulate" nei nomi delle funzioni e quindi sono facili da individuare.
* Questo potrebbe essere risolto in EF/CF rimandando la segnalazione fino alla
fine dell'esecuzione. Tuttavia, questo complica non poco l'oracolo dei bug.
* Attualmente non ci sono piani per risolvere questo problema.
* Initializer chiamabile: abbiamo osservato che quando si esegue il fuzzing di
contratti esportati dalla blockchain, EF/CF a volte riesce a chiamare le funzioni
initializer, anche se il contratto è già stato inizializzato. Normalmente, questo
dovrebbe attivare un revert, ma non lo fa nell'ambiente EVM di EF/CF. Chiamare di
nuovo l'initializer porta spesso a guadagni di Ether banali, perché ad esempio
l'initializer imposta una variabile *owner* o qualcosa di simile.
* Non siamo ancora sicuri di quale sia la causa principale di questo problema.
Tuttavia, è in genere facile da individuare, poiché la funzione initializer
è tipicamente chiamata `initializer`, `init` o simile.
## Insidie comuni
Abbiamo fatto del nostro meglio per rendere questo strumento piuttosto utilizzabile,
ma è comunque un prototipo di ricerca. Aspettati che le cose si rompano. Ecco alcuni
problemi comuni che abbiamo osservato:
* *D: ricevo uno strano errore di compilazione dovuto a una macro `TOKENPASTE`.*
R: questo accade spesso quando `efcfuzz` indovina il nome errato del contratto
(cioè, indovina un contratto astratto); prova a passare `--name YourContract`
per specificare il contratto target.