This vulnerability lets an attacker conceal malicious code that can be very difficult to spot without a deep understanding of how Solidity and the Proxy Pattern work. This has already been fixed on ZeppelinOS.
Solidity function calls’ internals
If you are a developer building for Ethereum you most likely code and think about your smart contracts in terms of Solidity, but that’s not how the network works with them.
From the network’s perspective, a smart contract is an account with a single chunk of code associated to it. If any other account sends a message¹ to the contract, its code will be executed on the EVM.
So how is it possible to call different functions if the contract only has one piece of consecutive code?
Ethereum defines a standard way of communicating between its components, which is the Application Binary Interface, or ABI for short. You can think of it as a low-level API, specifying not only which features are available in the system, but also how many things that we normally take for granted work. Some of these things are how functions should be called, how to pass them arguments, and how they return values.
The Ethereum ABI dictates that the
data parameter of your transaction must start with a function selector, which identifies which method you are trying to call. Using the selector your contract’s code jumps to the portion of itself that implements the function you’re trying to call.
Function selectors are just the first four bytes of the
sha3 hash of the function’s signature. For example, the
get’s selector is computed as
sha3(“get()”)[0:4], which gives us
set's one is the result of
There is only one exception to function selectors and that’s for the fallback function present in every smart contract, which doesn’t have a selector. It has the special behavior of being called when no
data parameter is provided, or when the given selector doesn’t match any of the contract’s methods.
The Proxy Pattern revisited
Much has been written about the Proxy Pattern, its different variations and their trade-offs. Regardless of the proxy pattern you choose, its core functionality will be the same: it forwards² all messages it receives to the current implementation of the contract.
Let’s take a look at how this works.
Don’t worry, you don’t need to understand how that scary assembly block works. It forwards the current message to the implementation, sending it the exact same
dataparameter it received.
Placing the forwarding logic in the fallback function allows us to forward any call into
Proxy, supposedly. Turns out, this doesn’t quite always happen.
Proxy also needs its own meta-functionality, as it needs to be upgradeable. So functions like
proxyOwner() won’t be forwarded, given that they exist and the fallback function isn’t executed.
Proxy selector clashing
Being a clever Ethereum dev, you may have realized that any function in the Proxy contract whose selector matches with one in the implementation contract will be called directly, completely skipping the implementation code.
Because the function selectors use a fixed amount of bytes, there will always be the possibility of a clash. This isn’t an issue for day to day development, given that the Solidity compiler will detect a selector clash within a contract, but this becomes exploitable when selectors are used for cross-contract interaction. Clashes can be abused to create a seemingly well-behaved contract that’s actually concealing a backdoor.
Armed with some rust code, we found that
clash550254402() has the same selector as
proxyOwner(). It took less than 15 minutes to find it in a newish Macbook Pro. A motivated hacker can optimize the process and dedicate much more resources to finding discrete-looking function names.
The Proxy pattern is the current approach being used across the Ethereum ecosystem to make smart contracts upgradeable, and the selector clashing attack allows any project using it — or an attacker who’s obtained access to the upgrading mechanism — to deploy code that conceals malicious functionality.
For example, most upgradability implementations have some notion of state migrations, which are functions that upgrade the contract’s storage. These are especially useful to disguise a selector clash given that auto-generated strings, like commit numbers, can be acceptable names for those functions, making a selector clash attack easy to disguise.
In the context of the security audit we conducted on ZeppelinOS, we found that this could be exploited by anyone and not just the Proxy owner, given that they intend to let any user of the network deploy implementations for other users to use. As another example, a function call that seems to move funds as it should may actually not be called at all, stealing someone’s money.
Before we found this vulnerability, Francisco Giordano from Zeppelin was already working on Transparent Proxies. It is an improved technique intended to let implementation contracts use the same function names as the
Proxy without the possibility of a selector clash. This eliminates the attack.
These new proxies work by forwarding any call as long as they don’t come from the Proxy owner. Clashes still exist, but if the caller is anyone other than the Proxy owner, the call is forwarded. This makes the Proxy owner the only account that can fall into a clash, hence users not exposed to concealment.
The only drawback is that other users won’t be able to read the
Proxy's own state (i.e. owner and implementation) using its ABI. They will need to use
web3.eth.getStorageAt() instead. This is a reasonably small price to pay for being certain that upgradable contracts do exactly what their implementation source code shows.
Exercise for the reader
For those who want to dig deeper in how this vulnerability can be exploited, we put together a small exercise. Your task is to try to steal the ropsten-ETH in this contract, and figure out what’s happening. Keep in mind that it is a
Proxy contract, so you should take a look at its implementation too.
You can do whatever you want with those contracts, just don’t fully empty its balance so other people can also play.
- Messages are how accounts communicate with each other. When you send a transaction, you are sending a message to another account. They are usually called internal transactions when the sender is a contract.
- Messages are not actually forwarded like in a traditional proxy. What’s happening is that we execute the implementation’s code as if it were the proxy’s via a
Get a high-quality smart contract audit from Nomic Labs.