
Solidity Security
Dieser Beitrag soll eine relativ ausführliche und aktuelle Einführung sein, die die früheren Fehler von Solidity-Entwicklern detailliert beschreibt, um zukünftige Entwickler davor zu bewahren, die Geschichte zu wiederholen.
Eine der Funktionen von Ethereum-Smart-Contracts ist die Fähigkeit, Code anderer externer Verträge aufzurufen und zu nutzen. Verträge handhaben typischerweise auch Ether und senden daher oft Ether an verschiedene externe Benutzeradressen. Der Vorgang, externe Verträge aufzurufen oder Ether an eine Adresse zu senden, erfordert, dass der Vertrag einen externen Aufruf absetzt. Diese externen Aufrufe können von Angreifern gekapert werden, indem sie den Vertrag zwingen, weiteren Code auszuführen (z. B. über eine Fallback-Funktion), einschließlich Aufrufen zurück zu sich selbst. Somit "tritt" die Codeausführung erneut in den Vertrag ein. Angriffe dieser Art wurden beim berüchtigten DAO-Hack eingesetzt.
Für weiterführende Literatur zu Re-Entrancy-Angriffen siehe Reentrancy Attack On Smart Contracts und Consensus - Ethereum Smart Contract Best Practices.
Dieser Angriff kann auftreten, wenn ein Vertrag Ether an eine unbekannte Adresse sendet. Ein Angreifer kann an einer externen Adresse sorgfältig einen Vertrag konstruieren, der bösartigen Code in der Fallback-Funktion enthält. Wenn ein Vertrag also Ether an diese Adresse sendet, wird der bösartige Code aufgerufen. Typischerweise führt der bösartige Code eine Funktion auf dem verwundbaren Vertrag aus und führt dabei Operationen durch, die der Entwickler nicht erwartet hat. Der Name "Re-Entrancy" rührt daher, dass der externe bösartige Vertrag eine Funktion auf dem verwundbaren Vertrag zurückruft und an einer beliebigen Stelle des verwundbaren Vertrags erneut in die Codeausführung "eintritt".
Zur Veranschaulichung betrachten wir den einfachen verwundbaren Vertrag, der als Ethereum-Tresor fungiert und Einzahlern erlaubt, nur 1 Ether pro Woche abzuheben.
EtherStore.sol:```solidity contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
Dieser Vertrag hat zwei öffentliche Funktionen. `depositFunds()` und `withdrawFunds()`. Die Funktion `depositFunds()` erhöht lediglich das Guthaben des Absenders. Die Funktion `withdrawFunds()` ermöglicht es dem Absender, den Betrag in Wei festzulegen, der abgehoben werden soll. Sie ist nur dann erfolgreich, wenn der angeforderte Abhebungsbetrag weniger als 1 Ether beträgt und in der letzten Woche keine Abhebung stattgefunden hat. Oder etwa nicht?...
Die Schwachstelle liegt in Zeile \[17\], wo wir dem Benutzer den angeforderten Betrag in Ether senden. Stellen Sie sich einen böswilligen Angreifer vor, der den folgenden Vertrag erstellt:
Attack.sol:```solidity
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;
// initialise the etherStore variable with the contract address
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable {
// attack to the nearest ether
require(msg.value >= 1 ether);
// send eth to the depositFunds() function
etherStore.depositFunds.value(1 ether)();
// start the magic
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
// fallback function - where the magic happens
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
Sehen wir uns an, wie dieser bösartige Vertrag unseren EtherStore-Vertrag ausnutzen kann. Der Angreifer würde den obigen Vertrag (sagen wir an der Adresse 0x0...123) mit der Vertragsadresse von EtherStore als Konstruktorparameter erstellen. Dadurch wird die öffentliche Variable etherStore initialisiert und auf den Vertrag gezeigt, den wir angreifen möchten.
Der Angreifer würde dann die Funktion pwnEtherStore() mit einer gewissen Menge Ether (größer oder gleich 1) aufrufen, sagen wir 1 ether für dieses Beispiel. In diesem Beispiel nehmen wir an, dass eine Reihe anderer Benutzer Ether in diesen Vertrag eingezahlt haben, sodass der aktuelle Kontostand 10 ether beträgt. Dabei würde dann Folgendes geschehen:
Attack.sol - Zeile [15] - Die Funktion depositFunds() des EtherStore-Vertrags wird mit einem msg.value von 1 ether (und viel Gas) aufgerufen. Der Absender (msg.sender) wird unser bösartiger Vertrag (0x0...123) sein. Somit gilt balances[0x0..123] = 1 ether.
Attack.sol - Zeile [17] - Der bösartige Vertrag ruft dann die Funktion withdrawFunds() des EtherStore-Vertrags mit einem Parameter von 1 ether auf. Damit werden alle Anforderungen (Zeilen [12]-[16] des EtherStore-Vertrags) erfüllt, da wir keine früheren Abhebungen vorgenommen haben.
EtherStore.sol - Zeile [17] - Der Vertrag sendet dann 1 ether zurück an den bösartigen Vertrag.
Attack.sol - Zeile [25] - Das an den bösartigen Vertrag gesendete Ether führt dann die Fallback-Funktion aus.
Das Endergebnis ist, dass der Angreifer mit einer einzigen Transaktion sofort das gesamte Ether (bis auf 1) aus dem EtherStore-Vertrag abgehoben hat.
Es gibt eine Reihe gängiger Techniken, die helfen, potenzielle Re-Entrancy-Schwachstellen in Smart Contracts zu vermeiden. Die erste besteht darin, (wann immer möglich) die eingebaute Funktion transfer() zu verwenden, wenn Ether an externe Verträge gesendet wird. Die Transferfunktion sendet mit dem externen Aufruf nur 2300 gas, was für die Zieladresse/den Zielvertrag nicht ausreicht, um einen anderen Vertrag aufzurufen (d. h. erneut in den sendenden Vertrag einzutreten).
Die zweite Technik besteht darin, sicherzustellen, dass die gesamte Logik, die Zustandsvariablen ändert, ausgeführt wird, bevor Ether aus dem Vertrag gesendet wird (oder ein externer Aufruf erfolgt). Im EtherStore-Beispiel sollten die Zeilen [18] und [19] von EtherStore.sol vor Zeile [17] stehen. Es ist gute Praxis, jeden Code, der externe Aufrufe an unbekannte Adressen durchführt, als letzte Operation in einer lokalisierten Funktion oder einem Codestück zu platzieren. Dies ist als das checks-effects-interactions-Muster bekannt.
Eine dritte Technik besteht darin, einen Mutex einzuführen. Das heißt, eine Zustandsvariable hinzuzufügen, die den Vertrag während der Codeausführung sperrt und so Reentrancy-Aufrufe verhindert.
Die Anwendung all dieser Techniken (alle drei sind unnötig, werden aber zu Demonstrationszwecken angewendet) auf EtherStore.sol ergibt den re-entrancy-freien Vertrag:```solidity
contract EtherStore {
// initialise the mutex
bool reEntrancyMutex = false;
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
<h3 id="re-example">Praxisbeispiel: The DAO</h3>
[The DAO](https://en.wikipedia.org/wiki/The_DAO_(organization)) (Dezentrale Autonome Organisation) war einer der größten Hacks in der frühen Entwicklungsphase von Ethereum. Zu dieser Zeit hielt der Vertrag über 150 Millionen US-Dollar. Reentrancy spielte eine große Rolle bei dem Angriff, der letztendlich zum Hard Fork führte, der Ethereum Classic (ETC) hervorbrachte. Eine gute Analyse des DAO-Exploits findest du in [Phil Daians Beitrag](http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/).
<h2 id="ouflow"><span id="SP-2">2. Arithmetische Über-/Unterläufe</span></h2>
Die Ethereum Virtual Machine (EVM) definiert Ganzzahl-Datentypen mit fester Größe. Das bedeutet, dass eine Ganzzahlvariable nur einen bestimmten Zahlenbereich darstellen kann. Eine `uint8` kann beispielsweise nur Zahlen im Bereich \[0,255\] speichern. Der Versuch, `256` in eine `uint8` zu speichern, führt zu `0`. Wenn man nicht vorsichtig ist, können Variablen in Solidity ausgenutzt werden, wenn Benutzereingaben ungeprüft sind und Berechnungen durchgeführt werden, die Zahlen ergeben, die außerhalb des Bereichs des sie speichernden Datentyps liegen.
Weiterführende Informationen zu arithmetischen Über-/Unterläufen findest du unter [How to Secure Your Smart Contracts](https://medium.com/loom-network/how-to-secure-your-smart-contracts-6-solidity-vulnerabilities-and-how-to-avoid-them-part-1-c33048d4d17d), [Ethereum Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#integer-overflow-and-underflow) und [Ethereum, Solidity and integer overflows: programming blockchains like 1970](https://randomoracle.wordpress.com/2018/04/27/ethereum-solidity-and-integer-overflows-programming-blockchains-like-1970/).
<h3 id="ou-vuln">Die Schwachstelle</h3>
Ein Über-/Unterlauf tritt auf, wenn eine Operation ausgeführt wird, bei der eine Variable fester Größe eine Zahl (oder ein Datenelement) speichern muss, die außerhalb des Bereichs des Datentyps der Variable liegt.
Zum Beispiel führt das Subtrahieren von `1` von einer `uint8`-Variable (Ganzzahl ohne Vorzeichen mit 8 Bits, also nur positiv), die den Wert `0` speichert, zur Zahl `255`. Dies ist ein Unterlauf. Wir haben der `uint8` eine Zahl zugewiesen, die unterhalb des Bereichs der `uint8` liegt; das Ergebnis *läuft um* und ergibt die größte Zahl, die eine `uint8` speichern kann. Ebenso lässt das Hinzufügen von `2^8=256` zu einer `uint8` die Variable unverändert, da wir die gesamte Länge der `uint` umlaufen haben (für die Mathematiker: ähnlich wie das Hinzufügen von $2\pi$ zum Winkel einer trigonometrischen Funktion, $\sin(x) = \sin(x+2\pi)$). Das Hinzufügen von Zahlen, die größer als der Bereich des Datentyps sind, wird als Überlauf bezeichnet. Zur Verdeutlichung: Das Hinzufügen von `257` zu einer `uint8`, die derzeit den Wert null hat, ergibt die Zahl `1`. Es ist manchmal lehrreich, sich Variablen mit festen Typen als zyklisch vorzustellen, bei denen wir wieder bei null beginnen, wenn wir Zahlen über der größten speicherbaren Zahl hinzufügen, und umgekehrt bei null (wo wir umso mehr von der größten Zahl herunterzählen, je mehr wir von 0 subtrahieren).
Diese Art von numerischen Besonderheiten ermöglicht es Angreifern, Code zu missbrauchen und unerwartete Logikabläufe zu erzeugen. Betrachten wir zum Beispiel den folgenden Zeitverriegelungsvertrag.
TimeLock.sol:```solidity
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = now + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Dieser Vertrag ist so konzipiert, dass er wie eine Zeittresor funktioniert, in den Benutzer Ether einzahlen können, und er dort für mindestens eine Woche gesperrt bleibt. Der Benutzer kann die Wartezeit, wenn er möchte, auf mehr als 1 Woche verlängern, aber nach der Einzahlung kann der Benutzer sicher sein, dass sein Ether für mindestens eine Woche sicher eingeschlossen ist. Oder etwa nicht?...
Falls ein Benutzer gezwungen ist, seinen privaten Schlüssel herauszugeben (man denke an eine Geiselnahme), kann ein solcher Vertrag nützlich sein, um sicherzustellen, dass Ether in kurzen Zeiträumen nicht erlangt werden kann. Wenn ein Benutzer 100 ether in diesem Vertrag gesperrt hätte und seine Schlüssel an einen Angreifer übergeben hätte, könnte ein Angreifer einen Overflow ausnutzen, um das Ether zu erhalten, unabhängig von der lockTime.
Der Angreifer könnte die aktuelle lockTime für die Adresse ermitteln, für die er nun den Schlüssel besitzt (es ist eine öffentliche Variable). Nennen wir diese userLockTime. Er könnte dann die Funktion increaseLockTime aufrufen und als Argument die Zahl 2^256 - userLockTime übergeben. Diese Zahl würde zur aktuellen userLockTime addiert und einen Overflow verursachen, wodurch lockTime[msg.sender] auf 0 zurückgesetzt wird. Der Angreifer könnte dann einfach die Funktion withdraw aufrufen, um seine Belohnung zu erhalten.
Schauen wir uns ein weiteres Beispiel an, diesmal aus den Ethernaut-Herausforderungen.
SPOILER-WARNUNG: Falls du die Ethernaut-Herausforderungen noch nicht absolviert hast, zeigt dies eine Lösung für eines der Level.```solidity pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances; uint public totalSupply;
function Token(uint _initialSupply) { balances[msg.sender] = totalSupply = _initialSupply; }
function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; }
function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } }
Dies ist ein einfacher Token-Vertrag, der eine `transfer()`-Funktion verwendet, die es Teilnehmern ermöglicht, ihre Token zu verschieben. Erkennst du den Fehler in diesem Vertrag?
Der Fehler liegt in der `transfer()`-Funktion. Die `require`-Anweisung in Zeile \[13\] kann durch einen Underflow umgangen werden. Betrachte einen Benutzer, der kein Guthaben hat. Er könnte die `transfer()`-Funktion mit einem beliebigen `_value` ungleich Null aufrufen und die `require`-Anweisung in Zeile \[13\] passieren. Das liegt daran, dass `balances[msg.sender]` Null ist (und ein `uint256`), sodass das Subtrahieren eines beliebigen positiven Betrags (außer `2^256`) aufgrund des oben beschriebenen Underflows eine positive Zahl ergibt. Dies gilt auch für Zeile \[14\], in der unser Guthaben mit einer positiven Zahl belastet wird. In diesem Beispiel haben wir also durch eine Underflow-Schwachstelle kostenlose Token erhalten.
<h3 id="ou-prevention">Präventive Techniken</h3>
Die (derzeit) übliche Technik, um sich vor Unter-/Überlauf-Schwachstellen zu schützen, besteht darin, mathematische Bibliotheken zu verwenden oder zu erstellen, die die Standard-Mathe-Operatoren ersetzen: Addition, Subtraktion und Multiplikation (Division ist ausgenommen, da sie keine Über-/Unterläufe verursacht und die EVM bei Division durch 0 einen Revert ausführt).
[OppenZepplin](https://github.com/OpenZeppelin/zeppelin-solidity) haben hervorragende Arbeit bei der Erstellung und Prüfung sicherer Bibliotheken geleistet, die von der Ethereum-Community genutzt werden können. Insbesondere ihre [Safe Math Library](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/math/SafeMath.sol) ist eine Referenz bzw. Bibliothek, um Unter-/Überlauf-Schwachstellen zu vermeiden.
Um zu demonstrieren, wie diese Bibliotheken in Solidity verwendet werden, korrigieren wir den `TimeLock`-Vertrag mithilfe der `SafeMath`-Bibliothek von Open Zepplin. Der überlauffreie Vertrag würde wie folgt aussehen:```solidity
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract TimeLock {
using SafeMath for uint; // use the library for uint type
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
function deposit() public payable {
balances[msg.sender] = balances[msg.sender].add(msg.value);
lockTime[msg.sender] = now.add(1 weeks);
}
function increaseLockTime(uint256 _secondsToIncrease) public {
lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Beachte, dass alle standardmäßigen mathematischen Operationen durch die in der SafeMath-Bibliothek definierten ersetzt wurden. Der TimeLock-Vertrag führt keine Operation mehr aus, die einen Unter- oder Überlauf verursachen könnte.
Eine 4chan-Gruppe hielt es für eine großartige Idee, ein Ponzi-Schema auf Ethereum zu bauen, geschrieben in Solidity. Sie nannten es Proof of Weak Hands Coin (PoWHC). Leider scheint es, dass die Autoren des Vertrags zuvor nie Über-/Unterläufe gesehen hatten, und infolgedessen wurden 866 Ether aus dem Vertrag entwendet. Einen guten Überblick darüber, wie der Unterlauf entsteht (der dem obigen Ethernaut-Challenge nicht unähnlich ist), gibt Eric Banisadars Beitrag.
Einige Entwickler implementierten außerdem eine batchTransfer()-Funktion in einige ERC20-Tokenverträge. Die Implementierung enthielt einen Überlauf. Dieser Beitrag erklärt das, allerdings finde ich den Titel irreführend, da er nichts mit dem ERC20-Standard zu tun hat, sondern einige ERC20-Tokenverträge eine verwundbare batchTransfer()-Funktion implementiert haben.
Normalerweise muss ein Vertrag, wenn Ether an ihn gesendet wird, entweder die Fallback-Funktion oder eine andere im Vertrag beschriebene Funktion ausführen. Es gibt zwei Ausnahmen, bei denen Ether in einem Vertrag vorhanden sein kann, ohne dass irgendein Code ausgeführt wurde. Verträge, die bei jedem gesendeten Ether auf Codeausführung angewiesen sind, können anfällig für Angriffe sein, bei denen Ether gewaltsam an einen Vertrag gesendet wird.
Weiterführende Literatur hierzu: So sichern Sie Ihre Smart Contracts: Teil 6 und Solidity-Sicherheitsmuster - Ether gewaltsam an einen Vertrag senden .
Eine gängige defensive Programmiertechnik, die nützlich ist, um korrekte Zustandsübergänge durchzusetzen oder Operationen zu validieren, ist die Invariantenprüfung. Diese Technik besteht darin, eine Reihe von Invarianten (Metriken oder Parameter, die sich nicht ändern sollten) zu definieren und zu prüfen, dass diese Invarianten nach einer (oder vielen) Operationen unverändert bleiben. Dies ist in der Regel gutes Design, vorausgesetzt, die geprüften Invarianten sind tatsächlich Invarianten. Ein Beispiel für eine Invariante ist der totalSupply eines ERC20-Tokens mit fester Ausgabe. Da keine Funktionen diese Invariante verändern sollten, könnte man eine Prüfung in der transfer()-Funktion hinzufügen, die sicherstellt, dass der totalSupply unverändert bleibt, um zu gewährleisten, dass die Funktion wie erwartet funktioniert.
Insbesondere gibt es eine scheinbare Invariante, die verlockend zu verwenden sein mag,
aber tatsächlich von externen Benutzern manipuliert werden kann (ungeachtet der Regeln, die
im Smart Contract festgelegt sind) .Dies ist das aktuelle Ether, das im
Vertrag gespeichert ist. Wenn Entwickler Solidity zum ersten Mal lernen, haben sie oft die
falsche Vorstellung, dass ein Vertrag Ether nur über payable
Funktionen akzeptieren oder erhalten kann. Diese falsche Vorstellung kann zu Verträgen führen, die falsche
Annahmen über das Ether-Guthaben in ihnen haben, was zu einer Reihe von
Schwachstellen führen kann. Der eindeutige Beweis für diese Schwachstelle ist die (falsche) Verwendung
von this.balance. Wie wir sehen werden, können falsche Verwendungen von this.balance zu
ernsthaften Schwachstellen dieser Art führen.
Es gibt zwei Möglichkeiten, wie Ether (gewaltsam) an einen Vertrag gesendet werden kann, ohne eine payable-Funktion zu verwenden oder irgendeinen Code im Vertrag auszuführen. Diese werden im Folgenden aufgeführt.
Jeder Vertrag kann die Funktion selfdestruct(address) implementieren, die den gesamten Bytecode von der Vertragsadresse entfernt und alle dort gespeicherten Ether an die im Parameter angegebene Adresse sendet. Wenn diese angegebene Adresse ebenfalls ein Vertrag ist, wird keine Funktion (einschließlich der Fallback-Funktion) aufgerufen. Daher kann die selfdestruct()-Funktion verwendet werden, um Ether gewaltsam an jeden beliebigen Vertrag zu senden, unabhängig von jeglichem Code, der in dem Vertrag vorhanden sein könnte. Dies schließt Verträge ohne payable-Funktionen ein. Das bedeutet, jeder Angreifer kann einen Vertrag mit einer selfdestruct()-Funktion erstellen, Ether an ihn senden, selfdestruct(target) aufrufen und so erzwingen, dass Ether an einen target-Vertrag gesendet wird. Martin Swende hat einen ausgezeichneten Blogbeitrag, in dem er einige Eigenheiten des Self-Destruct-Opcodes (Quirk #2) beschreibt, zusammen mit einer Beschreibung, wie Client-Knoten falsche Invarianten prüften, was zu einer ziemlich katastrophalen Zerstörung von Clients hätte führen können.
Der zweite Weg, auf dem ein Vertrag Ether erhalten kann, ohne eine selfdestruct()-Funktion zu verwenden oder payable-Funktionen aufzurufen, besteht darin, die Vertragsadresse vorab mit Ether zu beladen. Vertragsadressen sind deterministisch; die Adresse wird tatsächlich aus dem keccak256-Hash (manchmal synonym mit SHA3) der Adresse, die den Vertrag erstellt, und der Transaktionsnonce, die den Vertrag erstellt, berechnet. Genauer gesagt hat sie die Form: address = sha3(rlp.encode([account_address,transaction_nonce])) (siehe Schlüsselloses Ether für einige unterhaltsame Anwendungsfälle hierfür). Das bedeutet, jeder kann berechnen, wie eine Vertragsadresse aussehen wird, bevor sie erstellt wird, und somit Ether an diese Adresse senden. Wenn der Vertrag schließlich erstellt wird, hat er ein Ether-Guthaben ungleich null.
Lassen Sie uns einige Fallstricke untersuchen, die sich aus dem obigen Wissen ergeben können.
Betrachten Sie den äußerst einfachen Vertrag,
EtherGame.sol:```solidity contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
mapping(address => uint) redeemableEther;
// users pay 0.5 ether. At specific milestones, credit their accounts
function play() public payable {
require(msg.value == 0.5 ether); // each play is 0.5 ether
uint currentBalance = this.balance + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
// if at a milestone credit the players account
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
return;
}
function claimReward() public {
// ensure the game is complete
require(this.balance == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Dieser Vertrag stellt ein einfaches Spiel dar (das natürlicherweise [Race-Conditions](#race-conditions)) hervorruft, bei dem Spieler `0.5 ether`-Quanten an den Vertrag senden, in der Hoffnung, der Spieler zu sein, der zuerst einen von drei Meilensteinen erreicht. Die Meilensteine sind in Ether angegeben. Der Erste, der den Meilenstein erreicht, kann einen Teil des Ethers beanspruchen, wenn das Spiel beendet ist. Das Spiel endet, wenn der letzte Meilenstein (`10 ether`) erreicht ist, und die Benutzer können ihre Belohnungen beanspruchen.
Die Probleme mit dem Vertrag `EtherGame` entstehen aus der schlechten Verwendung von `this.balance` in den Zeilen \[14\] (und damit verbunden \[16\]) und \[32\]. Ein böswilliger Angreifer könnte gewaltsam eine kleine Menge Ether senden, sagen wir `0.1 ether`, über die Funktion `selfdestruct()` (weiter oben besprochen), um zu verhindern, dass zukünftige Spieler einen Meilenstein erreichen. Da alle legitimen Spieler nur `0.5 ether`-Schritte senden können, wäre `this.balance` nicht länger eine halbe Ganzzahl, da zusätzlich der `0.1 ether`-Anteil enthalten wäre. Dies verhindert, dass alle if-Bedingungen in den Zeilen \[18\], \[21\] und \[24\] wahr werden.
Noch schlimmer: Ein rachsüchtiger Angreifer, der einen Meilenstein verpasst hat, könnte gewaltsam `10 ether` senden (oder eine entsprechende Menge Ether, die das Vertragsguthaben über den `finalMileStone` treibt), wodurch alle Belohnungen für immer im Vertrag gesperrt würden. Der Grund ist, dass die Funktion `claimReward()` immer zurückweisen wird, aufgrund der require-Anweisung in Zeile \[32\] (d.h. `this.balance` ist größer als `finalMileStone`).
<h3 id="ether-prevention">Vorbeugende Techniken</h3>
Diese Schwachstelle entsteht typischerweise durch den Missbrauch von `this.balance`. Die Vertragslogik sollte, wann immer möglich, vermeiden, von exakten Werten des Vertragsguthabens abhängig zu sein, da dieses künstlich manipuliert werden kann. Wenn Logik auf Basis von `this.balance` angewendet wird, sollte man unerwartete Guthaben berücksichtigen.
Wenn exakte Werte des eingezahlten Ethers erforderlich sind, sollte eine selbst definierte Variable verwendet werden, die in payable-Funktionen erhöht wird, um den eingezahlten Ether sicher zu verfolgen. Diese Variable wird nicht durch den erzwungenen Ether beeinflusst, der über einen `selfdestruct()`-Aufruf gesendet wird.
Vor diesem Hintergrund könnte eine korrigierte Version des Vertrags `EtherGame` wie folgt aussehen:```solidity
contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
uint public depositedWei;
mapping (address => uint) redeemableEther;
function play() public payable {
require(msg.value == 0.5 ether);
uint currentBalance = depositedWei + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
depositedWei += msg.value;
return;
}
function claimReward() public {
// ensure the game is complete
require(depositedWei == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Hier haben wir gerade eine neue Variable erstellt, depositedWei, die den bekannten eingezahlten Ether nachverfolgt, und an dieser Variable führen wir unsere Anforderungen und Tests durch. Beachte, dass wir keinen Bezug mehr auf this.balance haben.
Ich habe noch kein Beispiel dafür gefunden, das in freier Wildbahn ausgenutzt wurde. Allerdings wurden im Underhanded Solidity Contest einige Beispiele für ausnutzbare Verträge gezeigt.
Die Opcodes CALL und DELEGATECALL sind nützlich, um Ethereum-Entwicklern die Modularisierung ihres Codes zu ermöglichen. Standardmäßige externe Nachrichtenaufrufe an Verträge werden über den Opcode CALL abgewickelt, wobei der Code im Kontext des externen Vertrags/der externen Funktion ausgeführt wird. Der Opcode DELEGATECALL ist mit dem standardmäßigen Nachrichtenaufruf identisch, außer dass der Code, der an der Zieladresse ausgeführt wird, im Kontext des aufrufenden Vertrags läuft, wobei msg.sender und msg.value unverändert bleiben. Diese Funktion ermöglicht die Implementierung von Bibliotheken, mit denen Entwickler wiederverwendbaren Code für zukünftige Verträge erstellen können.
Obwohl die Unterschiede zwischen diesen beiden Opcodes einfach und intuitiv sind, kann die Verwendung von DELEGATECALL zu unerwarteter Codeausführung führen.
Für weiterführende Lektüre siehe Ethereum Stack Exchange Question, Solidity Docs und How to Secure Your Smart Contracts: 6.
Die kontextbewahrende Natur von DELEGATECALL hat bewiesen, dass der Aufbau schwachstellenfreier benutzerdefinierter Bibliotheken nicht so einfach ist, wie man denken könnte. Der Code in Bibliotheken selbst kann sicher und schwachstellenfrei sein, aber wenn er im Kontext einer anderen Anwendung ausgeführt wird, können neue Schwachstellen entstehen. Sehen wir uns ein ziemlich komplexes Beispiel dafür an, das Fibonacci-Zahlen verwendet.
Betrachten wir die folgende Bibliothek, die die Fibonacci-Folge und Folgen ähnlicher Form erzeugen kann.
FibonacciLib.sol[^1]```solidity
// library contract - calculates fibonacci-like numbers;
contract FibonacciLib {
// initializing the standard fibonacci sequence;
uint public start;
uint public calculatedFibNumber;
// modify the zeroth number in the sequence
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Diese Bibliothek stellt eine Funktion bereit, die die *n*-te Fibonacci-Zahl in der Sequenz erzeugen kann. Sie ermöglicht es Benutzern, die Startzahl der Sequenz (`start`) zu ändern und die *n*-ten fibonacci-ähnlichen Zahlen in dieser neuen Sequenz zu berechnen.
Betrachten wir nun einen Vertrag, der diese Bibliothek nutzt.
`FibonacciBalance.sol`:```solidity
contract FibonacciBalance {
address public fibonacciLibrary;
// the current fibonacci number to withdraw
uint public calculatedFibNumber;
// the starting fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
// the fibonancci function selector
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) public payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the fibonacci number for the current withdrawal user
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
Dieser Vertrag erlaubt einem Teilnehmer, Ether vom Vertrag abzuheben, wobei der Betrag des Ethers der Fibonacci-Zahl entspricht, die der Abhebungsreihenfolge des Teilnehmers zugeordnet ist; d. h. der erste Teilnehmer erhält 1 Ether, der zweite ebenfalls 1, der dritte 2, der vierte 3, der fünfte 5 und so weiter (bis der Kontostand des Vertrags kleiner ist als die abzuhebende Fibonacci-Zahl).
Es gibt eine Reihe von Elementen in diesem Vertrag, die möglicherweise einer Erklärung bedürfen. Zunächst gibt es eine interessant aussehende Variable, fibSig. Sie enthält die ersten 4 Bytes des Keccak- (SHA-3-) Hashs des Strings "setFibonacci(uint256)". Dies ist als Funktions-Selektor bekannt und wird in calldata abgelegt, um festzulegen, welche Funktion eines Smart Contracts aufgerufen wird. Er wird in der delegatecall-Funktion in Zeile [21] verwendet, um anzugeben, dass wir die Funktion setFibonacci(uint256) ausführen möchten. Das zweite Argument in delegatecall ist der Parameter, den wir an die Funktion übergeben. Zweitens nehmen wir an, dass die Adresse für die FibonacciLib-Bibliothek im Konstruktor korrekt referenziert wird (Abschnitt Externe Vertragsreferenzierung erörtert einige potenzielle Schwachstellen in Bezug auf diese Art der Initialisierung von Vertragsreferenzen).
Können Sie einen oder mehrere Fehler in diesem Vertrag erkennen? Wenn Sie dies in Remix einfügen, mit Ether befüllen und withdraw() aufrufen, wird es wahrscheinlich einen Revert auslösen.
Sie haben vielleicht bemerkt, dass die Zustandsvariable start sowohl in der Bibliothek als auch im Hauptvertrag (dem aufrufenden Vertrag) verwendet wird. Im Bibliotheksvertrag wird start verwendet, um den Beginn der Fibonacci-Sequenz festzulegen, und ist auf 0 gesetzt, während sie im FibonacciBalance-Vertrag auf 3 gesetzt ist. Ihnen ist außerdem vielleicht aufgefallen, dass die Fallback-Funktion im FibonacciBalance-Vertrag es erlaubt, alle Aufrufe an den Bibliotheksvertrag weiterzuleiten, wodurch auch die Funktion setStart() des Bibliotheksvertrags aufgerufen werden kann. Wenn man bedenkt, dass wir den Zustand des Vertrags beibehalten, könnte es scheinen, dass diese Funktion es ermöglicht, den Zustand der Variable start im lokalen FibonnacciBalance-Vertrag zu ändern. Wenn das so wäre, könnte man mehr Ether abheben, da die resultierende calculatedFibNumber von der Variable start abhängt (wie im Bibliotheksvertrag zu sehen ist). Tatsächlich modifiziert die Funktion setStart() die Variable im -Vertrag nicht (und kann es auch nicht). Die zugrunde liegende Schwachstelle in diesem Vertrag ist erheblich gravierender als nur die Änderung der Variable .
Bevor wir uns dem eigentlichen Problem zuwenden, machen wir einen kurzen Abstecher, um zu verstehen, wie Zustandsvariablen (storage-Variablen) tatsächlich in Verträgen gespeichert werden. Zustands- bzw. storage-Variablen (Variablen, die über einzelne Transaktionen hinweg bestehen bleiben) werden nacheinander in Slots abgelegt, in der Reihenfolge, in der sie im Vertrag eingeführt werden. (Es gibt hier einige Komplexitäten, und ich empfehle dem Leser, Layout von Zustandsvariablen im Storage zu lesen, um ein gründlicheres Verständnis zu erlangen).
Schauen wir uns als Beispiel den Bibliotheksvertrag an. Er hat zwei Zustandsvariablen, start und calculatedFibNumber. Die erste Variable ist start, sie wird daher im Speicher des Vertrags in slot[0] (d. h. im ersten Slot) abgelegt. Die zweite Variable, calculatedFibNumber, wird im nächsten verfügbaren Speicher-Slot, slot[1], platziert. Wenn wir uns die Funktion setStart() ansehen, nimmt sie eine Eingabe entgegen und setzt start auf genau diese Eingabe. Diese Funktion setzt also slot[0] auf die Eingabe, die wir in der Funktion setStart() angeben. In ähnlicher Weise setzt die Funktion setFibonacci() calculatedFibNumber auf das Ergebnis von fibonacci(n). Auch hier wird einfach Speicher auf den Wert von gesetzt.
Betrachten wir nun den FibonacciBalance-Vertrag. Speicher slot[0] entspricht nun der Adresse fibonacciLibrary und slot[1] entspricht calculatedFibNumber. Genau in dieser falschen Zuordnung liegt die Schwachstelle. delegatecall behält den Vertragskontext bei. Das bedeutet, dass Code, der über delegatecall ausgeführt wird, auf den Zustand (d. h. den Speicher) des aufrufenden Vertrags wirkt.
Beachten Sie nun, dass wir in withdraw() in Zeile [21] fibonacciLibrary.delegatecall(fibSig,withdrawalCounter) ausführen. Dies ruft die Funktion setFibonacci() auf, die – wie wir besprochen haben – Speicher slot[1] modifiziert, was in unserem aktuellen Kontext calculatedFibNumber ist. Das ist wie erwartet (d. h. nach der Ausführung wird calculatedFibNumber angepasst). Erinnern Sie sich jedoch daran, dass die Variable start im FibonacciLib-Vertrag in Speicher slot[0] liegt, was im aktuellen Vertrag die Adresse fibonacciLibrary ist. Das bedeutet, dass die Funktion fibonacci() ein unerwartetes Ergebnis liefern wird. Dies liegt daran, dass sie auf start (slot[0]) verweist, was im aktuellen Aufrufkontext die Adresse ist (die, als interpretiert, oft recht groß sein wird). Daher wird die Funktion wahrscheinlich einen Revert auslösen, da sie nicht über die Menge an Ether in Höhe von verfügt, die zurückgeben wird.
Noch schlimmer: Der FibonacciBalance-Vertrag erlaubt es Benutzern, alle Funktionen von fibonacciLibrary über die Fallback-Funktion in Zeile [26] aufzurufen. Wie wir bereits besprochen haben, gehört dazu auch die Funktion setStart(). Wir haben besprochen, dass diese Funktion es jedem erlaubt, Speicher slot[0] zu modifizieren oder zu setzen. In diesem Fall ist Speicher slot[0] die Adresse fibonacciLibrary. Ein Angreifer könnte daher einen bösartigen Vertrag erstellen (ein Beispiel dafür ist unten angegeben), die Adresse in einen uint umwandeln (das lässt sich in Python einfach mit int('<address>',16) bewerkstelligen) und dann setStart(<attack_contract_address_as_uint>) aufrufen. Dadurch wird fibonacciLibrary auf die Adresse des Angriffsvertrags geändert. Wenn dann ein Benutzer withdraw() oder die Fallback-Funktion aufruft, wird der bösartige Vertrag ausgeführt (der den gesamten Kontostand des Vertrags stehlen kann), weil wir die tatsächliche Adresse von geändert haben. Ein Beispiel für einen solchen Angriffsvertrag wäre:```solidity
contract Attack {
uint storageSlot0; // corresponds to fibonacciLibrary
uint storageSlot1; // corresponds to calculatedFibNumber
// fallback - this will run if a specified function is not found
function() public {
storageSlot1 = 0; // we set calculatedFibNumber to 0, so that if withdraw
// is called we don't send out any ether.
<attacker_address>.transfer(this.balance); // we take all the ether
}
}
Beachten Sie, dass dieser Angriffsvertrag die `calculatedFibNumber` ändert, indem er den Storage-Slot `slot[1]` verändert. Im Prinzip könnte ein Angreifer jeden beliebigen anderen Storage-Slot ändern, um alle möglichen Angriffe auf diesen Vertrag auszuführen. Ich ermutige alle Leser, diese Verträge in [Remix](https://remix.ethereum.org) zu laden und mit verschiedenen Angriffsverträgen und Zustandsänderungen über diese `delegatecall`-Funktionen zu experimentieren.
Es ist auch wichtig zu beachten, dass wir, wenn wir sagen, dass `delegatecall` zustandserhaltend ist, nicht über die Variablennamen des Vertrags sprechen, sondern über die tatsächlichen Storage-Slots, auf die diese Namen zeigen. Wie Sie an diesem Beispiel sehen können, kann ein einfacher Fehler dazu führen, dass ein Angreifer den gesamten Vertrag und sein Ether übernimmt.
<h3 id="dc-prevention">Präventionstechniken</h3>
Solidity bietet das Schlüsselwort `library` zur Implementierung von Library-Verträgen (weitere Details finden Sie in der [Solidity-Dokumentation](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#libraries)). Dadurch wird sichergestellt, dass der Library-Vertrag zustandslos und nicht selbstzerstörbar ist. Die Erzwingung von Zustandslosigkeit bei Libraries verringert die Komplexität des Storage-Kontexts, die in diesem Abschnitt demonstriert wurde. Zustandslose Libraries verhindern außerdem Angriffe, bei denen Angreifer den Zustand der Library direkt ändern, um die Verträge zu beeinflussen, die vom Code der Library abhängen. Als allgemeine Faustregel gilt: Achten Sie bei der Verwendung von `DELEGATECALL` sorgfältig auf den möglichen Aufrufkontext sowohl des Library-Vertrags als auch des aufrufenden Vertrags und erstellen Sie, wann immer möglich, zustandslose Libraries.
<h3 id="dc-example">Praxisbeispiel: Parity Multisig Wallet (zweiter Hack)</h3>
Der zweite Hack der Parity Multisig Wallet ist ein Beispiel dafür, wie der Kontext von gut geschriebenem Library-Code ausgenutzt werden kann, wenn er in einem nicht vorgesehenen Kontext ausgeführt wird. Es gibt eine Reihe guter Erklärungen für diesen Hack, etwa diese Übersicht: [Parity MultiSig Hacked. Again](https://medium.com/chain-cloud-company-blog/parity-multisig-hack-again-b46771eaa838) von Anthony Akentiev, diese [Stack-Exchange-Frage](https://ethereum.stackexchange.com/questions/30128/explanation-of-parity-library-suicide/30130) und [An In-Depth Look at the Parity Multisig Bug](http://hackingdistributed.com/2017/07/22/deep-dive-parity-bug/).
Um diese Referenzen zu ergänzen, wollen wir uns die Verträge ansehen, die ausgenutzt wurden. Der Library- und der Wallet-Vertrag sind auf dem Parity-GitHub [hier](https://github.com/paritytech/parity/blob/b640df8fbb964da7538eef268dffc125b081a82f/js/src/contracts/snippets/enhanced-wallet.sol) zu finden.
Schauen wir uns die relevanten Aspekte dieses Vertrags an. Hier sind zwei Verträge von Interesse enthalten: der Library-Vertrag und der Wallet-Vertrag.
Der Library-Vertrag,```solidity
contract WalletLibrary is WalletEvents {
...
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
...
}
und der Wallet-Vertrag,```solidity contract Wallet is WalletEvents {
...
// METHODS
// gets called when no other function matches function() payable { // just being sent some cash? if (msg.value > 0) Deposit(msg.sender, msg.value); else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); }
...
// FIELDS address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; }
Beachte, dass der `Wallet`-Vertrag im Wesentlichen alle Aufrufe über einen Delegate-Call an den `WalletLibrary`-Vertrag weitergibt. Die Konstante `_walletLibrary`-Adresse in diesem Codeausschnitt dient als Platzhalter für den tatsächlich bereitgestellten `WalletLibrary`-Vertrag (der sich unter `0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4` befand).
Der beabsichtigte Zweck dieser Verträge war es, einen einfachen, kostengünstig bereitstellbaren `Wallet`-Vertrag zu haben, dessen Codebasis und Hauptfunktionalität sich im `WalletLibrary`-Vertrag befand. Leider ist der `WalletLibrary`-Vertrag selbst ein Vertrag und verwaltet seinen eigenen Zustand. Erkennst du, warum dies ein Problem sein könnte?
Es ist möglich, Aufrufe an den `WalletLibrary`-Vertrag selbst zu senden. Insbesondere konnte der `WalletLibrary`-Vertrag initialisiert und in Besitz genommen werden. Ein Benutzer tat dies, indem er die Funktion `initWallet()` auf dem `WalletLibrary`-Vertrag aufrief und so zum Eigentümer des Library-Vertrags wurde. Derselbe Benutzer rief anschließend die Funktion `kill()` auf. Da der Benutzer ein Eigentümer des Library-Vertrags war, wurde die Modifier-Prüfung bestanden und der Library-Vertrag führte die Selbstzerstörung aus. Da alle existierenden `Wallet`-Verträge auf diesen Library-Vertrag verweisen und keine Methode enthalten, um diesen Verweis zu ändern, geht ihre gesamte Funktionalität, einschließlich der Möglichkeit, Ether abzuheben, zusammen mit dem `WalletLibrary`-Vertrag verloren. Noch direkter gesagt: Sämtliches Ether in allen Parity-Multi-Sig-Wallets dieser Art geht sofort verloren oder ist dauerhaft unwiederbringlich.
<h2 id="visibility"><span id="SP-5">5. Standard-Sichtbarkeiten</span></h2>
Funktionen in Solidity haben Sichtbarkeitsspezifizierer, die festlegen, wie Funktionen aufgerufen werden dürfen. Die Sichtbarkeit bestimmt, ob eine Funktion extern von Benutzern, von anderen abgeleiteten Verträgen, nur intern oder nur extern aufgerufen werden kann. Es gibt vier Sichtbarkeitsspezifizierer, die im Detail in der [Solidity-Dokumentation](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#visibility-and-getters) beschrieben werden. Funktionen sind standardmäßig als `public` definiert, sodass Benutzer sie extern aufrufen können. Eine falsche Verwendung von Sichtbarkeitsspezifizierern kann zu verheerenden Schwachstellen in Smart Contracts führen, wie in diesem Abschnitt erörtert wird.
<h3 id="visibility-vuln">Die Schwachstelle</h3>
Die Standard-Sichtbarkeit für Funktionen ist `public`. Daher sind Funktionen, die keine Sichtbarkeit angeben, für externe Benutzer aufrufbar. Das Problem tritt auf, wenn Entwickler versehentlich Sichtbarkeitsspezifizierer bei Funktionen ignorieren, die privat (oder nur innerhalb des Vertrags selbst aufrufbar) sein sollten.
Betrachten wir kurz ein triviales Beispiel.```solidity
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() {
msg.sender.transfer(this.balance);
}
}
Dieser einfache Vertrag ist als ein Prämienspiel zum Raten von Adressen konzipiert. Um das Guthaben des Vertrags zu gewinnen, muss ein Nutzer eine Ethereum-Adresse erzeugen, deren letzte 8 Hexadezimalzeichen 0 sind. Sobald eine solche Adresse gefunden wurde, kann er die Funktion WithdrawWinnings() aufrufen, um seine Prämie zu erhalten.
Leider wurde die Sichtbarkeit der Funktionen nicht angegeben. Insbesondere ist die Funktion _sendWinnings() public, sodass jede Adresse diese Funktion aufrufen kann, um die Prämie zu stehlen.
Es ist gute Praxis, die Sichtbarkeit aller Funktionen in einem Vertrag immer anzugeben, selbst wenn sie absichtlich public sind. Neuere Versionen von Solidity zeigen bei der Kompilierung nun Warnungen für Funktionen an, für die keine explizite Sichtbarkeit festgelegt wurde, um diese Praxis zu fördern.
Beim ersten Parity-Multi-Sig-Hack wurde Ether im Wert von etwa $31M hauptsächlich aus drei Wallets gestohlen. Eine gute Zusammenfassung, wie genau dies geschah, liefert Haseeb Qureshi in diesem Beitrag.
Im Wesentlichen ist die Multi-Sig-Wallet (die hier zu finden ist) aus einem Basisvertrag Wallet aufgebaut, der einen Bibliotheksvertrag mit der Kernfunktionalität aufruft (wie in Praxisbeispiel: Parity Multisig (Zweiter Hack) beschrieben). Der Bibliotheksvertrag enthält den Code zum Initialisieren der Wallet, wie aus dem folgenden Codeausschnitt ersichtlich ist```solidity
contract WalletLibrary is WalletEvents {
...
// METHODS
...
// constructor is given number of sigs required to do protected "onlymanyowners" transactions // as well as the selection of addresses capable of confirming them. function initMultiowned(address[] _owners, uint _required) { m_numOwners = _owners.length + 1; m_owners[1] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = 1; for (uint i = 0; i < _owners.length; ++i) { m_owners[2 + i] = uint(_owners[i]); m_ownerIndex[uint(_owners[i])] = 2 + i; } m_required = _required; }
...
// constructor - just pass on the owner array to the multiowned and // the limit to daylimit function initWallet(address[] _owners, uint _required, uint _daylimit) { initDaylimit(_daylimit); initMultiowned(_owners, _required); } }
Notice that neither of the functions have explicitly specified a visibility. Both functions default to `public`. The `initWallet()` function is called in the wallets constructor and sets the owners for the multi-sig wallet as can be seen in the `initMultiowned()` function. Because these functions were accidentally left `public`, an attacker was able to call these functions on deployed contracts, resetting the ownership to the attackers address. Being the owner, the attacker then drained the wallets of all their ether, to the tune of \$31M.
<h2 id="entropy"><span id="SP-6">6. Entropie-Illusion</span></h2>
Alle Transaktionen auf der Ethereum-Blockchain sind deterministische Zustandsübergangsoperationen. Das bedeutet, dass jede Transaktion den globalen Zustand des Ethereum-Ökosystems verändert, und zwar auf berechenbare Weise ohne Unsicherheit. Das bedeutet letztlich, dass es innerhalb des Blockchain-Ökosystems keine Quelle für Entropie oder Zufälligkeit gibt. Es gibt keine `rand()`-Funktion in Solidity. Dezentrale Entropie (Zufälligkeit) zu erreichen, ist ein bekanntes Problem, und viele Ideen wurden vorgeschlagen, um es zu lösen (siehe zum Beispiel [RandDAO](https://github.com/randao/randao) oder die Verwendung einer Hash-Kette, wie von Vitalik in diesem [Post](https://vitalik.ca/files/randomness.html) beschrieben).
<h3 id="entropy-vuln">Die Schwachstelle</h3>
Einige der ersten Verträge, die auf der Ethereum-Plattform erstellt wurden, basierten auf Glücksspiel. Im Grunde erfordert Glücksspiel Unsicherheit (etwas, auf das man wetten kann), was es ziemlich schwierig macht, ein Glücksspielsystem auf der Blockchain (einem deterministischen System) aufzubauen. Es ist klar, dass die Unsicherheit aus einer Quelle außerhalb der Blockchain stammen muss. Dies ist bei Wetten unter Gleichgestellten möglich (siehe zum Beispiel die [Commit-Reveal-Technik](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract)), jedoch ist es erheblich schwieriger, wenn man einen Vertrag implementieren möchte, der als *die Bank* (wie bei Blackjack oder Roulette) fungiert. Eine häufige Falle ist die Verwendung zukünftiger Blockvariablen wie Hashes, Zeitstempel, Blocknummer oder Gaslimit. Das Problem dabei ist, dass diese vom Miner kontrolliert werden, der den Block schürft, und daher nicht wirklich zufällig sind. Betrachten Sie zum Beispiel einen Roulette-Smart-Contract mit einer Logik, die eine schwarze Zahl zurückgibt, wenn der Hash des nächsten Blocks auf eine gerade Zahl endet. Ein Miner (oder ein Mining-Pool) könnte \$1M auf Schwarz setzen. Wenn sie den nächsten Block lösen und feststellen, dass der Hash auf eine ungerade Zahl endet, würden sie ihren Block wohl nicht veröffentlichen und einen weiteren schürfen, bis sie eine Lösung finden, bei der der Block-Hash eine gerade Zahl ist (vorausgesetzt, die Blockbelohnung und die Gebühren sind geringer als \$1M). Die Verwendung vergangener oder gegenwärtiger Variablen kann sogar noch verheerender sein, wie Martin Swende in seinem exzellenten [Blogbeitrag](http://martin.swende.se/blog/Breaking_the_house.html) demonstriert. Darüber hinaus bedeutet die alleinige Verwendung von Blockvariablen, dass die Pseudozufallszahl für alle Transaktionen in einem Block gleich ist. Ein Angreifer kann seine Gewinne also vervielfachen, indem er viele Transaktionen innerhalb eines Blocks durchführt (sofern es einen maximalen Einsatz gibt).
<h3 id="entropy-prevention">Vorbeugende Techniken</h3>
Die Quelle der Entropie (Zufälligkeit) muss außerhalb der Blockchain liegen. Dies kann unter Gleichgestellten mit Systemen wie [Commit-Reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract) erfolgen oder durch Änderung des Vertrauensmodells hin zu einer Gruppe von Teilnehmern (wie bei [RandDAO](https://github.com/randao/randao)). Dies kann auch über eine zentralisierte Einheit erfolgen, die als Orakel für Zufälligkeit fungiert. Blockvariablen (im Allgemeinen gibt es einige Ausnahmen) sollten nicht als Entropiequelle verwendet werden, da sie von Minern manipuliert werden können.
<h3 id="entropy-example">Beispiel aus der Praxis: PRNG-Verträge</h3>
Arseny Reutov schrieb einen [Blogbeitrag](https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620), nachdem er 3649 aktive Smart Contracts analysiert hatte, die eine Art Pseudozufallszahlengenerator (PRNG) verwendeten, und 43 Verträge fand, die ausgenutzt werden konnten.
<h2 id="contract-reference"><span id="SP-7">7. Externe Vertragsreferenzierung</span></h2>
Einer der Vorteile des Ethereum-*Weltcomputers* ist die Möglichkeit, Code wiederzuverwenden und mit bereits im Netzwerk bereitgestellten Verträgen zu interagieren. Infolgedessen referenzieren viele Verträge externe Verträge und verwenden im allgemeinen Betrieb externe Nachrichtenaufrufe, um mit diesen Verträgen zu interagieren. Diese externen Nachrichtenaufrufe können die Absichten böswilliger Akteure auf nicht offensichtliche Weise verschleiern, was wir diskutieren werden.
<h3 id="cr-vuln">Die Schwachstelle</h3>
In Solidity kann jede Adresse als Vertrag gecastet werden, unabhängig davon, ob der Code an der Adresse dem Vertragstyp entspricht, der gecastet wird. Das kann irreführend sein, insbesondere wenn der Autor des Vertrags versucht, bösartigen Code zu verbergen. Lassen Sie uns das anhand eines Beispiels veranschaulichen:
Betrachten Sie ein Codefragment, das die [Rot13](https://github.com/al1ex/soliditysecurity/blob/HEAD/www.wikipedia.com/rot13)-Chiffre rudimentär implementiert.
`Rot13Encryption.sol`:```solidity
//encryption contract
contract Rot13Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,13))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
}
}
emit Result(text);
}
}
Dieser Code nimmt einfach einen String (Buchstaben a–z, ohne Validierung) und verschlüsselt ihn, indem er jedes Zeichen um 13 Stellen nach rechts verschiebt (mit Umwicklung bei 'z'); d. h. 'a' wird zu 'n' und 'x' wird zu 'k'. Die Assemblierung hier ist nicht wichtig, also mach dir keine Sorgen, wenn sie in diesem Stadium keinen Sinn ergibt.
Betrachte den folgenden Vertrag, der diesen Code für seine Verschlüsselung verwendet,```solidity import "Rot13Encryption.sol";
// encrypt your top secret info contract EncryptionContract { // library for encryption Rot13Encryption encryptionLibrary;
// constructor - initialise the library
constructor(Rot13Encryption _encryptionLibrary) {
encryptionLibrary = _encryptionLibrary;
}
function encryptPrivateData(string privateInfo) {
// potentially do some operations here
encryptionLibrary.rot13Encrypt(privateInfo);
}
}
Das Problem bei diesem Vertrag ist, dass die `encryptionLibrary`-Adresse nicht öffentlich oder konstant ist. Somit hätte der Ersteller des Vertrags im Konstruktor eine Adresse angeben können, die auf diesen Vertrag verweist:```solidity
//encryption contract
contract Rot26Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,26))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
}
}
emit Result(text);
}
}
welches die rot26-Chiffre implementiert (verschiebt jedes Zeichen um 26 Stellen, kapiert? :p). Auch hier muss man die Assembly in diesem Contract nicht verstehen. Der Deployer hätte auch den folgenden Contract verlinken können:```solidity contract Print{ event Print(string text);
function rot13Encrypt(string text) public {
emit Print(text);
}
}
Wenn die Adresse eines dieser Verträge im Konstruktor angegeben würde, würde die Funktion `encryptPrivateData()` einfach ein Ereignis erzeugen, das die unverschlüsselten privaten Daten ausgibt. Obwohl in diesem Beispiel ein bibliotheksähnlicher Vertrag im Konstruktor festgelegt wurde, ist es häufig der Fall, dass ein privilegierter Benutzer (wie ein `owner`) die Adressen der Bibliotheksverträge ändern kann. Wenn ein verknüpfter Vertrag die aufgerufene Funktion nicht enthält, wird die Fallback-Funktion ausgeführt. Beispielsweise wird bei der Zeile `encryptionLibrary.rot13Encrypt()` – wenn der durch `encryptionLibrary` angegebene Vertrag Folgendes wäre:```solidity
contract Blank {
event Print(string text);
function () {
emit Print("Here");
//put malicious code here and it will run
}
}
dann würde ein Ereignis mit dem Text "Here" ausgelöst. Wenn Benutzer also Vertragsbibliotheken ändern können, können sie im Prinzip dazu führen, dass Benutzer unwissentlich beliebigen Code ausführen.
Hinweis: Verwenden Sie keine Verschlüsselungsverträge wie diese, da die Eingabeparameter von Smart Contracts auf der Blockchain sichtbar sind. Außerdem ist die Rot cipher keine empfohlene Verschlüsselungstechnik :p
Wie oben gezeigt, können schwachstellenfreie Verträge (in einigen Fällen) so bereitgestellt werden, dass sie sich bösartig verhalten. Ein Prüfer könnte einen Vertrag öffentlich verifizieren und dessen Eigentümer veranlassen, ihn auf bösartige Weise bereitzustellen, was zu einem öffentlich geprüften Vertrag führt, der Schwachstellen oder bösartige Absichten aufweist.
Es gibt eine Reihe von Techniken, die diese Szenarien verhindern.
Eine Technik besteht darin, das new-Schlüsselwort zu verwenden, um Verträge zu erstellen. Im obigen Beispiel könnte der Konstruktor wie folgt geschrieben werden:```solidity
constructor() {
encryptionLibrary = new Rot13Encryption();
}
Auf diese Weise wird zur Bereitstellungszeit eine Instanz des referenzierten Vertrags erstellt, und der Bereitsteller kann den `Rot13Encryption`-Vertrag nicht durch etwas anderes ersetzen, ohne den Smart Contract zu ändern.
Eine weitere Lösung besteht darin, bekannte externe Vertragsadressen fest zu kodieren.
Im Allgemeinen sollte Code, der externe Verträge aufruft, immer sorgfältig geprüft werden. Als Entwickler kann es beim Definieren externer Verträge eine gute Idee sein, die Vertragsadressen öffentlich zu machen (was im unten aufgeführten Honey-Pot-Beispiel nicht der Fall ist), damit Benutzer leicht prüfen können, auf welchen Code der Vertrag verweist. Umgekehrt kann eine private Variable für die Vertragsadresse ein Zeichen für bösartiges Verhalten sein (wie im Praxisbeispiel gezeigt). Wenn ein privilegierter (oder beliebiger) Benutzer in der Lage ist, eine Vertragsadresse zu ändern, die zum Aufrufen externer Funktionen verwendet wird, kann es (im Kontext eines dezentralen Systems) wichtig sein, einen Zeit-Sperr- oder Abstimmungsmechanismus zu implementieren, damit Benutzer sehen können, welcher Code geändert wird, oder damit Teilnehmer die Möglichkeit haben, sich für die neue Vertragsadresse zu entscheiden oder sie abzulehnen.
<h3 id="cr-example">Praxisbeispiel: Re-Entrancy-Honeypot</h3>
In letzter Zeit wurden mehrere Honeypots im Mainnet veröffentlicht. Diese Verträge versuchen, Ethereum-Hacker auszutricksen, die versuchen, die Verträge auszunutzen, aber am Ende Ether an den Vertrag verlieren, den sie auszunutzen erwarten. Ein Beispiel nutzt den obigen Angriff, indem es im Konstruktor einen erwarteten Vertrag durch einen bösartigen ersetzt. Der Code ist [hier](https://etherscan.io/address/0x95d34980095380851902ccd9a1fb4c813c2cb639#code) zu finden:```solidity
pragma solidity ^0.4.19;
contract Private_Bank
{
mapping (address => uint) public balances;
uint public MinDeposit = 1 ether;
Log TransferLog;
function Private_Bank(address _log)
{
TransferLog = Log(_log);
}
function Deposit()
public
payable
{
if(msg.value >= MinDeposit)
{
balances[msg.sender]+=msg.value;
TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
}
}
function CashOut(uint _am)
{
if(_am<=balances[msg.sender])
{
if(msg.sender.call.value(_am)())
{
balances[msg.sender]-=_am;
TransferLog.AddMessage(msg.sender,_am,"CashOut");
}
}
}
function() public payable{}
}
contract Log
{
struct Message
{
address Sender;
string Data;
uint Val;
uint Time;
}
Message[] public History;
Message LastMsg;
function AddMessage(address _adr,uint _val,string _data)
public
{
LastMsg.Sender = _adr;
LastMsg.Time = now;
LastMsg.Val = _val;
LastMsg.Data = _data;
History.push(LastMsg);
}
}
Dieser Beitrag eines Reddit-Nutzers erklärt, wie dieser 1 Ether an diesen Vertrag verloren hat, indem er versuchte, den Reentrancy-Bug auszunutzen, von dem er annahm, dass er im Vertrag vorhanden sei.
Dieser Angriff richtet sich nicht speziell gegen Solidity-Verträge selbst, sondern gegen Drittanbieter-Anwendungen, die mit ihnen interagieren können. Ich füge diesen Angriff der Vollständigkeit halber hinzu und um zu verdeutlichen, wie Parameter in Verträgen manipuliert werden können.
Für weiterführende Literatur siehe The ERC20 Short Address Attack Explained, ICO Smart contract Vulnerability: Short Address Attack oder diesen Reddit-Beitrag.
Wenn Parameter an einen Smart Contract übergeben werden, werden diese gemäß der ABI-Spezifikation kodiert. Es ist möglich, kodierte Parameter zu senden, die kürzer sind als die erwartete Parameterlänge (z. B. indem man eine Adresse sendet, die nur 38 Hex-Zeichen (19 Bytes) statt der üblichen 40 Hex-Zeichen (20 Bytes) umfasst). In einem solchen Szenario füllt die EVM die kodierten Parameter am Ende mit Nullen auf, um die erwartete Länge zu erreichen.
Dies wird dann zum Problem, wenn Drittanbieter-Anwendungen Eingaben nicht validieren. Das deutlichste Beispiel ist eine Börse, die die Adresse eines ERC20-Tokens nicht überprüft, wenn ein Benutzer eine Auszahlung anfordert. Dieses Beispiel wird in Peter Venesses' oben erwähntem Beitrag The ERC20 Short Address Attack Explained ausführlicher behandelt.
Betrachten wir die Standard-Schnittstelle der ERC20-Transferfunktion, wobei die Reihenfolge der Parameter zu beachten ist:```solidity function transfer(address to, uint tokens) public returns (bool success);
Betrachten wir nun eine Börse, die eine große Menge eines Tokens hält (sagen wir `REP`), und einen Benutzer, der seinen Anteil von 100 Tokens abheben möchte. Der Benutzer würde seine Adresse, `0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead`, und die Anzahl der Tokens, `100`, übermitteln. Die Börse würde diese Parameter in der Reihenfolge kodieren, die von der `transfer()`-Funktion festgelegt ist, d.h. zuerst `address`, dann `tokens`. Das kodierte Ergebnis wäre `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000` `000000000000000000000000000000000056bc75e2d63100000`. Die ersten vier Bytes (`a9059cbb`) sind der [Funktionssignatur/-selektor](https://solidity.readthedocs.io/en/latest/abi-spec.html#function-selector) von `transfer()`, die zweiten 32 Bytes sind die Adresse, gefolgt von den letzten 32 Bytes, die die `uint256`-Anzahl der Tokens darstellen. Beachte, dass der Hex-Wert `56bc75e2d63100000` am Ende 100 Tokens entspricht (mit 18 Dezimalstellen, wie vom `REP`-Token-Vertrag festgelegt).
Ok, schauen wir uns nun an, was passiert, wenn wir eine Adresse senden, bei der 1 Byte (2 Hexadezimalstellen) fehlt. Konkret nehmen wir an, ein Angreifer sendet `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde` als Adresse (die letzten beiden Ziffern fehlen) und dieselben `100` Tokens zum Abheben. Wenn die Börse diese Eingabe nicht validiert, würde es kodiert als `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeadde00000000000000` `00000000000000000000000000000000056bc75e2d6310000000`. Der Unterschied ist subtil. Beachte, dass `00` am Ende der Kodierung aufgefüllt wurde, um die kurze Adresse auszugleichen, die gesendet wurde. Wenn dies an den Smart Contract gesendet wird, liest der Parameter `address` als `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde00` und der Wert liest sich als `56bc75e2d6310000000` (beachte die zwei zusätzlichen `0`). Dieser Wert entspricht nun `25600` Tokens (der Wert wurde mit `256` multipliziert). In diesem Beispiel würde der Benutzer, falls die Börse so viele Tokens hält, `25600` Tokens an die modifizierte Adresse abheben (während die Börse glaubt, der Benutzer hebe nur `100` ab). Offensichtlich besitzt der Angreifer in diesem Beispiel die modifizierte Adresse nicht, aber wenn der Angreifer eine beliebige Adresse erzeugen würde, die auf `0` endet (was leicht per Brute-Force möglich ist), und diese erzeugte Adresse verwendet, könnte er leicht Tokens von der ahnungslosen Börse stehlen.
<h3 id="short-prev">Vorbeugende Maßnahmen</h3>
Ich nehme an, es ist offensichtlich, dass die Validierung aller Eingaben vor dem Senden an die Blockchain diese Art von Angriffen verhindert. Es sollte auch beachtet werden, dass die Parameterreihenfolge hier eine wichtige Rolle spielt. Da das Auffüllen nur am Ende erfolgt, kann eine sorgfältige Anordnung der Parameter im Smart Contract potenziell einige Formen dieses Angriffs abschwächen.
<h3 id="short-example">Beispiel aus der Praxis: Unbekannt</h3>
Mir ist kein öffentlich bekannter Angriff dieser Art in freier Wildbahn bekannt.
<h2 id="unchecked-calls"><span id="SP-9">9. Ungeprüfte CALL-Rückgabewerte</span></h2>
Es gibt eine Reihe von Möglichkeiten, externe Aufrufe in Solidity durchzuführen. Das Senden von Ether an externe Konten erfolgt üblicherweise über die `transfer()`-Methode. Allerdings kann auch die `send()`-Funktion verwendet werden, und für vielseitigere externe Aufrufe kann der `CALL`-Opcode direkt in Solidity eingesetzt werden. Die Funktionen `call()` und `send()` geben einen booleschen Wert zurück, der angibt, ob der Aufruf erfolgreich war oder fehlgeschlagen ist. Diese Funktionen haben also eine einfache Einschränkung: Die Transaktion, die diese Funktionen ausführt, wird nicht zurückgesetzt (revert), wenn der externe Aufruf (initiiert durch `call()` oder `send()`) fehlschlägt, sondern `call()` oder `send()` gibt lediglich `false` zurück. Eine häufige Falle entsteht, wenn der Rückgabewert nicht geprüft wird, sondern der Entwickler erwartet, dass ein Revert erfolgt.
Für weiterführende Informationen siehe [DASP Top 10](http://www.dasp.co/#item-4) und [Scannen von Live-Ethereum-Verträgen auf den "Unchecked-Send"-Fehler](http://hackingdistributed.com/2016/06/16/scanning-live-ethereum-contracts-for-bugs/).
<h3 id="unchecked-calls-vuln">Die Schwachstelle</h3>
Betrachten wir das folgende Beispiel:```solidity
contract Lotto {
bool public payedOut = false;
address public winner;
uint public winAmount;
// ... extra functionality here
function sendToWinner() public {
require(!payedOut);
winner.send(winAmount);
payedOut = true;
}
function withdrawLeftOver() public {
require(payedOut);
msg.sender.send(this.balance);
}
}
This contract represents a Lotto-like contract, where a winner receives winAmount of ether, which typically leaves a little left over for anyone to withdraw.
The bug exists on line [11] where a send() is used without checking the response. In this trivial example, a winner whose transaction fails (either by running out of gas or being a contract that intentionally throws in the fallback function) allows payedOut to be set to true (regardless of whether ether was sent or not). In this case, the public can withdraw the winner's winnings via the withdrawLeftOver() function.
Verwende, wann immer möglich, die Funktion transfer() anstelle von send(), da transfer() einen revert auslöst, wenn die externe Transaktion zurückgerollt wird. Falls send() erforderlich ist, stelle stets sicher, dass der Rückgabewert überprüft wird.
Eine noch robustere Empfehlung ist die Anwendung eines Withdrawal-Musters. Bei dieser Lösung liegt es an jedem Benutzer, eine isolierte Funktion (d. h. eine withdraw-Funktion) aufzurufen, die das Senden von Ether aus dem Vertrag heraus übernimmt und dadurch unabhängig mit den Konsequenzen fehlgeschlagener Send-Transaktionen umgeht. Die Idee ist, die externe Send-Funktionalität logisch vom restlichen Code zu trennen und die Verantwortung für eine möglicherweise fehlgeschlagene Transaktion dem Endbenutzer zu übertragen, der die withdraw-Funktion aufruft.
Etherpot war eine Smart-Contract-Lotterie, die dem oben erwähnten Beispielvertrag nicht unähnlich ist. Der Solidity-Code von Etherpot kann hier gefunden werden: lotto.sol. Der Hauptfehler dieses Vertrags war die falsche Verwendung von Block-Hashes (nur die letzten 256 Block-Hashes sind verwendbar, siehe Aakil Fernandes' Beitrag darüber, wie Etherpot dies nicht korrekt umsetzte). Dieser Vertrag litt jedoch auch unter einem ungeprüften Aufrufwert. Beachte die Funktion cash() in Zeile [80] von lotto.sol:```solidity
...
function cash(uint roundIndex, uint subpotIndex){
var subpotsCount = getSubpotsCount(roundIndex);
if(subpotIndex>=subpotsCount)
return;
var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);
if(decisionBlockNumber>block.number)
return;
if(rounds[roundIndex].isCashed[subpotIndex])
return;
//Subpots can only be cashed once. This is to prevent double payouts
var winner = calculateWinner(roundIndex,subpotIndex);
var subpot = getSubpot(roundIndex);
winner.send(subpot);
rounds[roundIndex].isCashed[subpotIndex] = true;
//Mark the round as cashed
} ...
Beachte, dass in Zeile \[21\] der Rückgabewert der `send`-Funktion nicht überprüft wird und die folgende Zeile dann einen booleschen Wert setzt, der angibt, dass dem Gewinner seine Mittel gesendet wurden. Dieser Fehler kann einen Zustand ermöglichen, in dem der Gewinner sein Ether nicht erhält, der Vertragszustand jedoch anzeigen kann, dass der Gewinner bereits ausgezahlt wurde.
Eine ernstere Version dieses Fehlers trat im [King of the Ether](https://www.kingoftheether.com/thrones/kingoftheether/index.html) auf. Eine hervorragende [Post-Mortem-Analyse](https://www.kingoftheether.com/postmortem.html) dieses Vertrags wurde verfasst, die detailliert beschreibt, wie ein ungeprüfter fehlgeschlagener `send()`-Aufruf zum Angriff auf den Vertrag genutzt werden konnte.
<h2 id="race-conditions"><span id="SP-10">10. Race Conditions / Front Running</span></h2>
Die Kombination aus externen Aufrufen anderer Verträge und der Mehrbenutzer-Natur der zugrunde liegenden Blockchain führt zu einer Vielzahl potenzieller Solidity-Fallstricke, bei denen Benutzer *um* die Codeausführung *wetteifern*, um unerwartete Zustände zu erzielen. [Re-Entrancy](#reentrancy) ist ein Beispiel für eine solche Race Condition. In diesem Abschnitt werden wir allgemeiner über verschiedene Arten von Race Conditions sprechen, die auf der Ethereum-Blockchain auftreten können. Es gibt eine Reihe guter Beiträge zu diesem Thema, einige davon sind: [Ethereum Wiki - Safety](https://github.com/ethereum/wiki/wiki/Safety#race-conditions), [DASP - Front-Running](http://www.dasp.co/#item-7) und die [Consensus - Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#race-conditions).
<h3 id="race-conditions-vuln">Die Schwachstelle</h3>
Wie bei den meisten Blockchains bündeln Ethereum-Knoten Transaktionen in Pools und formen daraus Blöcke. Die Transaktionen gelten erst dann als gültig, wenn ein Miner ein Konsensverfahren gelöst hat (derzeit [ETHASH](https://github.com/ethereum/wiki/wiki/Ethash) PoW für Ethereum). Der Miner, der den Block löst, wählt auch aus, welche Transaktionen aus dem Pool in den Block aufgenommen werden. Dies erfolgt in der Regel nach der `gasPrice` einer Transaktion. Hierin liegt ein potenzieller Angriffsvektor. Ein Angreifer kann den Transaktionspool nach Transaktionen durchsuchen, die Lösungen für Probleme enthalten könnten, die Berechtigungen des Angreifers ändern oder widerrufen könnten oder einen Zustand in einem Vertrag ändern, der für den Angreifer unerwünscht ist. Der Angreifer kann dann die Daten aus dieser Transaktion übernehmen und eine eigene Transaktion mit einer höheren `gasPrice` erstellen und so erreichen, dass seine Transaktion vor der ursprünglichen in einen Block aufgenommen wird.
Sehen wir uns an, wie dies anhand eines einfachen Beispiels funktionieren könnte. Betrachten wir den Vertrag `FindThisHash.sol`:```solidity
contract FindThisHash {
bytes32 constant public hash = 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable {} // load with ether
function solve(string solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
Stellen Sie sich vor, dieser Vertrag enthält 1000 Ether. Der Benutzer, der das Preimage des SHA3-Hashs 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a findet, kann die Lösung einreichen und die 1000 Ether abrufen. Nehmen wir an, ein Benutzer findet heraus, dass die Lösung Ethereum! ist. Er ruft solve() mit Ethereum! als Parameter auf. Leider war ein Angreifer schlau genug, den Transaktionspool zu beobachten, um jemanden zu erwischen, der eine Lösung einreicht. Er sieht diese Lösung, prüft ihre Gültigkeit und reicht dann eine äquivalente Transaktion mit einer viel höheren gasPrice als die ursprüngliche Transaktion ein. Der Miner, der den Block löst, wird dem Angreifer aufgrund der höheren gasPrice wahrscheinlich den Vorzug geben und dessen Transaktion vor der des ursprünglichen Lösers akzeptieren. Der Angreifer nimmt sich die 1000 Ether und der Benutzer, der das Problem gelöst hat, erhält nichts (es ist kein Ether mehr im Vertrag übrig).
Ein realistischeres Problem ergibt sich beim Design der zukünftigen Casper-Implementierung. Die Casper-Proof-of-Stake-Verträge sehen Slashing-Bedingungen vor, bei denen Benutzer, die bemerken, dass Validatoren doppelt abstimmen oder sich fehlverhalten, einen Anreiz erhalten, einen Nachweis dafür einzureichen. Der Validator wird bestraft und der Benutzer belohnt. In einem solchen Szenario ist zu erwarten, dass Miner und Benutzer allen derartigen Beweiseinreichungen zuvorkommen (Frontrunning), und dieses Problem muss vor der endgültigen Veröffentlichung behoben werden.
Es gibt zwei Klassen von Benutzern, die diese Art von Frontrunning-Angriffen ausführen können: Benutzer (die die gasPrice ihrer Transaktionen ändern) und Miner selbst (die die Transaktionen in einem Block nach eigenem Ermessen neu anordnen können). Ein Vertrag, der gegenüber der ersten Klasse (Benutzer) anfällig ist, ist deutlich schlechter dran als einer, der gegenüber der zweiten Klasse (Miner) anfällig ist, da Miner den Angriff nur ausführen können, wenn sie einen Block lösen, was für einen einzelnen Miner, der einen bestimmten Block anvisiert, unwahrscheinlich ist. Hier liste ich einige Gegenmaßnahmen auf, bezogen darauf, welche Klasse von Angreifern sie verhindern können.
Eine Methode, die eingesetzt werden kann, besteht darin, eine Logik im Vertrag zu erstellen, die eine Obergrenze für die gasPrice festlegt. Dies verhindert, dass Benutzer die gasPrice erhöhen und über die Obergrenze hinaus eine bevorzugte Transaktionsreihenfolge erhalten. Diese vorbeugende Maßnahme mildert nur die erste Klasse von Angreifern (beliebige Benutzer). Miner können den Vertrag in diesem Szenario weiterhin angreifen, da sie die Transaktionen in ihrem Block beliebig anordnen können, unabhängig vom Gaspreis.
Eine robustere Methode ist, wann immer möglich, ein Commit-Reveal-Schema zu verwenden. Ein solches Schema schreibt vor, dass Benutzer Transaktionen mit verborgenen Informationen (typischerweise einem Hash) senden. Nachdem die Transaktion in einen Block aufgenommen wurde, sendet der Benutzer eine Transaktion, die die gesendeten Daten offenlegt (die Reveal-Phase). Diese Methode verhindert, dass sowohl Miner als auch Benutzer Transaktionen per Frontrunning angreifen, da sie den Inhalt der Transaktion nicht bestimmen können. Diese Methode kann jedoch den Transaktionswert nicht verbergen (der in einigen Fällen die wertvolle Information ist, die verborgen werden muss). Der ENS-Smart-Contract erlaubte es Benutzern, Transaktionen zu senden, deren übermittelte Daten den Betrag an Ether enthielten, den sie auszugeben bereit waren. Die Benutzer konnten dann Transaktionen mit beliebigem Wert senden. Während der Reveal-Phase wurde den Benutzern die Differenz zwischen dem in der Transaktion gesendeten Betrag und dem Betrag, den sie auszugeben bereit waren, erstattet.
Ein weiterer Vorschlag von Lorenz, Phil, Ari und Florian ist die Verwendung von Submarine Sends. Eine effiziente Umsetzung dieser Idee erfordert den CREATE2-Opcode, der derzeit noch nicht übernommen wurde, aber in kommenden Hard Forks wahrscheinlich eingeführt wird.
Der ERC20-Standard ist für die Erstellung von Tokens auf Ethereum recht bekannt. Dieser Standard weist eine potenzielle Frontrunning-Schwachstelle auf, die durch die approve()-Funktion entsteht. Eine gute Erklärung dieser Schwachstelle findet sich hier.
Der Standard spezifiziert die approve()-Funktion wie folgt:```solidity
function approve(address _spender, uint256 _value) returns (bool success)
Diese Funktion erlaubt es einem Benutzer, anderen Benutzern zu gestatten, Token in seinem Namen zu übertragen. Die Frontrunning-Schwachstelle tritt in dem Szenario auf, wenn eine Benutzerin, Alice, ihrem Freund `Bob` erlaubt, `100 Token` auszugeben. Alice entscheidet später, dass sie die Genehmigung von `Bob`, `100 Token` auszugeben, widerrufen möchte, also erstellt sie eine Transaktion, die `Bobs` Zuteilung auf `50 Token` setzt. `Bob`, der die Chain aufmerksam beobachtet hat, sieht diese Transaktion und baut eine eigene Transaktion auf, um die `100 Token` auszugeben. Er setzt einen höheren `gasPrice` auf seine Transaktion als `Alice` und erreicht, dass seine Transaktion vor ihrer priorisiert wird. Einige Implementierungen von `approve()` würden es `Bob` erlauben, seine `100 Token` zu übertragen, und dann, wenn `Alice` Transaktion committet wird, `Bobs` Genehmigung auf `50 Token` zurückzusetzen, was `Bob` effektiv Zugang zu `150 Token` verschafft. Die Minderungsstrategien für diesen Angriff sind [hier](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit) in dem oben verlinkten Dokument angegeben.
Ein weiteres prominentes Beispiel aus der Praxis ist [Bancor](https://www.bancor.network/). Ivan Bogatty und sein Team dokumentierten einen profitablen Angriff auf die ursprüngliche Bancor-Implementierung. Sein [Blogbeitrag](https://hackernoon.com/front-running-bancor-in-150-lines-of-python-with-ethereum-api-d5e2bfd0d798) und sein [Devon-3-Vortrag](https://www.youtube.com/watch?v=RL2nE3huNiI) erörtern im Detail, wie dies bewerkstelligt wurde. Im Wesentlichen werden Token-Preise auf der Grundlage des Transaktionswerts bestimmt; Benutzer können den Transaktionspool nach Bancor-Transaktionen durchsuchen und ihnen zuvorkommen, um von den Preisunterschieden zu profitieren. Dieser Angriff wurde vom Bancor-Team behoben.
<h2 id="dos"><span id="SP-11">11. Denial of Service (DOS)</span></h2>
Diese Kategorie ist sehr breit gefasst, besteht aber grundsätzlich aus Angriffen, bei denen Benutzer den Vertrag für einen kurzen Zeitraum oder in manchen Fällen dauerhaft funktionsunfähig machen können. Dies kann Ether dauerhaft in diesen Verträgen einsperren, wie es beim [Second Parity MultiSig Hack](#dc-example) der Fall war.
<h3 id="dos-vuln">Die Schwachstelle</h3>
Es gibt verschiedene Möglichkeiten, wie ein Vertrag funktionsunfähig werden kann. Hier möchte ich nur einige potenziell weniger offensichtliche, Blockchain-spezifische Solidity-Codemuster hervorheben, die zu DOS-Angriffen durch Angreifer führen können.
**1. Externe Aufrufe ohne Gas-Stipendien** – Es kann der Fall sein, dass du einen externen Aufruf an einen unbekannten Vertrag tätigen und die Transaktion unabhängig davon fortsetzen möchtest, ob dieser Aufruf fehlschlägt oder nicht. Typischerweise wird dies durch die Verwendung des `CALL`-Opcode erreicht, der die Transaktion nicht rückgängig macht, wenn der Aufruf fehlschlägt (siehe [Unchecked CALL Return Values](#unchecked-calls) für weitere Details und Beispiele). Betrachten wir ein einfaches Beispiel, bei dem wir einen Vertrag als Wallet haben, der langsam Ether auszahlt, wenn die Funktion `withdraw()` aufgerufen wird. Ein `Partner` kann seine Adresse hinzufügen und Gas aufwenden, um die Auszahlung aufzurufen, wodurch sowohl der `Partner` als auch der `Eigentümer` 1 % des gesamten Vertragssaldos erhalten.```solidity
contract TrickleWallet {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
require(partner == '0x0' || msg.sender == partner);
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance/100;
// perform a call without checking return
// the recipient can revert, the owner will still get their share
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
function() payable {}
// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
Beachten Sie, dass wir in Zeile [17] einen externen Aufruf durchführen, der 1% des
Vertragsguthabens an ein vom Benutzer angegebenes Konto sendet. Der Grund, warum der CALL-Opcode verwendet wird, ist sicherzustellen, dass
der Eigentümer weiterhin bezahlt wird, selbst wenn der externe Aufruf zurücksetzt. Das Problem ist, dass
die Transaktion das gesamte Gas an den externen Aufruf sendet (in Wirklichkeit wird nur der größte Teil des Transaktionsgases gesendet, ein Teil bleibt übrig, um die Verarbeitung des Aufrufs abzuschließen). Wenn der Benutzer böswillig wäre, könnte er einen Vertrag erstellen, der das gesamte Gas verbraucht, und alle Transaktionen, die withdraw() aufrufen, zum Scheitern bringen, weil das Gas ausgeht.
Betrachten Sie zum Beispiel den folgenden bösartigen Vertrag, der das gesamte Gas verbraucht,```solidity contract ConsumeAllGas { function () payable { // an assert consumes all transaction gas, unlike a //revert which returns the remaining gas assert(1==2); } }
Wenn ein Entnahmepartner entschied, dass ihm der Eigentümer des Vertrags nicht gefiel.
Er könnte die Partneradresse auf diesen Vertrag setzen und alle Gelder im
`TrickleWallet`-Vertrag für immer sperren.
Um solche DoS-Angriffsvektoren zu verhindern, stellen Sie sicher, dass ein Gas-Stipendium in einem
externen Aufruf angegeben wird, um die Gasmenge zu begrenzen, die diese Transaktion verwenden kann. In unserem
Beispiel könnten wir diesen Angriff beheben, indem wir Zeile \[17\] ändern zu:```solidity
partner.call.gas(50000).value(amountToSend)();
Diese Änderung erlaubt, dass nur 50.000 Gas für die externe Transaktion ausgegeben werden dürfen. Der owner kann einen höheren Gaspreis festlegen, damit seine Transaktion unabhängig davon, wie viel Gas die externe Transaktion verbraucht, abgeschlossen wird.
2. Schleifen über extern manipulierte Mappings oder Arrays – In meinen Abenteuern bin ich auf verschiedene Formen dieser Art von Mustern gestoßen. Typischerweise tritt es in Szenarien auf, in denen ein owner Tokens an seine Investoren verteilen möchte, und zwar mit einer Funktion wie distribute(), wie im Beispielvertrag zu sehen ist:```solidity
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets
// ... extra functionality, including transfertoken()
function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times the wei sent
}
function distribute() public {
require(msg.sender == owner); // only owner
for(uint i = 0; i < investors.length; i++) {
// here transferToken(to,amount) transfers "amount" of tokens to the address "to"
transferToken(investors[i],investorTokens[i]);
}
}
}
Beachte, dass die Schleife in diesem Vertrag über ein Array läuft, das künstlich aufgebläht werden kann. Ein Angreifer kann viele Benutzerkonten erstellen und so das `investor`-Array vergrößern. Im Prinzip kann dies so weit getrieben werden, dass das für die Ausführung der for-Schleife benötigte Gas das Block-Gas-Limit überschreitet, wodurch die `distribute()`-Funktion praktisch unbrauchbar wird.
**3. Besitzer-Operationen** – Ein weiteres verbreitetes Muster ist, dass Besitzer in Verträgen spezifische Privilegien besitzen und eine bestimmte Aufgabe ausführen müssen, damit der Vertrag in den nächsten Zustand übergehen kann. Ein Beispiel wäre ein ICO-Vertrag, der verlangt, dass der Besitzer den Vertrag mit `finalize()` abschließt, wodurch die Token anschließend übertragbar werden, d. h.``` solidity
bool public isFinalized = false;
address public owner; // gets set somewhere
function finalize() public {
require(msg.sender == owner);
isFinalized == true;
}
// ... extra ICO functionality
// overloaded transfer function
function transfer(address _to, uint _value) returns (bool) {
require(isFinalized);
super.transfer(_to,_value)
}
...
In solchen Fällen wird der gesamte Token-Vertrag funktionsunfähig, wenn ein privilegierter Benutzer seine privaten Schlüssel verliert oder inaktiv wird. In diesem Fall können keine Token übertragen werden, wenn der owner finalize() nicht aufrufen kann; d.h. der gesamte Betrieb des Token-Ökosystems hängt von einer einzigen Adresse ab.
4. Fortschreitender Zustand basierend auf externen Aufrufen - Verträge werden manchmal so geschrieben, dass für den Übergang in einen neuen Zustand Ether an eine Adresse gesendet werden muss oder auf eine Eingabe von einer externen Quelle gewartet wird. Diese Muster können zu DoS-Angriffen führen, wenn der externe Aufruf fehlschlägt oder aus externen Gründen verhindert wird. Beim Beispiel des Sendens von Ether kann ein Benutzer einen Vertrag erstellen, der kein Ether akzeptiert. Wenn ein Vertrag für den Übergang in einen neuen Zustand erfordert, dass Ether abgehoben wird (denken Sie an einen Zeitverriegelungsvertrag, der erfordert, dass alles Ether abgehoben wird, bevor er wieder verwendbar ist), wird der Vertrag den neuen Zustand nie erreichen, da Ether niemals an den Vertrag des Benutzers gesendet werden kann, der kein Ether akzeptiert.
Im ersten Beispiel sollten Verträge nicht über Datenstrukturen iterieren, die von externen Benutzern künstlich manipuliert werden können. Ein Abhebungsmuster wird empfohlen, bei dem jeder der Investoren eine Abhebungsfunktion aufruft, um Token unabhängig zu beanspruchen.
Im zweiten Beispiel war ein privilegierter Benutzer erforderlich, um den Zustand des Vertrags zu ändern. In solchen Beispielen kann (wo immer möglich) ein Fail-Safe verwendet werden, falls der owner handlungsunfähig wird. Eine Lösung könnte sein, den owner als Multisig-Vertrag einzurichten. Eine andere Lösung ist die Verwendung eines Timelocks, bei dem das require in Zeile [13] einen zeitbasierten Mechanismus enthalten könnte, wie z.B. require(msg.sender == owner || now > unlockTime), das es jedem Benutzer ermöglicht, nach einem bestimmten Zeitraum, der durch unlockTime festgelegt ist, abzuschließen. Diese Art von Abschwächungstechnik kann auch im dritten Beispiel verwendet werden. Wenn externe Aufrufe erforderlich sind, um einen neuen Zustand zu erreichen, berücksichtigen Sie deren mögliches Scheitern und fügen Sie möglicherweise einen zeitbasierten Zustandsübergang hinzu, falls der gewünschte Aufruf nie erfolgt.
Hinweis: Natürlich gibt es zentralisierte Alternativen zu diesen Vorschlägen, bei denen man einen maintenanceUser hinzufügen kann, der bei Bedarf vorbeikommt und Probleme mit DoS-basierten Angriffsvektoren behebt. Typischerweise enthalten solche Verträge Vertrauensprobleme hinsichtlich der Macht einer solchen Entität, aber das ist keine Diskussion für diesen Abschnitt.
GovernMental war ein altes Ponzi-Schema, das eine ziemlich große Menge an Ether angehäuft hat. Tatsächlich hatte es zu einem Zeitpunkt 1100 Ether angesammelt. Leider war es anfällig für die in diesem Abschnitt erwähnten DoS-Schwachstellen. Dieser Reddit-Beitrag beschreibt, wie der Vertrag die Löschung eines großen Mappings erforderte, um das Ether abzuheben. Das Löschen dieses Mappings verursachte Gaskosten, die das damalige Block-Gaslimit überschritten, und daher war es nicht möglich, die 1100 Ether abzuheben. Die Vertragsadresse ist 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3, und aus der Transaktion 0x0d80d67202bd9cb6773df8dd2020e7190a1b0793e8ec4fc105257e8128f0506b können Sie ersehen, dass die 1100 Ether schließlich mit einer Transaktion erhalten wurden, die 2.5M gas verbrauchte (nachdem das Block-Gaslimit eine solche Transaktion erlaubte).
Block-Zeitstempel wurden in der Vergangenheit für eine Vielzahl von Anwendungen verwendet, beispielsweise als Entropie für Zufallszahlen (siehe dazu den Abschnitt Entropie-Illusion), zum Sperren von Geldern über Zeiträume sowie für verschiedene zustandsändernde bedingte Anweisungen, die zeitabhängig sind. Miner haben die Fähigkeit, Zeitstempel geringfügig anzupassen, was sich als ziemlich gefährlich erweisen kann, wenn Block-Zeitstempel in Smart Contracts falsch verwendet werden.
Einige nützliche Referenzen hierzu sind: Die Solidity-Dokumentation, diese Stack-Exchange-Frage.
block.timestamp oder sein Alias now kann von Minern manipuliert werden, wenn sie einen Anreiz dazu haben. Konstruieren wir ein einfaches Spiel, das für Miner-Ausbeutung anfällig wäre,
roulette.sol:```solidity
contract Roulette {
uint public pastBlockTime; // Forces one bet per block
constructor() public payable {} // initially fund contract
// fallback function used to make a bet
function () public payable {
require(msg.value == 10 ether); // must send 10 ether to play
require(now != pastBlockTime); // only 1 transaction per block
pastBlockTime = now;
if(now % 15 == 0) { // winner
msg.sender.transfer(this.balance);
}
}
}
Dieser Vertrag verhält sich wie eine einfache Lotterie. Pro Block kann eine Transaktion `10 ether` setzen, um die Chance zu haben, den Kontostand des Vertrags zu gewinnen. Die Annahme hierbei ist, dass `block.timestamp` gleichmäßig über die letzten beiden Ziffern verteilt ist. Wenn das der Fall wäre, gäbe es eine 1/15-Chance, diese Lotterie zu gewinnen.
Wie wir jedoch wissen, können Miner den Zeitstempel anpassen, falls sie es müssen. In diesem speziellen Fall, wenn genug Ether im Vertrag angesammelt ist, hat ein Miner, der einen Block löst, einen Anreiz, einen Zeitstempel zu wählen, bei dem `block.timestamp` oder `now` modulo 15 `0` ist. Dadurch kann er den in diesem Vertrag gesperrten Ether zusammen mit der Blockbelohnung gewinnen. Da pro Block nur eine Person setzen darf, ist dies auch anfällig für [Front-Running](#race-conditions)-Angriffe.
In der Praxis sind Block-Zeitstempel monoton steigend, und daher können Miner keine beliebigen Block-Zeitstempel wählen (sie müssen größer sein als die ihrer Vorgänger). Sie sind außerdem darauf beschränkt, Blockzeiten nicht zu weit in der Zukunft zu setzen, da solche Blöcke wahrscheinlich vom Netzwerk abgelehnt werden (Knoten werden keine Blöcke validieren, deren Zeitstempel in der Zukunft liegen).
<h3 id="block-timestamp-prev">Präventive Techniken</h3>
Block-Zeitstempel sollten nicht für Entropie oder die Erzeugung von Zufallszahlen verwendet werden - d. h. sie sollten nicht der entscheidende Faktor sein (weder direkt noch durch eine Ableitung) für den Gewinn eines Spiels oder die Änderung eines wichtigen Zustands (sofern sie als zufällig angenommen werden).
Zeitsensitive Logik ist manchmal erforderlich, z. B. beim Entsperren von Verträgen (Timelocking), beim Abschluss eines ICO nach einigen Wochen oder beim Durchsetzen von Ablaufdaten. Es wird manchmal empfohlen, `block.number` (siehe die [Solidity-Dokumentation](http://solidity.readthedocs.io/en/latest/units-and-global-variables.html#block-and-transaction-properties)) und eine durchschnittliche Blockzeit zur Zeitschätzung zu verwenden; d. h. `1 Woche` bei einer `10 Sekunden`-Blockzeit entspricht ungefähr `60480 Blöcken`. Daher kann die Angabe einer Blocknummer, bei der ein Vertragszustand geändert wird, sicherer sein, da Miner die Blocknummer nicht so leicht manipulieren können. Der [BAT-ICO](https://etherscan.io/address/0x0d8775f648430679a709e98d2b0cb6250d2887ef#code)-Vertrag hat diese Strategie angewandt.
Dies kann unnötig sein, wenn Verträge nicht besonders um Manipulationen des Block-Zeitstempels durch Miner besorgt sind, aber es ist etwas, dessen man sich bei der Entwicklung von Verträgen bewusst sein sollte.
<h3 id="block-timestamp-example">Praxisbeispiel: GovernMental </h3>
[GovernMental](http://governmental.github.io/GovernMental/) war ein altes Ponzi-Schema, das eine ziemlich große Menge an Ether angesammelt hat. Es war außerdem anfällig für einen zeitstempelbasierten Angriff. Der Vertrag zahlte an den Spieler aus, der in einer Runde als letzter beigetreten war (für mindestens eine Minute). Somit konnte ein Miner, der selbst Spieler war, den Zeitstempel anpassen (auf einen zukünftigen Zeitpunkt, um so aussehen zu lassen, als sei eine Minute vergangen), um den Anschein zu erwecken, dass der Spieler länger als eine Minute als letzter beigetreten war (obwohl dies in Wirklichkeit nicht stimmt). Weitere Details dazu finden sich im [Beitrag zur Geschichte der Ethereum-Sicherheitslücken](https://applicature.com/blog/history-of-ethereum-security-vulnerabilities-hacks-and-their-fixes) von Tanya Bahrynovska.
<h2 id="constructors"><span id="SP-13">13. Vorsicht bei Konstruktoren</span></h2>
Konstruktoren sind spezielle Funktionen, die beim Initialisieren von Verträgen oft kritische, privilegierte Aufgaben ausführen. Vor solidity `v0.4.22` wurden Konstruktoren als Funktionen definiert, die denselben Namen wie der sie enthaltende Vertrag hatten. Wenn also ein Vertragsname während der Entwicklung geändert wird und der Konstruktorname nicht angepasst wird, wird er zu einer normalen, aufrufbaren Funktion. Wie man sich vorstellen kann, kann dies zu einigen interessanten Vertrags-Hacks führen (und hat es bereits getan).
Für weiterführende Lektüre empfehle ich dem Leser, die [Ethernaught Challenges](https://github.com/OpenZeppelin/ethernaut) zu versuchen (insbesondere das Fallout-Level).
<h3 id="constructors-vuln">Die Schwachstelle</h3>
Wenn der Vertragsname geändert wird oder ein Tippfehler im Konstruktornamen dazu führt, dass er nicht mehr mit dem Namen des Vertrags übereinstimmt, verhält sich der Konstruktor wie eine normale Funktion. Dies kann zu ernsten Konsequenzen führen, insbesondere wenn der Konstruktor privilegierte Operationen ausführt. Betrachten Sie den folgenden Vertrag```solidity
contract OwnerWallet {
address public owner;
//constructor
function ownerWallet(address _owner) public {
owner = _owner;
}
// fallback. Collect ether.
function () payable {}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(this.balance);
}
}
Dieser Vertrag sammelt Ether und erlaubt nur dem Eigentümer, das gesamte Ether durch Aufruf der Funktion withdraw() abzuheben. Das Problem entsteht dadurch, dass der Konstruktor nicht exakt nach dem Vertrag benannt ist. Konkret ist ownerWallet nicht dasselbe wie OwnerWallet. Somit kann jeder Benutzer die Funktion ownerWallet() aufrufen, sich selbst als Eigentümer festlegen und anschließend das gesamte Ether im Vertrag durch Aufruf von withdraw() an sich nehmen.
Dieses Problem wurde hauptsächlich im Solidity-Compiler in Version 0.4.22 behoben. Diese Version führte ein Schlüsselwort constructor ein, das den Konstruktor angibt, anstatt dass der Name der Funktion mit dem Vertragsnamen übereinstimmen muss. Es wird empfohlen, dieses Schlüsselwort zur Angabe von Konstruktoren zu verwenden, um Namensprobleme wie oben beschrieben zu vermeiden.
Rubixi (Vertragscode) war ein weiteres Pyramidensystem, das diese Art von Schwachstelle aufwies. Es hieß ursprünglich DynamicPyramid, aber der Vertragsname wurde vor der Bereitstellung in Rubixi geändert. Der Name des Konstruktors wurde nicht geändert, wodurch jeder Benutzer zum creator werden konnte. Eine interessante Diskussion zu diesem Fehler findet sich in diesem Bitcoin-Thread. Letztendlich ermöglichte es Benutzern, um den creator-Status zu kämpfen, um die Gebühren aus dem Pyramidensystem zu beanspruchen. Weitere Details zu diesem speziellen Fehler finden Sie hier.
Die EVM speichert Daten entweder als storage oder als memory. Es wird dringend empfohlen, genau zu verstehen, wie dies geschieht und welche Standardtypen für lokale Variablen von Funktionen gelten, wenn man Verträge entwickelt. Denn es ist möglich, angreifbare Verträge zu erzeugen, indem man Variablen unangemessen initialisiert.
Weitere Informationen zu storage und memory in der EVM finden Sie in der Solidity-Dokumentation: Data Location, Solidity-Dokumentation: Layout of State Variables in Storage, Solidity-Dokumentation: Layout in Memory.
Dieser Abschnitt basiert auf dem hervorragenden Beitrag von Stefan Beyer. Weitere Lektüre zu diesem Thema findet sich ausgehend von Sefans Inspiration, nämlich in diesem Reddit-Thread.
Lokale Variablen innerhalb von Funktionen sind standardmäßig storage oder memory, abhängig von ihrem Typ. Nicht initialisierte lokale storage-Variablen können auf andere unerwartete Storage-Variablen im Vertrag zeigen, was zu absichtlichen (d. h. der Entwickler platziert sie dort absichtlich, um später anzugreifen) oder unbeabsichtigten Schwachstellen führt.
Betrachten wir den folgenden, relativ einfachen Namensregistrierungsvertrag:```solidity // A Locked Name Registrar contract NameRegistrar {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name;
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
Dieser einfache Namensregistrar hat nur eine Funktion. Wenn der Vertrag `unlocked` ist, erlaubt er jedem, einen Namen (als `bytes32`-Hash) zu registrieren und diesen Namen einer Adresse zuzuordnen. Leider ist dieser Registrar anfänglich gesperrt und das `require` in Zeile \[23\] verhindert, dass `register()` Namenseinträge hinzufügt. Es gibt jedoch eine Schwachstelle in diesem Vertrag, die die Namensregistrierung unabhängig von der Variable `unlocked` erlaubt.
Um diese Schwachstelle zu besprechen, müssen wir zunächst verstehen, wie Storage in Solidity funktioniert. Als Überblick auf hoher Ebene (ohne technische Details – ich empfehle, die Solidity-Dokumentation für eine ordentliche Betrachtung zu lesen) werden Zustandsvariablen sequenziell in *Slots* gespeichert, so wie sie im Vertrag erscheinen (sie können gruppiert werden, aber nicht in diesem Beispiel, daher müssen wir uns darum nicht kümmern). Somit existiert `unlocked` in `slot 0`, `registeredNameRecord` in `slot 1` und `resolve` in `slot 2` usw. Jeder dieser Slots hat eine Bytegröße von 32 (es gibt zusätzliche Komplexitäten mit Mappings, die wir vorerst ignorieren). Der boolesche Wert `unlocked` sieht für `false` wie `0x000...0` (64 `0`en, ohne das `0x`) oder für `true` wie `0x000...1` (63 `0`en) aus. Wie man sieht, gibt es in diesem speziellen Beispiel eine erhebliche Verschwendung von Storage.
Die nächste Information, die wir benötigen, ist, dass Solidity komplexe Datentypen wie `structs` standardmäßig auf `storage` setzt, wenn sie als lokale Variablen initialisiert werden. Daher wird `newRecord` in Zeile \[16\] standardmäßig zu `storage`. Die Schwachstelle wird dadurch verursacht, dass `newRecord` nicht initialisiert wird. Da es standardmäßig auf `storage` gesetzt ist, wird es zu einem Zeiger auf Storage, und da es nicht initialisiert ist, zeigt es auf Slot `0` (d. h. dort, wo `unlocked` gespeichert ist). Beachte, dass wir in den Zeilen \[17\] und \[18\] dann `nameRecord.name` auf `_name` und `nameRecord.mappedAddress` auf `_mappedAddress` setzen; dies ändert tatsächlich die Storage-Position von Slot 0 und Slot 1, wodurch sowohl `unlocked` als auch der mit `registeredNameRecord` assoziierte Storage-Slot modifiziert werden.
Das bedeutet, dass `unlocked` direkt modifiziert werden kann, einfach durch den Parameter `bytes32 _name` der Funktion `register()`. Wenn also das letzte Byte von `_name` ungleich Null ist, wird es das letzte Byte des Storage-Slots `slot 0` modifizieren und `unlocked` direkt auf `true` ändern. Solche `_name`-Werte bestehen das `require()` in Zeile \[23\], da wir `unlocked` auf `true` setzen. Probiere dies in Remix aus. Beachte, dass die Funktion erfolgreich ist, wenn du einen `_name` der folgenden Form verwendest: `0x0000000000000000000000000000000000000000000000000000000000000001`
<h3 id="storage-prev">Vorbeugende Techniken</h3>
Der Solidity-Compiler gibt Warnungen für nicht initialisierte Storage-Variablen aus, daher sollten Entwickler diesen Warnungen beim Erstellen von Smart Contracts große Aufmerksamkeit schenken. Die aktuelle Version von mist (0.10) erlaubt es nicht, solche Verträge zu kompilieren. Es ist gute Praxis, die Schlüsselwörter `memory` oder `storage` explizit zu verwenden, wenn man mit komplexen Typen arbeitet, um sicherzustellen, dass sie sich wie erwartet verhalten. Ab Solidity-Version `0.5.0` sind `memory` und `storage` verpflichtend.
<h3 id="storage-example">Beispiele aus der Praxis: Honeypots: OpenAddressLottery und CryptoRoulette</h3>
Ein Honeypot namens OpenAddressLottery ([Vertragscode](https://etherscan.io/address/0x741f1923974464efd0aa70e77800ba5d9ed18902#code)) wurde bereitgestellt, der diese Eigenheit der nicht initialisierten Storage-Variablen ausnutzte, um Ether von einigen angehenden Hackern zu sammeln. Der Vertrag ist ziemlich komplex, daher überlasse ich die Diskussion diesem [Reddit-Thread](https://www.reddit.com/r/ethdev/comments/7wp363/how_does_this_honeypot_work_it_seems_like_a/), wo der Angriff recht klar erklärt wird.
Ein weiterer Honeypot, CryptoRoulette ([Vertragscode](https://etherscan.io/address/0x8685631276cfcf17a973d92f6dc11645e5158c0c#code)), nutzt diesen Trick ebenfalls, um etwas Ether zu sammeln. Wenn du nicht herausfinden kannst, wie der Angriff funktioniert, findest du in [An analysis of a couple Ethereum honeypot contracts](https://medium.com/@jsanjuas/an-analysis-of-a-couple-ethereum-honeypot-contracts-5c07c95b0a8d) einen Überblick über diesen und andere Verträge.
<h2 id="precision"><span id="SP-15">15. Gleitkommazahlen und Präzision</span></h2>
Zum Zeitpunkt dieses Textes (Solidity v0.4.24) werden Festkomma- oder Gleitkommazahlen nicht unterstützt. Das bedeutet, dass Gleitkommadarstellungen mit den Ganzzahltypen in Solidity umgesetzt werden müssen. Dies kann zu Fehlern/Schwachstellen führen, wenn es nicht korrekt implementiert wird.
Für weiterführende Informationen siehe [Ethereum Contract Security Techniques and Tips - Rounding with Integer Division](https://github.com/ethereum/wiki/wiki/Safety#beware-rounding-with-integer-division),
<h3 id="precision-vuln">Die Schwachstelle</h3>
Da es keinen Festkommatyp in Solidity gibt, sind Entwickler gezwungen, einen eigenen mit den standardmäßigen Ganzzahl-Datentypen zu implementieren. Dabei gibt es eine Reihe von Fallstricken, auf die Entwickler stoßen können. Ich werde versuchen, einige davon in diesem Abschnitt hervorzuheben.
Beginnen wir mit einem Codebeispiel (ignorieren wir der Einfachheit halber etwaige Über-/Unterlauf-Probleme).```solidity
contract FunWithNumbers {
uint constant public tokensPerEth = 10;
uint constant public weiPerEth = 1e18;
mapping(address => uint) public balances;
function buyTokens() public payable {
uint tokens = msg.value/weiPerEth*tokensPerEth; // convert wei to eth, then multiply by token rate
balances[msg.sender] += tokens;
}
function sellTokens(uint tokens) public {
require(balances[msg.sender] >= tokens);
uint eth = tokens/tokensPerEth;
balances[msg.sender] -= tokens;
msg.sender.transfer(eth*weiPerEth); //
}
}
Dieser einfache Token-Kauf-/Verkaufsvertrag hat einige offensichtliche Probleme beim Kauf und Verkauf von Token. Obwohl die mathematischen Berechnungen für den Kauf und Verkauf von Token korrekt sind, führt das Fehlen von Gleitkommazahlen zu fehlerhaften Ergebnissen. Wenn beispielsweise beim Kauf von Token in Zeile [7] der Wert weniger als 1 ether beträgt, ergibt die anfängliche Division 0, sodass die abschließende Multiplikation 0 bleibt (d. h. 200 wei geteilt durch 1e18 weiPerEth ergibt 0). Ebenso führt beim Verkauf von Token jeder Token-Wert unter 10 ebenfalls zu 0 ether. Tatsächlich wird hier immer abgerundet, sodass der Verkauf von 29 Tokens 2 ether ergibt.
Das Problem bei diesem Vertrag besteht darin, dass die Genauigkeit nur auf den nächsten ether (d. h. 1e18 wei) reicht. Das kann manchmal knifflig werden, wenn man mit decimals bei ERC20 Token arbeitet und eine höhere Genauigkeit benötigt.
Die richtige Genauigkeit in Ihren Smart Contracts beizubehalten ist sehr wichtig, insbesondere wenn Sie mit Verhältnissen und Raten arbeiten, die wirtschaftliche Entscheidungen widerspiegeln.
Sie sollten sicherstellen, dass alle Verhältnisse oder Raten, die Sie verwenden, große Zähler in Brüchen zulassen. Zum Beispiel haben wir in unserem Beispiel die Rate tokensPerEth verwendet. Es wäre besser gewesen, weiPerTokens zu verwenden, was eine große Zahl wäre. Um die Menge an Token zu ermitteln, könnten wir msg.value/weiPerTokens verwenden. Dies würde ein präziseres Ergebnis liefern.
Eine weitere Taktik, die Sie im Hinterkopf behalten sollten, ist die Reihenfolge der Operationen zu beachten. Im obigen Beispiel lautete die Berechnung zum Kauf von Token msg.value/weiPerEth*tokenPerEth. Beachten Sie, dass die Division vor der Multiplikation erfolgt. Dieses Beispiel hätte eine höhere Genauigkeit erreicht, wenn die Berechnung zuerst die Multiplikation und dann die Division durchgeführt hätte, also msg.value*tokenPerEth/weiPerEth.
Schließlich ist es bei der Definition beliebiger Genauigkeit für Zahlen eine gute Idee, Variablen in eine höhere Genauigkeit zu konvertieren, alle mathematischen Operationen durchzuführen und sie schließlich bei Bedarf wieder auf die Genauigkeit für die Ausgabe zu reduzieren. Typischerweise werden uint256 verwendet (da sie optimal für den Gasverbrauch sind), die ungefähr 60 Größenordnungen in ihrem Bereich bieten, von denen einige der Genauigkeit mathematischer Operationen gewidmet werden können. Es kann der Fall sein, dass es besser ist, alle Variablen in Solidity in hoher Genauigkeit zu halten und in externen Apps wieder in niedrigere Genauigkeiten zu konvertieren (im Wesentlichen funktioniert die Variable decimals so in ERC20 Token Verträgen). Um Beispiele zu sehen, wie das gemacht werden kann und welche Bibliotheken dafür geeignet sind, empfehle ich einen Blick auf Maker DAO DSMath. Sie verwenden ausgefallene Namen, WADs und RAYs, aber das Konzept ist nützlich.
Ich konnte kein gutes Beispiel finden, bei dem Rundung ein schwerwiegendes Problem in einem Vertrag verursacht hat, aber ich bin sicher, es gibt viele da draußen. Fühlen Sie sich frei, dies zu aktualisieren, wenn Sie ein gutes Beispiel im Kopf haben.
Mangels eines guten Beispiels möchte ich Ihre Aufmerksamkeit auf Ethstick lenken, hauptsächlich weil mir die coole Benennung innerhalb des Vertrags gefällt. Dieser Vertrag verwendet keine erweiterte Genauigkeit, arbeitet jedoch mit wei. Dieser Vertrag wird also Rundungsprobleme haben, aber nur auf der wei-Genauigkeitsebene. Er hat einige ernsthaftere Schwachstellen, die jedoch auf die Schwierigkeit zurückzuführen sind, Entropie auf der Blockchain zu erhalten (siehe Entropie-Illusion). Für eine weitere Diskussion über den Ethstick-Vertrag verweise ich Sie auf einen weiteren Beitrag von Peter Venesses, Ethereum Contracts Are Going to be Candy For Hackers.
Solidity hat eine globale Variable, tx.origin, die den gesamten Aufrufstapel durchläuft und die Adresse des Kontos zurückgibt, das den Aufruf (oder die Transaktion) ursprünglich gesendet hat. Die Verwendung dieser Variable zur Authentifizierung in Smart Contracts macht den Vertrag anfällig für einen Phishing-artigen Angriff.
Für weiterführende Lektüre siehe Stack-Exchange-Frage, Peter Venesses' Blog und Solidity – Tx.Origin-Angriffe.
Verträge, die Benutzer mithilfe der Variable tx.origin autorisieren, sind typischerweise anfällig für Phishing-Angriffe, die Benutzer dazu verleiten können, authentifizierte Aktionen auf dem anfälligen Vertrag auszuführen.
Betrachten Sie den einfachen Vertrag,```solidity contract Phishable { address public owner;
constructor (address _owner) {
owner = _owner;
}
function () public payable {} // collect ether
function withdrawAll(address _recipient) public {
require(tx.origin == owner);
_recipient.transfer(this.balance);
}
}
Beachte, dass dieser Vertrag in Zeile \[11\] die Funktion `withdrawAll()` mithilfe von `tx.origin` autorisiert. Dieser Vertrag ermöglicht es einem Angreifer, einen Angriffsvertrag der folgenden Form zu erstellen,```solidity
import "Phishable.sol";
contract AttackContract {
Phishable phishableContract;
address attacker; // The attackers address to receive funds.
constructor (Phishable _phishableContract, address _attackerAddress) {
phishableContract = _phishableContract;
attacker = _attackerAddress;
}
function () payable {
phishableContract.withdrawAll(attacker);
}
}
Um diesen Vertrag zu nutzen, würde ein Angreifer ihn bereitstellen und dann den Eigentümer des Phishable-Vertrags davon überzeugen, diesem Vertrag eine gewisse Menge Ether zu senden. Der Angreifer könnte diesen Vertrag als seine eigene private Adresse tarnen und das Opfer durch Social Engineering dazu bringen, eine Transaktion an die Adresse zu senden. Das Opfer könnte, sofern es nicht aufmerksam ist, nicht bemerken, dass sich an der Adresse des Angreifers Code befindet, oder der Angreifer könnte es als Multisignatur-Wallet oder ein fortschrittliches Speicher-Wallet ausgeben (denken Sie daran, dass der Quellcode öffentlicher Verträge standardmäßig nicht verfügbar ist).
In jedem Fall wird die Fallback-Funktion ausgelöst, wenn das Opfer eine Transaktion (mit ausreichend Gas) an die Adresse des AttackContract sendet. Diese ruft wiederum die Funktion withdrawAll() des Phishable-Vertrags mit dem Parameter attacker auf. Dies führt dazu, dass alle Gelder vom Phishable-Vertrag an die Adresse attacker abgezogen werden. Der Grund dafür ist, dass die Adresse, die den Aufruf ursprünglich initiiert hat, das Opfer war (d. h. der owner des Phishable-Vertrags). Daher ist tx.origin gleich owner und die require-Bedingung in Zeile [11] des Phishable-Vertrags wird erfüllt.
Attack.sol - Zeile [26] - Der Gesamtkontostand des EtherStore-Vertrags betrug 10 ether und beträgt jetzt 9 ether, sodass diese if-Anweisung erfüllt ist.
Attack.sol - Zeile [27] - Die Fallback-Funktion ruft dann erneut die Funktion withdrawFunds() von EtherStore auf und "tritt erneut ein" in den EtherStore-Vertrag.
EtherStore.sol - Zeile [11] - Bei diesem zweiten Aufruf von withdrawFunds() beträgt unser Kontostand immer noch 1 ether, da Zeile [18] noch nicht ausgeführt wurde. Somit gilt weiterhin balances[0x0..123] = 1 ether. Dasselbe gilt für die Variable lastWithdrawTime. Wieder erfüllen wir alle Anforderungen.
EtherStore.sol - Zeile [17] - Wir heben noch einmal 1 ether ab.
Die Schritte 4-8 wiederholen sich - bis EtherStore.balance >= 1 gilt, wie durch Zeile [26] in Attack.sol vorgegeben.
Attack.sol - Zeile [26] - Sobald im EtherStore-Vertrag weniger als 1 Ether (oder weniger) übrig ist, schlägt diese if-Anweisung fehl. Dadurch können dann die Zeilen [18] und [19] des EtherStore-Vertrags ausgeführt werden (für jeden Aufruf der Funktion withdrawFunds()).
EtherStore.sol - Zeilen [18] und [19] - Die Mappings balances und lastWithdrawTime werden gesetzt und die Ausführung endet.
startFibonacciBalancestartslot[1]fibonacci(n)fibonacciLibraryuintwithdraw()uint(fibonacciLibrary)calculatedFibNumberfibonacciLibrary