Ataques a smart contracts y cómo defenderse (III): Denegación de servicio (DoS)

En seguridad informática, un ataque de denegación de servicio, también llamado ataque DoS (por sus siglas en inglés, Denial of Service), es un ataque a un sistema de computadoras o red que causa que un servicio o recurso sea inaccesible a los usuarios legítimos.

En sistemas basados en blockchain los ataques tradicionales DoS no son tan efectivos por el hecho de tratarse de un sistema distribuido y descentralizado de forma que no existe un punto único de fallo y aunque se consiga inhabilitar uno de los nodos de la red el resto podría seguir funcionando sin problemas. Los ataques de DoS a redes como Ethereum se pueden producir principalmente por dos vías:

  • Inundar de la red con transacciones que consuman los recursos computaciones de este “ordenador universal”. Es por este motivo por el que Vitalik introdujo el concepto de gas. El atacante deberá pagar por cada transacción que realice, desincentivando este tipo de ataque.
  • Hacer inaccesible los recursos en un smart contract en concreto aprovechando errores de diseño e implementación en dicho smart contract.

En este artículo nos vamos a centrar en la segunda vía con dos posibles ataques de DoS a smart contracts, denegación de servicio por revert “inesperado” o por limite de gas por bloque.

dos.png

DoS por revert “inesperado”

Veamos el siguiente código de un smart contract para una subasta:

// ATENCIÓN: CÓDIGO INSEGURO
contract Auction {
    address currentWinner;
    uint highestBid;

    function bid() payable {
        require(msg.value > highestBid);

        require(currentWinner.send(highestBid)); // Reembolso al antiguo ganador. Revertir si el pago falla

        currentWinner = msg.sender;
        highestBid = msg.value;
    }
}

Cuando un usuario hace una oferta mayor que la máxima oferta hasta el momento, el programa intenta reembolsar al propietario de la anterior máxima oferta, revirtiendo la transacción si el reembolso falla. Esto significa que un licitador malicioso puede convertirse en el actual ganador de la subasta y asegurarse de que cualquier reembolso posterior a su dirección siempre falle, por ejemplo, forzando un error en la función fallback que reciba el pago. De esta forma, forzará que cualquier futura llamada a la función bid (ofertar en español) con una oferta mayor a la suya falle y se aseguraría ganar la subasta.

Una recomendación general es siempre implementar el patrón withdraw de forma que sea el usuario el que haga personalmente el reembolso.

Otro ejemplo es cuando un contrato debe iterar a través de array para pagar a los usuarios (por ejemplo, el pago de intereses a unos inversores):

// ATENCIÓN: CÓDIGO INSEGURO
address[] private refundAddresses;
mapping (address => uint) public refunds;

function refundAll() public {
    for(uint x; x < refundAddresses.length; x++) { // iteration
        require(refundAddresses[x].send(refunds[refundAddresses[x]]))
}

Es común querer asegurarse de que cada pago se realice con éxito pero hay que tener cuidado porque si revertimos la operación completa cunado un pago falla estaremos evitando que el resto de usuarios cobren. Un actor malicioso podría forzar el fallo del pago a su dirección evitando el resto de pagos.

DoS por limite de gas por bloque

En Ethereum, el límite de gas por bloque (o Block Gas Limit) se cambia en cada bloque. El nuevo limite de gas se decide mediante un algoritmo y votación de los mineros. Si se intenta enviar una transacción con un límite de gas que exceda el límite de gas del bloque, se produce un error “Exceeds block gas limit”.

Teniendo en cuenta la anterior, el ejemplo anterior muestra otra posible vulnerabilidad debido a este límite. Pagando a todos los usuarios iterando en un array de tamaño desconocido, se corre el riesgo de toparse con el límite de gas por bloque. Esto puede ocasionar problemas incluso en ausencia de un ataque intencional. Sin embargo, es especialmente peligroso si un atacante puede manipular la cantidad de gas necesaria. En el caso del ejemplo anterior, el atacante podría añadir una lista de direcciones al array de usuarios, en la que cada una de estas direcciones podría necesitar un reembolso muy pequeño. El coste en gas necesario para reembolsar a cada una de las direcciones del atacante podría, por lo tanto, terminar siendo mayor que el límite de gas del bloque, lo que impediría que la transacción de reembolso se realice. De nuevo, esta es una buena razón para usar el patrón withdraw (pull en vez de push)

Si fuera absolutamente necesario recorrer una array de tamaño desconocido, podría por ejemplo controlar el gas requerido por la transacción y dividirla en múltiples transacciones si se excede un limite. A continuación se muestra un ejemplo en el que se establece un limite de gas en la transacción de 200.000 weis y se sale del bucle while al alcanzar dicho limite. Posteriormente se lanzaría otra transacción empezando a iterar desde donde terminó la transacción anterior:

struct Payee {
    address addr;
    uint256 value;
}

Payee[] payees;
uint256 nextPayeeIndex;

function payOut() {
    uint256 i = nextPayeeIndex;
    while (i < payees.length && msg.gas > 200000) {
      payees[i].addr.send(payees[i].value);
      i++;
    }
    nextPayeeIndex = i;
}

Hay que tener en cuenta que de esta forma se rompe la atomicidad de la operación y tendríamos que ser especialmente cuidadosos. Es por esto que esta alternativa solo debe usarse cuando sea estrictamente necesario.

Fin de la serie

Con este artículo doy por terminado esta serie de artículos de ataques a smart contracts. Ésto ha sido solo una introducción y hay otros posibles ataques conocidos y muchos más que aparecerán con el tiempo. Hemos visto que es muy fácil cometer errores y dejar puertas abiertas a ataques en nuestros smart contracts. Es por ésto que debemos ser especialmente cuidadosos ya que estos errores pueden tener consecuencias catastróficas y además no es sencillo corregir errores a posteriori. La mayor parte del tiempo en el desarrollo de estos contratos se debe destinar a la revisión, pruebas y auditoría de éstos.

Espero que os haya gustado esta serie de artículos y os sean útiles.

Un saludo y ¡hasta el próximo artículo!

Anuncios

WordPress.com.

Subir ↑

A %d blogueros les gusta esto: