Skip to content

Latest commit

 

History

History
223 lines (190 loc) · 8.03 KB

File metadata and controls

223 lines (190 loc) · 8.03 KB

WBC

目标是通过设置 scored = true 来得分,可以通过 homerun()_homeBase() 函数来实现。

homerun()

    function homerun() external {
        require(block.timestamp % 23_03_2023 == 0, "try again");
        scored = true;
    }

要求区块时间戳必须是 230322023 的倍数来得分,不可能通过这种方式,所以需要从 ready() 函数来入手,解决 5 个子问题。

ready()

    address private immutable judge;

    constructor() {
        judge = block.coinbase;
    }

    function ready() external {
        require(IGame(msg.sender).judge() == judge, "wrong game");
        _swing();
    }

需要我们部署一个合约,并且实现 judge() 函数来返回一个地址,由于在 WBC 合约中 judge 地址为当前出块地址,所以我们需要在部署 WBC 合约的同一个区块部署我们的合约。

bodyCheck()

    modifier onlyPlayer() {
        require(msg.sender == player, "security!");
        _;
    }

    function bodyCheck() external {
        require(msg.sender.code.length == 0, "no personal stuff");
        require(uint256(uint160(msg.sender)) % 100 == 10, "only valid players");

        player = msg.sender;
    }

    function _swing() internal onlyPlayer {
        _firstBase();
        require(scored, "failed");
    }

随后需要执行 _swing() 函数,在执行该函数之前需要通过 bodyCheck() 函数,成为一个有效的玩家。

调用 bodyCheck() 函数需要你的合约代码长度为 0,这点可以通过构造函数中调用 bodyCheck() 函数来实现。此外需要满足 uint256(uint160(msg.sender)) % 100 == 10 这个条件,那么需要使用 create2 来计算 salt 值部署合约。

steal()

    function _firstBase() internal {
        uint256 o0o0o0o00oo00o0o0o0o0o0o0o0o0o0o0o0oo0o = 1001000030000000900000604030700200019005002000906;
        uint256 o0o0o0o00o0o0o0o0o0o0o0ooo0o00o0ooo000o = 460501607330902018203080802016083000650930542070;
        uint256 o0o0o00o0oo00oo00o0o0o0o0o0o0o0o0oo0o0o = 256; // 2^8
        uint256 o0oo0o0o0o0o0o0o0o0o00o0oo00o0o0o0o0o0o = 1;
        _secondBase(
            uint160(
                o0o0o0o00oo00o0o0o0o0o0o0o0o0o0o0o0oo0o
                    + o0o0o0o00o0o0o0o0o0o0o0ooo0o00o0ooo000o * o0o0o00o0oo00oo00o0o0o0o0o0o0o0o0oo0o0o
                    - o0oo0o0o0o0o0o0o0o0o00o0oo00o0o0o0o0o0o
            )
        );
    }

    function _secondBase(uint160 input) internal {
        require(IGame(msg.sender).steal() == input, "out");
        _thirdBase();
    }

_firstBase() 会调用 _sendBase(),传入一个 uint160 类型的数字。随后,_sendBase() 会检查这个数字是否与我们的合约中 steal() 函数返回的值相等。这个我们直接算就行。

execute()

    function _thirdBase() internal {
        require(keccak256(abi.encodePacked(this.decode(IGame(msg.sender).execute()))) == keccak256("HitAndRun"), "out");
        _homeBase();
    }

    function decode(bytes32 data) external pure returns (string memory) {
        assembly {
            mstore(0x20, 0x20)
            mstore(0x49, data)
            return(0x20, 0x60)
        }
    }

需要实现一个 execute() 函数,给出一个字符串,通过 decode() 解码后得到 "HitAndRun" 字符串本身。

那么我们需要返回的字符串的编码应该是

0000000000000000000000000000000000000000000000000000000000000020  // 偏移量
0000000000000000000000000000000000000000000000000000000000000009  // 字符串长度
486974416e6452756e0000000000000000000000000000000000000000000000  // HitAndRun

decode() 函数中,内存[0x20, 0x40) 存入了偏移量 0x20,我们需要在内存[0x40, 0x60) 存入 0x9,而代码中存放 data 的内存位置是 0x49,所以我们需要在[0x49, 0x60) 存放 00..9,具体为 0x60 - 0x49 = 0x17(23字节), 那么需要 45 个 0 拼上 9。最后再拼上"HitAndRun"对应的 ASCII 码,得到输入到 decode 的 data 为 bytes32(0x0000000000000000000000000000000000000000000009486974416e6452756e)

shout()

    function _homeBase() internal {
        scored = true;

        (bool succ, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("shout()"));
        require(succ, "out");
        require(
            keccak256(abi.encodePacked(abi.decode(data, (string)))) == keccak256(abi.encodePacked("I'm the best")),
            "out"
        );

        (succ, data) = msg.sender.staticcall(abi.encodeWithSignature("shout()"));
        require(succ, "out");
        require(
            keccak256(abi.encodePacked(abi.decode(data, (string))))
                == keccak256(abi.encodePacked("We are the champion!")),
            "out"
        );
    }

_homeBase() 函数要求我们实现一个 shout() 函数,其中两次连续的 staticcall 返回不同的字符串。

我们无法通过存储变量来区分第一次和第二次 staticcall,由于两次连续 staticcall 唯一的区别就是 gas 消耗量,所以通过 gasleft() 的变化来做区分。

POC:

contract Ans {
    WBC public immutable wbc;

    uint256 prev_gas;

    constructor(address wbc_) {
        wbc = WBC(wbc_);
        wbc.bodyCheck();
    }

    function win() external {
        wbc.ready();
    }

    function judge() external view returns (address) {
        return block.coinbase;
    }

    function steal() external pure returns (uint160) {
        return 507778882907781185490817896798523593512684789769;
    }

    function execute() external returns (bytes32) {
        prev_gas = gasleft();
        return bytes32(0x0000000000000000000000000000000000000000000009486974416e6452756e);
    }

    function shout() external view returns (bytes memory) {
        //console.log(prev_gas - gasleft());
        if (prev_gas - gasleft() <= 25500) {
            return ("I'm the best");
        } else {
            return ("We are the champion!");
        }
    }
}
    function testExploit() external {
        uint256 salt = 2;
        ans = new Ans{salt: bytes32(salt)}(address(wbc));
        ans.win();
        base.solve();
        assertTrue(base.isSolved());
    }

    function testSalt() external {
        uint256 salt;

        for (uint256 i = 0; i < 1000; ++i) {
            try new Ans{salt: bytes32(i)}(address(wbc)) returns (Ans) {
                salt = i;
                break;
            } catch {}
        }
        console2.log(salt);
    }
[PASS] testExploit() (gas: 281835)
Traces:
  [281835] WBCTest::testExploit()
    ├─ [161756] → new Ans@0x658dD904487B0834dCe7f00157B62c7B19243B86
    │   ├─ [22477] WBC::bodyCheck()
    │   │   └─ ← [Stop] 
    │   └─ ← [Return] 680 bytes of code
    ├─ [31728] Ans::win()
    │   ├─ [31211] WBC::ready()
    │   │   ├─ [256] Ans::judge() [staticcall]
    │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000
    │   │   ├─ [301] Ans::steal() [staticcall]
    │   │   │   └─ ← [Return] 507778882907781185490817896798523593512684789769 [5.077e47]
    │   │   ├─ [22383] Ans::execute()
    │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000009486974416e6452756e
    │   │   ├─ [308] WBC::decode(0x0000000000000000000000000000000000000000000009486974416e6452756e) [staticcall]
    │   │   │   └─ ← [Return] "HitAndRun"
    │   │   ├─ [678] Ans::shout() [staticcall]
    │   │   │   └─ ← [Return] 0x49276d207468652062657374
    │   │   ├─ [679] Ans::shout() [staticcall]
    │   │   │   └─ ← [Return] 0x57652061726520746865206368616d70696f6e21
    │   │   └─ ← [Stop] 
    │   └─ ← [Stop] 
    ├─ [25123] WBCBase::solve()
    │   ├─ [294] WBC::scored() [staticcall]
    │   │   └─ ← [Return] true
    │   └─ ← [Stop] 
    ├─ [286] WBCBase::isSolved() [staticcall]
    │   └─ ← [Return] true
    └─ ← [Stop] 

[PASS] testSalt() (gas: 271843)
Logs:
  2