Aus The big theDAO Heist FAQ von koeppelmann :
Wie funktionierte der Angriff genau? Dem Angreifer gelang es, 2 Exploits zu kombinieren . Der erste Exploit bestand darin, die Split-DAO-Funktion rekursiv aufzurufen. Das heißt, der erste reguläre Aufruf würde einen zweiten (unregelmäßigen) Aufruf der Funktion auslösen und der zweite Aufruf würde einen weiteren Aufruf auslösen und so weiter. Die folgenden Aufrufe werden in einem Zustand durchgeführt, bevor der Kontostand des Angreifers auf 0 zurückgesetzt wird. Dadurch konnte der Angreifer 20 Mal pro Transaktion splitten (muss die genaue Zahl nachschlagen). Mehr konnte er nicht tun – sonst wären die Transaktionen zu groß geworden und hätten irgendwann das Blocklimit erreicht. Dieser Angriff wäre schon schmerzhaft gewesen. Was es jedoch wirklich schmerzhaft machte, war, dass es den Angegriffenen gelang, diesen Angriff von denselben zwei Adressen mit denselben Token immer und immer wieder zu replizieren (ungefähr 250 Mal von jeweils 2 Adressen). Also fand der Angreifer einen zweiten Exploit, der es ermöglichte, zu splitten, ohne die Tokens im Haupt-DAO zu zerstören. Sie haben es geschafft, die Tokens zu übertragen, bevor sie an die Adresse 0x0 gesendet werden und erst danach werden sie zurückgeschickt.) Die Kombination beider Angriffe multipliziert den Effekt. Angriff eins auf sich selbst wäre sehr kapitalintensiv gewesen (Sie müssen 1/20 des gestohlenen Betrags im Voraus aufbringen) – Angriff zwei hätte lange gedauert.
Die erste Schwachstelle wird in Was ist eine rekursive Aufrufschwachstelle? .
F : Was ist die zweite Schwachstelle, die es dem Angreifer ermöglichte, „diesen Angriff von denselben zwei Adressen mit denselben Token immer und immer wieder zu replizieren (ungefähr 250 Mal von jeweils zwei Adressen)“?
Aus The DAO code folgt der splitDAO(...)
Funktionscode:
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;
}
Die Anweisung balances[msg.sender] = 0;
am Ende von splitDAO(...)
hätte verhindern sollen, dass dieselbe Adresse die splitDAO(...)
Funktion mehrmals erfolgreich aufruft, um Geld zu überweisen.
Und anhand der Fragen und Antworten gibt es eine Möglichkeit zu bestimmen, wie lange es gedauert hat, bis der DAO-Angreifer den Angriff ausgeführt hat? , jede der Transaktionen (die erste und zweite zumindest nach meiner manuellen Zählung) hat splitDAO(...)
29 Mal angerufen. Aber die 29 x splitDAO(...)
Aufrufe wurden wiederholt aufgerufen, wodurch 27996 interne Transaktionen erstellt wurden, 13996 waren interne Übertragungen ungleich Null. Berechnung: 13996 Transaktionen x 258,05656476 ETH = 3.611.759,68038 Ether, was ungefähr 3.641.694,241898506 Ether ($59.578.117,80) entspricht, die auf das Konto 0x304a554a310c7e546dfe434609bc68.349d628 verschoben wurden
Es ist nicht so sehr eine Schwachstelle, aber der Angriff hat seine DAO-Token geschickt zwischen zwei Konten übertragen, indem er function transfer(address _to, uint256 _amount)
.
Die Fallback-Funktion des angreifenden Kontrakts sieht also so aus:
function() {
transfer DAO tokens to other attacking contract
invoke splitDAO
}
Es gab 2 angreifende Verträge, die sich gegenseitig DAO-Token übertrugen. Wenn die Transaktion eines angreifenden Vertrags abgeschlossen war, balances[msg.sender] = 0
wurde korrekt gesetzt, aber die Token wurden auf den anderen Vertrag übertragen. Jetzt führt der andere Vertrag den Angriff durch, bis seine Transaktion abgeschlossen ist. Die Angriffskontrakte wechseln sich ab.
@ Rolands Antwort erwähnt, wie TheDAO dies hätte verhindern können.
// Burn DAO Tokens
Transfer(msg.sender, 0, balances[msg.sender]);
Die Absicht von Christoph Jentzsch scheint gewesen zu sein, den Token zu verbrennen. Stattdessen rief er ein Ereignis an. Transfer
Nur weil das Event statt gerufen wurde LogTransfer
??
Erklärt in dem ausgezeichneten Stück von Peter Vessenes :
Anstelle der Protokollierungsfunktion sollten wir Folgendes haben:
if (!transfer(0 , balances[msg.sender])) { throw; }
Dies würde die rekursiven Aufrufangriffe verhindern, aber auch die später für den Benutzer verfügbaren Token reduzieren.
Datenschutz ist ein Menschenrecht.eth
balances[msg.sender] = 0
hätte den Anruf sowieso nach dem ersten beenden sollensplitDAO(...)
. Es gibt einige Diskussionen darüber auf reddit.com/r/ethereum/comments/4onbkj/… .Datenschutz ist ein Menschenrecht.eth