
EF/CF - Fuzzing de Contratos Inteligentes Extremamente Rápido
EF/CF é uma nova abordagem para fuzzing de contratos inteligentes: em vez de usar um novo fuzzer personalizado, ele reaproveita a infraestrutura de fuzzing existente para código C/C++ e a aplica a contratos inteligentes. Atualmente, o AFL++ é o fuzzer suportado principalmente, embora haja também algum suporte muito rudimentar para libfuzzer e honggfuzz.
Por que usar a infraestrutura de fuzzing existente?
Quais são alguns dos problemas que encontramos pelo caminho?
./src/ethmutator/./src/evm2cpp/Este repositório é o ponto de entrada principal para o projeto EF/CF. Ele contém todo
o código relevante como subprojetos em ./src/ e vários scripts de conveniência para
instalação, scripts para lançar campanhas de fuzzing e diversos conjuntos de dados para
testar o fuzzer (e compará-lo com outras ferramentas).
./src/ - contém todo o código-fonte necessário para compilar e executar o EF/CF; para
reprodutibilidade, todas as dependências diretas são adicionadas como submódulos git../data/ - contém os conjuntos de dados usados durante a avaliação./scripts - contém scripts para executar experimentos, instalação, etc../docker - Dockerfile para fluxo de trabalho baseado em contêiner
./docker/tools/ contém dockerfiles para ferramentas com as quais comparamos o EF/CF.
Fizemos o nosso melhor para fixar as versões que avaliamos em nosso
artigo nos dockerfiles../EXPERIMENTS.md - contém um guia para
reproduzir os experimentos do nosso artigo../examples - contém exemplos de saídas produzidas pelo EF/CFDescrevemos a arquitetura, a implementação e resumimos os resultados de nossa avaliação em nosso artigo: pré-impressão no arxiv
Ao se referir ao EF/CF em trabalhos acadêmicos, use a seguinte entrada bibtex para citar:```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", }
## Início rápido
A maneira recomendada é executar EF/CF como um contêiner docker interativo.
1. Entre no contêiner com um shell ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
ou construa o container a partir do repositório clonado ``` make gitmodules # to fetch the git submodules make container-enter
1. Compile e então faça fuzz de um contrato solidity até que o primeiro crash/bug seja
descoberto: ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
Sem git? se você usa uma release tarball/docker, ignore isto.
Execute git submodule update --init para buscar os últimos commits dos submódulos em repositórios já clonados.
Certifique-se de executar isso também em ./src/eEVM.```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*Aviso:* Executar `git clone --recursive $repo` ou passar o argumento `--recursive` para `git sumbodule (update|init)` fará com que o git entre recursivamente nos submódulos do repositório AFL++, que não são necessários para este projeto. Portanto, para economizar espaço, é melhor evitar os checkouts recursivos de submódulos.
### Contêiner
Oferecemos os seguintes alvos de conveniência do make para fluxos de trabalho baseados em contêineres:```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
Se você quiser garantir um build limpo, pode usar o seguinte comando```sh make container-build CLEAN_CHECKOUT=1
Alternativamente, o container pode ser construído com o seguinte comando docker:```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
Note que há também um Dockerfile baseado em Archlinux e Fedora. Eles devem funcionar também, mas não são tão bem testados.
Para distribuir manualmente uma imagem docker (por exemplo, se incluindo algumas alterações locais), use:``` make container-release docker load -i ./efcf*.tar
Recomendamos as seguintes opções do docker para execução:
* `--security-opt seccomp=unconfined` - melhor desempenho de fuzzing
* `--net=host` - para acesso fácil a um nó Ethereum local
* `--tmpfs "/tmp/efcf/":exec,size=6g` - coloque os arquivos temporários do EF/CF em um ramdisk, se possível (menos desgaste do disco)
* `--privileged` - para executar `afl-system-config` ou `efcfuzz --configure-system`
* `-v` - para persistir os dados de saída do EF/CF
### VM / Bare-Metal
Para fluxos de trabalho baseados em VM ou bare-metal:```sh
make system-install # install efcf to current system (requires root or sudo rights)
Note que muitos dos scripts funcionam com a estrutura de diretórios relativa de qualquer forma, então
isso principalmente instala dependências e algumas ferramentas que são úteis de ter no seu
PATH. Testamos a execução de EF/CF nas seguintes distribuições Linux:
(A distribuição não importa muito; testamos LLVM 13 e 14, sendo o 14 a
escolha preferida. LLVM 11 ou 12 também podem funcionar, mas como sempre — quanto mais novo, melhor. O importante é que exista um LLVM compatível com o nosso fork do AFL++.)
Não testamos EF/CF nativamente no Mac OS. Provavelmente algumas coisas não funcionarão (por exemplo, afl-clang-lto no Mac OS parece não funcionar). A melhor opção é utilizar o 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
Testamos usando o docker desktop v4.21.1 e o uso básico de EF/CF funciona. No entanto, considere o seguinte:
* Se você vir segfaults durante a compilação: tente aumentar o limite de memória da VM que o docker usa no Mac OS.
* Tente habilitar a aceleração usando rosetta no docker - esperamos que isso seja um pouco mais rápido.
### Configuração de Desenvolvimento
As ferramentas geralmente não precisam ser instaladas. Instale as dependências
necessárias conforme o script `system-install.sh` ou conforme os Dockerfiles.
Para conveniência, temos alguns scripts para atualizar seu `PATH`:```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
Alguns dos scripts exigem uma chave de API para buscar metadados (por exemplo, ABI) do
serviço Etherscan. Se você tiver uma chave de API, deve definir a
variável de ambiente ETHERSCAN_API_KEY para passá-la aos scripts. Para
um fluxo de trabalho baseado em Docker, você pode iniciar o contêiner Docker com a
flag --env ou colocar sua chave de API no arquivo .etherscan_api_key, o que
incorporará a chave de API ao contêiner Docker.
Por conveniência, utilizamos um script wrapper que cuida de todos os detalhes
para você ao iniciar o fuzzer EF/CF: efcfuzz
Você pode definir várias opções de linha de comando para configurar o comportamento do
fuzzer em relação ao processo de compilação e fuzzing. Dê uma olhada em efcfuzz --help para uma
lista de opções.
Exemplos
Compile o código-fonte solidity para código nativo EF/CF e inicie o fuzzing por 5 minutos (ou seja, 300 segundos).```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
Alternativamente, lance com saída de fuzzing reduzida (`--quiet` suprime a saída do fuzzer base, enquanto `--print-progress` imprimirá um breve resumo do progresso do fuzzing), e execute o fuzzer em 4 núcleos.```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
Use o bytecode já compilado e compile o bytecode para código nativo EF/CF e comece o 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
O wrapper pode exportar o estado de um contrato a partir de um nó go-ethereum/erigon e iniciar
fuzzing a partir daí.```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
Além disso, você pode passar --include-address-deps=y para pesquisar recursivamente
endereços de outras contas no armazenamento do contrato exportado e também incluir
esses endereços na exportação de estado. No entanto, isso não inclui outros contratos
armazenados em tipos mapping do Solidity. Para realmente exportar todo o estado
recursivamente, passe também a flag --include-mapping-deps=y.
Mas atenção: essa busca recursiva pode levar a longos tempos de compilação e baixo
desempenho de fuzzing. Especialmente, contratos muito usados podem ter muito
estado interno, e usar o estado exportado deles pode tornar o fuzzing lento. Verifique
se o fuzzer consegue alcançar mais de 1k execuções/seg. Se não, é melhor
tentar criar um estado artificial e menor. Tente executar um nó go-ethereum local
no modo --dev e implantar seus contratos nele. Em seguida, exporte o estado ativo
a partir dele.
O wrapper armazena em cache as compilações, então uma segunda execução do fuzzing deve iniciar muito mais rápido,
pois o tempo de compilação inicial não é mais necessário. Se você quiser apenas
compilar e colocá-lo no cache, basta passar o argumento --build-only.
Exemplo: Fuzzing com propriedades
O EF/CF também suporta fuzzing baseado em propriedades usando a mesma definição de propriedade do fuzzer echidna. Propriedades (ou invariantes) são expressas como funções Solidity que atuam como um oráculo de bugs para o fuzzer. Por exemplo, você pode adicionar uma função Solidity:```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
Que representa a propriedade de que o total_balance deve estar sempre abaixo de
1000. O EF/CF reportará um bug se conseguir violar essa propriedade usando
alguma sequência de transações, ou seja, o oráculo retorna `false`.
Para dizer ao EF/CF que essa é uma propriedade, é preciso especificar uma lista de
assinaturas de funções em um arquivo, que será reconhecida pelo EF/CF como uma lista
de propriedades para verificar durante o fuzzing.
A maneira mais fácil é obter as assinaturas relevantes usando a flag `--hashes`
do compilador Solidity, por exemplo,```
solc --hashes ./path/to/your.sol | grep test_property > property_list
Agora você pode iniciar o fuzzer com:``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
Você também pode adicionar `--disable-detectors` para desabilitar os oráculos de bugs integrados baseados em ether.
Você pode tentar o seguinte exemplo para fuzzing baseado em propriedades:```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
Exemplo: Fuzzing para Eventos
EF/CF suporta fuzzing para violações de asserção expressas através de
eventos. Na verdade, também suportamos o uso de eventos personalizados arbitrários como oráculo
de bug. Por padrão, o EF/CF identificará um bug se o contrato alvo registrar um
dos seguintes eventos: AssertionFailed(), AssertionFailed(uint256),
AssertionFailed(string) e Panic(uint256).```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
Você também pode especificar tópicos/hashes de eventos personalizados adicionais para observar em um arquivo com `--event-assertions-list ./path/to/eventslist.txt`. Assim como na lista de propriedades anterior, você pode obter o formato usando `solc --hashes` e copiando os hashes e nomes de eventos para o arquivo da lista de eventos.
Por padrão, o EF/CF ignorará eventos que não foram emitidos pelo contrato alvo. Se quiser mudar isso, use `--event-assertions-target-only=n`.
(Nota: você pode usar `--assertions` para habilitar a verificação de asserções de eventos e do Solidity)
**Exemplo: Fuzzing para asserções do Solidity ^0.8**
Atualmente, não suportamos fuzzing para asserções arbitrárias em código Solidity para versões do Solidity anteriores à 0.8. Anteriormente, asserções do Solidity simplesmente acionavam um opcode `invalid`, resultando em um revert bastante abrupto. A versão 0.8 do Solidity mudou o comportamento: em vez de usar o opcode `invalid` para reverter transações, agora utiliza o mecanismo `revert` e sinaliza erros de volta ao chamador. Podemos utilizar esse tipo de propagação de erros como um oráculo de bugs no EF/CF. Atualmente, o EF/CF suporta a verificação do tipo de erro do Solidity `Panic(uint256)`. [Mais informações sobre erros do 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: você pode usar --assertions para habilitar a verificação de asserções
tanto de eventos quanto de solidity)
Especificações e Configuração do Sistema
Recomendamos alocar de 4 a 16 núcleos e aproximadamente 1 GB de memória por núcleo. Você
pode utilizar a flag --configure-system para configurar seu sistema para alta
velocidade de fuzzing, ou configurá-lo você mesmo. Em containers docker, você também precisa
configurar o host para obter o melhor desempenho. Se for um host não crítico, você pode
iniciar o container como --privileged e usar
/usr/local/bin/afl-system-config para configurar o sistema para alta velocidade
de fuzzing (observe que isso essencialmente executa o container como 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
## Executando um Experimento de Fuzzing
Para executar o experimento no conjunto de dados `data/tests/`, você pode usar o seguinte
comando para construir os contratos e o respectivo harness de fuzzing e depois executar o
fuzzer com diferentes configurações, e múltiplas repetições, etc. Como isso
levaria bastante tempo, podemos executar esses experimentos em paralelo. Dividimos os
experimentos de fuzzing em uma etapa de build e uma etapa de fuzz. As etapas de build irão
construir todos os contratos inteligentes sequencialmente (a construção em si usa múltiplos núcleos,
no entanto). Em seguida, lançamos 8 instâncias do fuzzer em segundo plano, que pegarão
os artefatos de build da etapa de build e iniciarão execuções de fuzzing. O Makefile
tentará automaticamente lançar tudo no container adequado se
`docker` ou `podman` estiver disponível.```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
Desativamos o seccomp e o sandboxing de rede ao iniciar os contêineres em segundo plano. Desativar o sandboxing do seccomp melhora o desempenho de fuzzing. Usar a rede do host permite que EF/CF acesse os nós Ethereum na rede local sem configuração adicional.
Usamos o script ./scripts/run-tools-on-dataset.py para executar as outras ferramentas
dentro de contêineres docker nesses datasets, por exemplo, com estes comandos para o
multi dataset:```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
Você precisa adaptar o script para configurar as ferramentas e o número de execuções.
### Configurando um Experimento de Fuzzing
Aqui, usamos o experimento `tests` como exemplo. Basta substituir a string
`tests` pelo nome do experimento nas seguintes etapas:
1. Reúna seu dataset em `./data/`, por exemplo, o dataset `./data/tests` com contratos de teste.
Para contratos Solidity, temos um `Makefile` genérico para compilar os
contratos: `sol.Makefile`. Você pode reutilizá-lo se desejar, veja
`./data/tests/Makefile` para um exemplo.
2. Crie um script para gerar os artefatos de build, incluindo qualquer etapa de
pré-processamento/coleta necessária. Por exemplo, para o dataset `tests`, temos
o script `./scripts/build-tests.sh`. Os artefatos de build devem ser
armazenados em `./builds/tests/${contract}.build.tar.xz`.
3. Crie um script para lançar a campanha de fuzzing, por exemplo, para o dataset `tests`,
crie um script chamado `./scripts/fuzz-tests.sh`. Normalmente, você pode usar a
função comum de campanha de fuzzing de `./scripts/common.sh`. Dê uma olhada no
`fuzz-tests.sh` como modelo.
4. Os resultados de `fuzz-tests.sh` serão armazenados em `./results/run-fuzz-tests/`.
5. Para resumir os resultados, fornecemos `./scripts/summarize.py` para os scripts de
lançamento baseados em bash e `./scripts/summarize_l.py` para scripts de lançamento
em python (a ferramenta `efcfuzz`).
Você pode precisar adaptar esses scripts dependendo da sua etapa 3.
### Experimentos de Fuzzing Existentes
#### Benchmarks
* <a href="./data/multi/">`./data/multi`</a> contém o benchmark de escalabilidade
que usamos para avaliar o quão bem uma ferramenta de análise escala para sequências
de transações mais longas. Ele consiste em três tipos de contratos:
* `multi_gen_*.sol` - contratos sintetizados automaticamente, que fazem uma série
de `require(input <= MAGIC)` e depois definem uma variável de estado interna. Se
todas as variáveis de estado forem definidas, então o `selfdestruct` (ou oráculo
do echidna) pode ser acionado.
* `multi_man_complex_*.sol` - variantes criadas manualmente que funcionam de forma
semelhante aos contratos do tipo `multi_gen`, mas apresentam restrições um pouco
mais complicadas (por exemplo, outras coisas além de igualdade e desigualdade com
um valor mágico)
* `justlen_*.sol` - estes são retirados do [exemplo echidna-parade](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - verificações de sanidade que verificam se um fuzzer/ferramenta
pode, em teoria, encontrar bugs que exigem 9 ou 10 transações. Aqui, o
analisador só precisa chamar 10 funções na ordem correta, sem argumentos.
Isso é bastante fácil para a maioria das ferramentas de análise.
* <a href="./data/throughput/">`./data/throughput`</a> contém os contratos que usamos
para avaliar o throughput. Esta é uma seleção de contratos de
tamanhos variados. Observe que corrigimos todas as vulnerabilidades nesses contratos,
para que as vulnerabilidades encontradas não impactem as medições de throughput.
* <a href="./data/cov-max-testset">`./data/cov-max-testset`</a> contém os
contratos que usamos para comparação de fuzzers baseados em cobertura de código.
#### Detecção de Bugs
* <a href="./data/ethbmc-vuln">`./data/ethbmc-vuln`</a> lista de contratos que
o EthBMC detectou como vulneráveis.
* <a href="./data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> lista de
contratos em que o EthBMC interrompeu a análise devido a timeout.
* <a href="./data/reentrancy">`./data/reentrancy`</a> um conjunto de contratos
vulneráveis a ataques de reentrância.
* <a href="./data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> um conjunto de contratos
que foram verificados como contendo um bug de reentrância como parte do
[estudo sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth).
* <a href="./data/sailfish-dao">`./data/sailfish-dao`</a> lista de todos os contratos
em que o
[sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs)
encontrou um bug de reentrância.
* <a href="./data/sereum">`./data/sereum`</a> uma lista de contratos
vulneráveis a ataques de reentrância de acordo com o [Sereum](https://github.com/uni-due-syssec/sereum-results).
* <a href="./data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a>
contratos do smartbugs curado, classificados como bugs de "controle de acesso"
([github smartbugs](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="./data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a>
contratos do smartbugs curado, classificados como bugs de "reentrância"
([github smartbugs](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### Testes
Os seguintes datasets contêm contratos de teste sintéticos básicos para testar as
capacidades do fuzzer:
* <a href="./data/tests">`./data/tests`</a> testes básicos reunidos de múltiplas
fontes que verificam as capacidades básicas de um fuzzer. Todos usam um oráculo
de selfdestruct.
* <a href="./data/tests-not-vuln">`./data/tests-not-vuln`</a> igual aos testes, mas não deve
ser detectado como vulnerável.
* <a href="./data/properties-tests">`./data/properties-tests`</a> testes para
fuzzing baseado em propriedades
* <a href="./data/assertions-tests">`./data/assertions-tests`</a> testes para
fuzzing de asserções.
## Fuzzing em Mais Detalhe
Utilizamos scripts wrapper para lançar o fuzzer real (AFL++ no nosso caso).
Isso é feito automaticamente ao usar o lançador `efcfuzz`.```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
Se você tem tmux e tmuxp instalados, então para desenvolvimento e inspeção a versão interativa do script pode ser útil:```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
Isso irá então compilar e executar o fuzzing por um bom tempo. Você pode então
`cd ./fuzz/out/SimpleDAO*` para ver os resultados do fuzzing. Nossos scripts de
wrapper fazem um trabalho extra além de iniciar o programa `afl-fuzz`, ou seja,
principalmente o pós-processamento dos resultados. Além disso, eles gerarão vários
scripts de conveniência para analisar os casos de teste gerados.
* `./a.sh` - Exibe uma forma legível para humanos de um caso de teste, wrapper do
`efuzzcaseanalyzer`.
* `./r.sh` - executa um caso de teste com as mesmas configurações de quando o
fuzzer foi executado.
* `./m.sh` - minimiza um caso de teste com as mesmas configurações de quando o
fuzzer foi executado.
* `./c.sh` - analisa a "cadeia" de casos de teste que levam ao caso de teste
fornecido. Útil para analisar/otimizar o fuzzer. Você pode ver rapidamente qual
caso de teste foi produzido por qual cadeia de mutações em quais entradas de fila.
Requer `fzf`.
Há também alguns outros relatórios de conveniência, como
* `./bugs` e `./bugtypes`, que resumem quaisquer bugs que foram identificados.
* `./crashes_min`, que contém crashes minimizados de todas as instâncias do
`afl-fuzz`.
**Visualizar a Cobertura de Código por Blocos Básicos da EVM**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
O script fuzz/evm-bb-coverage.sh calculará a cobertura de blocos básicos dado um diretório de saída do AFL. O harness pode opcionalmente despejar um trace de blocos básicos, que são então comparados a uma lista de blocos básicos gerada por evm2cpp (ou seja, os arquivos .bb_list em eEVM/contracts/).
Por padrão, também calculamos a cobertura que nossas sementes genéricas padrão (veja eEVM/fuzz/generic_seeds) produzem:```bash
$ cat coverage-percent-seeds.evmcov
10.5890
A lista de blocos básicos cobertos é armazenada no arquivo `all.evmcov`.
**Ver Resumo dos Casos de Teste Gerados**
`efuzzcaseanalyzer` pode ser usado para visualizar/resumir os casos de teste gerados, por exemplo,```
$ 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
Os resumos geralmente são armazenados nos arquivos crashes_tx_summary e
queue_tx_summary, mas este último pode ser um pouco verboso.
Analisar um único caso de teste que causa falha``` $ ./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 para obter o resultado real do alvo de fuzz, você pode executar:```
$ ./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)
Isso gera muita saída detalhada, incluindo algumas partes dos rastros de execução dos contratos e o resultado da verificação de saldo que o harness realiza.
Minimizando Entradas que Causam Falhas
Entradas que causam falhas frequentemente contêm transações não relacionadas devido à
abordagem de teste aleatório. Isso pode ser mitigado realizando uma minimização na
entrada que causa a falha (ou seja, reduzir a entrada enquanto ela ainda causar uma falha).
Se você quiser minimizar entradas que não causam falhas, então pode usar a flag -M para
ativar a minimização de acordo com a cobertura como critério de minimização.
O seguinte comando reduzirá o testcase e sobrescreverá o arquivo:``` $ 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), }
## Lendo o formato de caso de teste
O formato de caso de teste é voltado para fuzzing e não é tão simples de ler. Existem várias sutilezas das quais você precisa estar ciente.
* O formato de caso de teste é considerado como uma "fila" de transações que podem ser executadas. Assim que qualquer problema é encontrado, o processamento do caso de teste é interrompido. Isso inclui:
* Quando uma transação é revertida.
* Quando qualquer erro é encontrado pelo código de harness.
* Quando qualquer bug é acionado e detectado.
Como consequência, um caso de teste impresso não corresponde necessariamente ao que é executado - pode haver transações no final que não são executadas. Verifique a saída detalhada e use o minimizador para se livrar delas!
* Da mesma forma, pode haver `returns` demais ou flags `reenter` espúrias. Use
o minimizador de casos de teste para se livrar delas.
* Um contrato só sofre reentrada quando há outra transação na lista
após a transação que deveria fazer a reentrância (ou seja, há
outra entrada seguinte na fila).
* Mesmo que a flag `reenter` esteja definida para algo, isso não significa
necessariamente que o contrato sofrerá reentrada, apenas que o código de
harness tentará fazê-lo se possível. Por exemplo, se o contrato não realizar
uma chamada, a flag `reenter` é ignorada, pois não há possibilidade de
reentrar. Normalmente, o minimizador removerá quaisquer flags `reenter`
espúrias.
Em geral, muitos desses problemas desaparecem quando você usa o minimizador de casos de teste,
então é sempre uma boa ideia usá-lo antes de analisar casos de teste
gerados.
## Falsos Alarmes Conhecidos
Observamos vários tipos de falsos alarmes que parecem ser recorrentes ao fazer fuzzing de contratos com EF/CF.
* Contratos que pagam Ether por design. O oráculo de bugs Ether-gains do EF/CF
detectará esses contratos como vulneráveis, embora estejam operando conforme
o projetado:
* Contratos de apostas: muitos contratos de apostas apresentam alguma forma de
aleatoriedade, o que já é uma má prática no Ethereum. No entanto,
alguns contratos de apostas são implementados de uma forma que força você a adivinhar,
por exemplo, os dois últimos dígitos do próximo blockhash ou algo semelhante.
Isso pode ser habilitado usando um esquema de compromisso, ou seja, a primeira
transação compromete o usuário a um determinado valor e a segunda aciona
a adivinhação e o pagamento se ganhar. Esses contratos geralmente não são
exploráveis em uma blockchain real. No entanto, na blockchain simulada
do EF/CF, o fuzzer pode adaptar o compromisso depois que o valor na
segunda transação é observado. Esse fato é importante para o EF/CF
alcançar uma melhor cobertura de código. No entanto, também facilita para o EF/CF
identificar uma sequência de TX que permite ao fuzzer vencer deterministicamente no
contrato de apostas.
* Contratos que pagam juros: existem muitos contratos pequenos que permitem
investir Ether e depois pagar uma certa porcentagem de juros a cada
`N` blocos. O atacante simulado do EF/CF é capaz de esperar por `N` blocos
e então recebe o pagamento de juros, que é novamente detectado pelo
oráculo de bugs Ether-gains.
* Airdrops: alguns contratos de token permitem airdrops, ou seja, simplesmente
distribuem tokens para qualquer pessoa que os solicite até que certos limites
sejam atingidos. Por exemplo, os airdrops geralmente são habilitados apenas
por um curto período de tempo. Se tal contrato for implantado no EF/CF, há
grandes chances de que o limite de tempo seja definido de forma que os
airdrops ainda estejam habilitados. O EF/CF então detecta um Ether-gains se
os tokens distribuídos puderem ser vendidos novamente.
* Relato antecipado de `DELEGATECALL` controlável: atualmente, relatamos um
delegatecall controlável assim que ele é invocado. No entanto, existem vários
contratos que possuem funções que intencionalmente permitem ao chamador
realizar um delegatecall para um endereço arbitrário. No entanto, essas
funções revertem incondicionalmente a transação imediatamente após o
delegatecall. Isso impede que quaisquer atualizações de estado ou transferências
de ether persistam. Normalmente, essas funções têm palavras como "simulate"
em seus nomes de função e, então, são fáceis de identificar.
* Isso poderia ser corrigido no EF/CF adiando o relato até o final da
execução. No entanto, isso complica bastante o oráculo de bugs.
* Atualmente, não há planos para corrigir isso.
* Inicializador chamável: observamos que, ao fazer fuzzing de contratos
exportados da blockchain, o EF/CF às vezes consegue chamar funções
inicializadoras, mesmo que o contrato já tenha sido inicializado.
Normalmente, isso deveria acionar um revert, mas não o faz no ambiente EVM
do EF/CF. Chamar o inicializador novamente geralmente leva a ganhos triviais
de Ether, porque, por exemplo, o inicializador define uma variável *owner*
ou algo semelhante.
* Ainda não temos certeza de qual é a causa raiz desse problema. No entanto,
geralmente é fácil de identificar, já que a função inicializadora
normalmente é chamada de `initializer`, `init`, ou algo semelhante.
## Armadilhas Comuns
Fizemos o nosso melhor para tornar isso razoavelmente utilizável, mas ainda é um protótipo de pesquisa. Espere que coisas quebrem. Aqui estão alguns problemas comuns que observamos:
* *Q: Recebo um erro de compilação estranho devido a uma macro `TOKENPASTE`.*
A: Isso geralmente acontece quando o `efcfuzz` adivinha o nome errado do contrato (ou seja,
um contrato abstrato). Tente passar `--name YourContract` para
especificar o contrato alvo.