Get legendary items by breaking PNRG of MyCyptoChamp, an Ethereum online game (CVE-2018–12885)
Abstract
An Ethereum online game, MyCryptoChamp, has a vulnerability that it generates predictable random numbers. In this game, users can create champs and items by paying some Ether. At this time, the power of the champs and items are decided by the random numbers. The random numbers are generated using by keccak256()
function with a private variable and a blockhash of block.number-1
. However, they are all public, so attackers can easily access them and precompute the random number. This case is so similar with 1000 Guess’s vulnerability that I described before[1]. I exploited MyCryptoChamp using the internal transactions in the way I exploited 1000 Guess. You can find more detail explanation in this article: “Attack on Pseudo-random number generator (PRNG) used in 1000 Guess, an Ethereum lottery game. (CVE-2018–12454)”. After the exploitation, I got a powerful champ with three legendary items.
Details
As I mentioned before, this vulnerability is similar to 1000 Guess’s vulnerability. If you don’t know how to read private variables and how to exploit using internal transactions, I recommend you reading my previous article[1]. There are more details.
Vulnerable codes
randMod
function is the vulnerable function of MyCryptoChamp’s smart contract. It generates random numbers using keccak256
with a private variable randNonce
and blockhash(block.number-1)
. Both are public, so anyone can access.
Figure 2 and Figure 3 show that MyCryptoChamp generates champs and items using random numbers. Power of champs/items depends on the random numbers. Therefore, if an attacker can predict the random number, he can create most powerful champs and items.
Exploits
The way to exploit MyCryptoChamp is simple. Attackers precompute the random number by using randNonce
and blockhash(block.number-1)
. When the desired random number is generated, attackers send transactions to MyCryptoChamp and get champs/items.
First, let’s find out how to get randNonce
. we can read private variables like this[2]:
web3.eth.getStorageAt(contractAddress, position)
position
is index position of a variable. We can retrieve all storage variables in smart contract using the above way. In MyCryptoChamp, randNonce
is located at slot 11. Therefore, we can get randNonce
when contractAddress
is the address of MyCryptoChamp’s smart contract and position
is 11.
Now, all we have to do is finding out blockhash(block.number-1)
. To know it accurately, we should know the block including the transaction that we sent. If I send a transaction at block number N
, the transaction will be executed at block number N+a
because it takes some time that miners select and execute transactions. However, nobody exactly knows the a
. Therefore, before we send transactions, we cannot know the block number of the block executing our transactions.
Then, how we get blockhash(block.number-1)
and precompute the random number?. We can know the block number of the block executing our transactions only in the smart contract. Therefore, we should deploy a smart contract and precompute the random number in that contract. This smart contract should satisfy the following conditions:
- generate random numbers in the same way as target contract
- send internal transactions to the target contract if it generates the random number that attackers want
- revert if it generates the random numbers that attackers don’t want
To attack MyCryptoChamp, attackers need the contract that satisfying the above conditions. Because of 3 condition, attackers should continuously send transactions until they get the random numbers they want. Therefore, some gas costs are consumed. However, in most cases, it is a tiny compared to the benefits that they will get when their attack is success.
To exploit MyCryptoChamp, I deployed the following contract:
In createChp
function, the first random number is for the attack power and the second is for the defense power. In createItm
function, the first random number is for the rarity of the item and the second is for the item type. Both functions call MyCryptoChamp’s function when they generates the random numbers that attackers want. The random numbers generated in the attack contract is same with the random numbers generated in the contract of MyCryptoChamp because both are generated at the same block.
Figure 5. shows the champs and items that I created by exploiting this vulnerability. I created powerful champ and three legendary items.
Report
I reported it to the developers at end of June. Surprisingly, they returned back to me the gas cost that I spent on the exploit.
Now MyCryptoChamp is patched.
- Old smart contract: https://etherscan.io/address/0xa44e464b13280340904ffef0a65b8a0033460430
- New smart contract: https://etherscan.io/address/0x689FB61845488297dfE7586E5f7956475955d2Dc
They removed my champ and three legendary items. Actually, I cannot find the source code of the new smart contract, so I’m not sure it is safe now.
Conclusion
Generating random numbers in Ethereum smart contract is not easy. It is not safe using private variables and block variables of past blocks. I recommend to you read my previous article if you want to know more details[1]. If there are smart contracts generating random numbers, you should check it is completely random.
Reference
[2] https://web3js.readthedocs.io/en/1.0/web3-eth.html#getstorageat