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
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] = 0
se 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.
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 Transfer
en 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.
privacidadisahumanright.eth
balances[msg.sender] = 0
debería haber detenido lasplitDAO(...)
llamada después de la primera de todos modos. Hay alguna discusión sobre esto en reddit.com/r/ethereum/comments/4onbkj/… .privacidadisahumanright.eth