Attack on Pseudo-random number generator(PRNG) used in Cryptogs, an Ethereum (CVE-2018–14715)

Abstract

Cryptogs is the game of pogs on the Ethereum blockchain[1]. It is a kind of game of slap-match. This game generates random numbers using blockhash(uint blockNumber) to decide the winner. In Solidity, blockhash(uint blockNumber) function returns ‘0’ when blockNumber is older than 256 blocks from the current block(block.number)[2]. Cryptogs doesn’t check the blockNumber whether it is too old or not, so attackers can predict the random numbers and always win.

Details

There are two functions generating random numbers: endCoinFlip() and throwSlammer(). Both functions required preliminary functions: startCoinFlip() and raiseSlammer().

First, let’s check startCoinFlip() and endCoinFlip(). The two functions decide who goes first.

Figure 1. startCoinFlip()

In startCoinFlip(), it stores the current block number block.number into commitBlock[_stack].

Figure 2. endCoinFlip()

After calling startCoinFlip(), players should call endCoinFlip(). In endCoinFlip(), it generates random numbers using keccak256() with _reveal and blockhash of commitBlock[_stack]. As you can see in Figure 2, at line 464, uint32(block.number) > commitBlock[_stack] is the only condition for commitBlock[_stack]. Therefore, players can call endCoinFlip() after long periods of time. In Solidity, however, blockhash(uint blockNumber) is only works for 256 most recent blocks[2].

blockhash(uint blockNumber) returns ‘0’ if blockNumber is older than 256 blocks from the current block. Therefore, if a player calls endCoinFlip() after 256 blocks from calling startCoinFlip(), the pseudoRandomHash is same with keccak256(_reveal, 0) because block.blockhash(commitBlock[_stack]) is ‘0’. _reveal is an argument of endCoinFlip(), so players can put anything they want. Therefore, attackers can generate random numbers and get the first order.

Next, raiseSlammer() and throwSlammer() have the same vulnerability. In raiseSlammer(), the current block number block.number is stored into commitBlock[_stack].

Figure 3. raiseSlammer()

Then, pseudoRandomHash is computed the same way as endCoinFlip() at line 554.

Figure 4. a part of throwSlammer()

At line 573, one byte of pseudoRandomHash is selected as thisFlipper. At line 575, thisFlipper is checked whether it is under 80 or not. If it is under 80, one pog is flipped and the player in that turn gets the pog.

Just like endCoinFlip(), if an attacker call throwSlammer() after 256 blocks from calling raiseSlammer(), keccak256(_reveal, block.blockhash(commitBlock[_stack]) is same with keccak256(_reveal, 0). So, attacker can generate pseudoRandomNumber as they want and get the all pogs.

Exploit

So, the attack scenario is as follows:

  1. Create a new game and wait until a victim join the game.
  2. Call startCoinFlip() and wait until 256 blocks are generated
  3. After 256 blocks, call endCoinFlip() and get the first order
  4. Call raiseSlammer() and wait until 256 blocks are generated
  5. After 256 blocks, call throwSlammer() and get all pogs at the first turn

If we want to proceed as the above scenario, we should compute the proper pseudoRandomNumber that satisfying the following conditions:

  1. even → get the first order
  2. all 10 bytes of most significant bytes are under 80 → get all 10 pogs at the first turn

Because of the vulnerability, pseudoRandomNumber is computed as keccak256(_reveal, 0).So, I found when _reveal is ‘0x20182’, pseudoRandomNumber is ‘0x262f33140b273901193bc3cc78c5c7f6abf498af8cc1faafc5d5454dd63e3dec’ which is satisfying the above two conditions.

I exploited Cryptogs in Ropsten test network[3].

Figure 5. exploit result

Figure 5 shows the exploit result that I recorded. As you can see, I can get all 10 pogs at the first turn.

Report

I reported it to the developer and he replied to me very quickly. He said that he probably will not fix the vulnerability because there are no players in Cryptogs. So, I do not recommend playing Cryptogs. But, he said he will check his new game, Galleass, whether there is the same vulnerability.

Surprisingly, he gave me some bug bounty. At first I refused the bounty but he kept trying to give the bounty to me, so I finally received the bounty. I found several vulnerable smart contracts so far, but he is the first developer who give the bounty. As a security researcher, I think there should be more people like the developer of Cryptogs. So, I hope that bug hunters get the proper compensation.

Conclusion

When you generates random numbers using blockhash, you should remind that blockhash(uint blockNumber) is only works for 256 most recent blocks. If you try to use blockhash with too old block, it will return ‘0’.

References

[1] https://cryptogs.io

[2] https://solidity.readthedocs.io/en/latest/units-and-global-variables.html

[3] https://ropsten.etherscan.io/tx/0xed84edc7644eb4da5f91182b8bb638e9e7ad8cb2f17ebfec9d14ecca542a3a9c