Create legendary champs by breaking PRNG of Cryptosaga, an Ethereum RPG game (CVE-2018–12975)
--
Abstract
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, block.blockhash(block.number)
, and 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 [1]).
Details
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 random()
function.
Figure 3 shows the random()
function. It generates random numbers by keccak256()
function with block.blockhash(block.number)
, seed
, and now
.
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[4].
Therefore, 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:
web3.eth.StorageAt(0x6a5309dc905e85ce88f33bcd4d4d9a03ac68f847, 11)
Now, all we have to do is predict now
. 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[1] and MyCryptoChamp[3]. 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 revert()
. attack()
only calls payWithEth()
function when the transaction is executed at the time we want.
Exploit
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.
Conclusion
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.
References
[4] https://solidity.readthedocs.io/en/latest/units-and-global-variables.html