WEB3DEV Español

Cover image for Introducción a la Auditoría de Seguridad de Contratos Inteligentes: DoS
Delia Viloria T
Delia Viloria T

Posted on

Introducción a la Auditoría de Seguridad de Contratos Inteligentes: DoS

Contexto

La denegación de servicio (DoS) es un problema en la seguridad de los contratos inteligentes que es similar a la seguridad de red tradicional.

Conocimiento previo

Denegación de servicio (DoS) de seguridad de red tradicional: DoS es una abreviatura de Denial of Service, es decir, denegación de servicio. Una denegación de servicio sucede cuando una interferencia en un servicio reduce o elimina su disponibilidad. A continuación, se presentan ejemplos de ataques comunes de denegación de servicio contra protocolos de red: Ataque SYN Flood (Inundación), IP Spoofing, UDP Flood, Ping Flood, Ataque Teardrop, Ataque LAND, Ataque Smurf, Ataque Fraggle y así sucesivamente.

  • Ataque de denegación de servicio de contrato inteligente: es un problema de seguridad que puede provocar errores de lógica de código, problemas de compatibilidad o una profundidad de llamada excesiva (una característica de las máquinas virtuales de blockchain), lo que hace que los contratos inteligentes no funcionen correctamente. Los métodos de ataque de denegación de servicio de contrato inteligente son relativamente simples, incluyendo, pero no limitándose, a los tres que se presentan a continuación:

  • Ataque de denegación de servicio basado en la lógica del código: este tipo de ataque de denegación de servicio es normalmente causado por la imprecisión de la lógica del código del contrato. El ejemplo más común sucede cuando hay una lógica en el contrato que recorre el mapeo o array de entrada. Cuando no hay límite de longitud en el mapeo o array de entrada, un atacante puede consumir una gran cantidad de Gas al recorrer un mapeo o array súper largo para el recorrido del loop, causando el desborde de Gas de la transacción y eventualmente, hace que el contrato inteligente sea inoperable.

  • Ataque de denegación de servicio basado en llamada externa: este ataque de denegación de servicio se produce debido al manejo inadecuado de las llamadas externas del contrato. Por ejemplo, en un contrato inteligente, hay un nodo que cambia el estado del contrato en función de la ejecución de una función externa, pero no se ocupa del hecho de que la transacción falló. Con esta característica, un atacante puede causar una falla intencional en una transacción y el contrato inteligente seguirá intentando procesar los mismos datos inválidos. Dado que la tarjeta lógica del contrato inteligente no se puede utilizar más en este entorno, el contrato inteligente se vuelve temporal o permanentemente ineficaz.

  • Ataque de denegación de servicio basado en gestión de operaciones: en los contratos inteligentes, por ejemplo, la cuenta del propietario (Owner) suele actuar como administrador, con amplios privilegios. Cuando el rol del propietario falla o se pierde la clave privada, la función de transferencia, por ejemplo, es vulnerable a un ataque de denegación de servicio no subjetivo, que podría resultar, por ejemplo, en la activación o suspensión de la función de transferencia.

Ejemplo de Vulnerabilidad

En mi opinión, gracias al conocimiento común, todos están familiarizados con el concepto de ataque de denegación de servicio. El ataque de denegación de servicio de llamada externa es el tipo más común de los tres tipos de ataques DoS. Para proporcionar una introducción completa, te guiaremos a través de un ejemplo de código típico a continuación:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract KingOfEther {
   address public king;
   uint public balance;

   function claimThrone() external payable {
       require(msg.value > balance, "Precisa pagar mais para se tornar o rei");

       (bool sent, ) = king.call{value: balance}("");
       require(sent, "Falha ao enviar Ether");

       balance = msg.value;
       king = msg.sender;
   }
}
Enter fullscreen mode Exit fullscreen mode

Análisis de Vulnerabilidad

Podemos ver en el contrato anterior que el objetivo es seleccionar el “Rey del Ether” (King of Ether). El contrato ClaimThrone() permite a los usuarios competir por el título de “Rey del Ether” introduciendo cualquier cantidad de Ether mayor que el usuario anterior. Si el valor de la moneda es mayor que del ETH del jugador anterior, el ETH permanecerá en el contrato y el nuevo jugador será coronado “Rey del Ether”, mientras que el ETH del antiguo jugador se le devolverá de la misma forma.

Podemos ver que la lógica de generar el nuevo rey y devolver el antiguo rey se completa en la misma función, y el valor de retorno del reembolso también se comprueba en ClaimThrone(). Vamos a combinar estos recursos para completar el ataque.

Contrato de ataque

Nota: Los siguientes escenarios de ataque y la lógica del código de contrato son solo ejemplos y únicamente tienen fines de demostración.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Attack {
   KingOfEther kingOfEther;

   constructor(KingOfEther _kingOfEther) {
       kingOfEther = KingOfEther(_kingOfEther);
   }

   function attack() public payable {
       kingOfEther.claimThrone{value: msg.value}();
   }
}
Enter fullscreen mode Exit fullscreen mode

Comencemos analizando primero el proceso de ataque:

  1. Alicia despliega el contrato KingOfEther.
  2. Alicia invoca a KingOfEther.claimThrone() para enviar 1 ether al contrato KingOfEther para convertirse en el “Rey del Ether”.
  3. Bob invoca a KingOfEther.claimThrone() para enviar 2 ethers al contrato KingOfEther para convertirse en el nuevo rey.
  4. Alice recibe un reembolso de 1 ETH.
  5. Eve utiliza la dirección de KingOfEther para implementar el contrato de ataque
  6. Eve invoca a Attack.attack() para enviar 3 ether al contrato KingOfEther.
  7. El contrato de ataque se convierte en el nuevo rey.
  8. Bob no está satisfecho con el resultado, por lo que vuelve a invocar a KingOfEther.claimThrone(), esta vez enviando 20 ethers al contrato KingOfEther para demostrar su “competencia financiera”.
  9. Bob descubre que sus transacciones fueron revertidas y ya no es el nuevo rey. Hasta ahora, el ataque de Eve ha invalidado permanentemente el contrato KingOfEther y el contrato de ataque se ha convertido en el eterno “Rey del Ether”.

El rico y atractivo Bob encuentra esta situación frustrante; si es tan rico, ¿por qué no puede ser rey?.

Veamos por qué.

Cuando Bob invoca a KingOfEther.claimThrone() para enviar 20 ethers al contrato KingOfEther, se activará la lógica de reembolso de KingOfEther.claimThrone() y los 3 ethers de Eve se devolverán al contrato de ataque. Repasemos el contrato de Ataque una vez más. Este contrato no implementa el método fallback() de pago (method of payable),, por lo que no puede recibir ether. Como resultado, la lógica de reembolso de KingOfEther.claimThrone() siempre fallará y su valor de retorno siempre será falso. Las comprobaciones sent, "Falha ao enviar Ether" se invierten constantemente. Siempre que se active un reembolso, después que el contrato KingOfEther transmita el contrato de ataque, nadie puede convertirse en el nuevo rey. Por lo tanto, Eve ejecutó con éxito un ataque de denegación de servicio.

Sugerencias para la reparación

Como desarrollador:

  1. Se debe prestar atención en el desarrollo de contratos inteligentes para hacer frente a fallas constantes, como el procesamiento asíncrono de la lógica de llamadas externas potencialmente fallidas.
  2. Cuando usesCall para realizar llamadas externas, loops y rutas, vigila el consumo de gas.
  3. Evita el exceso de autorización de una única función. Se debe lograr una división razonable de permisos cuando se manejan permisos de contrato, y la administración de carteras multifirmas para roles con permisos para evitar la pérdida de permisos debido a la fuga de claves privadas.

A continuación se muestra un ejemplo de corrección para el contrato vulnerable mencionado anteriormente:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract KingOfEther {    
    address public king;    
    uint public KingValue;    
    mapping(address => uint) public balances;

    function claimThrone() external payable {        
        balances[msg.sender] += msg.value;
        require(balances[msg.sender] > balance, "Need to pay more to become the king");                

        KingValue = balances[msg.sender];        
        king = msg.sender;    }
    function withdraw() public {        
        require(msg.sender != king, "Current king cannot withdraw");
        uint amount = balances[msg.sender];        
        balances[msg.sender] = 0;
        (bool sent, ) = msg.sender.call{value: amount}("");        
        require(sent, "Failed to send Ether");    
    }
}
Enter fullscreen mode Exit fullscreen mode

El mapa de saldos se ha agregado al contrato de reparación, el cual registra la cantidad total de ether que cada persona ha invertido en el contrato. En comparación con el contrato anterior, el jugador ahora puede agregar ether para recuperar el trono después de perderlo. El punto clave de la versión reparada es que existe un método que maneja la lógica de reembolso de forma asíncrona. Para recibir un reembolso, los jugadores deben invocar manualmente a withdraw(). Incluso si un jugador malintencionado se niega a aceptar Ether, esto no tiene efecto y no provocará la denegación del servicio mencionada anteriormente.

Como auditor:

Análisis de los contratos internos:

  1. Determina si el contrato contiene errores lógicos que afecten su usabilidad.
  2. Presta mucha atención a la posibilidad de un DoS resultante de la profundidad excesiva de llamadas de máquina virtual (1024 es la profundidad máxima).
  3. Concéntrate en saber si la lógica del código consume una gran cantidad de gas.

Análisis de los contratos externos

  1. Presta mucha atención a los problemas de compatibilidad al interactuar con contratos externos, como: la compatibilidad del valor de retorno del TRC20-USDT que no se procesa, lo que resulta en el bloqueo de los tokens.
  2. Verifica si el valor de retorno de la llamada al contrato externo corresponde al resultado esperado.

Análisis de la gestión de derechos

La visibilidad de todos los métodos de funciones y los derechos de acceso deben ser examinados y confirmados durante la auditoría. Es necesario combinar los documentos de proyecto proporcionados por el equipo del proyecto para confirmar que los derechos están de acuerdo con las descripciones de los documentos del proyecto durante la auditoría. Si se determina que existen autorizaciones excesivas o una división de autoridad poco clara, es esencial comunicarse con el equipo del proyecto para mejorar sus procesos y métodos y asegurar que se eviten errores administrativos y operativos durante la vigencia del contrato.

Artículo original publicado por SlowMist. Traducido por Delia Viloria T.

Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas. 
Si prefieres puedes escribirnos a @web3dev_eshttps://twitter.com/web3dev_es en Twitter.

Discussion (0)