paradigm 2022 ctf 题解——Rescue
author:Thomas_Xu
环境配置:
由于题目环境需要使用docker,环境配置有点繁琐。我重新搭了一个hardhat框架的测试环境,而由于题目出在以太坊的主链上,并使用Alchemy
fork了一个主网节点进行测试.
Resucue
首先先来看一下这道题目的描述:I accidentally sent some WETH to a contract, can you help me?
看起来像是由于操作失误,导致像一个合约转了一些ETH,想要完成此Challange就必须试图挽救一下这笔损失.
接下来看一看合约代码:
1 | // SPDX-License-Identifier: UNLICENSED |
这是Setup
合约,从中我们可以获取以下信息:
- 我们不小心向
mcHelper
合约转了10ether
- 完成Challange的条件是要求
mcHelper
合约里没有这笔钱return weth.balanceOf(address(mcHelper)) == 0;
所以我们可能并不是要做到把这笔钱转回到我们账户上,而是只需要使这10个ETH从mcHelper
消失即可.
有了上面的分析,似乎已经有了眉目,接下来分析MasterChefLike
合约:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.16;
import "./UniswapV2Like.sol";
interface ERC20Like {
function transferFrom(address, address, uint) external;
function transfer(address, uint) external;
function approve(address, uint) external;
function balanceOf(address) external view returns (uint);
}
interface MasterChefLike {
function poolInfo(uint256 id) external returns (
address lpToken,
uint256 allocPoint,
uint256 lastRewardBlock,
uint256 accSushiPerShare
);
}
contract MasterChefHelper {
MasterChefLike public constant masterchef = MasterChefLike(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd);
UniswapV2RouterLike public constant router = UniswapV2RouterLike(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);
function swapTokenForPoolToken(uint256 poolId, address tokenIn, uint256 amountIn, uint256 minAmountOut) external {
(address lpToken,,,) = masterchef.poolInfo(poolId);
address tokenOut0 = UniswapV2PairLike(lpToken).token0();
address tokenOut1 = UniswapV2PairLike(lpToken).token1();
ERC20Like(tokenIn).approve(address(router), type(uint256).max);
ERC20Like(tokenOut0).approve(address(router), type(uint256).max);
ERC20Like(tokenOut1).approve(address(router), type(uint256).max);
ERC20Like(tokenIn).transferFrom(msg.sender, address(this), amountIn);
// swap for both tokens of the lp pool
_swap(tokenIn, tokenOut0, amountIn / 2);
_swap(tokenIn, tokenOut1, amountIn / 2);
// add liquidity and give lp tokens to msg.sender
_addLiquidity(tokenOut0, tokenOut1, minAmountOut);
}
function _addLiquidity(address token0, address token1, uint256 minAmountOut) internal {
(,, uint256 amountOut) = router.addLiquidity(
token0,
token1,
ERC20Like(token0).balanceOf(address(this)),
ERC20Like(token1).balanceOf(address(this)), //WETH
0,
0,
msg.sender,
block.timestamp
);
require(amountOut >= minAmountOut);
}
function _swap(address tokenIn, address tokenOut, uint256 amountIn) internal {
address[] memory path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;
router.swapExactTokensForTokens(
amountIn,
0,
path,
address(this),
block.timestamp
);
}
}
这个合约只有一个外部调用函数:swapTokenForPoolToken()
所以我们可以集中目光研究此函数即可.
接下来我们一步一步分析这个函数:
1 | function swapTokenForPoolToken(uint256 poolId, address tokenIn, uint256 amountIn, uint256 minAmountOut) external { |
在函数最后调用了一个添加流动性的函数,这可能就是这笔误转账最终的去向
1 | function _addLiquidity(address token0, address token1, uint256 minAmountOut) internal { |
不难发现,这里添加流动性时,居然是把自己所有的余额全部发送到router里去addLiquidity
,这是明显不合理的,只要我们能控制以下交易对的交换额,我们就可以把此合约的ETH余额全部加入到流动性池子当中。
Exploit
对应以上的分析,想要解决此Challange
,我们还得到两种token去控制流动性兑换的数量。
这里我们选择usdc和dai,在etherscan
里可以查到poolId
为1的时候对应的pair就是usdc和Weth,并可获得token对应的地址
当然我们也可以用脚本来获取交易对信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48const { expect } = require("chai");
const { ethers } = require('hardhat');
const masterLike = require('../contracts/rescue/MatserChefLike.json')
describe("Challange rescue", function() {
let attacker,deployer;
it("should return the solved", async function() {
[attacker,deployer] = await ethers.getSigners();
const Weth = await ethers.getContractAt("WETH9","0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", attacker);
const SetupFactory = await ethers.getContractFactory("Setup", attacker);
const setup = await SetupFactory.deploy({
value: ethers.utils.parseEther("50")
});
//Exploit
let abi = [{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"poolInfo","outputs":[{"internalType":"address","name":"lpToken","type":"address"},{"internalType":"uint256","name":"allocPoint","type":"uint256"},{"internalType":"uint256","name":"lastRewardBlock","type":"uint256"},{"internalType":"uint256","name":"accSushiPerShare","type":"uint256"}],"stateMutability":"view","type":"function"}];
let UniswapV2pairLikeAbi = [{"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}];
erc20_abi = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]
let contractAddress = "0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd";
let provider = await ethers.getDefaultProvider();
const MasterLike = new ethers.Contract(contractAddress, abi, provider);
for(let i = 0; i < 18; i++){
// let pool_info = await MasterLike.connect(attacker).poolInfo(i);
let pool_info = await MasterLike.poolInfo(i);
let lp_token = pool_info[0];
const pair = new ethers.Contract(ethers.utils.getAddress(lp_token), UniswapV2pairLikeAbi, provider);
const token0 = await pair.token0();
let token1 = await pair.token1();
let token_contract0 = new ethers.Contract(token0, erc20_abi, provider);
let token_contract1 = new ethers.Contract(token1, erc20_abi, provider);
let token0_name = await token_contract0.symbol();
let tokne1_name = await token_contract1.symbol();
// console.log(lp_token)
console.log(`lp_token is : ${lp_token} token0 is : ${token0_name} token1 is : ${tokne1_name}`);
}
// const ExploitFactory = await ethers.getContractFactory("Exploit",attacker);
// const exploit = await ExploitFactory.deploy(setup.address, {value : ethers.utils.parseEther("50")});
expect(await setup.isSolved()).to.equal(true);
});
});
获取到的交易对如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20pair_id:0,lp_address:0x06da0fd433C1A5d7a4faa01111c044910A184553,token0:WETH,token1:USDT
pair_id:1,lp_address:0x397FF1542f962076d0BFE58eA045FfA2d347ACa0,token0:USDC,token1:WETH
pair_id:2,lp_address:0xC3D03e4F041Fd4cD388c549Ee2A29a9E5075882f,token0:DAI,token1:WETH
pair_id:3,lp_address:0xF1F85b2C54a2bD284B1cf4141D64fD171Bd85539,token0:sUSD,token1:WETH
pair_id:4,lp_address:0x31503dcb60119A812feE820bb7042752019F2355,token0:COMP,token1:WETH
pair_id:5,lp_address:0x5E63360E891BD60C69445970256C260b0A6A54c6,token0:LEND,token1:WETH
pair_id:6,lp_address:0xA1d7b2d891e3A1f9ef4bBC5be20630C2FEB1c470,token0:SNX,token1:WETH
pair_id:7,lp_address:0x001b6450083E531A5a7Bf310BD2c1Af4247E23D4,token0:UMA,token1:WETH
pair_id:8,lp_address:0xC40D16476380e4037e6b1A2594cAF6a6cc8Da967,token0:LINK,token1:WETH
pair_id:9,lp_address:0xA75F7c2F025f470355515482BdE9EFA8153536A8,token0:BAND,token1:WETH
pair_id:10,lp_address:0xCb2286d9471cc185281c4f763d34A962ED212962,token0:WETH,token1:AMPL
pair_id:11,lp_address:0x088ee5007C98a9677165D78dD2109AE4a3D04d0C,token0:YFI,token1:WETH
pair_id:12,lp_address:0x795065dCc9f64b5614C407a6EFDC400DA6221FB0,token0:SUSHI,token1:WETH
pair_id:13,lp_address:0x611CDe65deA90918c0078ac0400A72B0D25B9bb1,token0:REN,token1:WETH
pair_id:14,lp_address:0xaAD22f5543FCDaA694B68f94Be177B561836AE57,token0:sUSD,token1:$BASED
pair_id:15,lp_address:0x117d4288B3635021a3D612FE05a3Cbf5C717fEf2,token0:SRM,token1:WETH
pair_id:16,lp_address:0x95b54C8Da12BB23F7A5F6E26C38D04aCC6F81820,token0:YAMv2,token1:WETH
pair_id:17,lp_address:0x58Dc5a51fE44589BEb22E8CE67720B5BC5378009,token0:WETH,token1:CRV
pair_id:18,lp_address:0xDafd66636E2561b0284EDdE37e42d192F2844D40,token0:UNI,token1:WETH
pair_id:19,lp_address:0x36e2FCCCc59e5747Ff63a03ea2e5C0c2C14911e7,token0:xSUSHI,token1:WETH
我们可以写出以下攻击合约:
1 | pragma solidity 0.8.16; |
这里先用11个ETH去交换usdc,交换得到的usdc发送到mcHelper
合约,这里的11个ETH其实只要大于10即可,目的是为了将mcHelper
里的WETH全部加到流动性池子里.也可以通过getAmountOut
函数计算处需要传入的usdc数目。
然后我换一些dai到自己合约,以便于触发mcHelper
的swapTokenForPoolToken()
函数
js测试代码:
1 | const { expect } = require("chai"); |
true
...
...
This is copyright.