¿Cuál fue la segunda vulnerabilidad utilizada en el ataque DAO del 17 de junio de 2016?

De las preguntas frecuentes sobre el gran atraco theDAO de koeppelmann :

¿Cómo funcionó exactamente el ataque? El atacante logró combinar 2 exploits . El primer exploit fue llamar recursivamente a la función split DAO. Eso significa que la primera llamada regular activaría una segunda llamada (irregular) de la función y la segunda llamada activaría otra llamada y así sucesivamente. Las siguientes llamadas se realizan en un estado antes de que el saldo del atacante vuelva a 0. Esto permitió al atacante dividir 20 veces (tener que buscar el número exacto) por transacción. No podía hacer más; de lo contrario, las transacciones se habrían vuelto demasiado grandes y eventualmente habrían alcanzado el límite de bloques. Este ataque ya habría sido doloroso. Sin embargo, lo que lo hizo realmente doloroso es que el atacante logró replicar este ataque desde las mismas dos direcciones con los mismos tokens una y otra vez (aproximadamente 250 veces desde 2 direcciones cada una). Entonces, el atacante encontró un segundo exploit que permitió dividir sin destruir los tokens en el DAO principal.. Se las arreglaron para transferir los tokens antes de que fueran enviados a la dirección 0x0 y solo después de esto son devueltos) La combinación de ambos ataques multiplicó el efecto. El ataque uno a uno habría sido muy intensivo en capital (debe aportar 1/20 de la cantidad robada por adelantado), el ataque dos habría llevado mucho tiempo.

La primera vulnerabilidad se analiza en ¿Qué es una vulnerabilidad de llamada recursiva? .


P : ¿Cuál es la segunda vulnerabilidad que permitió al atacante "replicar este ataque desde las mismas dos direcciones con los mismos tokens una y otra vez (aproximadamente 250 veces desde 2 direcciones cada una)"?

Del código DAO , el splitDAO(...)código de función sigue:

function splitDAO(
    uint _proposalID,
    address _newCurator
) noEther onlyTokenholders returns (bool _success) {

    Proposal p = proposals[_proposalID];

    // Sanity check

    if (now < p.votingDeadline  // has the voting deadline arrived?
        //The request for a split expires XX days after the voting deadline
        || now > p.votingDeadline + splitExecutionPeriod
        // Does the new Curator address match?
        || p.recipient != _newCurator
        // Is it a new curator proposal?
        || !p.newCurator
        // Have you voted for this split?
        || !p.votedYes[msg.sender]
        // Did you already vote on another proposal?
        || (blocked[msg.sender] != _proposalID && blocked[msg.sender] != 0) )  {

        throw;
    }

    // If the new DAO doesn't exist yet, create the new DAO and store the
    // current split data
    if (address(p.splitData[0].newDAO) == 0) {
        p.splitData[0].newDAO = createNewDAO(_newCurator);
        // Call depth limit reached, etc.
        if (address(p.splitData[0].newDAO) == 0)
            throw;
        // should never happen
        if (this.balance < sumOfProposalDeposits)
            throw;
        p.splitData[0].splitBalance = actualBalance();
        p.splitData[0].rewardToken = rewardToken[address(this)];
        p.splitData[0].totalSupply = totalSupply;
        p.proposalPassed = true;
    }

    // Move ether and assign new Tokens
    uint fundsToBeMoved =
        (balances[msg.sender] * p.splitData[0].splitBalance) /
        p.splitData[0].totalSupply;
    if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
        throw;

    // Assign reward rights to new DAO
    uint rewardTokenToBeMoved =
        (balances[msg.sender] * p.splitData[0].rewardToken) /
        p.splitData[0].totalSupply;

    uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved /
        rewardToken[address(this)];

    rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved;
    if (rewardToken[address(this)] < rewardTokenToBeMoved)
        throw;
    rewardToken[address(this)] -= rewardTokenToBeMoved;

    DAOpaidOut[address(p.splitData[0].newDAO)] += paidOutToBeMoved;
    if (DAOpaidOut[address(this)] < paidOutToBeMoved)
        throw;
    DAOpaidOut[address(this)] -= paidOutToBeMoved;

    // Burn DAO Tokens
    Transfer(msg.sender, 0, balances[msg.sender]);
    withdrawRewardFor(msg.sender); // be nice, and get his rewards
    totalSupply -= balances[msg.sender];
    balances[msg.sender] = 0;
    paidOut[msg.sender] = 0;
    return true;
}

La declaración balances[msg.sender] = 0;en la parte inferior de splitDAO(...)debería haber impedido que la misma dirección llamara a la splitDAO(...)función para transferir fondos con éxito varias veces.

Y a partir de las preguntas y respuestas, ¿ hay alguna forma de determinar cuánto tiempo le tomó al atacante DAO implementar el ataque? , cada una de las transacciones (la primera y la segunda al menos de mi conteo manual) llamó splitDAO(...)29 veces. Pero las splitDAO(...)llamadas 29 x se llamaron repetidamente, creando 27996 transacciones internas, 13996 fueron transferencias internas distintas de cero. Cálculo: 13996 transacciones x 258.05656476 ETH = 3,611,759.68038 ethers, que es aproximadamente los 3,641,694.241898506 Ether ($59,578,117.80) que se movieron a la cuenta 0x304a554a310c7e546dfe434669c0.62820b07d62820

Respuestas (2)

No es tanto una vulnerabilidad, pero el ataque transfirió inteligentemente sus tokens DAO entre 2 cuentas, usando function transfer(address _to, uint256 _amount).

Entonces, la función de respaldo del contrato de ataque se ve así:

function() {
  transfer DAO tokens to other attacking contract
  invoke splitDAO
}

Hubo 2 contratos de ataque que se transfirieron tokens DAO entre sí. Cuando finalizaba la transacción de un contrato atacante, balances[msg.sender] = 0se configuraba correctamente, pero los tokens se habían transferido al otro contrato. Ahora el otro contrato realiza el ataque hasta que finaliza la transacción. Los contratos de ataque se alternan.

Fuente

La respuesta de @Roland menciona cómo TheDAO podría haber evitado esto.

// Burn DAO Tokens
    Transfer(msg.sender, 0, balances[msg.sender]);

La intención de Christoph Jentzsch parece haber sido quemar la ficha. En su lugar, llamó a un Evento. ¿Solo porque el Evento fue llamado Transferen lugar de LogTransfer??
Explicado en la excelente pieza de Peter Vessenes :

en lugar de la función de registro, deberíamos tener:

if (!transfer(0 , balances[msg.sender])) { throw; }

Esto evitaría que funcionen los ataques de llamadas recursivas, pero también reduciría los tokens disponibles para el usuario más adelante.

balances[msg.sender] = 0debería haber detenido la splitDAO(...)llamada después de la primera de todos modos. Hay alguna discusión sobre esto en reddit.com/r/ethereum/comments/4onbkj/… .
En el hilo al que se hace referencia en el comentario anterior, hay algunos tweets de @koeppelmann sobre la transferencia de tokens entre las dos cuentas atacantes, pero aún no entiendo cómo se hace esto y si esta es la segunda vulnerabilidad explotada.