Private Variable Exploit – Hacking Smart Contract Security Series [Part 2]

Rate this post

Introduction

This post is part of our Smart Contract Security Series:

  1. Ownership Exploit
  2. Private Variable Exploit
  3. Reentrancy Attack
  4. tx.origin Phishing Attack
  5. Denial of Service Attack

In this part 2 of the series, we’ll examine how to exploit the “private” vulnerability in Solidity.

As the name suggests, private means not accessible to anyone outside.

  • Can we expect the same in Solidity smart contracts?
  • Is any variable declared as ” private” in Solidity is not accessible from the outside world?

If the answer is yes, then the assumption may be wrong. It is possible to access the private variables of smart contracts from the outside world.

For the same, the key is to understand the storage structure and its arrangement in Solidity.

The sections below detail the storage structure, followed by the exploit, and ultimately the conclusion.

Let’s begin!

Storage Structure

In Solidity, the variables are stored in either storage or memory.

The storage section can be viewed as writing to the hard disk i.e. permanent storage and writing to memory can be viewed as writing to RAM i.e. temporary storage.

The storage is arranged as slots. The total size of the storage is 2^256 bytes and each slot occupies 32 bytes each.

The data will be stored sequentially in the order in which they are declared.

To save space, the storage performs optimizations to accommodate neighboring variables if they fit within the 32 bytes, and in such cases, they are packed into the same slot.

The storage arrangement is given below.

Storage arrangement

As shown storage slot starts from 0 up to 2^256 and each storage slot is 32 bytes.

For an example contract, see how the variables get stored in the slots.

contract StorageArrangment {
   uint256  public a;     // gets stored in Slot 0 = 32 bytes

   bytes32  public b;     // gets stored in Slot 1 = 32 bytes

   address public addr;   // gets stored in Slot 2 = 20 bytes     
   bool   public  mybool; // gets stored in Slot 2  = 1 byte         
   uint16 public c;       // gets stored in Slot 2  = 2  bytes         
}

Slot0 is 32 bytes, Slot1 is 32 bytes, and Slot2 is 23 bytes. Thus as can be seen Slot2 accommodates the neighbors to save space.

In the case of inheritance, the storage variables of the base contracts take the first slot of the storage and afterward the derived contract storage variables.

Exploit

To exploit the private variables of the contract consider a simple contract named privateExploit.sol that performs a deposit and withdrawal as follows.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";

contract PrivateExploit is Ownable{
  uint256 public customerid;
  bytes32 private _password;

  constructor(uint256 id, bytes32 password){
    customerid  = id;
    _password = password;
  }
  modifier verifyPassword(bytes32 password)
  {
    require(password == _password, "Password does not match");
    _;
  }

  function depositFunds(uint256 _amount) payable public onlyOwner {
     // deposit amount to the contract
  }
 
  function withdrawFunds(bytes32 password) public  verifyPassword(password){
    (bool os, ) = payable(msg.sender).call{value:address(this).balance}("");
    require(os, "Failed to withdraw funds!");
  }
}

The PrivateExploit contract is described in brief.

  1. The contract is inheriting Ownable from OpenZeppelin to ensure that only the owner can operate on certain functions.
  2. The constructor initializes the customer id and a password.
  3. depositFunds() for depositing the amount. It is defined but not needed for our exploit.
  4. withdrawFunds() which takes the password as input, verifies the password using the modifier verifyPassword. If the password matches it will transfer the balance to the msg.sender.

Using Truffle or (any other tool such as Hardhat) deploy the contract. For the same on the terminal.

$ truffle develop
Fig: Start Truffle development env

Next would be to migrate so as to deploy the contract

💡 Note: You need to write the migration file. If you are new to Truffle please follow the guide here for writing deployment scripts.

During deployment, the constructor params need to be passed. The deployment script is given below.

const privateexploit = artifacts.require("PrivateExploit");

module.exports = function (deployer) {
  deployer.deploy(privateexploit, 1, "0x1234567890123456789012345678901234567890123456789012345678903132");
};

As the password is bytes32, I have set the password to.

"0x1234567890123456789012345678901234567890123456789012345678903132"

Feel free to use any customer id and the password.

$truffle migrate
Fig: Truffle Deployment

After the deployment is successful, you can now interact with the contract using the web3 API.

To exploit the private variables we make use of the web3 API:

web3.eth.getStorageAt(addressHexString, position [, defaultBlock] [, callback])

Each contract is made up of EVM bytecodes that handle the execution and storage of the contract’s state. This is a low-level function that returns the storage state of the contract. The data is stored in a key/value store.

The getStorageAt() function returns the value of the contract’s storage at a specific point.

Let’s use this and make the exploit. As seen from the above fig, the contract is deployed at the address “0xcFb962032Ba7A16eb7e5e1059BB3531E7d38CcfE”.

Fig: Exploit

In the above figure, the first param is the contract address, the second param is the slot number (0,1,2,..etc), and a callback function (console.log).

Due to inheritance, the Slot0 is occupied by the storage variables of the base contract, in this case, Ownable. Slot1 returns 0x01 (the customer id) and lastly Slot2 returns the password “0x1234567890123456789012345678901234567890123456789012345678903132”.

The password can then easily be used to withdraw the funds by the attacker.

Conclusion

In this part we used the web3 API getStorageAt() to exploit the private variables of the contract.

Ensure that you don’t store any sensitive information such as passwords, user names, private keys on the blockchain.

As the keyword is private, the user is tempted to assume that the variable is not visible outside the contract, but as we saw in this post this is untrue.

Happy Hacking — i.e., preventing the same! 🌍


Learn Solidity Course

Solidity is the programming language of the future.

It gives you the rare and sought-after superpower to program against the “Internet Computer”, i.e., against decentralized Blockchains such as Ethereum, Binance Smart Chain, Ethereum Classic, Tron, and Avalanche – to mention just a few Blockchain infrastructures that support Solidity.

In particular, Solidity allows you to create smart contracts, i.e., pieces of code that automatically execute on specific conditions in a completely decentralized environment. For example, smart contracts empower you to create your own decentralized autonomous organizations (DAOs) that run on Blockchains without being subject to centralized control.

NFTs, DeFi, DAOs, and Blockchain-based games are all based on smart contracts.

This course is a simple, low-friction introduction to creating your first smart contract using the Remix IDE on the Ethereum testnet – without fluff, significant upfront costs to purchase ETH, or unnecessary complexity.