WEB3DEV Español

Cover image for Entendiendo los Ataques de Repetición de Firma
Delia Viloria T
Delia Viloria T

Posted on

Entendiendo los Ataques de Repetición de Firma

Entendiendo cómo los ataques de repetición de firma suceden en Solidity y entender las formas de prevenirlo

Image description

El objetivo de este post de blog es proveer un entendimiento detallado de la vulnerabilidad, causada por el Ataque de Repetición de Firma y las formas de prevenirlo. Este es un adicional de la serie de nuestro blog anterior, donde compartimos los detalles de Sobreflujo y Bajo flujo de Integrales, Ataque de Ejecución de Frente, Entendiendo la Manipulación del Bloqueo de la Marca de tiempo. Problemas con la Autorización usando tx.origin, Pérdida de Precisión en Operaciones de Solidity, y Problemas con la Visibilidad de Modificadores en Solidity.

Introducción

Las firmas digitales sirven como una “huella dactilar” única en el mundo de las transacciones blockchain, proveyendo una forma de autenticación en un entorno donde depende, fuertemente, en la confianza. Estas firmas forman la columna vertebral de las transacciones en los ecosistemas blockchain, especialmente en la operación de contratos inteligentes. No obstante, si las medidas de seguridad impuestas para estos contratos inteligentes no son suficientemente robustas, se vuelven objetivos potenciales para ‘ataques de repetición de firma’. Estos ataques pueden permitir transacciones no autorizadas e incluso el robo de fondos. Por lo tanto, son un riesgo considerable para la seguridad total y la confianza de las aplicaciones descentralizadas.

Vulnerabilidad

Aún así, asumamos que Bob, quien es el receptor de la transacción de Alice, tiene intenciones malignas. Bob encuentra y copia la firma de Alice, su “huella digital” única, de una transacción anterior. Si el contrato inteligente no está al día, Bob puede explotar esta firma capturada, análoga de cómo un individuo sin escrúpulos puede usar la clave de una casa copiada, para ganar la entrada sin autorización.

¿Qué hace Bob con la firma capturada de Alice? Bob prefiere repetir o duplicar esta transacción en otra cadena, en la red Arbitrum, la cual también soporta la misma app DeFi. El resultado de esta transferencia accidental es de 50 ETH, transferidos desde la cuenta de Alice a la cuenta de Bob, pero esta vez en la red de Arbitrum. Esto constituye un ataque de repetición de firma: Alice, sin saber nada de esta actividad, sufre una pérdida imprevista sin su conocimiento o aprobación.

Hay numerosos factores que pueden hacer que los contratos inteligentes sean vulnerables a los ataques de repetición de firma. Una de las debilidades fundamentales es la falta de “nonce”. Esencialmente, un identificador único o un “número usado una vez (number used once)” para cada transacción. Un nonce es integral para la seguridad. Su ausencia, puede permitir a alguien como Bob para que, repetidamente, explote la firma capturada de Alice y haga transacciones fraudulentas.

Otro punto débil potencial es la falla de implementar revisiones específicas para diferentes redes de blockchain. Esta situación puede compararse a usar el mismo tipo de cerradura en cada puerta, permitiendo que todas las puertas sean accesibles si una sola llave se copia. Por lo tanto, cada firma de transacción también debería encapsular el ID de una red, un identificador único para la red específica, para prevenir el mal uso de firmas a través de cadenas cruzadas.

Por otra parte, el código en sí del contrato inteligente puede que sea vulnerable, incluso si las revisiones fallan en verificar correctamente las firmas del nonce y el ID de la red. Por lo tanto, es vital usar frameworks desarrollados bien probados de contratos inteligentes como OpenZeppeling, para evitar cometer errores.

Escenario de Ataque 1: Falta Nonce y Chain ID

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

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

contract Vault {
using ECDSA for bytes32;
address public owner;

constructor() payable {
owner = msg.sender;
}
function transfer(address to, uint amount, bytes[2]memory sigs) external {
bytes32 hashed = computeHash(to, amount);
require(validate(sigs, hashed), "invalid sig");

(bool sent, ) = to.call{ value: amount } ("");
require(sent, "Failed to send Ether");
}

function computeHash(address to, uint amount) public pure returns(bytes32) {
return keccak256(abi.encodePacked(to, amount));
}

function validate(bytes[2]memory sigs, bytes32 hash) private view returns(bool) {
bytes32 ethSignedHash = hash.toEthSignedMessageHash();

address signer = ethSignedHash.recover(sigs[0]);
bool valid = signer == owner;

if (!valid) {
return false;
}
return true;
}
}
Enter fullscreen mode Exit fullscreen mode

En la función transfer, el contrato revisa la validez de las firmas proveídas, recuperando la dirección del firmante desde el hash del mensaje firmado y comparándolo con la dirección del dueño.

No obstante, el código no incluye ningún mecanismo para revisar si un mensaje, válidamente firmado, es malignamente reusado en un contexto distinto o en otras redes. Un atacante maligno puede capturar una transacción válidamente firmada y repetirla múltiples veces, resultando en la transferencia de fondos no autorizada.

Este exploit surge desde el hecho que el contrato sólo revisa si la dirección recuperada del firmante es igual a la dirección del dueño, sin considerar si la firma ha sido usada anteriormente. Como resultado, un atacante puede capturar una transacción legítimamente firmada y realizarla repetidamente en el contrato, ejecutando exitosamente la función transfer múltiples veces en otras redes blockchain.

Escenario de Ataque 2: Chain ID faltante

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

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

contract Vault {
using ECDSA for bytes32;

address public owner;
mapping(bytes32 => bool) public executed;

constructor() payable {
owner = msg.sender;
}

function transfer(address to, uint amount, uint nonce, bytes[2] memory sigs) external {
bytes32 hashed = computeHash(to, amount, nonce);
require(!executed[hashed], "tx already executed");
require(validate(sigs, hashed), "invalid sig");

executed[hashed] = true;

(bool sent, ) = to.call{value: amount}("");
require(sent, "Failed to send Ether");
}

function computeHash(address to, uint amount, uint nonce) public view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), to, amount, nonce));
}

function validate(bytes[2] memory sigs, bytes32 hash) private view returns (bool) {
bytes32 ethSignedHash = hash.toEthSignedMessageHash();

address signer = ethSignedHash.recover(sigs[0]);
bool valid = signer == owner;

if (!valid) {
return false;
}

return true;
}
}
Enter fullscreen mode Exit fullscreen mode

En el ejemplo de arriba, los implementos del contrato nonce, hacen imposible la repetición de firmas existente porque, una vez que un nonce haya sido usado una vez, se vuelve inválido para transacciones futuras. Aunque el contrato de arriba es seguro para los ataques de repetición de firma en una cadena individual, aún es vulnerable a los ataques de repetición de firma si es desplegado en múltiples cadenas.

Contrato Seguro

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

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";
contract Vault {

using ECDSA for bytes32;
address public owner;
mapping(bytes32 => bool) public executed;
constructor() payable {
owner = msg.sender;
}

function transfer(address to, uint amount, uint nonce, uint chainId, bytes[2] memory sigs) external {
bytes32 hashed = computeHash(to, amount, nonce, chainId);
require(!executed[hashed], "tx already executed");
require(validate(sigs, hashed), "invalid sig");

executed[hashed] = true;

(bool sent, ) = to.call{value: amount}("");
require(sent, "Failed to send Ether");
}

function computeHash(address to, uint amount, uint nonce, uint chainId) public view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), to, amount, nonce, chainId));
}

function validate(bytes[2] memory sigs, bytes32 hash) private view returns (bool) {
bytes32 ethSignedHash = hash.toEthSignedMessageHash();

address signer = ethSignedHash.recover(sigs[0]);
bool valid = signer == owner;

if (!valid) {
return false;
}

return true;
}
}
Enter fullscreen mode Exit fullscreen mode

En el ejemplo de arriba, el contrato revisa ambos, nonce y chainID para que la firma sea considerada válida. En contraste a los ejemplos anteriores, donde simplemente le pregunta al usuario por su firma, esta estrategia te permite ser un poco más específico antes de pedirle a un usuario firmar una transacción. Usando nonce y chainID juntos, en el proceso de la firma, estás proveyendo una carga útil mucho más detallada para que el usuario firme. Las oportunidades para que el nonce y chainID sean reusados, son extremadamente bajas.

Prevención

Hay diferentes formas para prevenir los Ataques de Repetición de Firma en Solidity:

  • Un nonce es un número único que es creado para cada transacción. El uso de un nonce único para cada transacción, asegura que cada transacción sea distinta y no pueda ser replicada.
  • Usando un valor basado en el tiempo como un componente en la firma, es otro método para prevenir el Ataque de Repetición de Firma. Esto asegura que cada transacción sea única y no pueda ser repetida, luego de cierta cantidad de tiempo. Esto se puede lograr en Solidity, añadiendo una marca de tiempo a la firma.
  • Otra forma de prevenir el ataque de repetición es usar una estructura de firma específica de la cadena, como EIP-155, el cual incluye el chain ID en el mensaje firmado. Esto prevendrá que transacciones firmadas en una cadena, sean válidas en otra cadena con un ID distinto.

Conclusión

En conclusión, los ataques de repetición de firma son una amenaza significativa para la seguridad de los contratos inteligentes. Es crucial para los desarrolladores de contratos inteligentes, y para los usuarios, entender los riesgos asociados con esta vulnerabilidad, y tomar medidas apropiadas para mitigarlas.

Reusando firmas válidas, los atacantes pueden ejecutar transacciones sin autorización en la red blockchain, resultando en pérdidas financieras y en otras consecuencias dañinas. Sin embargo, usando nonces únicos, valores basados en el tiempo, esquemas de verificación de firma y frameworks bien auditados como OpenZeppelin, los desarrolladores pueden, efectivamente, desalentar los ataques de repetición de firma.

Sobre Nosotros

El proyecto Neptune Mutual resguarda la comunidad Ethereum de las amenazas cibernéticas. El protocolo usa coberturas paramétricas en oposición a los seguros discrecionales. Tiene un proceso en la cadena fácil y confiable. Esto significa que, cuando los incidentes son confirmados por nuestra comunidad, la resolución es rápida.

Únete en nuestra misión para cubrir, proteger y dar seguridad a los activos digitales en la cadena.

Este artículo fue escrito por Neptune Mutual y traducido por Delia Viloria T. Su original se puede leer aquí.

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)