首发于

Blockchain in CTF

babysandbox

Retr_0

这个人太懒了,签名都懒得写一个

paradigm-CTF 2021

前言:找Ver👴想复现下qwb final的区块链。Ver👴给我发了这个比赛下面的一道题,发现这个比赛里面有很多高质量的智能合约题。从这里开始写一些不错的题目。


babysandbox

看到题目名字就知道了题目考点: 沙盒 给出合约 BabySandbox.sol

pragma solidity 0.7.0;

contract BabySandbox {
    function run(address code) external payable {
        assembly {
            // if we\'re calling ourselves, perform the privileged delegatecall
            if eq(caller(), address()) {
                switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
                    case 0 {
                        returndatacopy(0x00, 0x00, returndatasize())
                        revert(0x00, returndatasize())
                    }
                    case 1 {
                        returndatacopy(0x00, 0x00, returndatasize())
                        return(0x00, returndatasize())
                    }
            }
            
            // ensure enough gas
            if lt(gas(), 0xf000) {
                revert(0x00, 0x00)
            }
            
            // load calldata
            calldatacopy(0x00, 0x00, calldatasize())
            
            // run using staticcall
            // if this fails, then the code is malicious because it tried to change state
            if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
                revert(0x00, 0x00)
            }
            
            // if we got here, the code wasn\'t malicious
            // run without staticcall since it\'s safe
            switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
                case 0 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    // revert(0x00, returndatasize())
                }
                case 1 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    return(0x00, returndatasize())
                }
        }
    }
}

Setup.sol

pragma solidity 0.7.0;

import "./BabySandbox.sol";

contract Setup {
    BabySandbox public sandbox;
    
    constructor() {
        sandbox = new BabySandbox();
    }
    
    function isSolved() public view returns (bool) {
        uint size;
        assembly {
            size := extcodesize(sload(sandbox.slot))
        }
        return size == 0;
    }
}

Setup.py中的isSolved()进行了是否成功解决challenge的check. 这里我不是很熟悉.slot这种用法,所以自己随便部署了一个进行试验。 应该就是取了题目合约的整个字节码。要求把合约变成一个账户。或者直接让合约自毁应该也可以。 然后我们分析下Sandbox中的各种方法

 if eq(caller(), address()) {
                switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
                    case 0 {
                        returndatacopy(0x00, 0x00, returndatasize())
                        revert(0x00, returndatasize())
                    }
                    case 1 {
                        returndatacopy(0x00, 0x00, returndatasize())
                        return(0x00, returndatasize())
                    }
            }
            

这里说的是如果caller也就是调用者是自己的话。那么就会直接调用。 delegatecall,也就是如果这里能设置出一些东西那么就可以成功改变合约状态了。

       if lt(gas(), 0xf000) {
                revert(0x00, 0x00)
            }
            
            // load calldata
            calldatacopy(0x00, 0x00, calldatasize())
            
            // run using staticcall
            // if this fails, then the code is malicious because it tried to change state
            if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
                revert(0x00, 0x00)
            }
            
            // if we got here, the code wasn\'t malicious
            // run without staticcall since it\'s safe
            switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
                case 0 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    // revert(0x00, returndatasize())
                }
                case 1 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    return(0x00, returndatasize())
                }
        }

第一行检测了gas是否够用,然后calldatacopy 从调用数据的位置 f 的拷贝 s 个字节到内存的位置 t 之后他就会利用staticall继续进行检测,但是我们可以发现,他从这里进入的staticcall 是进入了 自己的合约。 相当于对自己进行了一次重入。重入之后的调用方,就是msg.sender了。也就是可以正常进入delegatecall了。 但是他利用的是staticcall在外层,所以还是不能改变合约的原有状态。

但是通过之后 他利用call进行了第二次的合约使用。也就是这里的delegatecall就可以完成任何想做的事情了。也就是我们想要的合约销毁。

那么到这里 整体的思路就很清晰了:

  1. 首先进入run(address target)中,delegatecall无法进入,进入staticcall
  2. staticall中进入delegatecall 完成一次调用。
  3. call中进入delegatecall完成一次调用。
  4. 需要一个函数在staticcall中不改变合约状态,在call中改变。
  5. delegatecall的target只需要直接selfdestruct就可以了。

那么现在就考虑怎么给出一个办法,使得两次调用所执行的方法不同? 尝试思路:

  1. 我们考虑到利用全局变量进行赋值。但是可想而知这个方法并不可靠。因为我们是需要staticall通过检测的,全局变量赋值还是改变了合约的原有状态。 ``` function()payable external{ if(success==true){ selfdestruct(msg.sender); } else{ success=true; }

}

也就是利用类似上述的伪代码。这里是不可做的。

2. 利用特征进行判断。但是我们可以看到每次进行交易不管是传的gas还是什么所有的call和staticall中的特征都完全相同。 所以这个方法也很难进行bypass。

if(gas>value){ return ; } else{ selfdestruct(msg.sender); }

3. 考虑使用call外部变量进行改变,这种是可行的一个办法。我们可以通过在外部合约设置一个方法 我们利用内部的call方法进行请求,如果能正确返回状态值则代表当前状态就是call了。
因为外部Call方法的状态即使revert()他也会只返回一个状态码0,并不会直接阻断整个交易的正常运行。

fallback()external payable{ bool success; (success,)=address(0x3c725134d74D5c45B4E4ABd2e5e2a109b5541288).call(""); if(!success){ return; } else{ selfdestruct(address(0)); }

}
![](https://md.buptmerak.cn/uploads/upload_7c5b602324701175c7f645f86f0611fa.png)
这样就成功绕过了沙箱
4. 这个是从github的官方wp中学到的 ,感觉应该和3的意思相同? 用等同于python的语法try catch 这样可以直接避免直接revert()

contract Setup { BabySandbox public sandbox;

constructor() {
    sandbox = new BabySandbox();
}

function isSolved() public view returns (bool) {
    uint size;
    assembly {
        size := extcodesize(sload(sandbox.slot))
    }
    return size == 0;
}

}

学到了很多opcode以及call staticcall delegatecall的知识。

## Rever
一道很巧妙的opcode构造题。给出源码:

pragma solidity 0.8.0;

contract Deployer { constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } } }

contract Challenge { address public fwd; address public rev;

function safe(bytes memory code) private returns (bool) {
    uint i = 0;
    while (i < code.length) {
        uint8 op = uint8(code[i]);
        if (
               op == 0x3B // EXTCODECOPY
            || op == 0x3C // EXTCODESIZE
            || op == 0x3F // EXTCODEHASH
            || op == 0x54 // SLOAD
            || op == 0x55 // SSTORE
            || op == 0xF0 // CREATE
            || op == 0xF1 // CALL
            || op == 0xF2 // CALLCODE
            || op == 0xF4 // DELEGATECALL
            || op == 0xF5 // CREATE2
            || op == 0xFA // STATICCALL
            || op == 0xFF // SELFDESTRUCT
        ) return false;
        
        if (op >= 0x60 && op < 0x80) i += (op - 0x60) + 1;
        
        i++;
    }
    
    return true;
}

function flip(bytes memory a) private returns (bytes memory) {
    bytes memory b = new bytes(a.length);
    for (uint i = 0; i < a.length; i++) {
        b[b.length - i - 1] = a[i];
    }
    return b;
}

function deployOne(bytes memory code) private returns (address) {
    require(code.length < 101, "deployOne/code-too-long");
    require(safe(code), "deployOne/code-unsafe");
    
    return address(new Deployer(code));
}

function deploy(bytes memory code) public {
    fwd = deployOne(code);
    rev = deployOne(flip(code));
}

}

contract Setup { Challenge public challenge;

constructor() {
    challenge = new Challenge();
}

function test(string memory what) public view returns (bool) {
    return test(challenge.fwd(), what) && test(challenge.rev(), what);
}

function test(address who, string memory what) public view returns (bool) {
    bool ok;
    assembly {
        ok := staticcall(gas(), who, add(what, 0x20), mload(what), 0x00, 0x00)
        if ok {
            if iszero(iszero(returndatasize())) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0x00, returndatasize())
                ok := mload(ptr)
            }
        }
    }
    return ok;
}

}

``` 我们首先分析challenge合约中的函数。 首先safe函数是一个用于检测字节码的函数。它里面会ban一些特定指令,但他不会搜索全部字节码。下面有一个 当op在0x60-0x80间,他会对其进行0x1-0x21的字节码的跳转不进行这其中字节码的检测。 flip函数就是单纯的把字节码全部颠倒0x32ff变为0xff32 deployOne 先进行了2个检测,一个是字节码长度不超过100,另一个是通过safe检测。然后就会部署你的Opcode并且返回合约地址 deploy函数是用于同时部署两个合约的。正向字节码以及反向字节码。进入DelpoyOne进行检测。

Setup中第一个test函数时用来检测正向反向两个合约的。需要两个合约都通过检测。其中可以输入一个字符串。进入下面的test函数。

下面的test函数中,首先进行了staticcall 并且把string作为数据输入进去。 如果交易成功那么进入下一层判断。 下一层是如果返回的datasize()如果不为0 还会进行操作。如果为0就可以直接让ok=1了。所以说我们可以知道让他成功不一定需要全部走完。但是还是要看这里。 这里首先把指针指向mem[0x40:0x60],之后把返回的data copy到了这里。然后ok取mem[0x40:0x60]。也就是如果有返回值需要返回1.


发布于2021-09-18 15:18:15
+10赞
0条评论
收藏
内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66