This is our final free chapter in this smart contract hacking series, hopefully you enjoyed it, I am not sure what I am going to work on next, perhaps some malware analysis, reverse engineering or maybe some hacking in the cloud.
We are currently in 4th quarter and slammed with work so I wouldn't expect any more posts or the full blockchain release till after that eases up.
If you have any questions or comments you can hit us up at:
Cryptographic Implementations and Predictable PRNGs
Within operations that require random values we generally
need a form of randomness coupled with our algorithm. If we do not have
sufficient randomness and large character sets, we would end up with
cryptographic collisions or predictable values depending what we are doing. This
Is often the case in video game operations and data security encryption
schemes. For example, we do not want to create random values which are
predictable and repeatable based on known values or controllable values. With
controllable values an attacker could duplicate the value by reverse
engineering how it was originally created and what that random seed is. Also,
If the value is predictable within a game, we may be able to cheat the game by
creating our own valid values that exploit the perceived randomness.
Now we are not going to deep dive into cracking cryptography
or brute forcing hash values. First off it takes too much time and effort. Secondly
because there are easier more efficient ways of tackling cryptographic issues. Lastly,
we do not have time for rabbit holes in a week-long penetration test that require
us to explore many other attack vectors. Wasting a whole week on cracking a
single cryptographic issue would be a terrible and inefficient penetration test
leaving the rest of the target vulnerable. This may be suitable for R&D or
a CTF but not for a penetration test.
What you need to understand is that certain functions often
used as randomness on the blockchain is not suitable as a source of randomness.
Additionally, understanding how things are implemented will get you much
farther when it comes to cryptography then attacking it directly. You do not
need to break NSA level encryption by attacking it directly. Instead you should
concentrate on finding insecure implementations of these algorithms to get what
you need.
Oracle padding attacks are a great example of this if you
were in the hacking community back in the late 2000s. The padding attack relied
on error messages based on padding within blocks to determine a way to decrypt
them. This was a brilliant attack vector as you didn’t need to understand deep cryptographic
concepts to decrypt data blocks only how blocks work and how it was implemented.
With this knowledge you could leverage
the flawed implementation to get the decrypted values.
On the blockchain there are a number of insecure functionality
that developers like to use when implementing random values. Most of these are
very bad ideas for reasons we will discuss below.
For Example, the following non-exhaustive but often used list
of values are not suitable for randomness within sensitive operations. Usage of
these types of values for any sort of calculation is always suspect for closer
review:
ü Secret keys in private variables
ü Block Timestamps
ü Block Numbers
ü Block Hash values
Why you ask? Well regardless of the data being set as
private on the blockchain a private variable storage value is 100% readable on
the blockchain. There are no secret values. These can be queried as you saw in
the storage issues chapter. Also embedding hard coded values are certainly not
private as they are in the source code which may be posted directly on the
blockchain. Or could be reverse engineered out of the bytecode used to deploy
the contract when the source code is not available. If you can get a hold of
that value, then you can violate the security of that functionality.
Secondly do not rely on predictable values for randomness
especially from block data sources. Block timestamps are controlled by miners
which can aid in orchestrated attacks when used as a source of randomness. Also
block numbers are easy to query and create predictable attacks when used in calculations,
if internal functions are using a block number, they are all using the same
PRNG. Finally, block hash values are terrible to use for randomness as only the
last 256 block hash values on chain actually have a real value. Anything older than
256 is reduced to 0 meaning that every calculation will use the same value of
0. We will cover that in some of our examples.
This is not an exhaustive list but instead just a small
portion of bad decisions for random values. There are plenty of other values
which could be used within calculations as a random seed which are also
predictable. It is always important to review the data used in these
calculations when reviewing smart contract functionality. So, without the need
of a PHD in cryptography you should easily discern that all of the above
implementation examples are terrible for the inclusion of random data within cryptographic
operations.
Simple BlockHash Example
Let’s start out taking a look at a simple example of using a
blockhash value with a blocknumber value. While a hash of a block might seem
like a good idea as a random number there are numerous issues with it. Firstly,
a blocknumber is a known value set by a miner that persists for a set length of
time and can be queried and used in an attacker’s similar algorithm to produce
the same result and bypass controls. But there is also an underlying vulnerability
to this approach when coupled with a blockchash which we will take a look at
below.
Action Steps:
ü Open up your terminal and launch ganache-cli
ü Type out the code below into Remix
ü Within the Deploy Environment section dropdown change the JavaScript VM to the web3 Provider option.
ü Deploy the contract to ganache with the deploy button in Remix
1. pragma solidity ^0.6.6;
2.
3. contract simpleVulnerableBlockHash {
4. uint32 public block_number;
5. bytes32 public myHash;
6.
7. function get_block_number() public {
8. block_number = uint32(block.number);
9. }
10.
11. function set_hash() public{
12. myHash = bytes32(blockhash(block_number));
13. }
14.
15. function wasteTime() public{
16. uint test = uint(block.number);
17. }
18. }
The simple contract above is querying for the current block number
in the get_block_number function on line 8 and storing it within a block_number
variable created on line 4. This is the
current block number running on the blockchain.
Then we have a function on line 11 which takes the block
number and uses it with the blockhash button to retrieve the blockhash and
store it in the myHash variable.
BlockHash Vulnerability Walk and Talk:
Action Steps:
ü Execute the get_block_number function
ü Execute the set_hash function
ü Check the block_number value
ü Check the myHash value
ü Execute the wasteTime function 256 times
ü Execute the set_hash function
ü Check your myHash Value
ü What happened and what implications would this have on calculations your using this value with?
So, we have 2 variables of a block number and a block hash
associated with that block number. What’s the big deal. Well let’s walk through
this step by step and then play around with the remaining wasteTime function on
line 15 to find out.
Starting out if we have the deployed contract and we execute
the get_block_number function followed by the set_hash function we will get the
following result when checking the block_number and myHash variables.
We see the blocknumber of 3 and then a hex value
representing the block hash that starts with 0x995f. Now if we were to use this
hash as a random value or within some algorithm to create a random value it
might work depending what we were doing and the level of security required for
the length of time we need it to be perceived as random for. It wouldn’t be
secure but maybe good enough for your operations. However, a blockhash has a dark little secret
a developer may not be aware of. Block
hashes in Ethereum have short term memory when it comes to blocks older than
256 from the current block.
So, what happens when we calculate a block after a time
lapse? Let’s give that a try by executing the wasteTime button till we reach
block 259. Waste time sets a block value
and discards it to enumerate blocks for us, it doesn’t actually make any real
changes. Normally blocks on the Ethereum network enumerate on their own every
30 seconds and we would simply just wait for 256 blocks, but we don’t have
traffic on our blockchain so we will enumerate it ourselves with wasteTime.
After we reach block 259 we execute the set_hash function
again which will take block_number of 3 which is older than 256 blocks and get
the hash. If you retrieve the myHash variable again after executing the
set_hash function again it results in:
You will notice the myHash variable is now 0x000. because
blocks older than 256 from the current block are not stored and result in a value
of 0. Having a predictable value of 0 in
our random algorithm can very likely create a situation where it would be easy
to recreate the random number to bypass or cheat functionality in the smart
contract.
Video Walkthrough of Bad Randomness:
A classical terrible example is something similar to this.
1. Function checkWinner() public payable {
2. If (blockhash(blockNumber) % 2 == 0) {
3. Msg.sender.transfer(balance);
4. }
5. }
In the example above uses a blockhash function with a
blockNumber variable within its calculation. The issue with this calculation is
if that blockNumber variable is more than 256 blocks old it will return Zero
and based on the calculation the user will win every single time.
All the attacker would need to do is play the game to create the blocknumber variable. Then the attacker would simply wait for 256 blocks to pass before checking if he has won the game. By doing this the attacker would guarantee a win.
In order to see how this would work let’s take a look at a simple game of chance that implements this concept.
Action Steps:
ü Type out this code within remix
ü Deploy the code using Ganache and Web3 options
ü Try to locate the vulnerability within the code
ü Try to exploit the vulnerability this code so that you are always the winner
1. pragma solidity ^0.6.6;
2.
3. contract simpleVulnerableBlockHash {
4.
5. uint balance = 2 ether;
6. mapping (address => uint) blockNumber;
7. bool public win;
8.
9. constructor() public payable{
10. require(msg.value >= 10 ether);
11. }
12.
13. function get_block_number() internal {
14. blockNumber[msg.sender] = uint(block.number);
15. }
16.
17. function playGame() public payable {
18. require (msg.value >= 1 ether);
19. get_block_number();
20. }
21.
22.
23. function checkWinner() public payable {
24. if (uint(blockhash(blockNumber[msg.sender])) % 2 == 0) {
25. win = true;
26. msg.sender.transfer(balance);
27. } else{
28. win = false;
29. }
30. }
31.
32.}
After trying to
exploit this vulnerability yourself review the following video which walks you
through the code and how to exploit it.
Video Walkthrough of Attacking The Game:
Preventing Randomness Summary
The best way to prevent these issues is to avoid on chain
predictable values or secret values as your seed to operations and
calculations. We can do this with
trusted external Oracles. Oracles are
external data sources that your contract can use when it needs random values or
trusted data. There are projects that
specifically solve this problem for example ChainLink which has networks of
Oracle nodes that handle data queries and provide back trusted verified data
including random numbers. A simple example
for using Chainlink for a random number is found at the following link:
https://docs.chain.link/docs/get-a-random-number
It is always a good idea to avoid on chain secret data or
block related information when performing any sort of sensitive operation and
instead utilize an Oracle.
Bad Randomness References
https://docs.chain.link/docs/get-a-random-number
https://nvd.nist.gov/vuln/detail/CVE-2018-14715
This comment has been removed by a blog administrator.
ReplyDelete