¿Qué es una vulnerabilidad de llamada recursiva?

¿Qué es exactamente una vulnerabilidad de llamada recursiva?

Al crear contratos inteligentes, DAO o DAPP, ¿qué medidas puedo tomar para asegurarme de no ser vulnerable?

Respuestas (3)

Una explicación más simple

  1. El atacante crea un contrato de billetera ( 0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89 en el ataque del 17/06/2016) con un valor predeterminado (o alternativo) function ()para llamar a la función de DAO splitDAO(...)varias veces. El siguiente es un valor predeterminado simple function ():

    function () {
       // Note that the following statement can only be called recursively
       // a limited number of times to prevent running out of gas or
       // exceeding the call stack
       call TheDAO.splitDAO(...)
    }
    
  2. El atacante crea (o se une) a una propuesta dividida (n.º 59 en el ataque del 17/06/2016 ) con la dirección del destinatario establecida en el contrato de billetera creado anteriormente.

  3. El atacante vota Sí a la propuesta dividida.

  4. Una vez que expira la propuesta dividida, el atacante llama a la splitDAO(...)función de DAO.

    una. La splitDAO(...)función llama al valor predeterminado del contrato de billetera function ()como parte del envío de los éteres al destinatario.

    b. El valor predeterminado del contrato de billetera vuelve function ()a llamar al DAO splitDAO(...), que repite el ciclo desde a. arriba.

    C. El valor predeterminado del contrato de billetera function ()debe garantizar que no se produzca un error, ya que las transacciones se revertirán si se excede la pila de llamadas o el gas.


Los siguientes son fragmentos del código fuente de la DAO involucrados en este tipo de ataque:

DAO.splitDAO(...):

El problema en el siguiente código es que el pago se realiza (estado de cuenta withdrawRewardFor(msg.sender);) antes de restablecer las variables que llevan el control de los pagos que el destinatario tiene derecho a recibir ( balances[msg.sender] = 0;y paidOut[msg.sender] = 0;).

    function splitDAO(
        uint _proposalID,
        address _newCurator
    ) noEther onlyTokenholders returns (bool _success) {
        ...     
        withdrawRewardFor(msg.sender); // be nice, and get his rewards
        totalSupply -= balances[msg.sender];
        balances[msg.sender] = 0;
        paidOut[msg.sender] = 0;
        return true;
    }


DAO.withdrawRewardFor(...):

    function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
        if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
            throw;

        uint reward =
            (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
        if (!rewardAccount.payOut(_account, reward))
            throw;
        paidOut[_account] += reward;
        return true;
    }


ManagedAccount.payOut(...):

La declaración _recipient.call.value(_amount)()envía los éteres a la cuenta del destinatario, en este caso function ()se llama al valor predeterminado del contrato de billetera, lo que permite llamar DAO.splitDAO(...)a la función de forma recursiva.

    function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }        


Ver también:



Más información de fondo

Aquí está la publicación de blog original de Peter Vessenes que describió la vulnerabilidad de llamadas recursivas en DAO: Más ataques de Ethereum: Race-To-Empty is the Real Deal , con una solución sugerida para este problema.

De la publicación:

la vulnerabilidad

Aquí hay algo de código; a ver si encuentras el problema.

function getBalance(address user) constant returns(uint) {  
  return userBalances[user];
}

function addToBalance() {  
  userBalances[msg.sender] += msg.amount;
}

function withdrawBalance() {  
  amountToWithdraw = userBalances[msg.sender];
  if (!(msg.sender.call.value(amountToWithdraw)())) { throw; }
  userBalances[msg.sender] = 0;
}

Aquí está el problema: msg.sender podría tener una función predeterminada que se parece a esto.

function () {  
 // To be called by a vulnerable contract with a withdraw function.
 // This will double withdraw.

 vulnerableContract v;
 uint times;
 if (times == 0 && attackModeIsOn) {
   times = 1;
   v.withdraw();

  } else { times = 0; }
}

¿Lo que sucede? La pila de llamadas se ve así:

   vulnerableContract.withdraw run 1
     attacker default function run 1
       vulnerableContract.withdraw run 2
         attacker default function run 2

Cada vez, el contrato verifica el saldo disponible del usuario y lo envía. Por lo tanto, el usuario obtendrá el doble de su saldo del contrato.

Cuando el código se resuelve, el saldo del usuario se establecerá en 0 sin importar cuántas veces se haya llamado al contrato.

Y las soluciones sugeridas, de la publicación:

Enfoque de remediación 1: Obtenga su pedido correcto

El enfoque recomendado en los ejemplos de solidez actualizados que se publicarán próximamente es usar un código como este:

function withdrawBalance() {  
  amountToWithdraw = userBalances[msg.sender];
  userBalances[msg.sender] = 0;
  if (amountToWithdraw > 0) {
    if (!(msg.sender.send(amountToWithdraw))) { throw; }
  }
}

y

Enfoque de remediación 2: Mutexes

Considere este código en su lugar.

function withdrawBalance() {  
  if ( withdrawMutex[msg.sender] == true) { throw; }
  withdrawMutex[msg.sender] = true;
  amountToWithdraw = userBalances[msg.sender];
  if (amountToWithdraw > 0) {
    if (!(msg.sender.send(amountToWithdraw))) { throw; }
  }
  userBalances[msg.sender] = 0;
  withdrawMutex[msg.sender] = false;
}



Y de la publicación del usuario eththrowa en la publicación del foro de DAO El error descubierto en el contrato de token MKR también afecta a theDAO: permitiría a los usuarios robar recompensas de theDAO llamando recursivamente :

Este error: https://www.reddit.com/r/ethereum/comments/4nmohu/from_the_maker_dao_slack_today_we_discovered_a/57 también está presente en el código DAO, específicamente aquí en la función de retiro de recompensas DAO.sol:

if (!rewardAccount.payOut(_account, reward))
   throw;
paidOut[_account] += reward;
return true;

y aquí en ManagedAccount.sol

function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }

Esto permitiría a un usuario agotar muchas veces su derecho llamando al contrato de forma recursiva. Por extraño que parezca, el equipo de slockit detectó este error aquí en la sección de propuestas:

// we are setting this here before the CALL() value transfer to
// assure that in the case of a malicious recipient contract trying
// to call executeProposal() recursively money can't be transferred
// multiple times out of the DAO
p.proposalPassed = true;

pero se lo perdió en la sección de recompensas. Obviamente, todavía no hay recompensas en el DAO, por lo que este no es un problema que pueda costar dinero hoy.



P : Al crear contratos inteligentes, DAO o DAPP, ¿qué medidas puedo tomar para asegurarme de no ser vulnerable?

Prueba, auditoría, prueba, auditoría, ... . Al igual que con cualquier sistema de software, hay muchas áreas potenciales en las que pueden aparecer errores. Y cuanto mayor sea el valor que tenga, más interés tendrán los atacantes en él.

Del blog de Ethereum ACTUALIZACIÓN CRÍTICA Re: Vulnerabilidad DAO :

Los autores de contratos deben tener cuidado de (1) tener mucho cuidado con los errores de llamadas recursivas y escuchar los consejos de la comunidad de programación de contratos de Ethereum que probablemente se publicarán la próxima semana sobre la mitigación de dichos errores, y (2) evitar crear contratos que contengan más de ~ $ 10 millones de valor, con la excepción de los contratos de sub-token y otros sistemas cuyo valor está definido por consenso social fuera de la plataforma Ethereum, y que pueden ser fácilmente "bifurcados" a través del consenso de la comunidad si surge un error (por ejemplo, MKR), al menos hasta que la comunidad obtenga más experiencia con la mitigación de errores y/o se desarrollen mejores herramientas.

El hilo de reddit ¿Podemos por favor nunca más poner 100m en un contrato sin pruebas formales de corrección? sugerir alguna prueba de corrección formal (pero aún puede haber errores).

Habrá más avisos en las próximas semanas. Actualizaré esta respuesta.

Algunos recursos:

"El atacante crea una propuesta dividida con la dirección del destinatario y comienza el contrato de billetera creado anteriormente". No se puede analizar la oración. No tiene sentido gramatical.
¿Cómo es esto c. The wallet contract's default function () must ensure that an error is not thrown as the transactions will be rolled back if the call stack or gas is exceeded.posible? Si se excede el gas, ¿no debería EVM arrojar un error sin importar qué? @BokkyPooBah

Si su código se ve así en pseudocódigo:

function do:
   if (pool has mymoney = true)
     split(mymoney) 
     pool has mymoney = false

Al llamar repetidamente a esa función, tiene una especie de condición de carrera en la que puede gastar su dinero dos veces, tres veces, ... hasta el infinito .

La solución es simple, invierta dos operaciones:

function do:
   if (pool has mymoney = true)
     pool= pool - mymoney // 2
     split(mymoney) //1

Ver este compromiso por ejemplo de la solución

¿Las transacciones en EVM no se ejecutan atómicamente, por lo tanto, la condición de carrera no debería tener ningún efecto? @Roland Kofler

Una "vulnerabilidad de llamada recursiva" es un término ambiguo que debe evitarse porque es impreciso y puede significar 2 cosas.

Ataque reentrante

Probablemente te refieres a "vulnerabilidad de reentrada" o "ataque de reentrada", que es lo que describe la respuesta de @Roland. Nota: no todos los ataques de reentrada tienen que ser recursivos (en el sentido de que el código malicioso no tiene que volver a entrar de la misma manera: puede volver a entrar en un contrato a través de cualquier función accesible externamente).

http://forum.ethereum.org/discussion/1317/reentrant-contratos

https://github.com/LeastAuthority/ethereum-analyses/blob/master/GasEcon.md

Ataque de profundidad de llamada (ya no es posible con EIP 150)

En Ethereum, también es posible un "ataque de profundidad de llamada" (una de las formas en que se puede realizar es con llamadas recursivas).

¿Cómo el ataque de profundidad de pila hace que un envío () falle silenciosamente?

Ataque de pila de llamadas

Si alguien está leyendo esto ahora, solo le informamos que el ataque de profundidad de llamada ya no es posible después de la actualización de EIP 150.