July 20, 2021

Analysis of an RFID-based TOTP Hardware Token

Some month ago I started to look into some RFID-based TOTP hardware tokens. Out of curiosity I bought some and started to reverse engineer them. This was just meant to be a learning experience. My colleague, Matthias Deeg, got interested as well and bought another token. Together we learned a lot about those devices. This post tells the story about the research I have done on the Token2 OTPC-P2. Links to Matthias’s research on the Protectimus SLIM can be found at the end of this post.

Introduction

Time-based one-time passwords (TOTP) have been around for several years now and became more and more widespread as authentication factor in multi-factor authentication (MFA) methods. Protecting user accounts via two-factor authentication (2FA) using a static password and a TOTP is considered a good idea from a security standpoint and a best practice that can prevent different kinds of attacks.

For generating a one-time password, the algorithms described in RFC 4226 titled HOTP: An HMAC-Based One-Time Password Algorithm and RFC 6238 titled TOTP: Time-Based One-Time Password Algorithm are relevant.

A very popular TOTP generator is the mobile app Google Authenticator. And for implementing TOTPs in software products, a variety of software libraries is available for different programming languages.

The following TOTP example in Python uses the library PyOTP and illustrates all required configuration parameters for a TOTP:

  1. a cryptographic hash function (digest function)
  2. a shared secret key (seed value)
  3. the length of the generated TOTP
  4. the used time interval in seconds (time step between TOTPs)
#!/usr/bin/env python3
import hashlib
import pyotp
import time

# secret key (seed)
SECRET_KEY = "base32topsecret7"

# initialize the TOTP generator with a specific configuration
totp = pyotp.TOTP(SECRET_KEY, digest=hashlib.sha256, digits=6, interval=30)

# generate three TOTPs with a time interval of 30 seconds
for i in range(3):
    password = totp.now()
    print(password)
    time.sleep(30)

The following output exemplarily shows an output of this example with three sequently generated OTPs with a time interval of 30 seconds.

$ python totp_test.py
640660
808281
945719

Besides software-based TOTP generators like the mobile app Google Authenticator, there are also different kinds of hardware TOTP tokens.

Token2 OTPC-P2

The NFC-based card OTPC-P2 is distributed by Token2, a Swiss company based in Geneva. It is designed as a token for TOTP-based two-factor authentication. The form factor is identical to a typical credit card. The features of the token are specified by the distributor Token2 as shown in the following screenshot of the corresponding product website.

Token2 OTPC-P2 product description Token2 OTPC-P2 product description

There are two key differences when compared to the token of Protectimus:

  1. The card will wipe the configured secret seed when the time (or other configuration parameters) is updated.
  2. The PIN displayed on the e-Ink display cannot be read out via the NFC interface.

The following sections summarize the results of our case study concerning this NFC-based TOTP token. They contain information about the general operation of the token, the journey of reverse engineering some of its functionality, and identified security issues and interesting questions, which could not be answered yet.

Normal operation

The two-factor token arrives in an unconfigured state. To configure a card, two software solutions are provided by Token2: A Windows application and an Android app.

Using the provided software, the token can be configured via NFC. When the token is presented to e.g. an Android phone running the NFC Burner 2 app, the serial number and the current time from the card is read out and displayed. If needed, the first step is to edit the general configuration, as the illustrated in the following Figure.

Configuration view of the NFC burner 2 Android app

The second step is to burn the secret seed for the used TOTP HMAC algorithm onto the card, as shown in the following Figure.

Seed burning view of the NFC burner 2 Android app

Once the configuration and a seed value are set, the token can be used for TOTP-based two-factor authentication. To get a valid PIN, the user must press the button in the lower right corner of the card. The PIN is then shown on the e-Ink display of the tag. In contrast to the Protectimus card, the PIN cannot be read via the NFC interface. This interface seems to be only for configuration purposes.

NFC interface

On the NFC interface, the token can be identified as a typical ISO14443-A tag. Additional information imply, that the tag is a Java Card, which means, that ISO 7816-4 Application Protocol Data Units (APDUs) are used to exchange data.

Example: APDU to query the time and serial number of the card

The following Table illustrates the structure of the APDU command.

Field name Length (bytes) Descripton
CLA 1 Instruction class - indicates the type of command, e.g. interindustry or proprietary
INS 1 Instruction code - indicates the specific command, e.g. write data
P1-P2 2 Instruction parameters for the command, e.g. offset into file at which to write the data
Lc 0, 1 or 3 Encodes the number (Nc) of bytes of command data to follow
- 0 bytes denotes Nc=0
- 1 byte with a value from 1 to 255 denotes Nc with the same length
- 3 bytes, the first of which must be 0, denotes Nc in the range 1 to 65535 (all three bytes may not be zero)
Data Nc Nc bytes of data
Le 0, 1, 2 or 3 Encodes the maximum number (Ne) of response bytes expected
- 0 bytes denotes Ne=0
- 1 byte in the range 1 to 255 denotes that value of Ne, or 0 denotes Ne=256
- 2 bytes (if extended Lc was present in the command) in the range 1 to 65 535 denotes Ne of that value, or two zero bytes denotes 65 536
- 3 bytes (if Lc was not present in the command), the first of which must be 0, denote Ne in the same way as two-byte Le

The next Table shows the structure of the APDU response.

Field name Length (bytes) Descripton
Response Data Nr (at most Ne) Response data
SW1-SW2 2 Command processing status, e.g. 0x9000 indicates sucess

Byte 0 of a double sized UID (cascade level 2, 7 bytes) should always contain a manufacturer code. In the case of the Token2 OTPC-P2 token, this byte is set to 0x1D, which is the code for Shanghai Fudan Microelectronics Group Company Ltd. from China. The UID is not random and can therefore be used for tracking.

The following output illustrates the tag detection on the Proxmark3.

[usb] pm3 --> hf search
    Searching for ISO14443-A tag...
[+]  UID: 1D 01 A8 01 58 34 78
[+] ATQA: 00 44
[+]  SAK: 20 [1]
[+] MANUFACTURER: Shanghai Fudan Microelectronics Co. Ltd. P.R. China
[+]    JCOP 31/41
[=] -------------------------- ATS --------------------------
[+] ATS: 05 72 F7 A6 02 [ 9d 00 ]
[=]      05...............  TL    length is 5 bytes
[=]         72............  T0    TA1 is present, TB1 is present, TC1 is present, FSCI is 2 (FSC = 32)
[=]            F7.........  TA1   different divisors are NOT supported, DR: [2, 4, 8], DS: [2, 4, 8]
[=]               A6......  TB1   SFGI = 6 (SFGT = 262144/fc), FWI = 10 (FWT = 4194304/fc)
[=]                  02...  TC1   NAD is NOT supported, CID is supported
[#] Auth error


[+] Valid ISO14443-A tag found

The NFC interface of the token has two different states:

  1. Restricted: If the button on the tag has not been pressed, the card can still be discovered by tools like the Proxmark3. Android devices do not discover the card in that state, because sending and receiving APDUs fails.
  2. Activated: If the button is pressed, the tag gets activated. It will show a PIN on the e-Ink display and it will respond to APDUs. If there is no NFC traffic, the card will go back to sleep after a short timeout. However, if there is traffic and a field of an NFC reader, the card will stay active.

To better understand how e.g. the NFC Burner 2 Android app by Token2 communicates with the card, the initial communication was sniffed using a Proxmark3. The sniffed data show three stages that are typical when sniffing communication between a tag and an Android device.

  • Tag detection part 1: Android searches for tags in the reader’s field and performs the ISO 14443-3 initialization, anti-collision, and selects one tag.
  • Tag detection part 2: Android searches for more information on the tag. For example, it tries to find out if the application identifier (AID) 0xD2760000850101 – the identifier for the NDEF application on MIFARE DESFire tags – is presently using APDU commands.
  • Communication by the app: The actual communication between the tag and the app that is handling the tag (in this case NFC Burner 2 by Token2) takes place.

Sniffing the tag enumeration and the get serial number and time command with a Proxmark3 is demonstrated in the following output with the three stages annotated.

[usb] pm3 --> hf 14a sniff

[#] Starting to sniff. Press PM3 Button to stop.
[#] trace len = 1499


[usb] pm3 --> hf list -t 14a
[=] downloading tracelog data from device
[+] Recorded activity (trace len = 1499 bytes)
[=] start = start of start frame end = end of frame. src = source of transfer
[=] ISO14443A - all times are in carrier periods (1/13.56MHz)

   Start |      End | Src |  Daa ( deote prit eror)                               | CRC | Annotation
---------+----------+-----+ ------------------------------------------------------+-----+--------------------

[ Stage 1: Tag detection part 1 ]

       0 |     1056 | Rdr | 26(7)                                                 |     | REQA
    2260 |     4628 | Tag | 44 00                                                 |     |
   12688 |    17456 | Rdr | 50 00 57 cd                                           |  ok | HALT
   43392 |    44384 | Rdr | 52(7)                                                 |     | WUPA
   45652 |    48020 | Tag | 44 00                                                 |     |
   56480 |    58944 | Rdr | 93 20                                                 |     | ANTICOLL
   60148 |    65972 | Tag | 88 1d 01 a8 3c                                        |     |
   83952 |    94416 | Rdr | 93 70 88 1d 01 a8 3c cb 81                            |  ok | SELECT_UID
   95684 |    99204 | Tag | 04 da 17                                              |     |
  106912 |   109376 | Rdr | 95 20                                                 |     | ANTICOLL-2
  110580 |   116468 | Tag | 01 58 34 78 15                                        |     |
  124240 |   134768 | Rdr | 95 70 01 58 34 78 15 3c 26                            |  ok | SELECT_UID-2
  135972 |   139556 | Tag | 20 fc 70                                              |     |
  151984 |   156752 | Rdr | e0 80 31 73                                           |  ok | RATS
  157956 |   166148 | Tag | 05 72 f7 a6 02 3d 9d                                  |  ok |
  466736 |   470288 | Rdr | c2 e0 b4                                              |  ok | RESTORE(224)
  471556 |   475076 | Tag | c2 e0 b4                                              |     |
 2911456 |  2912512 | Rdr | 26(7)                                                 |     | REQA
 2913700 |  2916068 | Tag | 44 00                                                 |     |
 2934304 |  2939072 | Rdr | 50 00 57 cd                                           |  ok | HALT
 2963568 |  2964560 | Rdr | 52(7)                                                 |     | WUPA
 2965828 |  2968196 | Tag | 44 00                                                 |     |
 2976144 |  2978608 | Rdr | 93 20                                                 |     | ANTICOLL
 2979796 |  2985620 | Tag | 88 1d 01 a8 3c                                        |     |
 2993840 |  3004304 | Rdr | 93 70 88 1d 01 a8 3c cb 81                            |  ok | SELECT_UID
 3005572 |  3009092 | Tag | 04 da 17                                              |     |
 3017408 |  3019872 | Rdr | 95 20                                                 |     | ANTICOLL-2
 3021076 |  3026964 | Tag | 01 58 34 78 15                                        |     |
 3033936 |  3044464 | Rdr | 95 70 01 58 34 78 15 3c 26                            |  ok | SELECT_UID-2
 3045668 |  3049252 | Tag | 20 fc 70                                              |     |
 3060112 |  3064880 | Rdr | e0 80 31 73                                           |  ok | RATS
 3066084 |  3074276 | Tag | 05 72 f7 a6 02 3d 9d                                  |  ok |
 3366896 |  3370448 | Rdr | c2 e0 b4                                              |  ok | RESTORE(224)
 3371716 |  3375236 | Tag | c2 e0 b4                                              |     |

[ Stage 2: Tag detection part 2 ]

 3444720 |  3445712 | Rdr | 52(7)                                                 |     | WUPA
 3446980 |  3449348 | Tag | 44 00                                                 |     |
 3456816 |  3467280 | Rdr | 93 70 88 1d 01 a8 3c cb 81                            |  ok | SELECT_UID
 3468532 |  3472052 | Tag | 04 da 17                                              |     |
 3479760 |  3490288 | Rdr | 95 70 01 58 34 78 15 3c 26                            |  ok | SELECT_UID-2
 3491492 |  3495076 | Tag | 20 fc 70                                              |     |
 3506944 |  3511712 | Rdr | e0 80 31 73                                           |  ok | RATS
 3512916 |  3521108 | Tag | 05 72 f7 a6 02 3d 9d                                  |  ok |
 3814080 |  3832608 | Rdr | 02 00 a4 04 00 07 d2 76 00 00 85 01 01 00 35 c0       |  ok |
 4232468 |  4237204 | Tag | f2 07 a7 25                                           |     |
 4245824 |  4250592 | Rdr | f2 07 a7 25                                           |  ok |
 4448276 |  4454164 | Tag | 02 6a 82 93 2f                                        |     |
 4502624 |  4520000 | Rdr | 03 00 a4 04 00 07 d2 76 00 00 85 01 00 82 1d          |  ok |
 4835764 |  4840500 | Tag | f2 07 a7 25                                           |     |
 4848384 |  4853152 | Rdr | f2 07 a7 25                                           |  ok |
 5051604 |  5057492 | Tag | 03 6a 82 4f 75                                        |     |
 5105376 |  5108928 | Rdr | c2 e0 b4                                              |  ok | RESTORE(224)
 5110196 |  5113716 | Tag | c2 e0 b4                                              |     |
 5189904 |  5190896 | Rdr | 52(7)                                                 |     | WUPA
 5192164 |  5194532 | Tag | 44 00                                                 |     |
 5201888 |  5212352 | Rdr | 93 70 88 1d 01 a8 3c cb 81                            |  ok | SELECT_UID
 5213620 |  5217140 | Tag | 04 da 17                                              |     |
 5224720 |  5235248 | Rdr | 95 70 01 58 34 78 15 3c 26                            |  ok | SELECT_UID-2
 5236452 |  5240036 | Tag | 20 fc 70                                              |     |
 5306128 |  5309680 | Rdr | c2 e0 b4                                              |  ok | RESTORE(224)
 5435184 |  5436176 | Rdr | 52(7)                                                 |     | WUPA
 5437444 |  5439812 | Tag | 44 00                                                 |     |
 5448144 |  5458608 | Rdr | 93 70 88 1d 01 a8 3c cb 81                            |  ok | SELECT_UID
 5459876 |  5463396 | Tag | 04 da 17                                              |     |
 5470720 |  5481248 | Rdr | 95 70 01 58 34 78 15 3c 26                            |  ok | SELECT_UID-2
 5482452 |  5486036 | Tag | 20 fc 70                                              |     |
 5496960 |  5501728 | Rdr | e0 80 31 73                                           |  ok | RATS
 5502932 |  5511124 | Tag | 05 72 f7 a6 02 3d 9d                                  |  ok |
 5811744 |  5830272 | Rdr | 02 00 a4 04 00 07 d2 76 00 00 85 01 01 00 35 c0       |  ok |
 6239348 |  6244084 | Tag | f2 07 a7 25                                           |     |
 6252464 |  6257232 | Rdr | f2 07 a7 25                                           |  ok |
 6455156 |  6461044 | Tag | 02 6a 82 93 2f                                        |     |
 6498752 |  6516128 | Rdr | 03 00 a4 04 00 07 d2 76 00 00 85 01 00 82 1d          |  ok |
 6842644 |  6847380 | Tag | f2 07 a7 25                                           |     |
 6855136 |  6859904 | Rdr | f2 07 a7 25                                           |  ok |
 7058612 |  7064500 | Tag | 03 6a 82 4f 75                                        |     |
 7118976 |  7122528 | Rdr | c2 e0 b4                                              |  ok | RESTORE(224)
 7123796 |  7127316 | Tag | c2 e0 b4                                              |     |

[ Stage 3: Actual communication between the tag and app ]

 7199856 |  7200848 | Rdr | 52(7)                                                 |     | WUPA
 7202100 |  7204468 | Tag | 44 00                                                 |     |
 7213040 |  7223504 | Rdr | 93 70 88 1d 01 a8 3c cb 81                            |  ok | SELECT_UID
 7224772 |  7228292 | Tag | 04 da 17                                              |     |
 7235744 |  7246272 | Rdr | 95 70 01 58 34 78 15 3c 26                            |  ok | SELECT_UID-2
 7247460 |  7251044 | Tag | 20 fc 70                                              |     |
 7262304 |  7267072 | Rdr | e0 80 31 73                                           |  ok | RATS
 7268276 |  7276468 | Tag | 05 72 f7 a6 02 3d 9d                                  |  ok |
 8474336 |  8490624 | Rdr | 02 00 a4 04 00 06 b0 00 00 00 00 23 a5 20             |  ok |
 8796084 |  8800820 | Tag | f2 07 a7 25                                           |     |
 8809072 |  8813840 | Rdr | f2 07 a7 25                                           |  ok |
 9019588 |  9025412 | Tag | 02 90 00 f1 09                                        |     |
 9080304 |  9091920 | Rdr | 03 80 41 00 00 02 02 11 0d d8                         |  ok |
 9343428 |  9348164 | Tag | f2 07 a7 25                                           |     |
 9356544 |  9361312 | Rdr | f2 07 a7 25                                           |  ok |
 9818308 |  9850692 | Tag | 03 95 15 02 0d 38 36 35 39 36 32 31 34 36 35 33 38 31 |     |
         |          |     | 11 04 60 47 88 e8 90 00 61 a4                         |  ok |
11593328 | 11596944 | Rdr | b2 67 c7                                              |  ok |
11770948 | 11774468 | Tag | a3 6f c6                                              |     |

The output shows that the Android app sends a get serial number and time command to the token. Part of the response, e.g. 0x38363539363231343635333831, is encoded as ASCII and can easily be decoded to 8659621465381 – the serial number of the token. The time is encoded as four byte UNIX timestamp in big endian.

Sniffing the communication between tag and app was often used in this analysis as a method of reverse engineering. This was combined with analyzing the decompiled or disassembled code from the Android app, the Windows tool, or one of their libraries.

Internal card layout

The process of delayering an RFID card can unveil interesting information about the used chip, antenna design, or other important internals. This is typically achieved by putting the card into acetone. In most cases, acetone will weaken or dissolve some of the cards plastic parts and used adhesives by leaving the chips and antenna unharmed.

Card is only slightly damaged/dissolved by acetone (image by Philippe Teuwen)

Unfortunately, the Token2 OTPC-P2 card was pretty resistant to acetone. However, Philippe Teuwen, a very well known security researcher, hacker, and RFID specialist, was kind enough to help out and take things further.

By removing the plastics with sandpaper and a blade, Philippe Teuwen was able to delayer the whole card and get a closer look at its components.

Delayered card (image by Philippe Teuwen) Delayered card (image by Philippe Teuwen)

Delayered card (image by Philippe Teuwen) Delayered card (image by Philippe Teuwen)

There are several markings on the internal parts of the card, which provide more information about it.

  1. Big chip: T27D0 1951, most likely the main controller with the firmware
  2. Small chip: 04J0 1907, most likely a NFC controller
  3. Battery: CF052039 770401 FDK, a 3V lithium battery by FDK
  4. Markings on the PCB: T27-C04-T100L-V1.1 2019.06.10

The NFC controller on the card or the initial firmware is most likely built by Shanghai Fudan Microelectronics Group Co., Ltd. The first byte of a seven or ten byte UID indicates the manufacturer. In the case of Token2 OTPC-P2 card, this byte is 0x1D, which is associated with Fudan. The manufacturer is well known for RFID chips.

Authentication

An authentication is required to change the configuration of the token or to write a seed. The authentication procedure was visible when the communication between a tag and the Android app was sniffed with a Proxmark3. This observation could be confirmed by looking at the disassembled code of the Android app.

Decompiled authentication method of the NFC Burner 2 Android application Decompiled authentication method of the NFC Burner 2 Android application

By diving deeper into the code and the sniffed communication, the authentication process was reconstructed as follows:

  1. Request a challenge from the tag using the APDU 804B080000.
  2. Receive a challenge from the tag, e.g. F29E08B17821653E.
  3. Expand the challenge to a 16 byte value by padding zeros: F29E08B17821653E0000000000000000.
  4. Encrypt the challenge using the SM4 algorithm in ECB mode with a secret 16 byte long key.
  5. Send back the encrypted challenge using the APDU: 80CE00001073EA53B7E7E77DD81AEE5BC106-9E053A.
  6. The tags responds with 9000 indicating that the authentication was successful.

The whole part about encrypting the challenge with the SM4 cipher is not implemented in the Java- or Kotlin-based code of the Android app. The native library libesotpcommon.so is used for this. The Android app ships with different versions of this library, each for a specific platform. Fortunately, the 32 bit x86 library did still include the function names and debug symbols, as the following output illustrates.

> tree --charset=ascii lib
lib
|-- arm64-v8a
|   `-- libesotpcommon.so
|-- armeabi
|   `-- libesotpcommon.so
|-- armeabi-v7a
|   `-- libesotpcommon.so
|-- x86
|   `-- libesotpcommon.so
`-- x86_64
    `-- libesotpcommon.so

> file lib/x86/libesotpcommon.so
lib/x86/libesotpcommon.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=e35a887407b6adc3ba0e05298c006b1a9572e1c8, with debug_info, not stripped

However, the most interesting ingredient of the authentication is the key that is used to encrypt the challenge. It turned out that this key is hard-coded into the app, as the following Figure illustrates.

Decompiled class of the NFC Burner 2 Android application with static constants, e.g. the key for encrypting the challenge (DEFAULT_CUSTOMER_KEY) Decompiled class of the NFC Burner 2 Android application with static constants, e.g. the key for encrypting the challenge (DEFAULT_CUSTOMER_KEY)

Although the name DEFAULT_CUSTOMER_KEY implies that the key might be changeable, no such functionality was found.

We developed a small Python script which allows to perform an authentication using a cheap USB RFID reader like the ACR 122u.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2021 Gerhard Klostermeier (@iiiikarus)
#
# Get the challenge, calculate the response and send it back, performing a full authentication.
# Usage: ./do-authentication.py [key]
# Some info on sending APDUs with nfcpy:
#   https://nfcpy.readthedocs.io/en/latest/modules/tag.html#nfc.tag.tt4.Type4Tag.send_apdu


from sm4 import SM4Key
from nfc.clf import ContactlessFrontend
from nfc.tag.tt4 import Type4TagCommandError
from binascii import hexlify, unhexlify


def main(args):
  # Connect to reader.
  clf = ContactlessFrontend("usb")
  tag = clf.connect(rdwr={'on-connect': lambda tag: False})

  # Get challenge.
  print("[*] Requesting challenge from tag")
  cla = 0x80
  ins = 0x4b
  p1  = 0x08
  p2  = 0x00
  data = unhexlify("00")
  try:
    challenge = tag.send_apdu(cla, ins, p1, p2, data, check_status=True)
  except Type4TagCommandError as ex:
     print(f"[-] Error: No response from tag")
     return 1
  challenge = bytes(challenge)
  print(f"[+] Got challenge {hexlify(challenge).upper()}")

  # Challenge.
  print("[*] Inflating challenge")
  challenge = challenge + b'\x00' * 8
  print(f"[*] Challenge is now: {hexlify(challenge).upper()}")

  # Key.
  key = "8AD206883CA369482AB27182B6E83224"
  if (len(args) > 1):
    key = args[1]
  key_raw = unhexlify(key)
  key_sm4 = SM4Key(key_raw)
  print(f"[*] Using key: {hexlify(key_raw).upper()}")

  # Encrypt challenge (SM4).
  print("[*] Encrypting challenge with key using SM4")
  response_raw = key_sm4.encrypt(challenge)
  print(f"[+] Response is: {hexlify(response_raw).upper()}")

  # Send response.
  print("[*] Sending response to tag")
  cla = 0x80
  ins = 0xce
  p1  = 0x00
  p2  = 0x00
  data = response_raw
  auth = tag.send_apdu(cla, ins, p1, p2, data, check_status=False)
  auth = bytes(auth)
  print(f"[*] Got authentication response: {hexlify(auth).upper()}")
  if auth == b'\x90\x00':
    print(f"[+] Authentication was successfull!")
  else:
    print(f"[-] Authentication was not successfull!")

  clf.close()
  return 0


if __name__ == '__main__':
  import sys
  sys.exit(main(sys.argv))

By evaluating the authentication it became clear, that for each wrong authentication a counter is decreased. After five failed authentication attempts, the authentication method is blocked and it is not longer possible to execute any command that requires authentication.

Another interesting observation was made about the random number generator of the card. Details can be found in section Bad RNG.

Bad RNG

The OTPC-P2 token has an internal random number generator (RNG). For most security related applications, a random number generator must generate true random numbers and not pseudorandom numbers.

One use case where the RNG of the card is used is when an NFC reader asks the token for a challenge in order to authenticate (see Section Authentication). To evaluate the quality of the RNG, a simple Python script was developed which asks the token for a large number of challenges.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2020 Gerhard Klostermeier (@iiiikarus)
#
# Get challenges to verify the RNG of the tag.
# Usage: ./collect-challenges.py [challenge count] [retry count]
# Some info on sending APDUs with nfcpy:
#   https://nfcpy.readthedocs.io/en/latest/modules/tag.html#nfc.tag.tt4.Type4Tag.send_apdu


from sm4 import SM4Key
from nfc.clf import ContactlessFrontend, TransmissionError, TimeoutError
from nfc.tag.tt4 import Type4TagCommandError
from binascii import hexlify, unhexlify


def main(args):
  # Default values.
  challenge_count = 10
  max_retry = 3

  # Connect to reader.
  clf = ContactlessFrontend("usb")
  tag = clf.connect(rdwr={'on-connect': lambda tag: False})

  # Get challenge count.
  if (len(args) > 1):
    challenge_count = int(args[1])

  # Get retry count.
  if (len(args) > 2):
    max_retry = int(args[2])

  # Get challenge.
  print("[*] Requesting challenges from tag")
  cla = 0x80
  ins = 0x4b
  p1  = 0x08
  p2  = 0x00
  data = unhexlify("00")
  retry_counter = 0
  for challenge_nr in range(0, challenge_count):
    try:
      challenge = tag.send_apdu(cla, ins, p1, p2, data, check_status=True)
    except (Type4TagCommandError, TransmissionError, TimeoutError) as ex:
      print(f"[-] Error: {ex}. Reconnecting...")
      retry_counter +=1
      tag = clf.connect(rdwr={'on-connect': lambda tag: False})
      if retry_counter >= max_retry:
        print("[-] Too many errors. Abort!")
        return 1
      continue
    challenge = bytes(challenge)
    print(f"[+] Challenge {challenge_nr}: {hexlify(challenge).upper()}")

  clf.close()
  return 0


if __name__ == '__main__':
  import sys
  sys.exit(main(sys.argv))

Over eight megabytes of random data was collected this way. The data was collected in chunks of eight bytes, since a challenge from the token consists of eight bytes. A histogram visualization (that shows which byte is present how many times) quickly revealed that there are issues with the used RNG, as the following Figure illustrates.

Histogram of the random data collected by requesting challenges from the token Histogram of the random data collected by requesting challenges from the token

Upon further inspection it became clear, that it is the first byte of the eight byte challenge, which introduces the bad randomness. This is clearly visible when the histogram of only byte number 1 is compared with the histogram of bytes number 2 to 8, as the following Figures illustrate.

Histogram of byte number 1 of all collected challenges Histogram of byte number 1 of all collected challenges

Histogram of byte number 2 to byte number 8 of all collected challenges Histogram of byte number 2 to byte number 8 of all collected challenges

Inspecting the first byte of each challenge at bit level, the RNG error was narrowed down to two bits (bit number 5 and bit number 7) which do not have a 50:50 chance of being 1 or 0:

  1. bit 1: 0:50 %, 1:50 %
  2. bit 2: 0:50 %, 1:50 %
  3. bit 3: 0:50 %, 1:50 %
  4. bit 4: 0:50 %, 1:50 %
  5. bit 5: 0:66 %, 1:33 %
  6. bit 6: 0:50 %, 1:50 %
  7. bit 7: 0:66 %, 1:33 %
  8. bit 8: 0:50 %, 1:50 %

This reduces the 256 bit of entropy of a eight byte challenge to 254 bit of good entropy. This is likely still enough entropy for the authentication to be sufficiently secure. It is unclear, if the bad RNG can be exploited in other attack scenarios.

Known and unknown commands

A Java Card like the OTPC-P2 by Token2 can have lots of different APDUs. Although there is only one byte in an APDU that is declaring the instruction (INS), there could be many different contexts and parameters for one command. Even more commands or custom protocols can be designed within the data/payload field of an APDU.

It is close to impossible to enumerate all commands/payloads of an ISO 7816-4 tag. There is virtually an endless number of possibilities and the protocol or the RFID tags are not fast enough. Furthermore, ISO 7816 tags can have more than one application, with each application having its own APDUs and data format.

The OTPC-P2 seem to only use one application: 0xB00000000023. This was reconstructed from sniffing the communication between OTPC-P2 tags and the NFC Burner 2 Android app with a Proxmark3. Similar to enumerating all known APDUs/payloads, enumerating all applications on a Java Card is not feasible.

In an attempt to find applications that might have an application identifier (AID) close to the known application, a Python script was developed. This script just enumerates AIDs over a given range. However, no other valid AID was found.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2020 Gerhard Klostermeier (@iiiikarus)
#
# Search for files/AIDs using the select command.
# Usage: ./find-files.py [file id start] [file id stop] [retry count]
# Some info on sending APDUs with nfcpy:
#   https://nfcpy.readthedocs.io/en/latest/modules/tag.html#nfc.tag.tt4.Type4Tag.send_apdu


from nfc.clf import ContactlessFrontend, TransmissionError, TimeoutError
from nfc.tag.tt4 import Type4TagCommandError
from binascii import hexlify, unhexlify


def main(args):
  # Default values.
  file_id_start = 0xb00000000023
  file_id_stop =  0xffffffffffff
  max_reconnect = 50

  # Connect to reader.
  clf = ContactlessFrontend("usb")
  tag = clf.connect(rdwr={'on-connect': lambda tag: False})

  # Get file ID range.
  if (len(args) > 1):
    file_id_start = int(args[1])
  if (len(args) > 2):
    file_id_stop = int(args[2])

  # Get max. reconnect count.
  if (len(args) > 3):
    max_reconnect = int(args[3])

  # Test file/AIDs.
  print("[*] Testing for files/AIDs")
  cla = 0x00
  ins = 0xa4
  p1  = 0x04
  p2  = 0x00
  reconnect_counter = 0
  for file_nr in range(file_id_start, file_id_stop):
    data = file_nr.to_bytes((file_nr.bit_length() + 7) // 8, 'big')
    #print(f"[*] Sending data {hexlify(data).upper()}") # Print verbose.
    try:
      response = tag.send_apdu(cla, ins, p1, p2, data, check_status=True)
    except (Type4TagCommandError, TransmissionError, TimeoutError) as ex:
      #print(f"[-] Error: {ex}") # Print verbose.
      if type(ex) != Type4TagCommandError or str(ex) == "unrecoverable timeout error":
        print(f"[-] Error during command {hexlify(data).upper()}: {ex}. Reconnecting...")
        reconnect_counter +=1
        tag = clf.connect(rdwr={'on-connect': lambda tag: False})
        if reconnect_counter >= max_reconnect:
          print("[-] Too many errors. Abort!")
          return 1
      continue
    response = bytes(response)
    print(f"[+] Got response for data {hexlify(data).upper()}: {hexlify(response).upper()}")

  clf.close()
  return 0


if __name__ == '__main__':
  import sys
  sys.exit(main(sys.argv))

Although it is close to impossible to find all valid APDUs, the next step was to find at least some of them. A possibly complex tag like the OTPC-P2 is likely to have more commands than the known commands used by the customer software. To search for APDUs of the type case 1 or case 2 (short), the client software of the Proxmark3 was extended by the command hf 14a apdufind. The maintainers of the advanced Proxmark3 repository were kind enough to merge the changes.

[usb] pm3 --> hf 14a apdufind -h

Enumerate APDU's of ISO7816 protocol to find valid CLS/INS/P1/P2 commands.
It loops all 256 possible values for each byte.
The loop oder is INS -> P1/P2 (alternating) -> CLA.
Tag must be on antenna before running.

usage:
    hf 14a apdufind [-hlv] [-c <hex>] [-i <hex>] [--p1 <hex>] [--p2 <hex>] [-r <number>] [-e <number>] [-s <hex>]...


options:
    -h, --help                     This help
    -c, --cla <hex>                Start value of CLASS (1 hex byte)
    -i, --ins <hex>                Start value of INSTRUCTION (1 hex byte)
    --p1 <hex>                     Start value of P1 (1 hex byte)
    --p2 <hex>                     Start value of P2 (1 hex byte)
    -r, --reset <number>           Minimum seconds before resetting the tag (to prevent timeout issues). Default is 5 minutes
    -e, --error-limit <number>     Maximum times an status word other than 0x9000 or 0x6D00 is shown. Default is 512.
    -s, --skip-ins <hex>           Do not test an instructions (can be specified multiple times)
    -l, --with-le                  Search  for APDUs with Le=0 (case 2S) as well
    -v, --verbose                  Verbose output

examples/notes:
    hf 14a apdufind
    hf 14a apdufind --cla 80
    hf 14a apdufind --cla 80 --error-limit 20 --skip-ins a4 --skip-ins b0 --with-le

From sniffing the RFID traffic and from reverse engineering the client software for the OTPC-P2 token, the following commands (INS of APDU) were known:

  1. 0x41: Request data like the current time and serial number.
  2. 0xA4: Select an application or file. Only AID 0xB00000000023 seem to be used.
  3. 0x4B: Get a challenge for the authentication procedure.
  4. 0xCE: Authenticate. This must be done before configuration changes or burning a seed. The encrypted challenge must be sent for the authentication to be successful (see Section Authentication).
  5. 0xD4: Write/burn a configuration.
  6. 0xC5: Write/burn a seed.
  7. 0xC7: SealCard. This command was never used by the Android app and was recovered from the library SeedFlash.dll of the Windows application. It remains unsure, what exactly this command is used for.

With the apdufind command of the Proxmark3 some more commands/APDUs were detected. The unknown commands/APDUs are as follows:

  1. 0x808D000000: Unknown authentication mechanism. Using the authentication procedure and key from Section Authentication was not successful. It is limited to three attempts.
  2. 0x8040000000: Unknown command. The response is just the status word 0x9000, indicating success.

Some known commands were tested with different payloads. One of them, the get time and serial number command (0x41) showed an interesting behavior, where the data specify which values are queried. As the following figures show, 0x02 seems to be the current time and 0x11 the serial number. Other valid values were not found.

Original get time and serial number command

Modified data in get time and serial number command returning the serial number twice

Modified data in get time and serial number command returning the current time twice

[usb] pm3 --> hf 14a apdu -s -d 80 41 0000 02 0202
[+] ( select )
[+] >>> 80410000020202
[=] APDU: case=0x03 cla=0x80 ins=0x41 p1=0x00 p2=0x00 Lc=0x02(2) Le=0x00(0)
[+] <<< 951E020D38363539363231343635333831020D383635393632313436353338319000 | ....8659621465381..8659621465381..
[+] <<< status: 90 00 - Command successfully executed (OK).

It is likely that there are more unknown applications, instructions, or APDU structures and payloads. Within this research, it did not become clear what the unknown commands are used for or what they exactly do. The unknown authentication could be debug access or a backdoor for the reseller or manufacturer (Token2, Execlsecu).

Denial of service

A denial of service attack (DoS) aims to make a device or software unusable for its designed purpose. The observations of the authentication procedure (see Section Authentication) showed that a trivial attack vector for DoS is present: Everyone can change the card’s secret or configuration with the provided mobile application. This is because all cards use the same key for authentication. If someone overwrites the key or configuration, the token will no longer produce valid PINs, resulting in a denial of service state. Admittedly, this attack needs close physical proximity to an activated card. Furthermore, the card can be reinitialized with the correct data so that it will work again.

Another simple and partial DoS state is when the authentication failed five times in a row. After this, the card locks the authentication method, making changes to the card’s configuration impossible.

By accident, we were able to produce another DoS state for a card. After sending some random test APDUs to the token, it did not respond anymore. Even the display was no longer cleared. This state was permanent and the card was completely unusable afterwards – in other words bricked. However, reproducing this state on a second card was not successful. It could be that not only the sent data was responsible for entering this state, but also a sudden cut-off of the reader field at a specific point in time (tear-off attack).

e-Ink display issues

The Token2 OTPC-P2 card shows the current valid PIN on an e-Ink display after the button is pressed. Depending on the card’s configuration, the PIN is valid for either 30 or 60 seconds. The display can be configured to stay on for either 15, 30, 60 or 120 seconds.

To turn the i-Ink display off, the shown values must be reset. The card does this by coloring the full display to black and than back to white. After that, the PIN should no longer be visible. However, this is not the case. There seems to be a burn in effect on the display, that slightly shows the last PIN, even after the display was cleared (see the following Figures). This could be a security issue in case the card is configured to PINs being valid for 60 seconds and a display sleep time of e.g. 15 seconds. In that case, the valid PIN should only be visible for 15 seconds. However, an attacker might get a glimpse at the display shortly after and use the slightly visible and still valid PIN.

Activated OTPC-P2 token Activated OTPC-P2 token

Deactivated OTPC-P2 token with the last PIN still slightly visible Deactivated OTPC-P2 token with the last PIN still slightly visible

The Protectimus card is not affected by this issue. This might be because the token uses another e-Ink display or because it clears the display twice.

Instructions for destroying a card

On the backside of the Token2 OTPC-P2 card, there are instructions on how to cut the card in order to safely destroy it.

Instructions for destroying the card printed on the backside Instructions for destroying the card printed on the backside

However, when shining bright light through a card (see following Figure), it is visible that the horizontal cut will damage the Lithium battery. Destroying the battery will render the card unusable, because the real time clock (RTC) for the TOTP authentication would go out of sync. Cutting batteries, however, is considered dangerous. The manufacturer seems to know this, because there is also a warning label on the card saying Caution! Contains Lithium battery. The vertical cut does not destroy anything.

Internal components visible by shining bright light through the card (image by Philippe Teuwen) Internal components visible by shining bright light through the card (image by Philippe Teuwen)

It is unclear why the instructions are printed in this way. If the horizontal cut would be a bit higher, it would not destroy the battery. In that case, however, the battery would still be connected to the unharmed chips. An attacker could just reconnect the antenna and display and the card should work again with the previously stored secret still intact.

The most effective way would be to cut the chip containing the seed for the TOTP authentication. In this way, the battery would not be harmed and the secret seed would be very hard or impossible to recover.

Interesting data and unanswered questions

At the point of writing this case study, a lot of effort has gone into understanding how this card works. Although some interesting findings and security relevant observations were made, there are still a lot of open questions.

Some strange practices by the manufacturer or hints in applications raise even more questions.

The following list sums up some of the more interesting hints and open questions we had no time to further investigate yet.

  1. The Token2 OTPC-P2 uses the same \enquote{customer key} for authentication on all cards. Are there other customer keys for similar products? At least one other key was found in the Windows application distributed by Token2. Also, can the customer key be changed by a user?

  2. What else can the card do? There are hidden commands – one is another authentication – but for which purpose?

  3. How can the permanent denial of service sate (see Section Denial of service) be reproduced? What is causing it?

  4. The Windows application by Token2 has a test function to verify whether a card produces valid PINs after programming a seed. However, this test function just opens an OTP test website and sends the secret seed to it. This is considered a very bad practice, because the secret is therefore published to an untrusted website.

  5. Even if no button is pressed, the card responds to some commands. For example, the full ISO 14443 discovery process is supported (anti-collision, retrieving the UID, etc.), but the token does not respond to APDUs. Are there any other undocumented commands that might work at that stage?

  6. Is it possible to read out the current PIN via NFC? So far, neither the Android nor the Windows application have a function for that. Is there an undocumented function?

  7. Is there a way to change the time of the internal real-time clock (RTC) of the token without losing the configured secret seed? This has been possible in the previous generation of this token, maybe there is still some legacy code in the firmware.

Conclusion

TOTP hardware tokens are an interesting device class – especially when they support near-field communication which provides an easily accessible attack surface.

Unfortunately, TOTP tokens are usually a black box to the user and it is not evident from reading publicly available product specifications and documentation how they internally work and whether their operating mode is insecure.

During the research, we were able to find out some more details about the inner workings of TOTP hardware tokens. However, there are still many open questions concerning those devices and the class of TOTP hardware tokens in general which will hopefully be answered and publicly documented in the future.

Addendum

A blog post about Matthias and my reserach can be found at blog.syss.com.
A PDF version of this paper is available here On the Security of RFID-based TOTP Hardware Tokens.

© 2022 - Gerhard Klostermeier - Some rights reserved - Legal Notice