Destroying the Indestructible

This morning, I saw a link to Dharma’s IndestructibleRegistry. The idea behind this registry is that it keeps track of contracts that cannot be destroyed. It does this by verifying the contract’s bytecode on chain. In this post, I’ll show you how I managed to trick that verification and destroy an “indestructible” contract.

How Does IndestructibleRegistry Work?

The core of the contract is the _isPotentiallyDestructible() function. That function fetches the code for a given address and iterates over the bytecode according to the following algorithm:

The algorithm ignores push data to avoid flagging innocent things like PUSH1 0xFF. (0xFF is the bytecode for SELFDESTRUCT, but here it’s just a value being pushed onto the stack.) The code also skips over unreachable code and resumes analysis when a JUMPDEST is found. This avoids flagging things like the metadata hash Solidity inserts into contract code.

A First Attempt

I initially thought I could fool the analysis with the following simple bytecode. It attempts to use push data to hide the malicious code:

0x00 PUSH1
0x01 0x04         // jump destination
0x02 JUMP
0x03 PUSH3
0x04 JUMPDEST
0x05 CALLVALUE    // just get a 0 on the stack, the selfdestruct destination
0x06 SELFDESTRUCT // hidden in push data

However, as @z0age pointed out to me, section 9.4.3 of the Ethereum Yellow Paper specifies that a JUMPDEST inside push data is invalid. A viable exploit has to pass IndestructibleRegistry’s checks but also be executable by an Ethereum node, so this won’t work.

I dug around in the Geth source code and found validJumpdest() and codeBitmap(), which implement this logic in Geth.

The Geth logic for determining whether a JUMPDEST is valid is quite simple. It first determines which bytes of the code are non-executable data as follows:

A JUMPDEST is then valid if it’s not marked as data.

Finding the Real Exploit

Armed with a better understanding of both IndestructibleRegistry and how jump destinations are validated, I looked closely to see how the two algorithms differed.

The significant difference is how IndestructibleRegistry analyzes unreachable code. When it determines that some code is unreachable, it scans it byte by byte looking for JUMPDESTs and then goes back into “reachable” mode. This byte-by-byte scan is not how Ethereum nodes analyze the code when looking for valid jump destinations.

Ethereum nodes don’t do reachability analysis at all, but IndestructibleRegistry does this to avoid noise when the dangerous opcodes occur in innocuous places, like the metadata hash the Solidity compiler includes in the bytecode.

This difference lets us trick the registry into performing a different analysis than an Ethereum node in an exploitable way.

Here’s the exploit code, annotated from the perspective of IndestructibleRegistry:

0x00 PUSH1
0x01 0x06
0x02 JUMP         // get us into "unreachable" mode
0x03 PUSH2        // unreachable code that isn't a JUMPDEST: ignored
0x04 JUMPDEST     // back out of "unreachable" mode
0x05 PUSH3        // skip the next 3 bytes of push data
0x06 JUMPDEST
0x07 CALLVALUE
0x08 SELFDESTRUCT

Here’s the same code, annotated from the perspective of an Ethereum node:

0x00 PUSH1
0x01 0x06
0x02 JUMP
0x03 PUSH2        // skip the next 2 bytes of push data
0x04 JUMPDEST
0x05 PUSH3
0x06 JUMPDEST     // valid jump destination
0x07 CALLVALUE
0x08 SELFDESTRUCT

I was able to deploy that code to the blockchain, register it with IndestructibleRegistry, and finally self-destruct it.

contract registered as indestructible

contract registered as indestructible

contract destroyed

contract destroyed

Why Is IndestructibleRegistry Useful?

There are a number of reasons why something like the IndestructibleRegistry could be useful. From a security perspective, one important reason is to make sure that a contract’s code can’t change. Since the introduction of the CREATE2 opcode, it’s possible to deploy new contract code to the same address.

In our recent audit of the Orchid Network Protocol, we came across a potential need for this type of verification. In that system, a server needed to ensure that a smart contract will, at some point in the future, return a certain value when passed a certain set of parameters. To know what a contract will do in the future, you need to know (among other things) that the code cannot be changed. If the contract can be destroyed, the code can change, which means the future behavior is unknowable.

Impact

This vulnerability is low impact. In the case of Dharma, it’s not being used for anything security critical; it’s just an extra layer of assurance that their implementation contracts can’t be destroyed. In fact, the IndestructibleRegistry contract is listed as the lowest priority in the Dharma Smart Wallet audit conducted by Trail of Bits.

It’s difficult to know how others may be using the contract, but there’s already a fixed version available, and indestructible.eth now points there instead of the vulnerable version.

Is the New Version Safe?

You tell me! The code is tricky enough that it wouldn’t surprise me to find another vulnerability in the fixed code. I had fun figuring out the exploit in the original code, so I recommend others who are interested to give it a try with the new code.

Please share what you come up with!

Acknowledgements

The code for IndestructibleRegistry is quite similar to another (insecure) attempt at doing the same thing, which my colleague @GNSPS pointed me to during a recent audit. Kudos to @recmo for discovering the flaw in that code.

Kudos also to @z0age, one of the authors of IndestructibleRegistry. He was receptive and helpful as I worked through finding an exploit, and he quickly deployed a fix.


Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. Contact us.

More posts chevronRight icon