Attack on Pseudo-random number generator (PRNG) used in 1000 Guess, an Ethereum lottery game (CVE-2018–12454)

Abstract

An Ethereum lottery game, 1000 Guess, has a vulnerability that it generates predictable random numbers. This game decides a winner by a random number when the number of players who bet on the contract reaches to the predetermined number. The contract generates the random number using sha256() function with a private variable and the current block variables, such as block.timestamp, block.coinbase and block.difficulty. However, they are easily readable. First, a private variable is easily accessible by using web3.eth.getStorageAt. Second, it is well known that block variables can be easily manipulated by malicious miners. However, it will be also dangerous even though attackers are not miners. In this article, I will explain how to attack PRNG by using internal transactions.

Details

Figure 1. 1000 guess generates a random number with block variables and a private variable

1000 Guess generates a random number using sha256() function with block.timestamp, block.coinbase, block.difficulty and a private variable curhash. These are all accessible to anyone, so anyone can precompute the random number and can be a winner.

How to access a private variable?

First, how can we access to a private variable? Before that, let’s find out how to access a public variable. Accessing a public variable is very easy and intuitive. If there is a public variable uint data, we can access it in web3js like this:

contractInstance.methods.data().call()

Or, we can call a function that returns data, if exists,

function getData() public returns (uint){
return data;
}

In web3js, we call getData() to get data like this

contractInstance.methods.getData().call()

However, we cannot use the above methods to access a private variable. For this reason, some people think that a private variable is hidden. In Solidity documents, however, it said that everything is visible to all observers even though it is private [1].

Figure 2. In a contract, everything is visible to all external observers

Then, how to can we access to a private variable? In web3js, we can access a private variable like this [2]:

web3.eth.getStorageAt(contractAddress, position);

We can retrieve all variables in smart contracts using the above way. In a smart contract, variables are sequentially located at storage slot. For example, the first declared variable is located at slot 0, the second variable is located at slot 1. Therefore, if we want to read curhash, we should know the position of curhash.

Figure 3. variables declaration in the smart contract of 1000 Guess

The first variable is state, so curhash is located at slot 1004. constant variable is not stored in storage slot, so uint constant maxguess is excluded. Therefore, we can read curhash as follows:

web3.eth.getStorageAt(contractAddress, 1004);

When I write this post, I tried to read curhash and other variables using the following code. I tested on 0x386771ba5705da638d889381471ec1025a824f53 among several versions of 1000 Guess.

Figure 4. Example code of accessing variables

The result is as follows:

Figure 5. State of variables

From the results, we can know the current state of numguesses, curhash, _gameindex and _starttime. You can confirm the above results using getBettingStatus() function.

Generating random numbers with same block variables

Now, we know how to read the private variable, so we just compute curhash using the current block variables. It is clear that the current block variables are accessible. However, you cannot know the block number what your transaction is executed. For example, there is a smart contract that you can be a winner when you send a transaction at the block number is even. Simply thinking, you continuously observe the current block number, and then send a transaction to the contract when the block number is even. However, it can not be success always because your transaction is not executed at the block what you send a transaction. Therefore, if you want that your transaction is executed at a specific block, you should use another way.

Internal transaction can be a solution. First, you compute new curhash in your contract, and then calls 1000 Guess contract via an internal transaction in your contract. Then, the same curhash is calculated because the two computations are executed at the same block.

Exploit

I exploited one of 1000 Guess contracts: https://etherscan.io/address/0x386771ba5705da638d889381471ec1025a824f53. In this contract, bettingprice is 0.01 Ether and arraysize is 10. I prepared two accounts: a victim and an attacker.

First, the victim account continuously bet 0.01 Ether on 1000 Guess contract until numguesses is 9 which is arraysize-1. In this state, if someone bet on this contract, _finish() will be executed and then _winner will be decided by new curhash.

Second, the attack account deployed an attack contract as follows:

Figure 6. Attacker’s contract

attack() function needs 4 parameters:

  • address target: the address of 1000 Guess contract.
  • curhash: the current curhash in 1000 Guess contract
  • arraysize: the current arraysize
  • attackeridx: index of attacker address in guessess array

In the attack contract, it computes curhash in the same way with 1000 Guess contract. The attack account sent transactions to the attack contract continuously until the contract computes curhash that makes the attacker as a winner. After several trials, finally an internal transaction was transmitted and the attack account got the rewards.

Figure 7. Internal transactions executed on the target contract

I tested two times and I succeeded two times. Figure 7 shows the internal transactions. ‘0x48e9c58bb66d0…‘ is the attack contract and ‘0x1973f023e4c03ef…’ is the developer of 1000 Guess. When the attack was succeeded, 1000 Guess sent reward (0.099 Ether) to the attack contract and sent a fee (0.001 Ether) to the developer.

You can check the results on the followings:

You can see that there are several failed transactions because the attack contract executes revert() when curhash computed by the contract cannot make the attacker as a winner. However, if it sends an internal transaction to the target contract, it gets the reward without fails. Of course the attacker should pay some gases because he tried several times, but it is a tiny compared to the reward he will get.

When I exploited on the 1000 Guess contract, it contains only 0.01 Ether sent by the developer. So, after my exploit, I returned 0.01 Ether to the developer.

Report

I reported it to the developer. Then, he closed all 1000 Guess games immediately.

Figure 8. The developer closed the game

The site, http://www.1000guess.com, is completely down now. Now, you can only check Twitter account of 1000 Guess: https://twitter.com/1000guess.

Conclusion

Generating a random number in Ethereum smart contract is not easy. As I know, there are two methods to generate the random number safely. First way is using future block’s variables, and the other one is getting random number from outside of Ethereum network using libraries like Oraclize[3]. Developers should study how to generate the random numbers safely. Also, users should check how the contract generates random numbers, if they want to use the contract.

Reference

[1] http://solidity.readthedocs.io/en/v0.4.24/contracts.html

[2] https://web3js.readthedocs.io/en/1.0/web3-eth.html#getstorageat

[3] http://www.oraclize.it/