Capture the Ether

https://capturetheether.com/

Guess the secret number

  • 作法:基於uint8 n範圍為0~255,直接用brute force方法破解即可。
  • 修正:n範圍不能太小
    ​​​​contract GuessTheSecretNumberChallenge {
    ​​​​    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
    
    ​​​​    function GuessTheSecretNumberChallenge() public payable {
    ​​​​        require(msg.value == 1 ether);
    ​​​​    }
    
    ​​​​    function isComplete() public view returns (bool) {
    ​​​​        return address(this).balance == 0;
    ​​​​    }
    
    ​​​​    function guess(uint8 n) public payable {
    ​​​​        require(msg.value == 1 ether);
    
    ​​​​        if (keccak256(n) == answerHash) {
    ​​​​            msg.sender.transfer(2 ether);
    ​​​​        }
    ​​​​    }
    ​​​​}
    

Guess the random number

  • 作法:根據Layout of State Variables in Storageanswer可在鏈上直接查到。
  • 修正:
    • 別事先將機密資訊寫在智能合約中
    • 別使用block.blockhashblock.numberblock.timestamp作為產生隨機數的inputs
    ​​​​const number = BigNumber.from(
    ​​​​  await contract.provider.getStorageAt(contract.address, 0)
    ​​​​);
    
contract GuessTheRandomNumberChallenge {
    uint8 answer;

    function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

Guess the new number

  • 作法:藉由撰寫一份新的智能合約,仿照對象合約code計算出answer,再呼叫其guess()即可
  • 修正:別使用block.blockhashblock.numberblock.timestamp作為產生隨機數的inputs
    ​​​​function attack() external payable {
    ​​​​    require(address(this).balance >= 1 ether, "not enough funds");
    ​​​​    uint8 answer = uint8(uint256(
    ​​​​        keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
    ​​​​    ));
    ​​​​    challenge.guess{value: 1 ether}(answer);
    
    ​​​​    require(challenge.isComplete(), "challenge not completed");
    ​​​​    msg.sender.transfer(address(this).balance);
    ​​​​}
    
contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            tx.origin.transfer(2 ether);
        }
    }
}

Predict the future

  • 作法:
    • 基於answer的範圍僅限 0~9,可藉由撰寫一份新的智能合約,呼叫lockInGuess()隨意設定一個數字(e.g., 0),再多次嘗試呼叫attack()直到答案符合即可。
    • answer不符合將revert而還原合約state,因此不會受任何影響
  • 修正:
    • answer範圍不能太小

    • 別使用block.blockhashblock.numberblock.timestamp作為產生隨機數的inputs

      ​​​​​​​​function lockInGuess(uint8 n) external payable {
      ​​​​​​​​    challenge.lockInGuess{value: 1 ether}(n);
      ​​​​​​​​}
      
      ​​​​​​​​function attack() external payable {
      ​​​​​​​​    challenge.settle();
      ​​​​​​​​    require(challenge.isComplete(), "challenge not completed");
      ​​​​​​​​    tx.origin.transfer(address(this).balance);
      ​​​​​​​​}
      
      ​​​​​​​​receive() external payable {}
      
contract PredictTheFutureChallenge {
    address guesser;
    uint8 guess;
    uint256 settlementBlockNumber;

    function PredictTheFutureChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

Predict the block hash

  • 基於效能原因,blockhash函數只回傳最後256個blocks的實際區塊hash值。
  • blockhash(uint blockNumber) returns (bytes32):僅適用於最近的256個blocks,不包括當前塊。
    • 過了256 blocks之後會回傳0x00000000000000000000000000000000000000000000000000000000000000000
    • 等待257個blocks以呼叫settle()操作。
contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        bytes32 answer = block.blockhash(settlementBlockNumber);

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

Token sale

  • 作法:利用arithmetic overflow的方式破解
  • 過程:呼叫T.buy(2256 / 1018) → 檢查msg.value是否等於 (2256 / 1018) * 1018 = 2256 = 0) → 成功饒過require statement → 使得自己的balanceOf增加 → 呼叫T.sell(1)取得ether
  • 修正:T.buy()內部須補上overflow & underflow的檢查
contract TokenSaleChallenge {
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;

    function TokenSaleChallenge(address _player) public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance < 1 ether;
    }

    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);

        balanceOf[msg.sender] += numTokens;
    }

    function sell(uint256 numTokens) public {
        require(balanceOf[msg.sender] >= numTokens);

        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    }
}

Token Whale

  • 作法:
    1. 先用刪去法,把無誤的code排除。
    2. 排除後發現transferFrom() & _transfer()寫法是有問題的,可往此方向破解。
  • 過程:
    1. 產生另一個新帳號newPlayer
    2. player呼叫approve(newPlayer, 2256-1) → transferFrom[req3] passed
    3. newPlayer呼叫transferFrom(player, player, 1)transferFrom[req1+req2] passed
    4. 進入_transfer(player, 1)msg.sender == newPlayerbalanceOf[newPlayer] - 1 = 0 - 1 = 2256-1;newPlayer呼叫transfer(player, 1000000)isComplete = true
  • 修正:
    1. transferFrom()內部的msg.sender應為to,兩者不應混用
    2. _transfer()參數應為from, to, value,內部也不應使用msg.sender
contract TokenWhaleChallenge {
    address player;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    string public name = "Simple ERC20 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    function TokenWhaleChallenge(address _player) public {
        player = _player;
        totalSupply = 1000;
        balanceOf[player] = 1000;
    }

    function isComplete() public view returns (bool) {
        return balanceOf[player] >= 1000000;
    }

    event Transfer(address indexed from, address indexed to, uint256 value);

    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;

        emit Transfer(msg.sender, to, value);
    }

    function transfer(address to, uint256 value) public {
        require(balanceOf[msg.sender] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);

        _transfer(to, value);
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    function approve(address spender, uint256 value) public {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
    }

    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);

        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    }
}

Retirement fund

  • 作法:
    1. 先用刪去法,把無誤的code排除。
    2. 排除後剩下collectPenalty()是可能的切入點,然而collectPenalty()未有參數可供輸入,因此得用其他方式破解。例如強制發送ethers,以控制address(this).balance之值。
  • 過程:
    1. 撰寫一份新合約並傳送1 wei進去
    2. 呼叫selfdestruct(RF),強制使得address(this).balance = 1 ether + 1 weiwithdrawn = 2256-1
    3. 通過require(withdrawn > 0)beneficiary接收來自RF合約中的全部ethers
  • 修正:別使用address(this).balance作為判斷依據
  • 補充:以state variable記錄的etherBalanceaddress(this).balance有可能發生不同步的狀況,使得檢查失靈並造成漏洞。
contract RetirementFundChallenge {
    uint256 startBalance;
    address owner = msg.sender;
    address beneficiary;
    uint256 expiration = now + 10 years;

    function RetirementFundChallenge(address player) public payable {
        require(msg.value == 1 ether);

        beneficiary = player;
        startBalance = msg.value;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function withdraw() public {
        require(msg.sender == owner);

        if (now < expiration) {
            // early withdrawal incurs a 10% penalty
            msg.sender.transfer(address(this).balance * 9 / 10);
        } else {
            msg.sender.transfer(address(this).balance);
        }
    }

    function collectPenalty() public {
        require(msg.sender == beneficiary);

        uint256 withdrawn = startBalance - address(this).balance;

        // an early withdrawal occurred
        require(withdrawn > 0);

        // penalty is what's left
        msg.sender.transfer(address(this).balance);
    }
}

Mapping

  • 作法:
    1. 利用set(key, value)嘗試修改isComplete的值。
      • 每份合約共有2256slots的storage儲存空間
      • dynamic array: map[] in slot#1 = array length
      • dynamic array: elemSlot# = hash(1) + array index * elemSize
      • dynamic array: elemSize = 1 (uint256 / each element)
  • 過程:
    1. 先將map[]全展開 → set(2256-2, 0) → map.length = 2256-1
    2. elemSlot# = keccak256(1) + index → 使之overflow繞回#0
    3. 因此 index = 2256 - keccak256(1) = 2256 - 8008442244778998 → elemSlot# = 0
    4. isCompleteOffset = index → 呼叫set(index, 1)isComplete = true
  • 修正:別將更改state變數的權限設為public,尤其是dynamic array與mapping。
contract MappingChallenge {
    bool public isComplete;
    uint256[] map;

    function set(uint256 key, uint256 value) public {
        // Expand dynamic array as needed
        if (map.length <= key) {
            map.length = key + 1;
        }

        map[key] = value;
    }

    function get(uint256 key) public view returns (uint256) {
        return map[key];
    }
}

Donation

  • 作法:
    1. 當中的donate()寫法有誤:scale = 10^36 & uint256 etherAmount的單位不是ether(10^18),使得msg.value很小的情況下能通過require()的檢查
    2. 此外Donation donation;未標註storage or memory,則預設為storage。此語法意味著donation其實是指向了的slot#0 (i.e., Donation[] array length),而donation.timestampdonation.etherAmount各對應slot#0與slot#1 (i.e., address owner) 的位置。
    3. 基於上述兩點,因而產生了合約漏洞 → 可利用donate()嘗試修改owner的值
  • 過程:
    1. 產生另一個新帳號並呼叫donate(uint(newAddress)/scale)
    2. 上述執行後,使得donation.etherAmount=newAddressowner=newAddress → 呼叫withdraw()即可
  • 修正:
    1. uint256 scale = 10**18 * 1 ether;改為uint256 scale = 10**18;
    2. require(msg.value == etherAmount / scale);改為etherAmount * scale
    3. Donation donation;改為Donation memory donation;
contract DonationChallenge {
    struct Donation {
        uint256 timestamp;
        uint256 etherAmount;
    }
    Donation[] public donations;

    address public owner;

    function DonationChallenge() public payable {
        require(msg.value == 1 ether);

        owner = msg.sender;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function donate(uint256 etherAmount) public payable {
        // amount is in ether, but msg.value is in wei
        uint256 scale = 10**18 * 1 ether;
        require(msg.value == etherAmount / scale);

        Donation donation;
        donation.timestamp = now;
        donation.etherAmount = etherAmount;

        donations.push(donation);
    }

    function withdraw() public {
        require(msg.sender == owner);

        msg.sender.transfer(address(this).balance);
    }
}



Fifty years

  • 作法:
    1. upsert()的else分支,發現再次使用了未初始化的storage pointer。contribution.amount & contribution.unlockTimestamp分別覆寫了q.length & head
    2. 此外,因為queue.push(contribution);的關係,contribution.amount、也就是q.lengthmsg.value覆寫之後,q.length還會再+1 (q.push函數中的內部指令順序:q.length首先遞增,然後再複製queue entry)。
    3. 利用queue[queue.length - 1].unlockTimestamp + 1 days的overflow,使其值變為0,這樣便可以在timestamp = 0的情況下通過require()之檢查。
  • 過程:
    1. 呼叫upsert(1, 2256-86400) & msg.value = 1 wei (若呼叫upsert未設定msg.value=q.length的話,q.length將被視為0、也就是被視為empty,所有的值都讀不到了)。
    2. 呼叫upsert(2, 0) & msg.value = 2 wei,即可繞過require(timestamp...),使得head = 0。
    3. 還不能呼叫withdraw(2),合約裡的etherBalance = 1 ether + 1 wei + 2 wei,而uint256 total = 1 ether + 2 wei + 3 wei (多出2 wei),因此得先用selfdestruct強制發送2 wei到此合約,然後再呼叫withdraw(2)即可。
  • 修正:避免使用未初始化的storage pointer、timestamp要檢查overflow。
contract FiftyYearsChallenge {
    struct Contribution {
        uint256 amount;
        uint256 unlockTimestamp;
    }
    Contribution[] queue;
    uint256 head;

    address owner;
    function FiftyYearsChallenge(address player) public payable {
        require(msg.value == 1 ether);

        owner = player;
        queue.push(Contribution(msg.value, now + 50 years));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function upsert(uint256 index, uint256 timestamp) public payable {
        require(msg.sender == owner);

        if (index >= head && index < queue.length) {
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
        } else {
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        }
    }

    function withdraw(uint256 index) public {
        require(msg.sender == owner);
        require(now >= queue[index].unlockTimestamp);

        // Withdraw this and any earlier contributions.
        uint256 total = 0;
        for (uint256 i = head; i <= index; i++) {
            total += queue[i].amount;

            // Reclaim storage.
            delete queue[i];
        }

        // Move the head of the queue forward so we don't have to loop over
        // already-withdrawn contributions.
        head = index + 1;

        msg.sender.transfer(total);
    }
}

Fuzzy identity

利用pre-compute合約地址 + brute force的方式破解

interface IName {
    function name() external view returns (bytes32);
}

contract FuzzyIdentityChallenge {
    bool public isComplete;

    function authenticate() public {
        require(isSmarx(msg.sender));
        require(isBadCode(msg.sender));

        isComplete = true;
    }

    function isSmarx(address addr) internal view returns (bool) {
        return IName(addr).name() == bytes32("smarx");
    }

    function isBadCode(address _addr) internal pure returns (bool) {
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000badc0de";
        bytes20 mask = hex"000000000000000000000000000000000fffffff";

        for (uint256 i = 0; i < 34; i++) {
            if (addr & mask == id) {
                return true;
            }
            mask <<= 4;
            id <<= 4;
        }

        return false;
    }
}

Public key

  • 由於交易是由帳戶簽署的,因此它們帶有簽名,並且該簽名需要包含一個公鑰,供所有人驗證交易是否來自該帳戶。或者在ECDSA的情況下,公鑰實際上不是簽名的一部分,但可以從訊息和簽名中recover。
  • 幸運的是,owner帳戶只有一個轉出交易。我們可以使用一個節點來獲取它的數據,重新計算message hash,並recover地址的公鑰。
  • ECDSA 簽名由 (r,s,v) 值組成,以太坊根據 EIP-155 對序列化tx hash進行簽名,即 keccak256(rlp(nonce, gasprice, startgas, to, value, data, chainid, 0, 0))。
  • 對recoverPublicKey的另一個呼叫會產生未壓縮的公鑰,我們可以提交它來解決這個挑戰。
  • 一個有趣的結果是,如果你從不自你的帳戶發送交易,那麼在區塊鏈上唯一可見的就只有地址(一個 sha256 hash)。這使得從未發送交易的帳戶具有量子安全性。
  • 補充:account address: 其public key的keccak256 hash值之最後20個bytes
  • 補充:contract address: 由0xff prefix, deployerAddress, salt, 合約creationCode + constructorArgs組成bytecode的keccak256 hash值之最後20個bytes
  • 補充:r,s,v是交易簽名的值,可以用作獲取任何以太坊帳戶的公鑰。r,s是ECDSA簽名的輸出,v是recovery ID。
contract PublicKeyChallenge {
    address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
    bool public isComplete;

    function authenticate(bytes publicKey) public {
        require(address(keccak256(publicKey)) == owner);

        isComplete = true;
    }
}




Account takeover

  • 這次我們需要檢索實際的私鑰來模擬來自所有者合約的交易
  • 我們唯一的線索是owner的地址。檢查帳戶交易後,可能會注意到有兩筆交易在其簽名中使用相同的r值(ECDSA中的k值)。
  • 關於ECDSA,需要了解的是k值應該是隨機選擇的,不能重複使用。兩次使用相同的k值來簽署不同的訊息允許重新計算私鑰
contract AccountTakeoverChallenge {
    address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
    bool public isComplete;

    function authenticate() public {
        require(msg.sender == owner);

        isComplete = true;
    }
}