Intro to Smart Contract Security Audits | Accessing Private Data

SlowMist
8 min readMar 29, 2022

We learned about the self-destruction function in Solidity last time (hopefully everyone was able to understand it), and this time we’ll learn how to access private data.

Background overview

Let’s take a look at the three data storage methods in solidity:

1. Storage

  • The data in storage is stored permanently. It is stored in the slot as a key-value pair.
  • Data in storage gets written on the blockchain (so they change state), which is why using storage is very expensive.
  • The gas cost to occupy a 256-bit slot is 20,000 gas.
  • Modifying the value of storage will cost 5,000 gas.
  • A certain amount of gas is refunded when a storage slot is cleaned up (i.e. set non-zero bytes to zero).
  • storage has a total of 2²⁵⁶ slots, 32 bytes of data in each slot are stored sequentially in the order of declaration. The data will be stored from the right side of each slot, if adjacent variables fit into a single 32 bytes, then they are stored in packs into the same slot otherwise, a new slot will be enabled for storage.
  • Storage methods of arrays in storage are unique. Arrays in solidity are divided into two types.

a. Fixed-length array (fixed length)
Each element in a fixed-length array will have a separate slot for storage. Taking a fixed-length array with three uint64 elements as an example, in the following figure you can clearly see its storage method:

b. Variable-length array (the length varies with the number of elements)
The storage method of variable-length arrays is very strange. When encountering variable-length arrays, a new slot, slotA will be enabled to store the length of the array, and its data will be stored in another slot numbered slotV. SlotA represents the declared position of the variable-length array, length represents the length of the variable-length array, slotV represents the storage location of the variable-length array data, value represents the value of a certain data in the variable-length array, and index represents the index subscript corresponding to the value. However, length = sload(slotA), slotV = keccak256(slotA) + index and value = sload(slotV).

Variable-length arrays cannot know the length of the array during compilation, and there is no way to reserve storage space in advance, so Solidity uses slotA to store the length of the variable-length array.

Let’s write an example to verify how the variable-length array described above is stored:

After deploying the contract, call the addUser function and pass in the parameter a = 998. Once it’s done debugging, you can see how the variable-length array is stored.

The first slot here is where the length of the variable-length array is stored:
0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
This value is equal to:
sha3(“0x000000000000000000000000000000000000000000000000000000000000000”)
key = 0 is the number of the current slot.
value = 1, this means that there is only one piece of data in the variable-length array user[], which means the array length is 1.
The second slot here is where the data in the variable-length array is stored:
0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9
This value is equal to:
sha3(“0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563”)
The slot numbers are:
key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
This value is equal to:
sha3(“0x000000000000000000000000000000000000000000000000000000000000000”)+0
The data stored in the slot is:
value=0x0000000000000000000000000000000000000000000000000000000000003e6
This is 998 in hexadecimal, which is the value we passed in.
For more accurate verification, we call the addUser function again and pass a=999 to get the following result.

Here we can see that the new slot is:
0x6c13d8c1c5df666ea9ca2a428504a3776c8ca01021c3a1524ca7d765f600979a.
This value is equal to:
sha3(“0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564”)
The slot number is: key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
This value is equal to:
sha3(“0x000000000000000000000000000000000000000000000000000000000000000”)+1
The data stored in the slot is:
value=0x0000000000000000000000000000000000000000000000000000000000003e7
This value is 999 in hexadecimal, which is the value of a passed in by the addUser function we just called.

The examples above should give you a general understanding of how variable-length arrays are stored.

2. Memory

  • memory is a byte array with a slot size of 256 bits (32 bytes). Data is only stored during function execution and is deleted after execution. They are not saved to the blockchain.
  • Reading or writing a byte (256 bits) requires 3 gas.
  • In order to avoid too much work for miners, the cost will start to rise after 22 read and write operations.

3. Calldata (call data)

  • calldata is an unmodifiable, non-persistent area used to store function parameters and behaves basically like memory.
  • Calldata is required for arguments to calls to external functions, and can also be used for other variables.
  • It avoids duplication and ensures that data cannot be modified.
  • Arrays and structs with calldata data locations can also be returned from functions, but no assignment to this type is possible.

Now that we understand the three storage methods in solidity, let’s look at the four visibility keywords in the contract: In solidity, there are four visibility keywords: external, public, internal, and private. Function visibility is public by default. For state variables, except external that cannot be used to define variables, the other three can be used to define variables. The default visibility of state variables is internal.

1. External Keyword
External functions defined by external can be called by other contracts. The external function() decorated with external cannot be called directly as an internal function, that is to say, the calling method of function() must use this.function().

2. Public Keyword
Publicly defined functions can be called using internal functions or external messages. For state variables defined by the public, the system will automatically generate a getter function.

3. Internal Keyword
Functions and state variables defined internally can only be accessed inside the current contract or contracts derived from the current contract.

4. Private Keyword
Functions and state variables defined by private are only visible to the contract that defines it. None of the contracts derived from the contract can call, access functions, and state variables.

To sum up, the keywords that modify the variable storage in the contract only limit the scope of its invocation, but not whether it is readable or not. So today we will show you how to read all the data in the contract.

Vulnerability example

This time our target contract is a contract deployed on Ropsten testnet.

Contract address:
0x3505a02BCDFbb225988161a95528bfDb279faD6b
Link:
https://ropsten.etherscan.io/address/0x3505a02BCDFbb225988161a95528bfDb279faD6b#code

Contract source code

Vulnerability Analysis

From the above contract code, we can see that the Vault contract records sensitive data such as the username and password in the contract. From the prerequisite knowledge, we can understand that the keywords that modify the variables in the contract only limit its calling scope. This indirectly proves that the data in the contract is public and can be read arbitrarily, making it not safe to record sensitive data in the contract.

Reading Data

Next, we will examine the data in this contract. First, let’s look at the data in slot0: It can be seen from the contract that only one uint type of data is stored in slot0.

I’ll be using Web3.py to interact with the data here

After running

Let’s convert it using a base converter

Here we successfully got to the uint type variable count=123 stored in the first slot slot0 in the contract.

Let’s continue:

Three variables are stored in slot1: u16, isTrue, owner

From right to left are
owner = f36467c4e023c355026066b8dc51456e7b791d99
isTrue = 01 = true
u16 = 1f = 31
The private variable password is stored in slot2.

Let’s examine this:

Slots 3, 4, 5 store three elements in a fixed-length array.

Slot6 stores the length of the variable-length array

We can see from the contract code that the user’s id and password are stored in the form of key-value pairs. Let’s examine the id and password of the two users.

user1

user2

We have successfully read all the data in the contract. We proved that the private data in the contract can also be read.

Preventative Techniques

(1) As a developer
Do not store any sensitive data in the contract, as any data in the contract can be read.
(2) As an auditor
During the auditing process, attention should be paid to whether there is sensitive data in the contract, such as secret keys, passwords, etc.

References
Please refer to the following articles for additional information.

--

--

SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.