行业新闻

Paradigm-CTF 2题

Paradigm-CTF 2题

 

给出了以下合约

  1. Setup.sol
  2. Wallet.sol

Setup.sol

pragma solidity 0.5.12;

import "./Wallet.sol";

contract WETH9 is ERC20Like {
    function deposit() public payable;
}

contract Setup {
    WETH9 public constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    uint public constant WANT = 50 ether;

    Wallet public wallet;

    constructor() public payable {
        require(msg.value == WANT);

        address tokenModule = address(new TokenModule());

        wallet = new Wallet();
        wallet.allowModule(tokenModule);

        WETH.deposit.value(msg.value)();
        WETH.approve(address(wallet), uint(-1));

        wallet.execModule(tokenModule, abi.encodeWithSelector(TokenModule(0x00).deposit.selector, WETH, address(this), msg.value));
    }

    function isSolved() public view returns (bool) {
        return WETH.balanceOf(address(this)) == WANT;
    }
}
pragma solidity 0.5.12;

contract ERC20Like {
    function transfer(address dst, uint qty) public returns (bool);
    function transferFrom(address src, address dst, uint qty) public returns (bool);
    function approve(address dst, uint qty) public returns (bool);

    function balanceOf(address who) public view returns (uint);
}

contract TokenModule {
    function deposit(ERC20Like token, address from, uint amount) public {
        token.transferFrom(from, address(this), amount);
    }

    function withdraw(ERC20Like token, address to, uint amount) public {
        token.transfer(to, amount);
    }
}

contract Wallet {
    address public owner = msg.sender;

    mapping(address => bool) _allowed;
    mapping(address => bool) _operators;

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

    modifier onlyOwnerOrOperators {
        require(msg.sender == owner || _operators[msg.sender]);
        _;
    }

    function allowModule(address module) public onlyOwner {
        _allowed[module] = true;
    }

    function disallowModule(address module) public onlyOwner {
        _allowed[module] = false;
    }

    function addOperator(address operator) public onlyOwner {
        _operators[owner] = true;
    }

    function removeOperator(address operator) public onlyOwner {
        _operators[owner] = false;
    }

    function execModule(address module, bytes memory data) public onlyOwnerOrOperators {
        require(_allowed[module], "execModule/not-allowed");
        (bool ok, bytes memory res) = module.delegatecall(data);
        require(ok, string(res));
    }
}

可以看到成功的条件是拿到WETH代币的50ETH。
我们从头开始审计一下SetUp合约。可以看到首先要求了我们msg.value需要为50ETH。之后他先是新创建了TokenModule合约,以及wallet合约。之后他把50个ETH全部捐献到WETH这个币池中。approve用于定义交易上限。-1 也就是相当于随便使用了。然后调用了wallet的 execModule方法。发现execModule中利用的是delegatecall操作的是Wallet本身的方法。这里可以注意一下。

之后可以看一下Wallet.sol中的合约。ERC20Like类似ERC20代币规则的接口
估计是WETH实现时候用到的。TokenModule 这里用了一个接口实现Token转账。
Wallet合约中有两个修饰器。一个只允许发起交易人是合约创建者,还有一个是除了创建者之外的一个operater为真的但是add_operator只能通过onlyOwner来操作。 最后还有我们的execModule是需要满足onlyOwner或者operator条件。
那么我们就可以开始考虑如何先把两个修饰器通过。

我们这时候可以注意到,他其实给出了这个WETH合约的地址,我们去考虑查看下这个合约我们是否能进行一定方法的调用。

我们可以发现 其实只要我们一方能够给出50ETH这样的一个转账,同时也是可以实现触发Solved成功的。所以考虑只需要进行转账50ETH就足够了。


pragma solidity 0.5.12;

import "public/Setup.sol";

contract Exploit {
    WETH9 public constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

    constructor(Setup setup) public payable {
        WETH.deposit.value(50 ether)();
        WETH.transfer(address(setup), 50 ether);
    }
}

比较简单。


Lockbox

强网杯有一题和这题基本一模一样。但是强网杯未给出源码需要自己逆向。分析一下这题。

pragma solidity 0.4.24;

contract Stage {
    Stage public next;

    constructor(Stage next_) public {
        next = next_;
    }

    function getSelector() public view returns (bytes4);

    modifier _() {
        _;

        assembly {
            let next := sload(next_slot)
            if iszero(next) {
                return(0, 0)
            }

            mstore(0x00, 0x034899bc00000000000000000000000000000000000000000000000000000000)
            pop(call(gas(), next, 0, 0, 0x04, 0x00, 0x04))
            calldatacopy(0x04, 0x04, sub(calldatasize(), 0x04))
            switch call(gas(), next, 0, 0, calldatasize(), 0, 0)
                case 0 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    revert(0x00, returndatasize())
                }
                case 1 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    return(0x00, returndatasize())
                }
        }
    }
}

contract Entrypoint is Stage {
    constructor() public Stage(new Stage1()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

    bool public solved;

    function solve(bytes4 guess) public _ {
        require(guess == bytes4(blockhash(block.number - 1)), "do you feel lucky?");

        solved = true;
    }
}

contract Stage1 is Stage {
    constructor() public Stage(new Stage2()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

    function solve(uint8 v, bytes32 r, bytes32 s) public _ {
        require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?");
    }
}

contract Stage2 is Stage {
    constructor() public Stage(new Stage3()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

    function solve(uint16 a, uint16 b) public _ {
        require(a > 0 && b > 0 && a + b < a, "something doesn't add up");
    }
}

contract Stage3 is Stage {
    constructor() public Stage(new Stage4()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

    function solve(uint idx, uint[4] memory keys, uint[4] memory lock) public _ {
        require(keys[idx % 4] == lock[idx % 4], "key did not fit lock");

        for (uint i = 0; i < keys.length - 1; i++) {
            require(keys[i] < keys[i + 1], "out of order");
        }

        for (uint j = 0; j < keys.length; j++) {
            require((keys[j] - lock[j]) % 2 == 0, "this is a bit odd");
        }
    }
}

contract Stage4 is Stage {
    constructor() public Stage(new Stage5()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

    function solve(bytes32[6] choices, uint choice) public _ {
        require(choices[choice % 6] == keccak256(abi.encodePacked("choose")), "wrong choice!");
    }
}

contract Stage5 is Stage {
    constructor() public Stage(Stage(0x00)) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

    function solve() public _ {
        require(msg.data.length < 256, "a little too long");
    }
}
`

套娃合约。最重要的是看懂每个的逻辑 以及数据部署。怎么给套娃中的下一个合约传参。
这里给出了一个Setup.sol

pragma solidity 0.4.24;

import "./Lockbox.sol";

contract Setup {
    Entrypoint public entrypoint;

    constructor() public {
        entrypoint = new Entrypoint();
    }

    function isSolved() public view returns (bool) {
        return entrypoint.solved();
    }
}

相当于告诉我们的入口点是要从Entrypoint()开始。我们首先抛开其他传参布局因素,挨个解决套娃合约。

Entrypoint

contract Entrypoint is Stage {
 constructor() public Stage(new Stage1()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

 bool public solved;

 function solve(bytes4 guess) public _ {
     require(guess == bytes4(blockhash(block.number - 1)), "do you feel lucky?");

     solved = true;
 }
}

这里我们可以看到进行了一个随机数预测。没有什么难点。进入下一个。

Stage1

contract Stage1 is Stage {
 constructor() public Stage(new Stage2()) {} function getSelector() public view returns (bytes4) { return this.solve.selector; }

 function solve(uint8 v, bytes32 r, bytes32 s) public _ {
     require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?");
 }
}

这里进行了一个ecrecover 这是一个利用椭圆曲线进行验证的函数。这个privatekey在一个网站上是给出的。我们直接去查询最终的值就可以。http://www.privatekeys.info/ethereum/1

可以看到。这里的私钥是0x000000000000000(…)1
利用这个进行eth-sign。单纯的web3的sign-in是有问题的他会自动加入一个消息头。导致消息的 r s v不太相同。需要利用其它的库。
给出Sissel

关闭