What have I learned from the code engineering of MEV boss?
The complexity of the code combined with the artistic expression results in one of the most sophisticated code obfuscations I have ever seen.
Original title: "To Reverse A Big Brain"
Original author: @jtriley_eth, twitter
Original translation: zhouzhou, BlockBeats
Editor's note: In the process of reversing the MEV contract of bigbrainchad.eth, we found that it used complex code obfuscation and stack scrambling techniques, making it difficult for automated tools to handle. There are a large number of unnecessary code blocks and loops in the contract, and the complexity is further increased by dynamic jumps and timestamp control. Despite the use of tools such as Heimdall and Tenderly, progress was limited. In the end, we turned to Python for manual analysis, trying to find a breakthrough through block-by-block parsing and stack annotations. This reverse engineering is challenging but still not fully understood.
Bigbrainchad.eth is an account running a MEV contract where every transaction hash starts with 0xbeef, every function call starts with codeIsLaw, and every transaction input starts with 0xdeadbeef. At a high level, it's a multicaller. But under the hood, the complexity of the code combines with the artistic expression to form one of the most sophisticated code obfuscations I've ever seen.
Preliminary Attack
To reverse engineer a smart contract, we start with automated tools. Every reverse engineer wastes a lot of time on tasks that the tool could have done automatically. So we'll start with some popular tools.
Heimdall
Heimdall is the guardian of the rainbow bridge in Norse mythology, and in the world of Ethereum, it is a bytecode decompiler. Heimdall provides multiple levels of decompilation, so we start with the highest level, Solidity.
It should be noted that the output of Heimdall is not the "valid" Solidity that can be compiled in general. It is more like a heuristic tool to help us better understand the high-level control flow.
heimdall decompile bytecode.hex --include-sol
Simple? Not so easy, there are some null bytes mixed into the string, which may be an oversight by bigbrainchad, who wrote too much assembly code. Let's go one level deeper and look at Yul.
heimdall decompile bytecode.hex --include-yul
Have you ever had this feeling: what you thought was a fun little project suddenly spiraled out of control and went deep into the abyss of the EVM (Ethereum Virtual Machine), and your whole day was suddenly gone, like "Mr. Stark, I don't feel good"?
The output of Solidity and Yul is completely different. Some conditions are met, such as calldatasize is non-zero or the transaction caller and caller match, but these conditions do not cause a revert in Yul. Let’s go a little deeper and get into what is called a Control Flow Graph.
heimdall cfg bytecode.hex
The Control Flow Graph divides the bytecode of a smart contract into chunks, split up based on where the code takes a different path; this could be when you need to conditionally roll back a transaction based on some error, or it could be a threshold condition like tick math in Uniswap V3. The full content shown above is for dramatic effect, but there is a subtle problem hidden in it.
The jumpi instruction is a conditional jump instruction, which tells the EVM (Ethereum Virtual Machine) to “jump” to another place in the code to continue execution “if” some condition is non-zero, otherwise continue execution as if nothing happened. Typically, this would appear as a block pointing to two other blocks, one representing the "true" condition and the other representing the "false" condition, similar to the following.
If the condition is "true", then rollback the transaction, otherwise return successfully. This is very common in Solidity's require statement.
This is great and helps us draw the control flow graph, but there is a small problem here. If we look up to the previous block in the control flow graph, we see the following.
It is impossible to have a conditional jump with only one result in bytecode.
We see a block with a single result after a conditional jump. While this is technically possible in a high-level language, it should not be the case in bytecode. Every jumpi instruction should have two results, and even if one of them is completely invalid, it should be reflected in the code. So, what is going on?
Let's talk a little deeper now. Generally speaking, high-level languages like Solidity or Vyper handle all control flow logic themselves, which means that jump or jumpi instructions cannot be used directly. They can only use constructs like if, switch or function calls, even in Solidity's "inline assembly" (also known as intermediate representation, Yul).
This means that all jump and jumpi instructions are known at compile time and cannot come from unpredictable data like calldata (which is exactly what we see here). This implies that this contract is either written in assembly language or using an old version of Solidity before the introduction of the Yul optimizer. Strictly speaking, if Yul pipes are not used, you can overwrite function pointers via calldata to achieve this effect, but in the case of "via-ir" (through the intermediate representation), even function pointers are no longer direct jump targets, but are distributed via function IDs.
In either case, Heimdall can't help us further and we need to continue exploring other tools.
Tenderly
Tenderly provides some transaction tracking information and has played a key role in past vulnerability recovery and response. Let's see if we can use it to gain meaningful insights into how this program operates. We will use the following transaction hash: 0xbeef0ad930c2f0052758ce9ce579ad7f83bed913ffedb50046c3c674293d1fe5
Tenderly dashboard
There is no source code, so no debugging. However, it does include call information from a high level. While this may be useful later, for now we need the actual control flow steps to see what is going on under the hood.
Bytegraph
I hadn't heard of this app before this, but it also provides control flow graphs, which look much more interesting, and can even be manipulated via drag and drop. Let's feed the bytecode of the contract into it and see what we find.
Bytegraph
To be honest, there are other blocks here, maybe I'm not good with this tool yet, but I'm not going to find and connect all these nodes manually. Let's continue.
EVM Codes
I've given EVM dot codes a lot of praise before (and still do). It contains information about every EVM opcode (I recommend reading every one of them if you want to dig in), and also has a playground where you can test the opcodes. Let's try plugging in the bytecode and calldata and stepping through the transaction we mentioned earlier.
evm.codes
We encountered a runtime rollback error when re-executing a previous transaction. Although it is not shown here, the error message is a single word: "time". It turns out that this check reduces the block's timestamp to 16 bits and then subtracts the 5th and 6th bytes of calldata from it. As far as I know, EVM Codes does not have environment variable overwrite capabilities, so this attempt did not work.
Python3 and a dream
So far, progress has been very limited, so let's try Python. We'll break down the code block by block, dividing it by jumpdest, jumpi, jump, stop, revert, and invalid. Now let's write this to a file and start adding some stack comments in the hope of finding a breakthrough.
Comments indicating stack state
This process is very painful, but several things become obvious.
First, the stack dispatch is very messy, which can probably only be attributed to an old version of Solidity or our first encounter with code obfuscation - stack scrambling.
Stack scrambling is a means of deliberately misleading reverse engineers by changing the order in which variables are added, moved, and used on the stack. For example, consider the following two code snippets, which do exactly the same thing. The first snippet is straightforward, we push the input to mstore, which stores a value in memory, and then push the input to return, which returns a portion of memory to the caller. The second snippet does exactly the same thing, but in a less direct way. Unfortunately, Bigbrainchad's stack mangling technique is much more sophisticated. There is also a code obfuscation technique that breaks constants into other values and does arithmetic at runtime to generate the actual constants. Combine this technique with stack mangling, and you have a stack full of 30 seemingly useless values that are reassembled into the values the program actually needs as the program runs, which is really crazy.
Legendary Tool Appears: The GOAT
A few hours into this process, I received a message that @plotchy had developed a tool that creates control flow graphs through symbolic execution, which not only does the work of other control flow graph tools, but can also map out indirect jumps derived through data such as calldata. After some adjustments to the dependencies, we got the result shown in the figure below.
evm-cfg bytecode.hex -o plotchy/cfg.dot --open
Great! While the full image is for dramatic effect, it does include some nice highlighting to help us find the failing and succeeding code paths, and clearly mark which branch is truthy and which is falsy. This is not always obvious, though.
However, this also brings us to the second and third code obfuscation techniques we encountered.
First, there are a lot of unnecessary code blocks. Some jumps only happen once, and only one code block is referenced. Perhaps this is an issue with older versions of Solidity that have injected a lot of bloatware like Microsoft did, or there is some code obfuscation going on here.
Second, there are a lot of loops. Some loops only happen when specific calldata bytes have specific values, others are the result of those dynamic jumps mentioned above, of which there appear to be as many as ten. Some loops are short, while others run almost the entire contract. Maybe I'm being a bit conspiratorial, but some of these may just be honey pots. EVM-CFG Loops ...
The calldata for this transaction is as follows:
In this transaction, the MEV contract only performed two operations.
The first operation was to call the distributeETH function of a token, and the value passed was zero. Since the contract is generic, it is impossible to store each contract address, selector, and call value, which must be injected through calldata.
The second operation is a call to the original caller, bigbrainchad.eth. This time there is no calldata, but there is a call value.
With this in mind, we can break down the calldata as follows.
The first four bytes are cosmetic and ignored by the contract. Previous calls to this contract actually used all zeros 0x00000000, so some stylization was added in the process of maximizing efficiency. The next two bytes correspond to the timestamp control flow mentioned earlier; this is likely to ensure that transactions are only included within the expected block and the expected timestamp. The next byte is related to the control flow honeypot I mentioned earlier; perhaps it has a more important purpose, but the stack is too deep and I am a little tired. There are some unknown bytes after that.
The really interesting thing is the "destination address size" parameter. For the target address, value, and payload of the call, each is prefixed with a size. They use one byte for the target address and value size, and two bytes for the payload size. The first call in the series is to 0xf193..65b1, with a value of zero and a payload of 0xb8b9b54900, which is distributeETH(). The second call in the series is to 0xd215..0dfd, which is bigbrainchad.eth, with a value of 281314951393027 wei and no calldata.
Conclusion
As much as I wanted to finish this work, release some source code, and make a mock contract, this contract won me over. There is too much to unpack, too many code paths to follow manually, and too many ambiguities for automated tools to face. If you want to see the notes of my work, you can check them out via the GitHub link below, but for now, I will leave it alone.
If bigbrainchad.eth sees this article, good luck, your contract is really a work of art.
Disclaimer: The content of this article solely reflects the author's opinion and does not represent the platform in any capacity. This article is not intended to serve as a reference for making investment decisions.
You may also like
BlackRock’s Bitcoin ETF flips gold fund
SEC mulls approving Ethereum ETF options
Crypto mixer Bitcoin Fog founder receives 12.5-year prison sentence
Could XRP Reach $5, $10, or $20 in This Bull Run? Analysts Suggest a New Contender Might Lead!