Analysis of the Wintermute Hack: An Inside Job

Librehash
10 min readSep 26, 2022

--

There’s a lot to cover here, so this report is going to get right to the steak and potatoes here. If you’re not familiar with what happened, its suggested that you go check out some popular news sources to get yourself up to speed, then come back. For more analysis, follow on Twitter and Telegram.

Currently, the prevailing theory is that an EOA (externally owned address) that made the call on the ‘compromised’ Wintermute smart contract was itself compromised via the team’s use of a faulty online vanity address generator tool. The idea is that by recovering the private key for that EOA, the attacker was able to make calls on the Wintermute smart contract, which supposedly had admin access (this theory comes with the implicit assumption that ‘admin’ access would’ve also been sufficient in itself to execute such a compromise).

This study will refute that idea through the analysis of the smart contract itself, its interactions and transactions that are considered to be ones initiated by the hacker. In said analysis, we will see the knowledge required to execute this hack precludes the possibility that the hacker was a random, external entity that simply recovered the private key to an unsafe EOA that the team failed to revoke admin permissions for.

In other words, the relevant transactions initiated by the EOA make it clear that the hacker was likely an internal member of the Wintermute team.

In analyzing some of the reports that were published following the Wintermute hack announcement, it was claimed that the EOA in question that was supposedly compromised (0x0000000fE6A514a32aBDCDfcc076C85243De899b) was able to execute transactions against the Wintermute smart contract via the 0x178979ae method.

There’s no uploaded, verified code for the Wintermute smart contract in question that we’re examining here for some reason 0x00000000AE347930bD1E7B0F35588b92280f9e75.

https://etherscan.io/address/0x00000000AE347930bD1E7B0F35588b92280f9e75#code

This, in itself, is an issue in terms of transparency on behalf of the project. One would expect any smart contract responsible for the management of user / customer funds that’s been deployed onto a blockchain to be publicly verified to allow the general public an opportunity to examine and audit the unflattened Solidity code.

In either case, there exists the option to decompile the bytecode of the smart contract via Solidity. For this, we’re not going to use the tool provided by Etherscan. Instead, we’ll opt for the smart contract tools provided by ‘dedaub’.

https://library.dedaub.com/contracts/Ethereum/00000000ae347930bd1e7b0f35588b92280f9e75/decompiled

Since we don’t have the original source for the deployed smart contract, the best that we’ll get from any bytecode decompilation are the 4-byte hexadecimal function signatures that are roughly translated via bytecode instruction interpretation.

Switching to the ‘functions’ panel provided by dedaub allows us to quickly sift through the decompiled bytecode to the function in question that was called by the EOA that was supposedly compromised.

Below is a screenshot isolating this portion of the decompiled bytecode:

Specifically, if we visit the code, the area in question that led many to believe that the EOA had admin functionality was a line underneath this defined function that reads:

function 0x178979ae(uint256 varg0, uint256 varg1, uint256 varg2) public nonPayable { 
require(msg.data.length - 4 >= 96);
require(varg0 == address(varg0));
require(varg2 <= 0xffffffffffffffff);
require(4 + varg2 + 31 < msg.data.length);
require((?).length <= 0xffffffffffffffff, 65);
v0 = new bytes[]((?).length);
require(!((v0 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & ((?).length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31) < v0) | (v0 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & ((?).length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31) > 0xffffffffffffffff)), 65);
require(4 + varg2 + (?).length + 32 <= msg.data.length);
CALLDATACOPY(v0.data, 4 + varg2 + 32, (?).length);
MEM[32 + (v0 + (?).length)] = 0;
if (0xff & _setCommonAdmin[msg.sender]) {

Among the code excerpt above, the last one is what we’re going to scrutinize moving forward.

It has been stated that the variable, _setCommonAdmin, was set to include the EOA 0x0000000fE6A514a32aBDCDfcc076C85243De899b. Fortunately, with the dedaub smart contract tooling, we can put this to the test.

First, however, we need to pick through the decompiled code to see if any values for this variable have been placed within the smart contract storage itself (as is often the case whenever variables of this nature are created).

Lucky for us — it appears that it is .

The line of code in specific that we isolated was as follows:

mapping (uint256 => [uint256]) _setCommonAdmin

Code notes specify that the value for this variable is set at STORAGE[0x2] in the smart contract. So let's check and what result we get when we enter the allegedly compromised EOA into the 0x2 section of the smart contract's storage.

Entering the EOA value (0x0000000fE6A514a32aBDCDfcc076C85243De899b) will return a 'boolean' value. That means we're going to see either the integer '0' or '1' get returned to us after we input this value. If '0' is returned, that means false; if the result is '1', that means true. Essentially, false means that the _setCommonAdmin variable does not have the EOA in question stored as its value. Of course, '1', or true, would indicate the opposite.

Let’s see what we get below:

As we can see in the above screenshot, the boolean returned was ‘0’ — or false. That means that the EOA’s address was not defined as the value for the _setCommonAdmin variable.

Additionally, its worth noting in the code that there are variables indicated for the storage of _signers, owner_8 and owner_3. So the chances that the smart contract was designed to allow for unilateral action by a "common admin" (as the variable is named), appears to be little to none.

Digging a Little Deeper

Let’s examine one of the transactions in question that was part of the greater compromise collective.

TX ID: 0xd2ff7c138d7a4acb78ae613a56465c90703ab839f3c8289c5c0e0d90a8b4ce16

That transaction shows the transfer of 13.48M USDT from the Wintermute smart contract address to the 0x0248 smart contract (supposedly created and controlled by the Wintermute hacker).

If we take a look at the trace execution for this transaction, however, we’ll see that the transfer was a bit more complex than just sending funds from one smart contract to the other .

https://etherscan.io/tx/0xd2ff7c138d7a4acb78ae613a56465c90703ab839f3c8289c5c0e0d90a8b4ce16/advanced#internal

The first thing we’re going to do is start by breaking down the calldata sent by the EOA to the 0x0000000ea smart contract.

Below is the calldata for that transaction:

0x178979ae0000000000000000000000001111111254fb6c44bac0bed2854e76f90643097d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c42e95b6c8000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000c40d07f649000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000003b6d03400248f752802b2cfb4373cc0c3bc3964429385c2600000000000000000000000000000000000000000000000000000000

Here’s a breakdown of said calldata:

  1. 0x178979ae - This is the initial method called on the Wintermute smart contract
  2. 1111111254fb6c44bac0bed2854e76f90643097d - This is the smart contract address for the 1inch v4: Router, which was responsible for orchestrating the swap between the Wintermute smart contract and the 0x0248 smart contract.
  3. c40d07f6490 - This is the hexadecimal representation of the decimal (int) amount of Tether that was transferred from one smart contract to another (13472515450000 in uint256 format).
  4. 0x00000000000000003b6d03400248f752802b2cfb4373cc0c3bc3964429385c26 - In the transaction execution trace from ethtx.info, we can see that this value is assigned to the argument, pools attached to the unoswap function. Looking closer, we can see the 0x0248f752802b2cfb4373cc0c3bc3964429385c26contract address embedded within the value.

With that in mind, let’s take a peek at the intermediate smart contract that’s responsible for handling this orchestration / swapping of funds from the Wintermute smart contract to the 0x0248 contract.

That contract address is: 0x1111111254fb6c44bac0bed2854e76f90643097d

Below is a visualization of the interactions between EOA, Wintermute smart contract, 1inch v4: Router, and the 0x0248 smart contract address.

The functions within this contract will provide ample proof that the transactions that were executed by the EOA in question were likely piped in by the team itself.

This smart contract code adheres to the EIP-1271 standard, which provides a way to validate signatures from smart contracts (in instances where one is necessary). Examining the AggregationRouterV4 smart contract code shows us that funds cannot be moved without the caller validating that they have permission.

Within the AggregationRouterV4 smart contract, the function for verifying a signature when the caller is another smart contract is thus:

interface IERC1271 {
function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue);
}

Later in the code, we can see that it is required that calls made on the AggregationRouterV4 smart contract be validated (obtain 'success' / boolean value of '1' from the _validate function).

That code is thus:

function _validate(address signer, bytes32 orderHash, bytes calldata signature) private view {
if (ECDSA.tryRecover(orderHash, signature) != signer) {
(bool success, bytes memory result) = signer.staticcall(
abi.encodeWithSelector(IERC1271.isValidSignature.selector, orderHash, signature)
);
require(success && result.length == 32 && abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector, "LOP: bad signature");
}

Heading back to the Wintermute smart contract, we can see that there is an isValidSignature already defined within the contract itself.

The decompiled ABI from the unverified contract bytecode below:

function isValidSignature(bytes32 varg0, bytes varg1) public nonPayable { 
require(msg.data.length - 4 >= 64);
require(varg1 <= 0xffffffffffffffff);
require(4 + varg1 + 31 < msg.data.length);
require((?).length <= 0xffffffffffffffff, 65);
v0 = new bytes[]((?).length);
require(!((v0 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & ((?).length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31) < v0) | (v0 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & ((?).length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31) > 0xffffffffffffffff)), 65);
require(4 + varg1 + (?).length + 32 <= msg.data.length);
CALLDATACOPY(v0.data, 4 + varg1 + 32, (?).length);
MEM[32 + (v0 + (?).length)] = 0;
v1 = 0xdf2(v0, varg0);
return v1 & 0xffffffff00000000000000000000000000000000000000000000000000000000;
}

The smart contract also clearly defines _signers as a distinct variable, with associated values in the contract's storage.

Further down in the contract, function 0x324a (which is labeled as private), clues us in to the fact that this smart contract, as deployed, appears to require multiple

Additionally, the 4-byte keccak256 abi encoding for ‘transfer’ in Solidity is 0xa9059cbb and that is specified under function 0x394b1de1 (not called by the EOA to the Wintermute smart contract).

For reference (and for those curious where the signature is included at for the transaction), look no further than the raw TX hex for the main transaction we’ve observed here.

Likely signature has been highlighted above out of the raw TX

Further Suspicious Activity

Let’s head back to Etherscan, specifically, and take a look at the alleged compromised smart contract again — 0x00000000ae347930bd1e7b0f35588b92280f9e75

We’re going to change the scope on Etherscan so that it only shows us all transactions with this smart contract that involve the USDT (Tether) token specifically.

Doing so, gives us a really interesting result:

https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a=0x00000000ae347930bd1e7b0f35588b92280f9e75

As we can see in the screenshot above, the Wintermute smart contract (0x0000000ae) that was allegedly compromised, received two deposits from Kraken and Binance’s hot wallets. Its safe to assume that such a transfer must have been initiated from team-controlled exchange accounts.

Less than one minute after the ‘compromised’ Wintermute smart contract received over 13M USDT in funds, all of said Tether was sent out from the wallet in a manual transfer to the 0x0248 smart contract. As we saw prior, this transfer was initiated by the 0x0000000fe regular wallet address.

This sequence of events demands the following questions be asked:

  1. Is it plausible that the team initiated two withdrawals from two different exchanges (Binance and Kraken) to their smart contract less than 2 minutes from the time they were compromised?
  2. If our answer to #1 is ‘no’, then is it plausible that the attacker not only compromised the team’s smart contract but also their exchange accounts too? If so, how did the attacker learn about the existence of these exchange accounts?

Apart from the two questions posed above, we must still ask ourselves how Wintermute was compromised. And how the hacker possessed the knowledge to instrument the orchestration between smart contracts that we saw above.

These are all questions that have went entirely unanswered by the team. Instead, Evgeny has settled for defaulting to the vanity address generator tool private key compromise theory on Twitter (in a fairly unconvincing manner).

https://twitter.com/EvgenyGaevoy/status/1572329156142157825?s=20&t=hFxWy5pq5xJEpXR_5b4YKg

The rest of the tweets within the thread are fairly incoherent. For example:

https://twitter.com/EvgenyGaevoy/status/1572329161250975744?s=20&t=hFxWy5pq5xJEpXR_5b4YKg

No such evidence exists to support the idea that the team had ‘blacklisted’ the ‘router’ (what is this?). There’s also a reference to an ‘operator’ as the “contract that signs”, but smart contracts don’t sign, as we established prior. A signature could be passed to a smart contract within the context of a transaction that later gets verified via the EIP-1271 standard. But that’s the closest we’ve come to smart contracts being responsible for the signing of…anything.

The explanation given by Evgeny later the same day of the hack was rushed, hasty and sloppily published in a manner that gives the impression that he and/or the Wintermute team were relieved that they were able to potentially pull off a $160M+ heist with little to no scrutiny.

If the Wintermute team would care to refute this report, any and all responses are welcome. Hopefully the response addresses some of the questions and concerns that were raised within this analysis.

--

--

Librehash

Blockchain research, investigation, analysis and educational guides. Temporary home until app portal is repaired.