Loopring’s Frontend Vulnerability, Explained

StarkWare
StarkWare
Published in
5 min readMay 7, 2020
Photo by iMattSmart on Unsplash

TL;DR

We’ve found a critical vulnerability in Loopring’s frontend wallet, which allows anyone to gain control of every account created using it. The vulnerability arises because Loopring’s frontend wallet uses a 32-bit integer to derive each user’s private key. This would allow an attacker to reproduce all such private keys. We’ve demonstrated the vulnerability by recovering private keys for over a dozen accounts. This problem was communicated to Loopring, and it is acting quickly to address it.

A Tale of Two Keys

In Loopring a user has two types of keys:

  • Ethereum key
  • (Loopring) Account key

The Account key is needed for its SNARK-friendliness properties, as it is much (much) easier to prove a signature signed by the Account key than by the Ethereum key. The Account key, being SNARK-friendly, differs from the Ethereum key in several parameters (e.g. the elliptic curve). Because of that, it is not easily supported by other wallets.

When a user joins the system, she most likely has an Ethereum key, but not yet an Account key. So even though the user chooses MetaMask as her wallet when connecting to Loopring, it will only serve her Ethereum txs (for interactions with the contract, including deposits and withdrawals). But to submit orders, the user will have to use her Account key.

Loopring chose to run all the Account-key-related operations in the browser. Therefore, as part of the registration process, the user generates her Account key, and a map between the two key types is saved in the contract. From that moment on, every time the user needs to submit orders, she will use her Account key and not her Ethereum key.

The Account Key Derivation Process

To generate an Account key pair (public, private¹), the user is required to enter a password. This password, together with the user’s Ethereum address, is used to derive her Account key:

wallet.js

This in and of itself is bad practice: since the Ethereum addresses, as well as the Account Public Keys, are readable from the contract, a comprehensive search of passwords is feasible. And as history has taught us over and over again (see brain wallets²), most users are not good at choosing a strong password.

It doesn’t end here. Let’s inspect further:

Exchange.js
EdDSA.js

The Ethereum address and the password are used to generate the seed. Then, the seed is used to derive randomNumber. This randomNumber is later used to get entropy that will give the user the desired private key.

But how is randomNumber derived from the seed?

hashCode

Let’s take a look at hashcode:

bitstream.js

So in our case hashCode will go over the seed char by char, and will compute h from it.

But Math.imul is doing 32-bit integer multiplication³! This means the result is a 32-bit integer. The other operation we see here (for addition) is | 0. But bitwise operations in JavaScript results in 32-bits integers⁴ (all operands are transformed into 32-bit integers).

The result: hashCode returns a 32-bit integer! This means that no matter what the seed is, randomNumber will be a 32-bit integer.

The entire entropy to generate private keys, the entire keyspace, is 32-bits.

Implications

Regardless of the user’s password, his Account private key will be derived from a keyspace of 32 bits. We can compute all the possible Account keys in the system (as long as they were created using their frontend).

Therefore, we can find the private keys of all the users registered through the API. In fact, with just a short run, we have indeed managed to reproduce the private keys for over a dozen accounts, to demonstrate this vulnerability.

By controlling those keys, attackers can submit orders on behalf of users. This can enable them to actually steal funds. One way to do it is to submit enough orders to an illiquid pair with a price that is far from the current price. For example, if attackers want to steal DAI from accounts, they can send many orders offering to “buy 1 ETH for 10000 DAI”. At the same time, the attackers will place orders of “sell 1 ETH for 10000 DAI”. If the pair is not liquid enough eventually those orders will be executed (once the order book was consumed) and the attackers’ account will gain those DAI for a small amount of ETH.

Conclusions

Currently, secure Ethereum wallets come with a limited variety of cryptographic standards, and are not flexible enough for any sort of customization. A system that cannot use those standards must build its alternative wallet capability from scratch. This includes conducting security-related operations in the browser (which is inadvisable); using passwords to generate keys (which is considered bad practice); and introducing critical bugs in the process, like the hashcode bug described above (which is very bad).

Hopefully, in the near future Ethereum wallets will become more flexible and will support a broader set of cryptographic primitives. This, once again, will disconnect the process of creating secure wallets from that of building dApps, thereby preventing such bugs in the future.

Acknowledgement: Thanks to Louis Guthmann (@GuthL), a product manager and researcher at StarkWare, for his help in exploiting the vulnerability.

~Avihu Levy (@avihu28), Head of Product, StarkWare

— —

¹ The code makes use of ‘secret’ instead of the commonly used ‘private’.
² https://bitcoin.stackexchange.com/questions/8449/how-safe-is-a-brain-wallet
³ https://stackoverflow.com/questions/21052816/why-would-i-use-math-imul
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators

--

--