Wie führt der Stapeltiefenangriff dazu, dass ein send() stillschweigend fehlschlägt?

Aus einem Blogpost zum Thema Sicherheit von Christian Reitwiessner:

Aufgrund der maximalen Stack-Tiefe von 1024 kann der neue Bieter die Stack-Größe jederzeit auf 1023 erhöhen und dann bid() aufrufen, wodurch der send(highestBid)-Aufruf stillschweigend fehlschlägt.

Es scheint also, dass Sie den Stack so vorbereiten können, dass ein send() fehlschlägt, der Vertrag jedoch seine Ausführung fortsetzt und beendet wird.

Was ist die Logik hinter diesem Verhalten und ist es möglich, mehr als ein send() "anzugreifen" oder würde ein zweites send() eine Ausnahme verursachen, wenn das erste bereits "stillschweigend" fehlgeschlagen ist?

Antworten (1)

Begründung für die Tiefenbegrenzung:

Mit einer Aufruftiefenbegrenzung von 1024 - viele Programmiersprachen brechen bei hohen Stapeltiefen viel schneller ab als bei hoher Speichernutzung oder Rechenlast, sodass die implizite Grenze der Blockgasgrenze möglicherweise nicht ausreicht.

Die EVM muss deterministisch sein. Wenn ein Ethereum-Client in einer Sprache implementiert ist, die bei hohen Stapeltiefen bricht, dann wäre ein solcher Client nicht in der Lage, das Ethereum-Protokoll zu implementieren: Er wäre nicht in der Lage, die Ergebnisse gemäß Konsens zu produzieren.

Ein kürzlich von Vitalik Buterin vorgeschlagener Vorschlag ist, einfach Gas als begrenzenden Faktor zu haben (obwohl die Tiefe 2091 erreichen könnte).


function foo() {
  send()
  send()
}

Beide send()können angegriffen werden. Der Aufruf des ersten send()erhöht die Tiefe um 1, verringert sie aber wieder um 1, wenn sie zurückkehrt. send()nicht throw, daher werden keine Ausnahmen generiert.

Um anzugreifen foo(), ruft sich ein Angreifer rekursiv 1023 Mal selbst auf und ruft dann auf foo(). (In Solidity sollte die rekursive Funktion so sein external, dass die Aufruftiefe erhöht wird [im Gegensatz zu internen Funktionen, die Sprünge innerhalb des Codes sind und die Tiefe nicht erhöhen].)

(Ruft sich selbst 1023 Mal auf, da der erste Aufruf vor der Rekursion die Tiefe um 1 erhöht. Zum Zeitpunkt des Aufrufs foowird die Tiefe die Grenze von 1024 erreichen.)

BEARBEITEN: EIP 150 macht es unpraktisch, eine Stack-Tiefe von 1024 zu erreichen, wodurch Call-Depth-Angriffe effektiv eliminiert werden. Siehe Wie ändert EIP 150 den Anruftiefenangriff?

Was meinst du mit "ein Angreifer ruft sich rekursiv auf"? @eth
@Alper So etwas wie eine attack()Funktion, die das tutif (i==1023) foo() else { i++; attack(); }
Was ist die Logik dahinter, sich selbst anzugreifen, dh einen Vertrag mit einer Funktion zu schreiben, die sich selbst 1023 Mal rekursiv aufruft und dann etwas anderes aufruft, das nie ausgeführt wird? Auch dieser Exploit scheint keinen finanziellen Gewinn zu bringen.
@Gviz Du greifst dich nicht selbst an. Der Blogpost in der Frage hat ein Beispiel. Ein weiteres Beispiel ist, dass Sie einen nicht fungiblen Token von jemandem kaufen. Wenn ein Call-Depth-Angriff möglich wäre, könnte er verwendet werden, damit Sie den Token erhalten, aber am Ende die andere Person nicht bezahlen.
@eth danke ich sehe jetzt. Ein Punkt zur Klarstellung: Um die Stapeltiefe zu erhöhen, ist ein externer Anruf erforderlich. Reicht es aus, dass der Angreifervertrag eine Funktion attack() definiert hat, die mit this.attack() aufgerufen wird, um sich als extern zu verhalten?
@GViz Um die CALL-Stack-Tiefe zu erhöhen, muss der CALL-Opcode verwendet werden. Sie haben Recht, dass ein externer Aufruf erforderlich ist, da Solidity einen internen Aufruf (wie this.attack) mit JUMP-Opcode anstelle von CALL implementiert.
@eth in diesem Fall sollte die attack()Funktion extern aufgerufen werden und der Code sollte if (i==1023) someExternalContract.foo() else { i++; this.attack(); }korrekt sein? Unter der Annahme eines Gebotsvertrags würde der vorherige Höchstbietende keinen Wert erhalten und der Saldo des Vertrags würde wachsen.
@GViz Sieht richtig aus.