Recovering Assets from a Hacked Account with Flashbots

Kane Wallmann
8 min readNov 13, 2021

--

Flashbots Auction is awesome. It allows advanced Ethereum users to do things that were once practically impossible (at least for those of us who don’t operate several large Ethereum mining pools). With all the attention around MEV extraction bots, I’d forgive you for thinking that’s the only thing it’s good for. But this week I used it to recover a bag of ENS tokens (worth around $13k USD at the time) from a hacked account and returned them to their rightful owner. Sound like fun? Here’s how I did it.

In an act of desperation, twitter user LittleBitPrince posted his 12 word seed phrase in a public tweet. He had recently been hacked and all of his funds and NFTs stolen. In a last ditch middle-finger attempt to stop the hacker from getting away with every last scrap, this user broadcasted his seed phrase to the world.

LittleBitPrince has since deleted the tweet*, but it went something like this:

“One of my Ethereum accounts has been hacked: 0xcaC8E5397C09d1b1503Ab45A5fc7F8428BCf6DE5. All my tokens and NFTs are gone. There are 260 ENS tokens to be claimed and you can have them if you can get them before the hacker wakes up. My seed phrase is: program deny train foot scrap marble anxiety oblige hybrid clean [xxx] [xxx]. You’re welcome.

Those aren’t his real seed words or address, but the user did redact the last two words as shown.

The hacker was ostensibly unaware that the account was eligible to receive a sizable ENS airdrop. Any attempt to send ETH to the account to pay for the claim would be promptly sniped by the hacker. And even if the ENS was somehow claimed, there was a high chance they too would have been immediately transferred to the hacker.

*I asked LittleBitPrince’s permission to post this after seeing his tweet was deleted and he said it was fine so don’t pitchfork me.

Step 1 — Recover the seed phrase

For whatever reason, the victim excluded the last two words of his seed phrase from his tweet. Before anything else could be done, I had to first work out what those two words were. If you already know how seed phrases work (or don’t care), skip over this step.

With the last two words removed, it may seem like an incredibly difficult task to crack. After all, there are over half a million words in the English language right? Not quite. Let me give a quick run down of how seed phrases work. The seed phrases used ubiquitously in the cryptocurrency world come from a Bitcoin improvement proposal, BIP-39. There is a known set of 2048 words in a specific order (it’s alphabetic for lookup efficiency). Each word represents 11 bits of data. The index of the word into the word list is converted to bits and each of these bit strings are concatenated together. For example, the word “program” is the 1375th word in the list (0-indexed). 1375 in binary is 10101011111. So this is the first 11 bits. Apply the same process to each subsequent word and you will end up with 132 bits of data. Of the 132 bits of data, the first 128 are used for entropy and the last 4 are used as a checksum.

There are another few steps to go from this 128 bits of entropy to a private key, but I won’t go into details on that as it’s not relevant to the story. If you want to know more you can read BIP-44.

All of this is to say that with the last two words removed, only 18 bits of entropy are lost. This means to brute force, we only need to try 262,144 combinations. That’s child’s play for any modern piece of hardware.

The most efficient way to recover the seed phrase would be to loop over all these combinations, calculate the checksum, derive the private key and compare the resulting Ethereum address. But because I was racing against the clock and it may have taken 30 minutes or more to encode this into a script, I decided to take a slightly less efficient route and use the tools I already at my disposal to put together the following script in a fraction of the time:

const HDWallet = require('ethereum-hdwallet')
const bip39 = require('bip39');

const fs = require('fs')

const seedStart = 'program deny train foot scrap marble anxiety oblige hybrid clean'

const words = fs.readFileSync('./words.txt').toString('utf-8').split('\r\n')

for(let x = 0; x < words.length; x++){
for(let y = 0; y < words.length; y++){
const mnemonic = seedStart + ' ' + words[x] + ' ' + words[y]
const valid = bip39.validateMnemonic(mnemonic)

if(valid){
const wallet = HDWallet.fromMnemonic(mnemonic)
const address = wallet.derive(`m/44'/60'/0'/0/0`).getAddress().toString('hex')

if(address.toLowerCase() === 'caC8E5397C09d1b1503Ab45A5fc7F8428BCf6DE5'.toLowerCase()) {
console.log(mnemonic)
process.exit()
}
}
}
console.log(x + ' of ' + words.length)
}

One seed phrase can generate many keypairs and I made a time-saving assumption that the account was derived from the first path (i.e. `m/44'/60'/0'/0/0`). Luckily, that assumption held true. Otherwise, this would have taken longer to crack.

This script takes longer than the ideal solution described above because it is sort of brute forcing the checksum bits as well. But thanks to the existing libraries, I was able to put this script together in as little as 5 minutes. Every minute counts here as there might have been black hats competing for the prize too. I knew testing 4,194,304 combinations this way would still be relatively quick (less than 5 mins) and the time saved writing this naïve brute forcer easily made up for the reduction in efficiency.

Step 2 — Recover the funds

With seed phrase and private key in hand, I set out to recover the ENS tokens. At the time I didn’t know for certain if the hacker had a bot to snipe any ETH sent to the address. But this is a pretty common setup and, based on the tweet, I assumed this was the case or the owner would not have given up the account. This means I had to atomically send ETH, claim the ENS, and transfer the ENS back to an account I controlled. Enter Flashbots Auctions.

Flashbots Auctions let users submit bundles of transactions privately to miners such that these transactions never appear in the public mempool — they can’t be analysed by front runners and sandwich bots this way — and are guaranteed to be grouped together in the order provided. These properties are immensely useful for a range of use cases (extracting MEV a prominent one) but in this situation you can hopefully see how it can be used to circumvent a hacker’s bot.

Flashbots provide a javascript library that handles most of the heavy lifting in preparing and submitting bundles. The example code they provide is an excellent starting point and exactly what I used here to get the bundle firing off as quickly as possible. In this example code, aside from filling in the configuration values, the part we need to modify is here:

const signedTransactions = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: legacyTransaction
},
{
signer: wallet,
transaction: eip1559Transaction
}
])

`signBundle` takes an array containing the transactions we want to submit (along with a signer) in the order we want them mined. I had to perform 3 transactions for this recovery to work.

  1. Fund the hacked account with enough ETH to perform the next 2 steps.
  2. Claim the ENS token airdrop.
  3. Transfer the ENS back to my own account.

The first one is simple, just the following object which should be self explanatory:

{
from: myAddress,
to: hackedAddress,
gasPrice: GAS_PRICE,
gasLimit: 21000,
chainId: CHAIN_ID,
value: ethers.utils.parseEther('0.06'),
nonce: myNonce++
}

The next one was going to be a little trickier as I wasn’t actually sure how the ENS airdropped worked. I didn’t want to waste time digging through the smart contracts to find out. Instead, I imported the hacked account into MetaMask and went through the claim process on the ENS claim website. I followed the process all the way until the last step where the MetaMask confirmation window appears and then I copied the input data from there:

I used this data to prepare the second transaction object (data truncated):

{
from: hackedAddress,
to: '0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72',
gasPrice: GAS_PRICE,
gasLimit: 150897,
chainId: CHAIN_ID,
nonce: hackedNonce++,
data: '0x761229030000000000000000...'
}

0xC1836… is the ENS token and airdrop claim contract address. The last transaction was relatively easy, I used a similar process as above but for a token transfer.

{
from: hackedAddress,
to: '0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72',
gasPrice: GAS_PRICE,
gasLimit: 100000,
chainId: CHAIN_ID,
nonce: hackedNonce++,
data: '0xa9059cbb0000000000000000...'
}

For the gas price, I picked a value higher than the current market rate. I just needed enough that it was worthwhile for a miner to accept the bundle but I didn’t need to compete with other bots for the same MEV opportunity. A large bribe would be unnecessary given these conditions.

With the bundle prepared, the next step is to simulate it. This lets you know if any of your transactions will fail and is a good sanity check that everything you have prepared up until this point is correct. The example code provided by Flashbots does that here:

const simulation = await flashbotsProvider.simulate(signedTransactions, targetBlock)
// Using TypeScript discrimination
if ('error' in simulation) {
console.warn(`Simulation Error: ${simulation.error.message}`)
process.exit(1)
} else {
console.log(`Simulation Success: ${JSON.stringify(simulation, null, 2)}`)
}

If the simulation fails, it will give you useful information as to why and exit. In my case, everything was good to go and so I fired off the bundle and crossed my fingers. This is the bit where your palms get sweaty as you intently watch your terminal output for a positive result. Within a few blocks, my bundle was included and I had 260 ENS in my account. Success!

All said, it took just under 30 minutes from discovering the tweet to having the ENS in my account.

Step 3 — Return the funds

Now that I had the funds safely in my own account, it was time to get in contact with LittleBitPrince with the good news. There were some oddities about his story and some other information I found that I won’t go into details on, but in short, I wanted to be absolutely certain these ENS tokens did belong to him before returning them. He might have gotten those seed words from somewhere else and hoped someone would come along and return them to him instead of the real owner. You can never be too careful in moments like this.

I reached out to him via a twitter DM. I also located some addresses that this account had interacted with long before the hack took place. Importantly, one contained enough assets that if the hacker was in control of this account they would have certainly drained it by now. I asked LittleBitPrince to sign a message that I provided to him with this account. After everything checked out, I transferred the ENS tokens to him and he was decidedly jubilant as a result.

Thanks for reading. Follow me on twitter if you want.

--

--