Exploiting the interface of Etherscan for Ethereum attacks

Martin Derka
quantstamp
Published in
10 min readJul 19, 2018

--

Last weekend, my hometown was going through a major heatwave. When I woke up on Saturday, I stepped outside onto my deck and immediately realized that this is not a day for outdoor activities, except for perhaps going to a beach. I walked back inside and started thinking about a pleasant way of spending my time in the comfort of my air conditioned living room. I looked at the Game of Thrones book on my desk, but did not feel like reading about more of my favourite characters dying. While this is not the kind of reading that usually I do for pleasure, I decided to browse the blockchain. I opened Etherscan and started browsing through source codes of contracts deployed in the past week. I was not looking for a specific type of contract; I was just curious. I found some ICOs, several games, and then ran into this contract that caught my interest.

It appears to be a game where someone, presumably the contract owner, asks a question. If a player guesses the correct answer, they receive the contract’s balance as a reward. When I found the contract, it was holding a balance of approximately 1.03 Ether. Fishy, right?

Figure 1: The status of the contract when found.

By analyzing the code, we can immediately spot a few odd things. My first impression was that this contract must have been implemented by a beginner developer who is just getting into the Solidity and Ethereum space. However, after proper investigation, it turned out to be a carefully composed piece of code that only pretends to be naive. Let us have a look at everything that is going on here.

Figure 2: The source code of the contract.

The contract uses Solidity compiler of version 0.4.20 and above. Given that 0.4.25 just came out, it appears to be a bit old. Furthermore, it declares the version as “at least” (using the ^ character). This construct is valid for implementing interfaces and library contracts that are reused in other projects, but smart contracts deployed to the blockchain should fix a single version of solidity that they were implemented in and tested against. This is merely important for projects with ongoing development and does not pose any security threat to us.

The contract declares three internal attributes that represent its state: a question (string, line 16), the hash of the answer to this question (byte32, line 20), and the address of the person who asked the question (address, line 18). This is all that the contract needs to carry out its function, which is to decide whether a player provided the correct answer, and if this is the case, to transfer the balance to them. Rather oddly, the contract does not implement the constructor method where these attributes would be initiated. Thus, when deployed, all the attributes receive the default values. The contract further offers additional methods that allow the owner to set the question. Such a solution requires the owner of the contract to run multiple transactions and spend more gas. Again, this appears to be a beginner’s mistake, but in fact, it is a deliberate construct.

After deploying the contract, the owner is likely expecting to call method StartGame (line 22). This method sets a question, response hash, and also persists the address of the person who asked the question. However, all of these actions happen only if no response hash exists yet, otherwise the method has no effect. This is taken care of by wrapping the code inside of an if statement.

In addition to StartGame, the contract also provides a function NewQuestion (line 42). This function assumes that some question was asked before as it requires that msg.sender is the person who asked the current question. If that is the case, it overwrites the question and bytes of the hash. This method can be called repeatedly by the person who asked the question. This exposes the players to a “transaction ordering vulnerability” as follows. Assume that a question has been asked by the contract owner. Any player who sends a transaction with an answer would be waiting for the miners to record it in the blockchain. If the question poser changes the question at the same time by invoking NewQuestion method, and if this transaction is mined first, the player’s answer will be answering the wrong question. This method can be called repeatedly, regardless of the state of the contract.

Furthermore, the contract offers a method for playing. It accepts an answer and requires some Ether to be sent along with it. Interestingly, the method first checks that msg.sender == tx.origin. The best practices of Solidity warn against the use of tx.origin in order to verify that methods are called by certain users (most frequently the contract owner). However, this is a different and valid use. This construct ensures that there is no middle man in this method invocation, i.e., that the method was called directly by the player’s wallet and not through another contract. Then, if the hash of the provided answer is the same as the hash of the question asked, and if at least 1 Ether was sent along with it, the contract will pay out its balance to the successful player. Otherwise, the Ether sent along will be kept by this contract and will become a part of the reward.

The contract allows the question poser to stop the game and retrieve all the Ether by calling function StopGame (line 34). This is another exposure to the transaction ordering vulnerability. If a player sends an answer with some Ether, and if the question poser decides to stop the game while the player’s transaction is waiting, the player will lose either gas if the provided answer is correct (the player will win and receive the Ether sent along less gas), or all the Ether if the answer is wrong. This method can also be called repeatedly regardless the state of the contract.

Finally, it also implements an empty fallback (line 51).

So, let us summarize. We have some transaction ordering dependence that exposes us to some potential manipulation. Furthermore, the contract has some odd-looking and inefficient constructs. Most importantly, it seems to assume that the data in private attributes are hidden from the world. Beginner’s mistake — -we can look at the transaction history, we can precisely decipher what the answer is. With the balance of this contract being a bit over 1 Ether, participating in this game seemed like an appealing opportunity for any potential player.

At this point, I was convinced that this contract was a scam. I was convinced that something beyond my understanding would cause players to lose their Ether. I decided to see if I could learn more about how this scam worked. I used Etherscan to look at all of the transactions, which included a single internal transaction. I saw the screen below that told me that the owner performed exactly 2 actions:

  1. Created the contract in block 5873826 via transaction 0xf9f25d11…85c6
  2. Asked question “Imagine you are swimming in the sea and a bunch of hungry sharks surround you. How do you get out alive?” with the answer “Stop Imagining” and added 1.030103834778 Ether as the reward in block 5873943 via transaction 0x41365b3d…fa5b
Figure 3: The transaction presumably recording the question and answer.

No internal transactions were recorded. So, I transferred some Ether to my Metamask account and went ahead. I first answered the question with no Ether attached. I wanted to create a transaction with the proper answer to doublecheck that it will be encoded exactly that same as when the owner created it. If that is the case, SHA256 should produce the same result. I used MyEtherWallet and Metamask to call the method. An inspection of the created transactions told me my assumption was correct. That was all I needed. I sent in 1.05 Ether (strictly more than 1 in order to satisfy the condition), and the answer. When my transaction got mined, the expected happened! The balance of the contract increased to a bit over 2.08 Ether, and my wallet remained nearly empty.

Figure 4: The current state of the contract as it can be found here. The two most recent transactions from address 0x1d971732ec0fc7223204a34d5e915fa502c893ed are created by myself.

Clearly, I must have missed something. I kept inspecting the code and the transaction and could not wrap my head around it. The transaction ordering was correct. I could not see anything else in the code, so I deployed it locally and verified the behaviour through tests. Everything behaved exactly as expected. While I was investigating, the balance of the contract dropped to 0 as the question poser stopped the game roughly an hour after my response. And this transaction revealed everything.

The contract did not transfer the balance (including my Ether) to a wallet, but to another contract with private source code. When investigating the history of transactions, I found out that there is one internal transaction going to the game contract, and that this transaction does not show up in the history of the game contract’s transactions on Etherscan! The real sequence of actions was as follows:

  1. The creator deployed the middle-man contract in block 5806406.
  2. The creator deployed the game contract in block 5873826.
  3. The creator sent a transaction to the middle-man contract in block 5873890 that made two calls to the game contract. Both these calls are invisible in the transaction history (both internal and external) of the game contract on Etherscan, but they can be found in the traces of the transaction. The calls and parameters can be decoded using the game contract’s ABI that is available on Etherscan and any Ethereum input decoder, for example the one here. The first action calls StartGame with “Imagine you are swimming in the sea and a bunch of hungry sharks surround you. How do you get out alive?” as the question setting the right answer to “sZs”. The other action calls NewQuestion with the same question and sets the hash to some bytes (see Figure 5).
  4. The creator sent a direct transaction in block 5873943 to the game contract’s StartGame with the question “Imagine you are swimming in the sea and a bunch of hungry sharks surround you. How do you get out alive?” and answer “Stop Imagining”. This transaction had no effect on the state of the contract. The answer hash was no longer 0x0, but this hash was very visible on Etherscan.
  5. I came and played with the wrong answer.
Figure 5: The decoded inputs for the internal calls made to the game contract.

Let us now step back and admire the elaborate construction of this contract that targets the users of Etherscan and similar Ethereum explorers exploiting the fact that Etherscan does not show incoming transactions invoked by external contracts. First, let us look at how StartGame checks that no response exists yet. It wraps the body in an if statement, which plays the role of a silent invisible killer. There are some code alternatives that would achieve the same effect, most prominently using require(responseHash == 0x0). However, the if statement used on line 26 does not cause a throw, so the fact that the state of the contract did not change will not be indicated to the audience by failed transactions.

Second, method NewQuestion on line 42 accepts the answer hash, that is, a sequence of bytes. At the same time, playing the game (line 5) asks for the original answer in the form of a string, and StopGame on line 34 does not require the question poser to prove that the answer is known at all. As a result, if we somehow discover that this internal call happened, we are still unable to guess the answer. The blockchain will tell us what its hash is, but from the principle of irreversible hash functions such as SHA256, it does not tell us anything about the input. While we could theoretically start searching for the proper answer using brute force, the search space is infinite. In fact, I am not even aware of a proof that the answer exists! Since StopGame does not require the question poser to reveal the answer, I suggest that even the person who set this contract up does not know what it is, and in fact, the provided sequence are just some random bytes.

Lastly, independently of this issue, the developer made all the effort to skew the system to his own benefit. Remember line 9 in Play method that guaranteed that this method cannot be called via another contract, and the transaction ordering vulnerabilities? As a careful player, I would be able to go around the transaction ordering by invoking Play via another contract. This contract would first check that the question and balance did not change; i.e., nobody called StopGame or NewQuestion with a different question, and nobody beat me to the answer that I wanted to guess, and only in such a case, make a call. Yet, this is not possible, because the invocation must come from a wallet.

So, what is the message here? Mainly, Etherscan does not reveal everything. There are people out there who know it and exploit it! There are other Ethereum explorers that do not seem to suffer from this issue (I was able to find all the transactions here for example), but there is no guarantee that they do not suffer from different vulnerabilities.

This type of scam targets the imperfections of blockchain explorer UIs. In order to counter it, companies operating blockchain explorers should become aware of these vulnerabilities so that they can modify their UI in order to prevent clever scammers from exploiting their users.

Luckily, as a user you do not need to rely on an explorer to read the blockchain. If you want to reliably know what happened on the blockchain, read the chain directly!

Figure 6: The complete transaction history of the game contract as shown by a different Ethereum explorer.

Here are more contracts that I discovered that take advantage of the same vulnerability.

This contract is still alive: https://etherscan.io/address/0xce6B1AFf0fE66da643D7A9A64d4747293628D667#code

Game stopped, but Ether was stolen: https://etherscan.io/address/0xFf45211eBdfc7EBCC458E584bcEc4EAC19d6A624#code

Game stopped: https://etherscan.io/address/0x4bc53ead2ae82e0c723ee8e3d7bacfb1fafea1ce#code

Game stopped: https://etherscan.io/address/0x3B048ab84ddd61C2FfE89EDe66D68ef27661C0f2

Game stopped: https://etherscan.io/address/0x5ccfcDC1c88134993F48a898AE8E9E35853B2068#code

--

--

Martin Derka
quantstamp

Senior research engineer at Quantstamp with a Ph.D. from UWaterloo. Enjoys traveling, sports and rock-metal music. www.linkedin.com/in/mderka