
EF/CF - Fuzzing de contratos inteligentes extremadamente rápido
EF/CF es un nuevo enfoque para el fuzzing de contratos inteligentes: en lugar de usar un nuevo fuzzer construido a medida, reutiliza la infraestructura de fuzzing existente de C/C++ para contratos inteligentes. Actualmente, AFL++ es el fuzzer soportado principalmente, aunque hay también un soporte muy rudimentario para libfuzzer y honggfuzz.
¿Por qué usar la infraestructura de fuzzing existente?
¿Cuáles son algunos de los problemas que encontramos en el camino?
./src/ethmutator/./src/evm2cpp/Este repositorio es el punto de entrada principal para el proyecto EF/CF. Contiene todo
el código relevante como subproyectos en ./src/ y varios scripts de conveniencia para
la instalación, scripts para lanzar campañas de fuzzing y varios conjuntos de datos para
probar el fuzzer (y compararlo con otras herramientas).
./src/ - contiene todo el código fuente necesario para construir y ejecutar EF/CF; para
la reproducibilidad, todas las dependencias directas se añaden como submódulos de git../data/ - contiene los conjuntos de datos utilizados durante la evaluación./scripts - contiene scripts para ejecutar experimentos, instalación, etc../docker - Dockerfile para el flujo de trabajo basado en contenedores
./docker/tools/ contiene dockerfiles para las herramientas con las que evaluamos EF/CF
Hicimos todo lo posible por fijar las versiones que evaluamos en nuestro
artículo en los dockerfiles../EXPERIMENTS.md - contiene una guía para
reproducir los experimentos de nuestro artículo../examples - contiene ejemplos de salidas producidas por EF/CFDescribimos la arquitectura e implementación de EF/CF y resumimos nuestra evaluación resultados en nuestro artículo: preprint de arxiv.org
Cuando se refiera a EF/CF en trabajos académicos, utilice la siguiente entrada bibtex para la cita:```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", }
## Inicio rápido
La forma recomendada es ejecutar EF/CF como un contenedor docker interactivo.
1. Entra en el contenedor con una shell ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
o construye el contenedor desde el repositorio clonado ``` make gitmodules # to fetch the git submodules make container-enter
1. Compilar y luego fuzzear un contrato solidity hasta que se descubra el
primer fallo/error: ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
¿Sin git? Si usas una versión tarball/docker, ignora esto.
Ejecuta git submodule update --init para obtener los últimos commits de los submódulos en repositorios ya clonados.
Asegúrate de ejecutar esto también en ./src/eEVM.```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*Advertencia:* Ejecutar `git clone --recursive $repo` o pasar el argumento `--recursive` a `git sumbodule (update|init)` hará que git entre de forma recursiva en los submódulos del repositorio AFL++, que no son necesarios para este proyecto. Así que para ahorrar espacio es mejor evitar los checkouts recursivos de submódulos.
### Contenedor
Proporcionamos los siguientes objetivos make de conveniencia para flujos de trabajo basados en contenedores:```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
Si quieres asegurar una compilación limpia, puedes usar el siguiente comando```sh make container-build CLEAN_CHECKOUT=1
Alternativamente, el contenedor puede construirse con el siguiente comando de docker:```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
Tenga en cuenta que también hay un Dockerfile basado en Archlinux y Fedora. Deberían funcionar también, pero no están tan probados.
Para distribuir manualmente una imagen de Docker (p. ej., si se incluyen algunos cambios locales), use:``` make container-release docker load -i ./efcf*.tar
Recomendamos las siguientes opciones de Docker para el lanzamiento:
* `--security-opt seccomp=unconfined` - mejor rendimiento de fuzzing
* `--net=host` - para acceso fácil a un nodo Ethereum local
* `--tmpfs "/tmp/efcf/":exec,size=6g` - poner los archivos temporales de EF/CF en un ramdisk si es posible (menos desgaste del disco)
* `--privileged` - para ejecutar `afl-system-config` o `efcfuzz --configure-system`
* `-v` - para persistir los datos de salida de EF/CF
### VM / Bare-Metal
Para flujos de trabajo basados en VM o bare-metal:```sh
make system-install # install efcf to current system (requires root or sudo rights)
Ten en cuenta que muchos de los scripts funcionan con el diseño de directorios relativos de todos modos, por lo que esto principalmente instala dependencias y algunas herramientas que son útiles tener en tu PATH. Hemos probado ejecutar EF/CF en las siguientes distribuciones de Linux:
(La distribución no importa demasiado, probamos LLVM 13 y 14, siendo 14 la opción preferida. LLVM 11 o 12 también podrían seguir funcionando, pero como siempre - cuanto más nuevo, mejor. La parte importante es que haya un LLVM que sea compatible con nuestro fork de AFL++.)
No hemos probado EF/CF de forma nativa en Mac OS. Es probable que las cosas no funcionen (por ejemplo, afl-clang-lto en Mac OS parece no funcionar). La mejor opción es utilizar 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
Probamos con docker desktop v4.21.1 y el uso básico de EF/CF funciona. Sin embargo, ten en cuenta lo siguiente:
* Si ves segfaults al compilar: prueba a aumentar el límite de memoria de la VM que docker usa en Mac OS.
* Prueba a habilitar la aceleración usando rosetta en docker - con un poco de suerte será algo más rápido.
### Configuración de desarrollo
Las herramientas generalmente no necesitan instalarse. Instala las dependencias necesarias como en el script `system-install.sh` o como en los Dockerfiles.
Para mayor comodidad, tenemos algunos scripts para actualizar tu `PATH`:```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
Algunos de los scripts requieren una clave de API para obtener metadatos (por ejemplo, el ABI) del servicio Etherscan. Si tienes una clave de API, tienes que establecer la variable de entorno ETHERSCAN_API_KEY para pasarla a los scripts. Para un flujo de trabajo basado en docker, puedes lanzar el contenedor docker con la bandera --env o poner tu clave de API en el archivo .etherscan_api_key, lo que integrará la clave de API en el contenedor docker.
Por conveniencia, utilizamos un script contenedor que se encarga de todos los detalles por ti, al iniciar el fuzzer EF/CF: efcfuzz
Puedes establecer muchas opciones de línea de comandos para configurar el comportamiento del fuzzer con respecto al proceso de compilación y fuzzing. Echa un vistazo a efcfuzz --help para ver una lista de opciones.
Ejemplos
Compila el código fuente de solidity a código nativo EF/CF y comienza a hacer fuzzing durante 5 minutos (es decir, 300 segundos).```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
Alternativamente, inicie con salida de fuzzing reducida (`--quiet` suprime la
salida del fuzzer base, mientras que `--print-progress` imprimirá un breve resumen del
progreso del fuzzing), y ejecute el fuzzer en 4 núcleos.```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
Usa el bytecode ya compilado y compila el bytecode a código nativo EF/CF y comienza a fuzzear.```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
El wrapper puede exportar el estado de un contrato desde un nodo go-ethereum/erigon y comenzar
el fuzzing desde allí.```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
Además, puedes pasar --include-address-deps=y para buscar recursivamente
direcciones de otras cuentas en el almacenamiento del contrato exportado e incluir
también esas en la exportación de estado. Sin embargo, esto no incluye otros contratos
almacenados en tipos mapping de Solidity. Para exportar realmente todo el estado
de forma recursiva, pasa también la opción --include-mapping-deps=y.
Pero ten cuidado: esta búsqueda recursiva puede provocar tiempos de compilación largos y un
rendimiento de fuzzing deficiente. Especialmente, los contratos de uso frecuente pueden tener una gran
cantidad de estado interno y usar su estado exportado puede ralentizar el fuzzing. Comprueba
si el fuzzer puede lograr más de 1k execs/sec. Si no es así, es mejor que
intentes crear un estado artificial y más pequeño. Prueba a ejecutar un nodo local de go-ethereum
en modo --dev y despliega allí tus contratos. Luego exporta el estado en vivo
desde ese nodo.
El wrapper guarda en caché las compilaciones, por lo que una segunda ejecución de fuzzing debería arrancar mucho más rápido,
porque ya no se necesita el tiempo de compilación inicial. Si solo quieres
compilar y colocarlo en la caché, puedes pasar el argumento --build-only.
Ejemplo: Fuzzing con propiedades
EF/CF también admite fuzzing basado en propiedades usando la misma definición de propiedades que el fuzzer echidna. Las propiedades (o invariantes) se expresan como funciones de Solidity que actúan como oráculo de errores para el fuzzer. Por ejemplo, puedes añadir una función de Solidity:```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
Lo cual representa la propiedad de que total_balance siempre debe estar por debajo de
1000. EF/CF entonces reportará un bug si logra violar esta propiedad usando
alguna secuencia de transacciones, es decir, si el oráculo devuelve `false`.
Para indicarle a EF/CF que esto es una propiedad, necesitas especificar una lista de firmas de
funciones en un archivo, que EF/CF recogerá como una lista de propiedades
para comprobar durante el fuzzing.
La forma más sencilla es obtener las firmas relevantes usando la bandera `--hashes`
del compilador de Solidity, p. ej.,```
solc --hashes ./path/to/your.sol | grep test_property > property_list
Ahora puedes lanzar el fuzzer con:``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
También puedes añadir `--disable-detectors` para desactivar los oráculos de bugs integrados basados en ether.
Puedes probar el siguiente ejemplo para fuzzing basado en propiedades:```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
Ejemplo: Fuzzing para Eventos
EF/CF admite fuzzing para violaciones de aserciones que se expresan mediante
eventos. De hecho, también admitimos el uso de eventos personalizados arbitrarios como
oráculo de bugs. Por defecto, EF/CF identificará un bug si el contrato objetivo registró uno
de los siguientes eventos: AssertionFailed(), AssertionFailed(uint256),
AssertionFailed(string), y Panic(uint256).```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
También puede especificar temas/hashes de eventos personalizados adicionales a los que prestar atención en un
archivo con `--event-assertions-list ./path/to/eventslist.txt`. Al igual que con la
lista de propiedades anterior, puede obtener el formato usando `solc --hashes` y
copiando los hashes y nombres de eventos al archivo de lista de eventos.
Por defecto, EF/CF ignorará los eventos que no hayan sido emitidos por el contrato
objetivo. Si desea cambiar eso, use `--event-assertions-target-only=n`.
(Nota: puede usar `--assertions` para habilitar tanto la comprobación de eventos como de aserciones de solidity)
**Ejemplo: Fuzzing de aserciones de Solidity ^0.8**
Actualmente, no admitimos el fuzzing de aserciones arbitrarias en código solidity
para versiones de solidity anteriores a 0.8. Anteriormente, las aserciones de solidity simplemente
activaban un opcode `invalid`, lo que resultaba en un revert bastante brusco. La
versión 0.8 de Solidity cambió el comportamiento: en lugar de usar el opcode `invalid` para
revertir transacciones, ahora utilizan el mecanismo `revert` y señalan errores
de vuelta al llamador. Podemos utilizar este tipo de propagación de errores como un oráculo de bugs
en EF/CF. Actualmente, EF/CF admite la comprobación del tipo de error de Solidity
`Panic(uint256)`. [Más información sobre errores de
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: puedes usar --assertions para habilitar tanto la comprobación de aserciones
de eventos como de Solidity)
Especificaciones del sistema y configuración
Recomendamos asignar de 4 a 16 núcleos y aproximadamente 1 GB de memoria por núcleo.
Puedes utilizar la opción --configure-system para configurar tu sistema para fuzzing
de alta velocidad, o configurarlo tú mismo. En contenedores Docker también necesitas
configurar el host para obtener el mejor rendimiento. Si es un host no crítico, puedes
lanzar el contenedor como --privileged y usar
/usr/local/bin/afl-system-config para configurar el sistema para fuzzing de alta
velocidad (nota: esto esencialmente ejecuta el contenedor 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
## Ejecutar un Experimento de Fuzzing
Para ejecutar el experimento en el conjunto de datos `data/tests/` puedes usar el siguiente
comando para compilar los contratos y su arnés de fuzzing, y luego ejecutar el
fuzzer con diferentes configuraciones, múltiples repeticiones, etc. Como esto
llevaría bastante tiempo, podemos ejecutar esos experimentos en paralelo. Dividimos los
experimentos de fuzzing en un paso de compilación y un paso de fuzzing. Los pasos de compilación
compilarán todos los contratos inteligentes secuencialmente (aunque la compilación en sí usa múltiples
núcleos). Luego lanzamos 8 instancias del fuzzer en segundo plano, que tomarán
los artefactos de compilación del paso de compilación y comenzarán las ejecuciones de fuzzing. El Makefile
intentará automáticamente lanzar todo en el contenedor adecuado si
está disponible `docker` o `podman`.```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
Deshabilitamos seccomp y el sandboxing de red al lanzar los contenedores en segundo plano. Deshabilitar el sandboxing de seccomp mejora el rendimiento del fuzzing. Usar la red del host permite que EF/CF acceda a los nodos de Ethereum en la red local sin configuración adicional.
Usamos el script ./scripts/run-tools-on-dataset.py para ejecutar las otras herramientas dentro de contenedores Docker en estos conjuntos de datos, p. ej., con estos comandos para el conjunto de datos múltiple:```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
Debes adaptar el script para configurar las herramientas y el número de ejecuciones.
### Configuración de un experimento de fuzzing
Aquí, usamos el experimento `tests` como ejemplo. Simplemente reemplaza la cadena
`tests` con el nombre del experimento en los siguientes pasos:
1. Reúne tu conjunto de datos en `./data/`, p. ej., el conjunto de datos `./data/tests` con contratos
de prueba. Para contratos de solidity tenemos un `Makefile` genérico para compilar los
contratos: `sol.Makefile`. Puedes reutilizarlo si lo deseas, consulta
`./data/tests/Makefile` para ver un ejemplo.
2. Crea un script para generar los artefactos de compilación, incluyendo cualquier
paso de preprocesamiento/extracción necesario. Por ejemplo, para el conjunto de datos `tests` tenemos
el script `./scripts/build-tests.sh`. Los artefactos de compilación deben
almacenarse en `./builds/tests/${contract}.build.tar.xz`.
3. Crea un script para lanzar la campaña de fuzzing, p. ej., para el conjunto de datos `tests`
crea un script llamado `./scripts/fuzz-tests.sh`. Normalmente puedes usar la
función común de campaña de fuzzing de `./scripts/common.sh`. Echa un vistazo a
`fuzz-tests.sh` como plantilla.
4. Los resultados de `fuzz-tests.sh` se almacenarán en `./results/run-fuzz-tests/`.
5. Para resumir los resultados proporcionamos `./scripts/summarize.py` para los scripts
lanzadores basados en bash y `./scripts/summarize_l.py` para scripts lanzadores
de python (la herramienta `efcfuzz`).
Puede que necesites adaptar estos scripts dependiendo de tu paso 3.
### Experimentos de fuzzing existentes
#### Puntos de referencia (benchmarks)
* <a href="./data/multi/">`./data/multi`</a> contiene el benchmark de escalabilidad
que usamos para evaluar qué tan bien escala una herramienta de análisis a secuencias de transacciones
más largas. Consiste en tres tipos de contratos:
* `multi_gen_*.sol` - contratos sintetizados automáticamente, que hacen un montón
de `require(input <= MAGIC)` y luego establecen una variable de estado interna. Si
todas las variables de estado están establecidas, entonces se puede activar el `selfdestruct` (o el oráculo de echidna).
* `multi_man_complex_*.sol` - variantes creadas manualmente que funcionan de manera similar
a los contratos del tipo `multi_gen`, pero presentan restricciones un poco más complicadas
(p. ej., otras cosas además de igualdad y desigualdad con un valor mágico)
* `justlen_*.sol` - estos están tomados del [ejemplo echidna-parade](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - comprobaciones de sanidad que verifican que un fuzzer/herramienta
podría en teoría encontrar errores que requieren 9 o 10 transacciones. Aquí el
analizador solo necesita llamar a 10 funciones en el orden correcto sin ningún
argumento. Esto es bastante fácil para la mayoría de las herramientas de análisis.
* <a href="./data/throughput/">`./data/throughput`</a> contiene los contratos que
usamos para evaluar el rendimiento. Esta es una selección de contratos de
tamaño variable. Ten en cuenta que parcheamos todas las vulnerabilidades en estos contratos,
de modo que las vulnerabilidades encontradas no afecten las mediciones de rendimiento.
* <a href="./data/cov-max-testset">`./data/cov-max-testset`</a> contiene los
contratos que usamos para la comparación de fuzzers basada en la cobertura de código.
#### Detección de errores
* <a href="./data/ethbmc-vuln">`./data/ethbmc-vuln`</a> lista de contratos que
EthBMC detectó como vulnerables.
* <a href="./data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> lista de
contratos, donde EthBMC detuvo el análisis debido a un tiempo de espera agotado.
* <a href="./data/reentrancy">`./data/reentrancy`</a> un conjunto de contratos
vulnerables a ataques de reentrancia.
* <a href="./data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> un conjunto de contratos
que se ha verificado que contienen un error de reentrancia como parte del
[estudio sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth).
* <a href="./data/sailfish-dao">`./data/sailfish-dao`</a> lista de todos los contratos, donde
[sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs)
encontró un error de reentrancia.
* <a href="./data/sereum">`./data/sereum`</a> una lista de contratos
vulnerables a ataques de reentrancia según [Sereum](https://github.com/uni-due-syssec/sereum-results).
* <a href="./data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a>
contratos de los smartbugs seleccionados, clasificados como errores de "control de acceso"
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="./data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a>
contratos de los smartbugs seleccionados, clasificados como errores de "reentrancia"
([smartbugs github](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### Pruebas
Los siguientes conjuntos de datos contienen contratos de prueba sintéticos básicos para probar las capacidades del fuzzer:
* <a href="./data/tests">`./data/tests`</a> pruebas básicas recopiladas de múltiples
fuentes que verifican las capacidades básicas de un fuzzer. Todas usan un oráculo
de selfdestruct.
* <a href="./data/tests-not-vuln">`./data/tests-not-vuln`</a> igual que tests, pero no
deben detectarse como vulnerables.
* <a href="./data/properties-tests">`./data/properties-tests`</a> pruebas para
fuzzing basado en propiedades
* <a href="./data/assertions-tests">`./data/assertions-tests`</a> pruebas para
fuzzing de aserciones.
## Fuzzing en más detalle
Utilizamos scripts envoltorio para lanzar el fuzzer real (AFL++ en nuestro caso).
Esto se hace automáticamente cuando se usa el lanzador `efcfuzz`.```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
Si tienes tmux y tmuxp instalados, entonces para el desarrollo y la inspección la versión interactiva del script podría ser útil:```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
Esto compilará y hará fuzzing durante bastante tiempo. Luego puedes ejecutar
`cd ./fuzz/out/SimpleDAO*` para ver los resultados del fuzzing. Nuestros scripts envoltorio hacen
un trabajo extra además de lanzar el programa `afl-fuzz`, es decir, principalmente
posprocesan los resultados. Además, generan varios scripts de conveniencia
para analizar los casos de prueba generados.
* `./a.sh` - Imprime una forma legible de un caso de prueba, envoltorio de
`efuzzcaseanalyzer`.
* `./r.sh` - Ejecuta un caso de prueba con la misma configuración que cuando se ejecutó el fuzzer.
* `./m.sh` - Minimiza un caso de prueba con la misma configuración que cuando se ejecutó el
fuzzer.
* `./c.sh` - Analiza la "cadena" de casos de prueba que conducen al caso de
prueba dado. Útil para analizar/optimizar el fuzzer. Puedes ver rápidamente qué
caso de prueba fue producido por qué cadena de mutaciones sobre qué entradas de la cola.
Requiere `fzf`.
También hay otros informes de conveniencia, como
* `./bugs` y `./bugtypes` que resumen cualquier error que haya sido identificado.
* `./crashes_min`, que contiene crashes minimizados de todas las instancias de
`afl-fuzz`.
**Ver Cobertura de Código de Bloques Básicos de EVM**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
El script fuzz/evm-bb-coverage.sh calculará la cobertura de bloques básicos
dado un directorio de salida de AFL. El harness puede opcionalmente volcar una traza de bloques
básicos, que luego se compara con una lista de bloques básicos generada por
evm2cpp (es decir, los archivos .bb_list en eEVM/contracts/).
Por defecto también calculamos la cobertura que producen nuestras semillas genéricas predeterminadas (ver
eEVM/fuzz/generic_seeds):```bash
$ cat coverage-percent-seeds.evmcov
10.5890
La lista de bloques básicos cubiertos se almacena en el archivo `all.evmcov`.
**Ver Resumen de Casos de Prueba Generados**
`efuzzcaseanalyzer` puede utilizarse para ver/resumir los casos de prueba generados,
p. ej.,```
$ 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
Los resúmenes suelen almacenarse en los archivos crashes_tx_summary y
queue_tx_summary, pero este último puede ser un poco verboso.
Analizar un único caso de prueba que provoca un fallo``` $ ./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), }
Y para obtener el resultado real del objetivo de fuzzing, puedes ejecutar:```
$ ./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)
Esto le proporciona una gran cantidad de salida verbosa, incluidas algunas partes de los rastros de ejecución de los contratos y el resultado de la comprobación de saldo que realiza el harness.
Minimización de entradas que provocan fallos
Las entradas que provocan fallos a menudo contienen transacciones no relacionadas debido al enfoque de pruebas aleatorizadas. Esto se puede mitigar realizando una minimización de la entrada que provoca el fallo (es decir, reducir la entrada siempre que siga provocando un fallo). Si desea minimizar entradas que no provocan fallos, puede usar la bandera -M para habilitar la minimización según la cobertura como criterio de minimización.
El siguiente comando reducirá el caso de prueba y sobrescribirá el archivo:``` $ 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), }
## Lectura del formato de los casos de prueba
El formato de los casos de prueba está orientado al fuzzing y no es tan directo
de leer. Hay varias sutilezas que debes tener en cuenta.
* El formato de los casos de prueba se considera una "cola" de transacciones que
pueden ejecutarse. En cuanto se encuentra cualquier problema, el procesamiento
del caso de prueba se detiene. Esto incluye:
* Cuando una transacción revierte.
* Cualquier error encontrado por el código de instrumentación (harness).
* Cualquier bug que se active y se detecte.
En consecuencia, un caso de prueba impreso no se corresponde necesariamente
con lo que se ejecuta: puede haber transacciones al final que no se ejecutan.
¡Revisa la salida verbosa y usa el minimizador para eliminar esas!
* Del mismo modo, puede haber demasiados `returns` o marcadores `reenter`
espurios. Usa el minimizador de casos de prueba para eliminarlos.
* Un contrato solo se reingresa cuando hay otra transacción en la lista después
de la transacción que se supone que realiza el reingreso (es decir, hay otra
entrada siguiente en la cola).
* Incluso si el marcador `reenter` está establecido en algo, esto no significa
necesariamente que se reingrese al contrato, solo que el código del harness
intentará hacerlo si es posible. Por ejemplo, si el contrato no realiza una
llamada, el marcador `reenter` se ignora porque no hay posibilidad de
reingresar. Normalmente, el minimizador eliminará cualquier marcador
`reenter` espurio.
En general, muchos de estos problemas desaparecen cuando usas el minimizador de
casos de prueba, por lo que siempre es una buena idea usarlo antes de analizar
los casos de prueba generados.
## Falsos positivos conocidos
Hemos observado varios tipos de falsos positivos que parecen repetirse al hacer
fuzzing de contratos con EF/CF.
* Contratos que pagan Ether por diseño. El oráculo de bugs de ganancias de
Ether de EF/CF detectará estos contratos como vulnerables, aunque operan
según lo diseñado:
* Contratos de apuestas: muchos contratos de apuestas incluyen alguna forma
de aleatoriedad, lo que ya es una mala práctica en Ethereum. Sin embargo,
algunos contratos de apuestas están implementados de forma que te obligan
a adivinar, por ejemplo, los dos últimos dígitos del hash del siguiente
bloque o algo similar. Esto se puede habilitar mediante un esquema de
compromiso, es decir, la primera transacción compromete al usuario a un
cierto valor y la segunda activa la suposición y el pago si se gana.
Estos contratos normalmente no son explotables en una blockchain real.
Sin embargo, en la blockchain simulada de EF/CF, el fuzzer puede adaptar
el compromiso después de observar el valor en la segunda transacción.
Este hecho es importante para que EF/CF alcance una mejor cobertura de
código. Pero también facilita que EF/CF identifique una secuencia de TX
que permita al fuzzer ganar de forma determinista en el contrato de
apuestas.
* Contratos que pagan intereses: hay muchos contratos pequeños que te
permiten invertir Ether y luego pagan un cierto porcentaje de intereses
cada `N` bloques. El atacante simulado de EF/CF es capaz de esperar `N`
bloques y luego recibe el pago de intereses, lo que nuevamente es
detectado por el oráculo de bugs de ganancias de Ether.
* Airdrops: algunos contratos de tokens permiten airdrops, es decir,
reparten tokens a cualquiera que los solicite hasta que se alcanzan
ciertos límites. Por ejemplo, los airdrops suelen estar habilitados solo
durante un breve período de tiempo. Si un contrato de este tipo se
despliega dentro de EF/CF, es muy probable que el límite de tiempo esté
configurado de modo que los airdrops sigan habilitados. EF/CF entonces
detecta una ganancia de Ether si los tokens del airdrop pueden venderse
nuevamente.
* Reporte temprano de `DELEGATECALL` controlable: actualmente reportamos un
delegatecall controlable en cuanto se invoca. Sin embargo, hay múltiples
contratos que incluyen funciones que permiten intencionalmente al llamador
realizar un delegatecall a una dirección arbitraria. No obstante, estas
funciones revierten incondicionalmente la transacción inmediatamente después
del delegatecall. Esto evita que cualquier cambio de estado o transferencia
de Ether persista. Normalmente, estas funciones tienen palabras como
"simulate" en sus nombres y son fáciles de detectar.
* Esto podría corregirse en EF/CF retrasando el reporte hasta el final de
la ejecución. Sin embargo, esto complica bastante el oráculo de bugs.
* Actualmente no hay planes para corregirlo.
* Initializer invocable: hemos observado que al hacer fuzzing de contratos
exportados desde la blockchain, EF/CF a veces puede llamar a las funciones
inicializadoras, aunque el contrato ya haya sido inicializado.
Normalmente, esto debería provocar una reversión, pero no lo hace en el
entorno EVM de EF/CF. Llamar al inicializador nuevamente a menudo conduce a
ganancias triviales de Ether, porque, por ejemplo, el inicializador establece
una variable de *owner* o algo similar.
* Aún no estamos seguros de cuál es la causa raíz de este problema. Sin
embargo, normalmente es fácil de detectar, ya que la función
inicializadora suele llamarse `initializer`, `init` o algo similar.
## Errores comunes
Hicimos nuestro mejor esfuerzo para que esto sea algo usable, pero sigue siendo
un prototipo de investigación. Espera que algunas cosas fallen. Estos son
algunos problemas comunes que hemos observado:
* *P: Recibo un error de compilación extraño debido a una macro `TOKENPASTE`.*
R: Esto sucede a menudo cuando `efcfuzz` adivina el nombre incorrecto del
contrato (es decir, adivina un contrato abstracto); intenta pasar
`--name YourContract` para especificar el contrato objetivo.