tl;dr: The Trezor Model T had a vulnerability which allowed malware to silently replace a transaction’s change output with an output controlled by the attacker, stealing all funds in the account except for the transaction’s send amount. The issue has been fixed in firmware version 2.1.8. Users are advised to upgrade before making another transaction.
I responsibly disclosed the issue described below to SatoshiLabs on October 1st, 2019. They released a fix in firmware version 2.1.8 on November 6th, 2019. SatoshiLabs has been very cooperative and clear in their communication with me.
I am delighted that they paid out a generous bug bounty 😍.
The Trezor Model T is the successor of the Trezor One. Like for many Bitcoin enthusiasts, the Trezor has been my first hardware wallet, and I am grateful to SatoshiLabs for having invented and published many industry standards in use today, as well as created the very first commercial hardware wallet in the first place. <3
I performed the attack successfully by interactively modifying transactions within Electrum, running on Bitcoin Testnet.
The Trezor One was not affected.
While thinking about how to implement support for multi-signature securely on the BitBox02 hardware wallet, my colleagues Kaspar Etter and Sebastian Küng at Shift Cryptosecurity cautioned me about various potential issues. Kaspar warned me about potential issues surrounding the verification of the multisig co-signers (he will publish a post about this soon). Sebastian primed me by mentioning that issues with multisig verification could potentially affect singlesig verification as well, if the verification code does not clearly separate them. By combining their advice and looking over the multisig code of other hardware wallet vendors, I noticed an exploitable bug in the Trezor Model T.
A Bitcoin transaction is made up of inputs and outputs.
Inputs define which coins will be spent, and outputs define where you send your coins to.
Whenever you receive funds on a Bitcoin address, an unspent transaction output (aka UTXO) is created.
Let’s say you have received BTC 0.3, and BTC 0.5 in your wallet. You then have control over those two UTXOs. Your account balance is the sum of the UTXOs, in this case BTC 0.8. Hardware and software wallets usually do not display these details to you, and manage your coins in the background.
If you want to send for example BTC 0.7 to some recipient’s address, your wallet will create a transaction containing two inputs and two outputs. BTC 0.7 goes to the recipient, BTC 0.1 is change and goes back to your own wallet.
The change output is a special output, which returns the change to your wallet. Without it, your wallet would deduct BTC 0.8 (the sum of the inputs) instead of BTC 0.7 from your balance. In any case, all UTXOs used as inputs to a transaction are destroyed when the transaction is mined into the blockchain. The difference between the inputs and the outputs is the transaction fee collected by miners.
The above concepts apply the same way for multi-signature accounts. If you had a 2-of-3 multisig setup, then the transaction needs to be signed by two out of three co-signers, with the change output usually going back into the same 2-of-3 multisig account.
Note that each input is individually configured to be either a singlesig or a multisig input.
Change verification in hardware wallets
When you sign a transaction on your hardware wallet, you are asked to verify the details of the transaction. Most commonly this means the recipient address and the amount you want to send, as well as the transaction fee.
The hardware wallet asks to verify those details because the assumption is that the wallet software on your computer (or mobile phone) cannot be trusted, as it might be compromised by malware, and show fake information in its user interface.
Notably, the change output is usually not shown for user verification. The hardware wallet, at least in theory, knows it can spend it again in the future. Otherwise the change output would be shown to the user for verification. Some wallets, like the BitBox02, reject the transaction completely in this case, while others, like the Trezor, show the change output address and amount to the user for verification. Furthermore, a user cannot verify the change address easily. This is different with the recipient address and the amount, which the user can confirm with the recipient out-of-band, for example by calling them on the phone.
Exploiting a change verification bug in the Trezor Model T
The basic idea is: a malicious wallet app can try to replace the change output in your transaction with a 1-of-2 multisig change output, where one of the co-signers is you (or rather your Trezor), and the other co-signer is the attacker. By sending the change to this output, the attacker can immediately forward the funds to a wallet where the attacker has exclusive control. The attacker can do that without the cooperation of the user, as in a 1-of-2 multisig setup, only one of the two participant’s signatures is needed, not both.
Two of the relevant rules Trezor uses to determine whether to accept a change output are:
- That the change output has the same keypath prefix as all the inputs’ keypaths. In other words, that the change can be found in the same location as the inputs.
- In case of a multisig change output, that it has the same M-of-N and co-signer xpubs as all inputs.
Both checks have one idea in common: if you received funds before and you are able to spend them by using them as inputs, the same will be possible in the future when you spend the change, since the change is going back to the same account.
Subverting the first check for this attack is straight-forward: the attacker can simply use the same keypath prefix for their malicious change output.
The second check is sensible, but had a bug: the Trezor checked that a multisig change output had the same configuration as all multisig inputs, but it failed to also check that all inputs were of the same type. As a result, the change of a transaction that had both singlesig inputs and multisig inputs was accepted, as long as all the multisig inputs had the same configuration as the change output.
All the attacker needed to do then is to inject one additional 1-of-2 multisig input, and the malicious change output passed all checks and was silently accepted.
Sidenote: adding an input changes the transaction fee. The previous “expected” fee can be restored by adjusting the change value accordingly, so even very careful users will not be able to notice that anything is off.
A detailed example follows.
Let’s say a user sends a 10 BTC input: 1 BTC to the intended recipient, 8.99 BTC to a change (resulting in a fee of BTC 0.01).
A non-malicious wallet would create the following transaction:
The malicious wallet software takes the transaction and adds one 1-of-2 multisig input:
- The first key is the same as the user’s singlesig account key.
- The second key is controlled by the attacker.
The multisig input (UTXO) is prepared by the attacker on-chain and holds an arbitrary small amount, e.g. BTC 0.0001.
The wallet also replaces the 8.99 BTC change output with a multisig change output with the same multisig configuration.
The value of the change is adjusted so that the fee stays the same:
change.value = originalChange.value + multisigInput.value. The keypath is chosen so that it has the same prefix as the input keypaths.
The malicious transaction now looks like this:
The whole thing is sent to the Trezor for signing.
- The intended recipient (1 BTC, address) is confirmed.
- The fee is confirmed.
- The new change output is not shown to the user for verification, as it passes the
multifpis based on the added multisig input, which matches the multisig change output.
The Trezor returns the two signatures (one per input). The wallet can broadcast the modified transaction, resulting in shared custody of the 8.9901 BTC change.
The attacker quickly uses their key to spend the funds to a wallet only the attacker controls.
The attacker has successfully stolen 8.99 BTC (and gotten their upfront payment of BTC 0.0001 back).
Of course, the malicious wallet would not show the malicious input and change to the user, but show the transaction details as if it was created by the original wallet, so the user is unable to catch any mismatch.
The attacker is not only able to steal the change of a normal transaction, but to steal all remaining funds in the same account after the user confirmed one single-sig transaction. This is achieved by adding all UTXOs of the account to the transaction instead of the normally chosen subset.
Once the attacker compromised the user’s computer or mobile phone, the attack is easy to perform, with high expected return. A malicious/fake wallet would behave exactly the same as the original, except when performing the attack, which is invisible to the user.
Furthermore, even though the change amount would be lost to the attacker, the wallet can still show the expected balance as if nothing happened, delaying the time until the user notices the theft.
The attacker needs to pre-generate one multisig output per attacked user, with the attacked user as the co-signer. There are various ways of achieving this, for example by having the malicious wallet phone home, or by installing the malicious wallet with a pre-loaded private key so that it can create such an output itself.
The attacker can also devise heuristics to maximize their gain, for example by only executing the attack if the account holds a large amount of coins, or only attack users who are not likely to notice the theft quickly. In this case more users can be attacked before the issue becomes known and fixed.
What makes this attack interesting is that exploits leave a trace on the public blockchain with a special pattern: transactions with both singlesig and multisig inputs and one change output with the same M-of-N configuration, which is spent immediately afterwards.
Another interesting observation is that the attacker could also use a 2-of-3 input and change, so that the user never has the chance to spend the change, whereas in the 1-of-2 change, the attacker needs to forward the change immediately. This is achieved by sending the modified transaction to the attacker for signing, instead of broadcasting it directly.
I think this issue is an interesting one, as it highlights shortcomings that exist in nearly all hardware wallets, including the BitBox02, and what we could do to improve on them.
I want to mention a few possible extensions to hardware wallets which might have mitigated both this and the recently discovered keypath validation issue in the ColdCard. The following ideas come out of internal discussions and are still pretty vague, so I am eager to learn about the trade-offs and see whether it makes sense to implement those extensions.
By using security in depth, issues like these could be mitigated.
1. Register the account configuration on the device with user verification
Before the user can receive on an address of a certain account, they would first need to activate/register the account, confirming all relevant details:
- in a single-sig account, verify e.g.
"Bitcoin: Segwit, Account #2"
- in a multi-sig account, verify the type, as well as the M-of-N configuration, and all of the co-signers’ xpubs.
Ideally, the user would be able to choose a name for the account, so they can easily recognize on which account they are operating.
When confirming a receive address or a transaction, the device would show the user-chosen name and verify that all details match (all inputs and changes match the registered account configuration, etc.).
This would mitigate all sorts of accidental issues and reduce reliance on validation heuristics that might fail, especially when validating receive and change addresses.
The ColdCard does something similar to this with their multisig accounts, which was very nice to see.
2. The verify everything signing mode
This would be aimed at expert users, who are familiar with the concept of transaction inputs, outputs, script types, and changes.
As described above, the number of items in a transactions actually verified by the user is usually quite limited:
- recipient address and amount (multiple instances possible)
- transaction fee
- LockTime, if supported and set.
What is not shown for verification, but could be:
- account type: (Native) Segwit Pay-To-PubkeyHash, multisig including M-of-N and all co-signer xpubs, etc.
- number of inputs, number of outputs.
- all inputs: previous transaction hash and output index, amount, sequence, keypath, script type (must match account type)
- all change outputs: address, amount, keypath, script type (must match account type), marked as change
Expert users who enable this would be able to notice anything suspicious, like an out of place input or change, or an unusual keypath used in ransom attacks, mitigating a class of exploits that rely on missing or faulty validation.
3. More warnings about unusual transactions
The set of possible transactions is far larger than the set of transactions which a typical Bitcoin user or wallet creates in practice. Hardware wallets could do better in warning about unusual behavior, and if possible, prefer to reject the transaction over letting the user verify the edge case manually.
- Warn about unusually high fees (performed by Trezor, missing in the BitBox02 as of November, 2019).
- Warn about or reject non-standard keypaths, e.g. do not allow to use a keypath intended for single-sig inputs/changes in multi-sig inputs/changes (BIP44 purpose), or the wrong BIP44 coin. The BitBox02 enforces this.
Warn if there are more inputs than needed to cover the recipient amount and fee. To the best of my knowledge, no hardware wallet implements this.
This would prevent escalating the attacks based on malicious changes, so the attacker could not steal or ransom-attack all the user’s funds at once. Even though there are valid reasons to include unnecessary inputs, like improving privacy, it is uncommon today, so a warning could be shown.