Fast alle von uns haben Google Sheets oder Microsoft Excel verwendet, um Daten für Berechnungen einzugeben. Angenommen, Sie möchten die Namen der Mitarbeiter, ihre Telefonnummern, Titel und das Gehalt, das sie verdienen, eingeben.
In seiner einfachsten Form würde ein Datensatz oder Fall in Tabellen oder Excel so aussehen:
Wie Sie sehen, bestehen sowohl der Name als auch der Titel des Mitarbeiters aus Text, während die Telefonnummer und das Gehalt aus einer Zahlenfolge bestehen.
Aus semantischer Sicht verstehen wir als Menschen also, was diese Felder in der realen Welt bedeuten und können zwischen ihnen unterscheiden.
Auch wenn man keinen Abschluss in Informatik braucht, um den Unterschied zu erkennen, stellt sich die Frage: Wie verarbeitet ein Compiler oder Interpreter diese Daten?
Hier kommen Datentypen ins Spiel, für deren Angabe sich Programmierer entweder die Zeit nehmen oder nicht, abhängig von der Programmiersprache, in der sie programmieren.
Mit anderen Worten: Die Datenpunkte unter dem Namen und dem Titel des Mitarbeiters werden als Zeichenfolgen bezeichnet. Natürlich ist das Gehalt eindeutig eine ganze Zahl, da es keine Dezimalstellen gibt. Einfach ausgedrückt handelt es sich dabei um Datentypen, die beim Codieren als solche deklariert werden müssen, damit nur die richtigen, mit diesem Datentyp verknüpften Vorgänge ausgeführt werden.
So deklarieren wir einen ganzzahligen Datentyp in Solidity:
Allerdings enthält das Feld „Telefonnummer“ in der Tabelle oben einen Datenpunkt, der als eindeutige Zeichenfolge verwendet wird, aber diese Diskussion wird für einen anderen Tag stattfinden. Im Moment konzentrieren wir uns auf den primitiven Datentyp, mit dem wir alle grundlegende Arithmetik durchgeführt haben.
Ja, wir sprechen über den ganzzahligen Datentyp, der zwar für wichtige arithmetische Operationen wichtig ist, aber für jede Berechnung einen begrenzten Bereich hat.
Das wohl bekannteste Beispiel für einen Ganzzahlüberlauf in der realen Welt tritt bei Fahrzeugen auf. Diese auch als Kilometerzähler bezeichneten Geräte zeichnen im Allgemeinen auf, wie viele Kilometer ein Fahrzeug zurückgelegt hat.
Was passiert also, wenn der Wert der zurückgelegten Meilen den vorzeichenlosen Ganzzahlwert 999999 in einem sechsstelligen Kilometerzähler erreicht?
Sobald eine weitere Meile hinzukommt, sollte dieser Wert im Idealfall 1.000.000 erreichen, oder? Dies geschieht jedoch nicht, da eine siebte Ziffer vorgesehen ist.
Stattdessen wird der Wert der zurückgelegten Meilen auf 000000 zurückgesetzt, wie unten gezeigt:
Da die siebte Ziffer nicht verfügbar ist, führt dies per Definition zu einem „Überlauf“, da der genaue Wert nicht dargestellt wird.
Du verstehst das Bild, oder?
Umgekehrt kann auch das Gegenteil eintreten, auch wenn dies nicht so häufig vorkommt. Mit anderen Worten, wenn der aufgezeichnete Wert kleiner ist als der kleinste verfügbare Wert im Bereich, was auch als „Unterlauf“ bezeichnet wird.
Wie wir alle wissen, speichern Computer Ganzzahlen als binäres Äquivalent im Speicher. Nehmen wir der Einfachheit halber an, dass Sie ein 8-Bit-Register verwenden.
= 2⁸*1 + 2⁷*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1
= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 111111111
Wobei jedes Bit 1 ist und wie Sie sehen, können Sie keinen höheren Wert speichern.
Wenn Sie hingegen die Zahl 0 im 8-Bit-Register speichern möchten, würde das so aussehen:
= 2⁸*0 + 2⁷*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 2¹*0 + 2⁰*0
= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
= 000000000
Wobei jedes Bit 0 ist, was Ihnen sagen sollte, dass Sie keinen niedrigeren Wert speichern können.
Mit anderen Worten: Der zulässige Ganzzahlbereich für ein solches 8-Bit-Register liegt zwischen 0 und 511. Ist es also möglich, die Ganzzahl 512 oder -1 in einem solchen Register zu speichern?
Natürlich nicht. Als Ergebnis speichern Sie einen Wert, der dem Rücksetzwert der zurückgelegten Meilen im Beispiel des Kilometerzählers ähnelt, jedoch als Binärwerte.
Natürlich bräuchte man Register mit ein paar Bits mehr, um eine solche Zahl bequem unterzubringen. Andernfalls riskieren Sie erneut die Situation eines Überlaufs.
Im Fall von vorzeichenbehafteten Ganzzahlen speichern wir auch negative Ganzzahlen. Wenn wir also versuchen, eine Zahl zu speichern, die kleiner als der akzeptierte Bereich oder kleiner als Null ist, wie oben gezeigt, kommt es zu einem Unterlauf.
Da es bei jeder Berechnung darum geht, deterministische Ergebnisse zu erhalten, kann dies im besten Fall ärgerlich sein, im schlimmsten Fall jedoch zu einem Verlust von Millionen führen. Insbesondere, wenn diese Integer-Überlauf- oder -Unterlauffehler in Smart Contracts auftreten.
Während es Integer-Overflows und -Underflows schon seit Jahrzehnten gibt, hat ihre Existenz als Fehler in einem Smart Contract das Risiko erhöht. Wenn Angreifer solche Fehler ausnutzen, können sie dem Smart Contract große Mengen an Token entziehen.
Das wahrscheinlich erste Mal, dass ein Fehler dieser Art auftrat, war Block 74638, der Milliarden von Bitcoin für drei Adressen erzeugte. Es würde Stunden dauern, diesen Fehler mithilfe eines Soft Forks zu beheben, der den Block verworfen und damit die Transaktion ungültig gemacht hätte.
Zum einen wurden Transaktionen mit einem Wert von mehr als 21 Millionen Bitcoins abgelehnt. Dies war bei Überlauftransaktionen nicht anders, ähnlich wie bei der Transaktion, bei der so viel Geld auf die drei oben genannten Konten überwiesen wurde.
Allerdings kam es auch bei Ethereum-Smart-Contracts zu Integer-Überläufen und -Unterläufen, wobei BeautyChain ebenfalls ein prominentes Beispiel ist.
In diesem Fall enthielt der Smart Contract eine fehlerhafte Codezeile:
Dadurch konnten die Angreifer theoretisch eine unbegrenzte Menge an BEC-Token erhalten, was theoretisch einem Wert von (2²⁵⁶)-1 entsprechen könnte.
Schauen wir uns nun ein weiteres Beispiel für einen Smart Contract an, bei dem ein ganzzahliger Unter-/Überlauf auftritt.
Auf den ersten Blick interagieren in diesem Beispiel zwei Verträge, was zeigt, was im Fall eines Integer-Überlaufs passiert.
Wie Sie unten sehen können, ermöglicht Ihnen der TimeLock-Vertrag das Ein- und Auszahlen von Geldern, allerdings mit einem Unterschied: Letzteres können Sie erst nach einer bestimmten Zeitspanne ausführen. In diesem Fall können Sie Ihr Geld nur innerhalb einer Woche abheben.
Sobald Sie jedoch die Angriffsfunktion im Angriffsvertrag aufrufen, ist die Zeitsperre nicht mehr wirksam und der Angreifer kann den Guthabenbetrag sofort abheben.
Mit anderen Worten: Da mit der Anweisung type(uint).max+1-timeLock.locktime(address(this)) ein Ganzzahlüberlauf verursacht wird, wird die Zeitsperre aufgehoben.
Wenn Sie beispielsweise beide Smart-Verträge mit dem obigen Code bereitgestellt haben, können Sie testen, ob die Zeitsperre gilt, indem Sie die Ein- und Auszahlungsfunktionen im TimeLock-Vertrag aufrufen, wie unten gezeigt:
Wie Sie sehen können, erhalten wir durch Auswahl eines Betrags von 2 Ether den oben gezeigten Smart-Contract-Saldo von 2 Ether:
Insbesondere kann die spezifische Adresse, die den Saldo von 2 Ether hält, überprüft werden, indem man die Adresse in das Feld der Saldenfunktion einfügt und auf die Salden-Schaltfläche klickt:
Allerdings können Sie diese Gelder, wie oben erwähnt, aufgrund der bestehenden Zeitsperre noch nicht abheben. Wenn Sie auf die Konsole schauen, nachdem Sie auf die Auszahlungsfunktion geklickt haben, werden Sie einen Fehler finden, der durch das rote „x“-Symbol angezeigt wird. Wie Sie unten sehen können, lautet der im Vertrag angegebene Grund für diesen Fehler „Sperrzeit nicht abgelaufen“:
Schauen wir uns nun den bereitgestellten Angriffsvertrag an, wie unten gezeigt:
Um nun die Angriffsfunktion aufzurufen, müssen Sie einen Wert von 1 Ether oder mehr einzahlen. In diesem Fall haben wir also 2 Ether ausgewählt, wie unten gezeigt:
Klicken Sie anschließend auf „Angreifen“. Sie werden feststellen, dass die 2 Ether, die Sie eingezahlt haben, sofort abgehoben und dem Attack-Vertrag hinzugefügt werden, wie aus dem untenstehenden Saldo von 2 Ether hervorgeht:
Das darf natürlich nicht passieren, denn die Langzeitsperre soll bereits mit der Einzahlung in Kraft treten. Wie wir wissen, reduziert die Anweisung type(uint).max+1-timeLock.locktime(address(this)) natürlich die Sperrzeit durch Verwendung der Funktion raiseLockTime. Genau aus diesem Grund können wir das Ether-Guthaben sofort abheben.
Was uns zu der offensichtlichen Frage bringt: Gibt es Möglichkeiten, die Schwachstelle durch Ganzzahlüberlauf und -unterlauf zu beheben?
Da wir erkannt haben, dass die Schwachstelle durch Ganzzahlüberlauf/-unterlauf verheerende Folgen haben kann, wurden einige Korrekturen für diesen Fehler eingeführt. Schauen wir uns beide Korrekturen an und wie sie einen solchen Fehler umgehen:
Open Zeppelin bietet als Organisation viel, wenn es um Cybersicherheitstechnologie und -dienste geht, wobei die SafeMath-Bibliothek Teil seines Repositorys für die Entwicklung intelligenter Verträge ist. Dieses Repo enthält Verträge, die in Ihren Smart-Contract-Code importiert werden können, darunter auch die SafeMath-Bibliothek.
Sehen wir uns an, wie eine der Funktionen in SafeMath.sol auf Ganzzahlüberlauf prüft:
Nachdem nun die Berechnung von a+b erfolgt ist, erfolgt eine Prüfung, ob c<a erfolgt. Dies gilt natürlich nur im Fall eines Integer-Überlaufs.
Da die Compilerversion von Solidity 0.8.0 und höher erreicht, sind nun Überprüfungen auf Ganzzahlüberlauf und -unterlauf integriert. Daher kann man diese Bibliothek weiterhin verwenden, um nach dieser Schwachstelle zu suchen, sowohl wenn man die Sprache als auch diese Bibliothek verwendet. Wenn Ihr Smart Contract eine Compilerversion kleiner als 0.8.+ erfordert, müssen Sie natürlich diese Bibliothek verwenden, um einen Über- oder Unterlauf zu vermeiden.
Wenn Sie nun, wie bereits erwähnt, für Ihren Smart Contract eine Compilerversion ab 0.8.0 verwenden, verfügt diese Version über einen integrierten Prüfer für eine solche Schwachstelle.
Um tatsächlich zu überprüfen, ob es mit dem oben genannten Smart-Vertrag funktioniert, wird beim Ändern der Compiler-Version auf „^0.8.0“ und beim erneuten Bereitstellen der folgende „Zurücksetzen“-Fehler angezeigt:
Selbstverständlich erfolgt keine Einzahlung der 2 Ether, was an der Kontrolle auf den Überlauf des Zeitsperrwertes liegt. Daher ist keine Auszahlung möglich, da überhaupt kein Geld eingezahlt wurde.
Ohne Zweifel hat der Funktionsaufruf Attack.attack() hier nicht funktioniert, also ist alles gut!
Wenn Sie aus diesem langen Blog-Beitrag etwas lernen sollten, dann ist es, dass das Ignorieren dieser Schwachstelle, wie beim BEC-Angriff, sich als kostspielig erweisen kann. Wie Sie auch sehen können, kann es leicht zu nicht böswilligen Fehlern kommen, wenn diese Option nicht aktiviert wird. Oder es ist genauso einfach für Hacker, diese Sicherheitslücke auszunutzen.
Apropos und basierend auf unserem Verständnis, wie der BEC-Angriff stattgefunden hat, kann das Erkennen dieser Schwachstelle dank der angebotenen Korrekturen viel dazu beitragen, Angriffe beim Schreiben Ihrer Smart Contracts zu verhindern. Auch wenn es noch mehrere andere Schwachstellen bei Smart Contracts gibt, die darauf warten, Sie zum Stolpern zu bringen.