“Wallet Library Self-Destruct” exploit, code along

Bugs in Solidity are costly, putting yourself and many others at risk, so it’s important to take precautions when writing and deploying smart contracts. We’re going to explore one of those bugs, the “Multi-Sig Self-destruct Exploit”. We’re going to do this scenario using a simplified WalletLibrary contract.

The “Multi-Sig Library Self-Destruct” was a bug that was quite simple. In the previous blog we talked about the “Multisig wallet Exploit” and the WalletLibrary contract. Inside of the contract was an initWallet(address) function that made it possible for attackers to take control of calling contracts. This bug was the fault of both parties the calling contract and the WalletLibrary contract.

The bug will talk about today was a bug that was strictly tied to the WalletLibrary contract itself. A bug that blocked funds for over 587 wallets, locking up an estimated 513,774.16 in ether, worth around $286,840,113 USD in today’s market.

More info from the Parity team themselves, link.

Prerequisites:

It’s expected that you have a basic understanding of the Ethereum Blockchain technologies and the programming language Solidity, a smart contract concept, that compiles down to EVM bytecode.

If you don’t know what Smart Contract or Solidity is, explore these links below:

  1. I’m using a mac laptop.

2. NodeJS — is a JavaScript runtime built on Chrome’s V8 JavaScript engine you’ll need at least version 6.9.1 or higher.

3. NPM packages:

$ npm i -g ganache-cli

The first package we install is ganache-cli, which is a “NodeJS based Ethereum client for testing and development”. Ganache is a private blockchain with its own genesis block, that has all the functionality of the live Ethereum blockchain.

$ npm i -g truffle

The second package truffle, is a deployment and testing framework built to make contract deployment and management easier for the developer. We’re going to use this framework for our deployment flow and the console that it provides, which is a NodeJS console with a couple of extra packages injected into it.

Getting Started:

We’re going to run a few commands to scaffold out our project.

$ mkdir multisig_selfdestruct
$ cd multisig_selfdestruct

The commands above create our project folder and then changes to that project folder.

$ truffle init

Running the command above will scaffold a project with the directory structure like the one below.

$ touch contracts/WalletLibrary.sol
$ touch migrations/2_deploy_walletlibrary.js

These are files we’re going to use to store our WalletLibrary contract and our deployment script for that contract.

Inside of the root of our current project, there’s a file called truffle.js. Place the snippet of code inside of that file and save. This config file is for TruffleJS. The file will tell the TruffleJS tools what blockchain to work within development.

Running our private testing blockchain

In a brand new terminal window run the command below.

$ ganache-cli -u 0

This command will start up a brand new private testing blockchain with its own genesis block. When starting a Ganache blockchain you get an HD wallet. This wallet will have 10 accounts associated with it, available for use. The -u 0 part of the command will unlock the first account so we can create and sign transactions.

The WalletLibrary contract:

Here we have a WalletLibrary contract similar to the one in the previous blog. The only differences are the onlyOwner modifier and the kill function.

  • onlyOwner: This is a function modifier. Function modifiers are appended on to functions, like on line 29. This modifier checks to see if the person calling the function is the owner of the contract and if they’re not, then quietly exit the function. Good job!
  • kill: This function does exactly what it implies. Using the selfdestruct opcode, the owner of the contract is allowed to remove the contract and specify an address where the contract will send the remaining funds. It also has a function modifier appended to the end of it. Looking at the function as its defined, we can see that the owner is the only one who can “kill” the contract, which is what we expect. All is well then right? Well not exactly.

Can you spot the vulnerability in the contract?

Deploying our library:

If you haven’t started up your blockchain, then go ahead and do so. Check the “Running our private testing blockchain” section above for more details.

$ truffle migrate

Inside of a new terminal window, go ahead and type out the command above. This command will deploy our contracts to our private blockchain. If that command was successful you should see the contract hashes like below.

You might get a few compiler warnings which won’t break the contracts but you should explore them on your own because the information you learn is important. Below the warnings, we see that the contracts were successfully deployed. Right above each contract’s name are the transaction hashes of those contracts. Right next to the contract’s name are the contract’s address. the WalletLibrary address is 0xcd66a2baead395c369fb261a00507fb35202fc24. These will be different when you run the command in your terminal.

The Attack:

Now that we have our WalletLibrary contract setup we’re going to jump right into the attack. Imagine 587 wallets rely on this WalletLibrary contract and if anything happens to it, they won’t be able to use their wallets, effectively losing all their funds, rendering their contracts useless. The calling contracts could have had some upgradability mechanism to combat against this.

$ truffle console

Inside of your terminal go ahead and type out the command above. This will open up a NodeJS console with a couple of packages pre-installed for us via the TruffleJS development tool.

truffle(development)> Wallet.deployed().then(w => wallet = w)

Now that we are inside of our NodeJS console we can interact with our WalletLibrary contract. Here we get an instance of this library contract and set it to the wallet variable. If all went well we should see an output like below.

This shows us the properties of our Walletlibrary contract, from the bytecode to the methods on the contract. All of this is available for our use. More on contract interactions here.

truffle(development)> wallet.owner.call()

Once we run the command above we’re going to get an address back. The address will be exactly 0x0000000000000000000000000000000000000000. This is what the WalletLibrary defaults to when its owner variable doesn’t have a value. Since the WalletLibrary doesn’t need an owner its doesn’t need to be set, the logic makes sense.

truffle(development)> wallet.initWallet(web.eth.accounts[0])

This is probably one of the simplest attacks in hindsight. The attacker simply calls the initWallet function with their address and that’s it, they’re the new owner of the WalletLibrary contract. Since the initWallet function did not check to see if this was a calling contract and wasn’t set beforehand, they were able to set themselves as the owner of the library contract. But why would anybody want to be the owner of a contract that doesn’t have any funds? They can’t get funds from the contracts interfacing with this library directly. What is the point? It’s pretty useless to do this right? Well…

Once the command finishes, you’ll see the transaction results, like above.

truffle(development)> wallet.owner.call()

Now when we call the command above we get the attackers address. This will be whatever address you decided to call the initWallet function with.

The Real Attack:

Sometimes people are just haters or they make a mistake. I say this because the new owner of the contract flipped the kill switch on the library contract and nobody really knows why. In this link, the attacker explains themselves and I want to believe them, but man, maybe the NEO team? JK. Theres evidences he attempted this a number of times. Let’s see this in action.

truffle(development)> wallet.kill()

Done! The contract has erupted.

When you run the command you’ll see the transaction results printed to your terminal.

Transaction results above.

truffle(development)> wallet.owner.call()

Now when we try to check the owner variable we get a weird result.

When we try to check the owner or run any function off the WalletLibrary contract will get the error above. The contract is gone. All of the contracts interfacing with this library are no longer usable. The reason these contracts are no longer usable is because they were making delegatecall‘s to our WalletLibrary and it doesn’t exist anymore. Another reason these contracts are unusable is that the contracts didn’t have a way to upgrade to another library if the WalletLibrary ever became corrupt, which is exactly what happened.

Attack summary:

  • The WalletLibrary had an exposed function called initWallet which sets the owner of calling contracts.
  • The initWallet function could also set the owner of the WalletLibrary itself.
  • The WalletLibrary owner was not set beforehand.
  • The attacker made themselves the owner of the contract by setting their address via the initWallet function.
  • Being the owner allowed them to call any function on the contract as the “rightful owner”.
  • The attacker/owner decided to call the kill function.
  • The kill function runs the selfdestruct opcode, which destroys the contract completely.
  • Calling contracts make calls to the now self-destructed WalletLibrary contract and nothing.

More here.

Conclusion:

As you can see Solidity is like any other programming language but the same mistakes can be much more costly (literally). The original contract was created and audited by the Ethereum Foundation’s DEV team, Parity Technologies and others in the community. The Parity team took this contract and restructured it to be what they needed and this version of the WalletLibrary had been internally audited.

Rather than just having more audits, we strongly believe that more extensive and formal procedures and tooling around the deployment, monitoring and testing of contracts will be needed to achieve security. We believe that the entire ecosystem as a whole is in urgent need of such procedures and tooling to prevent similar issues from happening again, in particular, if and when the number and complexity of live contracts grows. — Parity Team

I agree with the statement above. The tooling in Ethereum/Solidity is just not moving at the same pace as the innovation being brought by these teams. This is an area I look to explore further. There are more functional languages like LLL and some others coming soon that will help with developer mindset. I look forward to contributing and adding to this community, to foster in a decentralized, self-sovereign world. Be careful my friends.

Feedback welcome and Thank you for your time.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Linkedin Twitter Github Email