Paradigm research partner reviews zero-day vulnerabilities in etherscan

“If you find a 0day (zero-day) loophole in etherscan, what would you do? I decided to build a’pinball machine’.”-samczsun, research partner of paradigmParadigm research partner reviews zero-day vulnerabilities in etherscan

Note: The original author is samczsun.

I like to challenge assumptions.

I like to try to do the impossible, find what others have missed, and shock them with things they have never seen before. Last year, I wrote a challenge for Paradigm CTF 2021 based on a very puzzling Solidity vulnerability. Although a variant was publicly disclosed, the vulnerability I found was never actually discussed. As a result, almost everyone who tried to challenge it was stumped by its seemingly impossible nature.

A few weeks ago, we were discussing plans for Paradigm CTF 2022, when Georgios tweeted a difficult tweet, and I thought it would be very cool to post a challenge on the same day as the launch of the conference call. However, this can’t be just an old challenge. I want to get something from this world, something no one will see, something beyond the limits of people’s imagination. I would like to write the first use 0day (zero-day) vulnerabilities Ethernet Square CTF challenge.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

How to create a 0day (zero-day) vulnerability

As security researchers, in order to optimize our time, we will make some basic assumptions. One is that the source code we are reading does produce the contract we are analyzing. Of course, this assumption is only valid when we read the source code from a trusted place, such as Etherscan. Therefore, if I can find a way for Etherscan to verify something incorrectly, I can design a truly roundabout puzzle around it.

In order to find out how to use Etherscan’s contract verification system, I have to verify some contracts. I deployed some contracts on the Ropsten testnet to toss and try to verify them. Soon, I saw the interface below.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

I chose the correct setting and proceeded to the next page. Here, I was asked to provide the source code of my contract.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

I entered the source code and clicked the verify button. Sure enough, my source code is now attached to my contract.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

Now that I know how things work, I can start teasing the verification process. The first thing I try to deploy a new contract, will foochange bar, and verify that the contract with the original source code. Not surprisingly, Etherscan refused to verify my contract.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

However, when I manually compare the two bytecode outputs, I noticed something strange. The contract bytecode should be in hexadecimal, but obviously some are not in hexadecimal.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

I know that Solidity attaches contract metadata to the deployed bytecode, but I never really considered how it affects contract verification. Obviously, Etherscan is scanning the bytecode of the metadata and then replacing it with a tag, “Anything in this area can be different, and we will still consider it the same bytecode.”

For potential 0day (zero-day) vulnerabilities, this seems to be a promising clue. If I can trick Etherscan into interpreting non-metadata as metadata, then I will be able to adjust the bytecode of my deployment in the area marked {ipfs} while still letting it verify as legitimate bytecode.

The easiest way I can think of to include some arbitrary bytecode in the creation transaction is to encode them as constructor parameters. Solidity encodes the constructor parameters by directly appending the ABI encoding form to the creation transaction data.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

However, Etherscan is so smart that it excludes constructor parameters from any kind of metadata sniffing. You can see that the constructor parameters are in italics to indicate that they are separate from the code itself.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

This means that I need to trick the Solidity compiler in some way to emit a sequence of bytes that I control so that it resembles embedded metadata. However, this seems to be a difficult problem to solve, because without some serious compiler arguments, I can hardly control the opcodes or bytes that Solidity chooses to use, and then the source code will look very suspicious.

I considered this question for a while, until I realized that it is actually very easy to cause Solidity to emit (almost) arbitrary bytes. The following code will cause Solidity to send out 32 bytes of 0xAA.

bytes32 value = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;

Encouraged, I quickly wrote a small contract that will push a series of constants so that Solidity will emit bytecodes that are exactly similar to the embedded metadata.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

To my delight, Etherscan marked the existence of IPFS hashes in the middle of my contract.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

I quickly copied the expected bytecode and replaced the IPFS hash with some random bytes, and then deployed the generated contract. Sure enough, Etherscan considered different byte businesses as usual and allowed my contract to be verified.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

With this contract, the source code suggests that a simple bytes object should be returned when calling example(). However, if you really try to call it, this will happen.

$ seth call 0x3cd2138cabfb03c8ed9687561a0ef5c9a153923f ‘example()’
seth-rpc: {“id”:1,”jsonrpc”:”2.0″,”method”:”eth_call”,”params”:[{“data”:”0x54353f2f”,”to”:”0x3CD2138CAbfB03c8eD9687561a0ef5C9a153923f”},”latest”]}
seth-rpc: error:   code       -32000
seth-rpc: error:   message    stack underflow (5 <=> 16)

I successfully discovered a 0day (zero-day) vulnerability in Etherscan, and now I can verify contracts that behave completely differently from the source code recommendations. And now, I just need to design a decryption game around it.

A wrong start

Obviously, this decryption game will revolve around the idea that the source code seen on Etherscan does not imply the actual behavior of the contract. I also want to make sure that players cannot simply replay transactions directly, so the solution must be unique for each address. The best way is obviously to require a signature.

But under what circumstances will players be required to sign some data? My first design was a simple puzzle with a single public function. The player will call the function with some input, sign the data to prove that they have proposed a solution, and if the input passes all the various checks, then they will be marked as a solver. However, when I fleshed out this design in the next few hours, I quickly became dissatisfied with the result of the matter. It started to become very cumbersome and inelegant, and I couldn’t bear to be in such a badly designed puzzle. It ruined such a great idea of ​​0day (zero-day) loopholes.

I have to admit that I couldn’t finish this thing before Friday, so I decided to sleep.

Pinball puzzle

Over the weekend I continued to try to iterate my initial design, but did not make more progress. It’s as if my current method has hit a wall, and although I don’t want to admit it, I know that if I want something that I am satisfied with, I may have to start over.

In the end, I found myself re-examining this issue from first principles. What I want is a decryption game in which players must complete various knowledge checks. However, I did not require that completing the knowledge check itself is a condition of winning. On the contrary, this may be one of the many paths that players are allowed to choose. Maybe players can accumulate points in the entire puzzle, and use this loophole to get some kind of reward. The condition to win is only the highest score, so the use of loopholes is indirectly encouraged.

I recalled a challenge I designed last year, Lockbox‌, which forced players to construct a single block of data to meet the requirements of six different contracts. The contract applies different constraints to the same byte, forcing players to be smart in the way they construct payloads. I realize that I want to do something similar here, I will ask players to submit a single data blob, and I will reward points based on certain data parts that meet specific requirements.

It was at this point that I realized that I was basically describing pinboooll, which was a challenge I faced during the DEFCON CTF 2020 finals. The gimmick of pinboooll is that when you execute a binary file, the execution bounces on the control flow graph, just like a ball bounces in a pinball machine. By correctly structuring the input, you will be able to find specific parts of the code and earn points. Of course, there is also a loophole, but frankly, I have forgotten what it is, and I am not going to look for it again. In addition, I already have a vulnerability that I want to exploit.

Since I was dealing with a zero-day vulnerability in operation, I decided to solve this problem as soon as possible. Finally, I spent a few hours to re-understand the working principle of pinboooll, and spent a few days to re-implement it. This solves the puzzle bracket problem, and now I only need to integrate this vulnerability.

Arming a zero-day loophole

My way to get Solidity to output the correct bytes has always been to just load a few constants and let Solidity issue the corresponding PUSH instruction. However, such arbitrary constants can be a huge red flag, and I want something that can be better integrated into it. I also have to load all the constants in a row, which is difficult to explain in actual code.

Because I really only need to hardcode two magical byte sequences (0xa264…1220 and 0x6473…0033), I decided to see if I can sandwich code between them instead of the third constant . In the deployed contract, I only need to replace the sandwiched code with some other instructions.

address a = 0xa264…1220;
uint x = 1 + 1 + 1 + … + 1;
address b = 0x6473…0033;

After some experiments, I found that this is possible, but only if the optimizer is enabled. Otherwise, Solidity will emit excessive value cleanup code. This is acceptable, so I continue to improve the code itself.

I can only modify the code in two addresses, but it would be strange to see a dangling address at the end, so I decided to use them in the conditional statement. I also had to prove the necessity of the second condition, so in the end I voted for a little reward. I did the first conditional check to check whether tx.origin matches the hard-coded value to give people an initial impression that there is no need to pursue this code path further.

if (tx.origin != 0x13378bd7CacfCAb2909Fa2646970667358221220) return true;
state.rand = 0x40;
state.location = 0x60;
if (msg.sender != 0x64736F6c6343A0FB380033c82951b4126BD95042) return true;
state.baseScore += 1500;

Now that the source code is ready, I must write the actual backdoor. My backdoor needs to verify whether the player has triggered the exploit correctly. If they don’t trigger it correctly, they will fail without giving any prompt. If they are successfully triggered, they will be rewarded. I want to make sure that the loopholes will not be easily replayed, so I decided to only require players to sign their own addresses and submit their signatures in the transaction. To increase the fun, I decided to require the signature to be located at offset 0x44 in the transaction data, where the ball usually starts. This will require players to understand how ABI coding works and manually relocate the ball data to other places.

However, here I encountered a big problem: it is impossible to put all this logic into 31-byte handwritten assembly. Fortunately, after some consideration, I realized that I had another 31 bytes available. After all, the truly embedded metadata contains another IPFS hash that Etherscan will also ignore.

After playing some code golf, I arrived at a working back door. In the first IPFS hash, I will immediately pop up the address I just pushed, and then jump to the second IPFS hash. There, I will hash the caller and partially set the memory/stack to call ecrecover. Then I jump back to the first IPFS hash, where I finish setting up the stack and executing the call. Finally, I set the score multiplier to equal (msg.sender == ecrecover()) * 0x40 + 1, which means no additional branches are required.

After encoding the backdoor to a certain size, I posted my Rinkeby address on Twitter to get some testnet ETH from the faucet , and then hinted to anyone watching this tweet that something might happen. Next, I deployed the contract and verified it.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

All that is left to do now is to wait for the destined person to discover the back door hidden in sight.

Note: As of press time, software developer Kane Wallmann from Rocket Pool has solved this puzzle. The specific process is here:

In addition, Etherscan developer Caleb Lau has also fixed the vulnerability.

Paradigm research partner reviews zero-day vulnerabilities in etherscan

Posted by:CoinYuppie,Reprinted with attribution to:
Coinyuppie is an open information publishing platform, all information provided is not related to the views and positions of coinyuppie, and does not constitute any investment and financial advice. Users are expected to carefully screen and prevent risks.

Leave a Reply