Attack on Pseudo-random number generator(PRNG) used in Cryptogs, an Ethereum (CVE-2018–14715)
Cryptogs is the game of pogs on the Ethereum blockchain. 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). Cryptogs doesn’t check the
blockNumber whether it is too old or not, so attackers can predict the random numbers and always win.
There are two functions generating random numbers:
throwSlammer(). Both functions required preliminary functions:
First, let’s check
endCoinFlip(). The two functions decide who goes first.
startCoinFlip(), it stores the current block number
startCoinFlip(), players should call
endCoinFlip(), it generates random numbers using
_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.
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
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.
throwSlammer() have the same vulnerability. In
raiseSlammer(), the current block number
block.number is stored into
pseudoRandomHash is computed the same way as
endCoinFlip() at line 554.
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.
endCoinFlip(), if an attacker call
throwSlammer() after 256 blocks from calling
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.
So, the attack scenario is as follows:
- Create a new game and wait until a victim join the game.
startCoinFlip()and wait until 256 blocks are generated
- After 256 blocks, call
endCoinFlip()and get the first order
raiseSlammer()and wait until 256 blocks are generated
- 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:
- even → get the first order
- 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.
Figure 5 shows the exploit result that I recorded. As you can see, I can get all 10 pogs at the first turn.
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.
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’.