Create legendary champs by breaking PRNG of Cryptosaga, an Ethereum RPG game (CVE-2018–12975)
Cryptosaga is a RPG game on Ethereum blockchain. Users buy their heroes by Ethereum and fight with monsters in dungeons to level up. There are 4 classes in heroes. When a user buys a hero, the class of the hero is determined by random number. The random numbers are generated with a private variable,
now. These are all accessible by anyone, so attacker can predict the random numbers and create legendary heroes. For a better understanding, I recommend to read my previous posts[1,2,3]. There is a more detail descriptions (especially in ).
Figure 1 shows a part of
payWithEth() function that generates heroes and Figure 2 shows
summonHero() function. As you can see,
_heroRankToMin is a class of hero. If
_heroRankToMin is ‘4', a legendary hero will be generated.
_heroRankToMin is depends on
_randomValue which is generated by
Figure 3 shows the
random() function. It generates random numbers by
keccak256() function with
block.blockhash(block.number) is a hash of the current block. It is always ‘0’. In Solidity,
blockhash() function is only works for 256 most recent blocks, except the current block.
keccak256(keccak256(block.blockhash(block.number), seed), now)) is same with
keccak256(keccak256(0, seed), now).
seed is a private variable that is declared at 11th among the contract storage variables. Therefore, we can access private variables like this:
Now, all we have to do is predict
now is same with
block.timestamp. We should know the exact timestamp to make new
seed more than 9950.
I implemented the above code (Figure 4) to get proper
now. The example results of the code are as follows:
Figure 5 shows the several
now that makes
seed more than 9950. It means that if our transaction is executed at the time among one of the above
now, we can make a legendary hero. However, we cannot predict when our transaction is mined by the miner. So, we cannot decide the time when our transaction is executed by the miner. Therefore, I used internal transactions again, just like I did in 1000 Guess and MyCryptoChamp. I deployed a smart contract as follows:
Figure 4 shows the smart contract to exploit Cryptosaga. We just call
attack() function with
time that we want. If
attack() is executed at the time that we don’t want, it calls
attack() only calls
payWithEth() function when the transaction is executed at the time we want.
Through the exploit contract, we can create heroes at the time we want. However, it is very difficult to avoid
revert() in the
attack() function because the transactions should be executed at exactly the same time as the
time I gave it as an argument. So, it requires many trials to succeed. However, luckily I succeeded in three attempts.
Then, I got a legendary hero.
Honestly, after the success, I tried more than 50 attempts to get more legendary heroes but I failed. It is possible attack but not easy.
When you use
blockhash(), you should always remind that it only works for 256 most recent blocks, except the current block. It will return ‘0’ when you try to use it with the current block.