The article describes a vulnerability in the KeepKey hardware wallet which allows triggering specific wallet functionality at times when it should not be available. Under certain limited conditions, this may be used to trick users into accepting unwanted actions on the device.


Consulting

I’m a freelance Security Consultant and currently available for new projects. If you are looking for assistance to secure your projects or organization, contact me.

Introduction

The details of this issue revolve around low level concepts and implementation details in the KeepKey firmware. The code area in question has been the source of other serious issues before, for example CVE-2019-18671, and was originally derived from the Trezor One firmware several years ago.

The limited hardware capabilities on the KeepKey wallet and its finite state machine (FSM) message handling require strong restrictions on how new logical tasks can be scheduled or interrupted to prevent errors. For user-facing tasks such as the confirmation of cryptocurrency transactions, it is also meaningful to disallow user interface actions to interrupt each other. This helps keeping the flow simple and unambiguous for the user while important actions such as transaction confirmations are performed.

In the codebase, this is implemented by the separation of communication messages into two classes: normal and tiny messages. Normal messages can trigger complex new tasks. In contrast, tiny messages are focused on essential user input and cancellations. If a message-related interaction is needed during a complex action, the global message handling is restricted to tiny messages, avoiding major interruptions and other problems. In the code, a global Boolean state variable determines if message processing is restricted or not.

The Vulnerability

The KeepKey developers made a number of changes to the USB packet handling code after adopting it from the original codebase in 2014. One of the changes was to rename the global tiny variable to msg_tiny_flag:

/* Tiny messages */
static bool msg_tiny_flag = false;

messages.c

The msg_tiny_flag was still used in an identical role for global message state handling after the rename.

In 2018, the KeepKey developers adopted U2F support (a two-factor authentication protocol) based on code from the Trezor. During this software port, they apparently missed the difference in the message handling on the KeepKey side and re-introduced a global tiny variable for use with the U2F code:

static volatile char tiny = 0;

usb.c

char usbTiny(char set) {
  char old = tiny;
  tiny = set;
  return old;
}

usb.c

usbTiny(1);

u2f.c

As a result of the double state handling in the KeepKey, the message restrictions of the U2F functionality and of all other message handling functionality are independent of each other and don’t lock in the originally intended way. This means that U2F actions and U2F dialogs can still be invoked while other functionality of the KeepKey has triggered the restricted message mode, and the same is true in the other direction as well.

Attack Scenario and Security Implications

The possibility to interrupt important dialogs and button confirmations breaks user assumptions about the basic interaction with the device. It can be leveraged to trick the user into interacting with the new dialog B that pops up while dialog A is supposed to be ongoing. Fortunately, this attack is limited to U2F <> non-U2F dialog combinations.

The most relevant attack scenario that I could find is related to a two-factor authentication (2FA) bypass with user interaction:

  • Precondition: local malware on the host computer has captured user credentials for a target website that is protected with U2F via the KeepKey, such as an important banking account that the KeepKey is registered to.
  • To start the attack, the malware first triggers some plausible user dialog on the KeepKey device that the user has to physically confirm by holding down the single button.
    • Alternatively, the malware waits until the user interacts with the wallet and requests some action on their own that has similar characteristics.
  • Confirmation dialogs for important actions on the KeepKey typically require several seconds of continuous button pressing.
  • During this time frame, the malware can launch an U2F confirmation request for the target website by proxying the U2F challenge of the attempted login.
  • Due to the message-handling bug, the U2F dialog is able to interrupt the existing dialog on the wallet while the user interacts with the harmless unrelated dialog that was started first.
  • Importantly, the U2F dialog confirmation is implemented in a way that it only requires a single split-second button press and accepts buttons that are in the pressed state when it is invoked.
    • If the U2F dialog is triggered while the KeepKey button is pressed, it is accepted immediately.
    • Alternatively, the user may accidentally accept the dialog normally when expecting the other dialog.
  • The U2F dialog briefly show up on the screen (not stealthy), but there is no way to abort or undo the U2F confirmation once accepted.
  • As an end result, the malware has gained access to the target website and bypassed the 2FA.

Note that this attack is based on a number of preconditions with regards to the host malware capabilities, known information, existing use of the KeepKey as U2F hardware token and user interaction. It also operates at the edge of what U2F is normally protecting against, since most U2F tokens do not have a screen to show what is being accepted. Still, without the discovered message handling vulnerability, this attack scenario would not be possible.

Fix

In their current patch, the KeepKey developers have not solved the problem of the double message state handling. Instead, they have chosen to apply a partial mitigation for the described U2F bypass attack by preventing U2F dialogs from getting auto-accepted immediately. This is definitely an improvement, but doesn’t fully resolve the underlying issue in my opinion.

    // wait for next commmand/ button press
    reader->cmd = 0;
    reader->seq = 255;
+    bool saw_button_up_at_least_once = false;
    while (dialog_timeout > 0 && reader->cmd == 0) {
      dialog_timeout--;
+      saw_button_up_at_least_once = saw_button_up_at_least_once || keepkey_button_up();
      usbPoll();  // may trigger new request
      // buttonUpdate();
-      if (keepkey_button_down() &&
+      if (saw_button_up_at_least_once && keepkey_button_down() &&
          (last_req_state == AUTH || last_req_state == REG)) {
        last_req_state++;
        // standard requires to remember button press for 10 seconds.

u2f.c

Formal Scoring

The mapping shown here aims to represent the impact of the U2F bypass scenario described above, but the issue is difficult to score. There may be different impact through other attack combinations.

Description CVSS 3.1 Score
VULN-22003 CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N 3.3 (Low)

Coordinated disclosure

The disclosure process with ShapeShift started out well, with good direct feedback. Unfortunately, there was a significant gap in the communication in April where I was unable to reach them via multiple communication channels. As a result, I did not have a chance to comment on their patch before the release or coordinate with them on a publication date. Still, it’s good to see that they released a firmware fix and public acknowledgment within the 90 day timeframe. I have recently heard back from them in May.

Relevant product

Product Source Known Affected Version Fixed Version Patch Publications IDs
ShapeShift KeepKey GitHub v7.2.1 v7.3.2 patch1 v7.3.2 Changelog VULN-22003

I’m not aware of other affected hardware wallets.

Note that I’ve included SatoshiLabs in the disclosure communication to ensure that there are no related vulnerabilities on the Trezor side where some the code originated from. We did not find an issue in the Trezor One product that required switching to a multi-vendor format for the coordinated disclosure.

A Note About Research Affiliation

I want to emphasize that this security research was done on my own time and initiative. In particular, the original research that led to the discovery of the issue was not sponsored by SatoshiLabs, for whom I do some paid freelance security research on the related Trezor project.

Detailed timeline

Date Information
2022-02-09 Confidential disclosure to ShapeShift, with CC to SatoshiLabs
2022-02-10 ShapeShift acknowledges receipt of the disclosure and assigns a VULN ID
2021-03-10 Technical call with ShapeShift
2021-04-26 ShapeShift releases patched firmware version v7.3.2
2021-05-05 Publication of this blog article

Bug bounty

ShapeShift paid a bug bounty for this issue.