Schwachstellen Smart Contracts Etherium. Codebeispiele

Mit diesem Beitrag beginne ich eine Reihe von Artikeln zum Thema Sicherheit von Ethereum-Smart-Verträgen. Ich denke, dass dieses Thema sehr relevant ist, da die Anzahl der Entwickler avalancheartig ansteigt und es niemanden gibt, der sie vor dem „Rechen“ schützt. Vorerst - Übersetzungen ...

1. Scannen des Live-Ethereums von ungeprüften Sendeverträgen


Original - Scannen von Live-Ethereum-Verträgen für das "Unchecked-Send ..."


Autoren: Zikai Alex Wen und Andrew Miller

Das Programmieren intelligenter Verträge in Ethereum ist bekanntermaßen fehleranfällig  [1]  . Kürzlich haben wir mehrere
Smart-End-Verträge der Spitzenklasse , wie  King of the Ether  und  The DAO-1.0 , gesehen, die Sicherheitslücken aufweisen, die durch Programmierfehler verursacht wurden.

Intelligente Vertragsprogrammierer werden seit März 2015 vor den spezifischen Programmiergefahren gewarnt, die entstehen können, wenn Verträge sich gegenseitig Nachrichten senden [6]

In mehreren Programmierhandbüchern wird empfohlen, häufige Fehler zu vermeiden (in offiziellen Ethereum-Dokumenten  [3]  und in einem unabhängigen Handbuch von UMD  [2]). ). Obwohl diese Gefahren hinreichend klar sind, um sie zu vermeiden, sind die Folgen eines solchen Fehlers furchtbar: Geld kann blockiert, verloren oder gestohlen werden.

Wie häufig sind die Fehler, die sich aus diesen Gefahren ergeben? Gibt es noch anfällige, aber aktive Verträge für die Blockkette Ethereum? In diesem Artikel beantworten wir diese Frage, indem wir die Verträge der aktiven Blockkette Ethereum mit einem neuen Analysetool analysieren, das wir entwickelt haben.


Was ist der Fehler "Unchecked-Send"?


Um eine Sendung an eine andere Adresse zu senden, verwenden Sie am besten das Send- Schlüsselwort . Sie fungiert als für jedes Objekt definierte Methode. Der folgende Codeausschnitt kann beispielsweise in einem intelligenten Vertrag gefunden werden, der ein Brettspiel implementiert.


 /*** Listing 1 ***/ 
if (gameHasEnded && !( prizePaidOut ) ) {
  winner.send(1000); // отправить выигрыш победителю
  prizePaidOut = True;
}

Das Problem hierbei ist, dass die  Sendemethode   möglicherweise fehlschlägt. Wenn dies nicht funktioniert, erhält der Gewinner kein Geld, aber die Variable prizePaidOut wird auf True gesetzt.

Es gibt zwei verschiedene Fälle, in denen die winner.send () - Funktion fehlschlagen kann. Wir werden später den Unterschied erkennen. Im ersten Fall handelt es sich bei der Adresse des  Gewinners um einen Vertrag (und nicht um ein Benutzerkonto), und der Code dieses Vertrags generiert eine Ausnahme (beispielsweise, wenn er zu viel "Gas" verwendet). Wenn dies der Fall ist, handelt es sich in diesem Fall möglicherweise um eine "Gewinnerfehler". Der zweite Fall ist weniger offensichtlich. Die virtuelle Maschine von Ethereum verfügt über eine begrenzte Ressource, die als Callstack bezeichnet wird”(Call Stack Depth), und diese Ressource kann von einem anderen Vertragscode verwendet werden, der zuvor in der Transaktion ausgeführt wurde. Wenn Aufrufliste   bereits durch die Zeit des Befehls erschöpft  die Sende  , schlägt die Ausführung Team, egal wie bestimmt den Gewinner . Der Preis des Gewinners wird nicht durch sein Verschulden zerstört! 



Wie können Sie diesen Fehler vermeiden?

Die Ethereum-Dokumentation enthält eine kurze Warnung vor dieser potenziellen Gefahr  [3] : "Bei der Verwendung von send besteht eine gewisse Gefahr  : Die Übertragung schlägt fehl, wenn der Aufrufstack eine Tiefe von 1024 hat (dies kann immer vom Anrufer verursacht werden) und schlägt beim Empfänger fehl endet mit "Gas". Um eine sichere Luftübertragung zu gewährleisten, überprüfen Sie immer den Rückgabewert von  send.  Oder noch besser: Verwenden Sie eine Vorlage, in der der Empfänger Geld abhebt. "

Zwei Sätze Die erste besteht darin, den Rückgabewert von send zu überprüfen,  um zu sehen, ob er erfolgreich abgeschlossen wurde. Wenn nicht, generieren Sie eine Ausnahme, um den Status zurückzusetzen.


  /*** Listing 2 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  if (winner.send(1000))
    prizePaidOut = True;
  else throw;
}

Dies ist eine angemessene Lösung für das aktuelle Beispiel, aber nicht immer die richtige Lösung. Nehmen wir an, wir modifizieren unser Beispiel so, dass der Gewinner und der Verlierer nach Beendigung des Spiels ihren Status zurücksetzen. Eine offensichtliche Verwendung der "offiziellen" Lösung wäre folgende:


/*** Listing 3 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  if (winner.send(1000) && loser.send(10))
    prizePaidOut = True;
  else throw; 
}

Dies ist jedoch ein Fehler, da dadurch eine zusätzliche Sicherheitsanfälligkeit entsteht. Während dieser Code den Gewinner vor einem Callstack- Angriff schützt  , machen er  Gewinner und  Verlierer auch anfällig für einander. In diesem Fall wollen wir den Angriff verhindern Aufrufliste , aber die Ausführung fortgesetzt werden, wenn ein Team  sendet aus irgendeinem Grunde nicht funktioniert.

Daher ist es sogar die beste bewährte Methode (empfohlen in unserem „Programmierhandbuch für Ethereum und Schlange“, obwohl dies auch für Solidity gilt), die Verfügbarkeit der Callstack- Ressource zu überprüfen . Wir können ein Makro callStackIsEmpty () definieren , das einen Fehler zurückgibt , wenn und nur dannCallstack ist leer.


/*** Listing 4 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  if (callStackIsEmpty()) throw;
    winner.send(1000)
    loser.send(10)
    prizePaidOut = True;  
    }

Besser noch, die Empfehlung aus der Ethereum-Dokumentation - "Vorlage verwenden, bei der der Empfänger das Geld nimmt" ist etwas mysteriös, hat aber eine Erklärung. Es wird empfohlen, den Code so zu reorganisieren, dass die Auswirkungen eines  Sendefehlers isoliert werden und nur jeweils ein Empfänger betroffen ist. Unten ist ein Beispiel für diesen Ansatz. Dieser Tipp ist jedoch auch gegen Muster. Es übernimmt die Verantwortung für die Überprüfung des Callstacks durch die Empfänger selbst, so dass sie wahrscheinlich in die gleiche Falle geraten .


/*** Listing 5 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  accounts[winner] += 1000
  accounts[loser] += 10
  prizePaidOut = True;
  }
 ...
function withdraw(amount) {
if (accounts[msg.sender] >= amount) {
  msg.sender.send(amount);
  accounts[msg.sender] -= amount;
  }
}

Viele hochintelligente Verträge sind anfällig. Lotterie "König des Äthers des Throns" - der berühmteste Fall dieses Fehlers  [4]  . Dieser Fehler wurde erst bemerkt, als 200 Airs (zum heutigen Preis mehr als 2.000 US-Dollar) keinen legitimen Lotteriegewinner erzielen konnten. Der entsprechende Code in King of the Ether ähnelt dem Code in Listing 2. Glücklicherweise konnte der Vertragsentwickler in diesem Fall die nicht zusammenhängende Funktion des Vertrags als "manuelle Überschreibung" verwenden, um die festgefahrenen Mittel freizugeben. Ein weniger gewissenhafter Administrator könnte die gleiche Funktion verwenden, um die Luft zu stehlen!


Das Scannen des Live-Ethereums von nicht geprüften Sendeverträgen wird fortgesetzt . Teil 2

Jetzt auch beliebt: