27 April 2020: Monero private key retrieval.
Summary
The 7th Ledger Security Bulletin describes a replay vulnerability in the Monero app leading to the retrieval of a private spend key. Another vulnerability leading to the same private key retrieval was found by Dušan Klinec.
The MITRE assigned organization assigned the CVE-2020-6861 and Dušan Klinec wrote a blogpost explaining the technical details of this vulnerability.
Observations and Notations
During a transaction, some computational elements are encrypted by the Nano with a key only known to the Monero application, and sent to the desktop client for later use, due to space limitations on the Nano. Let $enc(x)$ be the encryption of $x$.
Such elements are associated with an HMAC with a key that is dedicated to the current transaction. Let $henc(x)$ be a pair $(enc(x), hmac(enc(x)))$.
Two specific values 00...00h
and FF...FFh
are used to respectively identify the view key and the spend key. Those two values are named C_FAKE_SEC_VIEW_KEY
and C_FAKE_SEC_SPEND_KEY
.
In the general case, the secret view key $a$ is public and exported from the device upon request to the user. $henc(a)$ and $henc(b)$ are also known, as those are returned by the device when a transaction is started with the monero_apdu_open_tx
command.
Finally, with the notations:
- $P$: point
- $x$, $y$, $alpha$, $ss$, $c$: scalar
- $a$: scalar, private view key
- $b$: scalar, private spend key
- $l$: scalar, curve order
the subsequent two attacks rely on the following commands:
monero_apdu_sc_sub
: $henc(x), henc(y) \rightarrow henc(x-y)$monero_apdu_generate_key_derivation
: $P, henc(x) \rightarrow henc(8x.P)$monero_apdu_derive_secret_key
: $henc(P), index, \text{C_FAKE_SEC_SPEND_KEY} \rightarrow henc(\text{Keccak}(P \mathbin\Vert index)+b)$monero_apdu_mlsag_sign
: $henc(alpha), henc(x) \rightarrow ss = (alpha - c * x) \mod l$
Attack Details
Method 1
This method consists of first building a full encryption oracle and then use it to retrieve the secret spend key $b$.
Step 1: take control of zero
First, monero_apdu_sc_sub
is called with $henc(x)$ for both arguments and returns:
$\text{monero_apdu_sc_sub}(henc(a), henc(a)) = henc(0)$
A valid pair {$0$, $henc(0)$} can be injected in any command. Having the control of this zero value is important since it allows canceling parts of any other command’s intermediate computation.
Step 2: decryption oracle
Controlling the zero value, a decryption oracle can be built using the monero_apdu_mlsag_sign
command. The command can be called in the following way or any $henc(x)$ to retrieve $x$:
At this point, a decryption oracle is available.
Step 3 : Retrieving the spend key
The final step involves monero_apdu_derive_secret_key
. Let $fsk$ be the fake spend key C_FAKE_SEC_SPEND_KEY
:
$\text{monero_apdu_derive_secret_key}(henc(0), 0, fsk)$
\[= \text{Keccak}(0 \mathbin\Vert 0) + b = hmac(sk)\]$sk$ being the spend key.
So using the decryption oracle from step 2, $sk$ can be decrypted to get:
$b = sk - \text{Keccak}(0 \mathbin\Vert b0)$.
where b
is the private spend key.
Method 2
Removing monero_apdu_sc_sub
(and monero_apdu_sc_add
) from the protocol prevents the previous attack, but a more complicated one is still possible.
Step 1
Let’s assume that the private view key $a$ is already known, meaning that the user has accepted to export it.
Without the need of the device, a scalar $x$ can be found such that:
\(P = (8 x.a).G\) \(= (8x).a.G\) \(= 8x.A\)
and:
encoded_P = encode_point(P)
encoded_P = encode_scalar(decode_scalar(P))
Which means that P can be considered both as a valid input point and a valid scalar with no reduction modulo the order of the curve.
Step 2
Deriving the generated point $P$ at step 1:
$\text{monero_apdu_generate_key_derivation}(P=x.G, henc(a))$
The app computes:
- $a \leftarrow henc(a)$
- $8a.x.G = (8x).a.G = 8x.A$
which equals $P$ by definition above. The output is then $henc(P)$, hence we learn a valid set ${P, enc(P), hmac(P)}$.
Step 3
monero_apdu_derive_secret_key
can be called with the following parameter:
$\text{monero_apdu_derive_secret_key}(henc(P), 0, henc(b))$
\[sk = \text{Keccak}(P v0) + b = henc(sk)\]Step 4 : spend key extraction
Finally, monero_apdu_mlsag_sign
allows to retrieve the spend key based on the value obtained at step 2 and 3, and by using $P$ as a simple 256 bits scalar:
$\text{monero_apdu_mlsag_sign}(henc(sk), henc(P))$
\[ss = sk-c * P\]P
is considered a simple scalar in the above.
Now, recall that:
\[sk = \text{Keccak}(P \mathbin\Vert 0) + b\]and P
is known.
So replacing $sk$ in the previous computation results in:
\[ss = \text{Keccak}(P \mathbin\Vert 0) + b - c \times P\]and thus:
\(b = ss - \text{Keccak}(P \mathbin\Vert 0) + c \times P\).
So again, the spend key $b$ is retrieved.
Impact
Those vulnerabilities allow extracting a user’s Monero private spend key through a malicious Monero client. Being a flaw in the protocol between the Nano application and the Monero desktop client, it affects both Nano S and Nano X users.
Remediation
The vulnerability is fixed from Monero version 1.5.1. The specification update can be found on GitHub and the following changes were made:
- The
monero_apdu_secret_sub
has been removed from the application. - A strict state machine check has been added, to avoid using a command outside of its expected operating context.
- The computation of HMAC was changed to embed a type tag, avoiding type confusion. Some specific HMACS also embed a state machine tag.
- A better user interaction has been set up to request the user to confirm any transaction start.
Credits
We would like to thank the security researcher Dušan Klinec for the discovery of these vulnerabilities, his high quality report and also for his help.