Was war die zweite Schwachstelle, die bei der DAO-Attacke am 17. Juni 2016 ausgenutzt wurde?

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

Antworten (2)

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] = 0wurde 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.

Quelle

@ 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. TransferNur 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.

balances[msg.sender] = 0hätte den Anruf sowieso nach dem ersten beenden sollen splitDAO(...). Es gibt einige Diskussionen darüber auf reddit.com/r/ethereum/comments/4onbkj/… .
In dem Thread, auf den im obigen Kommentar verwiesen wird, gibt es einige Tweets von @koeppelmann über das Übertragen der Token zwischen den beiden angreifenden Konten, aber ich verstehe noch nicht, wie das gemacht wird und ob dies die zweite ausgenutzte Schwachstelle ist.