As part of the background research on the U2F HID handshake information leak, I discovered through manual code analysis that the Yubico libu2f-host host-side C library contained an out of bounds write vulnerability which could be triggered by a malicious U2F client device.

Since the libu2f-host library is the basis for PAM over U2F login functionality that can be added to Unix-like operating systems such as Linux, BSD and macOS, this issue had the potential to affect a number of specially protected systems. MITRE assigned CVE-2018-20340 for this issue.

Note: part two describes CVE-2019-9578, a closely related second vulnerability.

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.

The vulnerability

As described in depth in the U2F HID issue article, the U2F HID communication with an U2F client device is always initiated by the host side. The U2F client - usually a physical token connected via USB - then responds with a specific reply message to the initial request and the main U2F communication including the cryptographic challenges can be done over the arranged channel id.

The vulnerability is present due to faulty handling of the first reply message in the libu2f-host library.

The U2F handshake is initiated via the U2FHID_INIT message in the init_device function:

unsigned char resp[1024];
// [...]
size_t resplen = sizeof (resp);
dev->cid = CID_BROADCAST;

if (u2fh_sendrecv
    (devs, dev->id, U2FHID_INIT, nonce, sizeof (nonce), resp,
     &resplen) == U2FH_OK)
  {
    // note: client response is handled here

devs.c The actual communication happens in the u2fh_sendrecv function which is built to generically handle the send- and receive logic. Note that u2fh_sendrecv conveniently collects and returns the relevant bytes of the reply message(s) to the calling function. The generic nature of this utility function is part of the general problem, as I will show later.

In the beginning of init_device, the response buffer resp with the size 1024 bytes is allocated and resplen is set to value 1024, as shown in the previous code snippet.
resplen is then handed to u2fh_sendrecv where it is used under the recvlen name as an upper limit on the number of U2F “payload” bytes that may be accepted during the receive stage:

unsigned int maxlen = *recvlen;

u2fmisc.c

However, resplen is also used as a return parameter to specify how many U2F message payload bytes were actually written to the resp buffer:

*recvlen = datalen;

u2fmisc.c Since u2fh_sendrecv is a generic send/receive function, it is not designed to limit the type and length of the received message.
For the relevant U2F client response, the U2F specification defines a particular response message with a fixed number of 17 bytes of U2F payload. This data is normally contained in a single HID response packet.

The FIDO U2F standard mandates compatibility with larger responses:

An U2FHID host shall accept a response size that is longer than the anticipated size to allow for future extensions of the protocol, yet maintaining backwards compatibility.

However, u2fh_sendrecv imposes no meaningful checks at all and happily assembles multi-packet messages (via so-called continuation packets) which allows attackers to provide up to 1024 bytes of U2F payload data instead of the regular 17 bytes while still returning U2FH_OK and continuing in the program flow.

The actual vulnerability is triggered in init_device once the resp buffer of attacker-controlled size and with attacker-controlled contents is memcpy‘ed to the initresp struct that is at most 20 bytes in size, which leads to the out of bounds write:

if (u2fh_sendrecv
    (devs, dev->id, U2FHID_INIT, nonce, sizeof (nonce), resp,
     &resplen) == U2FH_OK)
  {
    U2FHID_INIT_RESP initresp;
    memcpy (&initresp, resp, resplen);
    dev->cid = initresp.cid;
    dev->versionInterface = initresp.versionInterface;
    dev->versionMajor = initresp.versionMajor;
    dev->versionMinor = initresp.versionMinor;
    dev->capFlags = initresp.capFlags;
  }

devs.c

Up to 1004 bytes of attacker-controlled data are written to the stack.

Attack scenario and security implications

The attack applies to host systems which use of libu2f-host through

  • an authentication module like pam_u2f.so (privileged execution)
  • other userspace applications that depend on libu2f-host such as yubikey-manager
  • the command-line tool bundled with libu2f-host

Once the library initiates a handshake with an U2F token, e.g. during an authentication attempt, the token can trigger the buffer overflow on the stack and potentially execute code with the relevant privileges or potentially skip the login authentication. Any malicious USB device which enumerates as a recognized U2F token class can perform this attack.

CVSS score

Yubico has assessed this with a CVSSv3 score of 6.3 via AV:P/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H.

In my opinion, the CVSS Scope (S) metric should be Changed if the attack allows to move past authentication barriers and therefore past the vulnerable component, but I find their overall classification reasonable.

Relation to other attacks

This vulnerability shares a number of similarities with the CVE-2018-14779 buffer overflow in Yubico host-side code related to smart cards which was discovered mid-2018 by Eric Sesterhenn of X41 D-Sec. Eric’s talk “In Soviet Russia Smart Card Hacks You” at 35C3 describes both the general patterns which are relevant for this attack vector as well as the specific impact that buffer overflows in privileged authentication code can have.

I find it plausible that drivers and low-level code for local, “safe” systems such as smartcards have less thoroughly analyzed or hardened interfaces since the involved developers are mainly focused on other development aspects or different attack scenarios. This is clearly an interesting area for further research.

Mitigations

I was surprised to find that the libu2f-host build system did not enable -fstack-protector-all or other protections such as -D_FORTIFY_SOURCE=2 when compiled from source:

RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 136 Symbols No 0 8 u2f-host

Particularly the missing stack canary is a serious problem in the context of this vulnerability as it reliably detects the attack and limits the impact to denial of service.

During discussion with Yubico, it turned out that most of the hardening flags are usually provided by the build environment. This meant that most binary packages were in fact shipped with mitigations in place by major distributions like Debian, Ubuntu and others since their build environment includes the hardening flags:

RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 2 4 u2f-host

However, it is possible that the library is used productively in other places and systems without these mitigations, e.g. as a self-compiled binary or as a code dependency. In my opinion, these hardening flags should be included and active by default.

Fingerprinting

During research, I found that the host library always uses the identical “nonce” when initiating contact with the U2F client:

/* FIXME: use something slightly more random as nonce */
unsigned char nonce[] = { 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1 };

devs.c

For the malicious client, this provides a simple and accurate way to detect that libu2f-host is used on the host side. Through this detection, a malicious U2F token can behave functionally correct when used against unaffected host-side U2F code (e.g. browsers) and only trigger the exploit once used with the vulnerable library.

POC

I’ve built a simple “malicious” U2F token on the basis of the Trezor One. The device detects the characteristic nonce value of the library (see above) and “attacks” with one packet full of AAAAA to demonstrate the issue. For hardened libu2f-host binaries with stack canary, this leads to a crash, as expected:

echo "dummy" | u2f-host -aauthenticate -o https://demo.yubico.com -d
USB send:
00ffffffff8600080807060504030201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
USB recv:
ffffffff860039414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141
*** buffer overflow detected ***: u2f-host terminated

Responsible disclosure

I responsibly disclosed the vulnerability to Yubico in December 2018 and received quick and competent feedback during the holiday season. They applied for CVE-2018-20340 and originally asked me for 90 days of disclosure embargo but managed to ship the release after about ~52 days.
A number of Linux distributions were given early warning to prepare security releases.

Overall, I am fairly satisfied with the disclosure process.

Relevant libu2f-host sources

variant source fix references
Yubico upstream GitHub 1.1.7 via 1, 2 YSA-2019-01, changelog
Debian package Debian 1.1.2-2+deb9u1 #921725, DSA-4389, bugtraq
Ubuntu package Ubuntu 1.1.4-1ubuntu0.1, 1.1.6-1ubuntu0.1 #1814153, CVE tracker
Arch package Arch 1.1.7-1 ASA-201902-7
SUSE / openSUSE   see references CVE tracker, #1124781, SUSE-SU-2019:1340-1
Gentoo Gentoo 1.1.10 678580
Fedora Fedora 1.1.8-1 1685955
Mac homebrew Homebrew see source -

Detailed timeline

Date info
2018-12-17 First email to Yubico, PGP key requested
2018-12-19 Technical disclosure to Yubico
2018-12-20 Yubico confirms the issue
2018-12-21 Yubico requests a CVE from MITRE
2019-02-08 12:00 CET Public disclosure
2019-02-11 Linux distributions such as Debian and Arch publish security advisories
2019-02-18 OpenSuse publishes security advisory
2019-04-09 Fedora releases a patch for Fedora30
2019-05-19 Fedora releases a patch for Fedora29
2019-05-24 SUSE publishes a security advisory
2019-06-05 Gentoo switches to a patched version

Bug bounty

Yubico provided a number of their hardware products as a bug bounty for this issue.