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.
In startCoinFlip()
, it stores the current block number block.number
into commitBlock[_stack]
.
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]
.
Then, 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.
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:
- Create a new game and wait until a victim join the game.
- Call
startCoinFlip()
and wait until 256 blocks are generated - After 256 blocks, call
endCoinFlip()
and get the first order - Call
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[3].
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
[2] https://solidity.readthedocs.io/en/latest/units-and-global-variables.html
[3] https://ropsten.etherscan.io/tx/0xed84edc7644eb4da5f91182b8bb638e9e7ad8cb2f17ebfec9d14ecca542a3a9c