Guía para la auditoría automática de contratos inteligentes. Parte 1: preparación para una auditoría

Introduccion


Nuestra empresa se dedica a la auditoría de seguridad de contratos inteligentes, y el problema del uso de herramientas automatizadas es muy grave. ¿Cuánto pueden ayudar a identificar lugares sospechosos, cuáles deberían usarse, qué pueden hacer y cuáles son los detalles del trabajo en esta área? Estas y otras cuestiones relacionadas son el tema de este artículo. Y el material será intentos de trabajar con contratos reales con la ayuda de los representantes y recetas más interesantes para lanzar este software extremadamente variopinto y muy interesante. Al principio quería hacer un artículo, pero después de un tiempo la cantidad de información se hizo demasiado grande, por lo que se decidió hacer una serie de artículos, uno para cada autoanalizador. La lista de la que tomaremos las herramientas se presenta, por ejemplo, aquí , pero si aparecen otras herramientas interesantes durante la escritura, las describiré con gusto y las probaré.


Debo decir que las tareas de auditoría fueron extremadamente interesantes, porque Hasta ahora, los desarrolladores no han prestado mucha atención a los aspectos económicos de los algoritmos y la optimización interna. Y la auditoría de contratos inteligentes ha agregado varios vectores de ataque interesantes que deben tenerse en cuenta al buscar errores. Además, resultó que aparecieron muchas herramientas para pruebas automáticas: analizadores estáticos, analizadores de bytecode, fuzzers, analizadores y muchos otros buenos programas.


El propósito del artículo: promover la distribución de un código de contrato seguro y permitir a los desarrolladores deshacerse rápida y fácilmente de errores estúpidos, que a menudo son los más molestos. Cuando el protocolo en sí es completamente confiable y resuelve un problema grave, la presencia de un error estúpido olvidado en la etapa de prueba puede arruinar seriamente la vida del proyecto. Por lo tanto, aprendamos a utilizar, como mínimo, herramientas que permitan que "poca sangre" elimine problemas conocidos.


Mirando hacia el futuro, debo decir que los errores críticos más comunes que encontramos en las auditorías siguen siendo problemas de implementación lógica, y no vulnerabilidades típicas como derechos de acceso, desbordamiento de enteros, reentrada. Una auditoría completa y completa de las soluciones es imposible sin desarrolladores experimentados que sean capaces de auditar la lógica de alto nivel de los contratos, su ciclo de vida, los aspectos de la operación real y el cumplimiento de la tarea, y no solo los patrones de ataque típicos. Es la lógica de alto nivel que a menudo se convierte en una fuente de errores críticos.


Pero las advertencias, los agujeros típicos y los errores que se dejan por descuido que no deben perderse son el destino de los analizadores automáticos, deben ser capaces de hacer frente a estas tareas mejor que las personas. Es esta tesis la que se pondrá a prueba.


Características de la auditoría de código de contrato inteligente


La auditoría de código de contrato inteligente es un área bastante específica. A pesar de su pequeño tamaño, el contrato inteligente Ethereum es un programa completo que puede organizar ramas complejas, bucles, árboles de decisión e incluso automatizar transacciones aparentemente simples que requieren pensar en todas las ramas posibles en cada paso. Desde este punto de vista, el desarrollo de blockchain es extremadamente bajo, exige muchos recursos y recuerda mucho el desarrollo de sistemas y software embebido en C / C ++ y lenguajes ensambladores. Es por eso que nos encanta ver en las entrevistas a los desarrolladores de algoritmos de bajo nivel, la pila de red, los servicios altamente cargados, todos los que se ocuparon de la optimización de bajo nivel y la auditoría de código.


Desde el punto de vista del desarrollador, Solidity también es bastante específico, aunque es fácil de leer por casi cualquier programador y en los primeros pasos y parece extremadamente simple. El código de solidez es bastante fácil de leer, es familiar para cualquier desarrollador que conozca la sintaxis C / C ++ y la POO, como JavaScript.


Aquí, la simplicidad del código es la clave para la supervivencia, nada pesado funciona, por lo que todo el arsenal de desarrollo de bajo nivel se utiliza en el trabajo: algoritmos que permiten el uso eficiente de los recursos, ahorran memoria: árboles Merkle, filtros Bloom, carga de recursos "perezosa", desenrollado de bucles, recolección manual de basura y mucho mas
Una pequeña cantidad de código fuente y el código de bytes resultante.


Un contrato inteligente por separado está limitado en el volumen del código de bytes, cada byte cuesta una cierta cantidad de gas y el máximo está limitado desde arriba, por lo que puede empujar unos 10Kb en la cadena de bloques (en este momento), ya no funcionará. Aquí hay un buen artículo sobre cuánto cuesta un contrato de despliegue y cuánto cuesta el gas . Por lo tanto, no se puede empujar mucho. Si exagera, varios miles de líneas de código "promedio" es el máximo. Varias docenas de métodos, la falta de agregación y la lógica generalmente compleja son extremadamente características de los contratos. Todo lo que no encaja requiere que seleccione el código en bibliotecas separadas, para cambiar y complicar el procedimiento de carga en la red. Los desarrolladores de Solidity pueden estar felices de insertar un montón de código en un contrato, pero simplemente tienen que organizar sus sistemas de contrato correctamente creando bibliotecas de clases separadas con su propio almacenamiento. Y es conveniente descomponer esas "clases" separadas en archivos separados, y por lo tanto, leer el código de los contratos es bastante bueno, todo está bien estructurado desde el principio; de lo contrario, no funcionará. Como ejemplo, recomiendo ver cómo ERC721 se hace en solidez openzeppelin .


Gas, gas, gas


Gas introduce una capa adicional de lógica en la ejecución del código del contrato, que requiere una auditoría. Además, a diferencia del código tradicional, la misma sección de código puede gastar diferentes cantidades de gas. Una tabla de códigos de operación EVM y su costo es útil para comprender las restricciones de gas.


Para demostrar por qué tiene que dedicar tanto tiempo a evaluar el gas, considere este pseudocódigo (por supuesto, poco realista; disparar en el bucle con éter es una mala idea):


//          function fixSomeAccountAction(uint _actionId) public onlyValidator { // … events[msg.sender].push(_actionId); } //   ,           function receivePaymentForSavedActions() { // ... for (uint256 i = 0; i < events[msg.sender].length; i++) { //  actionId   uint actionId = events[msg.sender][i]; //      action uint payment = getPriceByEventId(actionId); if (payment > 0) { paymentAccumulators[msg.sender] += payment; } emit LogEventPaymentForAction(msg.sender, actionId, payment); // … // delete “events[msg.sender][i]” from array } } 

El hecho es que el ciclo en el contrato se ejecuta eventos [msg.sender] .length veces, y cada iteración es una entrada en la cadena de bloques (transfer () y emit ()). Si la longitud de la matriz es pequeña, el ciclo cumple sus diez veces, distribuyendo el pago por cada acción. Pero, si la matriz de eventos [msg.sender] es grande, habrá muchas iteraciones y el gas gastado llegará al límite máximo de gas codificado (~ 8,000,000). La transacción caerá y ahora nunca funcionará, ya que no hay forma de reducir la longitud de la matriz de eventos [msg.sender] en el contrato. Si el ciclo no solo calcula un valor unitario, sino que escribe en la cadena de bloques (por ejemplo, se pagan algunas comisiones, pagos por acciones), entonces el número permitido de iteraciones es bastante limitado. Juzgue usted mismo: límite: 8,000,000, registrando un nuevo valor de 256 bits: 20,000. puede guardar o actualizar metadatos solo para un par de cientos de direcciones de 256 bits con algunos metadatos. Otra parte divertida es escribir un nuevo valor: 20,000 y una actualización de uno existente: 5,000, por lo que incluso con el mismo entorno de su contrato cuando realiza una transferencia tokens a una dirección que ya tiene tokens, gastas 4 veces menos gasolina (5,000 vs 20,000) en un registro.


Por lo tanto, no se sorprenda de que el tema del gas en los contratos inteligentes esté tan estrechamente relacionado con la seguridad de los contratos, porque la situación en la que los fondos están permanentemente atascados en el contrato desde un punto de vista práctico difiere poco de la situación cuando fueron robados. El hecho de que la instrucción ADD cuesta 3 gases y SSTORE (ahorro en almacenamiento): 20 000 significa que el recurso más costoso en blockchain es el almacenamiento, y las tareas de optimización del código de contratos se superponen en gran medida con las tareas de desarrollo de bajo nivel en C y ASM para embebido sistemas, donde el almacenamiento también es un recurso muy limitado.


Hermosa blockchain


Este es un párrafo muy positivo sobre por qué la cadena de bloques es tan buena desde un punto de vista de seguridad solo para el auditor. El determinismo de la ejecución del código del contrato es la clave para la depuración y reproducción exitosas de errores y vulnerabilidades. Técnicamente, cualquier llamada a un código de contrato puede reproducirse en cualquier plataforma con un poco de precisión, esto permite que las pruebas funcionen en todas partes y sean extremadamente fáciles de soportar, y la investigación de incidentes es confiable e innegable. Ahora siempre sabemos quién se llamó a qué función, con qué parámetros, qué código la procesó y cuál fue el resultado. Todo esto está completamente determinado, es decir juega en cualquier lugar, incluso en JS en una página web. Si hablamos de Ethereum, cualquier caso de prueba es extremadamente fácil de escribir en JavaScript conveniente, incluidos los parámetros difusos, y funciona muy bien donde haya Node.js.


Sin embargo, todas estas hermosas palabras no deberían relajar a los desarrolladores, porque, como se mencionó anteriormente, los errores más graves son lógicos, y para ellos el determinismo de ejecución es una propiedad ortogonal.


El entorno para el montaje del contrato.


Para escribir el artículo, tomé un antiguo contrato experimental para reservar una casa del diseñador de Smartz: https://github.com/smartzplatform/constructor-eth-booking . El contrato le permite crear un registro del objeto (apartamento o habitación de hotel), establecer el precio y las fechas de entrega, después de lo cual el contrato espera el pago y, si se recibe, fija el acto de reserva, mantiene los fondos en el saldo hasta que el huésped ingrese a la habitación y no confirmará la entrada. En este punto, el propietario de la habitación recibe el pago. El contrato es esencialmente una máquina de estado, cuyos estados y transiciones se pueden ver en Booking.sol. Lo hicimos bastante rápido, lo cambiamos durante el proceso de desarrollo y no logramos hacer una gran cantidad de pruebas, está lejos de ser una nueva versión del compilador y tiene una lógica interna más o menos rica. Entonces, veamos cómo los analizadores lo manejan, qué errores encontrarán y, si es necesario, agregamos el nuestro.


Trabaja con diferentes versiones de solc


Se deberán usar diferentes analizadores de diferentes maneras: algunos se lanzan desde la ventana acoplable, otros usan un código de bytes compilado listo para usar, y el auditor mismo no tiene que lidiar con un par, sino con docenas de contratos iniciales con diferentes versiones del compilador. Por lo tanto, debe poder "aplicar" diferentes versiones de solc de solc diferente, tanto en el sistema host como dentro de la imagen del solc y dentro de la trufa, así que le daré estas pocas opciones de pirateo sucio:


1 vía: trufa interior


Para esto, no se necesitan trucos, porque comenzando con la versión 5.0.0 de truffle, puede especificar la versión del compilador directamente en truffle.js, como en este diff .


Ahora truffle descargará el compilador requerido y lo ejecutará. Muchas gracias al equipo por esto, Solidity es un idioma joven, hay cambios serios en el idioma, y ​​pasar de una versión a otra para el auditor es inaceptable, de esta manera puede introducir nuevos errores y enmascarar los viejos.


Método 2: reemplazar / usr / bin / solc en el contenedor acoplable del analizador
Si el analizador se distribuye en forma de Dockerfile, puede reemplazarlo al ensamblar una imagen de Docker agregando una línea al Dockerfile que obtenga la versión deseada solc directamente de la imagen, que la extrae de la red y reemplaza / usr / bin / solc:


 COPY --from=ethereum/solc:0.4.19 /usr/bin/solc /usr/bin 

3 vías: reemplazando / usr / bin / solc


La forma más sucia en la frente, si no hay salida, puede reemplazar vilmente el binario / usr / bin / solc con un script como este (no olvide guardar el archivo original):


 #!/bin/bash # run Solidity compiler of given version, pass all parameters # you can run “SOLC_DOCKER_VERSION=0.4.20 solc --version” SOLC_DOCKER_VERSION="${SOLC_DOCKER_VERSION:-0.4.24}" docker run \ --entrypoint "" \ --tmpfs /tmp \ -v $(pwd):/project \ -v $(pwd)/node_modules:/project/node_modules \ -w /project \ ethereum/solc:$SOLC_DOCKER_VERSION \ /usr/bin/solc \ "$@" 

Descarga y almacena en caché la imagen del solc con la versión correcta de solc , va al directorio actual y ejecuta /usr/bin/solc con los parámetros pasados. No es una muy buena manera, pero tal vez para algunas tareas le convenga.


Código de aplanamiento


Ahora descubramos la fuente. Por supuesto, en teoría, los autoanalizadores (especialmente para el análisis de fuente estática) deberían recopilar un contrato, extraer todas las dependencias, combinar todo en un monolito y analizarlo. Pero, como ya dije, los cambios de una versión a otra pueden ser serios, y constantemente me topé con la necesidad de colocar un directorio adicional en la ventana acoplable, configurarlo dentro de la ruta, y todo esto para que obtenga correctamente las importaciones necesarias. Algunos analizadores entienden todo, la segunda no es, por lo tanto, una opción universal, para no sufrir el lanzamiento de directorios adicionales, es más conveniente para los analizadores que comen un solo archivo fusionar todo en un solo archivo y analizarlo solo.


Para hacer esto, use el aplanador de trufas regular .


Este es un módulo npm estándar, se usa de manera muy simple:


 truffle-flattener contracts/Booking.sol > contracts/flattened.sol 

: https://github.com/trailofbits/slither
Si necesita personalizar de alguna manera el aplanamiento, puede escribir su propio acoplador, por ejemplo, antes de usar la opción basada en python: https://github.com/mixbytes/solidity-flattener


Comencemos el análisis.


En el ejemplo del mismo anciano https://github.com/smartzplatform/constructor-eth-booking continuamos el análisis. El contrato indica la versión anterior del compilador "0.4.20", e intencionalmente tomé el contrato anterior para resolver problemas con el compilador. La situación empeora por el hecho de que un autoanalizador, por ejemplo, estudiando el código de bytes, puede depender de esta versión de solc, y aquí las diferencias en las versiones pueden afectar en gran medida los resultados o incluso romper todo. por lo tanto, incluso si está haciendo todo lo kosher usando las últimas versiones, aún puede ejecutar un analizador que se haya ajustado a la versión anterior del compilador.
Compilar y ejecutar pruebas


Para comenzar, simplemente extraiga el proyecto del github e intente compilar.


 git clone https://github.com/smartzplatform/constructor-eth-booking.git cd constructor-eth-booking npm install truffle compile 

Seguramente tienes problemas con la versión del compilador. Además, los autoanalizadores también tienen estos problemas, así que use cualquier medio para obtener el compilador 0.4.20 y compilar el proyecto. Acabo de registrar la versión necesaria del compilador en truffle.js y todo se ensambló como se describió anteriormente.


También corre


 truffle-flattener contracts/Booking.sol > contracts/flattened.sol 

como se indica en el párrafo sobre el aplanamiento, es contracts/flattened.sol daremos para su análisis a diferentes analizadores
Conclusión de la introducción.


Ahora, habiendo aplanado.sol y la capacidad de usar solc una versión arbitraria, puede comenzar a analizar. Omitiré los problemas con la ejecución de trufas y pruebas, hay mucha documentación sobre este tema, resuélvelo tú mismo. Por supuesto, las pruebas deben ejecutarse y ejecutarse con éxito. Además, para verificar la lógica, el auditor a menudo tiene que agregar sus propias pruebas, verificando lugares potencialmente con fugas, por ejemplo, verificando la funcionalidad del contrato en los límites de los arreglos, cubriendo con pruebas todas las variables, incluso aquellas destinadas estrictamente al almacenamiento de datos, etc. Hay muchas recomendaciones, además de que este es solo el producto que nuestra empresa suministra al mercado, por lo que el estudio de la lógica es una tarea puramente humana.


Iremos a los analizadores que son interesantes desde nuestro punto de vista, trataremos de introducir nuestro contrato en ellos y les presentaremos artificialmente vulnerabilidades para evaluar cómo reaccionarán los analizadores automáticos. El siguiente artículo estará dedicado al analizador Slither y, en general, el plan de acción es aproximadamente el siguiente:


Parte 1. Introducción. Compilación, aplanamiento, versiones de Solidity (este artículo)
Parte 2. Slither
Parte 3. Mythril
Parte 4. Manticora
Parte 5. Equidna
Parte 6. Herramienta desconocida 1
Parte 7. Herramienta desconocida 2


Este conjunto de analizadores se obtuvo porque es importante que el auditor pueda utilizar diferentes tipos de análisis, estáticos y dinámicos, y requieren enfoques completamente diferentes. Nuestra tarea es aprender a usar las herramientas básicas en cada tipo de análisis y entender cuál usar cuando.


Quizás en el proceso de un estudio detallado, nuevos candidatos aparecerán para su consideración, o el orden de los artículos cambiará, así que estad atentos. Para ir a la siguiente parte, "haga clic aquí"

Source: https://habr.com/ru/post/438336/


All Articles