New: Wallet recovery made easy with Ledger Recover, provided by Coincover

Get started

Blog posts, Donjon | 04/25/2023

Funds of Every Wallet Created With The Trust Wallet Browser Extension Could Have Been Stolen Without Any User Interaction


Things to know:

– Seed generation of Trust Wallet was flawed, the total entropy was only 32 bits. We have created a file containing all possible seeds.

– Fortunately, the Ledger Donjon discovered the vulnerability very quickly and likely avoided one of the biggest hack in the crypto ecosystem.

On November 14th 2022, Trust Wallet, a widely used software wallet, announced the release of its browser extension. It allows access to digital assets on several blockchains directly from the browser, and is a long-awaited addition to the existing iOS and Android apps.

The Ledger Donjon has recently discovered a critical vulnerability in this browser extension, allowing an attacker to steal all the assets of any wallet created with this extension, without any user interaction. By knowing the address of an account, it is possible to immediately compute its private key, then access all its funds. Below are details of the vulnerability, how the Ledger Donjon discovered it, its impact over time, an estimation of the vulnerable assets, and how Trust Wallet responded to fix it. But let’s start with recalling the basics.

How wallets are created

Entropy generation is tricky. As scientists, we like reproducibility and being able to explain phenomena with cause-and-effect principles. So, generally speaking it’s hard to generate randomness. Moreover, it’s tough to demonstrate that random numbers are correct, and a bad but not terminally flawed random number generator can easily fool the observer. For good randomness, we need uniform distribution of bits and bytes (and even all chunks size), and unpredictability. For an observer of a sequence, it must be impossible to have any information on the next part of the sequence to be generated.

As these properties are incredibly difficult to achieve, the cryptocurrency space tries to avoid relying on randomness as much as possible – but we still need it at one stage: when we create a new wallet.

You’re probably already familiar with your mnemonic, the 12 to 24 english words that allow you to backup your wallet (if not, you can check Ledger Academy article on this very topic).

This mnemonic encodes 16 to 32 bytes of entropy, according to the BIP 39 standard – the quality of this entropy is critical, since it’ll be the seed of all keys used by your wallet on all chains, following a deterministic derivation process defined by the BIP 32 and BIP 44 standards.

This Hierarchical Deterministic scheme is pretty much ubiquitous today, considering how easy it makes it for users to create a backup of an infinity of keys and its portability (despite BIP 39 being “unanimously discouraged for implementation”). Signer roaming is a powerful feature – when your favorite wallet fails or disappoints, you can just take your mnemonic with you (or even better, your Ledger device), switch to another one, keep your financial freedom and limit any impact of its downtime.

But again, it requires a flawless entropy source.

Overview of the Vulnerability

Trust Wallet relies on Trust Wallet Core, a cross-platform library that implements low-level cryptographic wallet functionality for many blockchains. It was mobile-focused, but it also targets Wasm since April 2022 (see #2132).

While most of the Trust Wallet Core is portable, a few modules and functions are very specific to  a target. This is notably the case for the secure random generation part, used to create cryptographic material such as private keys, and mnemonic for HD wallets. Each implementation leverages the pseudorandom number generator (PRNG) offered by the operating system:

  • For iOS, SecRandomCopyBytes is used.
  • For Android, the entropy is provided by an instance of java.security.SecureRandom.

This is usually a good practice, as such primitives are supposed to be safe.

Wasm backend

There is a difference with the Wasm target. This module can run on several environments, like any browser supporting Wasm, or Node.js. These platforms do not provide a common strong PRNG, and one cannot get access to the “classic” system interfaces from these environments. For example, a Wasm module running in Chrome for Linux could not directly read /dev/urandom.

To tackle this, a dedicated so-called “secure random generator” has been implemented in #2240. It is based on a PR made in emscripten (see PR #12240 in emscripten) written precisely to avoid reading /dev/urandom.

According to the author:

What we do here is simple, we wrap std::random_device with std::mt19937 and return a random uint32 value, inspired by emscripten-core/emscripten#12240.

There is an important problem here, which leads to a critical vulnerability for wallet-core for Wasm and for any product relying on it: the PRNG used is a Mersenne Twister, and it should not be used for cryptographic purposes. Moreover, the specialized version mt19937 takes a single 32-bit value as input seed.

What are the consequences here? The custom Random module for Wasm implements two functions: random32 which outputs a 32-bit random value, and random_buffer which fills a buffer of arbitrary size with random data. In the Wallet Core project, these functions are exclusively used by trezor-crypto, the cryptographic library developed by Trezor to ensure secure cryptography on their hardware wallets.

Now, let’s see how are generated HD wallets:

  • Entrypoint is HDWallet. It takes a strength, and a passphrase to protect it later:
C++
HDWallet::HDWallet(int strength, const std::string& passphrase)
    : passphrase(passphrase) {
    char buf[MnemonicBufLength];
    const char* mnemonic_chars = mnemonic_generate(strength, buf, MnemonicBufLength);
    if (mnemonic_chars == nullptr) {
        throw std::invalid_argument("Invalid strength");
    }
    mnemonic = mnemonic_chars;
    TW::memzero(buf, MnemonicBufLength);
    updateSeedAndEntropy();
}

https://github.com/trustwallet/wallet-core/blob/3.1.0/src/HDWallet.cpp#L45

This functions then calls mnemonic_generate to create a BIP-39 mnemonic:

C++
const char *mnemonic_generate(int strength, char *buf, int buflen) {
  if (strength % 32 || strength < 128 || strength > 256) {
    return 0;
  }
  uint8_t data[32] = {0};
  random_buffer(data, 32);
  const char *r = mnemonic_from_data(data, strength / 8, buf, buflen);
  memzero(data, sizeof(data));
  return r;
}

https://github.com/trustwallet/wallet-core/blob/3.1.0/trezor-crypto/crypto/bip39.c#L55

mnemonic_generate calls random_buffer, which outputs a random buffer filled using a Mersenne twister PRNG, whose instance has just been seeded:

C++
void random_buffer(uint8_t* buf, size_t len) {
    std::mt19937 rng(std::random_device{}());
    std::generate_n(buf, len, [&rng]() -> uint8_t { return rng() & 0x000000ff; });
    return;
}

https://github.com/trustwallet/wallet-core/blob/3.1.0/wasm/src/Random.cpp#L19

As the seed is just 32 bits, the Wasm version of wallet-core allows to create only 2^32 (~4 billion) possible mnemonics. All these mnemonics can be generated in a couple of hours in a single computer.

From there, an attacker is able to:

  • Compute all the seeds, private keys, then addresses of every cryptocurrency handled by Trust Wallet.
  • Scan the related blockchains to extract all the used addresses.
  • Compute the intersection to get all the addresses of wallets created by Trust Wallet for Wasm, and steal their funds.

Running such attack takes much more than a couple of hours, but is doable with a few GPUs in less than a day (see medium.com/@johncantrell97/how-i-checked-over-1-trillion-mnemonics-in-30-hours-to-win-a-bitcoin for a cost estimation. Attack is 256 times easier here).

Application to Trust Wallet browser extension

The Trust Wallet browser extension is an extension for Chromium-based browsers. It is clearly a MetaMask competitor, and is branded as a “secure multi-chain crypto wallet and gateway to thousands of Web3 decentralized applications (dApps).”

Extension is closed-source, but its code can be easily analyzed. It relies on the vulnerable Wasm implementation of Trust Wallet Core.

When a wallet is created, extension creates a 12-word mnemonic from a random 128-bit seed. Mnemonic is generated this way:

C++
generateMnemonicWallet(e){
return e||ct.error("Trying to generate wallet without password"),
this.walletCore.HDWallet.create(Js.DEFAULT_STRENGTH,e)
}

HDWallet.create is the auto-generated Wasm wrapper for the HDWallet constructor described above. That means the vulnerable random_buffer function is used, so mnemonics can be retrieved from the user address with a brute force attack.

This extension handles the following assets: AVAX, BNB, ETH, MATIC, SOL and TWT.

  • Addresses are identical for ETH, BNB, MATIC, AVAX and TWT. These are standard Ethereum addresses, sharing the same derivation path (m/44’/60’/0’/0/0).
  • Solana uses a different derivation path: m/44’/501’/0’/0’.

To drain the funds of all the Trust Wallet extension users, attacker can:

  • Compute and store every possible mnemonic, then Ethereum private key and Ethereum address, that can be generated by this extension.
  • Gather all the used Ethereum addresses created since the first release of the Trust Wallet browser extension, and store them locally.
  • Perform a lookup in the address database.
  • Empty wallet with the private key, if the address has been used.

These steps can be reproduced for every chain. We detail now how the Ledger Donjon implemented this attack on Ethereum and Binance Smart Chain, without, of course, draining the wallets.

Attacking Trust Wallet

The vulnerability allows an attacker to compute mnemonic from any address of a wallet created by the browser extension. For that, one needs to compute a mapping between the possible mnemonics and the resulting address.

Generating all the addresses the Trust Wallet extension can create

Based on the vulnerability in the PRNG previously explained, it is possible to enumerate all the addresses (and the related private keys) the Trust Wallet extension can create. My idea was to store every possible address in a big table. Then, from a list of addresses extracted from the Ethereum blockchain, one can check if some addresses are present in this table. If so, its private key can be computed.

Derivation from entropy to mnemonic then to Ethereum address uses the standard derivation mechanism BIP-32BIP-39, and the BIP-44 account hierarchy.

First difficulty was to enumerate all these addresses. Transformation from PRNG seed to address requires the following steps:

  • Entropy generation: initialize the Mersenne Twister with the seed, and  call it 16 times to gather the initial entropy.
  • Entropy to mnemonic: one SHA-256 to compute the final checksum embedded in the last word.
  • Mnemonic to seed: mnemonic are converted into a 512-bit seed using PKBDF2-HMAC-SHA512 with 2048 iterations. There are 2 SHA-512 computations per iteration, so total cost is 4096 SHA-512 computations.
  • Seed to BIP-32 master key: 1 HMAC SHA-512 costing 2 SHA-512 computations.
  • Master key to Ethereum private key: master key is derived on m/44’/60’/0’/0/0. This requires 3 hardened child private key derivations and 2 normal child key derivations.
    • Each hardened child private key derivation requires one HMAC SHA-512 (2 SHA-512) calculation and one addition on secp256k1.
    • Each normal child private key derivation requires a child private key derivation, and a scalar multiplication on secp256k1 to convert the private key provided in input to a public key.
  • Ethereum private key to address: this last step requires a private to public key conversion, so another scalar multiplication, and one Keccak-256 hash.

Total cost for all these steps is then:

  • Initialization and 16 calls to Mersenne Twister
  • 1 SHA-256
  • 4108 SHA-512
  • 5 point additions
  • 2 scalar multiplications on secp256k1

The most expensive steps are the SHA-512 computations and the scalar multiplications. To make it short, the overall process to transform the PRNG seed to an Ethereum address is slow. Running such computation on a single CPU would take months, and probably several weeks on the CPUs that were available in the Donjon. So, we implemented it using OpenCL (based on BIP39 Solver GPU) and ran it on 2 NVIDIA GeForce GTX 1080 Ti GPUs.

The output of this tool is a big file containing all the Ethereum addresses that the extension can generate. As there are 2^32 possible seeds, and each address is 20-bytes long, this table takes 80 Gb.

From there, table lookups are slow: to match an address, it would require iterating through all this big table.

To speed up these lookups, we split the table into 256 smaller tables, according to the first byte of the Ethereum address. Each table contains pairs of PRNG seeds, and their resulting Ethereum address.

Finally, to be able to perform fast lookups in each table, we sorted them according to the Ethereum address. It is now possible to do binary searches on these tables: lookups on these sorted tables are very cheap.

To save some disk space, we stored PRNG seed and only the first 8 bytes of each Ethereum address. The last 12 bytes are not necessary, as collisions are negligible in my use-case. Each entry takes then 12 bytes. Whole tables then take 48 Gb.

Here are the timings for each step:

Using these tables, it is possible to immediately retrieve the mnemonics used to generate an address. To assess the impact of the vulnerability, Binance asked me the mnemonic of 3 test addresses they provided. Here is the result:

Python
$ time ./crack_address.py --table-dir /mnt/traces/tw/ 0xdf6D9547e163D5E7eafBe2FeB24Bfa12A4C913C0 0xE1E0580cb5eA0c0FD034FF2cdfc872ce4493676C 0x02b2Ae981b138F066344774A2AD75225A046c377

Address: 0xdf6D9547e163D5E7eafBe2FeB24Bfa12A4C913C0
RNG seed: 0xc92b023d
Mnemonic: stick bench smart report motor arrive enter river scale manage viable squeeze
Private key: 84117778e20a98f488234c89b6386b4175bb66ccf2fc77d14203f72a5a22686a

Address: 0xE1E0580cb5eA0c0FD034FF2cdfc872ce4493676C
RNG seed: 0x1ae841e9
Mnemonic: install issue guilt rescue slight barrel gasp vendor glimpse avocado cart frequent
Private key: 3b0df65fe03a210661101172ca2a3e38fd75fdcc580248a2f28587c2ece0bba8

Address: 0x02b2Ae981b138F066344774A2AD75225A046c377
RNG seed: 0x2ed51a57
Mnemonic: ginger pave slight million pencil flame monkey detect power protect soft derive
Private key: 95a3be70efe04a7c06df98d68c4ccd7d453b0f39f6a097b24f2f2ac48d02494e

real    0m0,207s
user    0m0,179s
sys     0m0,028s

Retrieving the 3 mnemonics and private keys took a few hundred milliseconds. According to our tests, the process is actually fast enough to process in real time all the transactions on the Ethereum blockchain and to break all the vulnerable addresses as soon as they are used. By caching addresses already tested, the same applies for other blockchains such as BSC. In this attack scenario, one could monitor transactions when they reach the mempool, and compute sender or recipient private keys in real time.

Listing all the used Ethereum addresses

What we would like is to estimate the real number of vulnerable wallets, and their balance. This sounds easy, as all the transactions are public, hence all the addresses are available on the blockchain. However, there is no way to directly retrieve the list of the used addresses.

We implemented a method that iterates through every block of the Ethereum blockchain. We extracted the sender and recipient addresses of all the transactions, and the address parameters of every call to ERC-20 contracts.

Note that with this method, only used wallets can be detected: some vulnerable wallets that did not receive assets have never interacted with the blockchain.

We scanned the Ethereum blockchain between blocks 14820000 and 16096000. Block 14820000 was created on May 21 2022, hence just before the pull request that added the vulnerable code in Trust Wallet Core. 16096000 was the latest block when I wrote this post.

Public nodes seem to have a rate limit, so I queried several public nodes in parallel to gather a total of 147,910,120 addresses during several dozen hours. After duplicates are removed, we obtain a list of 32,613,317 unique addresses.

The same method has been used for Binance Smart Chain. Public BSC nodes have been scanned.

Estimating the number of vulnerable accounts

Finally, a tool to test if an address has been created by the Trust Wallet extension has been written. It makes a lookup in the generated tables, gets the PRNG seed, and from there compute the mnemonic, the Ethereum private key and the associated address.

Computation is very fast. Candidate addresses were sorted beforehand to minimize I/O and to perform a nested binary search. Lookups on the 32 million of addresses take a few minutes using a simple Python script.

Here is an example with an address taken from a public tweet replying to the announcement of the Trust Wallet extension. I took this one as an example as this address has never been used, so user funds are not at risk.

Python
$ time python3 pwn_address.py 0xE17282BBbD0f32cA98683933382633ab6d9778B0
RNG seed: 0x8ec170a8
Mnemonic: sorry slush already pass garden decade grid drip machine cradle call put
Private key: b9168c1fab55f840a5e04d96f7a38cb8803b67ec7c41f720fb6c487c08f3d536

real   0m0,194s
user   0m0,170s
sys     0m0,024s

Tool has been run on the dataset of 1,873,720 detailed above. Testing all the addresses and computing the private keys of vulnerable accounts took 4 min 22s, so it is very cheap.

With this list of vulnerable private keys, it’s possible to list all the corresponding addresses, their balances and obviously to drain them… During our investigations, around $30 millions were at risk at some point, but we didn’t monitor all chains and tokens overtime.

Remediation

2022, November the 17th

Vulnerability has been reported to Binance using their bug bounty program on 2022, November the 17th.

To confirm the vulnerability, Binance sent us 3 addresses and asked them to provide mnemonics:

Can you please try to run your tool and provide mnemonics for these 3 addresses?
Wallet 1 – 0xdf6D9547e163D5E7eafBe2FeB24Bfa12A4C913C0
Wallet 2 – 0xE1E0580cb5eA0c0FD034FF2cdfc872ce4493676C
Wallet 3 – 0x02b2Ae981b138F066344774A2AD75225A046c377
Thanks!
Best regards.

Once all the possible addresses have been precomputed, retrieving the mnemonic from an address is as simple as a lookup in a 4-billion entries table. The three mnemonics have been retrieved in 0.2s:

Python
$ time ./pwn_address.py 0xdf6D9547e163D5E7eafBe2FeB24Bfa12A4C913C0 0xE1E0580cb5eA0c0FD034FF2cdfc872ce4493676C 0x02b2Ae981b138F066344774A2AD75225A046c377
Address: 0xdf6D9547e163D5E7eafBe2FeB24Bfa12A4C913C0
RNG seed: 0xc92b023d
Mnemonic: stick bench smart report motor arrive enter river scale manage viable squeeze
Private key: 84117778e20a98f488234c89b6386b4175bb66ccf2fc77d14203f72a5a22686a

Address: 0xE1E0580cb5eA0c0FD034FF2cdfc872ce4493676C
RNG seed: 0x1ae841e9
Mnemonic: install issue guilt rescue slight barrel gasp vendor glimpse avocado cart frequent
Private key: 3b0df65fe03a210661101172ca2a3e38fd75fdcc580248a2f28587c2ece0bba8

Address: 0x02b2Ae981b138F066344774A2AD75225A046c377
RNG seed: 0x2ed51a57
Mnemonic: ginger pave slight million pencil flame monkey detect power protect soft derive
Private key: 95a3be70efe04a7c06df98d68c4ccd7d453b0f39f6a097b24f2f2ac48d02494e

real    0m0,229s
user    0m0,194s
sys     0m0,021s

2022, November the 21st

A few days after, on November the 21st, Trustwallet team publicly committed on Github the fix avoiding the generation of new flawed seeds. We were quite worried someone would notice it and exploit the vulnerability.

2022, November

Trustwallet team updated the app to warn their users, prevent them from generating new flawed seeds and removed the receiving flows.

From there, we monitored the situation and the funds at risk. Only a few days after the release of this vulnerable wallets, around $30 millions were at risk.

2023, March 

Trustwallet team granted us the highest bounty they offer : $100k

2023, April the 22nd

After months waiting for users to migrate their funds, Trustwallet team disclosed the vulnerability and wrote a postmortem. As of now, there are still  wallets with remaining funds that can be stolen (~$100k). Trust Wallet promised the reimbursement of stolen funds.

Conclusion

This vulnerability illustrates the worst case scenario of a crypto bug – compromised accounts forever.

Creating good randomness is a daunting task – Ledger devices rely on dedicated silicon logic in our certified smartcard chips that have been the gold standard of secure industries for the past 40 years to guarantee high quality randomness and tamper resistance.

Given the complexity of contacting the owners of those accounts and the possibility to use those compromised accounts on all kinds of different software and hardware wallets, TrustWallet did a pretty fine job reducing the risk for their users.

In the (very) (near) future it’s likely that bots will fight to be the first to steal funds deposited to those addresses, similar to what happened with brain wallets in the past.

Special thanks to Jean-Baptiste Bédrune for saving the world. Only a few days after the release of the Trust Wallet extension, almost $30 millions were at risk. A nightmare scenario could have occurred if an attacker found the vulnerability after a couple of months.

During our investigations, we also noticed that a few addresses were vulnerable while they had been generated a long time before the Trust Wallet release. That probably means this vulnerability exists in some other wallet implementations which is concerning…

Stay in touch

Announcements can be found in our blog. Press contact:
[email protected]

Subscribe to our
newsletter

New coins supported, blog updates and exclusive offers directly in your inbox


Your email address will only be used to send you our newsletter, as well as updates and offers. You can unsubscribe at any time using the link included in the newsletter.

Learn more about how we manage your data and your rights.