默认分类

DSCTF 初赛+决赛 智能合约题目

DSCTF 初赛+决赛 智能合约题目

初赛 Inject

这题其实在我的星球里面分析过了,当时赣网杯看过了这道字节码填空题,于是看到了这个就非常熟悉。但是由于这题的两次code转换给我绕晕了 没有非常快的解出,在临近结束的时候才拿到flag。这里不在逆向说明了,直接把源码放出来:

pragma solidity ^0.4.23;

contract EasyInject {
    address public target = 0x0;
    uint public randomnum = 0;
    bool public flag = false;
    
    function PayForFlag() public {
        address a = target;
        a.delegatecall(abi.encodeWithSignature(""));
    }

    function create_new_target(uint blockNumber) public returns(address addr) {

        uint[8] memory a;
        a[0] = 0x6300;
        a[1] = blockNumber;
        a[2] = 0x43;
        a[3] = 0x10;
        a[4] = 0x58;
        a[5] = 0x57;
        a[6] = 0x33;
        a[7] = 0xff;

        uint code = to_code8(a);

        uint[10] memory b;
        b[0] = 0;
        b[1] = 0x7d;
        b[2] = code;
        b[3] = 0x34;
        b[4] = 0x52;
        b[5] = 0x600b;
        b[6] = 0x6015;
        b[7] = 0xf3;
        b[8] = 0x33;
        b[9] = 0xff;

        uint initcode = to_code10(b);
        uint sz = GetSize(initcode);
        uint offset = 32 - sz;

        assembly {
            let p := mload(0x40)
            mstore(p, initcode)
            addr := create(0, add(p, offset), sz)
        }

        require(addr != 0x0);
        target = addr;
    }

    function to_code8(uint[8] chunks) internal pure returns(uint code) {
        for(uint i=chunks.length; i>0; i--) {
            code ^= chunks[i-1] << 8 * GetSize(code);
        }
    }

    function to_code10(uint[10] chunks) internal pure returns(uint code) {
        for(uint i=chunks.length; i>0; i--) {
            code ^= chunks[i-1] << 8 * GetSize(code);
        }
    }

    function GetSize(uint256 chunk) internal pure returns(uint) {
        bytes memory b = new bytes(32);
        assembly {
            mstore(add(b, 32), chunk)
        }
        for(uint32 i = 0; i< b.length; i++) {
            if(b[i] != 0) {
                return 32 - i;
            }
        }
        return 0;
    }
}

题目目标 是这个: bool public flag = false;
位于slot2 的位置。 而题目给了一个delegatecall的机会。在Payforflag里面。
但是call的合约是什么构成的呢?
首先是:

a[0] = 0x6300; PUSH4 0x00??????
a[1] = blockNumber; 
a[2] = 0x43;   // 堆块高度 blockNumber
a[3] = 0x10;   // LT
a[4] = 0x58;   // PC
a[5] = 0x57;   // JUMPI 
a[6] = 0x33;   //msg.sender
a[7] = 0xff;  //selfdestruct

其中blocknumber自己可控。然后这个结果 被用于转化to_code8。
因为他将此视为一个只有8个部分的字节码。分别是

PUSH4
参数
Blocknumber
LT
PC
JUMPI
msg.sender
selfdestruct

共8个部分。然后将此整合为一个整数。继续向下填。

b[0] = 0;    //stop
b[1] = 0x7d;//PUSH30
b[2] = code;//上面整合好的整数
b[3] = 0x34;//CALLVALUE
b[4] = 0x52;//MSTORE
b[5] = 0x600b; //PUSH1 0x0b
b[6] = 0x6015;// PUSH1 0x15
b[7] = 0xf3; // return
b[8] = 0x33; // msg.sender
b[9] = 0xff; //return

也就是他将上面的参数直接转了下来。用作这里的参数。 利用to_code10
将这段视为10个部分进行转化。
那么在这个不同长度的转化中就出现了问题,在转化8的过程中,如果当前code的部分多于8 那么就会有指令错误的进行偏移。甚至有指令会被抹去。
转10也就同理。
最后他要部署一个合约用于call,那么就一定需要返回东西。所以其内容应该为:

6560016002550034526006601af3
PUSH6 0x600160025500
CALLVALUE //0
MSTORE 
PUSH1 0x06
PUSH1 0x1a
RETURN

这样我们就可以部署一个

600160025500

的合约上去。在delegatecall的时候就可以满足条件了。
而这些一共是 14字节。 现在我们需要考虑的就是如何绕过他做为填空的那些字节。这个时候其实我并没有去详细计算怎么样才能让我这里开始运行。而是使用了fuzz 字节长度的办法。 首先我想到的是,PUSH4那里还需要3个字节。首先随便填一个也可以自己用作部署东西的字节:616060
然后后面开始不断的填充60 直到32字节 结合调试去查看我最终部署时候的字节长什么样。
最后在26字节的时候,可以发现其内容刚好可以覆盖前面的所有内容运行
65开头的所有内容。
填入的最后结果为:0x6160606060606060606560016002550034526006601af3000000

决赛 Stake

pragma solidity >=0.8.0;

interface IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);
    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);
}

contract CTF { // basic ERC20 with permit
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    uint256 internal immutable INITIAL_CHAIN_ID;

    bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;

    mapping(address => uint256) public nonces;

    uint randomNumber = 0;

    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals
    ) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;

        INITIAL_CHAIN_ID = block.chainid;
        INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
        _mint(msg.sender, 1e24);
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        balanceOf[msg.sender] -= amount;
        unchecked {
            balanceOf[to] += amount;
        }
        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public returns (bool) {
        uint256 allowed = allowance[from][msg.sender];

        if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;

        balanceOf[from] -= amount;
        unchecked {
            balanceOf[to] += amount;
        }
        return true;
    }

    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
        unchecked {
            address recoveredAddress = ecrecover(
                keccak256(
                    abi.encodePacked(
                        "\x19\x01",
                        DOMAIN_SEPARATOR(),
                        keccak256(
                            abi.encode(
                                keccak256(
                                    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                                ),
                                owner,
                                spender,
                                value,
                                nonces[owner]++,
                                deadline
                            )
                        )
                    )
                ),
                v,
                r,
                s
            );

            require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");

            allowance[recoveredAddress][spender] = value;
        }
    }

    function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
        return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
    }

    function computeDomainSeparator() internal view virtual returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                    keccak256(bytes(name)),
                    keccak256("1"),
                    block.chainid,
                    address(this)
                )
            );
    }

    function _mint(address to, uint256 amount) internal {
        totalSupply += amount;
        unchecked {
            balanceOf[to] += amount;
        }

    }

    function _burn(address from, uint256 amount) internal {
        balanceOf[from] -= amount;
        unchecked {
            totalSupply -= amount;
        }

    }
}
contract FLAG {
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;
    address public underlying;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    uint randomNumber = 0;

    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals,
        address _underlying
    ) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        underlying = _underlying;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        balanceOf[msg.sender] -= amount;
        unchecked {
            balanceOf[to] += amount;
        }
        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public returns (bool) {
        uint256 allowed = allowance[from][msg.sender];

        if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;

        balanceOf[from] -= amount;
        unchecked {
            balanceOf[to] += amount;
        }
        return true;
    }

    function deposit(uint amount) external{
        IERC20(underlying).transferFrom(msg.sender, address(this), amount);
        _mint(msg.sender, amount);
    }

    function withdraw(uint amount) external{
        _burn(msg.sender, amount);
        IERC20(underlying).transfer(msg.sender, amount);
    }

    function _mint(address to, uint256 amount) internal {
        totalSupply += amount;
        unchecked {
            balanceOf[to] += amount;
        }

    }

    function _burn(address from, uint256 amount) internal {
        balanceOf[from] -= amount;
        unchecked {
            totalSupply -= amount;
        }
    }
    fallback() external{}
}


contract staking{
    uint a=0;
    mapping(address => uint) public stakedAmount;
    mapping(address => uint) public startTime;
    FLAG public flag;
    uint private goal = 800000 * 1e18;
    uint public ratePerBlock = 1e16; // 0.01 token per block
    uint public max_income = 100 * 1e18; //max token income
    bool public claimed = false;
    bool public isSolved = false;

    uint randomNumber = 0;

    function solve() public{
        isSolved = flag.balanceOf(msg.sender) >= goal;
    }

    constructor() {
        CTF ctf = new CTF("CTF", "CTF", uint8(18));
        flag = new FLAG("FLAG", "FLAG", uint8(18), address(ctf));

        ctf.approve(address(flag), type(uint).max);
        ctf.approve(address(this), type(uint).max);
        flag.deposit(ctf.balanceOf(address(this)));
        flag.approve(address(this), type(uint).max);
    }
    function airdrop() public {
        require(claimed == false);
        flag.transfer(msg.sender, 10 * 1e18);
        claimed = true;
    }
    function stake(uint amount) public {
        uint curBlock = block.number;
        if(startTime[msg.sender] == 0){
            startTime[msg.sender] = curBlock;
            stakedAmount[msg.sender] = amount;
        }
        else{
            uint income = _calculateAccumulateIncome(stakedAmount[msg.sender], startTime[msg.sender], curBlock);
            if(income > max_income){
                income = max_income;
            }
            startTime[msg.sender] = curBlock;
            stakedAmount[msg.sender] += (income+amount);
        }
        flag.transferFrom(msg.sender, address(this), amount);
    }

    function _calculateAccumulateIncome(uint principal, uint start, uint end) internal view returns(uint){
        return principal * (end - start) * ratePerBlock / 1e18;
    }

    function unstake(uint amount) public{
        uint curBlock = block.number;
        uint principal = stakedAmount[msg.sender];

        uint income = _calculateAccumulateIncome(principal, startTime[msg.sender], curBlock);
        income = (income > max_income)?max_income:income;
        uint userTotalStake = principal + income;
        if(amount == type(uint).max){
            flag.transfer(msg.sender, userTotalStake);
            stakedAmount[msg.sender] = 0;
            startTime[msg.sender] = curBlock;
        }
        else{
            require(amount <= userTotalStake, "Nice Try");
            stakedAmount[msg.sender] = userTotalStake - amount;
            startTime[msg.sender] = curBlock;
            flag.transfer(msg.sender, amount);
        }
    }
    function transferUnderlyingWithPermit(address from, address _flag, address to, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
        address _ctf = FLAG(_flag).underlying();
        CTF(_ctf).permit(from, address(this), amount, deadline, v, r, s);
        CTF(_ctf).transferFrom(from, to, amount);
    }
}

公链题,赛时摆了直接开抄。 但其实漏洞点也很明显。 前两个合约 由于0.8.0 没有任何溢出问题,甚至第一个合约我还找到了相关的原合约进行对比,几乎100%相同。那么只可能出在staking里面。
读一下逻辑,它实现的其实是一个类似于存款 然后获利的一个情景。利用了ERC20代币的效果进行铸币等等操作。
STAKE和UNSTAKE就是模拟存款和取款(或结束投资)时的一些盈利。而我们的目标是要短时间内拿到大量代笔。只可能未授权从铸币者处转钱出来。
这个时候我们关注到了transferUnderlyingWithPermit这个函数。
他有一个transfrom的方法可以支持任意人进行转账。
但有一个permit操作是EIP-2612: permit – 712-signed approvals这个提案里面定义的。需要ecrecover签名通过。才能实现任意转账。
我们可以发现 这个方法中的_flag 地址是可控的,那么我们可以将_ctf 的地址就指向flag自己。这只需要实现一个合约即可。将flag放到对应的 underlying上就可以。而permit方法怎么办?flag中并没这个方法,但是他有callback,可以处理任何不满足条件的calldata,所以并不会revert。然后就可以顺理成章的进行转账操作了。从而实现盗窃大量代币的效果。
这也就是其完整攻击链了。

回复

This is just a placeholder img.